2019年に開催されたAPTOSコンペについての復習になります。画像コンペに関する知識が乏しいのでコンペ解法だけなく、画像処理に関すること全般知らないことをまとめていきます。
間違っている箇所があるかもしれませんがご了承ください。
コンペ概要
糖尿病網膜症という病気を眼球の画像から判定するタスクになります。
評価指標はQWKという指標でクラス分類のように思えますが、kaggleでは回帰で解いて閾値処理を行う方が精度が出るためか一般的なように思えます。

糖尿病網膜症に関してはこちらに、コンペ詳細はこちらになります。
上位解法
上位solutionはこちらのリンクにまとめられています。
Preprocessing
共通してやられていたのは背景の黒に対して、重要な情報が落ちないようにCropする処理、また、今回のデータの中で明暗差があったため、モデルが明るさ情報に頑健になるような前処理(2015年優勝者が行っていた処理)でした。
コードはこちらのnotebookを参考にしました。

全ての画像に同様のピクセル数の黒領域があるわけではないため、ピクセルの値から判定する実装が必要であったようです。
また、別の前処理方法として円形にクロップする処理が使われていました。
今回のデータは上記4つのタイプで構成されていたようですが、学習データには左から1,3番目のようなタイプが多く、テストデータでは一番右のタイプの画像が多かったことから円形にクロップすることで下記のように学習・テストを似たデータになるよう前処理しています。

グレースケールはRGBの値(赤、緑、青)を規定の割合(BT.601 と BT.709 といういい感じに混ぜる割合を決めた係数)で1つにまとめる処理が行われるようです。
ただ、open cvでimage = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)のようにグレースケール化する場合、ガンマ補正された状態でグレースケール化すると極彩色に対する変換が暗くなりすぎる問題が発生するようです。
一般に正規化したRGBの値(入力)と輝度(出力)は点線のように補正された値で保存されています。
これは人間の視覚は特に暗い色に対して敏感なので(暗所でカメラを使うと何も映らないが人には見えたりする)、グレーの線形の関係で保存するよりも、点線のように小さい入力に対して出力が多い(0~0.5の入力に対して0~0.72の識別バリュエーションがある)状態、つまり、低輝度の方が情報量が多い形式でデータを持つ方が、人間の知覚との都合が良いということが理由の1つとしてあるようです。(こちらのサイトがわかりやすかったです)
ただ、このまま表示させると明るい場所と暗い場所で入力と出力が非線形で実際の輝度とは異なるため、実線のようなγ曲線(y=x**2.2)の補正をかけることで、入力・出力の輝度を比例になるようにしています。(グレーの線形な線)
話しは戻り、何故そのままグレースケール化すると駄目かというと、保存されたRGB値で直接グレースケール化をすると点線の状態での値が使用されるため、一旦ガンマ補正を解いてグレーの棒線に変換した後(RGBの値と輝度の値が比例するように変換した後)でグレースケール化を行い、再度ガンマ補正(y=x**(1/2.2))を掛けることで正しくグレースケール化ができるようです。
こちらのサイトで詳しい解説があり大変助かりました。さらにdecolorというコントラストの情報を優先して保持するグレースケール化手法が紹介されており、今回のコンペの画像で比較してみました。

Decolorの処理だとコントラストが明確になり、何となく特徴量捉えられているような気がしなくもないです。
脱線したのでコンペの解法に戻ります。
8位の方は重複画像をHash値から判別する処理(notebook1,notebook2, notebook3)が重要であったと紹介しています。
様々なハッシュ関数(画像の輝度や隣接領域との差分によりハッシュ化する関数)というものが存在し、データを入力した時の類似データに対する出力が同じになることを利用して、重複データを抽出することができるようです。
ハッシュ関数により計算方法が違うようでこちらがわかりやすかったです。ライブラリもあり実装も簡単そうで汎用性が高そうな処理です。
Model, Loss
1位の方はモデル8個使用。
2 x inception_resnet_v2, input size 512
2 x inception_v4, input size 512
2 x seresnext50, input size 512
2 x seresnext101, input size 384
実験でinput sizeを384以上大きくすることによるメリットがないと判断したようです。2個ずつモデルを作成している理由は、ばらつきが大きいので異なるseedでの結果を用いて評価を行うためでした。
pooling層に対してもgeneralized mean poolingという方法が単なるaverage poolingより良かったようでgithubにコードがあります。
LossはsmoothL1を使用。他のLossも有効であるが、アンサンブルのために1つのLossを使用ということです(同一のlossによる方がweightを決めやすい?)。
smooth1はHuber Lossとも言われて下記の式で表されます。

