Sleep like a pillow

Deep Learning関係の話。

Kaggle APTOS 2019 Blindness Detection まとめ

はじめに

2019年6月の終わりごろから先日まで、KaggleのAPTOS 2019 Blindness Detectionに参加していました。 最終的な順位は11位でゴールドメダルを獲得するとともに、Kaggle Masterになりました。

以下、取り組みなどのまとめです。

www.kaggle.com

github.com

コンペ概要

Asia Pacific Tele-Ophthalmology Society (APTOS)という眼科学会が主催のコンペで、眼底画像から糖尿病網膜症の重症度(0~4の5段階)を推定するというものです。

データの内訳は以下の通りです。

  • train: 3662
  • public test: 1928
  • private test: 約10925

評価指標はQuadraticWeightedKappaと呼ばれるもので、変動しやすい指標らしいです(よくわかっていない...)。

コンペの形式はsynchronous KO competitionsと呼ばれるもので、推論を行うkernelを提出する形式です。 kernelを編集する際にはpublic testデータにしかアクセスできませんが、提出するとtestデータがprivateを含んだデータに置き換わった状態でもう一度kernelが実行されてスコアが計算されます。

方針

kernelやdiscussionでtrainデータとtestデータの分布が異なることが報告されていたので、kernel上でtestデータ(public + private)を用いたPseudo Labelingをやってみようと思っていました。

解法

前処理

2015年にも全く同じタスクと評価指標のコンペ(Diabetic Retinopathy Detection)が開催されており、このコンペのwinner's solutionで使われていた前処理を多くの人が試していました。

具体的には大まかに以下のような処理です。

  1. 眼の半径を算出して画像をクロップ。画像の中央にあたるベクトルを抜き出して、平均値以上の値を持つピクセルの数をカウントすることで半径を求める。
  2. 局所領域の平均値が127.5になるように、画像から局所領域の平均値を減じる。
  3. "boundary effects"を除くために、眼の領域の面積がオリジナルの円の90%になるように境界付近の領域を削除。

色々と試してみたのですが、結局1のみ使用するのが最もスコアが良かったです。

画像サイズ & Augmentation

画像サイズは256x256です。

Augmentationは2015年のwinner's solutionをベースに色々と試して以下のようになりました。

train_transform = transforms.Compose([
    transforms.Resize((288, 288)),
    transforms.RandomAffine(
        degrees=(-180, 180),
        scale=(0.8889, 1.0),
        shear=(-36, 36)),
    transforms.CenterCrop(256),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomVerticalFlip(p=0.5),
    transforms.ColorJitter(contrast=(0.9, 1.1)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])

工夫点はrandom rescaleの部分で、testデータに拡大されたような画像が多かったので拡大方向のrandom rescaleを行いました。その際に256x256の画像を単に拡大すると画像が劣化してしまうため、288x288の画像を縮小方向にrandom rescaleしてから256x256でcenter cropするようにしました(微妙にスコア上がった)。

1st-Level Models

ローカルで学習させたモデルです。

Model

以下の3つのモデルを使用しました。

  • SE-ResNeXt50_32x4d
  • SE-ResNeXt101_32x4d
  • SENet154

ImageNetでpretraindされたもので、BNはfreezeさせました。

Loss

今回のタスクは序数回帰という分類と回帰の中間のような問題で、主に以下の2つのアプローチがありました。

  • 分類問題として解く
  • 回帰として解いたのちにthresholdingでクラスラベル化

回帰の方がスコアが良かったのでMSE lossを用いて学習させました。

Optimizer

試行錯誤した結果、以下のような感じに落ち着きました。

  • SGD (momentum=0.9, weight_decay=1e-4)
  • CosineAnnealingLR (lr=1e-3 -> 1e-5)
  • 30 epochs

Dataset

外部データの利用が許可されていたので、2015年のコンペのデータを使用しました。 こちらのdiscussionを参考にして、2019年のtrainデータで5-fold cvを作って各foldの学習データに2015年のデータを追加するという方式をとりました。

2nd-Level Models

kernel上でtestデータに対して1st-Level Modelsによる推論を行い、得られた推定値のアンサンブルをtestデータのpseudo labelとして2nd-Level Modelsの学習に使用しました。 データセットは2019年のtrainデータの5-fold cvと、testデータを5等分してそれぞれのfoldの学習データに加えたものです。

kernelには9時間という時間制限があるため、epochを10にするとともに、より早く収束するようにoptimizerとしてRAdamを使いました。 他のパラメータは1st-Level Modelsと同じです。

本当は1st-Level Modelsすべてを学習させたかったのですがkernelの制限時間を超えてしまうため、①SE-ResNeXt50_32x4dとSE-ResNeXt101_32x4dのアンサンブルと②SENet154単体という2つのパターンを試して、これら二つをfinal submissionとして選択しました。

最終的にprivateLB: 0.930で14位になり、その後上位の方が数名BANされて11位になりました。

うまくいかなかったもの

  • EfficientNet

EfficientNetは上位勢のほとんどが使っていました。 今後のコンペでもスタンダードなモデルになっていくと思うので、 上位勢の解法を見て学習のノウハウをしっかりと抑えておきたいところです。