🏥

kaggle Sennet+HOAコンペ 上位解法まとめ(+参加振り返り)

2024/02/22に公開

はじめに

腎臓のCTスキャン画像から血管領域のセグメンテーション精度を競うSenNet + HOA - Hacking the Human Vasculature in 3D というkaggleコンペが2024/2/7まで開催されていました。
コンペ終了後に公開された上位チームの解法からたくさん学びがあったので、備忘録も兼ねてまとめていきつつ、ついでに個人的な振り返りも最後に少し書いてみたいと思います。

ちなみに本コンペの解法のまとめ記事を公開してくださっている方がおりますので、ぜひそちらも合わせて参考にしてみてください。

コンペ概要

  • 腎臓のCT画像から、動脈のピクセル領域を予測するモデルの精度を競うコンペでした。

  • 学習データは3人の被験者からスキャンしたサンプル(kidney_1~kidney_3)が提供されており、テストデータは別の被験者(Public/Privateそれぞれ各1名分)からスキャンしたデータで構成されています。ただし、解像度やスキャン位置などはそれぞれのサンプルごとに異なっています。

  • アノテーションは細かい血管まで精密にラベリングされた"密な(dense)アノテーション"と大まかな血管のみラベリングされた"疎な(spaerse)アノテーション"が存在しており、学習データはサンプルごとに両方(もしくは片方のみ)のアノテーションが提供されていました。テストデータはPublic/Privateいずれも密なアノテーションであることがホストから明言されていました。

サンプル名 アノテーション スキャン枚数 スキャン解像度 train/test 備考
kidney_1_dense dense 2279枚 50um/voxel train
kidney_1_voi dense 1397枚 5.2um/voxel train kidney_1の高解像度サブセット
kidney_2 sparse (65%) 2217枚 50um/voxel train
kidney_3_sparse sparse (85%) 1035枚 50.16um/voxel train
kidney_3_dense dense 501枚 50.16um/voxel train kidney_3のサブセット
kidney_5 dense 約500枚 50.28um/voxel (bin x2) test (Public)
kidney_6 dense 約1000枚 63.08um/voxel (bin x4) test (Private)
  • 評価指標はSurface Diceと呼ばれる指標が採用されていました。聞き馴染みは薄いですが、医療画像で使用される指標で、ざっくり言うとマスクを積み重ねて3D化した時に、形状が似ているほど良いスコアになるような指標になります。つまり、予測するのは画像単位の2Dマスクになりますが、評価は腎臓全体の3Dマスクに対して行われることになります。

上位チーム解法まとめ

1. Data

