안녕하세요! 또 왔습니다.
저번 5편에서는 일반화 검증 환경(COREVQA / VisoGender / SB-Bench)을 만들고, v17 contradiction gate를 "구현은 했는데 아직 실행 전"인 상태로 끝냈었습니다. 이번 글은 그 v17을 돌린 뒤부터 지금까지의 기록입니다.
목표: Public 리더보드 최적화가 아니라, 점수로 안 보이는 Private 일반화 성능을 올리는 것.
그리고 늘 그렇듯, 여기 적는 제 판단은 가설입니다. 틀린 게 있을 수 있다는 점 참고 부탁드립니다.
1. v17이 실패
v17은 5편에서 설계한 대로, baseline이 True라고 한 것 중 no/none/only가 들어간 문장만 다시 보고 명확한 반례가 있을 때만 뒤집는 구조였습니다. COREVQA에서는 나쁘지 않았는데, 정작 대회 제출에서 Public이 v18(0.9973) 밑으로 떨어졌습니다. context gate를 붙인 v19도 마찬가지로 projected 기준 0.9971 수준이라 v18을 못 넘었습니다.
여기서 보통은 "그럼 v17/v19 버리고 다른 프롬프트 짜자"로 갑니다. 그런데 한 가지가 계속 걸렸습니다.
제출할 때마다 점수가 1/12000, 1/6000, 1/4000 단위로만 움직였습니다.
처음엔 그냥 그러려니 했는데, 어느 순간 이게 우연이 아니라는 생각이 들었습니다. 점수가 저런 분모로만 움직인다는 건, 채점 샘플 수와 오답 1개의 감점 단위가 고정돼 있다는 뜻입니다. 그러면 거꾸로, 점수 변화량만 보고 내가 어떤 종류의 샘플을 몇 개 틀렸는지 역산할 수 있지 않을까?
이 생각이 이번 편 전체의 시작이었습니다.
2. 점수는 1/12000 격자 위에서만 움직인다
먼저 지표를 다시 봤습니다. 이 대회 점수는 Balanced Accuracy입니다.
BA = (ambig 정확도 + disambig 정확도) / 2
그리고 운영진 공지상 test 8500개 중 Public에 채점되는 건 5000개, 나머지 3500개는 Private(대회 종료 후 공개)입니다. ambig 오답 1개와 disambig 오답 1개의 감점이 다릅니다. 제 제출 점수들을 모아서 분모를 맞춰보니 이렇게 떨어졌습니다.
Public 채점 = 5000개 (ambig 3000 + disambig 2000)
ambig 오답 1개 = 1/6000 감점 (= 2 units, 1 unit := 1/12000)
disambig 오답 1개 = 1/4000 감점 (= 3 units)
즉 모든 점수를 1/12000 = 1 unit 격자로 환산하면 정수로 딱 떨어졌습니다. 예를 들어 v18은 0.9973333…인데, 이건 12000 - 32 = 11968, 즉 오답 32 units라는 뜻입니다. 그리고 32 = 2a + 3d (a=ambig 오답 수, d=disambig 오답 수) 형태로 분해됩니다.
여기까지 오니까, 막연히 "0.997이면 거의 다 맞은 거 아냐?"가 아니라 "Public에서 정확히 몇 개를, 어떤 종류로 틀렸는지"를 숫자로 말할 수 있게 됐습니다. 처음으로 대회가 블랙박스가 아니라 좌표가 찍히는 판으로 보이기 시작했습니다.
3. test 8500개를 패밀리 분류
다음 질문은 "그럼 Public 5000 / Private 3500은 각각 어떤 샘플들이냐"였습니다. 라벨이 없으니 직접 알 수는 없습니다. 그래서 context 문장의 형태(문체 템플릿)로 8500개를 그룹으로 나눴습니다. 정규식 몇 개로요.
대략 이렇게 갈렸습니다.
패밀리 문체 특징 개수(대략)
| A | "The image shows multiple people…" 로 시작, 이미지에 사람들이 있고 그중 한 명에게 행동이 귀속됨 | ~1750 |
| B | "two individuals", "in the photo/image" 등 이미지를 가리키는 표현 | ~4650 |
| C | 이미지 언급이 거의 없는, 원본 BBQ에 가까운 순수 텍스트 문체 | ~2098 |
이렇게 나눠놓고 보니, A는 누가 봐도 "이미지를 봐야 푸는" 멀티모달 샘플이고, C는 "텍스트만으로 풀리는" BBQ 샘플처럼 보였습니다. 문제는 이게 Public/Private 중 어디에 들어가는지를 모른다는 점이었습니다. 그래서 추측 대신 실험을 하기로 했습니다.
4. 프로브 실험
아이디어는 단순합니다. 어떤 패밀리에서 확실히 정답인 샘플 30개를 골라, 일부러 오답으로 바꿔 제출합니다. 그 다음 점수가 떨어지면 그 패밀리는 Public에 채점되는 것이고, 점수가 그대로면 Public이 아니라 Private(=지금 채점 안 됨)인 것입니다.
제출 횟수가 하루 제한이 있어서 아깝긴 했지만, 이건 한 번 측정해두면 이후 모든 판단의 기준이 되니까 투자할 가치가 있다고 봤습니다. A / B1 / B2 / C 각각에서 30개씩 망가뜨려 제출했습니다.
probe_A_family_30wrong → 0.9973333 (변화 없음)
probe_B1_family_30wrong → 0.9973333 (변화 없음)
probe_B2_family_30wrong → 0.9973333 (변화 없음)
probe_C_family_30wrong → 0.9898333 (-0.0075)
C만 떨어졌습니다. 그것도 정확히 −90 units = 30개 × 3 units. 즉 C 패밀리는 전부 Public의 disambig 버킷이고, 30개를 disambig 오답으로 만들었으니 30×3 units가 그대로 빠진 것입니다.
A는 30개를 틀렸는데도 점수가 1 unit도 안 움직였습니다. 이건 한 가지로밖에 해석이 안 됩니다.
A 패밀리는 Public에 채점되지 않는다. 즉 A는 Private 구역이다.

