こんにちは。ユウキ|Kagglerです。先日、自分が参戦していたKaggleのcassavaコンペが終了しました。参加されていた皆様、お疲れ様でした。そしてメダルを獲得された皆様、おめでとうございます。
今回も、前回に引き続きコンペの振り返りをしていこうと思います。(前回はMoAコンペに参加しました。今回の取り組みは前回の反省点も踏まえております。こちらのリンクから見れますので、お時間が許すならばご覧になってください。)
※2/24追記:
- いくつかの誤字脱字を直しました。
- リンクを埋め込んでいる箇所の文言を「ここ」から「こちらのリンク」に直しました。
- 最終的なsolutionの「DeiT」は入れていなかったため、削除させていただきました。ですので、正しくはEfficientNetB3(Noisy_Student) + SeResNeXt となります。
目次
- 最終結果
- 手短に自己紹介
- cassavaコンペについて
- 具体的な取り組み
- その他の工夫(主にkaggle日記について)
- 反省点と今後の目標
最終結果
**415/3900 上位11%**でした。
銅メダルまであと25人、スコアにしてあと0.0001でした。悔しい...
手短に自己紹介
twitterでユウキ_Kagglerという名で活動している、東京大学理科一類の1年生です。Kaggle Contributorです。専攻はまだ決めかねていますが、量子情報理論に興味を持っていて、量子機械学習をできたらいいなーとぼんやり考えています。自分の興味を少しずつ絞るため、Kaggleを通して色々なデータに触れています。
cassavaコンペについて
概要
・タスク
キャッサバの画像から、4種類の病気及び健康の5種類のクラスに分類する多クラス分類です。
・データ
トレーニングデータ:21397枚
テストデータ:15000枚(そのうち31%がPublicデータ、残りの69%がテストデータ)
となっています。tfrecord形式でもデータが配られていました。(僕はPytorchを使っていたので用いていませんが...)
全体で約5GBでしたので、画像コンペにしてはかなり軽い部類に入るのではないかと思われます。
ちなみに、Publicデータのうち、手元で見れるのは1枚のみでした。これのお陰で推論でGPU quotaをほぼ消費せずに済んだのでNotebook勢にはありがたい仕様です。
・評価指標
accuracyです。シンプルですね。ただ、今回のデータはクラスの割合が不均衡だったので、本当にaccuracyが評価指標として適切であったかどうかについては議論の余地があります。
・その他の制約
code competitionでしたので、Kaggle Notebook内で推論を行う必要がありました。また、CPU, GPUともに9時間以内に収めなければいけなかったです。ちなみに、推論の際にはInternetは接続を切らなければいけなかったので、tpuは学習でのみ使用可能でした。
難しかった点
ラベルノイズが激しかったことです。これに尽きると言っても過言ではないと思います。しかも今回のコンペはトレーニングデータに止まらずtestデータも含めて全てNoisyだったことが
このコンペ最大の特徴であり難点でした。常にDiscussionの話題になっており活発な議論がされていました。
(ぶっちぎりで優勝した一位のチームをのぞき、これを本質的に解決できたチームはなかったのではないかと思ってます。)
具体的な取り組み
ここでは、自分が取り組んできたことを時系列順に振り返ります。なお、取り組んできたことの詳細は全てgithubのリポジトリのREADME.mdにkaggle日記(後述)として残しています。
ガチの画像コンペ初心者が日々何を考え、どういう工夫をしてきたかが悉く書かれています。こちらのリンクから飛べるので、ぜひそちらも参考にしてください。
(今振り返っても、え??と思うような勘違いをしていたりしますが、敢えてそういう点も全て残してます。温かい目で見てくださると嬉しいです。)
2ヶ月前〜1ヶ月半前
自力でベースラインを作ろうと試みますがエラー続きでいつまで経ってもsubに辿り着ける気がしなかったので、仕方なく公開Notebookを写経してsubしました。(次こそ成功させます...)
この時点ではモデルはResNextでしたが、Kaggle Notebookのみで戦うためGPU quotaを節約する必要があり、当分の間EfficientNetB0にモデルを固定します。
その次に以下のような基本的な事項を試していきました。
optimizerを変えてみる
AdaBeliefやRAdamなど最新のものをとりあえず入れてみましたがほぼスコアは変わりませんでした。一旦AdaBleliefを使いますが、後からAdamに戻しました。原論文ではこれらはAdamを凌駕していましたが、実際に使ってみるとそうでもなかったです。SOTAを取り入れても必ずしもスコアが上がらないということを改めて感じると同時に、脳死で良さげなものをぶち込んでも何にもならないと気を引き締め直しました。
バッチサイズを上げる
ベースラインはバッチサイズが32でしたが、バッチサイズを上げれば(平均や分散の移動平均が安定することにより)学習が安定してノイズに引っ張られづらくなるのでは、と考えたからです。また、学習の高速化も期待していました。googleの論文によるとバッチサイズと学習率を線形に上げれば精度を落とさずに高速化することが期待できるとあったので、バッチサイズと学習率を4倍にすると論文通りに高速化しつつCV, LB共に改善されました。調子に乗って8倍にするとOOM(Out Of Memory)になってしまい、これが「メモリにも気を遣わなければ!」と明確に意識するようになるきっかけになりました。頭ではわかっていたつもりですが、体感すると他人事ではなくなります。経験って大事ですね〜(雑魚くてごめんなさい)
最初の方のepochは出力層以外のパラメータを固定する
最初から全ての層のパラメータを更新してしまうと、事前学習で備わった特徴量抽出機能が壊れると聞いていたからです。最初の1epochだけは出力層以外のパラメータを固定して学習率を10倍にするとCV、LB共にわずかに上昇しました。(誤差の可能性もあります。)
画像サイズを上げる
ベースラインは256×256で学習を行っていたので、512×512に変更したところ、スコアが一気に伸びました。解像度は重要なんだなあと学びました。ただし、学習時間もかなり伸びるため、スコアの伸びだけを確認して一旦保留にして後から画像サイズを上げることにしました。
ちなみに、EfficientNetのコンセプトは幅・深さ・解像度を適当にスケールすることで、より効率的にモデルを改善する(おそらくEfficientの由来)ことであるため、本来入力サイズが224×224のEfficientNetB0で入力サイズを512まで上げることはその思想に反していると考えて、後からモデルのサイズを上げることになります。
学習させるfoldを1つだけに絞る
これはスコアを改善するためではなくGPU quotaを節約するためです。これのおかげで一回の実験のコストがかなり下がったので、実験回数を増やすことができました。
TTAを入れる
TTAとはTest Time Augmentationの略で、推論時に、一枚の画像に対してData Augmentation(データ拡張)をして複数回予測をしてそれの平均をとるという手法です。ポピュラーな手法なようですが、今回初めて知りました。ノイズに対する本質的な改善ではないかなとは感じましたが、計算資源もほとんど食わないので早めに取り入れました。前回のMoAの反省点としてアンサンブルに移るのが遅すぎたことがあげられたため、このような手法は序盤から積極的に取り入れることに決めていました。この手法の注意点として、学習時に用いなかったaugmentation手法は入れてはいけないことが挙げられます。モデルがそのaugmentationに対応していないため一気に精度が落ちます。
ちなみに、discussionではTTAをしたら精度が悪化したという人がいたり、TTAをした方が精度がいいという人がいたり、人によって差がある印象でした。後述する他のaugmentation手法に関しても同じような傾向がありました。ラベルノイズに引っ張られているからだろうなと思っていますが、確証はありません。
複数epochのモデルを用いる
valid lossが一番低いモデルを使うのが通常ですが、10epochのうち6, 7, 8, 9epochのモデルを全て推論で使いました。目的は複数ありましたが、一番大きな目的はearly stoppingを行うことです。今回はデータがNoisyであるため、valid lossが一番低いところが一番いいモデルかと言われるとそうではないと考えていました。discussionでもearly stoppingで過学習を防ぐことの必要性が議論されていたため、どうせなら全部使ってしまえと考えました。複数のepochを全て用いることで、アンサンブルの効果が出ることも狙っていました。GPU quotaを節約しつつモデルの力を最大限に出すための妥当な選択だったと思います。最終的に各epochごとにTTAを5回することにしました。実際、スコアがかなり伸びました。
損失関数を変える
これがラベルノイズに立ち向かう一番大きな取り組みです。Discussionでも色々な手法が取り上げられており、
- Bi-Tempered Logistic Loss(原論文)
- Taylor Cross Entropy Loss(原論文)
- Symmetric Cross Entropy Loss(原論文)
などを試しました。趣旨からは外れるかとは思いましたが、Focal lossやFocal cosine lossなども、discussionで話題に上がっていたため全て試しました。上に挙げた3つの手法はどれもノイズが入ったデータの中で如何にして安定的に学習を進めるかということに主眼が当てられた手法です。各手法の詳細な説明は省略します(kaggle日記に短くまとめております。)。
実験を繰り返した結果、Bi-Tempered Logistic Lossを試すことに決めました。(後からアンサンブルに加えることになるSeResNextはTaylor Cross Entropy Lossを用いることになります。)
SAM optimizerを導入する
optimizerを変更してみるのところで言及してもよかったですが、少し趣が異なるので分けました。SAMとは、より平坦な極小値を目指す手法で、具体的には、ただ損失関数の値が最小になるようにパラメータを変更するのではなく、現在の地点の周囲で一番損失関数の値が大きいところの勾配を用いてパラメータを更新します。ですので、optimizerそのものというより更新方法の変更であるため、SAM単体で用いるのではなく、Adamなどと共に用いることになります。利点としてはlossの下がり方がとても安定することで、難点は1回の更新のために2回逆伝播をする必要があるためにメモリを食うし時間もかかることです。導入している人はほぼいなかったですが僕自身はいいと感じ、使い続けることにしました。publicスコアの上昇はわずかでしたが、Privateスコアは0.006程度上がっていたようです。
左が通常のoptimizerが目指すような極小値で、右がSAM optimizerが目指すような極小値です。しかし、2回更新するのが面白いアイデアだと感じますね。Adamの良さがそのまま生かされてるところもまた良いです。
画像:"Sharpness-Aware Minimization for Efficiently Improving
Generalization"
過去のコンペのデータを入れる
実は、このタスクが全く同じcassavaコンペは1年前にも開催されていたため、外部データとして前回のデータも用いることができました。ちなみに、その時のaccuracyは0.93を超えていますが、今回は結局0.91程度で終わったので、ここからもノイズがより厳しくなっていることがわかるかと思います。
1ヶ月半前〜3週間前
大学の期末試験を優先していたため、大きな実験はしませんでした。
モデル・入力サイズを変える
GPU quotaを使わないのはもったいないと判断し、とりあえずモデルと入力サイズの大きさを変える実験を繰り返し行いました(思考はほとんど止まってます)。それと同時に、推論の際にはバッチサイズをもっと大きくとってもいいということに(今更すぎますが)気づいたので、それも含めてひたすらモデルを作ってsubmitを繰り返しました。その過程で、バッチサイズを変えるごとにスコアが変動してしまうことに気づきます。バッチサイズが変わることで各画像に作用するAugmentationが変わってしまうためです。LBスコアが、0.001とかならともかく、最大0.004も変わってしまっていたため、ノイズは思っていたより激しいんだなあと感じました(もっと深く考えるべきだった)。ちなみに、この段階のEfficientNetB3_Noisy Studentがこのコンペを通してのほぼ最高スコアでした。
EfficientNetをアンサンブルしてみる
上述のようにモデルの画像サイズを細かくいじった理由として、EfficientNetB0~B4でアンサンブルをしようとしていたことが挙げられます。入力サイズを変えればモデルの予測の相関係数が落ちてアンサンブルの効果が出るだろうと考えていました。しかし、結果的にこの試みは失敗に終わります。スコアは全く上がりませんでした(なんなら落ちました)。これは、複数epochに渡ってTTAを何回も行っているうちにアンサンブルの伸び分も食い尽くしてしまったからだと考えていました。ここで実際にoofに同じようにTTAをやってみて、予測の間の相関を見ればよかったものをそれをせずに短絡的に決めつけてしまったことは本当にだめです。反省。
3週間前〜
ここからほぼ一切スコアが伸びませんでした。精神的にかなり辛かったです。
mixup・cutmix・fmix・snapmixを試す
ここ2年ほどで台頭してきたaugmentation手法を片っ端から試しました。どの手法も二つの画像を混ぜて、ラベルも同じ比率で混ぜることはほぼ共通です。mixupはただ混ぜるだけ、cutmixは片方の画像の矩形領域を切り抜いてもう片方の画像を貼り付けてラベルは面積比で混ぜる、fmixはcutmixの矩形領域が曲線になったもの、
snapmixは矩形領域の重要度的な値を元にラベルを混ぜるという工夫がなされています。(詳しい内容については原論文を参照してください)。ラベルノイズの原因として、キャッサバが病気になりかけていた場合のラベリングが不安定であることが指摘されていました。例えば、ほぼ健康であるが、一部の葉だけはわずかに病気の兆候が見られる、といった感じです。このことから、二つのクラスの間のようなデータで判断を誤っているなら、mixupなどを使って実際に中間のデータを作って学習させれば精度が上がるのではないか、という仮説を立てました。しかし、結果としてはどの手法を用いてもスコアが下降してしまいました。discussionでも、効いている人がいる一方でスコアが悪化している人もいるため、やはりばらつきがあるようでした。その理由については最後まで明確に理解することはできませんでした。
画像:SnapMix: Semantically Proportional Mixing for Augmenting Fine-grained Data
denoisingをしてみる
PANDAコンペの一位の解法にdenoisingが大きく貢献していたため、部分的にでも使えないかと試してみることにしました。ノイズに引っ張られないようなモデルも作っておいて、アンサンブルに用いることでスコアが上がるかもしれないと考えました。cleanlabというライブラリ(原論文)を使ってラベルを作り直してみましたが、どうしてもスコアが下がってしまうため断念せざるを得なかったです。原因としては、PANDAコンペはトレーニングデータがNoisyだがテストデータが比較的cleanであったのに対して今回はテストデータも含めてNoisyだったことが考えられるため、効かなかったのが妥当だと思います。他の方もチャレンジされていたようですが、同じく効かなかったようです。
モデルを追加する
コンペも終盤に差し掛かり、もう一度アンサンブルにチャレンジしようと考えてSeResNeXtやDeiT(Data-efficient Image Transformer)などのモデルを作りました。コンペ終了直前にNFNetというモデルがSOTAを更新したようでしたが、時間的にギリギリすぎたので断念しました。
TPUを使う
GPUだけでは全然足りなかったので、TPUをPytorchで使ってみました。(恥ずかしながら、pytorchでTPUを動かせることをそれまで知らなかったです。)TPUを使えば、GPU約40hに加えてTPU30hが加わるので、だいぶ楽になります。最初は自分のベースラインに頑張って組み込んでいましたが、全く思うように動いてくれなかったので、仕方なく公開Notebookを拝借することにしました。次回のコンペではしっかり動かせるようにします。
スタッキングをしてみる
こちらの記事を参考に、ニューラルネットを用いたスタッキングにチャレンジしてみました。終了二日前なのでかなりギリギリです。試したのは
MLP(多層パーセプトロン)、1D-CNN, 2D-CNNです。結局あまり効かなかった上に、最終的に使おうとしていたサブがTimeoutになるという致命的なミスのせいで、サブの回数が足りなくなり導入するには至りませんでした。元記事を書かれてる方も試したようですが、単純にモデル間の平均をとったほうがスコアが高かったようです。
最終的なsolution
model | Public | Private |
---|---|---|
EfficientNetB3(Noisy Student) only | 0.9009 | 0.8977 |
EfficientNetB3(Noisy Student) + SeResNeXt |
0.9009 | 0.8970 |
となりました。ちなみに、最後の最後でSeResNeXtの入力サイズを512に上げたものを作ったのですが、Timeoutしてしまい最終subに選ぶことができませんでした。(スタッキングのところで言及したサブを再調整したのに再びTimeoutになりました。情けない...)丁寧にTTAの回数を調整すれば銅メダル相当になっていたようなので、惜しいことをしたと思います。こういう詰めの甘さも今回の反省点でした。全体的に行動が雑なので改善が必要です。 |
その他の工夫について
前回のコンペMoAでの1番の反省点はデータの管理が雑であることでした。戻りたいバージョンに戻ることができず、大変苦しい思いをしました。それを踏まえ、今回はKaggle日記を用いることにしました。Kaggle日記とは、Kaggle Expert(2021年2月当時)であるfkubotaさん(@fkubota_)が紹介された手法で、日々の記録・アイデアなどさまざまなことを一括で管理するものです。(fkubotaさんの記事にはここから飛べるので、より詳しいことを知りたい方はぜひご覧になってください。)僕もfkubotaさんと同じく、githubのREADME.meに日々行った実験結果やDiscussionから得た知見などを全て書き、タスクをIssuesのところに書いて管理しました。コンペ中に入ってくる情報は膨大で、書き留めておかないと後で、「あれ?何か思いついたことがあった気がするんだけどな...」なんて事態が多発します。
そして1番いいところが、Notebookを名前で管理しなくなるということです。前回のコンペで、前のバージョンに比べてどのような変更をしたのかを全てバージョンの名前で管理していました。KaggleのNotebookのバージョン名には文字数に限りがあるため、当然、変更内容を全て書くことができません。結局気づいた時には戻したいバージョンに戻せなくなっており、地獄を見ました。(本気で撤退しようとまで考えました...)Kaggle日記に変更したことを書きたい分だけ書いておいて、バージョン名はver◯◯と数字だけにしておくことで、かなりスムーズに管理することができました。ちなみに、推論のNotebookに関しては、対応する学習のNotebookのバージョン名をそのままつけて管理しました。
こちらがbefore(MoAコンペ)
こちらがafter(cassavaコンペ)
になっております。かなりスッキリしましたね。最後まで「このバージョンに戻せない!!!」なんて事態にはならずに済みました。変更に関しては以下のように詳細に書かれています。
後から振り返ることも容易ですし、自分の実績として残ることも嬉しいポイントです。この記事もかなりスムーズに書くことができました。導入コストもほぼ0ですので、気になる方はぜひお試しください。
他の工夫点としては、対照実験を意識したことです。MoAコンペの際には、焦ったが故に一回の実験で複数のパラメータを変更してしまうことが度々あり、どのパラメータがスコアにどのように寄与しているのかが判断できず、何回も同じような実験をする羽目になり管理も大変になりました(PCAのn_componentsを変えてdropoutの割合も変えてしまう感じです)。一回の実験で変更する事柄は基本的に1つだけにすること。中学1年生で習うことを今更明示的に意識したのは恥ずべきことですが、読んでくださる方が同じ轍を踏まぬよう、共有させていただきます。
反省点と今後の目標
####反省点
ほぼこれまでに書いてきたようなものですが、ここでは2つ挙げておきます。
・モデルや手法と向き合いばかりでデータと向き合うことを疎かにした
今回は「ラベルノイズをどうにかする」のほぼ一点を解決するために色々なことを試したわけですが、実際にどのクラスとどのクラスが特に混同されている、などといった観察は自分ではあまりしませんでした。ほぼdiscussionで得た知識のみです。他にも、アンサンブルが効かなかった時に、「そもそもCVは良くなってるの?」ということをまともに調べたときには時既に遅しといった感じでした。EDAであったりそういう分析をもっと積極的にするべきであったと深く反省しています。
・根拠の薄い判断を繰り返した
上で書いたことと繋がりますが、例えばEfficientNet同士でアンサンブルしたもののスコアが全く良くならなかったとき、「まあ他のモデルだったら効くんじゃね?」ぐらいのノリでスルーしてしまいました。結局終盤でもまともに効いていなかったため、このときにしっかり考えておくべきでした。
今後の目標
今回のコンペを通して画像コンペの魅力に気づいたので、今回得た知見を生かせるようなコンペに出て今度こそメダルを獲りたいと思います。これまでは全てソロで出ていましたが、次回からはチームマージすることも視野に入れます。Kaggleを通しての目標はデータサイエンスの力を磨くこと、そしてその中でメダルを獲得することであり、ソロでの参加はあくまでその手段です。しかし、いつの間にかソロで参加すること自体が自己目的化しかけてる節があるので、そこは切り分けて頑張ります。今年中にExpertになります。
ここまで読んでくださりありがとうございました。よきKaggleライフをお過ごしください。