민트 재검증과 재설계: 과거 상태를 어디까지 복원할 수 있는가
이전 글에서는 감사 이후 멤버풀을 다시 비교하고, v47과 v49에서 새로운 최고 점수를 만든 과정을 정리했다.
그 과정에서 민트 메타데이터 처리 방식을 바꾼 v48과 v50은 서버에서 모두 반납했다.
하지만 실험 과정에 노이즈가 있었기에 재확인이 필요했다.
v50은 그중 일부를 개선했지만, 두 모델이 사용한 train_mint_ci 자체가 정확한 과거 상태를 담고 있는지도 다시 확인해야 했다.
결국 문제는 모델보다 데이터 쪽에 있었다.
history에서 과거 action을 복원할 수 있다는 것과, 그 action이 실행됐을 당시의 상태까지 정확히 복원할 수 있다는 것은 다른 문제였다.
다시 확인한 민트의 구조
민트는 train row의 history 안에 남아 있는 과거 user 발화와 assistant action을 별도의 학습 샘플로 만든 데이터였다.
예를 들어 history에 다음과 같은 흐름이 있다면,
user: main.py를 확인해줘
assistant_action: read_file
이를 다음 행동 예측용 학습 샘플로 다시 구성했다.
입력: main.py를 확인해줘
라벨: read_file
라벨은 history에 기록된 assistant action에서 가져올 수 있었다.
과거 user 발화와 그에 대응하는 action의 위치도 세션 step과 정렬할 수 있었다.
문제는 그 당시의 session_meta였다.
원본 민트 생성 방식은 과거 step을 하나 추출한 뒤, source row의 데이터를 복사하고 turn_index 정도만 조정하는 방식에 가까웠다.
따라서 민트 샘플에는 과거 action 시점이 아니라 그보다 나중인 source row의 상태가 남았다.
대표적으로 다음 정보들이 영향을 받았다.
- last_ci_status
- git_dirty
- open_files
- budget_tokens_remaining
- elapsed_session_sec
과거에는 파일이 열리지 않았는데 source row에서는 열려 있을 수 있고,
과거에는 테스트 전이었는데 source row에서는 이미 테스트가 끝난 상태일 수 있었다.
이 때문에 민트는 라벨과 텍스트 문맥은 과거 시점이지만, 메타데이터 일부는 미래 상태인 혼합 샘플이 될 수 있었다.
train_mint_ci는 정확한 CI 복원본이 아니었다
처음에는 history 안의 run_tests 기록을 순서대로 재생하면 과거의 CI 상태를 복원할 수 있다고 생각했다.
구현 방식은 대략 다음과 같았다.
history 시작 시 CI = none
run_tests 성공 → passed
run_tests 실패 → failed
이렇게 만든 데이터가 train_mint_ci였다.
하지만 여기에는 중요한 가정이 숨어 있었다.
history가 시작되는 시점의 CI 상태가 none이라는 가정이었다.
실제 history는 세션 전체 기록이 아니라 최근 일부 대화만 남긴 rolling window였다.
오래된 action은 history 앞부분에서 잘려 나갈 수 있었다.
따라서 현재 history 안에 run_tests가 없더라도, 실제로는 더 이전 step에서 테스트를 실행했을 수 있었다.
예를 들어 실제 세션이 다음과 같다고 해보자.
step 2: run_tests → failed
step 3: edit_file
step 4: read_file
step 5: 현재 row
현재 row의 history가 최근 step 3~4만 포함하면, run_tests 기록은 보이지 않는다.
이때 CI 재생 코드는 시작 상태를 none으로 두지만, 실제 상태는 failed일 수 있다.
전수 검증 결과 이 문제가 실제로 확인됐다.
같은 CI replay를 train 70,000행에 적용했을 때 metadata의 last_ci_status와 일치한 비율은 43.71%였다.
history 안에 run_tests가 보이는 행에서는 일치했지만, 테스트 기록이 보이지 않는 행에서는 일치율이 크게 떨어졌다.
더 문제가 된 것은 기존 민트의 CI가 예상보다 정확했다는 점이었다.
복원 여부를 판정할 수 있었던 민트 2,946개 중 source row의 최종 CI를 사용한 기존 민트는 2,908개가 맞았다.
반면 train_mint_ci는 1,287개만 맞았다.
즉, 잘못된 값을 고치려던 작업이 오히려 기존의 맞는 값 다수를 none으로 바꿔버렸다.
따라서 기존 결론을 수정해야 했다.
기존 판단:
run_tests를 재생해 민트 CI를 복원했다.
수정된 판단:
rolling history 이전의 CI 상태를 알 수 없기 때문에
해당 replay는 정확한 복원이 아니었다.
train_mint_ci는 이후 정확한 CI 복원본이 아니라, CI 값을 특정 방식으로 다시 작성한 실험 데이터로만 취급했다.
마스킹 효과라고 생각했던 결과도 묶음 효과였다
kf768ci74는 처음에 복원하기 어려운 민트 메타데이터를 제거한 실험으로 해석했다.
하지만 실제 입력을 다시 비교하니 한 가지 변수만 바뀐 것이 아니었다.
동시에 들어간 변화는 다음과 같았다.
- 민트의 CI를 부정확한 replay 값으로 변경
- open_files, git_dirty, budget, elapsed 제거
- 제거된 값이 0으로 들어가며 민트 행만의 zero-sentinel 생성
- Transformer의 [META] 텍스트도 함께 변경
특히 real train에는 budget과 elapsed가 모두 0인 행이 없었는데, 마스킹한 민트에서는 두 값이 모두 0이 됐다.
이는 단순한 정보 제거가 아니었다.
모델 입장에서는 다음과 같은 신호가 생긴 셈이었다.
budget=0이고 elapsed=0이면 민트 샘플일 가능성이 높다.
즉, 마스크가 민트 샘플을 완벽하게 구분하는 도메인 표식처럼 작동할 수 있었다.
그래서 kf768ci74의 holdout 상승을 “메타데이터를 제거한 효과”라고 해석할 수 없었다.
실제로 마스크만 적용한 m74는 기존 seed74 모델보다 holdout이 내려갔다.
CI 재작성만 적용한 cin74는 조금 올랐고, 둘을 함께 넣은 ci74는 더 크게 올랐다.
하지만 서버에서는 ci74를 사용한 v48과 cin74를 사용한 v50이 모두 반납했다.
| 기준 seed74 | v47 | 0.7820173794 |
| CI 재작성 + zero-sentinel 마스크 | v48 | 0.7811759252 |
| CI 재작성만 적용 | v50 | 0.7807140946 |
따라서 여기서 확인한 것은 좁은 범위였다.
민트가 정말 6,859개의 새 데이터를 만들었는가
CI 문제를 확인한 뒤에는 민트 데이터의 양도 다시 살펴봤다.
기존 mint1은 3,180행이었고, 더 많은 history 구간을 수확한 mint2는 6,859행이었다.
처음에는 mint2가 두 배 이상의 새로운 학습 정보를 제공한다고 생각했다.
하지만 rolling history를 기준으로 prefix를 그대로 세면 같은 과거 action이 여러 번 중복될 수 있었다.
예를 들어 하나의 과거 action이 이후 여러 row의 history에 반복해서 등장하면, rolling window의 시작점이 다를 때마다 서로 다른 prefix처럼 보인다.
처음 full-prefix를 기준으로 집계했을 때는 수만 개의 새로운 상태가 있는 것처럼 보였다.
하지만 세션 step에 맞춰 다시 정렬하자 결과가 달라졌다.
| 실제 row에서 빠진 세션 step | 3,205 |
| history에서 prompt/action 복원 가능 | 3,181 |
| 복원 불가능 | 24 |
| mint1이 이미 복원한 step | 3,180 |
| mint1이 놓친 복원 가능 step | 1 |
즉, mint1은 history에서 얻을 수 있었던 실질적인 신규 step을 거의 전부 사용하고 있었다.
mint2의 6,859행은 새로운 action 6,859개가 아니었다.
같은 3,180개 target이 서로 다른 rolling-window 길이로 반복된 변형이었다.
이 구조에서는 일부 target이 한 번만 등장하고, 깊은 history를 가진 target은 최대 여러 번 등장할 수 있다.
모든 행을 같은 가중치로 학습하면 특정 target을 과대표집하게 된다.
따라서 mint2가 실패한 이유도 단순히 “추가 데이터의 품질이 낮았다”라고만 볼 수 없었다.
mint1:
3,180개의 서로 다른 과거 target
mint2:
같은 target을 여러 rolling-window 형태로 반복한 6,859행
새로운 정보의 양이 두 배가 된 것이 아니라, 기존 정보의 표현이 중복된 것이었다.
중복을 엄격하게 다시 제거하다
다음으로 기존 민트가 real train과 중복되는지도 다시 확인했다.
기존 민트 생성기는 세션 안에서 같은 current_prompt가 반복되는 정도만 제거했다.
전체 train과 비교한 입력 중복까지 제거한 것은 아니었다.
전수 검사 결과는 다음과 같았다.
- 원본 민트: 3,180행
- real train과 exact (history, current_prompt)가 같은 행: 18개
- strict 중복 제거 후: 3,162행
prompt 문자열만 같은 행은 더 많았지만, history가 다르면 같은 상태라고 볼 수 없었다.
그래서 strict 중복은 현재 prompt뿐 아니라 history까지 정확히 같은 경우로 정의했다.
결과적으로 train_mint_strict는 3,162행이 됐다.
다만 중복을 제거해도 메타데이터 문제는 남았다.
과거 action의 라벨과 텍스트를 복원할 수 있다는 사실이, point-in-time metadata까지 완전히 복원할 수 있다는 뜻은 아니었다.
직접 주입 대신 2단계 학습으로
민트를 real train과 섞어 직접 학습시키면, 잘못된 메타데이터가 Transformer와 metadata head에 그대로 전달된다.
그래서 사용 방식을 바꾸기로 했다.
민트에서 상대적으로 신뢰할 수 있는 것은 과거 user 발화, 그 이전의 텍스트 문맥, 그리고 action label이었다.
반면 시점이 불확실한 메타데이터는 모델에 직접 주입하지 않는 편이 안전했다.
이를 바탕으로 2단계 학습 구조를 설계했다.
Stage A: 민트 text-only 사전학습
strict mint를 사용해 Transformer backbone이 user 요청과 action의 관계를 먼저 학습하게 했다.
이 단계에서는 session metadata를 사용하지 않았다.
strict mint text
→ Transformer backbone
→ action 예측 사전학습
Stage B: real-only hybrid fine-tuning
Stage A에서 학습한 backbone을 초기값으로 사용하되, 실제 train 70,000행으로 다시 학습했다.
이때부터는 원래 hybrid 구조처럼 정확한 real metadata를 함께 사용했다.
사전학습된 backbone
+ real transcript
+ real metadata
→ 최종 hybrid 모델
핵심은 민트 메타데이터를 최종 모델에 직접 가르치지 않는 것이었다.
민트는 텍스트와 action 사이의 사전학습 신호로만 사용하고, 최종 decision boundary는 real data로 다시 학습했다.
첫 2단계 구현에도 누수가 있었다
처음 만든 pt74는 strict mint 3,162행을 전부 Stage A에서 학습했다.
겉으로는 text-only 사전학습처럼 보였지만, 다시 검토하자 두 가지 문제가 있었다.
첫 번째 문제: [META] 문자열이 남아 있었다
코드에서는 119차원 metadata vector를 0으로 만들었다.
하지만 transcript를 먼저 생성한 뒤 vector만 0으로 처리했다.
따라서 Transformer가 보는 텍스트에는 여전히 다음 정보가 남아 있었다.
[META] tier=... ci=... dirty=... turn=... open=...
즉, metadata vector는 제거했지만 텍스트 채널로 메타데이터가 계속 들어가고 있었다.
정확한 text-only 사전학습이 아니었다.
두 번째 문제: holdout 세션의 민트가 Stage A에 포함됐다
Stage B의 성능을 검증할 holdout 세션과 같은 세션에서 나온 민트 620행이 Stage A에 들어가 있었다.
Stage B에서는 real holdout row를 직접 학습하지 않았더라도, backbone은 같은 세션의 과거 action label을 이미 본 상태였다.
이는 세션 단위 holdout 원칙에 맞지 않았다.
따라서 최초 pt74의 holdout 결과는 제출 자격 판단에 사용할 수 없었다.
clean pretext 데이터로 다시 시작하다
두 문제를 수정했다.
먼저 Stage A transcript에서 session_meta 자체를 제거했다.
[META] 줄도 샘플별 정보를 담지 않도록 처리했고, 119차원 metadata vector도 모두 0으로 만들었다.
다음으로 holdout 세션에서 나온 민트 620행을 제외했다.
strict mint 전체: 3,162행
holdout 세션 소속: 620행
clean pretext train: 2,542행
이 2,542행으로 Stage A를 학습한 뒤, Stage B에서 real train만 사용해 holdout을 평가했다.
이렇게 만든 모델이 pt74c였다.
검증을 통과한 뒤 배포용 모델을 만들 때는 holdout을 평가할 필요가 없기 때문에,
Stage A에서 strict mint 전체 3,162행을 사용하고 Stage B에서 real train 전체로 refit했다.
v52: 민트 계열에서 처음 살아남은 방식
clean strict-mint text-only 사전학습 모델을 v47 프레임에 넣은 제출이 v52였다.
기준은 같은 seed74를 사용한 기존 kf768 모델이었다.
기존:
민트 데이터를 real train과 직접 섞어 hybrid 학습
v52:
민트 text-only 사전학습
→ real-only hybrid fine-tuning
결과는 다음과 같았다.
v47: 0.7820173794
v52: 0.7828474005
v52는 v47보다 약 0.00083 상승했다.
당시 챔피언 v49의 0.7831843076에는 조금 미치지 못했지만, 민트 처리 방식 중에서는 처음으로 서버에서 살아남은 결과였다.
중요한 것은 민트를 더 많이 넣어서 오른 것이 아니었다.
같은 민트 정보를 쓰되, 불확실한 메타데이터가 최종 모델에 직접 전달되지 않도록 학습 경로를 바꾼 결과였다.
직접 주입:
텍스트 + 불확실한 메타데이터를 동시에 학습
2단계 학습:
민트에서는 텍스트와 action 관계만 학습
최종 상태 판단은 real data로 다시 학습
민트의 양보다 전달 방식이 중요하다는 증거였다.
어떤 메타데이터는 실제로 복원할 수 있었다
2단계 학습을 진행하면서 모든 메타데이터가 복원 불가능한 것은 아니라는 점도 확인했다.
세션 step과 인접 real row를 사용해 상태 전이를 검증한 결과, 일부 필드는 상당 부분 정확하게 복원할 수 있었다.
- user_tier, language_pref, workspace.loc, language_mix: 세션 안에서 불변
- turn_index: 세션 step과 정확히 일치
- last_ci_status: 3,180행 중 3,108행 복원 가능
- git_dirty: 3,016행 복원 가능
- open_files: 2,910행 복원 가능
CI, dirty, open_files가 모두 복원 가능하고 strict 중복도 제거한 교집합은 2,863행이었다.
반면 다음 값은 정확한 중간 상태를 알 수 없었다.
budget_tokens_remaining
elapsed_session_sec
두 값이 세션 안에서 감소하거나 증가한다는 방향은 알 수 있었지만, 누락된 과거 step의 정확한 값을 알 수는 없었다.
따라서 exact-core 데이터에서는 budget과 elapsed를 제외했다.
정확한 메타데이터는 도움이 되는가
이제 질문은 더 구체적으로 바뀌었다.
정확히 복원할 수 있는 메타데이터만 사용하면, text-only pretext보다 좋아지는가?
이를 확인하기 위해 exact-core 2,863행을 만들고 두 방식을 비교했다.
omit
민트 transcript에서 [META] 줄을 완전히 제거했다.
keep
정확하게 복원한 다음 정보만 [META] 줄에 남겼다.
tier
ci
dirty
turn
open_files
두 방식 모두 119차원 metadata vector는 0으로 만들었다.
따라서 차이는 Stage A의 텍스트에 정확한 [META] 정보를 보여주느냐뿐이었다.
다만 최초 서버 비교에서는 문제가 있었다.
blank 3,162행 모델과 keep 2,863행 모델을 비교했기 때문에 처리 방식뿐 아니라 학습 행 수도 달랐다.
따라서 이 차이를 메타데이터의 단독 효과로 해석할 수 없었다.
이후 같은 2,863행, 같은 seed79 계열에서 omit과 keep을 비교했다.
keep: v55 0.782074495
omit: v56 0.7815166923
keep이 약 0.00056 높았다.
다만 v55의 실행 manifest가 현재 기준으로 완전하지 않았기 때문에, 이를 완벽한 matched A/B라고 부르지는 않았다.
정확한 결론은 다음 정도였다.
정확하게 복원된 메타데이터를 Stage A transcript에 남기는 방향은 omit보다 근소하게 우세했지만, 최종 챔피언을 교체할 정도의 상승은 아니었다.
이 구간에서 바뀐 이해
민트에 대한 이해는 여러 번 수정됐다.
처음의 이해
history에서 더 많은 action을 수확하면
학습 데이터가 크게 늘어나 성능이 오른다.
mint2 이후
행을 많이 늘리면 오히려 성능이 내려갈 수 있다.
적당한 양의 mint1이 더 낫다.
rolling history 재검증 이후
mint2의 추가 행은 대부분 새로운 action이 아니라,
같은 target의 rolling-window 변형이었다.
메타데이터 감사 이후
과거 prompt와 action을 복원하는 것과
과거 session state를 복원하는 것은 별개의 문제다.
2단계 학습 이후
민트는 real train에 직접 섞는 데이터라기보다,
텍스트와 action 관계를 먼저 학습시키는
사전학습 데이터로 사용하는 편이 더 안전할 수 있다.
정리
민트 재검증에서 가장 중요했던 것은 데이터를 더 만드는 일이 아니었다.
처음에는 history 안에 많은 과거 action이 있으니, 이를 최대한 많이 학습 데이터로 만들면 된다고 생각했다.
하지만 실제로는 다음 문제들이 겹쳐 있었다.
- rolling history 때문에 같은 target이 여러 prefix로 중복됨
- history 시작 이전의 CI 상태를 알 수 없음
- 과거 action에 미래 source row의 메타데이터가 붙을 수 있음
- 값을 제거하면 zero-sentinel 도메인 표식이 생김
- holdout 세션의 민트가 pretext에 들어가면 세션 누수가 발생함
- metadata vector를 지워도 transcript의 [META]를 통해 정보가 남을 수 있음
결국 mint1이 복원한 3,180개 target은 history에서 얻을 수 있는 신규 step을 거의 전부 포함하고 있었다.
문제는 양이 아니라 정합성이었다.
직접 주입 방식의 v48과 v50은 서버에서 반납했다. 반면 strict 중복을 제거하고, 민트를 text-only로 사전학습한 뒤 real data로 다시 fine-tuning한 v52는 v47 대비 약 0.00083 상승했다.
v47: 0.7820173794
v52: 0.7828474005
챔피언을 바꾸지는 못했지만, 민트 계열에서 처음 서버에 전달된 방식이었다.
이 구간에서 얻은 결론은 다음과 같았다.
데이터 증강에서는 얼마나 많은 행을 만들었는지보다, 각 행이 어느 시점의 정보를 나타내는지와 그 정보가 어떤 경로로 모델에 전달되는지가 더 중요했다.
하지만 이 과정에서도 holdout 누수, 설정 기록 누락, 잘못된 변수 분리 문제가 계속 발견됐다.
실험이 복잡해질수록 사람이 매번 모든 조건을 확인하는 방식에는 한계가 있었다.
다음 글에서는 이 문제를 해결하기 위해 실험 기록, 실행 권한, 산출물 검증, 독립 감사를 하나의 흐름으로 묶은 실험 하네스를 재설계한 과정을 정리할 예정이다.
'고민의 흔적.. > AI Agent 행동예측 해커톤' 카테고리의 다른 글
| (연구일지 #14 조합 성능 > 단독 성능)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.25 |
|---|---|
| (연구일지 #13 자동화 하네스 구축)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.21 |
| (연구일지 #11 앙상블은 정말 필요한가?)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.21 |
| (연구일지 #10 이전 작업 부검하기)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.11 |
| (연구일지 #9 데이터 증강: 민트)Dacon AI Agent 행동 예측 대회 (0) | 2026.07.11 |