kaggle: Toxic Comment Classification Challenge まとめ

f:id:copypaste_ds:20190131222934p:plain

はじめに

過去コンペまとめ記事の一作目です。タイトルにもあるように今回は2017年12月にkaggleで開催された Toxic Comment Classification Challenge(以下、Toxicコンペ) をまとめたいと思います。 kaggleの楽しみ方として実際にコンペに参加してスコアを競うのも一つですが、過去コンペの解法を眺めているだけでも十分にkaggleを楽しめますし、何より勉強になるのでおすすめです。 今後も気になったコンペから順にまとめ記事を書いていくつもりですので、どうぞよろしくおねがいします。

コンペ概要

Toxicコンペは、JigsawとGoogleが創設したConversation AIチームが主催の 有害なコメントへのタグ付けコンペ です。データはWikipediatalk pageのコメントでNLPの知識が求められます。
Wikipediaは誰でも編集可能なインターネット百科事典ですが、各ページには利用者が質問や議論を行うためのtalk pageが用意されています。talk pageで利用者がページの改善について意見交換することでページの質を高めているというわけです。質問や議論では相手に敬意を払うことが大切ですが、なかには有害なコメントもあるようで、Toxicコンペの目的はそれらの有害なコメントを分類することです。
賞金・期間・参加者数などは以下の通りで、参加チームは4551チームと非常に多く人気のコンペであったことがわかります。

賞金 期間 参加チーム数 参加者数
$18,000 2017/12/20 ~ 2018/03/21 4,551 93,435

データの種類とタスク

テキストデータを用いた 多ラベル分類問題(Multilabel classification) です。各クラスの所属確率を出力するモデルを構築する必要があります。ただし多クラス分類問題(Multiclass classification)とは異なり、一つのサンプルが複数のクラスに属する可能性があります。クラスは6種類(toxic, severe_toxic, obscene, threat, insult, identity_hate) で、下図のようにclass imbalance かつ mulitlabel といった特徴があります。画像は後ほど紹介しますが、この EDA kernelから拝借しました。

f:id:copypaste_ds:20190131222854p:plain

f:id:copypaste_ds:20190131222911p:plain

データサイズは以下のとおりです。NLPコンペではありますが データが小さい ため、比較的参加しやすいコンペだったのだと思います。

ファイル名 データサイズ レコード数 カラム数
train.csv 68.8M 159,571 8
test.csv 68.4M 153,164 2

ちなみにtrain.csvのheadはこんな感じです。 コメント毎にIDとクラスラベル が与えられています。

f:id:copypaste_ds:20190201084729p:plain

試しにtoxicクラスのサンプルをいくつか見てみると、例えばこんなものがありました。

Bye! Don’t look, come or think of comming back! Tosser. 

FUCK YOU U USELESS BOT FUCK YOU U USELESS BOT FUCK YOU U USELESS BOT (以下繰り返し)

DELETE!?!?!? You delete vandalist pages?!?!?! You sick, sick bitch.

なんだか治安が悪いですね。英語が苦手な私でもきれいな言葉ではないことくらいはわかります。勿論toxicクラスのサンプル自体が全体で見ると少ないため、このような文章ばかりではありません。興味のある方は自分の手でデータを確認してみてください。(ちなみにservere_toxicクラスはさらに酷いコメントで溢れています)

評価方法

評価指標には mean columns-wise ROC AUC です。つまりクラスごとに算出したAUCの平均値で評価します。 ちなみに1st solutionは private LBで 0.9885 と非常に高いスコアを記録しています。1.0に近いスコアが出ていることから、タスクとしては簡単だったのかもしれません。

提出方法

提出のフォーマットは6種のクラスそれぞれの所属確率を以下のように出力します。 多クラス分類問題ではないので、各クラスの予測確率の和が1である必要はありません。

id toxic severe_toxic obscene threat insult identity_hate
00001cee341fdb12 0.5 0.5 0.5 0.5 0.5 0.5
0000247867823ef7 0.5 0.5 0.5 0.5 0.5 0.5