sparseデータの活用

  • テストデータがdenseアノテーションされているため、kidney_1_dense、kidney_3_denseのみを学習・検証に使用する方法が本コンペでは多く採用されていましたが、上位チームはsparseアノテーションデータ(kidney_2、kidney_3_sparse)も活用してサンプル数を増やしていました。
  • sparse性については特に気にせず使用しているチーム(1st, 2nd, etc...)もありましたが、denseデータで学習したモデルを使ってPseudoLabelingをすることで疑似的にdense化して活用しているチーム(3rd, 5th, etc...)が多かった印象です。
  • 少し変わった活用方法として、アノテーションのスパース率をそのままデータセットのサンプリングレートに適用しているチームもいました。(引用: 4th

外部データセットの活用

  • 外部データセットをPseudoLabelingしてサンプル数を増やすアプローチをとっているチームもいました。(引用: 5th, 6th
  • 疑似ラベルの質が精度に大きく影響するため、少しずつデータを増やしてステップを踏みながらラベリングすることでアノテーションの質を上げるような工夫をしていたようです。
  • 5thチームは腎臓だけでなく、脾臓のCT画像も外部データとして活用していました。

2. Model

アーキテクチャ

  • Depth情報を反映させるため2.5Dモデル、3Dモデルを使っているチームが若干多かったように感じますが、シンプルな2Dモデルを使用しているチームもいました。今回のコンペではあまり差がつく要素では無かったように思います。
  • 2.5Dモデルはch次元に連続するスライスを重ねた画像を入力とする2Dアーキテクチャを採用しているチームがほとんどでしたが、ch数は3~7の範囲で各チームごとに若干の違いがありました。(引用: 1st, 6th, etc...)
  • UNetにconv層を追加してより大きな画像サイズの特徴量を扱えるようにすることで小さい領域の検出率を上げる工夫しているチームもありました。ちなみにアイデア自体はコンペ中にこちらのDiscussionで共有されていました。(引用: 1st, etc...)

3. Train

3D Rotate Augmentation

  • 1stチームが採用していた手法で、画像を重ねたボリュームからランダムに回転させた軸に沿って画像を切り出すAugmentation手法です。

  • 今回のコンペではかなり有効で、1stチームのアブレーション実験によると、このAugmentation導入前後でPrivateスコアが0.682 ⇒ 0.835に向上したそうです。
  • また類似の手法として、7thチームはランダムではなくあらかじめ設定した角度で軸を回転させてスライスした画像を生成することでデータ水増しを行っていました。

Loss関数

  • BCE、Dice、Focalの組み合わせ(orいずれか)を使用しているチームが多かったですが、上位チームはより評価指標に合ったloss関数を使用していました。
  • よく使われていたり、印象的だったloss関数は以下の通りです。
    • BoundryLoss(引用: 4th, 6th, etc...)
      • 予測したマスクとGTマスクの境界間の距離に基づいた損失関数
      • Surface Diceは3Dマスクの表面形状が重要な指標なので、コンペ指標とマッチしていたと思われる
    • CustomLoss(引用: 1st)
      • Surface Diceを損失関数化したもの
      • アイデア自体はDisccussionで共有されていた
      • 実際にちゃんと機能する関数を実装するのはかなり難易度が高いが、実現できれば最もベストな損失関数だったように思われる。(1stチームのアブレーション実験によると、Privateで+0.148の改善)

4. Inference

Multi-View TTA

  • 複数の軸(XY、ZY、XZ)でスライスした画像に対してそれぞれ推論するTTAを行うことでスコアを向上させているチームが多かったです。
  • 3次元データならではのTTA手法で、単視点では検出しづらい領域も、別視点から検出できる可能性があるので全体的にパフォーマンスが上がるようです。

リスケーリング

  • Privateデータの解像度の違いへの対策として、Privateデータ解像度とTrainデータ解像度の比率でリサイズしてから推論する方法を採用しているチームもありました。(引用: 5th , 6th, etc...)
  • コンペ中から解像度の違いによるShakeの懸念は議論されていたので、このような方法で対策をとるチームが多かったようです。
  • 多くのチームは単純に画像単位でリサイズしていましたが、5thチームはより正確な方法として3次元的にリサイズする方法を採用していました。

参加してみての振り返り

私もこのコンペに参加していたのですが、結果は14thで残念ながら金圏に届かず。。。といったところでした。惜しいといえば惜しいのですが、金圏上位とはスコアに明確な差があり、改めて上位ソリューションを読んでみると色々と反省するところが見えてきたので、最後に少し振り返ってみたいと思います。

ちなみにソリューションの内容はこちらに公開しているので、興味がある方がいましたらぜひご覧ください。
https://www.kaggle.com/competitions/blood-vessel-segmentation/discussion/475260

反省点

Lossの検討不足

  • 以前に参加したセグメンテーションコンペでそれなりの実績があったので、あまり考え無しにずっとDiceLossを使って実験をしていたのですが、ちゃんと指標にあったlossを検討すべきでした。
  • 「lossは下がるがスコアが連動しない」現象が発生していたのですが、全エポックで重みを保存してベストなチェックポイントを選択するという場当たり的な対応をしていたのがやはり良く無かったです。
  • 一応、「指標を直接最適化するようなlossじゃないから」という仮説を立ててより指標にマッチしたカスタムロスの実装を試みたのですが、難しすぎてすぐに匙を投げ、他のlossについても結局直接的に指標を最適化をするわけではないので、そんなに大差無いだろうと思って深く検討していませんでした。
  • カスタムロスまで実現していたチームはごくわずかでしたが、上位チームはより良いlossを探し出していたので、やはり学習を安定させるという意味でもlossの検討は大事だったように思います。
  • 今更ですが、指標が特殊だった時点でlossの検討優先度は上げるべきだったのかもしれません。

PseudoLabelの精度不足

  • 変換時にラベルがずれてしまうことを懸念して重めのAugmentationを見送った代わりに、外部データとsparseデータにPseudoLabelingしてサンプル数を増やす方針を取っていたのですが、ラベルの質にもっと気を使うべきでした。
  • 今回はkidney_1_denseのみで学習させたモデルで1回だけPseudoLabelingをしていたのですが、追加データの効果を引き出すためには上位チームのように少しずつデータを追加しながら学習とラベリングを何サイクルも繰り返してラベルの質を極力上げる必要があったのかもしれません。
  • データ数を倍以上にしてるのにスコアが微増だった時点でラベルの質があまり高くないことに気付くべきだったように思います。

まとめ

今回のコンペはサンプル数が少なく、Shakeも激しかったので一見すると運要素が強いように感じましたが(実際、評価サンプルは1個なので運もそれなりに絡むのですが)、それでも上位チームの多くは経験や与えられた情報からしっかりと洞察することで安定した強いモデルを作成していたように思います。
サンプル数が少ない場合は、cv、lbどちらにしてもスコアの数値だけを重視して改善を重ねるとShakeに巻き込まれてしまう可能性が高くなるので、どのアイデアがなぜ効くのかを仮説をしっかり立てて、深く考えながら実験することが重要だったのかなと思います。

また、コンペに参加して上位ソリューションを読んでみることで、3Dセグメンテーションタスクの知見が得られたのはもちろんのこと、サンプル数が十分に得られないケースに対してどのようなアプローチが有効なのかを学ぶことができたと感じています。
実際の業務でも分野によってはサンプルが十分に確保できない場面はあるので、今回のコンペで学んだ知見をこれからに活かしていきたいです。

Discussion