오류 내성 곡선 — C-5 마스킹 재현율

무엇을

같은 오류 구간(0~20%)에서 C-5 마스킹 재현율 곡선을 낸다.

왜 검색 곡선과 갈라 두는가

주 실패 모드가 다르다. 검색은 단어가 바뀌면 못 찾는 것이고, 마스킹은 구분자가 사라지는 것이 주 실패 모드다(2.4절) — 010-2345-6789 가 01023456789 이 되거나 자릿수가 하나 날아가면 정규식이 통째로 빗나간다. 지표도 다르다(Recall@5 / 누락 건수).

주입기 쪽에서 이미 고친 함정 하나

09-14 에 주입본이 정규화된 텍스트라 오류율 0% 에서도 하이픈이 사라지는 버그를 잡았다 — 그대로 뒀으면 0% 지점부터 교란이 들어가 곡선의 출발점이 거짓이 됐을 것이다. 편집은 원문에, 정규화는 WER 을 잴 때만 쓴다.

완료 조건

주의

⚠ 오류율 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/ 규칙 변경, 미결) ② 주입기가 실재 음절만 만들게(곡선이 달라진다 — 기준을 바꾼 뒤 재측정해야 한다)