수민 '-'

플오그래밍

제가 작성하는 모든 글은 절대 상업적인 이용이 아니며, 그저 개인적인 공부 용도로만 사용하는 것임을 밝힙니다.

PANDA 챌린지 1등 솔루션 - Team PND, 라벨 클리닝·타일 앙상블

Prostate cANcer graDe Assessment (PANDA) 는 전립선 생검 WSI에서 Gleason grading을 자동화하는 Kaggle 대회다. 참가자는 슬라이드 전체에 대한 ISUP grade(0~5) 를 예측하고, 성능은 Quadratic Weighted Kappa(QWK) 로 평가했다.

이번 글은 대회 개요와 함께, 1등팀 Team PND가 공개한 Google Slides(Rist Kaggle Workshop, 2020-09-24, 발표: fam_taro) · Discussion write-up · GitHub를 함께 읽고 “어떻게 이 문제를 풀었는지” 를 정리한다.


1. PANDA 챌린지가 무엇인가

대회 목표

  • 병리 슬라이드(WSI) 한 장에서 전립선암 유무ISUP grade group을 예측
  • Gleason pattern(3, 4, 5) 조합 → Gleason score → ISUP 1~5로 환산하는 임상 워크플로를 AI로 보조하는 것이 목표
  • PANDA Challenge (Kaggle)

데이터·규모 (요약)

항목 내용
이미지 최대 약 1만 장 규모의 H&E 염색 생검 WSI (radboud, karolinska 등 다기관)
라벨 슬라이드 단위 ISUP grade (암 없음 = 0)
특징 TMA가 아니라 실제 진단용 생검 전체 슬라이드
검증 대회 후 Nature Medicine 등에서 다기관 외부 검증 수행
WSI 해상도 level 0·1·2 피라미드 (대략 16× / 4× / 1× 다운샘플). 많은 참가자가 level 1 사용
테스트 약 940장, Public:Private ≈ 42:58 (Public 약 395, Private 약 545)

라벨·주석자 이슈 (슬라이드 강조)

슬라이드는 공식 문서의 주석 체계를 근거로 radboud train 라벨 노이즈를 반복해서 짚는다.

  Karolinska Radboud
Train 전문의 1명 진단 리포트 기반 학생 판독
Test 전문의 3명 전문의 3명

Radboud에서 학생이 test를 맞춘 경우 Acc 0.720, QWK 0.853 수준이라, train 라벨이 test 기준보다 불안정할 수 있다는 해석이 PND 접근(기관별 denoise)의 배경이 된다.

또 train에 동일 생검·다른 슬라이스 중복이 500~1,500장 추정(discussion)된다. 호스트는 train에만 해당한다고 했으나 중복 ID 목록은 제공하지 않았다.

Grand Challenge PANDAGoogle Research 블로그에도 동일 취지가 정리되어 있다. 2020년 4~7월 Kaggle에서 진행되었고, 1,010팀 규모의 code competition이었다 (GPU 제출 6시간, 인터넷·커스텀 패키지 불가).

평가 지표: Quadratic Weighted Kappa

ISUP grade는 순서형(ordinal) 라벨이다. 단순 accuracy보다, 틀린 정도에 가중을 두는 QWK가 메인 지표로 쓰였다.

  • 예: 2등급을 3으로 예측 vs 2를 5로 예측 → 후자가 더 큰 패널티
  • 대회 초반 10일 안에 일부 팀이 내부 검증에서 pathologist 수준(κ 약 0.90 근처)에 도달했다는 보고가 있다 (Nature Medicine 논문)

약한 지도 학습(Weak supervision)

픽셀 단위 Gleason 마스크 없이 슬라이드 라벨만으로 학습하는 방식이 상위권 공통 패턴이다.

  1. WSI에서 조직이 많은 타일(패치) 를 여러 장 샘플링
  2. CNN으로 타일 특징 추출
  3. concat / pooling 으로 슬라이드 단위 표현을 만든 뒤 ISUP grade 분류

Nature Medicine 논문에서는 이를 “concatenate tile pooling”류 접근으로 묶어 설명한다. PANDA 1등 코드도 같은 계열이다.


2. Team PND 1등 결과와 공개 자료

Team PND 멤버: arutema47, fam_taro, poteman (GitHub: @yukkyo, @kentaroy47). 슬라이드 기준 대회 중 Public 22위(0.910) → 최종 Private 1위(0.940).

