정답지 편향 제거 (전체 풀링 라벨링)

아카이브됨 — 검색 실험 결과 §8·§9에 흡수됐다 (2026-07-29)

라벨링 실적·검증 verdict·v5 수치는 그쪽이 정본이다. 남긴 이유: 상·하한 3가정 채점표와 세 갈래 부트스트랩 전체 표가 여기에만 있다. 단 그 표의 MRR 값은 골든셋 v4 기준이라 현행 성적과 섞어 읽으면 안 된다.

2차 실험(6.8.7~6.8.8)에서 정답지 자체를 고친 기록이다. 성적표가 아니라 정답지 이야기다. 성적 정본은 검색 2차 실험 (임베딩·BM25·리랭커) · 정답지 제작 경위는 골든셋 개념 → 상·하한 채점 · paired bootstrap · 후보 캐시

0. 세 줄 요약

  1. 정답지가 e5로 만들어져 있었다. 새 임베딩이 찾아온 청크의 26~32%를 아무도 읽어본 적이 없었고, 채점은 그걸 전부 오답으로 셌다.
  2. 세 모델의 후보를 합집합으로 묶어 2,105개를 추가 판정했다. 신규 정답 42개(2.0%). 골든셋은 v5가 됐고 **미판정률이 전 조합 0.0%**가 됐다.
  3. 편향을 걷어내니 KURE의 이득이 +0.0068 → +0.0297로 4배 커졌다. 그래도 신뢰구간 하한이 −0.0042라 판정은 뒤집히지 않았다 → 검색 2차 실험 (임베딩·BM25·리랭커)

1부 · 무엇이 문제였나

1. 정답지는 e5가 만들었다

정답 라벨은 e5(M0)가 뽑아온 후보만 사람이 판정해서 만들어졌다(골든셋). 새 모델이 그 풀 밖에서 진짜 정답을 찾아오면 라벨이 없어 오답으로 세어진다.

리랭킹 상위 10개 기준 미판정 비율(6.8.7 실측):

조합상위 10개 중 미판정
m0_r1 e5 + R1 (현행)0.7%
m1_r1 bge-m3 + R126.0%
m2_r1 KURE + R132.0%

새 모델이 사용자에게 보여줄 10개 중 3분의 1을 아무도 읽어본 적이 없었다. 계획 §9의 *“잘할수록 손해를 본다”*가 수치로 나타난 것이다 → 검색 2차 실험 (임베딩·BM25·리랭커)

2. 구간에 가둬봤다 — 6.8.7

전부 라벨링하기 전에, 상·하한 채점으로 판정이 갈리는지부터 봤다.

이름미판정 청크를성격
하한오답으로 센다당시 채점. 새 모델에 가장 가혹
응축순위에서 빼고 센다판정된 것끼리만 겨룸
상한정답으로 센다새 모델에 가장 관대

C_doc MRR@10 (골든셋 v4 기준):

조합하한응축상한
m0_r1 e5+R10.74430.74460.74610.0018
m0_r2 e5+R20.68050.68050.68050.0000
m1_r1 bge-m3+R10.74120.75460.79160.0504
m1_r2 bge-m3+R20.65810.66840.71330.0552
m2_r1 KURE+R10.75110.76390.80020.0491
m2_r2 KURE+R20.67020.67910.73500.0648

M0의 폭이 0.0018로 사실상 없다. 두 가지를 동시에 말해준다 — ① 정답지가 M0에 맞춰져 있다는 직접 증거이고, ② 그러므로 1차 실험 수치는 그대로 신뢰해도 된다.

2-1. 세 갈래 검정 — 판정이 갈렸다

164문항 복원추출 1만 회, 시드 20260729 고정, 기준선 m0_r1 대비 → paired bootstrap

조합현행 채점 (하한↔하한)보수 (도전 하한↔기준 상한)낙관 (도전 상한↔기준 하한)판정
m0_r2−0.0638 [−0.1005, −0.0275] ✱−0.0657 ✱−0.0638 ✱기각
m1_r1−0.0030 [−0.0414, +0.0364]−0.0049 [−0.0434, +0.0347]+0.0473 [+0.0106, +0.0862] ✱라벨링 필요
m1_r2−0.0862 [−0.1358, −0.0377] ✱−0.0881 ✱−0.0310 [−0.0778, +0.0169]기각
m2_r1+0.0068 [−0.0290, +0.0433]+0.0050 [−0.0309, +0.0421]+0.0559 [+0.0197, +0.0935] ✱라벨링 필요
m2_r2−0.0741 [−0.1216, −0.0272] ✱−0.0760 ✱−0.0093 [−0.0569, +0.0396]기각

