시작은 NER : Named Entity Recognition과 관련없이 공부를 시작했는데 결과물이 관련된 결과가 나와서 처음부터 다시 공부했습니다. “생각보다 괜찮네?” 였는데 “이럼 쓸만한데?” 정도까지 변해가는 과정을 적었습니다. 사실 관심있으신 분이 별로 없으실 거 같아요. NER 자체에 관심있거나, 개인정보 시스템 개발자 정도…보시면 좋을거 같네요… CJK 문제라고 하는 것을 좀 해결할수 있지 않을까? 생각해보고 있습니다.
자연어 처리에 대해서는 한 7-8년 전부터 공부했던 것 같습니다. 처음 고민은 뉴스를 수집할 때 중복된 뉴스가 너무 많다는 문제 때문에 제목에서 유사성을 판단하기 위해서 주어와 동사를 추출해 같은 뉴스인지 다른 방향의 뉴스인지를 비교해 보려고 했습니다. 다만 이것도 기자들의 낚시성 제목에 걸려서 잘 안되더라고요.
그리고 5년 전에는 서버 사용자가 입력하는 명령어를 자연어 처리 기법을 이용해 이 사용자가 주로 어떤 작업을 하는지, 그리고 현재 가지고 있는 권한의 범위를 판단하기 위해서 트랜스포머(Transformer)와 BERT를 공부했던 기억이 있습니다.
최근 제 주변의 AI 전문가라고 하시는 분들은 자연어 문장을 LLM에 직접 전송하여 파악하는 방식을 사용하기도 합니다. 빠르게 대응해야 하는 비즈니스 환경 특성상 어쩔 수 없는 선택일 수 있으나, 단순 개체명 추출이나 비식별화 작업을 위해서 전체 문장을 LLM으로 전송하는 방식은 연산 비용과 대기 시간(Latency) 측면에서 다소 과도한 접근일 수 있습니다.

이번에 메타의 BART 모델을 다루면서 자연어 처리에 대한 세 번째 학습을 진행하게 되었고, 이 과정에서 여러 리서치를 하면서 OpenAI의 Privacy Filter 작동 방식을 접하게 되었습니다(사실 별로 상관은 없죠…). 이를 통해 전통적인 자연어 처리 기법인 개체명 인식(Named Entity Recognition, NER)을 다시 검토하게 되었으며, 생각보다 높은 정밀도와 경량화 효율을 보여주는 것을 확인했습니다. 본 글에서는 최근 공개된 OpenAI Privacy Filter의 기술적 메커니즘을 분석하고, 이를 한국어 실무 환경에 맞춰 일부 과중한 구조를 경량화하여 구현한 오픈소스 파이프라인 privyscope-ko의 설계 방식을 기술합니다.
목차
1. 개체명 인식(NER)의 기술적 발전: 전통적 모델부터 OpenAI Privacy Filter까지
개체명 인식(Named Entity Recognition, 이하 NER)은 비구조화된 텍스트(Unstructured Text) 내에서 인명, 기관명, 장소, 날짜, 개인식별정보(PII) 등 미리 정의된 범주의 단어나 구(Phrase)를 감지하고 분류하는 기술입니다.
과거의 NER 방식과 최근 등장한 OpenAI Privacy Filter의 구조적 차이는 자연어 처리 엔진의 패러다임 변화를 명확히 보여줍니다.
[전통적 NER (BiLSTM/BERT)]
- BIO 태깅 (3가지 상태)
- Independent Argmax 추론
- 단순 백본 미세조정
- PyTorch / GPU 의존적 추론
[OpenAI Privacy Filter (2025~)]
- BIOES 태깅 (5가지 상태로 경계 명확화)
- Learned Transition Biases + Constrained Viterbi
- Autoregressive pretraining → Bidirectional conversion
- High-throughput / Long-context (128k) 대응
BIO 대비 BIOES 태깅 스키마의 우수성
기존 NER 체계는 BIO(Beginning, Inside, Outside) 3가지 태그를 사용했습니다. 그러나 이 스키마는 단일 토큰으로 구성된 개체명이나 단어 경계가 모호한 띄어쓰기 미적용 문장에서 경계 오탐을 자주 유발합니다.
Privacy Filter와 최신 NER 파이프라인은 이를 개선하여 BIOES 태깅 체계를 채택합니다.
- B (Begin): 개체명이 시작하는 토큰
- I (Inside): 개체명 내부 토큰
- O (Outside): 개체명이 아닌 일반 토큰
- E (End): 개체명이 끝나는 토큰
- S (Single): 단일 토큰으로 완결되는 개체명

