pop-web

スマートかつクールでアトラクティブなブログです

SIGNATE Student Cup 2020【予測部門】参加記(pop-ketle版)

これ何?

タイトルの通りです。SIGNATE Student Cup 2020に参加したので感想・学んだことを書きます。 色々学びました。辛いコンペでした。

SIGNATE Student Cup 2020って?

データ分析のコンペです。今回は英語の求人情報を4つに分類するというタスクのコンペでした。
①データサイエンティスト(DS)
機械学習エンジニア(ML Engineer)
③ソフトウェアエンジニア(Software Engineer)
コンサルタント(Consultant)
この4つです。

評価指標はF1scoreでした。 与えられたデータは求人情報のテキストオンリーです。これだけを使って分類をします。

どんなデータだったのか

どんなテキストか直接例をここにあげると規約的にやばいかもしれないので、テキスト例が乗ってるフォーラムを上げておきます。 SIGNATE Student Cup 2020【予測部門】 | SIGNATE - Data Science Competition

フォーラムを見てくれた人は分かったと思いますが、与えられたテキストは、本当にこんなんで予測できるのか?みたいなテキストでした。少なくとも私はそう思いました。おそらく求人情報の最初の一文だけ抜き取ったんじゃないか、そのくらい何ともいえないテキスト群でした。フォーラムにもあるように同じ文章で別の正解ラベルのものすらありましたし。 スコアとしても最終スコアの一位の人が0.5038085です。そのくらい分類するのが難しいタスクだったと言えるのではないでしょうか。

また、このタスクの一番特異な点として最後までフォーラムを盛り上げていたものに、trainとtestの正解ラベルの分布が明らかに違うというものがありました。 pandas-profilingの結果ですが、trainはこんな分布でした。

f:id:pop-ketle:20200827185732p:plain

それに対して、testの分布はクラス数が1~4まで順にこのように予測されていました。[404, 320, 345, 674] これも過去フォーラム参照SIGNATE Student Cup 2020【予測部門】 | SIGNATE - Data Science Competition

とまあ、少なくともtrainとtestの分布が違うのは明らか、そこからPublicとPrivateの分布がどうなってるのか不明という、何とも運要素の高いコンペだったと思っています。(もちろん上位はちゃんとshakeしないモデルを組めてるのですが) ここら辺はSIGNATEの決まりなので仕方ないのですが、kaggleみたいに最終サブを2つくらい選べると良かったなあとすごく思っています。

自身の結果について

なんで上でそんなに愚痴ってるのかというと、今回僕がものすごくshake downしたからです。 多分一番順位落としたんじゃないかな?調べてないけど。

順位としてはこんな感じでした。

暫定順位 最終順位 暫定評価 最終評価
36位 156位 0.4739678 0.4032951

暫定順位はサイトとして出るわけではなく、僕が23:59分に最後に確認したのがこうだったという意味です。
いやー、非常に辛いものがありました。瞬間最大風速7位だったので、結構夢を見れたのですが、そこから2週間色々試せど試せど思うようにうまくいかず何の進歩も生み出せず、ただただ順位が下がり続け、しかも最後に大幅なshake downと辛い戦いでした。色々疲れてもいたので、1日ほど寝込んで今この記事を書いています。 ちなみに自分の持ってた提出の中で最も最終評価が良かったのはこれです。

暫定評価 最終評価
0.4400458 0.4548169

正直、一つしか最終サブ選べない状態で0.03もスコアが下がるこれを選択はできないよなーという感じです。これを選んでいたら50位入るかなーという感じですね。とはいえ選べなかったので言っても仕方ないですが。 後ろの方で書きますが、コードで大きな失敗をしていたので、CVも信用できずこれを選ぶ選択肢にはどうしてもならなかったんですよ。

自分について

コンペ参加はこれが2回目で、かつNLPは全く触ったことがない、という状態で参加しました。一応bag of wordとかtf-idfとかword2vec、bertなんかの用語の存在くらいは知っていましたが、実際それらがどんな意味を持つものなのかはそんなに詳しく知らない。そんなレベル感で参加しました。

