サポートベクター回顧

プログラミングとか機械学習とか画像処理などに興味がある人のブログというかメモというか。アウトプット苦手なので頑張ります。

Kaggle振り返り🥇:RSNA Screening Mammography Breast Cancer Detection

Kaggleコンペ「RSNA Screening Mammography Breast Cancer」に参加し、public 87th → private 10thでソロ金でした!

www.kaggle.com

まさか金メダルを取れるとは思いもしてなかったのですごくうれしかったです。銅圏から金圏までshakeupしましたし、棚ぼた感は否めないですが、、

参加体験記を書いていこうと思います。 KaggleのDiscussionの方にも解法は載せたので(https://www.kaggle.com/competitions/rsna-breast-cancer-detection/discussion/391378)、この記事は感想・考察・反省点を散りばめながら書いていきます。

コンペの概要

マンモグラフィ検査の画像(乳房のX線撮影画像)から、乳がんを予測するコンペです。診断よりはスクリーニング目的。データは医療画像ということもあって不均衡データ(負例が2%程度)ですが、データ数は5万件ほどありました。訓練専用の臨床情報のテーブルデータもありましたが、近年では珍しいシンプルな画像2値分類タスクでした。

患者さんの乳房ごと(左胸・右胸)に乳がんかどうかを予測するという目的です(ひとりの患者さんに対して左胸にがんがあるかどうか、右胸にがんがあるかどうか、を予測する感じ)。そのため、一人の患者さんに対して複数の画像がありましたし、乳房ごとにも撮影方向が違う複数の画像がありました。

評価指標はpF1というF1スコアの確率版です。

 pF_1 = 2\frac{pPrecision \cdot pRecall}{pPrecision+pRecall}

where:

 pPrecision = \frac{pTP}{pTP+pFP}

 pRecall = \frac{pTP}{TP+FN}

ただ、不均衡ということもあってこの評価指標はだいぶ不安定だったようです。確率に対する評価指標なのに閾値を用いてバイナリ化することで精度が大きく上がってしまうというtipがコンペ序盤に見つかり、参加者のほとんどがバイナリ化していました。評価指標はAUCの方が良かったんじゃないかなと私は思いましたし、Discussionにおいても多くの人がこの問題に言及していました。

Solution

概要

他の方は多入力モデルが多い中、私が構築したモデルはシンプルな単一入力モデルで、パイプラインもシンプルでした。最終的に提出したモデルは、以下の3つのモデルの単純平均アンサンブルです。(モデル名はtimmから)

  • tf_efficientnetv2_s (no aux loss)
  • tf_efficientnetv2_s (using aux loss)
  • maxvit_tiny_tf_384.in1k (using aux loss)

model pipeline

Baselineは自分で作りましたが、@vslaykovskyさんの(https://www.kaggle.com/code/vslaykovsky/train-pytorch-aux-targets-weighted-loss-thres)と似ていますし、実際aux lossのあたりはすごく参考にさせていただきました。

交差検証戦略

  • StratifiedGroupKFold (n=4, groups=patient_id)

使用データ・前処理

  • 外部データは使用せず。
  • ルールベースのROI抽出
  • nvJPEG2000を使用し、DICOM画像の読み込みを高速化
  • 解像度は、efficientnetを使うときは1536x960(height x width)、maxvitを使うときは1536x768に設定。
  • チャンネル数は3
  • min-max Scaling (-1.0~1.0)
  • VOI-LUT処理を適用
  • DICOM画像から8ビット画像に変換(軽量化のため)
  • レーニング時と推論時で前処理パイプラインが変わらないように注意

不均衡対策

  • バッチサイズは8に設定していたが、各バッチの多数派(がんでない)と少数派(がん)の比率が7:1になるようにバッチごとにオーバーサンプリングをして調整し、ミニバッチ内に少数派データが必ず存在するようにした。オーバーサンプリングは復元抽出で実装したのでもしかしたら使われていない少数派データもあるかもだけど、そこはあんまり気にしなかった。

使用したアーキテクチャ

  • EfficientNetV2:サイズは主にSを使用。多くの参加者が使っていたし訓練・推論も速く精度も出るため使いやすかった。最初はこれで実験回して後でサイズ大きくしようって思ってたけど結局最後まで使ってた。
  • MaxVit:あまり聞かないアーキテクチャだけど、ざっくりいうと高解像度でも効率よく学習ができるVitというイメージ。VitだけどCNNも使っているのでCoAtNetの仲間っぽい。これを使った理由は単純にアンサンブル用にVitベースのモデルが欲しかったのと、高解像度の画像が動かせるVitだったから。正直論文読んでないしどんな構造かまだあんまり分かってない(よくない)。

Data Augmentation

  • albumentationsを使用
import albumentations as A

A.HorizontalFlip(p=0.5)
A.VerticalFlip(p=0.5)
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2, rotate_limit=45, p=0.8)
A.OneOf([
    A.RandomGamma(gamma_limit=(50, 150), p=0.5),
    A.RandomBrightnessContrast(brightness_limit=0.5, contrast_limit=0.5, p=0.5)
], p=0.5)
A.CoarseDropout(max_height=8, max_width=8, p=0.5)

モデルパラメータ

  • drop_rate: 0.8
  • drop_path_rate: 0.2
  • criterion: BCEWithLogitsLoss
  • optimizer: Adam (lr: 1.0e-4)
  • scheduler: OneCycleLR (pct_start: 0.1, div_factor: 1.0e+3, max_lr: 1.0e-4)
  • epoch: 5
  • batch_size: 8 (gradient accumulationを4に設定したので、仮想的には8*4=32のバッチサイズ)
  • 混合精度を使用 (training and inference)
  • aux loss(補助損失)を使用
    • aux targetはsite_id, laterality, view, implant, biopsy, invasive, BIRADS, density, difficult_negative_case, age(10分割にしてビニング)を使用。用意されていた特徴量はほぼ使った。
    • aux loss weight: 1.0
    • LB ProbingのDiscussionより、LBには訓練データにはないmachine_idが存在するという情報を得たので、machine_idは使わなかった。

推論

  • prediction_idごとの上位3つの予測値の平均を取って集約
    • Discussionでこの集約方法で、平均で集約・最大値で集約するより精度が上がったという報告があったので採用
    • あとでここの集約の部分のコード見直したら実はミスってたっぽい。CV上がってたから気づけなかった、、
  • 推論時間を短縮するため、4-fold CVで得られた4つのモデルの平均アンサンブルをする方法は推論には採用せず、代わりにすべてのデータで訓練された1つのモデルを使用するようにした。本コンペは前処理がすごく時間がかかるので、こうでもしないとアンサンブルする際にもれなくNotebook Timeoutになる。CVはスコアのチェックと閾値を求めるために主に使用。
  • アンサンブルは普通に平均。Nelder-Mead法を用いて重みを最適化することも試したけど、普通の平均とあんま変わんなかった。
  • 結果
Model ROAUC local cv pF1 public pF1 private pF1
1. tf_efficientnetv2_s (no aux loss) 0.897 0.424 0.48※ 0.41※
2. tf_efficientnetv2_s (using aux loss) 0.894 0.435 0.48※ 0.45※
3. maxvit_tiny_tf_384.in1k 0.896 0.443 Not submitted Not submitted
ensemble (1+2+3 prob mean) 0.917 0.482 0.54 0.49

※ 上位3つの予測値の平均じゃなくて普通の平均で集約

開発環境

貧乏学生なので定額制のクラウドGPUであるPaperspace Gradientを使用。最初の2か月はProプラン(A4000中心)、最後の1か月はGrowthプラン(A6000, A100中心)。今回のコンペは高解像度が上手く行くかつ大量データなのでメモリ多めで性能の良いGPUじゃないときつそうな印象だった。GPU的には全然Paperspaceのもので事足りるが、インスタンスが6時間制限だったり、GPUもいつでも確保できるわけではなかったりするので学習を進めるのもなかなかきつかった。オンプレGPUほしい、、

うまくいったこと

  • 大きめの解像度
    • 512 < 1024 < 1536 といった感じに解像度を上げるほど精度が上がっていった。
    • 1536より上は試していない。GPUメモリの都合上だったり、訓練時間長くなるからめんどくさかったり、ケロッピ先生が1536で高精度出していたりしたので。正直試す価値はあった。
  • 強めの正則化
    • 強めのdropout(dropout rate=0.8)
      • dropoutのrateは0.2, 0.5, 0.8でグリッドサーチしたけど上げるごとにpF1やAUCが上がっていった。
    • stochastic depth rate = 0.2 (timmのdrop_path_rate)
      • こちらもいろいろ試しましたが0.2がベストだった。
  • (強めの正則化に関連して)強めのaugmentation
    • 医療画像だけど回転45度・上下反転などもして、ありえない医療画像を作った方が方が精度は良かった
      • 医療画像だからaugmentationには気を使ってたけど、その必要はなかったっぽい?
    • 明るさ・ガンマ補正のaugmentationもパラメータの範囲広めると精度向上
    • アフィン変換の平行移動などではみ出した画像を0で埋めるか、反射させるかどちらも試したが、反射させた方が精度は比較的良かった。
      • 反射させると割とありえない画像というか奇妙な画像ができてしまうが上手く行った。このことからこのタスクはテクスチャ重視なのでは?と考えてがっつりaugmentationをかけて上手くいくことや、画像サイズの拡大が精度向上につながることの理解につながった
      • アスペクト比を維持するためにセンタークロップなどを試してみても精度は上がらなかったので、やっぱ形状よりテクスチャ重視だった気はする。
  • 不均衡対策(oversampling
    • 最初はうまく行かず、「なにもしない」が最適解だと思ってた。だが、画像サイズを大きくするとoversaplingが上手く行き始めた。
      • 小さい画像サイズで試行錯誤し続けてたから気づくのが遅かった、、計算速度の速い小さい画像サイズで試行錯誤するべきか、本番用の大きい画像サイズで試行錯誤するべきかどっちを選ぶかはちょっと難しそう
      • でも不均衡データだけどデータ数は十分だし、不均衡対策を施さなくても上手く行きそうなタスクではありそう。不均衡対策をすると収束が速くなるので採用していたが。
  • ROI抽出
    • AUCを中心にシンプルに精度あがった。計算速度も上がった。もっと早めにしておくべきだった
  • Auxiliary Loss(補助損失)
    • シンプルに精度向上
    • ただ、site_idという撮影施設を表す特徴量を使ってしまっているあたりコンペ専用のモデルができてしまった感はある。別施設のマンモグラフィ画像にもこのモデルが応用できるかというと微妙なところ(一応aux lossなしでも銅メダル相当のモデルはできていましたが)
  • VOI-LUT処理
    • ざっくりいうとDICOM画像を人間の目でも見やすいようにピクセル値を変換する処理。
    • 最終的に精度はちょっと上がった気がする(AUCを中心に)。けど実験初期のころはこの処理をしてもしなくてもあんま変わんなかったりした。
  • 異常値処理
    • 真っ黒な画像があったり、ノイズだらけの画像があったのでそれを削除した。確か4枚ぐらいあったような

うまくいかなかったこと

  • 損失関数による不均衡対策
    • Focal Loss
      • ちょっと試してWeighted Cross Entropyの方が良さそうだったのであまり実験を行っていない。
    • Weighted Cross Entropy
      • クラス数の逆数を重みにするのを中心に試したがあんまり
      • over samplingと組み合わせてもあんまり
  • 距離学習(Metric Learning)
    • ArcFaceで軽く実験をしてみたが、binary cross entropyとそんな変わんなかった。
    • パラメータ探索すればもう少し行ける?でもデータ数十分だしわざわざ距離学習するようなテーマでもなさそう。
  • センタークロップしてアスペクト比維持
    • むしろ精度悪化
    • というか収束がゆっくりになっている感じ?
  • TTA
    • 左右反転で試したが、精度はTTA無しと比べてあまり変わらないし、推論時間が増えるので採用せず。
  • 大きいモデル
    • efficientnetv2_lしか試してないが、sよりちょっといいぐらいであまり変わらなかった。推論時間が増えるので採用せず。
  • CutMix、MixUp
    • あまり変わらない、もしくは微減。収束は早くなってた気もするしもしかしたら効くパターンもあるかも
  • augmentation / 推論にCLAHE
    • 悪化
  • Grid変形・弾性変形
    • あんま変わんなかったか、しない方が良かった記憶がある(曖昧)
    • ちなみに、弾性変形は脳MRI対象では精度が上がるみたいな論文を見たことがあります。あとガンマ補正も。
  • 大き目の矩形でCutOut

上位解法

  • 多入力→上位の中でも賞金圏の方々はほとんど多入力だった気がします。
    • 片方の乳房ごと(MLO, CC, 2枚の画像)
      • 左胸ごと、右胸ごとに撮影方向の違う複数の画像を多入力
      • 2枚を超える際はランダムサンプリングなど
      • 撮影方向が違うとがんの疑いがある部分が映らなかったりするみたいなので効果的
    • 患者ごと(左MLO, 左CC, 右MLO, 右CC, 4枚の画像)
      • 患者ごとの左胸、右胸の画像を一気に多入力
      • 4枚を超える際はランダムサンプリングなど
      • 4枚に満たない患者さんもいるので、その場合はオーバーサンプリング
    • 局所的な画像と大域的な画像の多入力モデル
    • LSTMで多入力を受け付けるモデル
    • 複数の画像は単に画像同士を連結する方法も
    • 今思えばチャンネル方向に複数の画像を積んでみるのはありだったかも?
    • 一位の方は単入力だった。自分も単入力
  • 外部データの使用→これも賞金圏の方が結構されていた
    • 事前学習に使ったり
    • pseudo labelを付けたり
    • 賞金圏に行けるかどうかのポイントだったかも
  • 2段階モデルなど自分のと比較して複雑なパイプライン
    • シンプルな単入力モデルで事前学習→多入力モデルをfine-tuningしたり、事前学習したモデルを凍結してHeadだけ学習したり
  • 明らかなネガティブサンプルは早い段階で削除し、推論高速化
    • 推論に時間がかかる「パラメータが大きいモデル」や「アンサンブルモデル」を使うまでもないサンプルを抽出し削除
    • 閾値を用いたりして
  • モデル→EfficientNet, NFNet, NextVit, ConvNextなど
    • なんだかんだ推論の速いefficientnetとefficientnetv2が多かった印象。次いでconvnext
    • 推論時間のかかるvit系を使っている方はあんまいなかった印象
  • ROI抽出しない
    • アスペクト比が歪まなかったり、乳房のサイズが全画像を通して変わらない(大きい人は大きいまま、小さい人は小さいままに映る)のが良い方面に働いているかも。
    • 私はテクスチャ情報を重要視していたが、多入力とかだと乳房の形状・大きさも有効な特徴量として働いてくる気もする。のでアスペクト比の維持が重要になってくる可能性もありそう。
  • Global Pooling → GeM Pooling, Max Pooling
    • がんは非常に小さい領域に現れるため、mean poolingよりmax poolingとかの方が特徴マップからがんが存在すると言う情報を抽出しやすい、と言う考察。たしかに。でもその分ノイズも拾いやすくなりそうだけど、GeMだとmeanとmaxのいいとこ取りができて良いのでは?もしくはmeanとmaxそれぞれを採用したモデルのアンサンブル。
  • ルールベースではなく、YoloやFaster R-CNNでROI抽出
  • パーセンタイルを用いた閾値
  • SWA
  • Multi Sample Dropout
  • 画像をresizeする前にaugmentation
  • 学習時はランダムクロップ
  • 補助損失としてYoloで抽出したマスクを予測?
  • k-meansでバックグラウンドノイズを削除

反省点・やりたかったができなかったこと

  • CVの全分割の結果をあまり見なかった。
    • 計算量の関係もあったが、CVの全部の結果を見ずにhold outの結果(CVの一分割の結果)だけを見て実験を進めていた。
    • 閾値処理があるコンペだったので、CV間で結果が割とばらつく。そのため、一個のfoldに注目しすぎるのは良くなかったかも。 →なんだかんだモデルの精度上がる+アンサンブルすると閾値はあんまバラつかなくなっていった
    • とはいえ、CVをいちいち見ていたら時間的にきつい部分もあるので、どうしたものか、、

↑コンペ終盤でこの反省をしていたが結果を見てみれば金だったので、大量データかつ深層学習だとCVじゃなくてHoldoutでもいいのかも。閾値がバラついてたら怖いからCVは一応したけど

  • 小さめの画像サイズで試行錯誤してたけど、本番用の大き目の画像サイズでもある程度の試行錯誤を行うべきだったかも
  • Grad-CAMとかで注目箇所デバッグしていけばよかったかも。サイズごとでどう変わるかなど
    • 実装めんどくさくて後回しにしてしまった。猛省
  • 途中まで解像度縮小→拡大をしてしまっていた
    • データセットの容量削減のため大きい元画像から1024xのサイズに一旦resizeして保存していたが、これを1536とかに改めて拡大すると精度がそこまであがらなかった。Resize2回以上していた。これに気付いてResize一回で済むように変えると精度が大幅に向上。
    • 普通に考えると画質が落ちるが、ここまで精度が落ちるとは思っていなかった。これで1ヶ月ぐらい無駄にした感はある
    • これから画像タスクに取り組む場合、Resizeはなるべく一回になるようにする
  • シングルモデルに時間かけすぎた
    • 計算も重く、画像コンペに不慣れなのもあって満足できるシングルモデルを作るのに時間がかかった
    • アンサンブル試したの締め切り3, 4日前とかだった気する。もうちょい余裕持って試して、アンサンブルに効きそうなモデル作る時間が有ればよかった
  • 多入力はアイデアとしては思いついていたけど実装にはたどりつけなかった。
    • 計算資源的に、時間的にきつそうといった思いもあったし、正直めんどくさいという気持ちも、、
    • 正直ソロだったし計算資源もクラウドだしきつかったんじゃない?(っていう言い訳)

感想

実は2か月ほどLBが全然上がらなかった時期もあり、何度もやめようかと思ったぐらい個人的には精神的にかなりきついコンペでした。 でもこうやって金メダルを取ることができたのできつくても完走しようと決めて走り続けたのは結果論ですが良かったです。

また、今回結果的に金メダルを取ることができましたが、私自身運で取れてしまったんじゃないかと感じています。評価指標が不安定だったため、LBもほとんど信用できませんでした。それに加え不均衡データなのに閾値分類という、直感的ですがshakeしやすいコンペだったと思います。おそらく選べなかったけど金圏のsubを持っている方も多かったのではないでしょうか?一応私はBest Local CVのモデルがLBでも良いモデルだったので躊躇なくsubに選べましたし、CVのfold間でも精度・閾値が非常に安定していたのでロバストなモデルが構築できていたのだと思います。

私は大学では医療データ(テーブルも画像も)を扱った研究をしていたので、この医療画像コンペで上位入賞できたのは学生研究生活の集大成?みたいな感じがしてなんとなく嬉しかったです笑。今回金メダルを取ることができ、Kaggle Masterが現実的に目指せる目標になったので今後も頑張っていきます。