Re:ゼロから始めるML生活

どちらかといえばエミリア派です

【参加録】CommonLit - Evaluate Student Summaries

雑記です。

2023/10/12 09:00 JSTまで行われてたCommonLitコンペにひっそりと参加していました。 久しぶりにkaggleコンペを最後まで完走してちょっとだけ気分が良いので、振り返りを書いていこうと思います。

tl;dr;

  • 最終順位25位
    • public: 65位(mcrmse=0.433), private: 25位(mcrmse=0.463)
    • solution: ここに公開しました
      • DebertaV3 + Hand crafted text featureがとにかく効いた
      • 複数文をinputするとDebertaV3の学習が安定しなかったので、custom headerで対応
      • token長でsortしたあとに推論することで推論高速化
  • kaggleでアイコンの色を紫にした
    • notebook expertは根気強くnotebook公開したら結構簡単
    • アイコンの周りを紫にしたい人はnotebook公開していったら良いと思う

解法など

自分の解法などは基本的にkaggle notebookとして公開しているので、公開情報についてはそちらを御覧ください。 (実際のnotebook見たほうが具体性あってわかると思いますし)

コンペのとき考えたこと

解法自体より「なんでこんなことしようと思ったの?」という思考プロセスのほうが個人的に後々有用だったりするので、どんなこと考えて取り組んでいたのか書いてみようと思います。 

たぶんTransformerが強いんだろうな

NLPコンペに真面目に取り組むのが初めてで、最近のHuggingFace系のツールの使い方すら全くわからない状況からスタートでした。

右も左もわからない状態だったので、こちらのnotebookを参考に、試運転で作ったのがこちらのnotebookです。

www.kaggle.com

どうやら最近のNLPコンペではDebertaV3を使うのがデファクトスタンダードのようで、自分もDebertaV3を使うようにしました。

このときのLBのスコアは0.509という至って平凡なスコアでしたが、0から自分の手でbaseline作ってsubまで行けたのが良かったです。 これでなんとなくツールの使い方・スコアの感覚が掴めたので、あとから振り返ると自作のbaselineを作ったからあとから実験回すときにコードの修正が楽だった気がします。

流行りのLLMってのを使ってみよう!

その後、ちょうど話題になってたLLAMA2を使ってみたりと若干迷走してました。

www.kaggle.com

これに関しては今度覚えてたら別のブログで書こうと思うんですが、LLMをシステムに組み込むって大変だなと感じました。 正直、動きはしましたが使い物にならなかったので、この時点で今回はLLMを使うのを諦めました。

Transformer無しでもそこそこ良いスコア出てね?

8月中旬くらいからちゃんとscoreを上げようと思うようになりました。

このときは公開notebookでこちらのnotebookがちょっと気になっていました。

www.kaggle.com

notebookの趣旨としては「Transformerなしでhand madeの特徴量をLGBMに突っ込んでもそこまで悪いscoreではなくない?」というものでした。

これを見たときに感じたのは「Transformerでscoreが良くなるケースとhand madeの特徴量で良くなるケースがあるのでは?」と感じ実装してみました。 これに関しては予想が的中して、金圏手前のscoreが出ました。

小話1: 金圏手前のnotebookを公開する事件発生

この時点では自分はあまりコンペの順位に執着がなく、notebookのupvoteだけを目的にやっていたので、動きそうなコードを片っ端から公開していました。

ある日、この「DebertaV3 + LGBMで動かしたら面白いんじゃね?」という安直なアイデアを思いつき、実装してsubmitしました。 submitしてpublic scoreが計算されるまでには数時間の待ち時間が発生するんですが、このとき自分は「どうせ大したスコアは出ないだろ、hahaha」という軽い気持ちでpublic scoreが計算される前にnotebookを公開しました。(後からpublic scoreがnotebookに反映されるのでこういうことが可能です)

次の日、LBを見て自分がスクロールしないでめっちゃ上の方(その時点で14位、金圏のちょっと手前、ハイパラチューニングするだけで金圏)にいるのを見て、「エライことしてしまった…」とちょっと後悔する事件がありました。

この後自分のnotebookがめっちゃforkされたり、discussionで言及されたりしてちょっとびっくりしてました。

www.kaggle.com

結果的にこのとき公開したnotebookが200以上upvoteをもらったので、金圏手前の順位と引き換えにupvoteをかき集めたという形になりました。(いいんだか悪いんだかよくわかってない)

小話2: 使っちゃいけないライブラリがあることが後で判明

この公開したnotebookなんですが、autocorrectというパッケージを使用していました。 こちらはLGPLライセンスであり、このコンペの規約に即していない可能性がdiscussionで指摘されていました。