勉強になるkernelとdiscussion

上位解法ほどのスコアは出せませんが、kernelとdiscussionは良いアイディアと実装で溢れています。どのアプローチも勉強になったので簡単にまとめておきます。

Stop the S@#$ - Toxic Comments EDA | Kaggle

EDA kernelです。データの確認、可視化、集計結果の考察、前処理、基本的な特徴量抽出、モデリングなど初学者には参考になるものばかりだと思います。以降の解法を読む上で一度データを確認しておきたい方は、このEDAを見ることをおすすめします。データの雰囲気が分かり、解法が理解しやすくなるはずです。

NB-SVM strong linear baseline | Kaggle

NBSVM(Naive Bayes - Support Vector Machine)を参考にした(?)解法です。公開kernelの中では最もVoteを稼いでいました。NBSVMの論文へのリンクが貼られていますが、kernelで実装されているものは元論文の手法にいくらかアレンジを加えたものでした。アイディアとしてはベイズの定理を用いて特徴量を抽出した後、SVMで分類するだけなのですが、特徴抽出時に使用する式を少し変えているのとSVMの代わりにロジスティック回帰を使用している箇所が元論文と異なります。

Logistic regression with words and char n-grams | Kaggle

TFIDFで特徴抽出した後、ロジスティック回帰でモデリングしています。私のようなNLP初学者はこのようなシンプルなモデルからはじめると良いのかもしれません。TFIDF特徴を作成する際は sklearnのTfidfVectorizerを使用しています。これを使えばngramの指定や文字単位(word, charなど)の指定が簡単にできるので便利ですね。個人的には analyzer='word', ngram_range=(1, 1)とanalyzer='char', ngram_range=(2, 6) で作成した行列をconcateして特徴量にする部分が勉強になりました。analyzer='char'に設定して特徴量にする方法は覚えておこうと思います。ロジスティック回帰モデルはクラスごとに6つ作成します。multilabel なので2クラス分類モデルをラベルの種類数分用意する必要があるようです。欠点としてはクラスの共起性をモデルに組み込みにくいことでしょうか。

LightGBM with Select K Best on TFIDF | Kaggle

TFIDFで特徴量抽出した後、LightGBMでモデリングしています。さきほどの解法のロジスティック回帰部分をLightGBMに変えています。

Wordbatch 1.3.3 FM_FTRL LB 0.9812 | Kaggle

TFIDFと集計ベースの特徴量生成した後、FM_FTRL(FTRLの関数をFMとしたアルゴリズム、私もあまりわかってません)でモデリングしています。実装にはwordbatch(kagglerが作った文章特徴抽出ライブラリ)を使用しています。(wordbatchは所見だったので勉強になりました。)集計ベースの特徴量には文字数、単語数、大文字数、リンク数、メールアドレス数など一つ一つ作成しています。こういう特徴量生成を見るとkaggle感があって良いですね。気になる方はコードを読んでみてください。

[For Beginners] Tackling Toxic Using Keras | Kaggle

kerasを使ったLSTMのtutorialです。文章の前処理(Tokenization -> Index Representation)、Embedding, LSTM, GrobalMaxPool, dropout などの役割と実装が丁寧に説明されています。NNでNLPをはじめてみたい方におすすめできるkernelです。

Improved LSTM baseline: GloVe + dropout | Kaggle

GloVeとLSTMを用いたNN解法です。NN解法の多くはWord2vec, GloVe, fastTextなどで単語をベクトル表現した後、BidirectionalのLSTMやGRUを通し、Denseに繋げているようです。このkernelではGloVe -> Bi-LSTM -> GlobalMaxPool1D -> Dense -> dropout -> Dense のNNを作成しています。

Capsule net with GRU | Kaggle

Gapsule Netを実装しています。Capsule Netとは一般的なNNのニューロン(入出力がスカラー)を入出力がベクトルのCapsuleという構造に一部置き換えたネットワークだそうです。(よく知らないので勉強します。)コードを拝借して近々動かしてみようと思います。

