天色グラフィティ

機械学習やプログラミングでいろいろ作って遊ぶブログ

Quoraコンペの振り返りと上位解法まとめ

f:id:ejinote:20190215163908p:plain

KaggleのQuora Insincere Questions Classificationコンペに参加しました。

f:id:ejinote:20190215162126p:plain

結果は121位で、銀メダルでした。これで銀メダルが3枚目です。わーい。

Public Leaderboardで692位と振るわず、コンペのdeadlineが修論発表の当日だったので直前ほとんど何も出来ず、「Quora何もわからなかった」という絶望感でいっぱいでした。

蓋を開けてみたら多くのTeamが消え、あれよあれよという間に銀メダルになりました。

タスクの概要

QuoraというQ&Aサイト(日本で言うYahoo!知恵袋)に投稿された質問の中で、有害なもの(攻撃的、差別的、卑猥など)を分類するコンペです。

タスクの種類は0 or 1の2値分類で、正例がかなり少ない不均衡なデータセットになっています。

f:id:ejinote:20190215172356p:plain

評価指標はF1 scoreです。確率として出力した予測値をどこから1として、どこから0とするかの閾値決めが大きく最終スコアに影響を与えました

コンペの形式としてはKernel経由の提出のみを受け入れるKernel Onlyコンペかつ、締切後にテストデータを差し替えて順位を決定する2-stage制コンペでした。 1st stageではテストデータが56,000件程度であるのに対し、2nd stageでは376,000件と6倍程度に増えることが予告されていました。

制限時間内に実行出来なかった場合は順位がつかないという厳しいペナルティがあるため、素早く前処理をしたり、上手くモデルを訓練する必要のあるコンペだったと言えるでしょう。

上位陣の手法

まずは上位陣の手法をまとめます。

1st place solution (Psi)

https://www.kaggle.com/c/quora-insincere-questions-classification/discussion/80568

モデル

f:id:ejinote:20190215162823p:plain
モデルの概要

画像は上のリンクからの引用です。

  • Bidirectional LSTMの後にkernel_size=1のConv1Dを挟むのが特徴的
  • 別で作成したExtra Featureを後で加える
  • 多くのモデルを利用するためには、1つのモデルの学習時間は短くしたい
  • バッチごとに異なる長さに系列を埋め込む
  • maxlenを最長系列長に合わせるのではなく、95%点で設定する
  • Cyclic Learning Rateを利用。最適化手法はNadam
  • 図で示したモデルを10個訓練し、アンサンブル

バッチごとに系列長を変えるのは確かに実行時間の改善にかなり役立つ、合理的なテクニックです。今回のデータセットでは、系列長の長さはかなり短い方に偏っていて、95%点の長さに合わせて系列を切るのは妥当だと思います。

自分でも実装して使えるようにしておきます。

Embedding

  • EmbeddingはGloveとParagramの重み付き平均を利用
  • 単語を単数形・複数形に直したり、記号を削除したりして、できるだけ多くの単語を学習済みのEmbeddingと対応させる
  • 含まれていない単語の扱いはいろいろ検討したが、結局ランダムなベクトルを利用

Embeddingの重み付き平均は初めてみました。Gloveの重みを大きくしたのは単体での性能がGloveの方が上だったからでしょうか?(本人に聞いている途中です) それぞれの重みはクロスバリデーションの精度を見ながらチューニングしたそうです。

多くの単語をEmbeddingと対応させることはかなり重要という感触を僕も持っていました。単数形・複数形の変換は行っていなかったので、ライブラリに加えておきます。

閾値

  • F1スコアが指標だったので、最適な閾値を探すのが重要
  • バリデーションの閾値を流用するのは良くなかったので、固定の閾値を利用してもなお、結果が安定する方法を探した
  • 確率値をそのまま用いるのではなく、順位に変換した。閾値は、Fold間の精度の平均と標準偏差を考慮し、下振れしたときのスコアが最も良い値に固定した。

たしかに。閾値に対してF1スコアが大きく変動することには気づいていたので、Rankを取る操作は気づいて然るべきでした。

異なる種類のモデルをアンサンブルする時、出力の分布の形を揃えるためにRankに変換したあと平均を取るテクニックが存在します(これをRank Averageといいます)。 僕は「異なるモデルをアンサンブルする時に使う手法」という捉え方をしてしまっていたのですが、他の場所でも使えるのですね。固定観念にとらわれず、視野を広げる必要がありそうです。

バリデーション

10-foldにCVを切り、v回モデルを訓練する。精度の平均や中央値を利用することで、より正しくそのモデルを評価できる。

すごい時間がかかりそうですね……

2nd place solution (takapt)

