【APTOS2019参戦記録】

【APTOS2019参戦記録】

はじめに

本記事は先の週末(2019年9月8日)までやっていたAPTOS 2019 Blindness Detection コンペの振り返り記事となります。

今回のコンペにおいて、自分たちのチームは結果としてPublicからPrivate大きく順位とスコアが伸び、初の銀圏52位(Top2%)となりました。 というわけでこの記事においては記憶に残っている限りのざっくりしたコンペの外観と担当した前処理・学習パイプラインの話をします。 因みにチームメイトのmocobtさんの参戦記録はこちらです。

なお、今回のコンペにコード郡に関してはこちらのGitHubリポジトリに公開済みです。

🍊🍊

コンペ概要

  • 糖尿病網膜症判定コンペ
  • 期間は2019年6月29日から2019年9月8日まで(当初は9月5日まで、後に伸びた)
  • 眼底画像データをから病状のレベルを5段階で判定
  • データ数はTrainが3662枚でPublicTestが1928枚かつ、PublicTestはTest全体の15%
  • 外部データの使用ありで2015年にも同様のコンペが行われておりこちらの画像の枚数が2019年に比べて大分多い
  • 評価指標はQuadraticWeightedKappa(cohen kappa score)
  • 推論のみKernelOnlyコンペ、実行時間の制限は9時間

外部データの取り回しやモデル選択、画像の前処理、あとは不安定な評価指標のハンドリング?が大事だったのかなと思っています。 もう少しあけすけな言い方をすれば2015年データ含む外部データは使わないと厳しいし、モデルはEfficientNetがとても強かったし、評価指標はseedを少し変えるとガンガンブレるので当てにならないとも言えます。

解法

概略

  • data: A.2019trainと2015trainの0以外のlabelを利用、pretrainはなし B.2019trainのlabelの割合を保ったまま2015のデータをlabelごとに追加、pretrainはなし
  • validation: diagnosis(label)に対して層化抽出でA. 5fold B.HoldOut
  • preprocss: エッジ検出を使って円形に領域を切り抜き、A.HorizontalFlipとRandomRotationをかけて320にResize B.こちらは256にResize
  • model: EfficientNet-b4、regression
  • hyperparameter: Adam、StepLR、MSELoss
  • postprocess: 閾値の最適化、TestTimeAugmentaionを5回(LRFlip、RandomRotaion)
  • ensemble: WeightAverage、WeightはLBを見ながら高い方に若干の重みを載せた、あとは若干PublicKernelも混ぜている

data

ExternalDataの使用が許可されており、トレーニングに時間の制限がなく、hostから与えられていたデータも必ずしも多くなかったため、多くのチームが外部データを使っていました。

我々のチームでは学習の際に2019年のコンペのデータに加えて2015年のコンペのデータのうち、0以外のラベルがついたデータを利用しました。

なお、foldに関しては画像コンペに関してはテーブルコンペに比べて凝った切り方をしているイメージもなかったことやそもそも評価指標であるQuadraticWeightedKappaが不安定な指標で有ることが指摘されていたことも含めて取り敢えずのラベルによる層化抽出で最後まで走りきりました。

また、上手く行かなったからという理由にはなってしまいますがDiscussionで提案されていたAPTOS2015を事前学習に利用して2019でfinetuningする手法は使っていません。

補足
  • データセットに関してはコンペ当初からラベルのバランスが偏っていることが気になっていた。
  • 当初2015データを利用したときはラベルバランスを整えるため、すべてのラベルが均等になるようにAPTOS2015データを投入してみたりしていた。ただ、これは上手く行かなった。
  • データの分布やバリデーションセットの混同行列を見るにつけてラベル2とその前後の分類が難しそうだったのでこのあたりのデータを大量投入するか、ということで0以外のラベルを学習させたところ良い結果が出たので採用した。
  • 他にはトレーニングのラベルの割合をAPTOS2019trainと同じ割合で維持したままAPTOS2015データを可能な限り加えるなども試したが前述のデータセットでの学習結果に及ばなかった。