A simple technique for extending dataset | Kaggle

翻訳を利用した augmentation について紹介されています。翻訳による変換を2回行うことでデータの水増しをねらいます。具体的には、英語 -> ドイツ語 -> 英語、英語 -> フランス語 -> 英語、英語 -> スペイン語 -> 英語 といった具合に変換します。変換前と変換後の文章では単語や言い回しが微妙に異なるためデータの水増しが可能です。

上位解法概要

上位解法はembeddingとRNNを用いたNN解法 が目立ちます。 kernelを見るとロジスティック回帰、NBSVM、LightGBMなどの手法も多く見られましたが、NNを使わずに上位を目指すことは難しかったようです。NLPといえばNNが定石の時代になりつつあるのでしょうか。(よく知らない) discussionには上位解法がいくつか共有されていますが、今回は 1st ~ 3rd solutionのみまとめました。

f:id:copypaste_ds:20190201001528p:plain

1st place solution: 1st place solution overview | Kaggle

解法の要点は以下の4つです。

  1. 様々な学習済みのembedding手法 (baseline public LB of 0.9877)
  2. 翻訳によるデータの水増し (Translations as train/test-time augmentation) (boosted LB from 0.9877 to 0.9880)
  3. pseudo-labeling (boosted LB from 0.9880 to 0.9885)
  4. Robust CV + stacking framework (boosted LB from 0.9885 to 0.9890)

1. 様々な学習済みの embedding 手法

今回のタスクでは embedding layer が重要であると判断して、embedding layerのチューニングに集中したそうです。それ以降の構造は Bi-GRU と Dense×2 なのでkernelの解法とあまり違いがないように思えます。注目のembeddingですが、Common Crawl, Wikipedia, Twitterのデータで学習済みのfastTextとGloVeを使用したようです。

2. 翻訳によるデータの水増し

train/testの両方で翻訳によるaugmentationをしたそうです。(英語 -> ドイツ語 -> 英語と変換するやつ)ドイツ語、フランス語、スペイン語で実施し精度向上に大きく貢献したそうです。train-val split時にはleak対策のため、同じ文章から作成したサンプルは同一セットにまとめるよう注意します。少し細かいですがこの手のテクニックは実務でもコンペでも重要だと思うので知っておくと良さそうです。予測時には4つのコメント(オリジナルとフランス語、ドイツ語、スペイン語を経由したもの)の平均をとります。(ここもしかしたら読み間違えているかも。言語ごとにモデルを作っている可能性もある...??)

3. pseudo-labeling

標準的なものからLossを変えたものなど、いくつかのpseudo labelingを試したそうです。最も効果的だったのは標準的なもの(test sampleに予測値を付与し、それをtrainに加えた上で再度学習する)だったそうです。今回のタスクはスコアが0.98以上と非常に高く、疑似ラベルの信頼性が高かったことも影響しているのかもしれません。コメントにはスコアがそこそこのタスクで同様のことを試した際にはうまくいかなかったという経験談もありました。
ちなみにコメント欄でpocketさんがわかりやすい図を共有していたので貼り付けておきます。lossを全てlog lossに設定すること以外はこの理解で間違いないようです。 f:id:copypaste_ds:20190201115215p:plain

4. Robust CV + stacking framework

stackingにはベイズ最適化でゴリゴリチューニングしたLightGBMを使用し、単体モデルよりも~0.001程度スコアを向上したそうです。LightGBMは深さ浅めでL1ノルム強めにすると良かったとのこと。ブレを抑えるべくseedを変えたDARTとGBDTをそれぞれ6つ学習してバギングしたようです。次にCVについてですが、CVスコアとしては accuracy, log loss, AUCを確認していたそうです。モデルを採用するかはstackingに加えた際に、CV-logloss, CV-AUC, public LBの全てが改善されているか否かで判断します。多くのモデルを捨てることになりますが、overfittingを避けるためにこうしたとのこと。指標を複数用意してTrust CVしていたのですね。

