Kaggleのコンペ、「HuBMAP - Hacking the Human Vasculature」に参加し、19thで銀メダルを獲得しました!
このコンペはPublic LBでは5thで金圏まで残っていたので、正直今回の結果においては悔しさが大きいです。ただPublic LBで金圏に残ることができたのは初だったので少し成長を感じましたね(シェイクするの分かってたコンペでしたが...)
参加体験記を書いていこうと思います。 KaggleのDiscussionの方にも解法は載せたので(リンクは以下)、この記事は感想・考察・反省点を散りばめながら書いていきます。
コンペの概要
健康なヒトの腎臓組織スライドの2次元PAS染色組織画像において、毛細血管、細動脈、細静脈などの微小血管構造のインスタンスをセグメンテーションするコンペティションです。
医療画像系のセグメンテーションのコンペになりますね。タスクとしては、セマンティックセグメンテーションよりはインスタンスセグメンテーション(物体検出+セマンティックセグメンテーション)でした。
評価指標はセグメンテーションの確信度に対するAverage Precisionです。いまいち数式や言葉などで簡単に説明できる評価指標ではないですが、セグメンテーションの予測結果と真値のマスクの重なりが正しければ正しいほど高い値が出る評価指標です。詳細はコンペのページへ。
このコンペは単なるインスタンスセグメンテーションのコンペなのですが、次に示すような特徴がありました。以下はコンペのデータセットの説明からの引用を翻訳したものです。
競技データは、2つのDatasetに分割された5つのWhole Slide Images(WSI)から抽出されたタイルで構成されている。Dataset 1のタイルは、専門家のレビューを受けたアノテーションを持つ。Dataset 2は、同じWSIからの残りのタイルで構成され、専門家のレビューを受けていない疎アノテーションが含まれています。
- Test DatasetのタイルはすべてDataset 1のものである。
- 2つのWSIがTraining Dataset、2つのWSIがPublic Test Dataset、1つのWSIがPrivate Test Datasetを構成する。
- Training Dataには、Public TestのWSIのDataset 2のタイルが含まれるが、Private TestのWSIのタイルは含まれない。
また、Dataset 3として、さらに9つのWSIから抽出されたタイルも含まれる。これらのタイルはアノテーションされていない。このデータに対して半教師付き学習または自己教師付き学習のテクニックを適用して、予測をサポートすることができます。
要はDatasetが3つあって、各々の特徴は以下のようになります。
- Dataset 1 → 専門家のレビューを受けた正確なアノテーション。訓練データにもあるし、テストデータはすべてこのデータセットのものである。
- Dataset 2 → 専門家のレビューを受けていないあまり正確ではないアノテーション。訓練データとしてのみ使われる。
- Dataset 3 → アノテーションなし。訓練データとしてのみ使われる。
テストデータのアノテーションの質は統一されていますが、訓練データのアノテーションの質はバラバラということですね。
さらにWSIという染色組織画像の標本も14個あります。EDAをしたところ各々のデータセット等の内訳は以下の通りになりました。
- WSI 1:
- Dataset 1: Train Data
- Dataset 2: Train Data
- WSI 2:
- Dataset 1: Train Data
- Dataset 2: Train Data
- WSI 3:
- Dataset 1: Public Test Data
- Dataset 2: Train Data
- WSI 4:
- Dataset 1: Public Test Data
- Dataset 2: Train Data
- WSI 5:
- Dataset1: Private Test Data
- WSI 6, 7, 8, 9, 10, 11, 12, 13, 14:
- Dataset 3: Train Data
ちょっと分かりづらいかもですが、プライベートテストデータのWSIが訓練データに一切存在しないことが注意点です。パブリックテストデータのWSIは訓練データに存在するので、パブリック-プライベート間でドメインシフトが起こります。つまりシェイクする可能性も高いということです。
以上のような、①データセットごとのアノテーションの質の違い、②パブリック-プライベートテスト間でのドメインシフトの2点に注意しながら進めていくことが重要なコンペでした。
解法
概要
最終Subに選択したのは、Cascade Mask R-CNN (Backbone: ConvNeXt-tiny) のシングルモデルでした。
ただ、訓練の過程において色々と工夫をしています。何段階もFine TuningとPseudo Labelingを繰り返しながらモデルを改善していったイメージです。詳細は以下のFigure 1です。ちょっとわかりづらい部分もあるかもしれませんが...
以下、Figure 1の説明です。
- 1st Training Phase:
- 2nd Training Phase:
- Dataset 1と2、そして1st PhaseでPaseudo LabelingされたDataset 3を用いて、ConvNeXtをBackboneとしたモデルの訓練を行いました。このPhaseでは、1st Phaseで訓練したモデルの重みをそのまま持ってきてFine Tuningしました。
- 3rd Training Phase:
- Final Training Phase:
- Dataset 1、および3rd PhaseでPseudo LabelingされたDataset 2と3を用いて、モデルを訓練しました。ここでは、2nd Phaseで訓練したモデルをそのまま持ってきてFine Tuningしました。3rd PhaseのモデルをFine Tuningするよりは精度が出ました。
- Submission:
モデル
- ConvNeXt-tinyを主に使いました。
- ResNet、ResNeXtも使っていたんですが、Pseudo Labelingと併用すると良い結果が出なかったので途中から使わなくなりました。アンサンブルにも効かなかったです(少なくともPublic LBでは)
- mask2former, ConvNeXt-smallなども軽く試しましたが精度がなかなか出なかったです。でもこれはおそらくパラメータチューニングが足りなかったからかと。
- mmdetection 3.xを使用しました。
- 前半はDetectronを使っていましたが、mmdetectionの方が色々と融通が効きそうだったので途中で変更しました。
Annotations Typeについて
- 染色組織画像には以下の3種においてアノテーションがつけられていました。
- blood_vessel: 血管。本コンペのターゲットクラス
- glomerulus: 糸球体
- unsure: アノテーターが自信を持って血管と言えないもの
- つまり、予測対象は血管だけなのですが、そのほかの組織にもアノテーションがつけられていました。ただ今回私はblood_vesselのみを用いてモデルを構築しました。
Data Augmentation
- 訓練時は、画像を以下のようにランダムに拡大縮小しました。
- scales=[(640, 640), (768, 768), (896, 896), (1024, 1024), (1152, 1152), (1280, 1280), (1408, 1408), (1536, 1536)]
Test Time Augmentation (TTA)
- 以下の複数のスケールからの出力結果をシンプルにアンサンブルしました。この3つのサイズを選んだ理由は特になくて適当(LB見ながら良さげだったので)です。
- scales=[(1024, 1024), (1280, 1280), (1536, 1536)]
Post-Processing
- TTAをした後にNon-Maximum Suppression(NMS)を使用したぐらいです。
- マスクを拡大する膨張(Dilation)というモルフォロジー処理をするとLBが上がるという報告がDiscussionで多かったのですが、それはしませんでした。
Pseudo Labeling
- 今回ここを頑張りました。Pseudo LabelingはFigure 1に示したように2回行いました。先ほども説明しましたが、1回目はDataset 3に対するラベリング、2回目はDataset 2と3に対するラベリングです。
- Pseudo Labelingで得たマスクを集約する際にもTTAとNMSを用いていますが、これは先ほど説明した時と同様scales=[(1024, 1024), (1280, 1280), (1536, 1536)]の組み合わせでTTAしています。
CV戦略
- Validation SetはPrivate LBの設定に寄せました。具体的には以下の通りです。
- WSI == 1 & Dataset == 1 → Validation Dataset
- WSI == 1 & Dataset != 1 → これはTrainにもValidationにも使いませんでした。
- それ以外 → Train Dataset
- ただ、提出時のモデルは全てのデータを訓練に使用しています。
- こうやって設定することで、Public LBにOver Fitする恐れはあまりなくなったように思えます。ただCVはPublic LBとなかなか相関しませんでした。ですが、実験が後半に進み、3rd Training PhaseやFinal Training Phaseになると相関し始めました。
- なので今回選んだSubもLocalのBest CVを選ぶことができ、割と安心に最終Subを選ぶことができました(結局シェイクしたけど)。
うまくいったこと
- Pseudo Labeling
- Dataset 1のアノテーションの質を信頼する
- TTA
- NMS
- 画像のスケールアップ
- 画像をスケールアップするのは、過去コンペであったりmmdetectionのデフォルト設定がスケールアップしていたりしたのでなんとなくしたのですが、なんで精度向上に貢献したのかはいまいち分かっていないです。オリジナルの画像サイズが512x512なので、それ以上に拡大しても情報量が増えるわけではないんですよね...
- 一応自分の中の考察としては以下
うまくいかなかったこと
- パラメータサイズが大きいモデル
- mask2former, ConvNeXt-small
- パラメータチューニング不足のため?
- 訓練時にunsureとglomerulusを使用
- 上手くいかないというより、あまり変わらず。使用しない方が若干良かったぐらいです。
- Dilation
- 生のDataset 2を訓練データとして使用した場合においてはLBは改善しました。
- Dataset 1のみを訓練データとしてとして使用した場合はLBは悪化しました。
- さらにPseudo Labeling付きのデータを訓練データを加えるにつれてDilationの最適なiterが少なくなり、最終的には使用しない方が良くなりました。
- Data Augmentationで画像を反転
- なんで?Public LBにだけ悪影響だったのかも
- エッジ周辺でのスムーズな推論
- 推論対象画像は、大きな組織画像をたくさんの矩形に切り抜いた画像だったので、組織の切れ目の部分はうまく推論ができない可能性があります。それの対策です。
- このリンクのような処理をしました。https://www.kaggle.com/competitions/hubmap-kidney-segmentation/discussion/238013
- 実装ミスの可能性もあり
- glomerulusに重なった予測マスクの削除
- 他の上位解法ではあったので効かないということはなさそう。IoU閾値などの調整不足かと。
- 他のモデルとのアンサンブル
- 他の上位解法はみなさんアンサンブルしているので、効かないということはないと思います。私の問題かと。
上位解法
- 1位
- 全体的な戦略として、バウンディングボックスの予測精度を最適化することに力を注いだ。理由としてはAverage Precisionは主にバウンディングボックス予測に頼っており、マスク予測の精度はあまり影響しないかららしい。
- The Exponential Moving Average (EMA) modelsの使用。初めて効きましたがこの辺りの話だと思います→https://timm.fast.ai/training_modelEMA。モデルの重みを更新する際に、移動平均のイメージで前の重みを保持しながら新しい重みを更新していく手法みたいです。確かに私も訓練時は精度の上がり下がりが激しかったのでこういった平滑化手法は効くのかも。有名なものだとSWAに近い手法?
- 主要なモデルはRTMDet。性能が良く、高速。
- 768の画像サイズで、3つのクラス全てを使用して訓練。Dataset 1と2を使用して訓練。
- 激しめに回転、拡大縮小をするAugmentation
- バッチサイズは8で、Dataset 1から3枚、2から5枚の画像で構成される。バッチ中にDataset 1と2が混在しているところが精度改善につながった?
- 最終的なモデルはアンサンブルで、バウンディングボックス予測はRTMDet, YoLo-x, Mask R-CNNのWBF。マスク予測はMask R-CNNのマスクヘッドを使用し、入力画像サイズは1440。TTAは無し。
- ライセンス的にも問題なさそうなYoLo-xを使っているところもさすがです。
- Dilationは使用していない(存在を忘れていたらしい…)
- 2位
- CV戦略の工夫。訓練データと検証データにWSIの重複がないようにCVを切りたいが、そうすると訓練データのDataset 1の数がかなり減ってしまう。そのため、WSIを左右で分割して訓練データのDataset 1の数を増やす。一部訓練データと検証データでWSIが重複するが、重複した訓練データはstaintoolsを使ってDataset 3のスタイルに変換して、重複の影響を最小限にする。
- staintoolsという染色組織画像の標準化とAugmentation用のライブラリがあるんですね、、、知りませんでした https://github.com/Peter554/StainTools
ケロッピ先生の提案した、エッジ周辺の推論対策
- 私はこのエッジ対策を推論時のみ行った際うまくいかなかった。やっぱり訓練時もする必要がありそう。実装めんどくさいけど。
- ref: https://www.kaggle.com/competitions/hubmap-hacking-the-human-vasculature/discussion/419143#2316842
激しめのAugmentation。回転、反転、弾性変換、拡大縮小、明るさの変更やガウスノイズの付与まで。
- 訓練は2段階。1段階目ではDataset 1と2を使用。2段階目ではDataset 1のみを使用してFine Tuning。Fine Tuning時で5つのチェックポイントを選択してSWAを実行。1位の方のEMAといいSWAといい、モデルパラメータの平滑化や平均化が結構ポイントだったのかな?
- モデルはCascade Mask R-CNNでBackboneはswin-t, coat-small, convnext-tiny, convnext-small。mmdetection 2.xを使用
- 後処理
- Dilationは信用できないけどLBが伸びた。なので最終の2つのsubでDilationありとなしのものをそれぞれ選択。
- CV戦略の工夫。訓練データと検証データにWSIの重複がないようにCVを切りたいが、そうすると訓練データのDataset 1の数がかなり減ってしまう。そのため、WSIを左右で分割して訓練データのDataset 1の数を増やす。一部訓練データと検証データでWSIが重複するが、重複した訓練データはstaintoolsを使ってDataset 3のスタイルに変換して、重複の影響を最小限にする。
- Others
- 物体検出部分とセマンティックセグメンテーション部分の2段階に分けてモデルを構築
- 物体検出→WBF→セグメンテーション→マスクアンサンブル。Sartouriusコンペティションでもこのパイプラインのソリューションは多かった。
- アンサンブル。バウンディングボックスのWBFやマスクのvoting、averagingなど
- やはりPrivate LB likeにCVを切る方が多い。
- Augmentationで回転拡張する方も多い。コントラストの変化も。
- Annotation Typeに関しては、glomerulusやunsureも使う方もいれば、unsureだけ使わない方もいたり様々。私もこの部分を色々変えながら実験したが、精度に関しては大きな差が出ることはなかった印象。
- Dataset 1と2を混ぜて訓練する方もいるが、その後にDataset 1だけでFine Tuningする方が多い。
- Dataset 3を使っていない方も多かった印象
- Pseudo Labeling
- 思ったよりしていない方も多かった。
- Dataset 3のみにつける方もいればDataset 2にもつけ直す方がいる。Dataset 2に関しては付け直すのではなく、オリジナルのDataset 2のマスクにPseudo Labelingされたマスクを加える例も。
- バウンディングボックスに対してつけ直す方法も。
- マルチスケールトレーニング&推論
- 後処理で小さいピクセルのインスタンスを削除したり、確信度の閾値調整をしたり。
- TTA時に回転、反転など
- Dilationに関して
- Dilationは使用していない方がシェイクせずに上位に残っている印象
- マスクではなくバウンディングボックスをDilationする方法も
- 物体検出部分とセマンティックセグメンテーション部分の2段階に分けてモデルを構築
反省点・やりたかったができなかったこと
- アンサンブル。おそらく一番の敗因はシングルモデルだったことだと思います。アンサンブルが上手くいかず、途中からサボりました...なるべくシングルモデルは実務では有効ですがKaggleでは損をするだけなので、もっとアンサンブルに時間をかけたかったですね。以下、具体的な反省点(言い訳)です。
- ConvNeXtに加えてResNet、ResNeXtをアンサンブルの候補モデルにしたところうまくいかなかった。実際ConvNeXtの精度が抜き出てたところもあるので、もう少し近い精度のモデルをアンサンブルの候補にしていたらうまくいっていたかも…
- 物体検出で得られたバウンディングボックスの部分をWBFでアンサンブルする方法を試みようと思ったが、実装できなかった。mmdetectionでバウンディングボックスを抽出してWBF、その結果をマスク予測ヘッドに入力する感じなのかと思ったけどうまくいかず…上位の方が公開してくださったコードを参考に実装できるようにします。
- NMSとかWBFとかバウンディングボックス部分に関するアンサンブルだけ意識していたけど、マスク部分に関するアンサンブルをもっと意識すべきだった(マスクのVotingなど)。
- これは完全な言い訳ですが、アンサンブルに時間をかける余裕はなかったです。実験上手く行き始めたのがコンペ終了3日前とかで、銀圏上位突入がコンペ終了1日前、金圏突入がコンペ終了当日でした。とはいえもうちょっと早い段階からアンサンブル考慮すべきでしたね。
- 確信度の閾値調整、NMSなどのIoUの調整、小さいピクセルの削除なども時間がなくできなかった。
- 学習率もほぼチューニングせず結構適当なので、チューニングしたらもっと精度高くできていたかも?
- バウンディングボックス予測の精度を軽視したこと。
感想
悔しい結果となりましたが、Public LBを金圏Finishできたので成長が感じられるコンペになりました。加えて、セグメンテーションも物体検出も初めてだったので新しい知識もたくさん取り入れることができ、収穫も多かったです。
なんといってもドメインシフトにある程度対抗できたことが嬉しかったです!去年参加した、「Open Problems - Multimodal Single-Cell Integration」ではドメインシフトにやられて800位ぐらいシェイクダウンしたので
なんだかんだ15位ぐらいシェイクダウンしましたが、金圏の方含めメダル圏外に吹っ飛ばされてしまった方々も多かったので吹っ飛ばされずよかったです。
Grand Masterへの道のりはまだまだ長そうですが、地道に頑張ります。