Post

학생 손글씨 풀이에서 오개념 찾기 ② — 0.68에서 0.75로: 같은 문항 예시, 모범 풀이, 그리고 '생각하는' 리랭커

학생 손글씨 풀이에서 오개념 찾기 ② — 0.68에서 0.75로: 같은 문항 예시, 모범 풀이, 그리고 '생각하는' 리랭커

①편에서는 손글씨를 읽는 단계(VLM)와 오개념을 진단하는 단계(LLM)를 나눠 public LB 0.667을 만들었습니다. 그 뒤 하루 사이에 다른 팀이 0.762로 올라섰고, 격차는 0.085였습니다. 이 글은 그 격차를 0.012까지 줄인 과정입니다. 순서대로 쓰면 다음과 같습니다.

단계public LB
①편 끝 (진단기 앙상블)0.667
+ 같은 문항 few-shot 진단기0.677
+ 문항별 모범 풀이0.688
+ 추론형 리랭커 (test 31%만)0.709
+ 추론형 리랭커 (전체)0.750

대회 진행 중이라 코드와 대회 이미지는 싣지 않습니다. OOF는 train 5-fold 중 2개 fold(600행)에서 잰 값입니다.

1. 손실은 어디서 나는가

후처리를 이것저것 시험해 보기 전에 천장부터 쟀습니다.

  • 정답이 NONE인 행은 이미 0.94로 거의 맞히고 있습니다. 잃는 점수의 95%는 오개념 코드를 고르는 쪽에서 나옵니다.
  • 정답 데이터에 있는 “최종 답이 맞았는가” 정보를 그대로 넣어 NONE 판정을 완벽하게 만들어도 +0.034에 그칩니다.
  • 보정, 가중치 학습, 스태킹, 이웃 코드 재정렬 등 후처리 10여 가지를 교차적합으로 검증했지만, 0.003을 넘는 것이 하나도 없었습니다.

그러니 0.085는 후처리로 메울 수 있는 격차가 아니었습니다. 코드 판별력 자체를 올려야 했습니다.

2. 같은 문항의 다른 학생을 보여주기

test 문항의 96%는 train에도 있습니다. 같은 문제를 다른 학생이 풀었고, 그 풀이에는 라벨이 붙어 있습니다. 지금까지는 이 정보를 “이 문항에서 어떤 코드가 자주 나오나”라는 빈도로만 썼습니다.

그래서 진단 프롬프트에 같은 문항을 푼 다른 학생의 풀이 최대 5개와 그 라벨을 직접 넣었습니다. 예시는 코드가 겹치지 않도록 돌아가며 골랐고, 학습할 때는 자기 자신과 같은 fold의 행은 빼서 정답이 새지 않게 했습니다.

OOF (fold 0, 진단기 입력 전사별)기존few-shot
zero-shot 32B 전사0.6130.628
파인튜닝 8B 전사0.5820.612
전처리 + Qwen3.8 전사0.6110.617

모든 전사와 두 fold 모두에서 올랐습니다. 모델이 “이 문제에서 이렇게 쓰면 이 코드”를 풀이 대 풀이로 비교할 수 있게 된 덕분입니다.

3. 문항마다 모범 풀이 한 장

다음은 “어디서부터 틀렸는가”를 찾기 쉽게 만드는 것이었습니다. 오개념의 근원은 풀이가 정답 경로에서 처음 갈라지는 지점입니다. 그 지점을 찾으려면 정답 경로가 필요합니다.

추론 모드를 켠 오픈 모델(Qwen3.8-27B)로 499개 문항마다 모범 풀이(중간값 포함)와 흔한 오답 2~4개를 한 번씩 만들어 진단 프롬프트에 넣었습니다. 생성은 문항 단위라 라벨이 새지 않고, 인터넷 없이 오픈 웨이트로 재현되므로 대회 규정도 지킵니다.

결과는 모든 전사·fold에서 +0.01~0.03이었고, 앙상블 OOF는 0.650에서 0.661로 올랐습니다. LB도 0.677에서 0.688로 같은 방향으로 움직였습니다.

