CommonLit - Evaluate Student Summaries振り返り [24位]
はじめに
先日CommonLit - Evaluate Student Summariesコンペが終了し、結果はpublic5位->private24位で残念ながら金メダル獲得には至りませんでした。ほぼベストsubを選べており金圏subはなかったのでそういう点での悔しさはありませんでしたが、他の上位入賞者のソリューションを見るに、私の手元のCVスコアで改善が見られなかった実験(後述)がキーだったっぽいのを見ると少し悔やまれる結果でした。
今回は解法の解説がメインの記事になります。ちなみに終了一ヶ月前に金圏マージした海外の方と2名で取り組みました。
- 問題設計:英文(prompt_text)と生徒の要約文(text)が与えられ、後者に対するwording(言葉遣い)とcontent(内容)の2つのスコアを予測
- 評価指標:macro-F1 score
- 最終順位:24位/2,106チーム(チーム銀)
サマリ
うまくいったこと、うまくいかなかったこと、やるべきことは次のとおりです。今回は赤字の部分のみ解説します。
うまくいったこと
- prompt_textとtextで共通の単語やフレーズ、N-gramなどをインプットに追加
- 各種プーリング結果を結合させたカスタムヘッド
- 逆翻訳結果に対するPseudo labeling
- リッジ回帰によるアンサンブルウェイト算出(intercept=0, alpha=500)
- Weighted Loss
- レイヤーごとに学習率を変更
- Dynamic Paddingを利用した高速化
うまくいかなかったこと
- 一部レイヤーの重み凍結
- AWP
- MLM
- 目的変数の回転
- 学習と推論とで異なるmax lengthの利用
- より大きなmax lengthの利用
やるべきだったこと(敗因)
- より大きなmax lengthの利用の試行錯誤
うまくいったこと
prompt_textとtextで共通の単語やフレーズ、N-gramなどをインプットに追加
今回のコンペでは、要約対象の原文である"prompt_text"とそれを生徒が要約した"text"が与えられました。採点対象である後者だけでなく、原文である前者も重要なインプットであるわけですが、トークン数が多く、特にtestデータでは5000トークンを超えるようなプロンプトもあったため、長いトークンにも対応した形で学習・推論させるかが鍵でした。
金圏を目指す上では後述のmax lengthを増やして原文のprompt_textをそのまま投入する方が筋が良かったわけですが、我々はprompt_textとtext間で共通の単語やN-gramなど複数のアプローチで抽出し、各々をインプットの末尾に加えた異なるモデルを作成しました。結果的にCVスコア、LBスコア共に飛躍的に向上したので、これ自体は上位銀を取る上ではかなり有効な取り組みでした。
各アプローチによる抽出イメージは次の通りです。
ペア文章
文章A:He bought a red pen yesterday.
文章B:She bought a red pen two years ago.
共通◯◯
①単語:bought | a | red | pen | .
②N-gram(2-gramの例):bought a | a red | red pen
③フレーズ(3トークン以上):bought a red pen
④キーフレーズ(Rake):red pen
②N-gram系は共通部分が多いフレーズが混じるとどうしても抽出量が多くなってしまうので、次のようなアプローチで抽出した③フレーズなどもバリエーションに加えました。若干競プロを思い出しました。
- 共通3-gramを抽出し、各々の開始位置をリストに格納&昇順ソート
- 1の1つ目の要素をフレーズの開始位置とする
- 次の要素を確認し、以下の場合分けに沿って処理を進め、フレーズの終了位置を特定する
- 差が1→更に次の要素の確認に進む
- 差が1以外→前回要素+2がフレーズの終了位置とし、今回要素が新フレーズの開始位置とする
- 上記の繰り返し
各種プーリング結果を結合させたカスタムヘッド
BERT系のbackboneで獲得したembeddingをヘッド部分でどのように集約させるかは様々な方法がありますが、今回は各プーリング方法(Mean, Max, GeM, Attention)により得られたものを組み合わせるカスタムヘッドを作成しました。
例えばGeM Pooling × AttentionPoolingの組み合わせだと次のような感じ。
def forward(self, inputs):
# 中略 #
gemtext_out = self.gempooler(lasthiddenstate, attention_mask) attpool_out = self.attpooler(lasthiddenstate, attention_mask) context_vector = torch.cat((gemtext_out, attpool_out), dim=-1)
要約(text)部分以外をmaskさせるattentionアプローチも少し頭によぎりましたが、残念ながら他のアプローチを優先。
逆翻訳結果に対するPseudo labeling
要約を英語→中国語→英語に逆翻訳したものに対して、チームメイトの互いのモデルで予測した数値をpseudo labelingとして用いデータセットに加えたところ精度が向上しました。リークを防ぐために、お互いのCVを合わせてOOFでラベルづけしました。
ちなみにpseudo labelingデータの使い方として、次のようなものも試しましたが、大差はありませんでした。
- pseudo labelingで事前学習→元のデータセットで学習
- pseudo labelingデータに対する重みを調整
- pseudo labelingデータの目的変数をオリジナルの目的変数との加重平均に変換
リッジ回帰によるアンサンブルウェイト算出(intercept=0, alpha=500)
スタッキングには一部LightGBMも使いましたが、最終層にはリッジ回帰を用いました。また単純にリッジ回帰を用いるのではなく、次のような手法でアンサンブルウェイトを算出することで学習データへのoverfitを防ぐことができ、CV・LBスコア共に普通のリッジ回帰や線型回帰よりも良いスコアが得られました。
- 大きなalpha(=500)を設定(特定のモデルのウェイトが大きくなることを回避)
- interceptを0に固定(アンサンブルウェイト算出のため)
- coefficientsの和が1になるように標準化(アンサンブルウェイト算出のため)
- 全データに対してfitさせるのではなく、OOFで行い、coefficientsの平均をアンサンブルウェイトとする(過学習回避)
やるべきだったこと(敗因)
敗因はずばりmax lengthの調整に時間をかけなかったことです。テストデータには学習データに含まれないような長いトークンのprompt_textが含まれていることはdiscussionのLB probing情報からわかっていました。大きなmax lengthを用いて学習させたり、trainとinferenceで異なるmax lengthを適用するアプローチは試していたのですが、CVスコアが悪化したのでサブミットすらしませんでした。
長いトークンのプロンプトもそこまで多くはないだろうとタカをくくって、テストデータの分布をLB probingにより調査することや、上記のサブミットをサボってしまったことが悔やまれます。
終えてみて
本腰入れて取り組んだNLPコンペは今回が初めてでした。前回のPSPコンペに引き続き単独でpublic金圏までたどり着くことができたり、結果的に上位銀メダルを取れたことは成長を感じる部分でもありましたが、どちらかというと金メダルを取り切れない自分の実力不足を痛感した感じです。金メダルを求めてまた次のコンペを探してみます。