Post

RSNA 무릎 ② — 베낄 것도 데이터도 라벨도 바닥났을 때: 천장 세 개를 재고 0.945를 얻었다

RSNA 무릎 ② — 베낄 것도 데이터도 라벨도 바닥났을 때: 천장 세 개를 재고 0.945를 얻었다

①편은 “왜 0.944에서 멈췄는가”로 끝났습니다. 답은 판독문 라벨의 천장(0.9032) 이었고, 제가 만지던 축들이 전부 그 아래였다는 것이었습니다. 이번 글은 그다음 하루입니다. 점수가 몇 주 만에 움직였고(0.944 → 0.945), 남은 길이 어디인지도 숫자로 좁혀졌습니다.

대회 진행 중이라 코드와 대회 이미지는 싣지 않습니다. ‘실측’은 다른 조건을 그대로 두고 한 가지만 바꾼 비교로 확인한 값이고, 나머지는 ‘추정’이라고 적었습니다.

0. 한 장 요약

 아침저녁
최고 점수0.9440.945
1위(0.961)까지0.0170.016
우리 모델의 총 기여+0.003 (으로 알고 있었다)+0.006 (실측 분해)
측정 바닥 위의 모델링 신호없음+0.0119 (깊이 커버리지)

하루에 세 개의 천장을 쟀습니다. 셋 다 낮았습니다. 그런데 그 과정에서 내가 틀리게 알고 있던 숫자 하나와 아직 안 본 구조적 결함 하나가 나왔고, 그 둘이 다음 20일의 방향을 정했습니다.


1. 천장 ①: 공개 상위 모델을 베끼면 얼마인가 → +0.001

우리 모델은 정답 58건에서 0.91대입니다. 공개 파이프라인의 한 계열은 0.9305입니다. 그러니 “그들의 레시피를 복제하면 되지 않나”가 자연스러운 생각이었고, 실제로 그 레시피는 추론 영수증에 그대로 적혀 있습니다 — 96슬라이스 스택, 384 해상도, 물리 FOV 130mm, 5슬롯 배치, 정답 58건 상위 3에폭 선택.

그런데 공개 저자 본인의 변경 기록이 그 값을 이미 재놓았습니다.

v9: residual/D4 가중치 0.50/0.50 → 0.25/0.25, 여기에 Global96/Repair-v1을 0/0 → 0.25/0.25 — v7 = 0.942 → v9 = 0.943

정답 58건에서 0.9305와 0.9104를 내는 모델 둘을 통째로 집어넣고 LB가 +0.001 움직였습니다. 이것이 “공개 소스에서 가져올 수 있는 것”의 천장입니다. 복제 계획은 여기서 접었습니다. 레시피 대조표를 만드는 데 든 시간은 아깝지 않았습니다 — 뒤에 나올 ④가 바로 그 대조에서 나왔기 때문입니다.

2. 천장 ②: 대회가 준 메타데이터에 남은 것 → +0.0097 (그것도 오라클로)

우리가 모델에 넣는 메타는 7차원뿐입니다. 평면, 지방억제, 슬라이스 수, 두께 정도. 그러니 안 쓰는 것이 많아 보였습니다 — TR/TE, 대비 유형, 성별, 자기장, 좌우, 코일, 기하.

전수로 집계하고, GPU 없이 천장을 쟀습니다. 방법은 cross-fit 스태커입니다: 기존 모델의 OOF 로짓에 메타를 붙여 소견별 회귀를 7개 fold로 적합하고 남은 fold로 예측해, 누수 없이 “메타가 점수를 얼마나 고칠 수 있는가”를 잽니다.

피처군차원교차적합 Δ오라클 Δ (과적합 포함 상한)
기하 8종8+0.0021+0.0032
슬롯별 대비 유형20+0.0017+0.0035
TR/TE10+0.0016+0.0032
성별4+0.0006+0.0012
좌/우2−0.0001−0.0000
전부57+0.0011+0.0097

측정 바닥이 ±0.0043입니다. 라벨당 57개의 자유 파라미터를 같은 데이터에서 적합하고 같은 데이터에서 평가해도 +0.0097이 한계라면, 이 방향에 실험을 돌릴 이유가 없습니다.

그리고 존재하지 않는 것들도 확정됐습니다. 나이·기관·검사일은 채움률이 0% 입니다(익명화). 환자자세는 상수입니다. 환자 ID는 4,407건이 전부 고유해서 — 반복 환자가 없으니 누수도 없지만 신호도 없습니다. 대회가 준 세 필드 중 하나는 다른 필드와 24,371행 전부 동일해서 정보가 0이었습니다.