특히 한국어와 같이 조사나 어미가 붙는 교착어 체계에서는 단일 토큰 개체명(S-PER 등)과 개체명의 종결 위치(E-LOC 등)를 명확히 학습하는 BIOES 방식이 단어 경계 식별 정밀도를 극대화합니다.
Constrained Viterbi Decoding 메커니즘
독립적인 토큰별 분류(Independent Argmax)를 수행할 경우, O 태그 직후에 I-PER가 등장하는 등의 문법적 불연속(Fragmented Span)이 발생할 수 있습니다.

OpenAI Privacy Filter는 단일 포워드 패스(Single Forward Pass)로 출력된 Logit 레이어 위에 제약된 비터비 디코더(Constrained Viterbi Decoder) 및 전이 편향(Transition Biases)을 결합합니다. 이를 통해 문맥 전체의 전이 확률을 최적화하고, 문법적으로 불가능한 스판 전환을 차단하여 경계 안정성을 확보합니다.
2. OpenAI Privacy Filter 아키텍처 구조 및 특징
OpenAI가 공개한 Privacy Filter는 전통적 인코더 전용(Encoder-only) 모델의 한계를 극복하기 위해 독특한 사전 학습 및 변환 방식을 사용합니다.
[Autoregressive Pretrained Checkpoint (gpt-oss 기반)]
│
▼ (체크포인트 변환)
[Bidirectional Banded Attention Token Classifier (Band Size = 128)]
│
▼ (Post-training)
[Supervised Classification Loss (33 Logits per Token: 8 Labels × 4 Boundary Tags + O)]
│
▼ (Inference Time)
[Constrained Viterbi Sequence Decoding]
아키텍처 및 처리 메커니즘 분석
- 사전 학습 및 변환 구조:
gpt-oss아키텍처 기반의 생성형 디코더 모델을 자동생성(Autoregressive) 방식으로 사전 학습한 후, 이를 양방향 대역 어텐션(Bidirectional Banded Attention, Band Size 128) 토큰 분류기로 변환했습니다. - MoE (Mixture of Experts) 적용: 전체 1.5B 파라미터 중 토큰당 Top-4 루팅을 사용하는 128개의 스파스 전문가(Sparse MoE) 레이어를 구성하여, 실제 활성화 파라미터(Active Parameters)를 50M 수준으로 극단적으로 낮추었습니다.
- 128K Long-context 지원: Chunking 없이 장문의 문서를 한 번에 입력받아 높은 스루풋(Throughput)으로 PII를 추출합니다.
- 운영 점수(Operating Points) 제어: 정밀도(Precision)와 재현율(Recall) 간의 트레이드오프를 런타임 매개변수 조절만으로 간편하게 제어할 수 있도록 설계되었습니다.
3. 실무 경량화를 위한 기법 제어: OpenAI 방식 대비 privyscope-ko의 간소화 전략
OpenAI Privacy Filter의 아키텍처는 훌륭하지만, 일반적인 온프레미스 인프라 및 Edge 컴퓨팅 환경에서 1.5B 규모의 MoE 모델이나 128K 컨텍스트 레이어를 운용하는 것은 여전히 메모리 점유율과 서빙 비용 측면에서 부담이 될 수 있습니다.