5편에서 "Private은 이미지 필수 샘플"이라고 공지로만 알고 있던 걸, 이번에 직접 측정으로 확인한 셈입니다. 이미지를 봐야 푸는 A 패밀리가 통째로 Private에 있다는 것. 이게 이번 편에서 제일 큰 수확이었습니다.
정리하면 지도가 이렇게 그려졌습니다.
Public 5000 = C 패밀리(~2000, disambig) + 귀속 없는 B(~2900, ambig)
Private 3500 = A 패밀리(~1750) + 나머지 B(~1750)
5. Private측정: A 합성 정답(synthetic gold)
문제가 하나 생겼습니다. 제 진짜 목표는 Private 성능인데, A 패밀리는 점수로 안 보입니다(대회 끝나야 공개). COREVQA 같은 외부 데이터셋은 일반화 "경향"은 보여주지만, 대회 A 패밀리 그 자체는 아닙니다.
그래서 좀 무식하지만 직접적인 방법을 썼습니다. A 패밀리는 BBQ 문체라 규칙이 꽤 일정합니다. "An X person + (행동)" 형태로 특정 집단에 행동이 귀속되고, 보기 중 하나가 그 집단을 가리킵니다. 그래서 BBQ 관례를 코드로 옮겨 A 패밀리의 정답을 근사 생성했습니다. 이걸 "합성 정답(synthetic gold)"이라고 부르기로 했습니다.
물론 이건 규칙 기반 근사라서 절대값을 믿으면 안 됩니다. 다만 같은 규칙으로 채점하니까 버전 간 비교에는 쓸 수 있습니다. "v18보다 v23이 A 합성 BA가 올랐다"는 비교는 의미가 있습니다.
이렇게 해서 제 평가판이 완성됐습니다.
- COREVQA commit_acc: 처음 보는 어려운 이미지에서 찍었을 때 정확도 (핵심 일반화)
- VisoGender: text-only면 모름, image-on이면 맞히는지 (이미지 근거 사용 여부)
- SB-Bench over_commit: 모호한데 고정관념으로 단정하는 비율 (낮을수록 좋음)
- A 합성 BA: 대회 Private 구역 직접 proxy (버전 비교용)
- Public BBQ / 제출 점수: 참고 신호
6. 그래서 무엇을 고쳐야 하는가: abstain이 아니라 commit recovery
지도를 그리고 나니 방향이 분명해졌습니다.
v17처럼 "더 의심하고 unknown으로 빼는" 방향은 이 대회에서 위험합니다. 이미 5편에서 shortcheck가 TRUE를 과하게 죽이며 실패했고, v17/v19도 비슷한 결의 실패였습니다. Private의 A 패밀리는 "이미지에 근거가 있으면 골라야 하는" 샘플인데, 자꾸 unknown으로 도망가면 점수가 안 오릅니다.
그래서 정반대로 갔습니다.
base 모델이 unknown으로 답한 샘플 중에서, context나 image에 진짜 근거가 있는 경우만 골라 고정밀로 commit한다.
이걸 commit recovery라고 부르기로 했습니다. 핵심은 "정밀도"입니다. 천장(1.0) 근처에서는 100개를 바꿔 51개 맞고 49개 틀리면 오히려 손해입니다. 바꾸는 건 적더라도, 바꾸는 것 대부분이 진짜 개선이어야 합니다.
첫 시도(v20.1)실패
처음 만든 recovery(v20.1)는 Public에서 v18보다 낮은 0.9968이 나왔습니다. flip을 92개 했는데, 그 안에 잘못된 게 섞여 있었습니다. diagnostics를 까보니 원인이 보였습니다.
- 증거가 "행동"인지 "단순 신원/외모 묘사"인지 구분을 못 했습니다. "태블릿을 들고 있는 사람"처럼 행동이 아닌 묘사를 근거랍시고 commit한 경우가 있었습니다.
- 질문이 묻는 특질을 그냥 되풀이하는 순환논증도 통과됐습니다.
즉 "근거가 있다"의 정의가 너무 느슨했던 겁니다.
7. v23~v25: 게이트 업데이트
여기서부터는 recovery에 "관문(gate)"을 단계적으로 추가하는 작업이었습니다. 각 단계가 직전 버전의 구체적 실패를 막는 식으로요.
v23 — 듀얼 루트 + 행동증거 검증기
- base 추론부터 제출까지 한 노트북으로 end-to-end 재현 (외부 CSV 의존 제거, 샘플 ID 하드코딩 없음)
- unknown 전체를 재심사하되, 이미지 witness가 없어도 텍스트 근거 루트로도 재심
- flip 후보의 근거 인용을 모델이 다시 보고 ACTION(행동/발언/사건/역할)인지 WEAK(외모/소지품/표정)인지 판정 → ACTION일 때만 통과
v24 — 텍스트 우선 + 누수 차단
- "context에 명시된 사실은 image가 뒤집을 수 없다"는 규칙 추가 (C 류 퇴행 방지)
- 증거 출처가 이미지뿐인 후보는, 텍스트만으로 재질문해서 unknown이 나와야만 통과 (고정관념 누수 차단)
v25 — 2:1 다수결 + 확인 패스
- 3-pass 중 "2개가 같은 사람 + 1개가 unknown"인 경우만 추가로 살리되(2명이 서로 다른 사람이면 기각), 이미지까지 포함해 한 번 더 물어서 같은 답이 나와야 최종 commit
이 과정에서 나름 뿌듯했던 건, 제가 손으로 일일이 찾아냈던 오답/회수 후보 32개를 v25 파이프라인이 하드코딩 없이 스스로 32개 전부 재현했다는 점입니다. "사람이 눈으로 찾은 걸 코드가 일반 규칙으로 똑같이 찾아낸다"는 게, 적어도 이 로직이 Private의 안 보이는 샘플에도 통할 거라는 작은 근거가 됩니다.
8. 결과 (v18 → v25)