今回のコンペ参加で感じたのですが、NLPコンペってもしかして、BERT系のSOTAな優れたモデルをガチャガチャして、テキストの良い感じの分散表現を得るのが最大重要事項みたいな感じで、あんまり特徴量エンジニアリングの余地とかないんですかね。
tf-idfとか全く使いませんでした。いや、正確には初手LightGBMでtf-idf使ったモデル作ったんですが、次にフォーラム参考にして試したBERTが割と手軽に段違いで性能が良かったので、もうtf-idfとか時代遅れなんだなと思い込んで、アンサンブルに組み込むことすら考えませんでした。

あと、これも後ろでもうちょっと詳しく書きますが、前処理でちょっとごちゃごちゃやったらむしろスコアが落ちたのもあって(少なくともPublic LB上では)、前処理もなんか本当にやることあるのか?素人が下手に触らずにBERT様に任せた方が良いのでは?という感じでした。

このせいでモデルに汎用性が出なくてshake downしたんだろうなというのも今となっては感じているんですけど、まあそれは後の祭りなので。

マイソリューション

そんな僕の最終提出解法は、再翻訳したデータをBERTに突っ込むという単純なものです。再翻訳にはGoogle 翻訳のライブラリ?を使いました。これです。

from googletrans import Translator

有料らしいGoogle CloudのAPIは使ってないので規約違反はしてないはずです。 この再翻訳をドイツ、イギリス、フランス、日本語、韓国語と用意してそれぞれの言語データに対して、StratifiedKFoldでCVしたもので学習させて、最後に予測確率をf1スコアで重み付けしてみて平均とったものをargmaxしたという感じです。(後から中国語も作ったけど、提出したモデルには入ってない)

ちなみに後から、同じこと試そうとしたら再現性が出なかったんですが、これは一体...
これ作ったのがコンペ終了までまだ2週間もあるぞ!という、まだまだこれからスコア伸びてくでしょ!という希望に満ち溢れた時期だったので、あまり再現性とか意識せずゴリゴリコード上書きしてたんですよ。加えてGoogle colabで学習ぶん回してた関係上、今回Google Driveでそのままコード管理してたんで、変更履歴とかも残ってなくて...ちゃんとGithubとか使って、コードは管理するべきですね。学びました。

試行錯誤

そんなこんなあってできたここまでが僕の提出した解法です。
ここからは何の進歩も生み出せなかった、暗黒の2週間の間に色々試したことを、思い出せる範囲で書いていきます。

テキストの前処理

よくある小文字に変換するとか短縮系をなくすとかのやつです。
当たり前のようにPublic LB上ではスコアが落ちました。そのため、やはり細かいことは考えずBERTに直接突っ込んじゃった方がBERTがうまいこと処理してくれるのかなという考えを持ちました。

実際のテキストとして読む場合でも、短縮系とか使ってるのや、大文字小文字の違いは微妙なニュアンスの違いにつながってくると僕は考えています。特に英語圏は強調したい言葉を大文字で書くみたいなのはありますし。

過去の似たタスクのソリューション見ても、あんまりこの手の前処理をやったという話を自分では見つけられなかったので、やらない方が良さそうだなと思い、切り捨てました。

...ただPrivate LBを見てみるとPublic LBからスコアが上がってるんですよね。やっぱりある程度は効くんですかね?うーん...?

Pseudo-Labeling

ここら辺で、データに処理を加えるよりは単純に数を増やす方が良さそうだなと感じ始めたのでこのpseudo-labeling(擬似ラベリング)を試し始めました。

僕の理解している限りではこんな手法です。(間違ってたらどんどん教えてください。学んでる最中なので)

  1. とりあえず普通にモデルを作る
  2. 1で作ったモデルでtestデータを予測してラベルをつける、これが擬似ラベル
  3. この擬似ラベルデータからいくつかサンプリングして(train:pseudoが2:1くらいが良いらしい? (参考文献Kaggle State Farm Distracted Driver Detection - Speaker Deck))trainに混ぜ込んで学習を回すことで、擬似ラベルをリファインニングしていって、精度を上げていく
  4. 2~3を何度か繰り返す?(繰り返すのかどうかはよくわからない、計算資源やスコアの変化を見ながらという感じなのかな。あと、refineごとにモデルのウェイトの初期化を挟まないと過学習になる恐れがある)

これがうまくいく理由の直感的な理解として

  • 精度の低めなラベル、つまりノイズの入ったデータが入り込みロバスト性が上がる。
  • 単純にデータ量が増えるので精度が上がる