✱ = 95% 구간이 0을 포함하지 않음

보수 검정이 0 위        → 채택 (가장 불리하게 봐도 이긴다)
낙관 검정마저 0을 품음  → 기각 (라벨을 더 붙여도 못 이긴다)
그 사이                 → 라벨링이 판정을 좌우한다

R2 조합 셋은 여기서 확정 기각됐다(어떤 가정으로도 진다). m1_r1·m2_r1만 남았고, **“채택도 기각도 못 한다”**는 상태가 됐다. 그래서 읽기로 했다.

2-2. 산출물 (6.8.7)

src/eval/label_bias.py3단 채점(하한·응축·상한) + paired bootstrap
scripts/rerank_bias_bounds.py3갈래(현행·보수·낙관) 판정
scripts/rerank_on_cache.py --dump-ranked순위 목록 저장 — chunk_id만 담는다(NDA)
data/bias_bounds_200.json결과

pytest 12건 추가(라벨 전무·전부 경계, 시드 고정 재현성) 포함 전체 515건 통과.


2부 · 어떻게 고쳤나 — 6.8.8

3. 설계 — 정답지는 하나로 둔다

모델별로 정답지를 만들면 서로 다른 시험이 되어 비교 자체가 불가능해진다. 그래서 정답지는 하나로 두고, 세 임베딩의 dense_doc 후보를 합집합으로 묶어 판정 풀을 넓혔다. 기존 판정분은 재사용하고 증분만 판정한다.

합집합문항당 36.0개 (최소 25 / 최대 50)
신규 판정 대상2,105개 (문항당 12.8)

범위를 dense_doc만으로 잡았다. BM25는 당시 미채택이었고 dense_full은 프로덕션 경로가 아니라, 판정 기준인 C_doc 점수에 영향을 주지 않는다(전 필드로 넓히면 2,496개) → 후보 캐시

기존 파이프라인을 그대로 썼다 — 라벨러 rounds/r8/SKILL.md(v8, 재현율 0.863 / 정밀도 0.750) → 독립 검증자 VERIFY_SPEC.md. 200문항 골든셋이 만들어진 바로 그 흐름이다.

여기서는 절대 정확도보다 일관성이 중요하다

증분을 더 엄격한 방법으로 라벨링하면 기존 정답과 잣대가 갈린다. 그러면 e5(기존 라벨 위주)와 KURE(신규 라벨 위주)가 서로 다른 기준으로 채점된다. 지금 고치려는 편향을 다른 편향으로 바꾸는 꼴이다. 그래서 같은 스킬·같은 검증 절차를 썼다.

4. 실적 — 전수 2,105개, 라벨러 4 + 검증자 4

샤드문항라벨러 정답/판정검증 verdict추가제거보류
shard1437 / 530 (1.3%)통과 40 · 수정 3003
shard24213 / 525 (2.5%)통과 42000
shard33713 / 525 (2.5%)통과 36 · 수정 1101
shard4429 / 525 (1.7%)통과 41 · 수정 1001
16442 / 2,105 (2.0%)재작업 0105

독립 검증자 4명이 2,105개를 전부 재독해 추가 1건 · 제거 0건. 라벨러 4명의 판정이 사실상 그대로 통과했다. 지어낸 chunk_id 0건, 스키마 위반 0건.

4-1. 정답률이 낮아 한 번 의심했다

shard1의 1.3%는 기존 판정 풀(310/6,751 = 4.6%)의 1/3.5다. **“덜 고른 것 아닌가”**를 의심할 신호가 셋 있었다.

  1. 정답률이 기존의 1/3.5
  2. R8(쪼개진 표를 한 단위로 묶는 규칙, 우선순위 3위) 적용이 43문항 전부 0건 — RFP는 표가 청크 경계에서 갈리는 일이 흔한데도
  3. 기존 정답 청크와 번호가 바로 인접한(::NNNN ±1) 신규 후보 25개 중 정답 판정은 2개

VERIFY_SPEC에 *“라벨러는 대개 덜 고르는 쪽으로 틀린다 — 놓친 정답이 초과 오답보다 많았다”*가 명시돼 있어 재독시켰고, 추가 0건으로 돌아왔다. 인접 청크 25개 중 23개의 배제 사유가 원문 인용과 함께 남아 있었고(예: 구축일정 간트표에 “안정화” 행 이름만 있고 값이 없음), 문항 간 일관성도 확인됐다(g200:0160267cg200:af85ee75에서 같은 유형을 동일하게 배제).

낮은 정답률은 라벨러의 태만이 아니라 실제 분포였다. 의심 신호가 있을 때 재독시킨 게 값을 했다.