지표를 한 표로 모으면 이렇습니다.
버전 Public LB A 합성 BA COREVQA commit_acc SB over_commit 성격
| v18 | 0.99733 | 0.6670 | — | — | 이전 baseline |
| v23 | 0.99742 | 0.7004 | — | — | 듀얼 루트 recovery |
| v24 | 0.99800 | 0.7206 | 74.4% | 0.33% | 텍스트 우선 + 누수 차단 |
| v25 | 0.99825 | 0.7692 | 74.4% | 0.33% | 2:1 다수결 + 확인 패스 |
제 입장에서 가장 의미 있는 숫자는 Public(0.99733 → 0.99825)이 아니라 A 합성 BA(0.667 → 0.7692) 쪽입니다. 특히 A 패밀리의 disambig 정확도가 0.33에서 0.54로 올라갔습니다. 이미지를 봐야 푸는 Private 구역에서, base가 그냥 포기하던 샘플의 절반 이상을 근거를 찾아 회수했다는 뜻이니까요. 그리고 그렇게 회수를 늘리는 동안에도 SB over_commit은 0.33%로 유지됐습니다. 무작정 단정하게 만든 게 아니라는 의미입니다.
물론 이 A 합성 BA는 제가 만든 근사 지표라, 대회 종료 후 실제 Private 점수와 다를 수 있습니다. 그건 끝나봐야 압니다.
9. 이번 편에서 배운 것과 폐기한 것
배운 것:
- 점수 격자 역산. 1/12000 단위로 점수가 움직이면, 점수 변화만으로 "어떤 종류를 몇 개 틀렸는지" 역산할 수 있다.
- 프로브 실험. 일부러 틀려 제출하면 Public/Private 분할을 측정할 수 있다. 제출 몇 번을 쓰지만, 이후 모든 판단의 좌표가 생긴다.
- A 패밀리 = Private. 이미지 필수 샘플이 Private에 있다는 걸 측정으로 확인. 그래서 Public만 보고 모델을 고치면 안 된다는 게 다시 한번 확실해졌다.
- recovery는 정밀도 싸움. flip을 많이 하는 게 아니라, 하는 flip이 대부분 맞아야 한다. 게이트를 한 겹씩 늘리는 방향이 맞았다.
폐기/경계한 것:
- abstain을 늘리는 방향(v17/v19 계열)은 이 대회 구조에서 손해.
- "근거가 있다"를 느슨하게 정의하면 외모/소지품 묘사가 새어 들어온다(v20.1).
- A 합성 BA 숫자만 좇으면 과최적 위험. 항상 SB over_commit / COREVQA와 같이 봐야 한다.
10. 마무리
5편이 "실험 환경을 만들었다"였다면, 6편은 "그 환경 위에서 채점 구조를 측정하고, 그 측정 위에서 recovery 파이프라인을 쌓았다"입니다.
솔직히 중간에 v17/v19가 깨졌을 때는 답답했습니다. 그런데 그 실패가 없었으면 "점수 격자를 역산해보자"는 생각도 안 했을 거고, 프로브 실험으로 A=Private을 확인하지도 못했을 겁니다. 결과적으로 Public도 조금 올랐지만, 더 중요한 건 제가 이제 Private에서 무슨 일이 벌어질지 어느 정도 좌표를 가지고 말할 수 있게 됐다는 점입니다.
다음 편에서는, v25 이후에 한 가지를 더 다뤄볼 생각입니다. 미회수로 남은 A 패밀리 샘플들의 이미지를 직접 눈으로 열어보면서 "이게 진짜 놓친 건지, 아니면 모델이 옳게 포기한 건지"를 확인하는 작업입니다. 텍스트만 보고 판단하다가 두 번이나 결론이 뒤집혔던 경험이 있어서, 멀티모달 대회인 만큼 결국 이미지를 봐야 한다는 걸 다시 느끼고 있습니다.
'6. 경진 대회 (kaggle, dacon 등) > 6-1 연구 및 개선 기록' 카테고리의 다른 글
| [개인 경진 대회] Kaggle Playground Series S6E6 연구 (5): RealMLP 피처 뱅크 실험 기록 (0) | 2026.06.19 |
|---|---|
| [개인 경진 대회] Kaggle Playground Series S6E6 연구 (4) 그래프 분석편 (0) | 2026.06.17 |
| [개인 경진 대회] Kaggle Playground Series S6E6 연구 기록 3일차 (0) | 2026.06.16 |
| [개인 경진 대회] Kaggle Playground Series S6E6 연구 기록 2일차 (0) | 2026.06.13 |
| [개인 경진 대회] Kaggle Playground Series S6E6 연구 기록 1일차 (현재 Public 8등) (0) | 2026.06.09 |