이 측정의 범위를 정직하게 적습니다. 이것은 “메타를 점수 보정으로 쓸 때”의 천장이지 “메타를 모델 입력으로 넣어 특징 추출 자체를 바꿀 때”의 천장이 아닙니다. 이 구분이 ④에서 중요해집니다.

3. 천장 ③: 공개 라벨셋으로 바꾸면 → 우리가 이기거나 동률

①편에서 판독문 라벨의 천장이 0.9032라고 적었습니다. 그렇다면 다른 팀의 LLM 라벨은 더 나을까요. 공개된 라벨셋 네 종(파일 일곱 개)을 받아 정답 58건에 대조했습니다.

라벨셋정답 58건 macro
우리 약라벨0.9055
공개 A (blend)0.8927
공개 A (v2)0.8873
공개 A (full)0.8780
공개 B0.8700
공개 C (최신 상용 모델)0.8352

우리 우위 0.0128은 n=58의 잡음 안이라(이 데이터에서 승자의 저주 +0.0107) “더 낫다”가 아니라 “더 나쁘지 않다” 가 정확합니다. 다만 공개 C의 −0.0703은 잡음 밖이라 확실히 열위입니다 — 모델이 새롭다고 라벨이 좋아지지 않습니다.

더 중요한 것은 분포입니다. 서로 독립인 다섯 팀의 LLM 라벨이 전부 0.835~0.893에 몰려 있고, 이것은 “제3자가 같은 판독문을 다시 읽었을 때”의 실측 0.8348과 같은 자리입니다. 4,407건 전체의 불일치를 봐도 정보 차이가 아니라 보정 차이였습니다 — 어떤 셋은 우리가 음성으로 본 셀 3,968개를 양성이라 하고, 다른 셋은 우리가 양성으로 본 셀 5,496개를 음성이라 합니다. 같은 텍스트를 다르게 자를 뿐입니다.

판독문 경로는 커뮤니티 전체가 같은 벽에 부딪혀 있습니다. 상위권은 더 좋은 판독문 라벨로 이기는 것이 아닙니다.


4. 그런데 점수는 움직였다 — 기반을 갈아탔다

세 천장이 전부 낮게 나온 날, 실제로 점수를 올린 것은 기반 노트북 교체였습니다. 우리는 공개 계보의 한 판본 위에 우리 모델을 얹어 쓰고 있었는데, 그보다 한 단계 위 판본으로 갈아탔습니다. 결과는 0.944 → 0.945.

작아 보이지만 두 가지가 의미 있습니다. 첫째, 몇 주 만의 첫 상승입니다. 둘째, ①에서 읽은 +0.001과 정확히 일치합니다 — 공개 저자가 자기 변경으로 잰 값과 우리가 얻은 값이 같았습니다. 예측이 맞은 드문 사례라, 그 수치를 앞으로의 계획 근거로 쓸 수 있게 됐습니다.


5. 내가 틀리게 알고 있던 숫자 — 우리 기여는 +0.003이 아니라 +0.006

여기가 오늘의 핵심입니다.

우리 모델은 파이프라인에 두 번 들어갑니다. 한 번은 공개 분기 내부에 섞이고, 또 한 번은 최종 순위 융합에서 묶음 가중치로 들어갑니다. 저는 몇 주 동안 “우리 모델 기여는 +0.003”이라고 말해왔는데, 그것은 묶음 경로만 끄고 잰 값이었습니다.

두 경로를 모두 끈 제출을 내보니 답이 나왔습니다.

1
2
3
4
5
6
7
0.944   전부 켬
0.941   묶음 경로만 끔
0.938   두 경로 모두 끔  ← 진짜 공개 단독

총 기여   0.944 − 0.938 = +0.006
  묶음      0.944 − 0.941 = +0.003
  내부      0.941 − 0.938 = +0.003

두 배였습니다. 그리고 이것이 다른 결과의 해석도 바꿉니다. 같은 날 내부 경로의 혼합 비율을 0.25 / 0.40 / 0.55로 찍었는데 세 점이 전부 0.944로 완전한 평지였습니다. 저는 처음에 “이 경로는 쓸모없다”고 읽었는데, 분해를 보고 나니 정반대입니다 — 그 경로는 +0.003을 내고 있고, 이미 최적 근처라 평평한 것이었습니다.