4. 결정타: 후보 8개 중에서 “생각해서” 고르기

공개 노트북 하나가 쓰던 아이디어를 가져왔습니다. 2차 리랭크입니다. 우리 앙상블의 후보 순위를 보면:

상위 K개 안에 정답이 있는 비율K=1K=3K=5K=8K=10
OOF0.580.740.820.880.91

정답은 상위 8개 안에 88% 들어 있는데, 1순위로는 58%만 맞힙니다. 고르는 능력만 올리면 됩니다.

리랭커에는 다음을 한꺼번에 줍니다.

  • 정방향으로 돌린 풀이 사진과 전사
  • 그 문항의 모범 풀이와 흔한 오답
  • 같은 문항을 푼 다른 학생의 라벨된 풀이
  • 후보 8개 코드의 이름과 설명(A~H)

그리고 추론 모드로 학생 풀이를 모범 풀이와 한 줄씩 비교한 뒤 RANKING: C, A, F처럼 1~3순위를 내게 했습니다. 이 순위에 보너스를 줘서 1차 블렌드와 합칩니다.

첫 결과: 좋은데 절반이 실패

첫 OOF는 0.661에서 0.697로 올랐습니다. 그런데 로그를 보니 600행 중 341행(57%)이 답을 내지 못했습니다. 추론에 쓸 수 있는 토큰 4,096개를 학생 풀이를 따지는 데 다 써 버리고, 마지막 RANKING: 줄에 도달하지 못한 것입니다. 실패한 행은 1차 순서를 그대로 썼는데도 0.036이 올랐으니, 실제 효과는 훨씬 클 것이었습니다.

budget forcing

해결책은 단순합니다. 토큰이 떨어져 추론이 잘린 행은 지금까지의 추론 뒤에 </think>와 RANKING:을 붙여서 모델이 바로 답만 이어 쓰게 합니다. 생각한 내용은 그대로 두고 결론만 받아 내는 방식입니다(budget forcing).

OOF 600행답을 못 낸 행MAP@3
1차 블렌드–0.661
+ 리랭커 (forcing 전)3410.697
+ 리랭커 (forcing 후)170.734

리랭커 혼자서도 1순위 정답률이 66%로, 블렌드(58%)보다 높았습니다.

LB

test 3,498행 전체를 리랭크하는 데는 서버 한 대(A5000 24GB × 2)로 약 14시간이 걸렸습니다. 그래서 1/3이 끝났을 때 먼저 내 봤더니 0.688이 0.709가 됐습니다. OOF에서 기대한 기울기 그대로였습니다. 전체를 적용하자 0.750이 나왔습니다. 1위와의 격차는 0.085에서 0.012로 줄었습니다.

5. 배운 것

  1. 천장부터 재라. 후처리 천장이 +0.034라는 걸 먼저 알았기 때문에 그쪽에 시간을 더 쓰지 않았습니다.
  2. 같은 문항이 반복되는 데이터에서는 “비슷한 사례”가 최고의 특징이다. 빈도(사전분포)보다 사례(few-shot)가 강했습니다.
  3. 추론 모델은 토큰 예산을 따로 관리해야 한다. 좋은 추론이 답 직전에서 잘리면 아무 소용이 없습니다. 실패율을 로그에서 꼭 확인할 것.
  4. 부분 제출로 빨리 검증하라. 14시간짜리 추론을 기다리는 대신 1/3 시점에 내서 방향이 맞는지 확인했습니다.

다음

남은 0.012를 위해 다음을 준비하고 있습니다.

  • 리랭커가 답을 못 낸 나머지 5% 재실행
  • 같은 행을 두 번 추론해 투표하기(self-consistency)
  • 라벨 없는 풀이 5만 장으로 같은 문항의 예시를 촘촘하게 만들기

마침 이 글을 쓰는 동안 서버의 GPU 드라이버가 자동 업데이트로 바뀌어 GPU가 멈췄습니다. 리랭크가 끝나고 정확히 2분 뒤였습니다. 대회 기간에는 apt-mark hold를 걸어 두는 것을 권합니다.

This post is licensed under CC BY 4.0 by the author.