いのいち勉強日記

Turingで自動運転の開発をしています。京大でPhDをとりました。Kaggle Grandmasterです。

第3回富士フィルムコンペ参戦記

第2回に続き第3回も富士フィルムコンペ、brainsconに参加して入賞することができました!
前回は特別賞入賞でチェキでしたが、今回は5位で賞品もとても良いカメラに。

 
入賞イベントで上位解法を聞くことができたし、懇親会でいろんな方とお話しできたのでとても楽しい大会でした。
この大会では色々実験したので、上位の方の解法と一緒に簡単に紹介したいと思います。半分ポエムですので気軽に呼んでいただければ幸いです!笑

f:id:sys-bio:20200204192017p:plain

コンペの内容と特徴

今回の問題は写真の画像からそれがいつ撮られたか年代を推測するといったものでした。
画像サイズは縦横256のカラー画像で、1979年〜2018年の40年のどこで撮られたかを予測します。訓練データが6686枚テストデータが1671枚あり、データセットのサイズは小さめでした。さらに、年代の分布がかなり偏っており最小クラスは3枚くらいしかなかったです。
評価指標は少し特殊で、上下1年のズレを許容した正解率でした。つまり、正解ラベルが1997年の場合、1996年、1997年、1998年のどれかを出力していれば正解とみなされます。
またテストデータにPublic / Privateはなく、サブミットも無限にできました。

ほとんどの方がクラス分類で解いており、回帰を使っている人はほとんどいませんでした。僕も回帰を少し試しましたがあんまり使えなかったです。
上位の方は最終的に0.76を超えていました。カメラゲットの基準はだいたい0.70くらいでした。

やったこと

次に僕が実際にやったことを簡単に紹介したいと思います。

まず工夫した点はターゲットを正解ラベルだけでなく上下1年も入るようにしたことです。
例えば、ターゲットが[0, 0, ..., 0, 1, 0, ..., 0, 0]とあったとします(以下、簡略化のため[0, 1, 0]とします)。これを[0.15, 0.7, 0.15]のようにしました。はじめ、最終層はSoftmaxにしていたので合計が1になるようにしています。また、この方法だと1979年と2018年の時に困るので、両サイドプラス1年して1978年〜2019年の42クラス分類として解いていました。
このアイディアを元に以下の条件を試しました。

target 最終層 Loss
[0.15, 0.7, 0.15] Softmax Categorical Crossentropy
[0.3, 0.4, 0.3] Softmax Categorical Crossentropy
[0.5, 0.9, 0.5] Softmax Categorical Crossentropy
[0.2, 0.6, 0.2] Softmax Categorical Crossentropy
[0.1, 0.8, 0.1] Softmax Categorical Crossentropy
[1.0, 1.0, 1.0] Sigmoid Binary Crossentropy
[0.5, 1.0, 0.5] Sigmoid Binary Crossentropy
[0.1, 1.0, 0.1] Sigmoid Binary Crossentropy


真ん中に重みがある方がスコアは比較的よくなりました。実験は全部ResNet18でやってます。

最初の方はとりあえず少し良さそうな以下の2つを使いました。

target 最終層 Loss
[0.1, 0.8, 0.1] Softmax Categorical Crossentropy
[0.15, 0.7, 0.15] Softmax Categorical Crossentropy

このようにターゲットを工夫して予測した値の結果をプロットしてみると、2箇所にピークが立つような画像がありました。
このあたりも加味した予測をするために、上下1年の予測値を0.5倍して足しました。
どういうことかというと、(..., 1996年, 1997年, 1998年, ...)の予測値が[..., 0.1, 0.4, 0.2, ...]とすると、真ん中の1997年の予測値は...
0.1 x 0.5 + 0.4 x 1.0 + 0.2 x 0.5 = 0.55
これを1997年の予測値としました。そうして1979年〜2018年の予測値としました。はじめにターゲットを作成する時に1996年と2017年を追加しましたが、この重み付けの処理をする時は追加したこの二つの年代については計算しなかったので、結局40クラスの予測結果を出したことになります。
この重み付け計算をすることでだいたいスコアが0.01~0.02くらい良くなったと思います。

