schift-ko-pii-v5

34M parameter Korean PII detector โ€” 3x smaller, beats the 111M v4.

Hybrid detection: the model handles person names and addresses, the SDK postprocessor handles structured patterns (phone numbers, ID numbers, dates, URLs). 136 MB, CPU-only.

Benchmark (benchmark_v2, 253 cases)

Compared against every public Korean PII model on HuggingFace (as of Aug 2026).

Model Params F1 P R
OpenMed-PII-Korean-NomicMed-Large 395M 0.041 0.029 0.071
OpenMed-PII-Korean-QwenMed-XLarge 600M 0.084 0.061 0.138
LFM2.5-Encoder-350M-PII 350M 0.254 0.207 0.328
FrameByFrame/korean-pii-e5-base ~110M 0.435 0.319 0.684
seungkukim/korean-pii-masking-v2 ~110M 0.553 0.672 0.470
vmaca123/korean-pii-ner-v3 ~125M 0.569 0.810 0.439
schift-ko-pii-v4 111M 0.702 0.602 0.842
schift-ko-pii-v5 34M 0.823 0.812 0.834

All models evaluated raw (no postprocessing) except v4/v5 which use the SDK postprocessor (regex for structured patterns). v5 with postprocess disabled scores F1=0.578 (P=0.847, R=0.439) โ€” still competitive, and the hybrid approach pushes it to #1.