www.kaggle.com

このDiscussionが出たときちょっとだけ焦って、autocorrectを使わないでもLB性能がちゃんと出せることを示したnotebookを公開したりしました。

www.kaggle.com

まあ、autocorrect使ってなくても良いスコアが出てるんだから文句ないでしょ…

え、私のwordingのスコア悪すぎ…

参加した人はきっとわかってくれると思うんですが、この「どう見てもwordingのscore悪すぎ」問題に悩んだ人は多いんじゃないでしょうか?

真相はわかりませんが、個人的な見解はsolution のdiscussionに書いときました。

www.kaggle.com

自分の見解としては、「精度が悪いのはwordingではなくむしろcontentで、主催者によるscoreの前処理変換によってwordingが悪くなっているように見えてしまっている」と考えています。

prompt_textを入力に使う

今回の場合、contentは下記のような方針で採点されています。

  • Main Idea
    • How well did the summary capture the main idea of the source?
    • 要約は出典の主旨をどの程度捉えていたか?
  • Details
    • How accurately did the summary capture the details from the source?
    • 要約は、情報源の詳細をどの程度正確にとらえているか?
  • Cohesion
    • How well did the summary transition from one idea to the next?
    • 要約は、あるアイデアから次のアイデアにうまく移行していたか?

つまり、要約元の情報と要約文の情報の一貫性が問われているわけです。

一方、今回のコンペでは、下記の4種類のテキストが与えられていました。

  • 題材となる文章 (prompt_text, 数百 ~ 数千トークン)
  • 質問文 (prompt_question、数十トークン)
  • タイトル (title、数トークン)
  • 生徒が作成した回答 (text、数十 ~ 数百トークン)

多くの場合、下記のようにSEPトークンを使用して複数文を結合したものをinputとしていると思います。

この入力のテキスト長が長いと学習時間が長くなるので、はじめは最も長いprompt_textは使用していませんでした。

「真に悪いのはcontentのスコア」という仮説を立ててからは、要約元の文との整合性をDebertaに学習させてスコア向上させるためにprompt_textを含めることにしました。

自作カスタムヘッダーを使う

テキスト長を広げてDebertaの学習を行うと、今度は自分の場合は学習が発散しました。(validation scoreがやたらと悪くなる)

この理由を考えたんですが、「inputしているtoken数が多いことが関係している」という仮説をたてました。(未だにはっきりしたことはわかってませんが)

通常、Transformerを使うと図のようにCLS tokenの出力に対して全結合層を付与して出力を得ます。

このとき、CLS tokenにはinputのtoken全体の情報が凝縮されている(であろう)のに対し、今回の予測対象はあくまで生徒が書いた要約文です。 そのため、全結合層に使用している情報に問題文の情報が乗ってしまい、ノイズが多くなってしまっていると考えました。 そこで、ノイズを軽減するために下記のようなcustom headerで対応しようと考えました。

入力には

  • prompt_text (要約元)
  • prompt_question (設問)
  • text (生徒の要約文)

を使用しますが、最終層の出力のうちtextの部分だけをregressorのinputとするようにしました。 これによって学習時のlossが安定し、事なきを得ました。

custom header (poolerと呼ぶ人もいるかもしれない)は下記の文献が非常にわかりやすいので、詳しくはそちらをご参照ください。

www.ai-shift.co.jp

様々な実装は下記のnotebookが非常にわかりやすいです。

www.kaggle.com

おまけ

みんなのPython勉強会#98で、この記事の内容の一部をLTしました。

speakerdeck.com

え、私のTransformer推論遅すぎ…

大体ここまででCV上は他の参加者とシングルモデルで大差ない程度のスコアが出ていました。(CV-LBスレッドで確認してました)

なので、「多分ちゃんと推論すれば上位の参加者と勝負できるモデルは手元に出揃っているんだろうな」というなんとなくの自信はありました。 ただ、推論が9時間で終わらない問題に数日悩まされてました。 いくら強いモデルであっても9時間以内に推論が終わらなければスコアが計算されないので、最後の方はそこに頭を使ってました。

推論バッチ内でのサンプルのtoken数をなるべく同じにする

この問題に対してはTransformerでの推論の際に、推論バッチ内のサンプルのtoken数をなるべく同じような長さにするということでした。

TransformerのTokenizerは、(設定次第ではありますが)batchの中で最大長のtextに合わせてpaddingをしようとします。 つまり、長いtextと短いtextが同じbatch内に含まれていると長いtextに合わせてpaddingされてしまい、やらなくて良いpad tokenの計算が増えてしまいます。