교훈: 기여를 잴 때는 모델이 들어가는 모든 경로를 세야 한다. 한 경로만 끄고 “총 기여”라고 부르면 절반을 놓칩니다. 그리고 절반을 놓치면 “우리 모델을 개선해봐야 소용없다”는 잘못된 결론으로 갑니다 — 실제로는 모델이 좋아지면 두 경로가 함께 좋아집니다.


6. 아직 안 본 구조적 결함 — 깊이의 41.9% 만 보고 있었다

1절에서 만든 레시피 대조표가 여기서 값을 했습니다. 공개 상위 계열과 우리의 입력 구성을 나란히 놓으니 한 칸이 눈에 띄었습니다 — 깊이 커버리지.

우리는 한 시리즈에서 12장을 뽑고, 각 장을 인접 슬라이스 3장으로 2.5D 입력을 만듭니다. 그런데 12장의 중심은 볼륨 전체에 균등히 퍼지는데 삼중창의 두께는 고정이었습니다. 그러면 볼륨이 깊을수록 틈이 벌어집니다.

원본 슬라이스 수중심 간격실제 커버리지
201.7100%
30 (중앙값)2.6100%
48 (p95)4.370%
968.635%
144 (축상 최대)13.023%

평균이 41.9% 입니다. 볼륨의 절반 이상을 아예 보지 않고 있었습니다.

그럼 이게 실제로 점수를 가르는가. 기존 모델의 OOF에서 “우리 12장이 촘촘히 덮은 study”와 “성기게 덮은 study”를 나눠 보면 전자가 0.0103 높습니다. 그런데 간격은 촬영 프로토콜이고 프로토콜은 곧 병원이라, 이건 사이트 지름길일 수 있습니다. 그래서 사이트마다 자기 중앙값으로 가르고 사이트별 차이를 가중 평균했습니다.

1
2
3
전역 (교란 포함)      촘촘 − 성김 = +0.0103
사이트 내부 (교란 제거) 촘촘 − 성김 = +0.0119   ← 오히려 커졌다
촘촘이 이긴 사이트      16 / 23

교란이 아니었습니다. 같은 스캐너·같은 프로토콜 안에서도, 우리 12장이 볼륨을 촘촘히 덮은 쪽이 확실히 높습니다. 측정 바닥(±0.0043)의 2.8배로, 이 프로젝트에서 몇 주 만에 처음 보는 바닥 위 신호입니다.

고치는 데 든 것: 한 줄

레시피 대조에서 참고한 공개 구현은 입력 프레임을 통째로 다시 짜는 방식이었고, 저도 처음엔 그 규모를 예상했습니다. 그런데 우리 코드를 읽어 보니 삼중창의 간격이 이미 설정 키로 있었습니다. 다만 값이 1로 고정돼 있었을 뿐입니다.

그래서 그 키에 “적응” 모드를 하나 추가했습니다 — 중심 간격의 절반으로 잡으면 연속한 삼중창이 빈틈없이 이어집니다. 위 표의 모든 깊이에서 커버리지가 100%가 됩니다.

이 수정의 성질이 측정과 정확히 맞물립니다:

  • 이미 촘촘한 얕은 볼륨은 동작이 전혀 안 바뀝니다 (간격이 1 그대로).
  • 성긴 study만 촘촘해집니다. 관찰이 가리킨 바로 그 방향입니다.
  • 비용이 늘지 않습니다. 표본 수는 그대로이고 각 표본이 보는 두께만 넓어집니다.

마지막 항목이 중요합니다. 이전에 “슬라이스를 12 → 24로 늘리는” 축을 닫은 적이 있는데(+0.0013, 바닥 미만), 그것은 표본을 2배로 뽑되 각 표본의 두께는 그대로여서 비용이 2배였습니다. 이번은 표본 수를 유지한 채 두께를 넓히는 것이라 기구도 비용 부호도 반대입니다. 같은 “깊이” 축처럼 보여도 다른 실험입니다.

쓰기 전에 서빙 경로부터 봤다

20 GPU-시간을 태우기 전에 한 가지를 확인했습니다 — 추론이 이 간격을 어디서 읽는가. 다행히 체크포인트 설정에서 읽고 있어서, 새 설정으로 학습한 모델은 서빙에서도 자동으로 적응 모드가 됩니다.

