pii-ner-kcelectra
한국어 텍스트에서 개인정보(이름·주소·학교·전화번호)를 탐지해 블러/마스킹하기 위한 NER 모델입니다. beomi/KcELECTRA-base-v2022를 합성 데이터로 파인튜닝했습니다.
라벨
| 라벨 | 대상 |
|---|---|
NAME | 사람 이름 (성+이름, 이름 단독) |
ADDR | 주소 (도로명주소, 지번주소) |
SCHOOL | 학교 (초·중·고 — 전체형 서울강남초등학교 및 약칭 강남중, 서울고) |
PHONE | 전화번호 |
ID | 메신저/SNS 아이디 (라인, 카톡, 텔레그램 등) |
BIO 스킴: O + 각 라벨의 B-/I-
학습 데이터
실제 개인정보를 전혀 사용하지 않은 템플릿 주입 합성 데이터 20,000문장 (3 epoch):
- 이름: 통계청 성씨 532종 × 남녀 이름 약 62만 종 랜덤 조합
- 주소: 행정안전부 도로명주소 전체분(2026.07)에서 샘플링한 실제 주소 64,221건 (도로명 + 지번 혼합)
- 전화번호: 휴대폰/지역번호 형식 랜덤 생성
- 안내문·서식·대화·기사·업무체 등 38종 템플릿 + PII 없는 negative 문장 포함
주입 위치가 곧 정답 라벨이므로 라벨 노이즈가 없고, 사전에 없는 희귀 이름 조합도 문맥(호칭, 조사 등)으로 일반화하도록 학습됩니다.
v2 — 도메인 적응: 위 합성 데이터에 더해 ① 실제 SNS/커뮤니티 크롤링 텍스트 22k(이름 사전·ID 패턴으로 PII 부재 검증) — 절반은 negative, 절반은 PII 주입 ② NAVER NER 뉴스 코퍼스 20k(gold 이름 라벨)를 섞어 재학습했습니다. 구어체·해시태그 텍스트에서의 과마스킹이 크게 줄었습니다 (실전 300건 기준 오탐 엔티티 5,488 → 200 내외). KMOU 뉴스 코퍼스(2,422 엔티티) 이름 탐지 82.6% — 미탐 대부분은 익명 이니셜(A/B)·공인.
v3 — 학교(초중고) 라벨 추가: 도로명주소 지역명 4천여 종에 초등학교/중학교/고등학교를
붙인 전체형과 약칭(서울강남초, 강남중, 서울고)을 합성해 학습 풀에 추가했습니다.
약칭은 중고·최고·기초·고3 등 일반어와 충돌하므로 이들을 하드 네거티브로 함께
넣어 정밀도를 유지했습니다. 학교 약칭 탐지 재현율 29/30(추가 전 0/30),
하드 네거티브 오탐 0. 전체형은 정규식 레이어(…초등학교/중학교/고등학교)도 병행 커버.
성능
실데이터 검증 — NAVER NER 코퍼스(뉴스 도메인, 학습에 미사용) 1,000문장 / PER 엔티티 1,398개:
| 모델 | 이름 탐지 recall¹ | 이름 완전 마스킹² |
|---|---|---|
| 본 모델 (v3) | 91.0% | 86.6% |
| 베이스라인 (KoELECTRA modu-ner) | 85.7% | 66.6% |
v2는 v1(뉴스 탐지 97.9%) 대비 뉴스 이름 recall을 약간 양보하는 대신, 구어체 실전 텍스트에서의 대량 오마스킹을 제거하고 ID 라벨을 추가한 버전입니다.
¹ 예측 span이 이름과 한 글자 이상 겹침 (블러가 이름을 건드렸는가) ² 이름 본체(조사·문장부호·괄호 병기 제외)의 모든 글자가 마스킹됨. 남은 실패는 대부분 예명·별명("미생", "가비")과 외국인 음역 복합 표기입니다.
참고로 학습과 동일한 생성 방식의 합성 홀드아웃(300문장)에서는 NAME/ADDR/PHONE 모두 recall 100%가 나오지만, 이는 in-distribution 수치이므로 성능 지표로는 위의 실데이터 결과를 보는 것이 적절합니다. 비-PII 문자 오마스킹은 약 2.3%.
⚠️ 뉴스 외 도메인(구어체 커뮤니티, OCR 텍스트 등)에서는 성능이 다를 수 있으니 적용 전 자체 데이터로 검증을 권장합니다.
사용법
from transformers import pipeline
pipe = pipeline("token-classification",
model="kiyuyeon/pii-ner-kcelectra",
aggregation_strategy="simple")
text = "김민수(010-1234-5678)가 서울시 강남구 테헤란로 12길 34로 이사했다."
for e in pipe(text):
print(e["entity_group"], text[e["start"]:e["end"]])
마스킹 적용:
def mask(text, entities, ch="■"):
chars = list(text)
for e in entities:
chars[e["start"]:e["end"]] = ch * (e["end"] - e["start"])
return "".join(chars)
print(mask(text, pipe(text)))
권장: 블러 용도로는 모델 단독보다 정규식과의 union이 안전합니다.
프로젝트의 pii_mask.py는 다음 정규식 레이어를 모델 span과 합쳐 마스킹합니다:
- 전화번호: 대시/공백/붙여쓰기(
01012345678), 지역번호,+82국제형식. 숫자 경계 가드로 주문번호 등 긴 숫자열 내부 오탐 방지 - 주민등록번호, 이메일
- 계좌번호: 대시 구분 3그룹+(마지막 그룹 4자리 이상 조건으로 날짜 배제), 대시 없는 10~14자리는 "계좌/입금/송금/출금/환불" 문맥 키워드가 있을 때만 (주문번호·운송장번호와 구분이 불가능하므로)
- 주소:
행정구역 + …로/…길/…동 + 번지패턴 (도로명·지번) - 학교:
접두어 + 초등학교/중학교/고등학교전체형(약칭은 모델이 담당) - 지역명: 도로명주소 데이터 기반 지역명 사전 4,005개 (시도·시군구·읍면동, #수원 등 해시태그 포함)
- 실명 백스톱: 성씨+상용이름 사전 일치(우측 경계 조건으로 용언 오탐 방지), 해시태그 실명(#김세빈), 성씨·이니셜+호칭("김 씨", "A씨", "박 대표")
한계
- 합성 템플릿 기반 학습이라 실제 도메인(구어체 커뮤니티, OCR 텍스트 등)에서는 성능 저하 가능
- 외국인 이름(음역), 예명·닉네임은 학습 범위 밖
- 주민등록번호·이메일·계좌번호는 이 모델의 라벨에 없음 — 위의 정규식 레이어가 담당
- 문맥 키워드 없는 대시 없는 숫자열 단독(예:
110123456789)은 계좌번호로 판정하지 않음