오류 내성 곡선 — C-5 마스킹 재현율
무엇을
같은 오류 구간(0~20%)에서 C-5 마스킹 재현율 곡선을 낸다.
왜 검색 곡선과 갈라 두는가
주 실패 모드가 다르다. 검색은 단어가 바뀌면 못 찾는 것이고,
마스킹은 구분자가 사라지는 것이 주 실패 모드다(2.4절) —
010-2345-6789 가 01023456789 이 되거나 자릿수가 하나 날아가면 정규식이 통째로 빗나간다.
지표도 다르다(Recall@5 / 누락 건수).
주입기 쪽에서 이미 고친 함정 하나
09-14 에 주입본이 정규화된 텍스트라 오류율 0% 에서도 하이픈이 사라지는 버그를 잡았다 — 그대로 뒀으면 0% 지점부터 교란이 들어가 곡선의 출발점이 거짓이 됐을 것이다. 편집은 원문에, 정규화는 WER 을 잴 때만 쓴다.
완료 조건
- 다섯 구간의 누락 건수(재현율이 아니라 건수 — 절대 규칙은 건 단위다)
- 패턴별로 쪼개서 본다. ⚠ 표본이 있는 것은 P4·P6·P7 뿐이고 나머지는 「측정 불가」로 적는다
- NER 적용 전/후를 같은 곡선 위에 얹는다 — NER 이 오류에 얼마나 견디는지가 여기서 보인다
- 수치에 측정일·커밋·명령·표본 수(§5)
주의
⚠ 오류율 0% 에서 누락이 0건이 아니면 곡선을 내기 전에 그것부터 고친다. 2026-09-09 실측이 0% 상태에서 이미 누락 2건이다 — 그 위에 곡선을 얹으면 「오류 때문에 뚫렸다」와 「원래 뚫려 있었다」가 섞인다. 그래서 이 티켓은 NER 뒤에 온다.
2026-09-15 — NER 이 있어도 5% 부터 뚫린다. 그리고 하네스 채점에 구멍이 있었다
먼저 — 오류가 섞이면 하네스의 C-5 채점이 가짜 통과를 낸다
하네스는 «골든셋 raw_span 이 결과에 그대로 남았나» 로 봤다. 주입이 01012345678 을 공일공 1234 5678 로 바꾸면 원문 조각은
없지만 번호는 보인다. 그래서 «살아남음» 규칙을 따로 세웠다(evaluation/metrics/masking_robustness.py) —
번호는 연속 4자리 · 이름은 2글자 · 주소는 3글자가 남으면 노출. 주입이 개인정보 자체를 지운 건 「소거」 로 분모에서 뺀다.
그 규칙을 0% 에 대 보니 깨끗한 입력에서도 하네스가 틀리고 있었다. 규칙 마스킹이 GS-415 를 ******* 77길 12에서 로 가려
번지가 그대로 보이는데 raw_span 전체가 없으니 통과로 셌다 — 부분 마스킹이 절대 규칙을 가짜로 통과시키는 구조였다.
하네스 C-5 판정을 같은 규칙으로 바꿨다(harness.py, 회귀 테스트 test_partial_mask_leaving_house_number_is_a_miss).
규칙만 쓸 때 누락은 3 → 4건(GS-415 추가)이고, 규칙 + NER 은 여전히 0건이다.
실측
측정일 2026-09-15 · 커밋 38f2fc3-dirty · 골든셋 v1-150 · 시드 20260914·20260915·20260916 (점마다 최저치)
ELASTICSEARCH_URL=http://localhost:9200 .venv/bin/python scripts/measure_error_tolerance.py [--only retrieval --device mps | --only masking]
결과 data/processed/error-tolerance/2026-09-15-{retrieval,masking}.json · 로컬 Darwin arm64
오류를 넣은 문자열 = 골든셋 고객 발화 136건 전부(09-14 캘리브레이션과 같은 말뭉치) → 개인정보 항목만 채점
판정 24건(raw_span 있는 양성) · 소거 0건 · 음성 4건
실제 WER → 0.000 0.050 0.101~0.102 0.151 0.200
규칙만 누락 4 5 5 5 5
규칙+NER 누락 0 1 2 2 2
음성 과잉(둘 다) 0/4 0/4 0/4 0/4 0/4
| 누락 항목 | 규칙만 | 규칙 + NER |
|---|---|---|
| 0% | GS-056(P6) · GS-412(P6) · GS-413(P6) · GS-415(P7) | — |
| 5% | + GS-033(P7) | GS-415(P7) |
| 10~20% | 5% 와 같다 | GS-415(P7) · GS-056(P6) |
패턴별: 번호(P1~P5)는 두 방식 모두 20% 까지 누락 0. 뚫린 것은 전부 P6·P7 이다. ⚠ 패턴당 표본이 2~6건이다 — «번호는 오류에 강하다» 가 아니라 «이 표본에서 번호 누락이 안 나왔다» 다.
무엇이 뚫렸나 — 실제 출력
5% 시드 20260915 입력 「성북구 정 릉로 77길 12에서」 → NER 「성북구 정 *********에서」 띄어쓰기 오류로 도로명이 갈라져 앞 어절을 놓침
10% 시드 20260915 입력 「그 김 민준 씨가」 → NER 「그 김 민준 씨가」 이름이 갈라지자 PER 를 못 줌
둘 다 spacing_split(띄어쓰기 쪼개짐)이다 — 실측 프로파일에서 두 번째로 많은 유형(12건)이다. P6·P7 경계 규칙
(pii_ner/domain/services/spans.py)이 어절 단위라 어절이 쪼개지면 경계가 끊긴다. 고칠 후보: 짧은 한글 조각이 인접할 때 이어서 태깅 ·
주소 덩어리에서 1음절 어절을 흡수. → 같은 날 고쳤다 — 아래.
⚠ 이 곡선은 낙관 쪽으로 기운다. 주입기는 실측 편집 81건 중 46.9%(환청·의미 붕괴)를 흉내 내지 못한다 — 같은 WER 에서 실제 STT 보다 덜 파괴적이다. 「오류에 강하다」로 옮기지 않는다. 실제 전사로 잰 곡선이 아니다.
⚠ 모델 학습 전 값이다. 경로에 학습 대상 모델이 없어(주입기 · BM25 · 사전학습 KoE5 · bge-reranker · 사전학습 NER) 재었지만, 정성윤 님 학습 모델이 올라와 검색·마스킹 경로가 바뀌면 같은 명령으로 다시 잰다.
같은 날 고쳤다 — 쪼개진 1음절을 붙인 사본을 한 번 더 태깅
pii_ner/domain/services/spans.py join_split_syllables — 1음절 한글 어절을 2음절 이상인 이웃에 붙인 사본을 만들고
(김 민준→김민준 · 정 릉로→정릉로, 1음절끼리·정상 띄어쓰기는 그대로), 원문과 사본 양쪽을 태깅해 합집합을 가린다.
사본 구간은 원문 오프셋으로 되돌려 사이 공백까지 덮는다(자리수 보존). 테스트 고정.
같은 명령 재측정 (측정일 2026-09-15 · 커밋 38f2fc3-dirty · 시드 3개 · 점마다 최대 누락)
실제 WER → 0.000 0.050 0.101~0.102 0.151 0.200
규칙+NER (보강 전) 0 1 2 2 2
규칙+NER (보강 후) 0 0 1 1 1 ← GS-037(P6)
음성 과잉 0/4 0/4 0/4 0/4 0/4
하네스(깨끗한 입력) 누락 0 · 과잉 0.0 · PII 라벨 없는 발화 131건 중 새로 가린 구간 0개 (scripts/measure_ner_over_masking.py)
남은 1건(GS-037)은 주입기가 만든 가짜 음절 때문이다.
10% 시드 20260914 입력 「제 이름 박서연이곴 번호는 …」 → 「제 이름 박서연이곴 번호는 *************예요」
이고 가 자모 혼동으로 「이곴」— 한국어에 없는 음절이 되자 모델이 PER 를 하나도 주지 않았고, 규칙 폴백(server/apps/masking 이름 문맥 규칙)도
조사를 못 벗겨 5글자 후보를 버렸다. 주입기가 문맥 없이 자모를 바꿔 실재하지 않는 음절을 만든다는 한계는 w5-stt-error-injector 가
이미 적었다 — 실제 STT(음절 사전 위의 디코딩)가 내기 어려운 출력이다.
⚠ 그래도 절대 규칙 기준으로는 1건 누락이다. 지우지 않는다. 고칠 길은 둘인데 둘 다 여기서 하지 않았다 —
① 규칙 폴백이 문맥(「이름」) 뒤 긴 후보를 버리지 않고 어절째 가리게(과잉이 늘어난다 — server/ 규칙 변경, 미결)
② 주입기가 실재 음절만 만들게(곡선이 달라진다 — 기준을 바꾼 뒤 재측정해야 한다)