AugmentationはMixupと下記のものを使いました。

def get_imgaug_seq_soft(self, p=.5):
    return Compose([
        RandomRotate90(),
        Flip(),
        Transpose(),
        ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.2, rotate_limit=45, p=0.2),
        OneOf([
           CLAHE(clip_limit=2),
            IAASharpen(),
            IAAEmboss(),
            RandomBrightnessContrast(),            
        ], p=0.3),
        HueSaturationValue(p=0.3),
    ], p=p)

これに加えてMixupで学習した後、Mixupなしで追加学習させました。
これはこの論文を参考にしています。この手法を使うと少しですが精度が良くなることが多いです。
arxiv.org

あとは...

  • BackboneはSEResNeXT50
  • StratifiedKFoldで5CV
  • 検証データで評価指標が最大となるEpochのモデルで予測
  • アンサンブルは適当に重み付け

こんな感じでやったものがだいたい0.7265くらいでした。
コンペ始まって3週間くらいまででやったことはだいたいこんな感じです。

そして、ここからちょいちょい実験しつつもベンガル語と浮世絵のコンペをやってて、気がついたら終了1週間前くらいでした。

この時点で0.76くらいのスコアを出してる人も何人かいて、なんとか追いつきたいと思いつつ良いアイディアが思いつきませんでした。なので、とりあえずアンサンブルと擬似ラベルでゴリ押ししていきました。

擬似ラベルは、テストデータが少なかったので確信度でセレクションせず全て使いました。
ターゲットは[0.15, 0.7, 0.15]のSoftmax/Categorical Crossentropyに固定して以下のモデルを5CVやってアンサンブルしました。
試したモデルは以下ですが、結局良かったのは太字のものをアンサンブルしたものになります。

  • EfficientNetB4
  • DenseNet121
  • SEResNeXT50
  • Xception
  • InceptionResNet

これで0.7384くらいになりました。

さらに、もう一回擬似ラベルをしました。
そしてターゲットは[0.5, 1.0, 0.5]のSigmoid/Binary Crossentropyに変更し、BackboneはEfficientNetB4で5CVしました。
これで0.7432くらいになりました。

まだなんか見落としているなーと思いながらなかなかスコアが上げられず苦戦していました...

そして最終日。ふとした瞬間にあることに気がつきました。
Augmentationミスってるんじゃないか?ってことです。
そう思ったのはコンペのSlackを何回か読み返していた時に、

参加者「そもそも人間でも難しいのに、なぜ予測できるのですか?」
運営者「運営側でも、人間でも難しい問題なので、できないと思っています」

このようなやり取りがありました。
人間でも難しい問題で、運営側もできると思ってない。ですが実際は上下1年のズレを許しているとはいえ、0.75くらいの精度が出ています。これってつまり訓練データとテストデータでとても似ている画像があるんじゃないか?この問題を解くことに関していえば、変にAugmentationしてしまうと逆に特徴量を隠してしまっているんじゃないか?そんな風に考えました。
そこで先ほど挙げたAugmentationを全部抜いてMixupだけにしました。そのほかの条件は擬似ラベルを最高精度のものにした以外は1つ前と同じ条件で行ったところ...
スコアが一気に0.75583まで上がりました。
最終的なスコアはこれになります。

時間が少なくこれ以上は諦めてしまったのですが、今思えば、AugmentationをかけていたモデルでもAugmentationなしでFine tuningすれば、短い時間でもっと色々試せたな...と反省しています。2段階で学習する論文のやつがうまくいくなら、その方法でもきっとうまくいきそうな気がします。

後日談ですが、入賞者イベントの時に問題を作成した方にデータセットについて聞いてみました。訓練とテストで同じ写真の画像(もしくは同じ画像から切り取った画像)はなかったけど、同じユーザーが撮った写真は訓練/テストに振り分けたとおっしゃられていました。僕が使っていたAugmentationではその情報が消えてしまっていた可能性があります。
入賞者イベントの懇親会で他の参加者から教えていただいたのですが、今回良くなかったのはCLAHEのようです。