validation

今回はラベル付きの画像データが与えられていたため、基本的には素直にtrainデータの20%をvalitationセットにする方針で行っていました。 また、学習時間や提出にかかる時間のこともあり最終盤までSingleFoldで学習を行ったモデルを利用していました。

補足
  • Discussionで早期から手元のスコアとLBのスコアの乖離が大きいという話はあったのでvalidationを完璧に切るのは難しいと認識していた。
  • 特にコンペの後半から使っていたQuadraticWeightedKappaの最適化はvalidationセットの分布にフィットさせていたため、validationセットが重要であることは認識していた。

preprocessing

前処理としては画像の切り抜きと各種Augmentation、そしてResizeをどのチームもそれぞれ行っていました。 画像の切り抜きに関しては前回コンペ手法を利用した手法がPublicKernelに挙げられており、他にもいくつか提案されていましたが我々のチームでは最終的にオリジナルな手法を用いました。

また、Augmentationに関してはWhiteNoiseやRandomEraserなどを試したものの、結局HorizontalFlipとRandomRotationのみを使うこととしました。 最後にResizeに関してですが、今回のコンペにおいて画像サイズは400程度あると良かったようです。ただ、結局モデルの学習時に置いては我々はハードウェアの制限や実装力の不足で320程度のサイズを利用していました。

補足
  • 今回のコンペでは画像の縦横比が非常にラベルバランスと関係性があり、これが実質的にラベルのリークとなっている可能性がDiscussionで指摘されていた。そのため、いくつかの切り抜き手法では画像の縦横比を変更せずに切り抜く手法が提案されていた。(画像の縦横比を変更して正方形に整えるともとの画像の比率が情報として画像に含まれてしまう。)
  • 前項を踏まえた訳ではないが、我々のチームでは以下の手法で切り抜いていた。(前述のDiscussionが出る前からアスペクト比を変更することに違和感があったのでオリジナルな手法を実装していた。)

f:id:icebee:20190910203542p:plain
Edge Crop

  • Augmentationは中盤までWhiteNoiseが有効に作用していたが、過去手法を参考にGaussianBlurをもとの画像に重ねて画像の輝度を調整する前処理を導入して以降、効かなくなった。(スコアは導入以降の方が高かった。)
  • RandomEraserは、実質的に切り抜きの画像ごとの違いを補完してくれることを期待して使っていたが通してイマイチだった。
  • mocobtさんがサーベイして教えてくれた所によれば、毛細血管の一部が判別に寄与しており、これを取り出すために400程度の画像サイズが必要だったそうだ。(こちらの論文だそうで。)
  • 今回の画像で注視しておくべきだった点は前述の毛細血管とその毛細血管が収まる中心窩があった。よってそのままではハードウェアの限界で画像サイズを素直に取り込めなかったとしても画像内該当領域を切り抜いてしまうというアプローチはあったかもなという話をコンペ後の反省会でしていた。

model

モデルに関して最初は取り敢えずtorchvisionに実装済みのモデルを使っていくつか試していましたが、PublicKernelにてEfficientNetを用いた各種Kernelが圧倒的なスコアを叩き出していたことを確認してからはこちらからコードを拝借してきて使っていました。

EfficientNet自体は大きいモデルの方がスコアが良くなる傾向を認識していました。よって後半に至るにあたってb0からb4,b5のモデルに切り替えて行きたかったのですが、ハードウェア制限に加え、画像サイズやバッチサイズもそれぞれサイズが必要だったこともあって、それらのバランスをとった形となります。

補足
  • ざっくりと試したモデルはMobileNetV2、ResNet101/152、DenseNet121、EfficientNetb0/b1/b3/b4/b5。

hyperparameter

Optimizerは結局Adamしか試しておらず、多少いじってみたものの上手く行かなかったのでPublicKernelのパラメータをそのまま拝借してきたものを使っていました。

Schedulerも多少は他のものも試したのですが結局自前でパラメータ調整をしたStepLRで最後まで行きました。