この二つが理由として挙げられるのかなと思っています。

これももちろんうまくいかなかったのですが、その理由としては元々スコアが低く出るタスク(f1で0.5レベル)だったために、あまりにも信頼性の低いラベルが入り込んでしまったというのがうまくいかなかった理由なのかなと思いました。

この方法はkaggle: Toxic Comment Classification Challengeではうまくいった方法らしいので (参考文献kaggle: Toxic Comment Classification Challenge まとめ - copypasteの日記)期待していたのですが残念でした。

(今見ると上の参考文献URLに下のように書いてますね。何で早く気付けなかったのか、いや、一次ソースのディスカッションのソリューションだけ読んでコメントまで見てなかったからですが...)

今回のタスクはスコアが0.98以上と非常に高く、疑似ラベルの信頼性が高かったことも影響しているのかもしれません。コメントにはスコアがそこそこのタスクで同様のことを試した際にはうまくいかなかったという経験談もありました。

ついでに書きながら思いつきましたが、上のフォーラムにもあるようにこのコンペに同じテキストがtrainとtestにまたがってある上、違うラベルが付与されてることもあるみたいなデータだったので、そのせいもありそうですね。

adversarial validation

TrainデータとTestデータの分布が異なる場合に有効な手法のようで、まさにこのコンペにぴったりじゃん!と思って試した手法です。(参考文献Adversarial Validationを用いた特徴量選択 - u++の備忘録)

これはデータがtrainかtestかどうかを分類するモデルを作り、trainの中のtestぽいデータの確率順にソートして上から順にvalidとして使うことで、テストっぽい分布のデータをバリデーションに使うという手法です。

これは結構分離する精度が良かったので、良さそうだなとと思ったのですが、提出してみたら、Public LBで0.4515126程度。しかもどうもvalidデータに毎回同じデータが選ばれすぎてる?みたいな過学習ぽい感じもあったので(ちょっとここら辺何言ってるのか分かりにくいかもしれないですが、ここら辺コードをバグらせていたのでうまく説明できないです、僕も何を言っているのかわからないです)、もう一つ精度が伸びないなという感じで使うのをやめた手法です。結局これも、暫定評価0.4515126、最終評価0.4173049でかなりshake downしているので、まあ使わなくて正解だったのかなという感じはします。

後から思うにここら辺多分コード、というか設計のバグが起因していたと思います。 というのも、もともと各言語のモデルを作ってアンサンブルするという手法だったのですが、あるときかから、なぜか再翻訳のデータを全部最初に合体させて一つのtrainデータとして扱うようにコードを書いてて、リークしまくりみたいな状態だったからです。(リークしないように工夫もしてたのですが、それがうまくいっていたのかはかなり微妙)

今思い返しても何でこんなことをしたのか、こんなことに長い間気付かなかったのか、全く理解ができないのですが、悪夢を見ていたからとしか言いようがありません。 色々試しても鳴かず飛ばずでかなり焦っていたというかストレスを感じて、早く新しい手法を試さなきゃという風に視野が狭くなっていたのでしょう、全体を俯瞰してみれる余裕が大切ということかもしれません。

roBERTa

roBERTaというBERTの派生系でほぼ完全上位互換と言っても良さそうなモデルっぽいものがあるということを知ったので、(自然言語処理知らないで割りと適当言ってるので、それは違うよという時は教えてください)これとBERTを足して二つのモデルをアンサンブルする感じで行こうかと思ったのだが、スコアとしては大体Public LBで0.44くらい、CVの結果を見てもroBERTaそんなに良い結果出てるか?という感じだった。というかむしろBERT一本の方が断然良い。これならBERT一本の方が良いかもしれんなPubilc LB 0.47で全然精度違うし。ということでroBERTaも試していたのだが最終的にはroBERTaを切り捨ててしまった。

これが汎用性という点で致命的なミスだったと言える。というのも、最終評価でスコアが比較的高めなやつは全部BERTとroBERTaのアンサンブルのやつだったからだ。 今回のコンペを通しての一番大きな学びは、モデルの汎化性能を上げるためには複数の種類のモデルのアンサンブルを積極的にやっていくべきというものである。汎化性能を考えると、多少(0.03が多少か?という話はあるが)精度が落ちていたとしても、複数のタイプのモデルを使ったものを選ぶべきなのだろう。自分の身に降りかかったものとして、このことをこれ以上なく強く実感できた。