그런데 그 값을 첫 번째 체크포인트에서만 읽고 있었습니다. 동질성 검사는 다른 키에만 걸려 있었고요. 즉 기존 모델과 새 모델을 한 앙상블에 섞으면 정렬상 첫 모델의 간격으로 전부 입력이 만들어지고, 나머지 절반은 학습과 다른 것을 먹습니다 — 예외 없이 점수만 깎이는 종류의 사고입니다. 공교롭게도 코드 자신이 다른 키에 대해 똑같은 경고를 적어 두었더군요. 그 검사에 간격도 넣었습니다. 섞이면 첫 study 전에 죽습니다. 조용한 손실보다 낫습니다.


7. 하루에 두 번, 슬롯을 잃을 뻔했다

점수와 무관해 보이지만 실제로는 가장 비싼 부분입니다. 제출은 하루 다섯 장뿐이고, 빈 슬롯은 그 자체로 손실입니다.

첫 번째. 데이터셋을 갱신하면서 폴더 업로드 옵션을 빠뜨렸더니, 로그에 “폴더를 건너뜁니다”가 조용히 찍히고 파일 하나만 올라간 버전이 만들어졌습니다. 원격 파일 목록을 세어 보니 가중치가 0개였습니다. 그대로 뒀으면 0.945를 낸 설정이 다음 실행부터 죽었을 것입니다. 다행히 커널 자동 푸시 감시에 “원격 가중치 개수”를 선행 조건으로 걸어둬서, 깨진 데이터셋 위에 아무것도 올리지 않았습니다.

두 번째. 변형 커널 일곱 개를 만들었다고 생각했는데, 실제로는 한 개도 안 만들어졌습니다. 내려받은 메타데이터에 숫자 커널 ID가 들어 있었고, 플랫폼은 사람이 읽는 이름보다 그 숫자를 우선합니다. 그래서 일곱 번의 푸시가 전부 같은 커널 하나의 이름만 바꿔가며 덮어썼습니다. 0.945를 낸 커널까지 그 과정에서 사라졌습니다(점수 기록은 남습니다).

조기 단서는 하나뿐이었습니다 — 슬러그가 다른데도 버전 번호가 4→5→6→7→8→9→10으로 연속 증가한 것. “성공적으로 푸시됨”은 아무것도 보장하지 않았고, 목록을 세어 보고서야 드러났습니다.

여기서 검증 규칙을 세 겹으로 정리했습니다.

  1. 무엇이 적재됐나 — 로그의 파일 이름을 센다. 개수가 아니라.
  2. 설정값이 실제로 바뀌었나 — 영수증을 본다. 로그는 하드코딩된 값을 찍은 전례가 있다.
  3. 영수증이 노출하지 않는 값 — 푸시한 소스를 본다.

실제로 이 규칙이 바로 다음 장을 지켰습니다. 복원한 커널 하나에서 제가 표식을 빠뜨리는 바람에 로그가 “모델 6개”로만 찍혔는데, 개수만으로는 기존 모델과 구별이 안 됐습니다. 파일 이름을 세어 보고서야 의도한 가중치가 실렸음을 확인했습니다. 아니었다면 기존 제출과 글자 하나 다르지 않은 제출로 슬롯 한 장을 날렸을 것입니다.


8. 지금 서 있는 자리

1
2
3
4
최고 0.945 · 1위 0.961 · 격차 0.016
닫힌 천장   공개 복제 +0.001 · 메타 전부 +0.0097(오라클) · 판독문 라벨 0.9032
살아 있는 것 깊이 커버리지 41.9% → 100% (사이트 내부 +0.0119)
확정된 것   우리 모델의 총 기여 +0.006 — 모델이 좋아지면 두 경로가 함께 좋아진다

오늘의 성과는 0.001이 아니라, 다음에 어디를 팔지가 숫자로 좁혀진 것이라고 생각합니다. 베낄 것도, 데이터에 남은 것도, 라벨에 남은 것도 각각 천장이 측정됐고, 그 셋이 전부 닫히고 나서야 우리가 입력을 절반만 보고 있었다는 사실이 눈에 들어왔습니다. 세 천장을 재지 않았으면 못 봤을 것입니다.

전망은 정직하게 적습니다. 깊이 커버리지 수정의 OOF 효과가 그대로 전이되지는 않습니다 — OOF에서 LB로 가는 길은 손실이 큽니다. 낙관적으로 잡아도 0.948~0.951 정도이고, 1위까지는 여전히 멉니다. 다만 몇 주 만에 처음으로 기구가 설명되는 경로가 생겼고, 남은 3주에 돌릴 것이 있습니다.

다음 글에서는 이 수정의 4-fold 판정 결과를 적겠습니다. 이기든 지든 그대로 쓰겠습니다.

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