Lossに関しては前半にClassifierで解いていたときはCELoss、RegressionにしてからはMSEです。あとは実験的にやった二値判別ではBCELogitsを使っていました。

補足
  • 手元のCVがまったくLBと相関していなかった関係でどう調整すれば良いのか分からなかったというのが正直な所。
  • とはいえ単純に経験不足も大きかった。(NNをまっとうに弄ったのはFreesoundが初めてで今回が二回目なので。)
  • MSEにラベルのサンプル数に応じた重みをつけるようなことも試したが上手く行かなかった。

postprocess

QuadraticWeightedKappaの最適化に関してはPublicKernelで公開されていた手法を(多少自分のパイプラインに組み込めるように書き換えて)使っていました。

後処理としてのTTAは回数を増やすと取り敢えずスコアが伸びていたのであとは実行時間と相談して5回から10回程度行っていました。なお、Augmentationはpreprocessと同じくHorizontalFlipとRandomRotationです。

補足
  • 特にこのpostprocessにおけるQuadraticWeightedKappaの最適化はvalidationのラベル分布に依存しており、結構shakeが怖いポイントだった。が、当時の自分の順位からしてメダルを拾いに行くためには必要な博打だろうと思ってそのまま使い続けた。

ensemble

最終的には前述した手元の2モデルにPublicKernelを加えました。混ぜ方としては、採用したモデルがすべてregressionで解いていたことから各モデルの出力を重み付きで平均を取った上で、各モデルそれぞれの最適化した閾値に関しても平均を取り、それを用いてクラス分類をしています。

補足
  • 前半で各モデルのクラス分類後の結果を平均を取っていたときは上手く行かなかった。
  • 終盤でアンサンブルの手法を再検討したときに対案としてvotingも考えたが今回はこちらの手法を用いた。

学習パイプライン

今回のコンペの前半は自分は最近流行りのパイプラインというやつを作ってみた!という感じでした。 自分とチームメイトのmocobtさんは過去にMalwareコンペでも学習パイプラインを作っておりましたが、当時はシェイクに膝を撃ち抜かれ二人してエンジニアリングに注力してデータを見なかった結果がこれじゃ!と嘆くことになっていました。なお、当時はテーブルデータだったこともあり、正直今回のパイプラインより大分時間と労力を使った印象があります。 そういう意味で画像コンペで今回自分が作ったパイプラインはほとんどすべてPytorchの記述方法に乗っかる形で作っております。 基本的はPytorchのモジュールをyamlファイルで指定したconfigに従って呼びつつ、オリジナルな関数が実装してある場合はそちらを優先的に見に行くという感じですね。一応GitHubのリポジトリを公開済みですので気になる方は見ていただけると嬉しいです。

パイプラインを組んでやっていた事の追記

  • 実験が早くなったのはとても良かった。管理もGoogleスプレッドシートyamlの番号を対応させれば簡単。
  • 一方、パイプラインを組むと初期コストがどうしてもかかるのでコンペの撤退判断は難しくなる。
  • GitHub上に学習結果(実行log、train/validのloss/qwk曲線、生tsv、終盤はvalidationセットにおける混同行列も)を投げ上げてどこからでも見えるように出来ていたのも良かった。
  • GCP上にインスタンスを立てて実験していたのでその気になれば実験を財布の許す限り平行出来た。ハードウェアを途中から強くするのもの簡単。
  • 上記パイプラインの外側で更に実験を連続で回すスクリプトを書いており、学習終了とともに自動でKaggleのDatasetに投げ上げるようにしていた。そのため、カーネル上での推論への引き継ぎもかなり楽でこれは良かった。
  • あとは必ずしもパイプラインのお陰だけではないのだけれど特にコンペの終盤において多くの参加者が悩まされていた、カーネルの不調も外部で学習して推論だけKaggle上でやっていた自分たちはほとんど影響を受けなかった。この辺りは安定していた。

おまけ

銀メダルの代償