public 125th → private 22ndでkaggle初メダルを獲得することができました!
正直初メダル狙い・銅メダル狙いだったのでまさかの二桁前半順位でびっくりしました。ソロ銀を当面の目標にしていたので素直にうれしいです。
ざっくり解法と感想を書きたいと思います。 一応、Discussionの方にもゴミみたいな英語で解法は投稿しました→https://www.kaggle.com/competitions/feedback-prize-english-language-learning/discussion/369584
コンペの概要
NLPコンペでfeedbackシリーズ3回目。8年生から12年生の英語学習者によって書かれたエッセイから、cohesion・syntax・vocabulary・phraseology・grammar・conventionsの6つのスコアを予測する。スコアは1.0 ~ 5.0で0.5刻み。評価指標はMCRMSEで回帰タスク。
画像やテーブルデータは研究などで触ってきたのですが、テキストデータは全く触ってきませんでした。なので、序盤は自然言語処理に関する勉強を図書館で本を借りてきたりYoutubeを見たりしながら勉強するところから始めました。
解法はfeedback1&2、NBME、commonlitを参考にしました。
やったこと
「普通にFeedback3データで学習したモデル」+「Feedback1にpseudo labelを付けたデータとFeedback3データを混ぜて学習したモデル」+「Feedback3データで学習したモデルの埋め込みを使ってRapids SVRで学習したモデル」+「Pretrainedモデル埋め込みを使ってRapids SVRで学習したモデル」の加重平均アンサンブルをしました。
CV戦略は公開ノートにもあがっていたMultiLabelStratifiedKFoldをK = 5で使用しました。 ちなみにこのMultiLabelStratifiedKFoldの自分の使い方はDiscussionによるとバグっていたぽいのですが(参考:https://www.kaggle.com/competitions/feedback-prize-english-language-learning/discussion/368437)、これに気付いたのがコンペ終盤でどうしようもできず。序盤でシングルモデルのCVとLBが相関しなかったのはこれが原因なのかな?なんだかんだコンペ終盤にはCVとLB相関してたしこのバグは放置しました。
いくつかのステップでモデルを改善することができたため、段階的にやったことを説明していきます。
1. 強いシングルモデルを作る
メダルを確実にとるならシングルモデルが強くないとダメだと自分の中で勝手に思っているので、まずは強いシングルモデルを作ることを目指しました。 手作業でチューニングした結果、以下のような構成が上手く行きました。
- Epoch:
- base model: 4
- large, xlarge model: 2
- Learning Rate:
- base model: encoder -> 1e-5, decoder -> 1e-4
- large, xlarge model: encoder ->1e-5, decoder -> 1e-5
- 各層に異なった学習率を適用 (Layerwise LR Decay): encoderの一番手前の層が、一番奥の層の0.5倍の学習率になるように、奥の層から学習率を段階的に減衰するアプローチを使用しました。
- Batch Size: 8
- 一番最後の層を初期化
- Optimizer: AdamW (weight_decay = 1e-2)
- Scheduler: cosine_schedule_with_warmup (warmup_ratio: 0.1, num_cycles: 0.5)
- Criterion: SmoothL1Loss
- Head: Mean Pooling
- xlargeモデルを使う際は、層の手前半分を凍結
- gradient_clip_val: 1.0
- token length: 512 (とアンサンブル用に4096。基本512のほうが精度が良かった)
他にもなんかやってるかもしれませんが、こんな感じです。シングルモデルで多くの人がPublic 0.43を達成していて、それを目的にしていたのですがどうしても達成できず、アンサンブルにかけることにしました。
この段階でのシングルモデルのLocal CV最高はDeberta-xlargeの0.4502でした(Deberta-v3じゃなかった)。また、この段階でいろんなシングルモデルを7個ぐらい作ったのですが、それらのアンサンブルで既にprivate銀圏に入っていたみたいです。
2. Pseudo Labeling
上記の構成・パラメータで訓練された以下7個のモデルを使用し、アンサンブルでfeedback prize1のデータにPseudo Labelingしました。
- deberta-v3-base (token_length = 512)
- deberta-v3-base (token_length = 4096)
- deberta-v3-large (token_length = 512)
- deberta-v3-large (token_length = 4096)
- deberta-large (token_length = 512)
- deberta-xlarge (token_length = 512)
- muppet-large (token_length = 512)
リークを防ぐため、5分割それぞれのfoldでlabelingし、5通りのpseudo labelを生成しました。あとはfeedback1のデータと今回のデータに重複があったのでそれも除去しました。何気なくやってましたがこのリークを防ぐのがかなり重要でした。
ステップ1で構築したモデルを使ってfine-tuningする形で、feedback1のデータとfeedback3のデータを混ぜて1epochだけ学習しました。(要はfeedback1で事前学習→feedback1+3のデータで1epochだけfine-tuningしたみたいな感じ)
この段階でのシングルモデルのLocal CV最高はDeberta-xlargeの0.4472でした。ここでシングルモデルのPublicスコアが0.43に到達し、アンサンブルモデルでPublic銅圏に入ることができました。
3. Rapids SVR
pseudo labelingをした段階でメダル圏内に入ったし、もうアイデアが尽きて終わった感はあったのですが、Rapids SVRが上手く行くみたいなDiscussionがあったので、とりあえずやってみてアンサンブル候補に入れてみるかってなりました。今回私はpaperspace gradientを開発環境にしていたのですが、paperspaceはcolabと違ってRapidsが普通にインストールできたので良かったです。
まずはRapids SVRをステップ2で構築したモデルの埋め込みに適応しました。CVはいまいちでしたが、今までのモデルとアンサンブルをするとCV, LBともに増加したので採用しました。実はこの段階のモデルがPrivate最高でした。
次はhuggingfaceで公開されているようなpretrainedモデルに適応しました。こちらの方がCVが高かったです。今までのモデルとアンサンブルをするとさらにCV, LBともに増加したので採用しました。この段階で、アンサンブルモデルでPublic銀圏に入ることができました。
4. Ensemble
アンサンブルは加重平均(blending?)です。重みはNelder-Mead法で最適化しています。今回6つのターゲットがあったので、それぞれのターゲットごとに重みを調整しました。
また、Local CVへのOverfitを防ぐ目的で、5 foldすべてにおいて同じ重みを使いました。
最終的に以下のモデルをアンサンブルしました。
Model | Local CV Score |
---|---|
deberta_v3_base_4096 | 0.4536 |
deberta_large_512 | 0.4513 |
muppet_large_512 | 0.4571 |
deberta_xlarge_512 | 0.4502 |
deberta_v3_base_512_pseudo | 0.4489 |
deberta_v3_large_4096_pseudo | 0.448 |
deberta_xlarge_512_pseudo | 0.4472 |
deberta_v3_base_512_pseudo_svr | 0.4529 |
deberta_v3_large_4096_pseudo_svr | 0.4531 |
deberta_xlarge_512_pseudo_svr | 0.4538 |
deberta_v3_large_4096_svr | 0.4519 |
deberta_large_512_svr | 0.4553 |
deberta_v3_base_4096_svr | 0.4542 |
ensemble | 0.4436 |
モデル自体はもっと多く作ったのですが、モデルの容量とか精度とか最適化した重みとかいろいろ考えた結果これらのモデルが選ばれました。(機械的にじゃなくてちょっと手動で選んじゃった感じです。)
最終的には今までの手順を使ってall data trainをし、5 fold model と all data train modelの6モデルの予測値平均を取り、最終subとしました。私の手元ではLocal CVとLBがかなり相関しており、Best Local CV = Best Public LBだったので躊躇なくラストsubを選べました。
うまく行かなかったこと
- MLM(Masked Language Modeling)
- feedback1のデータを使って初期に試したが上手く行かなかった。
- Discussionにもデータの分布が違うとかでみんなうまく行ってなさそうだった。今考えるとアンサンブルモデルの候補に入れるのはありだったかも。
- AWP(Adversarial Weight Perturbation)
- Pytorch Lightning用にわざわざ実装したが使いこなせなかった。
- ほかの上位の方はこれでスコアを上げているので、チューニング次第で上手く行くはず。でもこれのチューニングに時間をかけるぐらいなら他のことに時間かけようと思っちゃいました。
- 2層以上の層の初期化
- 予測値(または埋め込み)+メタデータ(ミススペル数、単語数など)を使ってlightgbmなどでスタッキング
- 上手く行かず。特徴量エンジニアリングはtransformerにやらせるのが一番?
- 改行トークンの追加
- もっとアンサンブル用のモデルを増やす
- 実はもっといろんなモデルは作っていたのだが、これ以上追加すると精度が下がる傾向にあった。スタッキングなら行けた?
- 各モデルに対してちゃんとチューニングするべきだった?各モデルはdeberta-v3でチューニングしたパラメータを使用していたが、、
- mean pooling以外のhead
- weighted layer pooling, attention pooling, 1dcnn, lstmなど
- うまく行かなかったというよりはmean poolingに適したハイパラにチューニングされてしまった感はある
やりたかったができなかったこと
- スタッキング
- やりたかったけど時間がなくてできなかった。
- というか実装はしたけど、実装が悪かったのかやり方が悪かったのかCVが上がらなくて止めた。今後のためにちゃんと実装しておきたい。
- 疑似ラベル2回以上とか
- MultiLabelStratifiedKFoldのバグ修正
- 疑似ラベルを繰り返して生成
上位の方がされていたこと
- lstm, weighted layer pooling, concat some layer, attention pooling, GeM poolingなど様々なhead
- n層のフリーズ
- スタッキング
- AWPを遅めのエポック(3とか)から開始する
- 自分は基本2エポック目から開始していて上手く行かなかった。エポック数も見るべきだったかも
- pseudo labeling関連
- Knowledge Distillation→外部のデータではなく、与えられたデータに疑似ラベルをつける。これは初めて知った
- feedback3のtrainに疑似ラベルを付け、オリジナルの値との平均を取って新たな疑似ラベルを付ける。1位の方がされていた。
- feedback1だけで事前学習→feedback3でfinetuning。feedback1と3のデータの分布が違うみたいなので、こっちの方が今回のコンペにあったモデルになりそう。
- いろいろなやり方を皆さんされていた
- 山登り法でアンサンブルモデルを選択→3位の方がされていたが自分はうまく行かなかった。
- マルチサンプルドロップアウト
- foldを多くしてSVR
- 加重平均アンサンブルで負の重み→自分は0~1に重みを制限していたので使えなかったが、負の重みも使えるらしい?
- 大量のアンサンブル
- 自分は13モデルのアンサンブルだったが、多分これは結構少ない方だと思う。
感想
自分の解法を振り返ってみると、意外と上位解法と同じようなことができていたなという印象でした。pseudo labelingで(多分)リークしなかったこと・シングルモデルにしばらく拘ったのが上位に行けた決め手だと思っています。でもスコアや上位解法を見る限り金圏に行くにはもう何枚か壁がありそうでした。今回の結果には十分満足しているので、次はkaggle expertを目指して頑張ります。