その他に試したこと

上記4つは特に効果的だったようですが、その他の実験結果についても言及されているので興味がある方はぜひ。

2nd place solution: 2nd place solution overview | Kaggle

モデルの多様性を意識してRNN, DPCNN, GBMを学習させてアンサンブルしたそうです。NNモデルの要点について3つ共有されていました。 最終的に30個のモデルを作成し、予測値の平均をとったそうです。

1. pre-trained embeddings (fastText, GloVe twitter, BPEmb, Word2vec, LexVec)

1位解法に続いて2位解法でもembeddingをいろいろ試しています。私はBPEmb, LexVecは初見だったのでとても参考になりました。embeddingモデルのまとめはこのgitgubにまとまっていたので興味のある方は覗いてみて下さい。BPEmbに関しては275言語に対応しているようです。

2. 翻訳によるデータの水増し

ここも1位解法と同様ですね。詳細は述べられていませんが、ドイツ語、フランス語、スペイン語の翻訳を利用したaugmentationをしたそうです。

3. 多言語に翻訳した上でBPEmbでembedding

英文をドイツ語、フランス語、スペイン語に変換した後(英文に戻すことなく)学習済BPEmbでEmbeddingしたそうです。BPEmbをうまく使用していますね。

3rd place solution: About my 0.9872 single model | Kaggle

3位解法として最良のシングルモデルが共有されていました。使用したのはRNNモデルでモデル構造とパイパーパラメータ、前処理について書かれていたので簡単にまとめます。実装にはkerasを使用したようです。

モデル構造

  • 1層目
    • 基本的にはfastTextとGloVe twitter embeddingをconcatしたものだそう。単語ベクトルがない単語は"something"に置換したとのこと。加えて、大文字の単語は1, それ以外は0のフラグも特徴量も追加したようです。
  • 2層目
    • SpatialDropout1d(0.5)
  • 3層目、4層目
  • 5層目
    • LSTMのlast state, maximum pool, average poolと別で作成しておいた2つの特徴量(ユニークな単語の割合、大文字単語の割合)をconcate したそうです。NN初心者の私にとっては非常に参考になりました。
  • 6層目
    • dense

パイパーパラメータと前処理

  • batch size: 512. 大きめに設定したほうが結果が安定したそうです。
  • Epoch: 15
  • Sequence length: 900
  • optimizer: Adam with clipped gradient
  • 前処理としてUnidecodeライブラリで文章をASCIIに変換した後、文字と句読点以外は除外したそうです。その他にもスペルミスを一生懸命直したところスコアが改善したようです。詳細はdiscussionに書かれているので興味のある方はぜひ。

その他の上位解法

discussionは解法の宝庫ですね。余力がある方はこちらもぜひ。

順位 リンク
3rd 3rd Place Solution Overview | Kaggle
5th 5th place Brief Solution | Kaggle
12th 12th place single model solution share | Kaggle
15th 15th Solution Summary: Byte Pair Encoding | Kaggle
25th 25th Place Notes (0.9872 public; 0.9873 private) | Kaggle
27th 27th place solution overview | Kaggle
33rd 33rd Place Solution Using Embedding Imputation (0.9872 private, 0.9876 public) | Kaggle
34th 34th, Lots of FE and Poor Understanding of NNs [CODE INCLUDED] | Kaggle

おわりに

ブログにまとめるまでにそれなりに時間はかかりましたが、NLPの分類タスクの雰囲気がつかめたのは収穫です。上位解法ももちろん勉強になりましたが、個人的にはkernelで多様なアプローチを読むほうが面白いし勉強になると感じたので「kaggleでNLPの勉強してみたいけど、どこから手を付けていいかわからない。」という方は是非kernelを読んでみて下さい。読むだけで学びがありますし、終了したコンペではありますが kernelを真似して、late submission してみるのも面白いかもしれません。
続編はマイペースに書いていこうと思いますが、もしかしたらめんどくさくなるかもしれません。。めんどくさくなってしまったらごめんなさい。