https://www.kaggle.com/c/quora-insincere-questions-classification/discussion/81137

  • 1層のBi-GRUに特徴量を足したシングルモデル
  • seed average
  • 系列長をバッチごとに調節
  • スペル訂正(大文字小文字の区別をせず、編集距離1の単語を探す)
  • max pooling+average poolingを最初と最後のoutputに適用
  • GloVeとwiki-newsのEmbeddingに加え、64次元のembeddingをコンペのデータセットで学習(fastText)
    • 僕もやったんですが、あまり伸びなかったです……
  • 全て大文字、最初が大文字、最初だけ大文字、Embeddingに含まれていない語彙、のフラグをEmbeddingに加える
  • Adamを利用、学習率は各エポックで0.8倍ずつする

kernelに出ていたモデルはどれもRNNの部分を複雑にしたものが多かったですが、時間制限があるkernelコンペではモデルをシンプルに保ち、seed averageをするのが良さそうですね。

takaptさんも系列長の長さをバッチごとに変えるというテクニックによってたくさんseed averageをする時間を稼いでいますね。

3rd place solution (Guanshuo Xu)

https://www.kaggle.com/c/quora-insincere-questions-classification/discussion/80495

Kernel: https://www.kaggle.com/wowfattie/3rd-place

  • Spacy Tokenizer
  • Embeddingと対応付けるため、stemmingやスペル訂正などを行う
  • 学習途中のモデルを保存しておき、最後に混ぜるcheckpoint ensemble
  • maxlen=55と短め。多くの文章が短めだったので計算時間の短縮にかなり効いていると思われる

7th place solution (yufuin)

URL: https://www.kaggle.com/c/quora-insincere-questions-classification/discussion/80561

  • FWORD (fuckとかshitとか)は「*を含んでいる単語」として定義。これらの埋め込みは「fuck」「shit」「*」の平均を利用
    • 有害コメント分類系で効きそうなテクニック
  • Embeddingの後に強めのDropout(drop=0.4)
  • 同じ長さの系列同士をまとめてminibatchを作る(Bucketing)。学習速度が3倍程度早くなる
    • 精度への影響は無いのかな? 長い順で並べ直すとすごく精度が落ちた記憶がある
  • checkpoint ensemble

10th place solution (tks)

https://www.kaggle.com/c/quora-insincere-questions-classification/discussion/80718

  • 複数のpretrained embeddingを組み合わせるためにProjection Meta Embedding(PME)を利用
    • pretrained embeddingをconcatしたあと、Dense+ReLUで低次元に写像して、Embeddingのように使う

複数のEmbeddingをどう組み合わせるかという議論はコンペ中も行われていて、concatする、meanを取る、Embeddingごとにモデルを作ってアンサンブル、などがありましたが、PMEも面白そうですね。次回の自然言語処理コンペで試してみたいです。

13th place solution (Canming Wang)

https://www.kaggle.com/c/quora-insincere-questions-classification/discussion/80499

Kernel: https://www.kaggle.com/canming/ensemble-mean-iii-64-36

  • LaTeXやHTMLタグの除去
  • Glove0.64+Params0.36で重み付き平均したEmbedding
  • AdamWとweight decayを利用

15th place solution (Bai)

https://www.kaggle.com/c/quora-insincere-questions-classification/discussion/80540

Kernel: https://www.kaggle.com/xiaobai1123q/15th-place-solution

  • 4つのモデルのアンサンブル
  • GloVeとFastTextを連結したEmbedding
  • maxlen=57(短い)

覚えておきたいテクニック

  • 系列長を動的に変更する+ミニバッチの95%点の長さで切る
  • Embeddingの重み付き平均
  • できるだけ多くの単語をEmbeddingと対応させる
  • 閾値でスコアが大きくブレるときは、より良い閾値の設定方法を考える
  • FWORDっぽい単語の埋め込みはFWORDの平均を利用
  • HTMLタグなどの除去

【おまけ】151th place solution (僕)

  • Focal Lossの利用
    • わずかな改善
    • クラスが不均衡なので、使えるかと思って試した
    • Lovasz Lossも検討しましたが、時間との兼ね合いでボツに
  • HTMLタグの除去
    • LaTeXの除去もやっておけばよかった
  • Embeddingにできるだけ単語を押し込む
    • upper()lower()などを利用
    • 単複の変換もやっておけばよかった

2-stage制コンペで大切なことは何よりも2nd stageに進むことです!

モデルは基本的にはPublic Kernelに上がっているものをベースにしていますが、必要の無いデータの出力やモデルの保存などを省き、前処理もできるだけ高速に行うようにしています。言語処理ならばmaxlenなどを小さめに設定する必要もあるでしょう。

終わりに

solutionがアップされ次第、追記していきたいと思います。

Kaggleをやってみたい!という相談を最近良く受けます。僕で良ければTwitter(@SakuEji)のDMなどでご相談にも乗れますし、kaggler-jaというコミュニティもおすすめです。 盛んに質問や議論が飛び交うあたたかいコミュニティなので、怖がらずに登録してみると良いのではないかと思います。

登録は以下のURLから。メールアドレスを入力すると招待リンクが送られます。

https://kaggler-ja.herokuapp.com/

ちなみに修論から開放された僕はいま、電線、Elo、Santanderと3つのコンペを触っています。 引き続き楽しんでいきましょう! Happy Kaggling!