https://www.kaggle.com/code/rhtsingh/speeding-up-transformer-w-optimization-strategies より引用

そのため、なるべくbatch内のtoken長を同じにするように、token長でsortした状態でbatchを割り振って余分なPADトークンの数を減らすことで高速化することができました。(Uniform Length Batching というテクニックらしいです)

https://www.kaggle.com/code/rhtsingh/speeding-up-transformer-w-optimization-strategies より引用

まあこれやってもfold=4で学習したモデルのうち3つ推論するのが限界だったんですが…token長が長いとどうしても推論に時間かかるので、仕方ないかなと思ってます。

コンペやる上で気をつけてたこと

最後に、コンペやる上で気をつけてたことを書いていこうと思います。

自分のコードを書く

以前やっていたコンペで「baselineを自分で書かないと後々痛い目を見る」というのは経験則で知っていたので、どれだけ大変でも自分の手を動かしてコードを書くことは気をつけてました。

kaggleでは特に、公開notebookにpublic LBのスコアが紐づいているのでハイスコアのnotebookをコピーして使いたくなる気持ちに駆られますが、これをやると他人のコードを噛み砕く必要があります。 大量のnotebookをすべて読みこんで、必要な箇所だけ自分のコードに反映していく作業は結構骨が折れるので、コンペの最後まで自分が使うコードは一度自分の手で書いておいて、そこに公開情報を踏まえて自分の手で反映していくのが一番簡単なのではないでしょうか。

今回はタスク自体も比較的シンプルな部類だったのでbaselineも自分で書けましたし、結局最後までbaseline実装のほとんどが残っていたので、baseline実装は大事だなと改めて感じました。

これをやっといたおかげで(自分で書いたコードなので当然ですが)コードを読みこむ負荷がだいぶ下がり、その分実験をたくさん回せた気がしました。

目標を持つ: 「アイコンの輪っかを紫以上にする」

ちょっと話は変わりますが、コンペに参加して最初の1週間くらい、スコアが上がらないどころか謎のエラーでsubmitすらできず(謎のsubmission.csv not found) しんどい思いをしました。 モチベーションもあんまり上がってこなかったので、そのタイミングでちょっとした目標を作ることにしました。

「目標: アイコンの輪っかを紫以上にする」

kaggleでは、金色だとGrand Master, オレンジだとMaster, 紫だとExpertといった形でアカウントの周りに称号ごとに色が付きます。 自分は過去にkaggleでメダルを取ったことがなく、当時緑色(Novice)でした。 なんとなく「緑とか水色の輪っか、ダッさいなー」と思い、なんとかこの色を変えてやろうとあれこれ調べました。

通常、kaggle competition expertになるにはメダル2つ必要です。 自分は過去にメダルを取ったことが無いので、このコンペで仮に1位になったとしてもcompetition expertになることは不可能です。

しかし、kaggleのアイコンの色はcompetition だけでなく、discussion/notebook/datasetなど、他の称号でも色をつけることができるんです。 とくに、自分の場合は文章を書いたりコードを公開することは技術ブログを書いていて慣れているので、あとは英語に直せば普通にできそうだと感じました。

試しに1〜2個notebookを公開してみると、お情けでupvoteしてくれる人がそこそこいて、結構簡単にbronzeを取ることができました。 「notebook公開して5個bronze medal 取るくらいはちょろくね?」ということに気づき、その後にnotebookをバンバン公開して無事(notebook) expertになることができました。(金圏手前のnotebookを公開する事件も起こしましたが)

感想

今回、軽い気持ちで参加したコンペが意外と楽しかったので最後まで完走することができました。 kaggleを完走したのは多分RFCX以来2回目で、前回よりは多少は存在感を残せたかもしれませんね。

無事アイコンの周りの色も紫になったので、次の目標はオレンジ色ですかね。 notebook 銀メダルをあと5個取れば行けるので、年内にオレンジ色にできるように頑張ろうと思います。(やるとは言ってない)

肝心のcompetitionの方では、真面目にやればソロでも銀メダルまでは取れることは今回わかりました。 ただ、振り返ってみると結構的はずれな改善策を試してみていたことが多かった印象で、上位陣とはまだまだ実力差があるなと感じました。 もうちょっと筋の良い改善策を見抜いてバンバン打ってかないと一桁順位はなかなか厳しいという気がしていて、この辺の勘所が分からなかったのが今回金を取れなかった主な原因かなと思います。

終わってみて他の日本人が金メダル取ってるのを見て羨ましかったので、次は金メダル取れるようにもうちょい頑張ろうと思いました。