これはヒストグラム補正の一種で、前回の富士フィルムのコンペ、日付認識の時にも使われていました。

KaggleでもRecursion Cellular Image Classificationの10位の人が使っていたりとちょいちょい見かけます。

今回のコンペではこの処理をすると逆に精度が下がってしまうパターンでした。ギリギリまで気づけなかったのは残念でしたがいい経験になりました。ちゃんと実験しないといけないですね...。

その他試したことは...

  • GeM...あんま変わらず
  • Focal Loss...あんま変わらず
  • Up sampling...あんま変わらず、時間かかる
  • 画像サイズ 320...ちょっと上がる、時間かかる


 
あと回帰もうまく使えないかということで、こんなモデルも作成しました。

f:id:sys-bio:20200204170408p:plain
回帰付きモデル

これでだいたい0.70くらいで、Mixupを使わない場合と比較すれば結構良いモデルです。しかし、回帰を使っているのでMixupができずこれ以上スコアを向上できる気がしなかったのでやめました。今思えば、回帰の部分も無理やりMixupに組み込んだり、Backboneの部分だけ最初にMixupで学習してからこのアーキテクチャに埋め込んだらおもしろいなと思ったりしましたが、後の祭りです。

上位解法

上位の方の解法で印象に残った部分を簡単に紹介します。

3位の解法

学習時の画像サイズを変えたモデルをいくつも作成しアンサンブルされてました。ラベルは[0.9, 1.0, 0.9]でSoftmaxでした。画像サイズを色々混ぜるのは有効だったそうです。詳細はブログにまとめてくださっているのでそちらを参考にしてください!
hrhr08hrhr.hatenablog.com

2位の解法

印象的だったのは2モデルで出した40クラス分類の予測値を単層のニューラルネットでアンサンブルしてたところです。0.71のEfficientNet(確かB3)と0.73のResNet50の結果をニューラルネットにかけることで0.76までスコアを上げてました。
個人的にはMixupのalphaを4に設定していたところも勉強になりました。これまでalphaは0.2~0.7くらいまでしか設定したことはなかったのですが、2位の方はalphaを色々試した結果、4が一番良かったらしく興味深かったです。300 epochsくらい学習されていたので、その辺も関係してくるなと感じました。

1位の解法

1位の方も画像サイズをいろいろ試されていました。また、BackboneもEfficientNetB7とかなり大きいモデルをつかってたのが印象的でした。環境はGoogle Colaboratoryだったそうで、画像サイズを上げた時にバッチサイズは下がるが学習率を調整することでうまく学習されてました。またCVを15 flod作られていたところも印象的でした。あと一番衝撃を受けたのは、学習を回すためにGoogleアカウントを10個作られていたところです。スコアを上げ切ってやるという意思の部分でも完敗でした。

Never Stop賞

前回、上位に入られていたので今回は入賞者にはなれなかったのですが、上位の方がみんな使うようなベースラインを公開されたり、1番高いスコアを出されたりと今大会の盛り上げ役でした。解法はブログにまとめてくださっています。
sh1r09.hatenablog.com

シンプルな解法だし、前処理、後処理の部分がスマートですごく勉強になりました。特に後処理は僕がやったやつよりも秀逸で、よくデータを見られているなと感じました。

まとめ

何度も言いますが、富士フィルムのコンペ、楽しかったです!今回はデータサイズも小さく取り組みやすい課題でしたが、工夫の余地がたくさんありました。さらに、入賞者イベントでいろんな方とお話しできるのは本当に素晴らしい機会だと思います。次回もまた開催されるみたいなので、興味ある方はぜひ参加してみてください!僕も参加資格があればまた参加したいです。
最後になりましたが、大会の開催に関与してくださった全ての皆様に心からの感謝を申し上げます。また、入賞者イベントで話しかけてくださった参加者の皆さまにも感謝です。またどこかでお会いできたら嬉しいです。