...とはいえ現実問題Public LBで0.03もスコアが落ちていたモデルなので、今過去に戻ったとして、1つモデル選べよと言われても、このモデルを選べたかというと正直無理だろうなという気持ちはある。暫定スコアはもう見れないのでおぼろげな記憶でしか語れないが、0.47と0.44では順位が36位から70位近くまで落ちるはずである。複数のタイプのモデルのアンサンブルをしておくといいという知識があったとしても、1サブしか選べないのではこれを選べるかといえば無理だろうなと思う。そういうわけで、今回の結果は避けようのない結果だったと真摯に受け止めている。

その他細々とした取り組み

  • t-SNE

センテンスの分散表現からk-NNあたり使って、似たテキストをくっつけたりとかできないかなと、t-SNEをかけて可視化しつつ様子をみたのだが、perplexity次第でどうにでもなる感じで、何ともいえない結果だった

クラスタができてるといえなくもない気もしたのだが、上に上げたフォーラムにもあるように同じ(似た)文で違うラベルというのも相当数あるし、やっぱりこれを使ってもうまくいかないだろうなと感じたので信頼できず使うのをやめた。

f:id:pop-ketle:20200828092650p:plain
perplexity=5.0

f:id:pop-ketle:20200828092754p:plain
perplexity=30.0

  • text generation

今回の与えられていたテキストが、募集要項の最初の一文のみ、みたいな感じだったのでテキスト生成で続き書けば良いんじゃね?と(悪夢を見ていておかしくなっていたので)考えて、試しかけたが、こんなの全く信頼できないだろと、我に帰ってテキスト生成するだけでやめれた。(逆にいうとテキスト生成まではしてしまった。)

  • lgbm stacking

これも一応ほんとに最後の最後に試したけど、あんまり精度出ないなで終わりました。

まとめ

辛い辛いコンペだった。

自然言語オンリーコンペはコンペごとの取り組みの違いみたいなのが生まれにくく、SOTAなモデルをガチャガチャして精度出す感じで、あんまり面白くないのかもしれないなと正直なところ思ってしまった。逆にいえば、一度知識を得てしまえばあまり苦労せず安定して好スコアを出せるのかもしれないが。

コンペとしては面白くないのかも知れないけれども、自然言語処理自体には依然興味はあるのでそこら辺は悪しからず。

後そろそろチームを組んで議論するという経験をしてみたいものですが、何とかチーム組めたりしませんかね... 今回のコンペも色々コンペ中に話し合いたいことあったんですよね... 基本独学でやってるもんで、なんか今やってるのが正しいことなのかわからないのがすごい精神にきます...

色々学ぶことはありました。

学んだこと

  • そもそもNLPについて
    NLP、ノー知識だったのでHuggingFaceのTransformersやらライブラリの存在を知れたこと。いろんなモデルがあるんだということ。あまり勉強間に合ってないけど、attenstionとかの存在を知れたこと。
    ただ、NLPについてはまだまだ全く分かりません。今回初めてNLPの処理にこんなにGPU資源が必要だということを知りました。また、自然言語処理は分野として面白いけど、この方面にキャリアを進めていくのは茨の道になりそうだなということも感じました。というのも近年発展が凄まじいし、正直自分が何かやらなくても自分よりすごい人がどんどんやってくれる分野だろうなと感じました。実際今回の僕はBERTにタダ乗り状態でしたし... 未来について考えると精神が死んでくるのでここらでやめておきますが。

  • Pseudo-Labeling

  • adversarial validation

  • アンサンブル
    複数モデルをアンサンブルした方が良いぞということ。

こんな感じですかね?まだまだ後から書けそうなことを思いついて、追記するかも知れませんがとりあえずこんな感じで僕の参加記は終わろうと思います。長々見てくれた人がいたらありがとうございました。

余談ですが、今回はてなブログMarkdown記法で書いてみたのですが書きにくい、かつ読みにくいと感じました。(やっぱり改行の仕様が酷いんだよなMarkdown、何だよ半角スペース二回って)次の記事からははてな記法に戻すと思います。読みにくく感じたらすみません。