Not included: bbanany/qwen25-3b-korean-pii-* (3B CausalLM, binary PII/NOT_PII classifier on pre-extracted spans โ€” different task, not NER), upgle/bert-pii-korean and alphagyuu/* (no model weights uploaded), psh3333/* (gated), mncai/* (broken config).

v5 is 3x smaller than v4 and scores higher on the harder benchmark.

Raw model (no postprocess)

Model Params raw F1 raw P raw R
LFM2.5-350M-PII 350M 0.254 0.207 0.328
schift-ko-pii-v4 111M 0.702 0.602 0.842
schift-ko-pii-v5 34M 0.578 0.847 0.439

v5 raw recall is low because the model only predicts private_person and private_address. All other categories (phone, email, dates, IDs) are handled by the regex postprocessor โ€” by design, not by limitation.

Category breakdown (benchmark_v1, 93 cases)

Category v5 (34M) v4 (111M) LFM2.5 (350M)
Person (standard) 1.00 1.00 0.67
Person (rare surnames) 1.00 1.00 0.60
Person (short names) 1.00 1.00 0.57
Address (urban) 1.00 1.00 0.67
Address (rural) 1.00 1.00 0.60
Phone (via postprocess) 0.86 0.86 0.92
Vehicle plate 1.00 1.00 0.00
Date (Korean) 1.00 1.00 0.00
Resident ID 1.00 1.00 0.75

Installation

pip install schift-ko-pii

Usage

1. Basic detection

from schift_ko_pii import detect

# Raw model output (person + address only)
spans = detect("ํ”ผ๊ณ  ๊น€๋ฏผ์ˆ˜์˜ ์ „ํ™”๋ฒˆํ˜ธ๋Š” 010-1234-5678์ด๋‹ค.")
for s in spans:
    print(f"  [{s['id']}] {s['label']}: {s['text']} (score: {s['score']})")
# [person_1] private_person: ๊น€๋ฏผ์ˆ˜ (score: 0.99)

# With postprocessing โ€” adds regex-based detection for phone, ID, dates, etc.
spans = detect("ํ”ผ๊ณ  ๊น€๋ฏผ์ˆ˜์˜ ์ „ํ™”๋ฒˆํ˜ธ๋Š” 010-1234-5678์ด๋‹ค.", postprocess=True)
# [person_1] private_person: ๊น€๋ฏผ์ˆ˜
# [phone_1] phone_number: 010-1234-5678

2. Mask and review

from schift_ko_pii import mask

result = mask(
    "ํ”ผ์˜์ž ๊น€์ฒ ์ˆ˜(์ฃผ๋ฏผ๋ฒˆํ˜ธ 850205-1234567)๊ฐ€ ํ”ผํ•ด์ž ๋ฐ•์˜ํฌ์—๊ฒŒ "
    "์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 521์—์„œ ๊ธˆ 3,000๋งŒ์›์„ ์ „๋‹ฌํ•˜์˜€๋‹ค.",
    postprocess=True
)

print(result["masked"])
# ํ”ผ์˜์ž [์‚ฌ๋žŒ1](์ฃผ๋ฏผ๋ฒˆํ˜ธ [์ฃผ๋ฏผ๋ฒˆํ˜ธ1])๊ฐ€ ํ”ผํ•ด์ž [์‚ฌ๋žŒ2]์—๊ฒŒ
# [์ฃผ์†Œ1]์—์„œ ๊ธˆ 3,000๋งŒ์›์„ ์ „๋‹ฌํ•˜์˜€๋‹ค.

# Entity map โ€” review what was detected
for e in result["entities"]:
    print(f"  {e['id']:>15}: {e['text']}")
#       person_1: ๊น€์ฒ ์ˆ˜
#  resident_id_1: 850205-1234567
#       person_2: ๋ฐ•์˜ํฌ
#      address_1: ์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 521

3. Selective replacement

from schift_ko_pii import mask, apply

text = ("๊ณ„์•ฝ์ž ๋‚จ๊ถํ˜œ์ง„(010-9876-5432)์˜ ๊ฑฐ์ฃผ์ง€ "
        "๋ถ€์‚ฐ๊ด‘์—ญ์‹œ ํ•ด์šด๋Œ€๊ตฌ ์šฐ๋™ 1414๋ฒˆ์ง€ ๋งˆ๋ฆฐ์‹œํ‹ฐ 102๋™ 305ํ˜ธ์—์„œ "
        "์ฐธ๊ณ ์ธ Mike Johnson์„ ๋ฉด๋‹ดํ•˜์˜€๋‹ค.")

result = mask(text, postprocess=True)

# User reviews and decides what to redact
redacted = apply(text, result["entities"], {
    "person_1": "โ—‹โ—‹โ—‹",       # ๋‚จ๊ถํ˜œ์ง„ โ†’ โ—‹โ—‹โ—‹
    "person_2": "์™ธ๊ตญ์ธA",    # Mike Johnson โ†’ ์™ธ๊ตญ์ธA
    "phone_1": "010-****-****",
    # address_1 intentionally left unmasked
})
print(redacted)
# ๊ณ„์•ฝ์ž โ—‹โ—‹โ—‹(010-****-****)์˜ ๊ฑฐ์ฃผ์ง€
# ๋ถ€์‚ฐ๊ด‘์—ญ์‹œ ํ•ด์šด๋Œ€๊ตฌ ์šฐ๋™ 1414๋ฒˆ์ง€ ๋งˆ๋ฆฐ์‹œํ‹ฐ 102๋™ 305ํ˜ธ์—์„œ
# ์ฐธ๊ณ ์ธ ์™ธ๊ตญ์ธA์„ ๋ฉด๋‹ดํ•˜์˜€๋‹ค.

4. Bulk masking (replace all with ***)

from schift_ko_pii import mask, apply

text = "ํ”ผํ•ด์ž ์ด๋„(์—ฌ๊ถŒ M12345678)๋Š” ์„œ์šธ ์„œ์ดˆ๊ตฌ ๋ฐ˜ํฌ๋Œ€๋กœ 58์— ๊ฑฐ์ฃผํ•œ๋‹ค."
result = mask(text, postprocess=True)

# Replace everything with ***
clean = apply(text, result["entities"])
# ํ”ผํ•ด์ž ***(์—ฌ๊ถŒ ***)๋Š” ***์— ๊ฑฐ์ฃผํ•œ๋‹ค.

5. Using transformers directly

from transformers import AutoModelForTokenClassification, AutoTokenizer, pipeline

model = AutoModelForTokenClassification.from_pretrained(
    "schift-io/schift-ko-pii-v5", trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained("schift-io/schift-ko-pii-v5")

pipe = pipeline("token-classification", model=model, tokenizer=tokenizer,
                aggregation_strategy="simple")

results = pipe("์ฐธ๊ณ ์ธ ์ œ๊ฐˆ๋Ÿ‰์˜ ์ฆ์–ธ์„ ์ฑ„ํƒํ•œ๋‹ค.")
for r in results:
    print(f"  {r['entity_group']}: {r['word']} ({r['score']:.2f})")
# private_person: ์ œ๊ฐˆ๋Ÿ‰ (0.95)

6. Processing files

from schift_ko_pii import mask, apply

with open("document.txt") as f:
    text = f.read()

result = mask(text, postprocess=True)

# Show what was found
print(f"Found {len(result['entities'])} PII entities:")
for e in result["entities"]:
    print(f"  {e['id']}: {e['text']}")

# Redact all and save
clean = apply(text, result["entities"])
with open("document_redacted.txt", "w") as f:
    f.write(clean)

API (free, no model download needed)

from schift import Schift

client = Schift(api_key="...")  # free at schift.io
result = client.pii.redact("๊น€๋ฏผ์ˆ˜์˜ ์ „ํ™”๋ฒˆํ˜ธ๋Š” 010-1234-5678์ž…๋‹ˆ๋‹ค.")
# Postprocessing is always enabled on the API.

Labels

Label Description Detected by Examples
private_person Person names (Korean, Hanja, foreign) Model ๊น€๋ฏผ์ˆ˜, ๋‚จ๊ถํ˜œ์ง„, Mike Johnson, ็”ฐไธญๅคช้ƒŽ
private_address Street/postal addresses Model ์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 521
phone_number Phone numbers Postprocess (regex) 010-1234-5678, 02-1234-5678
resident_id Resident registration numbers Postprocess (regex + checksum) 850205-1234567
account_number Bank accounts, passport, vehicle plates, business IDs Postprocess (regex) M12345678, 52๊ฐ€1234
private_date Dates in Korean format Postprocess (regex) 2024๋…„ 3์›” 15์ผ
private_url URLs, IP addresses Postprocess (regex) 192.168.1.1
secret API keys, passwords Postprocess (regex)

Architecture

  • Encoder: Conv + attention hybrid, 6 layers, hidden 512
  • Head: Linear classifier (512 โ†’ 33 labels)
  • Params: 34M
  • Format: safetensors float32 (136 MB)
  • Tokenizer: klue/roberta-base
  • Max length: 512 tokens
  • Tagging: BIES (Begin/Inside/End/Single)
  • Requires: trust_remote_code=True

Running benchmarks

python benchmark/run_benchmark.py --benchmark benchmark/benchmark_v2.jsonl --postprocess

License

Schift License v2.0 โ€” Apache 2.0 base with revenue threshold. Free for everyone under $10M annual revenue. Research, education, and non-profit always permitted.

Citation

@software{schift_ko_pii_v5_2026,
  author = {Schift Inc.},
  title = {schift-ko-pii-v5: 34M Korean PII Detection Model},
  year = {2026},
  url = {https://huggingface.co/schift-io/schift-ko-pii-v5},
}
Downloads last month
-
Safetensors
Model size
34M params
Tensor type
F32
ยท
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support

Evaluation results

  • F1 (with postprocess) on benchmark_v2 (253 cases)
    self-reported
    0.823
  • Precision (with postprocess) on benchmark_v2 (253 cases)
    self-reported
    0.812
  • Recall (with postprocess) on benchmark_v2 (253 cases)
    self-reported
    0.834