5. 골든셋 v5

정답 청크   310 → 353     (+43)
판정 풀   6,751 → 8,856   (문항당 41.2 → 54.0)

보류 5건은 미반영 — 정답으로 치지 않는다. 오염보다 누락이 덜 해롭기 때문이다. 쟁점은 표기 정규화다. 가장 날카로운 건 g200:15de45f1로, 표에 “계약일로부터 3개월 / 2024년 11월 1일까지”가 있고 기준답변은 “계약일로부터 90일”이라 3개월 = 90일을 같은 값으로 볼지가 걸린다(R6).

6. 편향이 사라졌다 ★

상위10 미판정률   전 조합 0.0%     ← 6.8.7에서 M0 0.7% / M1 26.0% / M2 32.0%
하한 = 응축 = 상한   완전 일치      ← 보정할 여지 자체가 소멸

6.8.7에서 도입한 상·하한 구간이 한 점으로 붕괴했다. 이게 풀링이 성공했다는 증거다 — 더 이상 “라벨이 없어서 오답이 된” 청크가 없다.

편향 제거 후 KURE의 실제 이득은 +0.0068이 아니라 +0.0297이었다. 4배 넘게 커졌다. 그런데도 판정은 바뀌지 않았다 — 최종 성적과 신뢰구간 → 검색 2차 실험 (임베딩·BM25·리랭커) §7


3부 · 하다가 틀린 것

7. “정답률 1.3% → 미채택”으로 판단했다가 철회했다 ★

shard1 결과를 받고 이렇게 적었다.

상한 가정이 비현실적이었다. 실측 정답률이 1.3%뿐이니 하한이 거의 참값이고, 하한 기준 구간 [−0.029, +0.043]이 0을 품으니 임베딩 교체는 승격 근거가 없다.

틀렸다. 정답률만 보고 그 정답이 점수를 얼마나 움직이는지는 재지 않은 판단이었다.

찾은 7개를 실제로 채점에 반영해봤다(43문항분만).

조합반영 전반영 후Δ
e5 + R1 (현행)0.74430.7443+0.0000
bge-m3 + R10.74120.7473+0.0061
KURE + R10.75110.7590+0.0078

7개뿐인데 KURE가 +0.0078 올랐다. 그 7개가 6문항에 붙었고, MRR은 정답 하나가 한 문항 점수를 0 → 0.5로 바꿔버리는 지표다. e5가 꿈쩍도 안 한 건 예상대로다 — 기존 라벨이 애초에 e5 후보에서 나왔으니 더 얻을 게 없다(§1).

교훈 — 정답률이 낮은 것과 지표에 영향이 없는 것은 다른 얘기다

MRR은 상위 순위에 가혹하게 민감하다. 2,105개 중 42개(2.0%)라는 개수 비율은 “무시해도 되는 크기”처럼 보이지만, 그게 어느 문항 몇 등에 꽂히느냐에 따라 지표는 크게 움직인다.

판정은 개수 비율이 아니라 지표에 반영해서 봐야 한다.

8. 표본에서 한 예측이 전수에서 그대로 맞았다

철회 직후, 표본으로 전수를 외삽했다. shard1 43문항 중 6문항이 정답을 얻었으니 전수 164문항이면 약 23문항 → KURE 상승분 약 +0.0297 추정.

전수 결과가 정확히 +0.0297이었다.

예측한 것맞았나
상승폭 약 +0.0297✅ 정확히 일치
격차가 유의성 경계에 걸린다✅ 구간 하한 −0.0042
경계를 넘는다넘지 못했다

표본 추정이 맞았고, 경계에 걸린다는 예상도 맞았고, 경계를 넘지는 못했다. 그래서 전수까지 간 것이 헛수고는 아니었다 — 외삽만으로는 ①과 ③을 구분할 수 없었다.


9. 관련 문서

문서무엇을 보러
검색 2차 실험 (임베딩·BM25·리랭커)성적·판정 정본. 이 라벨링이 무엇을 바꿨나
골든셋정답지가 원래 어떻게 만들어졌나
골든셋 200문항 제작골든셋 버전 이력 (v4 → v5)
상·하한 채점 · paired bootstrap이 문서가 쓴 방법
검색 2차 실험 (임베딩·BM25·리랭커)§9가 이 편향을 최대 위험으로 지목했다

코드: src/eval/label_bias.py · scripts/rerank_bias_bounds.py · scripts/rerank_on_cache.py --dump-ranked 라벨링 스킬: .harness-mem/goldenset-skill/rounds/r8/SKILL.md(v8) · VERIFY_SPEC.md 정본: Plans.md 6.8.7 / 6.8.8