구분 링크·내용
대회 Prostate Cancer Grade Assessment
Write-up 1st Place Solution [PND] (Discussion #169143)
슬라이드 RistKaggleWorkshop_20200924_PANDA_1st
코드 kentaroy47/Kaggle-PANDA-1st-place-solution

단계별 점수 (슬라이드 “Our solution: Summary”)

단계 Public Private
Model 0 (노이즈 제거용 B1, 원본 라벨) 0.882 0.936
Model 1 (arutema, B0, 클린 라벨) 0.900 0.934
Model 1 + Model 2 앙상블 (최종) 0.904 0.940

README 재현 노트: 시드 고정 시 private 0.939, 라벨 클리닝만 적용한 5-fold 단순 모델은 private 0.935(3위 수준).

코드는 CC-BY-NC 4.0이며, 이후 Nature Medicine 등 후속 논문에서 인용되었다.

TOP 4 Solution (Kaggle Discussion)

이 글은 1등(PND) 파이프라인을 중심으로 정리했지만, 2~4등 write-up도 같은 대회 맥락에서 함께 보면 좋다. PND 슬라이드에서는 shake-up이 작았던 상위팀(2, 4, 6, 11위 등)이 라벨 노이즈 제거에 공을 들였다고 짚는다.

순위 팀(제목 기준) Discussion
1st PND 1st Place Solution [PND]
2nd Save the Prostate 2nd Place Solution [Save the Prostate]
3rd (write-up 제목: 3rd place solution) 3rd place solution
4th NS Pathology 4th place solution [NS Pathology]

2~4등 세부 아키텍처·점수는 각 Discussion 본문을 기준으로 확인하면 된다. (Kaggle 페이지는 로그인 후 읽는 것이 안전하다.)


3. 1등 파이프라인 한눈에 보기

GitHub README의 재현 순서는 대략 “노이즈 라벨 제거 → 깨끗한 라벨로 재학습 → 두 종류 모델 앙상블” 이다.

핵심은 Model 0으로 라벨 클리닝 → Model 1·2만 최종 추론에 쓰는 2단계 구조다. 슬라이드는 총 3개 모델만 만들었다고 명시한다.

슬라이드 5단계 요약:

  1. imghash 유사도 ≥ 0.9image_id는 같은 fold에 배치 (networkx로 그룹핑, imagehash 노트)
  2. 원본(노이즈 포함) 라벨로 학습
  3. OOF 예측 vs 원본 라벨 gap이 큰 샘플 제거
  4. 클린 라벨로 재학습
  5. Model 1 + Model 2 앙상블

3-1. 상위권 공통 베이스라인 (슬라이드 “Basic approach”)

많은 팀이 참고한 public kernel:

타일링 요지:

  • (tile_size, tile_size) 격자 분할 후, 타일별 픽셀 합으로 조직 많은 순 정렬
  • 예: tile 256 × 36타일 → 입력 약 1536×1536
  • bin label: ISUP 2 → [1,1,0,0,0], ISUP 4 → [1,1,1,1,0] (순서형을 multi-hot으로)
  • TTA: tile mode, h/v flip, transpose

공개 LB 0.87대를 노렸으나 재현이 어려웠다는 코멘트도 슬라이드에 있다.


3-2. Local CV는 높은데 Public LB는 낮았던 이유

슬라이드 제목 그대로 “Why Local CV ≠ Public LB?” 가 PND 설계의 출발점이다.

요인 설명
중복 이미지 같은 생검이 다른 fold에 들어가면 CV가 과대평가
라벨 노이즈 Radboud CV 1.0에 가까워도 Public LB는 radboud에서 ~0.85 수준 보고
작은 test set Public/Private 모두 수백 장 → QWK 변동 큼
shake-up Private가 Public보다 점수가 높게 나온 팀 다수

PND 가설: denoise가 (1) 잘못된 라벨(2) 어려운 샘플을 함께 제거 → 쉬운 예제에 강하고 어려운 예제에 약한 모델 → Private(상대적으로 easy 비중↑)에서 유리했을 수 있다.

상위권(2, 4, 6, 11위 등)은 대체로 noise reduction을 했다고 슬라이드에서 짚는다.


4. 데이터 준비: 중복 슬라이드·K-fold·타일 PNG

4.1 중복 이미지 그룹핑 (선택)

  • perceptual hash로 유사 슬라이드를 묶는 스크립트: imagehash grouping 노트
  • 저장 예: input/duplicate_imgids_imghash_thres_090.csv
  • README에서는 input에 결과를 넣어 두고 스킵 가능하다고 안내

4.2 5-fold 분할

cd src
python data_process/s00_make_k_fold.py
  • 고정 seed → input/train-5kfold.csv

4.3 학습용 타일 PNG 생성

python data_process/s07_simple_tile.py --mode 0
python data_process/s07_simple_tile.py --mode 2
python data_process/a00_save_tiles.py
설정 값 (README)
타일 개수 64 (8×8 그리드)
타일 크기 192 px
resolution level 1
mode 0과 2 두 가지 (패딩/오프셋 방식 차이)

출력 디렉터리 예: numtile-64-tilesize-192-res-1-mode-0, ...-mode-2

제출 노트북의 get_tiles는 WSI에서 배경(흰색)을 제외하고 조직이 많은 타일을 고르는 로직이다. 패딩·mode에 따라 타일 시작 위치가 달라져, 같은 슬라이드도 서로 다른 뷰를 학습에 넣을 수 있다.

# submitted_notebook.ipynb 요지
def get_tiles(img, tile_size, n_tiles, mode=0):
    pad_h = (tile_size - h % tile_size) % tile_size + ((tile_size * mode) // 2)
    # ... 타일 분할 후 픽셀 합이 작은(조직 많은) 순으로 n_tiles 선택

실습에서 tifffile/OpenSlide로 패치를 직접 잘라 본 경험과 연결하면, 대회 1등팀은 오프라인에서 PNG 타일을 미리 뽑아 두고 학습 속도를 올린 구조라고 보면 된다.


5. 1단계: Base 모델로 “라벨 노이즈” 찾기

5.1 Base 학습 (final_1, EfficientNet-B1)

python train.py --config configs/final_1.yaml --kfold 1  # 1~5
  • fold당 약 18시간 (Titan RTX 1장 기준)
  • 출력: output/model/final_1/

5.2 Hold-out 예측

python kernel.py --kfold 1  # 1~5
  • 각 fold 검증셋 예측 → local_preds~~~.csv

5.3 노이즈 제거 스크립트

python data_process/s12_remove_noise_by_local_preds.py

생성 CSV 예:

파일 용도
local_preds_final_1_efficientnet-b1.csv hold-out 전체 예측
..._removed_noise_thresh_16.csv Model 1 학습용 (기본 클리닝)
..._removed_noise_thresh_rad_13_08_ka_15_10.csv Model 2 학습용 (radboud 라벨 추가 제거)

최종 제출에 쓴 fold CSV 예: train-5kfold_remove_noisy_by_0622_rad_13_08_ka_15_10.csv

라벨 클리닝 규칙 (슬라이드 수치)

OOF 예측 ISUP와 원본 라벨 차이(gap)가 threshold를 넘으면 제거.

  • 예: 예측 4.1 vs 라벨 4 → gap 0.1 → 유지 / 예측 0.5 vs 라벨 4 → gap 3.5 → 제거
용도 gap threshold 제거 비율 제거 수 (Total / Rad / Ka)
Model 1 (arutema) 1.6 5.6% 596 / 445 / 151
Model 2 (fam_taro) 기관·등급별 (radboud 약 20% 목표) 14.0% 1,488 / 1,153 / 335

Ablation (Model 2 계열): denoise 전 Public 0.892 / Private 0.916 → denoise 후 0.901(+0.009) / 0.932(+0.016).


6. 2단계: 클린 라벨로 두 모델 재학습

Model 0 — fam_taro (노이즈 탐지 전용, 슬라이드 Model 0)

  • EfficientNet-B1, 원본 라벨, 5-fold
  • OOF 예측만으로 클린 CSV 생성 (최종 제출에는 미사용)

Model 2 — fam_taro (EfficientNet-B1, GeM pooling)

python train.py --config configs/final_2.yaml --kfold 1
python train.py --config configs/final_2.yaml --kfold 4
python train.py --config configs/final_2.yaml --kfold 5
  • fold 1, 4, 5만 최종 inference에 사용 (LB 성능 좋은 fold 선별)
  • fold당 약 15시간
  • 제출 노트: final_2_efficientnet-b1_kfold_{k}_latest.pt
  • 타일: 64 × 192 × 192 (입력 약 1536×1536), GeM pooling, cosine 30 epoch
  • 학습 시 ISUP + 첫 번째 Gleason 점수 동시 예측 → 출력 10차원 (예: 3+4 → 1st gleason 3). 추론에는 ISUP만 사용
  • Public LB 상위 fold 3개만 학습·추론 (제출 시간 제한)

Model 1 — arutema47 (EfficientNet-B0, avgpool, tile 36)

  • train_famdata-kfolds.ipynb (또는 .py)
  • 타일 36 × 256 × 256, avg pooling, bin label, mixup·cutout, cosine 20 epoch
  • 더 큰 backbone(ResNeXt 등)은 overfitting으로 기각
  • 가중치 예: efficientnet-b0famlabelsmodelsub_avgpool_tile36_imsize256_mixup_final_epoch20_fold*.pth

두 모델은 백본·타일 수·pooling·보조 타깃이 달라 오류 상관이 낮은 앙상블을 노린 구성이다.


7. 추론·앙상블 (Kaggle Notebook)

submitted_notebook.ipynb 흐름 요약:

  1. 테스트 WSI마다 OpenSlide로 타일 추출 (get_tilesconcat_tiles로 8×8 몽타주)
  2. Model 2 (EfficientNet-B1, GeM) 예측
  3. Model 1 5-fold EfficientNet-B0 예측 평균/결합
  4. 기관별 처리·pseudo label threshold 등 Config로 미세 조정
  5. 최종 ISUP grade 제출

로컬 검증 시 karolinska / radboud를 나눠 QWK를 출력하는 코드도 포함되어 있다 (기관별 성능 갭 확인용).

재현용 커널 예:

학습된 가중치는 repo의 ./final_models에 포함되어 있다.


8. 슬라이드 “Why did we win?”·결론

Google Slides 결론 파트 요약:

  1. Private test가 Public보다 easy 비율이 높았을 가능성 — denoise 후 easy sample에 강한 모델이 유리
  2. imghash 기반 fold — 중복 슬라이드가 fold를 넘나들지 않게 해 CV·denoise 안정화. seed만 바꿔도 점수가 크게 흔들리는 팀이 있었던 이유로, random split leakage를 지적
  3. 공식 문서의 주석자(annotator) 정의를 읽는 것이 의료 영상 과제에서 중요 — “Train annotator가 Test를 얼마나 맞추는지”를 보면 라벨 신뢰도를 가늠할 수 있다

팀 회고(슬라이드): CV와 LB가 불안정할 때 라벨 정의를 더 일찍 의심했어야 했다, PyTorch Lightning seed 설정 미숙지, imghash clustering(17위 솔루션)도 유용해 보였다.

효과 없었던 시도 (Appendix)

  • denoise 전 Mixup/CutMix만으로는 한계
  • NMS·K-means 기반 타일링 등 다른 타일 방식
  • Karolinska ↔ Radboud CycleGAN
  • segmentation head 부착 분류 (FP32 이슈)
  • Class balanced loss, first-gleason loss weight 0.5
  • CleanLab — 당시(2020.09) 분류 라벨만 지원, denoise에는 참고만

Discussion #169143은 GitHub README가 공식 write-up으로 연결하는 스레드다.


9. 재현 시 체크리스트 (GitHub README 기준)

  1. Docker 권장: docker/build.shrun.shexec.sh
  2. train_images, train_masks 다운로드
  3. (선택) imagehash 중복 그룹 / train-5kfold.csv
  4. 타일 PNG 생성 (mode 0, 2)
  5. final_1 5-fold 학습 → kernel.pys12_remove_noise
  6. final_2 (fold 1,4,5) + train_famdata-kfolds
  7. Kaggle Dataset에 가중치 업로드 후 submitted_notebook.ipynb 경로 수정

일부 중간 산출물은 input/에 이미 있어 스킵 가능하다고 README에 명시되어 있다.


마치며

  • PANDA는 전립선 생검 WSI에서 ISUP grade를 맞추는 대규모 병리 AI 챌린지이며, QWK와 weak supervision이 표준 설정이다.
  • Team PND 1등은 타일 기반 CNN + 라벨 클리닝 2단계 + 이종 EfficientNet 앙상블로 private 0.940을 기록했다.
  • 실무적으로는 OpenSlide EDA로 기관·등급 분포를 본 뒤, 타일 샘플링·fold·노이즈 라벨 전략까지 연결하는 흐름이 자연스럽다.
  • 워크샵 슬라이드는 README보다 Local CV vs LB, annotator, denoise 수치·ablation 설명이 풍부하다.
  • 다음에는 s07_simple_tile / get_tiles 로직을 직접 따라 하거나, 클린 라벨 CSV만 바꿔 소형 EfficientNet baseline을 돌려 보는 것도 좋다.

참고

대회·1등 자료

TOP Solution (Discussion)

대회·연구 배경