δがパラメータで、aが真値と予測値の差分となります。なので閾値δより誤差が小さい場合は、二乗誤差(Mean Square Error)となり、閾値より大きい場合は絶対誤差(Mean Absolute Error)となります。
MSEは勾配が誤差に比例するため、大きく外れた値に対する勾配が大きくなり、勾配が一定のMAEと比較すると外れ値に影響されすぎてしまう性質があります。
Huber Lossは両者の良いところを取り入れて、誤差が小さい場合MSEを入れつつ、誤差が大きくなるとMAEに切り替わることで外れ値に対してもロバストになっているようです。
2位の方は速度の観点からEfficient-netのみを用いたようです。
B3 image size: 300
B4 image size: 460
B5 image size: 456
4位の方はEfficientNet-B7で224のサイズで学習させているようです。
意図としては、大きなモデルで大きい画像サイズを学習した場合、過学習しやすいことを発見したということで、大きいモデルには小さい画像を、小さいモデルには大きな画像を入力することで過学習を抑えているということでした。
EfficientNet-B7 (224×224) LB 0.840
EfficientNet-B6 (240×240) LB 0.832
EfficientNet-B5 (256×256) LB 0.831
EfficientNet-B4 (320×320) LB 0.826
EfficientNet-B3 (352×352) *LB Don’t Know
EfficientNet-B2 (376×376) LB 0.828
7位の方はSeResNext50, SeResNext101, InceptionV4を使用していて、Global Average PoolingをHeadとして使用したようです。
畳み込み層を通した後、特徴量マップを全結合層に通して最終出力を計算するというのが、通常の流れかと思います。
Global Average Pooling(論文 3.2章参照)では下図のように最後の各特徴量マップを全結合ではなく、特徴量マップ毎に要素の平均値を取ります。

Global Average Pooling Layers for Object Localization
この後、通常通り多クラスならば、Softmax関数を適用することで出力を得ます。
学習するパラメータが減少するので汎化性が向上することが期待できることや、全結合層を使用した場合、識別パターンの各特徴量マップ各位置に過学習することがありますが、GAPはマップ全体から計算されるので、この影響が小さいということが言われているようです。
損失関数に関しては focal + kappa>soft-CE (label smoothing)>CE の順番で精度が良かったということでした。
Label smoothingに関しては別途勉強が必要ですが、下記資料が詳しく書いてありました。
その他
・Label Smoothing: An ingredient of higher model accuracy
気持ちとしては閾値の境界上にある対象(分類に自信がない対象)や、間違ったラベルを持つ対象に対して、hard labelだと誤差伝搬が大きくなりすぎますが、ラベルを一様分布に近づける([1, 0]⇒[0.9, 0.1])soft labelに変換することで、誤差が大きくなりすぎず、微妙な対象に対して過剰な予測値がでることを防ぐことができるようです。
コードも公開してくれているのでチェックしてみます。optimizerはRAdamというものが良かったようです。
損失関数を最適化するOptimizerに関しては勉強不足ですが、2014年からAdamという確率的勾配降下法(全体から一部を学習データとして使用して、重みの更新を繰り返す方法)による、適応学習率(勾配の大きさにより学習率を調整するアルゴリズム)を用いるのが主流となっているようです。
ただ、これはデータが十分でなかったり、ノイズが多く含まれる場合に、学習初期の重みが大きく更新されるタイミングで確率的に選ばれ、局所解におちいる可能性があることから、学習初期では小さな学習率で、徐々に学習率を上げていくwarm-upという方法が併用されています。
さらにwarm-upには下記のビデオにあるように、学習済みモデルからfine tuningする際に、バッチサイズが大きいと学習済みモデルの重みが壊れてしまう問題を防ぐ働きもあるようです。
ただ、warm-upの問題点として、データセットに依存したパラメータ設定が必要であるため、試行錯誤を必要とします。
これに対して、RAdam(論文)は学習初期で重みの分散が大きくならないよう学習率を更新するようです。つまり、warm-upのパラメータ設定を自動でいい感じに調整してくれるAdamと言えるかと思います。(数式に関しては理解できていないので今後勉強する必要があります…)
Augmentation
1位の方は画像の明度、輝度、色相、彩度など下記を実施。
contrast_range=0.2,brightness_range=20.,hue_range=10.,
saturation_range=20.,blur_and_sharpen=True,rotate_range=180.,
scale_range=0.2,shear_range=0.2,shift_range=0.2,do_mirror=True,
2位の方は下記を実施。
Blur, Flip, RandomBrightnessContrast, ShiftScaleRotate, ElasticTransform, Transpose, GridDistortion, HueSaturationValue, CLAHE, CoarseDropout
4位の方は下記を実施
Dihedral, RandomCrop, Rotation, Contrast, Brightness, Cutout, PerspectiveTransform, Clahe.
7位の方はAlbumentationsというpackageを用いて実装していました。上記の処理を含めて色々と試してみました。こちらは別記事でまとめていきます。
その他
5位の方の解法ではPseudo Labelingが重要であったと紹介されていました。学習データとして使用したのは一部のスコアが高いものに限定していたようです。
10thの方は学習に使うデータセットを工夫していました。今回のコンペは2015年のデータを外部データとして使用できたみたいですが、2019年を5foldで分割した後に、4/5を2019年のデータ+2015年のデータ全てで学習データを作成して、検証データとして残りの1/5を使用したようです。(LB0.811→LB0.827)
11thの方はコードを公開してくれており、綺麗なコードでパイプラインを作成する際に参考になりそうです。
まとめ
画像処理に関する知識がほぼない状態だったので疲れた…
過去のコンペだから?なのか、割とアンサンブルと前処理などを愚直にやっていけばメダルが取れたように思います。