따라서 한국어 개인정보 마스킹 파이프라인인 privyscope-ko를 구축할 때는 실무에서의 즉각적인 배포와 압도적인 실행 속도를 확보하기 위해 몇 가지 무거운 기법을 의도적으로 제거하고 경량화했습니다.
사실 알파벳으로 이루어진 데이터에 대해서 정확도가 높다고 나와있어서 한국어 기반 엔진으로 변경을 해봤습니다.
privyscope-ko에서 생략 및 경량화한 기법
- MoE 및 거대 언어 모델 백본 생략 (Simplified Transformer Encoder)
- OpenAI: 1.5B 파라미터 기반 MoE 트랜스포머
- privyscope-ko:
klue/roberta-base인코더 단일 모델 사용 (~110M 파라미터).
- 128K 초장문 컨텍스트 윈도우 축소 (Max Sequence Length = 256)
- OpenAI: 128,000 토큰 지원
- privyscope-ko: 실무 환경의 대다수 PII 입력 문장 단위 처리에 맞춰 최적 윈도우 크기를 256으로 설정. CPU 환경에서 메모리 풋프린트를 최소화했습니다.
- PyTorch 런타임 종속성 제거 (ONNX Runtime Only & INT8 Quantization)
- OpenAI: PyTorch 및 CUDA 기반 디바이스 추론 스택
- privyscope-ko: 런타임 시 PyTorch 의존성을 완전히 제거하고, INT8 양자화된 ONNX Runtime만을 활용. 약 105MB의 경량 바이너리로 독립 실행이 가능하도록 설계했습니다.
- 검증 함수 기반 2단계 하이브리드 파이프라인 (Two-stage Hybrid Architecture)
- 단순 딥러닝 추론에만 의존하지 않고, 정규식 엔진(Regex Filter)과 검증 로직(Checksum Validator)을 1단계에 배치하여 정형 PII(주민등록번호, 전화번호 등)를 선제적으로 완벽하게 필터링합니다.
[입력 텍스트]
│
├────► Stage 1: Regex Engine + Verification (주민번호 Checksum 등 정형 PII)
│ │
│ ▼
└────► Stage 2: INT8 ONNX NER (KLUE-RoBERTa + BIOES + Constrained Viterbi)
│
▼
[Union Span Merging & Redaction]
4. privyscope-ko 오픈소스 파이프라인 구현 및 모듈 분석
privyscope-ko는 한국어 특화 PII 엔티티(PER, PHONE, ID_NUM, EMAIL, LOC, BANK, DATE, SECRET)를 탐지하는 하이브리드 엔진입니다.
ONNX 추론 및 Constrained Viterbi 디코딩 핵심 코드
아래 코드는 PyTorch 런타임 없이 ONNX 모델과 제약된 비터비 디코딩 알고리즘을 결합하여 한국어 텍스트를 마스킹하는 핵심 구현부입니다.
Python
import onnxruntime as ort
import numpy as np
import re
class PrivyscopeKOEngine:
def __init__(self, onnx_model_path: str):
# PyTorch 없이 ONNX Runtime으로만 추론 세션 생성
self.session = ort.InferenceSession(onnx_model_path)
# BIOES 태그 세트 정의 (8종 엔티티 * 4 + O = 33 클래스)
self.labels = self._init_bioes_labels()
def _init_bioes_labels(self):
entities = ["PER", "PHONE", "ID_NUM", "EMAIL", "LOC", "BANK", "DATE", "SECRET"]
labels = ["O"]
for ent in entities:
for prefix in ["B-", "I-", "E-", "S-"]:
labels.append(f"{prefix}{ent}")
return labels
def _viterbi_decode(self, logits: np.ndarray) -> list:
"""
Constrained Viterbi Decoding: 문법적으로 불가능한 BIOES 전이 방지
"""
seq_len, num_classes = logits.shape
dp = np.full((seq_len, num_classes), -np.inf)
backpointer = np.zeros((seq_len, num_classes), dtype=int)
# Start 토큰 초기화 ('O' 또는 'B-', 'S-' 만 허용)
for idx, label in enumerate(self.labels):
if label == "O" or label.startswith("B-") or label.startswith("S-"):
dp[0][idx] = logits[0][idx]
# Dynamic Programming 전이
for t in range(1, seq_len):
for next_idx, next_label in enumerate(self.labels):
for prev_idx, prev_label in enumerate(self.labels):
# 전이 제약조건 검증 (예: O 다음 I 등장 불가)
if not self._is_valid_transition(prev_label, next_label):
continue
score = dp[t-1][prev_idx] + logits[t][next_idx]
if score > dp[t][next_idx]:
dp[t][next_idx] = score
backpointer[t][next_idx] = prev_idx
# 최적 경로 역추적
best_path = [np.argmax(dp[-1])]
for t in range(seq_len - 1, 0, -1):
best_path.append(backpointer[t][best_path[-1]])
best_path.reverse()
return [self.labels[i] for i in best_path]
def _is_valid_transition(self, prev: str, curr: str) -> bool:
if curr.startswith("I-") or curr.startswith("E-"):
# I, E 태그는 이전 태그가 동일한 엔티티의 B 또는 I 태그여야 함
prev_entity = prev.split("-")[-1] if "-" in prev else ""
curr_entity = curr.split("-")[-1]
return (prev.startswith("B-") or prev.startswith("I-")) and (prev_entity == curr_entity)
return True
def redact(self, input_ids: np.ndarray, original_text: str, offsets: list) -> str:
# 1. ONNX 모델 추론 수행
inputs = {self.session.get_inputs()[0].name: input_ids}
logits = self.session.run(None, inputs)[0][0] # Shape: [SeqLen, 33]
# 2. Viterbi 디코딩 적용
decoded_tags = self._viterbi_decode(logits)
# 3. 스판 추출 및 마스킹 오프셋 계산
spans = self._extract_spans(decoded_tags, offsets)
# 4. 문자열 마스킹 처리
masked_text = list(original_text)
for start, end, label in sorted(spans, key=lambda x: x[0], reverse=True):
masked_text[start:end] = f"<{label}>"
return "".join(masked_text)
5. 성능 검증 및 벤치마크 분석
privyscope-ko의 정량적 평가를 위해 독립된 한국어 PII 평가 데이터셋을 구성하고, 정밀도(Precision), 재현율(Recall), 엄격한 Strict F1-Score 평가를 진행했습니다.
엔티티별 Strict F1-Score 성능
| 엔티티 범주 (Entity) | Precision | Recall | Strict F1-Score |
| PER (인명) | 0.945 | 0.938 | 0.941 |
| PHONE (전화번호) | 0.997 | 0.996 | 0.996 |
| ID_NUM (주민번호 등) | 0.999 | 0.998 | 0.998 |
| LOC (주소/위치) | 0.912 | 0.898 | 0.905 |
| SECRET (APIKey/비밀번호) | 0.931 | 0.920 | 0.925 |
| Overall Average | 0.941 | 0.932 | 0.936 |
- 목표 지표인 Strict F1 ≥ 0.93 조건을 성공적으로 달성했습니다.
- 정규식과 Checksum 검증 로직이 결합된
ID_NUM및PHONE영역에서는 0.99 이상의 정밀도를 확보했습니다.
추론 연산 속도 및 하드웨어 자원 점유율 비교
[처리 속도 및 메모리 비교 (단일 문장 기준)]
OpenAI Privacy Filter (1.5B MoE)
[████████████████████] ~180ms / VRAM 4.2GB
PyTorch KLUE-RoBERTa
[████████] ~35ms / VRAM 1.8GB
privyscope-ko (INT8 ONNX Runtime)
[██] ~8.5ms / RAM 120MB (CPU 전용 실행 가능)
PyTorch 및 대형 디코더 기반 아키텍처를 제거하고 ONNX Runtime 인퍼런스 엔진으로 경량화함에 따라, CPU 단독 환경에서도 단일 문장당 8.5ms 이하의 높은 처리 성능을 달성했습니다.
Example)
$ pip install privyscope-ko
$ privyscope redact "안녕하세요. 지난 7월 20일에 OOO 제품을 주문한 홍길동입니다. (주문번호: 20260720-123456) 주문한 지 일주일이 넘었으나 배송이 시작되지 않아 주문 취소 및 환불을 요청합니다. 제 연락처는 010-1234-5678이며, 당초 배송지는 서울시 강남구 테헤란로 123, 101동 1004호였습니다. 환불금은 OO은행 123-4567-8901-23(예금주: 홍길동) 계좌로 입금해 주시면 됩니다. 출고 전이 라면 즉시 취소 처리 부탁드리며 처리 완료 시 문자로 안내해 주시기 바랍니다. 감사합니다."

6. 결론 및 향후 과제
OpenAI Privacy Filter가 보여준 BIOES 스키마와 Constrained Viterbi Decoding 도입은 개체명 인식 기술이 나아가야 할 방향성을 제시해 주었습니다.
실무 엔지니어링 관점에서는 이러한 최신 알고리즘 기법을 채용하면서도, 거대한 백본 모델과 초장문 레이어를 과감히 덜어내고 ONNX 양자화 + 정규식 2단계 하이브리드 파이프라인으로 경량화하는 접근법이 훨씬 뛰어난 비용 대비 성능을 가져옵니다.
privyscope-ko는 이러한 설계 사상을 기반으로 구축되었으며, 최신 NLP 메커니즘을 한국어 환경에 맞춰 가장 가볍고 빠르게 사용할 수 있도록 오픈소스로 공개되었습니다. 향후 언어팩 확장(Multilingual Support) 및 WASM 컴파일을 통해 웹 브라우저 로컬 단에서도 작동 가능한 비식별화 엔진으로 고도화할 계획입니다.