4th Place Solution
データの水増し戦略
今回のコンペの途中、60位くらいから一気に6位まで順位が跳ね上がるという経験をしました。
順位上昇の一番のポイントはtrainデータの水増し戦略だったのかなと思います。
※「水増し」と言えるのか微妙な気もしてきました。
間違いやさらに詳しい情報等ございましたらコメントください。
------<2020-12-22 追記>------
コメントにて本方法は「pseudo labeling」だと教えていただきました。ありがとうございました。
書籍『Kaggleで勝つデータ分析の技術』p.266にも掲載されていました。
頭のどこかにこの部分の内容だけちらっと残っていたのかもしれません。
ちゃんと知っていればこんなにいろいろ書かなくても「pseudo labelingが効きました」で終わりでした。
--------<追記終わり>--------
概要
- Precisionが高いモデルを活用し、testデータから「ほぼ間違いなくスパムだろう」というメールを抽出
- trainデータに追加して再度学習
- これらを繰り返してtrainデータを水増しし、testデータの「スパム:非スパム」の割合に近づける
具体的には以下の手順でtrainのスパムメールを水増ししました。
- 元のtrainデータで学習を行う(汎化性能評価でy=1のPrecisionが1.0であることを確認)
- 学習したモデルでtestデータを予測し、y=1の予測確率が設定した閾値よりも高いメール(疑似スパム)を抽出・スパムラベルを付けてtrainに加える
- 疑似スパムを加えたtrainデータで学習
- 2., 3.を繰り返す
- 1., 3.にて学習したモデルでtestデータ全体の予測も行う(提出用・アンサンブル用)
データを「濾過」しているようなイメージでしょうか。
1.~4.によって、trainデータでのスパム割合をtestデータでのスパム割合に近づけました
(実運用場面ではテストデータでのスパム割合は分かっていないはずなので工夫が必要)。
この「濾過」を複数の閾値(0.7, 0.75, 0.8, ..., 0.95, 0.99)を設定して行いました。
しっかりとした検討はできていませんが、今回4位になれたのはこの水増し戦略がうまく効いたからかなと感じています。
水増しまでの経緯
ルール上、trainデータとtestデータで「スパム:非スパム」の割合が大きく異なることが分かっていました。
また、testデータでのスパムの割合も知らされていました。
最初はラベルの偏りへの対処ということで、普通にアンダーサンプリングしてLGBMで予測したりCNNで予測したりしていましたが、上位には遠く及びませんでした。
そもそもtrainデータでのスパムメールが少なすぎたため、アンダーサンプリングを行うと非スパムを大量に捨てることになります。それはもったいないと考えていました。
また、このままでは学習も汎化性能評価も難しいと感じていたため、trainデータのスパムメールを水増しすることを考えました。
試行錯誤
画像データの水増し(augmentation)はよく耳にしますが、NLPでの水増し方法は知りませんでした。
そのため、最初はググって目についた「文中の単語を類似単語と入れ替える」方法を試してみました。
結果、この方法はうまくいきませんでした。
※外部データを使わずに類似単語を持ってくる方法として
「fasttextの学習済み単語分散表現上での類似度の高い単語と入れ替える」
は良い案だと思ったのですが…。
※この実験の際、同時に5FoldでのCVにコードを変えており、そのCVでの学習がうまくいっていませんでした。
したがって、類似単語との入れ替えの効果は不明なままです。要因を交絡させるとか初歩的で恥ずかしい…。
どうしたものかと悩んでいたとき、手元のモデルによるスパム判定のPrecisionが1.0なのが目を引きました。
これはつまり
「未知データに対して、スパムと判定したメールはすべて実際にスパム
(含まれるスパムメールのうちどれだけ漏れなくスパムと判定できるかは置いておいて)」
である可能性が高いと期待されるモデルが手元にあるということかなと考えました。
このモデルでtestデータの予測を行えば、スパム予測確率が1に近いメールはほぼスパムと見て間違いないでしょう。
このほぼスパムなメールを抽出してtrainに加えることで、比較的保守的にデータの水増しができるはずです。
※この方法、NLPにおける水増し方法を調べている際(?)にちらっと見かけたような気もしますが、
当初は「予測を元にtestからtrainにデータを移すなんて、trainデータがおかしくなるのでは?正気か?!」
と感じており、まさか使うことになるとは思わずメモっていませんでした…悲しい…。
<2020-12-22 追記> 先述のとおり、「pseudo labeling」という手法です。
以下の点に鑑みても、抽出したメールがスパムである可能性は高いと踏んでいました。
- testデータではスパムメールの割合が大きい
- 現実場面での例でもスパムメール判定はかなりの高精度で実現されているため、きっちり判別することが原理的に可能なはず
実際、この水増しによって60位くらいから一気に6位まで跳ね上がりました。
ですので、以下は補足のようなものです。
前処理
- メールの前処理はtextheroというライブラリを使用
- 行った前処理は以下
- 大文字を小文字に変える
- \nの前後にスペースを入れる
- httpから始まる文字列をhttpに置換
- 数字を0に置換
- 最初の9文字(Subject: )を除去
- 句読点の除去は行っていない
- 文のまとまりをモデルが知るために必要かもと考えたため
- 発音区別符号、ストップワードの除去も行っていない
- 無いとは思うが、万が一textheroのストップワード辞書が外部データとみなされると怖かったため
- tf.keras.preprocessing.text.Tokenizer()でトークン化してモデルに入力
モデルのアルゴリズム
- CNN + fasttextの学習済み単語分散表現を使用
- fasttextの学習済み単語分散表現を用いたembedding層
- 1次元の畳み込み層と1次元のプーリング層を1層ずつ
- ハイパラは最初に決めたものを使用し、チューニングは行っていない
- ※書籍『機械学習・深層学習による自然言語処理入門』を参考にモデルを構築
本来はハイパラチューニングを行ったり、BERTを使用したりするべきでした。
ただ、今回は上述のスパム:非スパムの割合の非対称性への対処を優先+最後までそちらに注力してしまったため、手を回せませんでした。
予測
- 複数の閾値・各濾過時点でのtestデータ全体での予測確率の平均をとり、アンサンブルとした
- testデータでのスパムメール17000件に鑑みて、testデータ予測確率の上位17000件をスパムメールと判定して提出
- 閾値や使用する濾過時点を変えつつ提出を繰り返す
感想・反省
- 実際の運用では避けるべき、今回のルールや設定に甘えた、詰めの甘い分析が多かった
- testデータのスパム割合が分かっている点
- 実際の運用ではここまで正確に分かっていることは無いはず
- 最終submissionを選ばないということ・testデータからtrainデータに引っ張ってきていたことから、CV戦略が適当になった点
- train, valid, 汎化性能評価用のデータセットをホールドアウトで作成
- 最終的には結局、Publicスコアを見ながら試行錯誤してしまった
- ※競技としてルールを活用して勝ちにいこうということで甘んじた
- 閾値の選定や別のモデルなどを試さず突っ走ってしまい、突き詰めきれない甘さが出た
- モデルの選定よりも、trainデータの水増しだけに注力してしまった
- BERTも使ってみるべきだったし、CNNも層数を増やすなどしてもよかったはず
- 何件くらい予測が外れているのかの検討もするべきだった
- F1なのが少し面倒だが、デモデータでも作ってシミュレーションすればよかった
- 予測が外れた生データを見るなどして考察を深めるべきだった
- 生データをちゃんと見るべきだった
- 他の方と異なる着眼点で良いスコアを出せていたのであれば嬉しい
スパムメール判定は機械学習やNLPの本を読んでいて一番はじめに目にすることの多い事例だと思います。
そのスパムメール判定を自分の手でやってみる機会を得られて興味深かった・楽しかったです。
ありがとうございました。