Privacy Filter β Korean
Korean fine-tune of OpenAI Privacy Filter for span-level PII detection. Adapted via LoRA on attention projections only β the base's sparse-MoE backbone (1.5B / 50M active params) is kept frozen.
Open Test Notebook β load the model and run all examples interactively.
Capabilities
| Category | Description | Example |
|---|---|---|
private_person |
Personal name (Korean / Western / handles) | κΉλ―Όμ, John Smith |
private_address |
Physical / postal address | μμΈνΉλ³μ κ°λ¨κ΅¬ ν ν€λλ‘ 123 |
private_phone |
Phone number | 010-1234-5678 |
private_email |
Email address | minsu@example.com |
private_date |
Birthday / personally-identifying date | 1985λ 3μ 12μΌ |
private_url |
Personal URL | github.com/minsu |
account_number |
Bank, card, RRN, passport, etc. | 110-234-567890 |
personal_handle |
Username / handle | @minsu_dev |
ip_address |
IP address | 192.168.1.5 |
Benchmark Results
Held-out KDPII Korean PII test set, span-level F1:
| label | base | fine-tuned | Ξ |
|---|---|---|---|
private_phone |
0.65 | 1.00 | +0.35 |
private_url |
0.21 | 1.00 | +0.79 |
private_email |
0.86 | 1.00 | +0.14 |
account_number |
0.31 | 0.98 | +0.67 |
private_date |
0.00 | 0.90 | +0.90 |
private_address |
0.00 | 0.78 | +0.78 |
private_person |
0.06 | 0.69 | +0.63 |
| Overall | β | β | +0.58 |
Quick Start
Install
β οΈ Requires
transformers5.x (currently dev / from source). Theopenai_privacy_filterarchitecture is not in any stable 4.x PyPI release. If youpip install transformersand load this model, you'll seeKeyError: 'openai_privacy_filter'.
pip install --upgrade "git+https://github.com/huggingface/transformers.git" peft torch safetensors accelerate
The --upgrade flag is critical β without it, pip install is silently
no-op when an older transformers is already present.
After installing, restart your Python runtime / kernel so the new transformers replaces any version pre-loaded into the process. Sanity-check:
python -c "from transformers.models.auto.configuration_auto import CONFIG_MAPPING_NAMES; assert 'openai_privacy_filter' in CONFIG_MAPPING_NAMES, 'openai_privacy_filter missing β re-install transformers from source and restart runtime'"
If you're using Colab, the test notebook handles this automatically (auto-restart).
Load Model
from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch
MODEL_ID = "FrameByFrame/privacy-filter-korean"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
model = AutoModelForTokenClassification.from_pretrained(
MODEL_ID, trust_remote_code=True, torch_dtype=torch.bfloat16
)
model.eval()
if torch.cuda.is_available():
model.cuda()
trust_remote_code=True is required because Privacy Filter ships a custom
OpenAIPrivacyFilterForTokenClassification class (gpt-oss-style sparse MoE).
Inference
The model emits per-token BIOES labels. The helper below decodes them into character-offset spans with simple constrained logic:
def extract_pii(text: str, max_length: int = 512):
enc = tokenizer(
text,
truncation=True,
max_length=max_length,
return_offsets_mapping=True,
return_tensors="pt",
)
offsets = enc.pop("offset_mapping")[0].tolist()
enc = {k: v.to(model.device) for k, v in enc.items()}
with torch.no_grad():
logits = model(**enc).logits
pred_ids = logits.argmax(-1)[0].tolist()
id2label = model.config.id2label
spans = []
active = None # (label, start, end)
for tok_idx, lid in enumerate(pred_ids):
label = id2label[int(lid)]
if label == "O":
if active is not None:
spans.append(active); active = None
continue
prefix, cat = label.split("-", 1)
c_start, c_end = offsets[tok_idx]
if prefix == "S":
if active is not None: spans.append(active); active = None
spans.append((cat, c_start, c_end))
elif prefix == "B":
if active is not None: spans.append(active)
active = (cat, c_start, c_end)
elif prefix in ("I", "E"):
if active and active[0] == cat:
active = (active[0], active[1], c_end)
else:
if active is not None: spans.append(active); active = None
if prefix == "E":
spans.append((cat, c_start, c_end))
if active is not None:
spans.append(active)
return [
{"label": cat, "start": s, "end": e, "text": text[s:e].strip()}
for cat, s, e in spans
if text[s:e].strip()
]
Test
Korean: name + phone + email
>>> extract_pii("κΉλ―Όμμ μ νλ²νΈλ 010-1234-5678μ΄κ³ μ΄λ©μΌμ minsu@example.comμ
λλ€.")
[
{"label": "private_person", "start": 0, "end": 3, "text": "κΉλ―Όμ"},
{"label": "private_phone", "start": 12, "end": 25, "text": "010-1234-5678"},
{"label": "private_email", "start": 33, "end": 50, "text": "minsu@example.com"},
]
Korean: address + name
>>> extract_pii("μμΈνΉλ³μ κ°λ¨κ΅¬ ν
ν€λλ‘ 123μ μ¬λ λ°μ§μμ¨μκ² μ°λ½μ£ΌμΈμ.")
[
{"label": "private_address", "start": 0, "end": 5, "text": "μμΈνΉλ³μ"},
{"label": "private_address", "start": 6, "end": 9, "text": "κ°λ¨κ΅¬"},
{"label": "private_address", "start": 10, "end": 17, "text": "ν
ν€λλ‘ 123"},
{"label": "private_person", "start": 22, "end": 25, "text": "λ°μ§μ"},
]
Note: the model follows KDPII's address convention where each toponym component is its own span. Most downstream redaction systems concatenate adjacent address spans.
Korean: form-style document
>>> extract_pii('''κ³ κ° μ 보
... μ΄λ¦: μ΄μμ§
... μλ
μμΌ: 1985λ
3μ 12μΌ
... μ£Όμ: λΆμ°κ΄μμ ν΄μ΄λꡬ μ°λ 1457
... μ°λ½μ²: 010-9876-5432''')
[
{"label": "private_person", ..., "text": "μ΄μμ§"},
{"label": "private_date", ..., "text": "1985λ
3μ 12μΌ"},
{"label": "private_address", ..., "text": "λΆμ°κ΄μμ"},
{"label": "private_address", ..., "text": "ν΄μ΄λꡬ"},
{"label": "private_address", ..., "text": "μ°λ 1457"},
{"label": "private_phone", ..., "text": "010-9876-5432"},
]
English: account + email
>>> extract_pii("Wire to acct 110-234-567890, contact minsu@example.com")
[
{"label": "account_number", "start": 13, "end": 26, "text": "110-234-567890"},
{"label": "private_email", "start": 36, "end": 53, "text": "minsu@example.com"},
]
Redaction
Wrap the spans into a redactor:
def redact(text: str, mask: str = "[REDACTED]") -> str:
spans = extract_pii(text)
spans.sort(key=lambda s: s["start"], reverse=True)
out = text
for s in spans:
out = out[: s["start"]] + f"[{s['label'].upper()}]" + out[s["end"]:]
return out
>>> redact("κΉλ―Όμλμ λ²νΈλ 010-1234-5678μ
λλ€.")
"[PRIVATE_PERSON]λμ λ²νΈλ [PRIVATE_PHONE]μ
λλ€."
Output Schema
Each detected entity is one dict:
| field | description |
|---|---|
label |
One of the 9 categories above |
start |
Character offset start (inclusive) |
end |
Character offset end (exclusive) |
text |
The matched substring |
Training Details
| Base model | openai/privacy-filter (sparse MoE, 1.5B total / 50M active params, 128 experts top-4) |
| Method | LoRA r=16, alpha=32, dropout=0.05 on attention projections (q/k/v/o_proj); classifier head fully trainable; everything else frozen |
| Trainable params | |
| Datasets | KDPII (Korean, ~53k records, deterministic 5/5/90 test/val/train), korean_rrn_synthetic (train only) |
| Optimizer | AdamW, lr=5e-4, cosine schedule, warmup 0.1 |
| Batch | 64 per device Γ 2 GPUs = 128 effective |
| Epochs | 10, early stopping on eval_span_f1 (patience 3) |
| Sequence length | 512 |
| Precision | bf16 mixed (saved as bf16 safetensors after merge_and_unload) |
| Hardware | 2Γ NVIDIA RTX A5000 (24 GB each) |
| Final eval span F1 | 0.848 (validation) |
For full reproduction details, see TRAINING.md.
Known Limitations
private_personresidual error is dominated by KDPII'sPS_NICKNAMEpolicy. ~40% of remaining person errors are online-handle-style strings (e.g.,νλΉμ€λ§₯μ¬νΉ,νΌν°μμ ) that KDPII labels asPS_NICKNAME β private_person. Downstream redaction is unaffected; classification systems may want to post-classify handles separately.- Foreign names (Western, Japanese, Arabic transliterations) detected at lower rates due to limited training exposure.
private_addressboundaries follow KDPII's split convention (each toponym component is a separate span). Production redactors typically concatenate adjacent address spans during post-processing.- Raw model output may have leading/trailing whitespace in span offsets;
the
extract_piihelper above strips them viatext.strip()on the slice.
License
Apache 2.0 (inherited from base OpenAI Privacy Filter).
Citation
If you use this model:
@misc{framebyframe-privacy-filter-korean-2026,
title = {Privacy Filter Korean: LoRA fine-tune of OpenAI Privacy Filter for Korean PII},
author = {FrameByFrame},
year = {2026},
url = {https://huggingface.co/FrameByFrame/privacy-filter-korean}
}
- Downloads last month
- 173
Model tree for FrameByFrame/privacy-filter-korean
Base model
openai/privacy-filter