Kaggle Automated Essay Scoring 2.0 コンペ上位解法まとめ
これは何?
この記事は、2024年4月〜7月に行われた Kaggle Learning Agency Lab - Automated Essay Scoring 2.0 コンペの上位解法をまとめた記事です。
まず最初にコンペの概要およびデータセットの特徴を紹介した上で、上位解法 (1-4位) の紹介をします。筆者はコンペに参加していたもののあまり真剣にやっていなかったため解法の理解が間違っている箇所もあるかもしれません。間違いにお気づきの際には X(旧Twitter) などでご指摘いただけますと幸いです😇
(注記: この記事を書き上げた後に tk2 さんが同様のコンペ上位解法まとめを書かれていることに気づきました汗。コンペの背景などは tk2 さんのものの方が丁寧に書かれているのでこちらを参照されると良いかと思います😇)
コンペ概要
中高校が書いたエッセーを自動的に採点するシステムの開発を目的としたコンペでした。12年前に行われた第一回エッセーコンペからの進歩を図るという意味合いで実施されたコンペとのことです。
参加者は与えられたエッセーデータセットを使用して、1-6のスコアを予測するモデルを構築することが求められました。与えられたエッセーは何らかのテーマ(プロンプト)に基づいて書かれた作文とそれに対する評価であり、テーマが与えられていないエッセーは存在していませんでした。
コンペのDisucssionでも積極的に議論がなされていましたが、このコンペでは(シンプルな)CVとLBの相関が取りづらいという点が特徴的でした。以下に述べる通り、Trainセットのデータに2つの種類のデータが混在していたことがその原因だった模様です。上位入賞者は須くこの事実を発見し対処していたのが印象的でした。以下に、データセットの特徴について簡単に述べます。
どのようなデータセットだったのか?
- 何らかのテーマ(プロンプト)に基づいて書かれた作文に対して、評価者が1-6でスコアをつけたデータでした。
- Persuadeというコーパスから取得されたデータセットであることがコンペの序盤から議論されていました。
- Trainセットは以下の2種類のコーパスで構成されていました
- Persuadeコーパスから取得されたもの: 約13,000件
- Persuadeデータ("古い"データ)と呼ばれていたため、以降はこの表記で統一します。
- Persuadeコーパスに存在しないもの: 約4,500件
- Kaggle-Onlyデータ("新しい"データ)と呼ばれていたので、以下同文。
- Persuadeコーパスから取得されたもの: 約13,000件
- これら2種類のエッセーは共通したプロンプトに対して書かれたものでしたが、評価基準が異なっていたのか評価の分布に違いがありました
- この事実を把握して対処しないと上位に入ることは不可能でした
- Persuade コーパスには独立型の作文課題(フリーテーマのエッセー)も含まれていますが、これらは今回のコンペのデータとして使用されていませんでした。
- また、これらのデータを用いてデータセットを水増しするといった改善は上位解放には存在してませんでした。
上位陣に共通していた工夫
(書いた後で読み返してみて2-4は当たり前のことすぎるのであえて書いておく必要があるのかどうか迷いましたが、まぁ共通した工夫ではあるので一旦残しておきます😇)
1. データセットの偏りへの対応
2段階学習を行ってデータの偏りに対処する上位解法が目立ちました。具体的には以下のようなプロセスでした。
-
- 事前学習(Pre-train)段階:
- Persuade コーパス("古い"データ)を使用してモデルを学習
- 場合によっては、補助的なデータセットも追加して使用
-
- 微調整(Fine-tune)段階:
- 事前学習したモデルの重みを読み込む
- "新しい"データ(Kaggle-Onlyデータ)のみを使用して、モデルをさらに学習させます。
テストデータに近い分布のKaggle-Onlyデータに対する性能を向上させるのが重要だったようで、1位解法ではこのアプローチにより、PublicLBのスコアが少なくとも0.015ポイント向上したとのことです。
いくつかの上位解法で2段階目の学習の正解ラベルに工夫がなされていました。1位解法のPseudo Labeling、3位解法はSoft Labelingなどです。詳細は各解法の説明部分をご参照ください。
今回紹介する上位解法のうち、4位解法だけは2段階学習を行わず、テキストに新旧どちらのデータかを明示するラベル([A][B])を付与してモデルにヒントを与えるというアプローチでした。
2. モデル (DeBERTa)
- 今回紹介する上位解法は全てDeBERTaが基本でした。
- DeBERTa v3 largeが多かったですが、3位解法はbaseの方が学習が安定していたと報告していました。
- 2位と3位はMLM(Masked Language Model)による事前学習も追加していました
- 4位解法が
Qwen/Qwen2-1.5B-Instruct
を利用していて非常に珍しかったのが印象的でした。 - 言うまでもなく全ての解法がアンサンブルを行なっていました。
- この記事では各解法でのモデル一覧の紹介はしません。元のSolutionをご覧ください。
3. 回帰問題として解く
- これも多くの上位解法で共通でした
- 1-6のスコアの分類問題ですが、回帰問題として解かれていました。
- 回帰問題ではDropoutをゼロにするのが良いらしく、それに倣った解法が多かったです。
4. QWK閾値の最適化
- 評価指標がQWKなので多くの解法で閾値最適化が行われていました
- ただしオーバーフィッティングを避けるために閾値最適化を行う手法に多少の工夫が必要だった模様
- 3位解法が閾値最適化が効かなかったと書いていたのが印象的でした。
上位解法
ここから、具体的な上位解法についてメモしていきます。被っている内容もたくさんあるので、適宜省略しています。
1st Place Solution
1. 2段階学習
- テストデータに近いKaggle-Onlyデータに特化した学習を行った
- LBで少なくとも+0.015の改善が行えた
- 手法詳細
- 1段階目(事前学習): Persuadeデータ("古い"データ)を使用。場合によっては追加のPersuadeデータやEllipseデータセットなどの補助データも使用(ただし効果は限定的)
- 2段階目(微調整): Kaggle-Onlyデータ("新しい"データ)のみを使用
- 各ステップでは、prompt_id+scoreをラベルとして使用したStratified 5-fold分割を採用
pre-trainと書かれていたのでMLMかなとも思ったのですが、以下のような記述で1段階目にもprompt_id+score
をラベルとするStratified 5-foldを利用したと書かれていたのでMLMではないと判断しました。
Since the new data was clearly using slightly different scoring criteria, I set up my training as a pre-train -> fine-tune, two-staged process. Pre-train on the old data (+ maybe aux data) and fine-tune on the new. This gave a boost of at least 0.015 on public LB and made CV-LB-correlation much better.
Within each stage I used stratified 5-fold split using prompt_id+score as labels.
2. Pseudo Labeling
- スコアの基準が異なるようなので、Kaggle-Onlyデータ("新しい"データ)で学習したモデルを使用してPersuadeデータ("古い"データ)に新しいスコアを付与して利用した。
- それにより、古いデータと新しいデータのスコアリング基準の違いを橋渡しし、より一貫したデータセットを作成できた
- LBで+0.004-0.007の改善が行えた
- 手法詳細 (2ラウンド Pseudo Labeling)
- 1ラウンド目: mean(元のスコア, アンサンブル予測)を使用
- 2ラウンド目: アンサンブル予測のみを使用
- 常に正確な浮動小数点予測を使用し、丸めは行わない
- 備考
- 2ラウンド目はCVは改善したがPublicLBは悪化したので全面的に信頼できなかった
- 蓋を開けてみると強かったのでTrustCVでよかった
3. DeBERTaモデルの学習と最適化
- 異なるサイズ(base, large)のDeBERTaモデルを使用した
- 異なるPooling(CLS, Gem)を組み合わせた
- 異なるLoss(MSE, BinaryCrossEntropy)を使用したモデルを組み合わせた
主なハイパラ
- Context Length: 1024
- バッチサイズ: 8
- 学習率(torso): 1e-5(事前学習), 5e-6(微調整)
- 学習率(head): 2e-5(事前学習), 1e-5(微調整)
- エポック数: 2
- ウォームアップ: 事前学習の15%
- 学習率スケジューラ: コサイン減衰
- 勾配クリッピング: 10
- 重み減衰: 0.01
- ドロップアウト: 0
- 重み初期化: normal_(mean=0.0, std=0.02), bias=0
- 追加トークン: "\n", " "(@cdeotteの共有を参考)
- その備考
- 主にPooling, Loss, Context Lengthを変更し、まれに学習率を変更した
- Seed Averaging
- Fine-tuningでは約4500しか学習データがなく非常に不安定だった
- QWKの閾値最適化する場合は特に不安定だった
- そのため、すべてのモデルで3つのSeed Averagingを行った
- Fine-tuningのところだけSeed Averagingを行ったので計算時間も大したことなかった
- Fine-tuningでは約4500しか学習データがなく非常に不安定だった
- 差分学習率 (モデルの異なる部分に対して異なる学習率を適用する技術) は効果がよくわからなかった。(効いてないとは言ってない)
- BaseはPublicLBでは高いスコアを出せたがCVとPrivateLBはLargeの圧勝だった
- ML_Bear追記
- 追加トークン: debertaが無視してしまうトークンを追加してあげるべきとのこと。ここには書いてなかったが "\n\n" を " | " とかに入れ替えるのも良いとか。
- ドロップアウト: これも別コンペ(Commonlit)の@cdeotteの共有に詳しいが、回帰問題ではドロップアウトを0にするのが推奨されている
config = AutoConfig.from_pretrained(PATHS.model_path)
config.attention_probs_dropout_prob = 0.0
config.hidden_dropout_prob = 0.0
config.num_labels = 1 # REGRESSION
model = AutoModelForSequenceClassification.from_pretrained(MODEL, config=config)
4. 閾値の最適化
-
scikit.optimize
のminimize
関数を使用し、1 - QWK
を "Powell"法で最小化 - 15の異なる初期値(開始点)から最適化を実行:
- 各開始点から最適化プロセスを開始し、それぞれ最適な閾値を探索
- 15回の最適化プロセスで得られた閾値の平均を採用
- この方法により、局所最適解のリスクを減らし、より安定した信頼性の高い閾値を得た。
- 各モデルの閾値を個別に事前計算し(3シード平均)、アンサンブルの重みを閾値にも適用
- ML_Bear追記
- QWK最適化はKaggle本に詳しい
- ここでは"Nelder-Mead"法が紹介されていたはず
- QWK最適化はKaggle本に詳しい
5. アンサンブル
- シンプルな平均アンサンブルの使用
- オーバーフィッティング対策
- 各モデルの閾値を個別に事前計算(3つのシードを使用)
- アンサンブルの重みを閾値にも適用
- 閾値とアンサンブルの重みを独立して最適化することを避け、オーバーフィッティングのリスクを軽減した
- 最終的に7つのDeBERTa largeモデル(各3シード、計21モデル)のアンサンブルが最高スコアを達成
- モデルのバリエーションなどは省略 (元のDiscussionを参照ください)
うまくいかなかったこと
- GBDT
- モデル単体としては弱かった
- 第2ステージ(Stucking?)では何も貢献していないように見えた
- (それほど時間をかけて検証したわけではない)
- 入力へのプロンプトの追加
- 逆翻訳
- debertaによる分類モデル
- attention pooling
2nd Place Solution
2位解法も1位解法と同様の2段階学習でした。また、Backbone(deberta-v3-large)などの共通点も多かったため、それらの部分は省略します。
1. Ordinal Regressionの活用
- いくつかのモデルでOrdinal Regressionを利用した
- これがアンサンブルに大きく寄与していると考えている
- 実験では Ordinal regression loss > MSELoss > bce loss の順でCV, PublicLBが良かった
- コード例は解法Discussion参照のこと
- [参考] Ordinal Regressionの一般的な説明 (by Claude 3.5 Sonnet)
- 概要: Ordinal Regressionは、順序のある離散的なカテゴリ(この場合は1-6のスコア)を予測するのに適した手法です。
- 実装方法:
- ラベルの変換: 元のスコア(1-6)を順序付きの二値分類問題に変換します。
- 例: スコア4は[1, 1, 1, 0, 0]に変換されます(4未満は1、4以上は0)。
- 意味:
- 1以上か? Yes (1)
- 2以上か? Yes (1)
- 3以上か? Yes (1)
- 4以上か? No (0)
- 5以上か? No (0)
- 損失関数: BCEWithLogitsLossを使用します。
- モデル出力: モデルは各閾値(1/2, 2/3, 3/4, 4/5, 5/6)を超える確率を出力します。
- 予測: 出力された確率を元のスコアに変換して最終予測とします。
- ラベルの変換: 元のスコア(1-6)を順序付きの二値分類問題に変換します。
- メリット:
- スコア間の順序関係を明示的にモデルに学習させることができます。
- 通常の回帰や分類と比べて、順序付きカテゴリデータの特性をより適切に扱えます。
2. MLM (Masked Language Model) の利用
- 全学習データを利用して10epochのMLMを行った
- アンサンブルで利用したモデルのバックボーンはすべてこれ
- 2段階学習を行う前の試行錯誤の非常に速い段階で試した
- その時はCV/PublicLBで+0.005-0.008程度効いていた
- 2段階学習を行うようになった後で実際にどれくらい効いているのかは調べてない
- MLMの例はこのCodeが役立つから読んでみたらいいよ
- 利用したパラメーター →
lr=1e-5, max_sequence_length=1024, batch_size=2.
- 利用したパラメーター →
- MLMの有用性はDon't stop the pretrainingを読むとよくわかるよ
- RoBERTaのような大規模モデルをあるドメインのタスクに適応させるためには、その事前学習済みモデルを、さらに特定のドメインに絞ったデータで追加の事前学習(中間タスク)させることが有用だということを示した論文とのこと(ref)。
3. 閾値最適化
-
OptimizedRounderを利用して最適化した
- スコア5-6の閾値が最適化されていないことに気づいたのでこれを最適化するループを書いて処理した
- Kaggle-Onlyデータのみで閾値最適化するとCVにオーバーフィットしている印象があったので全学習データを利用して閾値最適化した
4. その他
- 一部のモデルでCoPE (Contextual Position Encoding) を利用した
- うまくいかなかった試み:
- 分類モデル
- 重み付けアンサンブル
- Reina (Retrieval-Enhanced Input Augmentation)
- Add elements of each token (各トークンに追加の特徴や情報を付加する?)
- Separated head training (ヘッドのみの学習)
- Stacking
- MoE (Mixture of Experts)
- Ranking loss
- AWP (Adversarial Weight Perturbation)
- [参考] Reinaの補足 (by Claude 3.5 Sonnet)
- Reina: Retrieval-Enhanced Input Augmentation
- 入力テキストを拡張するための検索ベースの手法
- 詳細な手順:
- フォーカステキストA(現在の入力)に対して、訓練データセット内で最も類似したテキストBを検索します。
- テキストB、Bのスコア、そして元のテキストAを連結します。
- この拡張された入力をモデルの訓練に使用します。
- 利点: コンテキストの拡張 / データセット内の関連情報の活用 / スコアの一貫性の向上
- 課題: 計算コスト / 不適切な類似テキストの選択リスク / モデル複雑性の増加
- 疑似コード:
- Reina: Retrieval-Enhanced Input Augmentation
def find_most_similar(focus_text, dataset):
# 類似度計算(例:TF-IDFとコサイン類似度)
# 最も類似したテキストを返す
...
def reina_augment(focus_text, dataset):
similar_text, similar_score = find_most_similar(focus_text, dataset)
augmented_input = f"{focus_text} [SEP] {similar_text} [SEP] {similar_score}"
return augmented_input
# 使用例
augmented_text = reina_augment(current_input, training_dataset)
model.train(augmented_text)
利用したコードへのリンク
- 学習コード: https://github.com/Syhen/2nd-Place-Solution-Kaggle-Learning-Agency-Lab-Automated-Essay-Scoring-2.0
- Inferenece Notebook: https://www.kaggle.com/code/syhens/aes2-voting?scriptVersionId=184408611
3rd Place Solution
Soft Labeling
2段階目の学習では1段階目のOOF予測をソフトラベルとして使用して学習を行った
- ターゲットラベルと予測値を0.8:0.2の比率でブレンド
- 通常の固定ラベル(1-6のスコア)の代わりに0-1に変換して利用
- 例
- 元のラベルが4(0-1スケールで0.6)で、
- OOF予測が3.5(0-1スケールで0.5)だった場合、
- 新しいソフトラベル = 0.8 * 0.6 + 0.2 * 0.5 = 0.58 となります。
- ブレンドの比率はハイパラなので実験で最適な値を見つけた
- 参考: 3位解法は以下のような2段階学習
- 1段階目: 全データ("古い"+"新しい")で学習し、"新しい"データでの評価に基づいてモデルを保存
- 2段階目: 1段階目の重みを読み込み、"新しい"データのみで学習し、"古い"データで評価
モデルなど
- 回帰問題 (BCE Loss)
- Backbone: DeBERTa-v3 (base, large)
- baseの方がlargeより安定して性能が高かった
- private LBでもbaseのモデルが一番性能が高かった
- が、このモデルはアンサンブルには入らなかった
- 学習の安定のためにMLMとlayer freezeを活用した
- layer freeze
- DeBERTa-v3-base/xsmall:9層
- DeBERTa-v3-large:6層
- layer freeze
- CV戦略:プロンプト名とスコアに基づくMultilabelStratifiedKFoldを採用
- 細かい工夫
- データ前処理:DeBERTaが無視する"\n"を"[BR]"に変換し、特殊トークンとして設定
- アンサンブル: 異なるモデルサイズ、最大長、Head (MeanPooling&LayerNorm, AttentionPooling, LSTMPooling) を組み合わせた10個のモデルを使用
- Dropoutをオフに設定 (1位解法と同様)
4. うまく行ったこと / 行かなかったこと
うまく行ったこと:
- Soft Labeling
- MLM
- CV戦略
- ドロップアウトの無効化
うまく行かなかったこと:
- 疑似ラベリング (FeedbackPrizeデータを使用)
- 補助クラス (FeedbackPrizeモデルを使用した6ラベル予測 / Privateには効いていたっぽい)
- GPR (Gaussian Process Regression)
- LightGBMなどを用いたスタッキング
- 閾値調整
4th Place Solution
データソースタグの追加と利用
- データソースごとに以下のタグを付与した
- Kaggle-Onlyデータ("新しい"データ): [A] (full_textを'[A] {full_text}'に書き換える)
- Persuadeデータ("古い"データ): [B] (同 '[B] {full_text}')
- この工夫でモデルがデータソースの違いを認識し、それぞれに適した特徴を学習できるようになった
- 付与前後の学習の状況の例 (non-persuadeの学習が大幅に改善している)
QWK (non-persuade, persuade)
Epoch | 付与前 | 付与後 |
---|---|---|
1 | 0.8289 (0.7896, 0.8403) | 0.8282 (0.7953, 0.8371) |
2 | 0.8370 (0.7946, 0.8491) | 0.8401 (0.8082, 0.8487) |
3 | 0.8347 (0.7838, 0.8493) | 0.8446 (0.8093, 0.8541) |
4 | 0.8260 (0.7742, 0.8409) | 0.8424 (0.8033, 0.8528) |
5 | 0.8229 (0.7703, 0.8381) | 0.8395 (0.8022, 0.8496) |
- メインのスコア予測タスクに加えて、データソースを分類するタスクを追加した (auxiliary training)
モデルなどの工夫
- モデルによっては"古い"データと"新しい"データで別々の分類ヘッドを使用
- テストデータがKaggle-Onlyデータ("新しい"データ)であると仮定し、Early StoppingはKaggle-Onlyデータの性能で行う
- 大規模アンサンブル
- 45モデルの平均
- deberta-large, deberta-v3-large, Qwen/Qwen2-1.5B-Instruct
- 3 random seeds
- transformersのdeberta実装を改造して使った。トレーニングが15%以上、推論が30%以上速くなった。
- (ML_Bear追記: 詳細が書かれていないがコメントでflash attentionに触れられていたのでその辺りの改善なのだろうか?)
Discussion