Metric Learning 入門

f:id:copypaste_ds:20190301002935p:plain

はじめに

metric learningについて学ぶ機会があったので忘れないうちに得た知識を書き留めておきます。学んだ期間は10日程度と短く、deep learningも含めて初心者ですので疑いながら読んでいただければと思います。間違いを見つけた方はご指摘ください。本記事ではmetric learningの概要から画像データ・テーブルデータへの適用結果まで簡単に紹介します。

metric learningとは

metric learningとは データ間の計量(距離や類似度など)を学習する手法 です。直感的には、意味の近いデータは近く、意味の遠いデータは遠くなるように計量を学習 します。以下の図は靴の画像データに対してmetric learningを適応したイメージ図です。(画像はこのページから拝借しました。)

f:id:copypaste_ds:20190226193837p:plain

スニーカー同士、ブーツ同士、ヒール同士が近くに配置されていることがわかります。また、ヒールが高いほど右方向に配置されていることもわかります。このように、metric learningを応用すれば、靴の形状を考慮した距離を学習することができます。
ちなみに「計量を学習する」を「特徴量(特徴量空間)を学習する」あるいは「特徴量生成用の関数を学習する」と考えてもよいです。この考え方ついてはマハラノビス距離学習の説明時に軽く触れます。  

metric learningの利点は意味的な距離を考慮した特徴量を学習できることです。教師ラベルを上手く準備できれば、特徴量空間の作り方(考慮したいデータの意味)をある程度コントールできます。 応用範囲は多義に渡り、例えば以下のようなタスクがあります。

意味的な距離を考慮した特徴量空間を上手く学習できれば、未知クラスのデータに対してもある程度頑健に対応できる点も強力です。 先程の靴の例だと、学習データに含まれない高さのヒールも空間の右上にうまいこと配置されるイメージでしょうか。

マハラノビス距離学習

metric learningの古典的な手法にマハラノビス距離学習があります。アイディア自体はdeep learningを用いたmetric learningと似ています。
マハラノビス距離学習では以下の式の共分散行列\( M \)を学習します。

f:id:copypaste_ds:20190226195628p:plain

\( M \)が決まれば距離が定まるので距離を学習していることになります。
ところで、\( M \)が半正定値行列であればマハラノビス距離は以下のように変形できます。

f:id:copypaste_ds:20190226195818p:plain

つまりデータ間のユークリッド距離が適切になるような変換\( L \)(あるいは特徴量\(L \boldsymbol{x}, L \boldsymbol{y} \))を学習する手法 とも考えられます。 このように、「計量を学習する」を「特徴量(特徴量空間)を学習する」あるいは「特徴量生成用の関数を学習する」と考えることができます。

次に\( M \) (= \( L \) )の決め方ですが、マハラノビス距離学習では以下の最適化問題を解けば良いです。\( S \)(類似データの組)と\( D \)(非類似データの組)は事前に用意しておく必要があります。よくある準備方法として、同じクラスのデータなら似ている、異なるクラスのデータなら似ていないとする方法があります。

f:id:copypaste_ds:20190226200339p:plain

このように似ているデータは近く、似ていないデータは遠くなるように学習させるのがmetric learningの特徴です。

deep metric learningとは

最近はdeep learningを用いたmetric learning(deep metric learning??)が注目されています。deep metric learningではマハラノビス距離学習で言うところの\( L \) ( = \( M \) )をdeep learningで学習します。これによって非線形変換が可能になります。

今回は以下の手法を順に紹介します。

  • siamese network
    • 2サンプルを一組で入力するやつ(contrastive lossと一緒に紹介)
  • triplet network
    • 3サンプルを一組で入力するやつ(triplet lossと一緒に紹介)
  • L2 softmax lossを使ったnetwork(本記事ではL2 softmax networkと呼ぶことにします)
    • metric learningっぽいくないけどそれなりに強いやつ

f:id:copypaste_ds:20190226201747p:plain

siamese network

siamese networkはずいぶん前(少なくとも2006年)に提案された手法です。(論文はこちら)特徴は2サンプルを一組で入力する点とサンプル間の距離をcontrastive lossで明示的に調節する点です。図の \( f \) をdeep learningに置き換えればdeep metric learningとなります。

f:id:copypaste_ds:20190226204013p:plain

マハラノビス距離学習と同様に、contrastive lossにも類似データは近く、非類似データは遠くするような工夫がされています。図にある通り、類似サンプルの組を入力した場合は \( d_i \) が小さくなるように、非類似サンプルを入力した場合は \( -d_i \) が小さくなるよう( \( d_i \) が大きくなるように)に学習させます。距離\( d \) は好きに選べば良いですが、元論文ではユークリッド距離が採用されていました。\( m \)(定数)を足してヒンジロスに通しているのは、\( -d_i \) を限りなく小さく( \( - \inf \) など)することでlossを下げる現象を防ぐためです。 metric learningで学習した特徴量を抽出する際は1サンプルずつ関数 \( f \) に通して、lossの手前の出力を使えば良いです。特徴量の抽出方法は他の手法も同様です。

triplet network

2014年に提案された手法です。今回はこの論文で紹介されているtriplet lossを紹介しますが、triplet network自体はこの論文で提案されています。最大の特徴は3サンプルを一組で入力する点です。

f:id:copypaste_ds:20190226205158p:plain

triplet networkもsiamese networkと同様に類似サンプル同士は近く、非類似サンプルが遠くなるようにlossを設計しますが、サンプルの準備方法が異なります。サンプルの組は以下の手順で作成します。

  1. 基準となるサンプル \( a_i \)(anchor)を選択する
  2. \( a_i \) と似ているサンプル \( p_i \) 、似ていないサンプル \( n_i \) を一つずつ選択する

サンプルの組を作った後は、各サンプルを同じ関数 \( f \) に通してlossを計算すれば良いです。また、contrastive lossと同様に距離 \( d \)は好きに設定できます

サンプルの選び方と直感的理解

siamese networkと同様にtriplet lossもサンプルの組の選び方が重要です。
サンプル選択の理解を深めるためにもう少し式を眺めてみます。

f:id:copypaste_ds:20190226210529p:plain

lossは低い程良いので青枠部分がゼロ以下になるのが理想です。ただし、この関係を満たすサンプルの組は学習に役立たない(既にlossがゼロ)ので、学習時には青枠部分がゼロより大きくなるサンプルの組(学習が難しい組)を選択した方が良いそうです。

ちなみに理想的な関係式を移行して図にすると以下のように描けます。

f:id:copypaste_ds:20190227191500p:plain

図のオレンジの線(非類似サンプルまでの距離)が青線(類似サンプルまでの距離)+緑線(マージン)より長くなるように学習させているわけです。

L2 softmax network

2017年に提案された手法です。(論文はこちら)contrastive lossやtriplet lossのように明示的に距離を操作することはしません。softmax関数の手前で2つの処理をするのが特徴です。(どうやら暗黙的にコサイン類似度で調節しているという話も聞きましたが知識不足でよくわかりません)

f:id:copypaste_ds:20190226211758p:plain

処理の内容は簡単で、softmax関数に通す前にL2ノルムで割って定数倍するだけです。L2ノルムで割る(=単位ベクトル化する)ことで、softmax関数で予測容易なサンプルの予測値を限りなく1に近づける現象を抑えているそうです。また、表現力を調整する定数 \( \alpha \) はハイパーパラメータで事前に決める必要があります。(正直あまり知りません)

MNISTで実験

MNISTデータを使って3つの実験をしてみました。基本的に同じ数字は似ている、違う数字は似ていない組としてmetric learningを行います。(実験1-3だけは別ですが)

実験条件

実験条件は以下のとおりです。

  • CNNの構造
    • Convolution(kernel_size=3)+ReLuを4つ重ねた後、全結合層256次元
    • Convolutionのチャネル数は 16 -> 32 -> 64 -> 128
  • 最適化手法
    • Adam
  • 特徴量抽出のタイミング
    • lossの手前
    • L2 softmaxの場合は単位ベクトル化する前
  • データ
    • train: 60,000枚
    • test: 10,000枚
  • チューニング
    • ほぼしていません。 

実験1-1: 表現力の確認

まずはmetric learningがそれっぽく動作してくれるのか確認しました。実験手順は以下のとおりです。

  1. 10クラスで学習し、可視化
  2. metric learningで得た特徴量を用いて分類モデルを学習し、精度評価

まずは10クラスで学習した後、試験データの1000サンプルをtSNEで可視化しました。

f:id:copypaste_ds:20190227202009p:plain

no metric learning(metric learning未使用)はデータが混ざり合っており、metric learningを使った場合は上手く分かれているように見えます。また、contrastive lossとtriplet lossはデータ間の距離を考慮しているようにも見えますが、L2 softmaxは全体的にきっちりわける傾向が強いように見えます。(曖昧な表現をしているのはtSNEも多様体学習しており、2次元プロットの距離が高次元空間の距離を反映しているとは限らないためです)
次にクラスごとに特徴量ベクトルのセントロイドを算出して、セントロイド間の距離をヒートマップで可視化してみました。 距離は0~1にスケールしてあります。

f:id:copypaste_ds:20190228203728p:plain

文字が小さくて恐縮ですが、no metric learning, triplet loss, contrastive lossに関してはtSNEの可視化と同様の傾向が見て取れます。一方でL2 softmaxに関しては、tSNEの可視化では近く見えた3と5も実は遠い距離にあることがわかります。 (L2 softmaxを使用した場合にはコサイン類似度を使用したほうが良さそうですが、今回は比較のためにユークリッド距離を使用しています。そもそも直接比較はできない数値なのであくまで参考程度に)

次にmetric learningで得た特徴量を用いて分類モデルを学習させました。評価指標はaccuracyです。

f:id:copypaste_ds:20190226220958p:plain

no ML(metric learning未使用)よりもmetric learningのほうが高いaccuracyを示しています。中でもL2 softmax lossが特に優れています。これはL2 softmax networkが分類モデルを学習させている(最終的にsoftmaxでlossを計算している)ため、分類モデルにとって都合の良い特徴量を作成しているのだと思います。後段で分類モデルを使いたいならL2 softmaxが良さそうです。

実験1-2: 未知クラスの表現力を確認

次に未知クラスの特徴量をそれっぽく再現できるか実験してみました。実験手順は以下の通りです。

  1. 3, 4を除いた8クラスで学習し、可視化
  2. metric learningで得た特徴量を用いて分類モデルを学習し、精度評価

まずは8クラス(数字の3, 4以外)で学習した後、3, 4も合わせてtSNEで可視化しました。 実装はtriplet lossとl2 softmax lossで行いました。 3, 4以外は比較的分かれており、4は9の近く、3は丸みを帯びた文字の近くに位置していそうです。

f:id:copypaste_ds:20190227202049p:plain

生成した特徴量を用いてKNNを学習させました。KNNには3, 4も訓練データとして与えています。正規化済みの混同行列は以下のようになりました

f:id:copypaste_ds:20190227202058p:plain

metric learningモデルにとっての未知クラスの分類精度(オレンジ部分)はtriplet lossのほうが良いことがわかります。未知クラスを扱うときはtriplet lossが有効ということでしょうか。既知クラスの精度でL2 softmax lossが強いことは実験1-1の結果と一致します。triplet lossの精度を見るとクラス3が0.93、クラス4が0.85とそこそこ高いので未知クラスでもそれっぽい特徴量を作成できたことがわかります。

実験1-3: 奇数/偶数を学習

最後に奇数(1, 5, 7, 9)と偶数(0, 2, 6, 8)の2クラスで類似/非類似の組を作って実験してみました。 奇数/偶数で数字の形は必ずしも似ていないので少し無茶ぶりをしたつもりです。実験手順は以下の通りです。 実装にはtriplet lossを使用しました。

  • 3, 4を除いた上で奇数/偶数の特徴量を学習し、可視化
  • 3, 4も合わせて可視化し、位置を確認

まずは3, 4を抜いて学習させて可視化してみました。 左図はtSNEの可視化、右図が各クラスのセントロイド間のユークリッド距離を0~1に正規化したものです。

f:id:copypaste_ds:20190227202242p:plain

tSNEの可視化結果を見ると奇数/偶数で分離できそうに見えます。 右図の距離行列を見ても、奇数は奇数同士近く、偶数は偶数同士近く、奇数と偶数は遠く配置されていることがわかります。 無茶ぶりのつもりでしたがdeep learningにとっては簡単なタスクだったのでしょうか。
次に未知クラス(3と4)も合わせて可視化してみました。

f:id:copypaste_ds:20190227202257p:plain

4と9、 3と5が混ざり合っているように見えます。 今回の学習方法だとdeep learningは偶数/奇数の概念までは学習できないので4は奇数側(9の近く)に寄ってしまいました。 未知クラスも視野に入れるなら、metric learningで考慮したい意味(距離)は慎重に考える必要がありそうです。

天気データで実験

日本の天気データを使ってMNISTと似たような実験をしてみました。テーブルデータでもそれっぽく学習できるのか確かめることが目的です。

データの準備

気象庁のページから過去11年分の12都道府県の天気データをダウンロードしました。データは1時間おきの計測データで、クローリング失敗やそもそもの欠損などはpandasのdropnaで雑に処理をしました。必要なカラムだけ抽出して整形したデータは以下の通りです。今回は同じ都道府県ならば似ている、異なる都道府県なら似ていないとしてmetric learningしてみます。

f:id:copypaste_ds:20190227194517p:plain

入力データの次元があまりに少ないと面白くないので、1日1サンプルとして最大で12都道府県×11年×365日分のサンプルを用意しました。(実際には1/3程度が欠損で消えましたが...)都道府県ごとのサンプル数は沖縄が最大で3682サンプル、神奈川が最小で1425サンプルで若干隔たりがあります。

特徴量としては月(ダミー変数)と以下の項目を用意しました。ダミー変数を含めると52次元のデータとなります。画像データと比べると次元はかなり少ない印象です。

  • 朝/昼/夜ごとに以下の統計量を算出
    • 気温の最小・平均・最大・標準偏差
    • 降水量の平均・最大・合計
    • 降雪量の平均・最大・合計
    • 風速の最小・平均・最大・標準偏差

データの準備はとても雑ですが、ひとまずこれで準備完了です。

実験条件

実験条件は以下のとおりです。

  • MLPの構造
    • 全結合層(256次元)+ReLUを3つ
  • 最適化手法
    • Adam
  • 特徴量抽出のタイミング
    • Lossの手前
    • L2 softmaxの場合は単位ベクトル化する前
  • データセット
    • train: 9年分(2008年 ~ 2017年)
    • test: 2年分(2017年 ~ 2019年)
  • チューニング
    • ほぼできていません

実験2-1: 表現力の確認(その1)

まずは簡単そうな問題設定で動作確認をしてみました。実験手順は以下のとおりです。

  1. 4クラス(札幌、東京、大阪、沖縄)で学習し、可視化
  2. metric learningで得た特徴量を用いて分類モデルを学習し、精度評価

下図がtSNEの可視化結果です。metric learning未使用の場合は季節や月でクラスタができているように見えます。

f:id:copypaste_ds:20190227200727p:plain

札幌と沖縄の特徴量表現は比較的簡単ですが、東京と大阪の違いを学習させることは少し難しいようです。 triplet loss, L2 softmax lossは東京と大阪もある程度分離できていますが、contrastive lossは混ざり合っているように見えます。 ひとまずテーブルデータでもそれっぽく動作することが確認できました。

次にmetric learningで得た特徴量を用いて分類モデルを学習させました。評価指標はaccuracyです。

f:id:copypaste_ds:20190228224334p:plain

MNISTのときと同様に、metric learningを使ったほうが精度が高い結果となりました。 今回もL2 softmaxが最良の結果を示しています。

実験2-2: 表現力の確認(その2)

クラス数を増やして実験2-1と同様の実験をしてみました。クラス数が増えるので難易度も上がります。実験手順は以下のとおりです。

  1. 9クラス(札幌、青森、山形、東京、富山、奈良、山口、福岡、沖縄)で学習し、可視化

metric learning未使用の場合は実験2-1と同様に季節でクラスタが分かれているように見えます。 metric learningを使うことでクラスタが一つになるようですが、タスクが難しくなったせいか全体的にやや混ざり合っているように見えます。また、contrastive lossはミミズ型になりやすく少しチューニングに手間がかかりました。

f:id:copypaste_ds:20190227200911p:plain

下図は各クラスのセントロイド間のユークリッド距離を0~1に正規化したものです。 地理的な関係が反映されているかどうかはさておき、contrasitve lossとtriplet lossは距離を学習している気配があります。L2 softmaxは相変わらずはっきりと分ける傾向にあり、沖縄を除くとどの都道府県も似たような距離関係にあります。

f:id:copypaste_ds:20190228203953p:plain

実験2-3: 未知クラスの表現力を確認

最後に未知クラスの特徴量をそれっぽく再現できるか確認してみました。triplet lossとL2 softmax lossで実装しています。実験手順は以下の通りです。

  1. 9クラス(札幌、青森、山形、東京、富山、奈良、山口、福岡、沖縄)で学習し、未知クラス(秋田、神奈川、大分)も含めて可視化
  2. metric learningで得た特徴量を用いて分類モデルを学習し、精度評価

9クラスで再度学習させてtSNEで可視化してみました。左図は既知クラスのみ、右図は未知クラスも含めて可視化しています。 既知クラスのほうは辛うじてクラスごとのクラスタを確認できますが、未知クラスのほうは完全に混ざり合っているように見えます。 秋田は青森と、神奈川は東京と、大分は福岡と混ざり合ってしまったようです。チューニングすればある程度改善できると思いますが、タスクとして少し難しかったのかもしれません。

f:id:copypaste_ds:20190228204030p:plain

実験1-2と同様にmetric learningで学習した特徴量を用いて分類問題を解いてみました。正規化済みの混同行列は下図の通りです。

f:id:copypaste_ds:20190227201503p:plain

未知クラスの精度が著しく低いことがわかります。やはり既知クラスと混ざり合ってしまい上手く特徴量を表現できなかったようです。 一応2種類のlossを比較しておくと、未知クラスにはtriplet lossが強く、既知クラスにはL2 softmax loss が強いという実験1-2と同様の結果が得られました。

まとめ

  • metric learningは計量を学習する手法
  • 意味的な距離を考慮した特徴量を作成できる
  • 画像だけでなくテーブルデータにもある程度機能する
    • 実データで使えるかどうかはよくわかりません
  • 意味的な距離を考慮したいならtriplet loss
  • 未知クラスを主に扱うならtriplet loss
  • 分類タスクの特徴量生成に使うならL2 softmax loss
  • 素人のブログなのであまり信じすぎないでください

おわりに

metric learningの概要からToyデータによる実験結果まで簡単に紹介しました。冒頭でも述べた通り、私自身metric learningを学び始めて間もないですので、記事中に間違いがある可能性は非常に高いです。間違いを見つけた方はご指摘ただければと思います。感想としてはlossの設計で特徴量空間をある程度コントロールできるのは面白いと感じました。本記事で紹介していないlossやautoencoderとの組み合わせなど、データを変えながら遊んでみたいです。そういえばkaggleのタンパク質コンペ(?)の1st place solutionがmetric learningを使った解法だったそうです。今後はコンペでも積極的に使用されるかもしれませんね。

参考

[1] deep metric learningによるcross-domain画像検索 - ZOZO Technologies TECH BLOG
[2] Deep Metric Learning Using Triplet Networkの論文を流し読む – Urusu Lambda Web
[3] http://researchers.lille.inria.fr/abellet/talks/metric_learning_tutorial_CIL.pdf

kaggle: Mercari Price Suggestion Challenge まとめ

f:id:copypaste_ds:20190205143710p:plain

はじめに

過去コンペまとめ記事の三作目です。タイトルにもあるように今回は2017年11月にkaggleで開催されたMercari Price Suggestion Challengeをまとめたいと思います。これまでにToxicコンペとPort Segroコンペのまとめ記事も書いてますのでよければそちらもご覧下さい。以下にリンクをまとめておきます。

コンペ略称 リンク
Toxicコンペ kaggle: Toxic Comment Classification Challenge まとめ - copypasteの日記
Porto Seguroコンペ kaggle: Porto Seguro's Safe Driver Prediction まとめ - copypasteの日記

コンペ概要

メルカリコンペは株式会社メルカリが主催した、商品の販売価格予測コンペです。テーブルデータですが文章データも含まれているためNLPの知識が必要になります。 また、kernel only コンペなので実験環境と実行時間に制限があることも特徴です。
メルカリは誰でも簡単に売買ができるフリマアプリで、出品時に売り手が商品の価格設定をする必要があります。その際、価格を相場以上にすると売れませんし、相場以下にすると損をしてしまいます。出品する前にきちんと相場を調べておけば解決できますが、手間がかかりますし、そもそも調べ方がわからない人も少なくありません。そのような背景から、出品時に適切な販売価格を提示してくれるシステムがあると便利そうです。そのようなシステム導入を視野に入れて、メルカリコンペでは出品時に商品の適切な販売価格を予測することを目的とします。

賞金・期間・参加者数は以下の通りで、 同じNLPコンペであるToxicコンペと比較すると参加者は少ない印象です。ルールが特殊(kernel only & 2stage制 )であることは理由の一つかもしれません。

賞金 期間 参加チーム数 参加者数
$60,000 2017/11/22 ~ 2018/02/22 2,384 2,782

特別ルール

メルカリコンペでは通常のコンペには無い2つの特別ルールが設けられていました。それぞれ kernel only, 2stage と呼ばれるものです。

kernel only

kernel onlyコンペでは予測結果のファイルだけでなく、前処理〜モデリング〜ファイル出力までのスクリプトをkernelごと提出する必要があります。 kernelはマシンスペックや実行時間が決まっているため、通常コンペのように潤沢な計算資源と計算時間を活用することができません。当時のkernelの環境は 4cores / 16GB RAM / 1GB disk / GPUなし計算時間を60分未満にする必要がありました。

2stage

2stageの特徴はprivate LB用の評価データが配布されないことです。通常は配布されたテストデータの一部をpublic LB用の評価データ、残りをprivate LB用の評価データとするのが基本です。一方の2stageコンペでは、配布されたテストデータの全量がpublic LB用の評価データ(stage 1)、競技者には非公開のテストデータがprivate LB用の評価データ(stage 2)となります。

特別ルールの影響

kernel only × 2stage のコンペでは計算時間の観点で注意が必要です。それはprivate LB用の評価データでスコア計算をする際もkernelの時間制約を満たす必要があるためです。メルカリコンペでは private LB用の評価データサイズがpublic LB用の評価データサイズの約5倍と大きかったので、stage2で計算時間制約を守れず(あるいはメモリ制約を守れず)スコアがつかなかった競技者が続出しました。(769/2384チームがスコアなし)事前に対策をすれば防ぐことは可能ですが、private LB用の評価データは非公開(サイズのみ公開される)であるため計算時間の見積もりには注意が必要です。

データの種類とタスク

テーブルデータ(数値データ+カテゴリデータ+文章データ)を用いた回帰問題です。商品ごとに販売価格を予測するモデルを構築します。テーブルデータですが文章データも含まれるためNLPの知識が必要になります。
目的変数の分布は下図(左)の通りで歪んだ分布となっています。下図(右)はlogをとった場合の分布ですが、①分布が正規分布に近づく②評価指標がlogをとった上でのRMSEである(後で説明します)ことからlogをとった上で予測することが定石だったようです。画像は後ほど紹介しますがこのEDA kernelから拝借しました。

f:id:copypaste_ds:20190206223611p:plain

データサイズは以下のとおりです。test_stg.tsv(コンペ開催中は非公開のstage2用の評価データ)のサイズがtest.tsvの約5倍であることに注意する必要があります。2stageコンペでは必ず確認しましょう。データサイズは比較的小さい印象です。

ファイル名 データサイズ レコード数 カラム数
train.tsv  322M  1,482,535  8
test.tsv  147M  693,359  7
test_stg2.tsv  737M  3,460,725  7

ちなみにtrain.tsvのheadはこんな感じです。商品ごとに商品名、ブランド名、価格などのデータが与えられています。 f:id:copypaste_ds:20190205213822p:plain

各カラムの意味は以下のとおりです。正確な説明はデータ説明ページを御覧ください。

カラム 意味
train_id 商品ID
name 商品名
item_condition_id 商品の状態
category_name 3分類のカテゴリ
brand_name ブランド名
price 価格(目的変数)
shipping 送料が売手負担か否か
item_description 商品説明

評価方法

評価方法はRMSLE (Root Mean Squared Logarithmic Error)です。つまりlog1pをとった上でRMSEで評価することになります。評価指標がRMSEなのでLightGBMやNNを使えば評価指標を直接最適化することができます。
RMSLEの直感的説明は後で紹介するこのページで紹介されています。以下の図は¥3,000の商品に対してRMSLEを変化させたときの推定値の誤差範囲を表しています。 f:id:copypaste_ds:20190212233639p:plain

仮にRMLSE=1.0の場合は推定値の誤差範囲は¥1,103 ~ ¥8,156となります。図から分かる通り、logをとるため誤差範囲の上振れ幅と下振れ幅は等しくなりません。 ちなみに一位のチームがコンペ終了後に公開したkernelはRMLSE=0.3875で、誤差範囲は¥2,051 ~ ¥4,387でした。これは実際のプロダクト上でも稼働可能なスコアだそうです。

提出方法

提出のフォーマットは以下の通りで、IDごとに予測価格を出力します。

test_id price
0 1.5
1 50
2 500
3 100

勉強になる Kernel と Discussion

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

Mercari Interactive EDA + Topic Modelling | Kaggle

EDANLPチュートリアルがまとめられています。以降の解法を読む上で一度データを確認しておきたい方は、EDA部分だけでも見ておくことをおすすめします。
NLPチュートリアルは本コンペの目的と直接の関係はありませんが、NLPで有名な手法(よく使われる手法??)が順に紹介されています。説明されている項目は以下のとおりです。

  • 正規表現を用いた文章のtokenize
  • WordCloudを用いた頻出単語の可視化
  • tf-idfによる特徴量生成
  • SVD + tSNEによる次元圧縮と可視化
  • K-means, LDAによるクラスタリングと可視化
  • トピック毎に単語の分布を確認

文章データの分析では「特徴量抽出 -> クラスタリング -> 次元圧縮 -> 可視化 -> 結果解釈 」の順に作業することはままあると思うので初学者におすすめです。

Ridge Script | Kaggle

基本的な特徴量抽出をした上でRidge 回帰でモデリングしています。処理手順を箇条書きにすると以下のとおりです。

  • 各カラムの欠損補完と型変換
  • name, category_nameからCountVectorizerで出現回数の特徴量を作成
  • item_descriptionからTfidfVectorizerでtf-idfの特徴量を作成
  • brand_nameをLabelBinarizerでダミー変数化
  • item_condition_id, shippingをダミー変数化
  • 各特徴量をconcatしてRidge回帰モデルを学習

これらのアイディアは他のkernelでも頻出するので、理解を深めたい方は一度kernelをForkして手を動かしてみると良いかもしれません。

ELI5 for Mercari | Kaggle

Ridge回帰モデルの変数重要度をELI5で可視化しています。下図を見ればわかるように単に回帰係数を眺めるよりも重要変数の把握が捗ります。 f:id:copypaste_ds:20190207115920p:plain

変数重要度の算出アルゴリズムとしてpermutation importance, LIME, SHAPなどが知られていますが、雰囲気だけでも知りたい方はkaggle learnのMachine Learning Explainabilityがおすすめです。

A simple nn solution with Keras (~0.48611 P 12a776 | Kaggle

entity embeddingを応用したNNのベースラインモデルです。 各変数をembedding層でベクトル表現 -> 系列データにGRUを適用 -> 全変数をconcateしてDense + dropoutの構造でNNを構築しています。kerasを使ったtokenizeから学習までの必要最低限のコードなので初学者におすすめです。

Wordbatch FTRL+FM+LGB (LBL 0.42506) | Kaggle

FTRL, FM_FTRL, LightGBMのensembleをしています。FTRL, FM_FTRLの実装はToxicコンペでも登場したwordbatchライブラリを使用しています。予測時には加重平均をとっており、重みはFTRL : FTRL_FM : LightGBM = 0.18 : 0.55 : 0.27としています。重みの決め方についての言及はありませんが、FTRL_FM > LightGBMの関係をみるにFTRL_FMも強力な手法なのでしょうか。(知識不足でよくわかりません)

CNN GloVE single model-Private LB 0.41117 (~35th) | Kaggle

kernelコンペの性質上シンプルな解法が多い中、比較的複雑なモデルがあったので紹介します。シングルモデルですがシルバーメダル相当の強力なNNモデルで、構造は以下の通りです。

f:id:copypaste_ds:20190208103056p:plain

正直なところNN初学者の私にはこの図以上のことは説明できません。図のprice statistics部分では category_name, brand_name, shippingをgroupbyのkeyとしてmedian, mean, stdの統計量を算出した後、それらを四則演算することで特徴量を作成しています。

Mercari Golf: 0.3875 CV in 75 LOC, 1900 s | Kaggle

本コンペの1位チームがコンペ終了後に公開したkernelです。1st place solutionの核となる部分だけまとめた解法で、解法自体はシンプルですが非常に強力なkernelです。後でも紹介しますがtkmさんの解説動画を見ると理解が捗ると思います。モデルは隠れ層が3層のMLPです。

上位解法概要

上位解法はNN解法が目立ちます。LBを見ると1位のスコアが飛び抜けており、discussionを見る限り1位・2位の方が途中でチームマージしてトップを走り続けたようです。1位解法は非常にシンプルなNNで構成されており、多様性の出し方やモデリング方法など必見です。今回も1位〜3位解法をまとめます。 f:id:copypaste_ds:20190208111001p:plain

1st place solution: 1st place solution | Kaggle

最終的な解法は12個のMLPのアンサンブルです。いろいろ試行錯誤した結果、複雑なモデルからシンプルなモデルに収束したようです。チームマージ前とチームマージ後の解法が共有されていたため順にまとめようと思います。(マージ後の最終モデルが見所です)ちなみにスコアの推移は以下の通りで、マージ直後のモデルでも1位相当のスコアを記録していることが分かります。 f:id:copypaste_ds:20190208122257p:plain

Pawelさんの解法(チームマージ前、当時暫定2位)

複雑なモデルを作成していたそうですが、詳細な説明がないため雰囲気しか分かりません。ポイントは以下の3つだそうです。

  1. モデルは複数のRidge回帰のアンサンブルです。各Ridge回帰で使用する説明変数を変える(使用するカテゴリ変数の組合せを変える)ことで多様性を出したそうです。
  2. 残差MLPを学習させたそうです。具体的には、(1)で作成したRidge回帰の予測値と目的変数の残差を新たな目的変数として学習したMLPを作成したそうです。
  3. 残差LightGBMを上記の残差MLPと同様に作成したそうです。

個人的に残差モデルを作ってboostingさせるアイディアは初見だったので勉強になりました。

Kostantinさんの解法(チームマージ前、当時暫定1位)

モデルはMLPとCNNのアンサンブルです。こちらも詳細な説明がないため雰囲気だけまとめます。

  1. 3つのMLPを学習させたようです。特徴量は公開kernelと変わりないですが、Ridge回帰とeli5で算出した変数重要度を用いていくつかの変数に手直しを加えたとのこと。(そこが知りたい)
  2. conv1dを使ったCNN学習させたようです。スコアはそれほど高くないが、アンサンブルにとても貢献したそうです。

チームマージ時点

kernel onlyの制約上、上記2つの解法を愚直にアンサンブルすることはできず、双方のアイディアを取り入れた一つのモデルを構築するのに苦戦したそうです。(ここで2週間ほど費やしたらしい) 結局、3種のデータセットと4つのモデルを使ったアンサンブルに落ち着いたとのこと。多様性を出すために以下の項目を組合せたようですが、どのように組合せたかはわかりません。

  • different tokenization, with/without stemming
  • countvectorizer / tfidfvectorizer

最終的なモデル

モデルは適切にチューニングされた12個のMLPアンサンブルです。実験を通じて同じデータで異なるモデルを学習させるよりも、異なるデータを同じモデルに学習させたほうが多様性を表現できることに気づき、モデルはMLPMLPの構造は一種類ではない)と決めたそうです。 計算時間制約を考えてもMLPは都合が良かったとのこと。 解法のポイントを7つに分けてまとめます。(正直知らない工夫ばかりでテンション上がりました)

1. シンプルなMLP

1位解法最大の特徴です。3種類のデータセットに対して4つずつMLPモデルを構築しています。各データセットMLPの構造は基本的に同じですが、計算時間を考慮して入力層のユニット数は少し調節されていました。(192が2つと128がひとつ) 確かに全てMLPですが、データセットを変えたり、後で説明する2種類のMLPを用意したり、学習/ 推論の直前で特徴量を2値化したりと多様性を与える工夫がちりばめられています。
これらのMLPのアイディアを凝縮したkernelが公開されているので是非みていただきたいです。最終モデルに使用したMLPと同じもので、モデルでシンプルさがよく分かると思います。(tkmさんの解説動画もあります)

www.kaggle.com

最終モデルのソースコードgithubに共有されているので詳細を知りたい方はこちらをご覧ください。

github.com

2. 2種類のMLP

構築したMLPは2種類に大別できます。一つは損失をHuber lossとした回帰モデルで、もう一つが分類モデルを経由した回帰モデルです。各データセットに対して2つずつ学習させていました。評価指標がRMSEであるにも関わらずHuber lossで最適化する点や、回帰問題をあえて分類モデルで解くあたりが独創的だと感じました。ちなみにHuber lossモデルは隠れ層が3つ、分類モデルは隠れ層が2つの構造です。分類モデルを経由した回帰モデル作成の手順を簡単にまとめておきます。

  • 目的変数の値域を64のビンに区切り、各ビンの中点をそのビンの代表値に設定する。
  • 目的変数と各ビンの代表値とのユークリッド距離を算出し、softmaxに通すことで64個の確率的クラスラベルを用意する。(softmaxのexp(ax)のaはハイパーパラメータ)
  • 多クラス分類問題としてMLP(出力層をsoftmax)を学習
  • MLPの予測確率と各ビンの代表値の加重平均を最終的な予測値とする(回帰問題に帰着)

この分類モデルを経由した回帰モデルは、過学習することなく単体で良いスコアを記録したようです。それに加えてモデルの多様性の観点からもアンサンブルに貢献したそうです。

3. 3種のデータセット

どのデータセットもCountVectorizerとTfidfVectorizerを用いたスパース特徴量が基本になります。使用カラム(name, item_description, category_name)、Cleaningの有無、ベクトル表現方法(CountVectorizer, TfidfVectorizer)、ngramの範囲、binary表現有無 などの組合せから、様々なスパース行列を作成し、データセットごとに使用するスパース行列の組合せを変えています。

4. 特徴量の2値化

学習/推論の直前で特徴量を全て2値化(非ゼロか否か)する処理を加えています。この処理をするモデルはデータセットごとに2つでHuber lossモデルと分類モデルの1つずつでした。2値化するための閾値はいくつか試したそうですが、それによるスコア改善は見られなかったようです。

5. 学習速度のチューニング

学習速度のチューニングは非常に重要だったらしく、特にepochごとにbatch sizeを2倍にすることは学習を早めると同時にスコア向上につながったそうです。それに加えてepochごとに学習率を上げることで、徹底的に学習速度を速める工夫をしています。これらのパラメータは2epoch目で最良のvalid score、3epoch目で過学習するようにチューニングしたそうです。

6. L2正則化とPRELU

1層目にL2正則化を加えることは効果的だったようです。またtensorflowモデル(あとで説明しますが2種のpackageでモデルを作っている)ではRELUよりもPRELUが効果的だったそうです。

7. tensorflowとMXNetでの実装

最終的に選択した2つのsubmit(コンペ終了前に提出用のsubmitを2つ選択するルールがある)はtensorflowとMXNetそれぞれで実装したモデルにしたそうです。packageを複数使った理由は読み取れませんでしたが、tensorflowは各コアで別のモデルを学習でき、MXNetはスパースデータに対するCPUのMLP実装が効率的で実行時間が短かいなどの特徴があったのだとか。メモリ使用量やmultiprocessingに関しても説明されていますので興味のある方はdiscussionを読んでみてください。

2nd place solution: 2nd Place Solution | Kaggle

4つのモデル(Redge×1, NN×3)を構築しています。その他にもLightGBMを学習させたようですが上手く機能しなかったそうです。同じデータで異なるモデルを学習させる方法はうまくいかなかったので、データを変えて異なるNNモデルを複数用意したとのこと。RidgeとNNについてそれぞれ簡単にまとめます。

Ridge回帰

特徴量は1-ngramとアレンジを加えたbigramsです。1-ngramsは商品名と商品説明に対してそれぞれ適用したそうです。アレンジを加えたbigramsの処理手順は以下のとおりです。

  1. nameとitem_descriptionの先頭5単語をconcat
  2. np.uniqueで重複を除外
  3. とりうる全ての組合せで2-ngrams

このようなbigramsのとり方は初見だったので参考になりました。

NN

特徴量を変えたNNを3つ構築しています。特徴量のバリエーションは以下のとおりです。また、1位解法と同様に、epochごとにbatch sizeを2倍にして学習速度を速めたそうです。

  • name, descriptionにCountVectorizerしたものを特徴量にしたNN
  • name, descriptionに対してanalyzer='char'でCountVectorizerしたものを特徴量にしたNN
  • fastTextによるembeddingを層(name, descriptionで重みを共有)をもつNN

2nd place solution kernelが公開されているのでモデルの詳細を知りたい方はこちらをご覧ください。NN構造は公開kernelと比較的似ているため省略します。

3rd place solution: 3rd solution. 3300s to 0.3905 at public | Kaggle

モデルはNNとFMのensembleです。最終出力は2モデルの加重平均(NN×0.6 + FM×0.4)としたそうです。各モデルで使用した特徴量とNNに関してはモデル構造を簡単にまとめます。NNの構造構築に特に注力したそうです。

FM

使用した特徴量は以下のとおりです。

  • name, countvector
  • brand onehot
  • desctiption, tfidfvector
  • category1-3 onehot
  • condition
  • shipping

NN

使用した特徴量は以下のとおりです。

  • name + brand, maxlength=8, 10
  • description, maxlength= 64, 72
  • category3, onehot
  • condition
  • shipping
  • lenght of description
  • mean and std price
  • count of the category3

NNは図のような構造をしています。(図は私が作成したので間違いがある可能性があります。input1~4の項目は自信が持てなかったので記入しませんでした。)モデルの詳細を知りたい方は 3rd place solution kernelをご覧ください。

f:id:copypaste_ds:20190213112158p:plain

またNNの学習パラメータは1epochずつ以下のように設定したようです。7epoch目でwomen, beautyカテゴリのデータに絞っているのは、データの大多数が女性向け商品であるため女性向け商品を重要視して学習させるためだそうです。

エポック バッチサイズ データ数 学習率 最適化手法
1 907 600k 0.0055 adam
2 907 600k 0.0055 adam
3 1007 600k 0.0055 adam
4 1007 all 0.0055 adam
5 1424 400k 0.0055 adam
6 1424 800k 0.008 adagrad
7 900 women, beauty 0.008 adagrad

その他の上位解法

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

順位 リンク
4th GitHub - ChenglongChen/tensorflow-XNN: 4th Place Solution for Mercari Price Suggestion Challenge on Kaggle (https://www.kaggle.com/c/mercari-price-suggestion-challenge)
6th 6th place solution!! | Kaggle
9th 9th place solution 0.3975 | Kaggle
13th 13th Solution | Kaggle
14th 14th solution,CNN+finetune+FM | Kaggle
16th 16th place solution | Kaggle
18th 18th Place with no Neural Nets - LGB and FM - 0.40604 | Kaggle
22nd 22nd Around the world solution | Kaggle
22nd 22nd place kernel now public | Kaggle

日本語記事

Kaggleは凄かった! 更に簡単な出品を目指して商品の値段推定精度を改善中 - Mercari Engineering Blog

メルカリコンペの閉会式に関するまとめ記事です。閉会式では上位入賞者の解法発表やパネルディスカッション形式のトークなどが企画されたようで、その時の動画が共有されています。パネルディスカッションは日本語動画ですので英語が苦手な方でも見ることができます。

Kaggle メルカリ価格予想チャレンジの初心者チュートリアル

メルカリコンペを題材にした機械学習初心者向けのkaggleチュートリアルです。コンペ概要説明〜データの確認〜前処理〜ランダムフォレストによるモデリング まで丁寧に説明されています。

Kaggle メルカリコンペの優勝コードを眺める動画を作った - tkm2261's blog

tkmさんの優勝コード解説動画です。コードやモデルのどこがすごいのか丁寧に説明されているのでおすすめです。

【Kaggle】「Mercari Price Suggestion Challenge」に参加したあと、改めて色々調べてみたのでまとめる - St_Hakky’s blog

St_Hakkyさんもメルカリコンペのまとめ記事を書いていました。twitterのつぶやきや企業がコンペに出す意味についてもまとめられています。

おわりに

今回はメルカリコンペをまとめました。ベジネスでの活用シーンもイメージしやすく、馴染みのあるアプリが題材ということでいつも以上に楽しめた気がします。特に1st place solutionは衝撃的で、やや興奮状態でまとめ記事を書いた記憶があります。シンプルでありながら工夫が各所に散りばめられていて本当にかっこいいです。kaggleには他にもすごい解法や知見があると思うとわくわくしますね...!! 次回はAvitoかQuoraあたりをまとめるつもりですので良ければそちらも御覧ください。

kaggle: Porto Seguro's Safe Driver Prediction まとめ

f:id:copypaste_ds:20190201215419p:plain

はじめに

過去コンペまとめ記事の二作目です。タイトルにもあるように今回は2017年9月にkaggleで開催されたPorto Seguro's Safe Driver Predictionをまとめたいと思います。前回はToxic Comment Classification ChallengeというNLPコンペについてまとめたので興味のある方はこちらの記事をご覧ください。他者のアイディアや解法を読んでいるだけで面白いですし勉強になるので、思い返すとここ一週間は過去コンペばかり漁っていたような気がします。飽き性ではありますが今のところは週一本ペースでまとめ記事を書くことを目標にしているのでどうぞよろしくおねがいします。

コンペ概要

Porto Seguro(ブラジル最大の自動車保険および住宅保険会社の一つ)が開催する保険金請求予測コンペです。データは非常にきれいなテーブルデータで、ドメイン知識を含めて特別な知識は必要ありません。
自動車保険加入者が翌年に保険金請求するか否かを予測することは、保険料や保険金を設定する上で非常に重要です。このコンペではドライバーが来年自動車保険に保険金請求をするか否かを予測することを目的としています。
賞金・期間・参加者数などは以下の通りで、参加チームは5169チームと非常に多く人気のコンペであったことがわかります。(実は参加者が多いコンペを狙ってまとめ記事を書いています)

賞金 期間 参加チーム数 参加者数
$12,000 2017/09/30 ~ 2017/11/30 5,169 5,864

データの種類とタスク

テーブルデータを用いた2クラス分類問題です。連続変数・カテゴリ変数・バイナリ変数で構成されており目的変数も2値なので、よくあるデータ形式だと思います。データサイズが小さい、タスクは2クラス分類と一般的、データがきれいなどの理由から初学者にも取り組みやすいことが人気につながったのかもしれません。 正例データのサンプル数が少ない不均衡データで、目的変数の分布は下図のとおりです。画像は後ほど紹介しますが、ここから拝借しました。

f:id:copypaste_ds:20190202214844p:plain

不均衡データの扱いは少し厄介ですが、これも良くあるケースだと思います。
データサイズは以下の通りで、テーブル数は2つでサイズも小さめです。

ファイル名 データサイズ レコード数 カラム数
train.csv 100M 595,212 59
test.csv 164M 892,816 58

ちなみにtrain.csvのheadはこんな感じです。(カラム数が多いので適当に選びました)
連続変数とカテゴリ変数の両方があり、カラム名から変数の意味はわかりません。

f:id:copypaste_ds:20190204105818p:plain

カラムは大きく4つに別れており、それぞれ ind(18種)、reg(3種)、car(16種)、calc(20種)の計57種類です。各カラムの意味は公開されていないため、データの意味や傾向は集計やモデリングを通じて理解していく必要がります。(一応、型の情報としてbinはバイナリ変数、catはカテゴリ変数であることは周知されていました)

評価方法

評価指標は標準化ジニ係数(Normalized Gini Coefficient)です。 標準化ジニ係数は0〜1の値を取る評価指標で1に近いほど良いです。 この指標の直感的理解にはこちらのkernelが役立ちます。一応、コンペ概要ページにも簡単な説明があります。

提出方法

提出のフォーマットは以下のとおりです。idごとに保険金請求する確率を以下のように出力します。

id target
0 0.1
1 0.9
2 1.0

勉強になる Kernel と Discussion

上位解法ほどのスコアは出せませんが、kernelとdiscussionは良いアイディアと実装で溢れています。どのアプローチも勉強になったので簡単にまとめておきます。
最後に紹介しているtkmさんのyoutube動画は、これからkaggleを始めたい方にとてもおすすめです。

Data Preparation & Exploration | Kaggle

データの確認、特徴量抽出、特徴量選択、特徴量のスケーリングまでが簡単にまとめられています。以降の解法を読むにあたって簡単にデータを確認しておきたい方には見ておくことをおすすめします。特徴抽出ではカテゴリ変数をダミー化することに加えて、sklearnのPolynomialFeaturesで変数間の相互作用を表現しています。また特徴量選択ではsklearnのVarianceThresholdやSelectFromModelを用いた方法が紹介されており、とても参考になります。

Stratified KFold+XGBoost+EDA Tutorial(0.281) | Kaggle

xgboostによるモデリングが紹介されています。特徴量抽出やモデリング自体に特に工夫は見られませんが、モデリングする上で必要最小限のコードでテーブルデータコンペ初学者におすすめです。

Resampling strategies for imbalanced datasets | Kaggle

不均衡データの対処法であるunder samplingとover samplingについてまとめられています。pandasの基本メソッドで実装できるものから、不均衡データ処理用のライブラリであるimbalanced learnを用いた手法まで図を用いて直感的に説明されています。特にimbalaned learnを用いたunder sampling(Tomek linksとCluster Centroids)やover sampling(SMOTE)の説明はあまり目にしないため非常に参考になりました。kernelの最後にはおすすめの文献も紹介されており個人的にお気に入りのkernelです。

Python target encoding for categorical features | Kaggle

target encodingの実装と評価が紹介されています。target encodingの実装は単純に目的変数の平均値を求めるのではなく、smoothingやnoise機能が考慮されていて非常に勉強になりました。kernelの評価実験をみると今回はtarget encodingが有効であることがわかります。

Dimensionality reduction (PCA, tSNE) | Kaggle

PCAとtSNEを用いた次元圧縮と可視化が紹介されています。手法の説明はありませんが図が多く考察部分の説明が丁寧なので、初学者でも十分理解できると思います。前処理の有無による結果の違いも紹介されていて参考になりました。今回のデータセットは正例・負例の分離が難しいことを結果から確認できます。それからtoy dataに対するtSNEの挙動についてはこちらのdiscussionで紹介されています。

Tune and compare XGB, LightGBM, RF with Hyperopt | Kaggle

hyperoptを用いたrandam forest, xgboost, lightgbmのパラメータチューニングが紹介されています。最適化手法の説明は無く、探索するパラメータも少ない(実用するには探索項目が少ない)ためこれだけ真似しても使いこなせるかどうか怪しいところですが、サンプルスクリプトがまとめられている点で有用だと思います。(RF, xgboost, lightgbmでデータの渡し方が微妙に異なるので)最近では同じくハイパーパラメータチューニングに使用するoptunaというライブラリが公開されているので、興味がある方は本記事の後半で紹介する日本語の記事を御覧ください。

2-level Stacker | Kaggle

シルバーメダル相当のstackingモデルのスクリプトです。このkernelはコンペ終了後に公開されたそうでスコアも良い非常に強力なkernelです。カテゴリ変数をダミー化した後、lightgbm×3, LogisticRegression×2, ExtraTreesClassifier×2, MLPClassifier×1のアンサンブルをしており、その実装は Ensembleクラスにまとめられています。Ensembleクラスはbase_models, stacker_1, stacker2の3層(数え方が分かりませんがここでは3層ということで)のstackingが実装されており、stacker_1の学習はmode引数で2種類の方法を選択できます。stackingのアイディアを知っている方なら実装もすぐに読めると思うので気になる方は是非読んでみてください。個人的に最も勉強になったkernelです。

Entity Embedding Neural Net | Kaggle

kerasを使ったentity embeddingの実装です。カテゴリ変数ごとembedding layerでカテゴリをベクトル表現したものと量的変数にDenseを通したものを全てconcateして、後段のDenseとdropoutにつなげています。本コンペの2nd solutionと似た解法です。ちなみに、Rossmannコンペの3rd place solutionで紹介されており論文も公開されています。

Gini Coefficient - An Intuitive Explanation | Kaggle

今回コンペの評価指標である標準化ジニ係数ジニ係数を簡単な数値例を交えて紹介しています。

Big GP | Kaggle

Genetic Programmingで作成した特徴量を使用したkernelです。はじめてGPの特徴量を見たときは「これが黒魔術か...!!」と驚いた記憶があります。kernelには特徴量作成ロジックについての言及はありませんが、upuraさんが以前ブログに書いていたので実装に興味のある方はこちらを参考にすると良さそうです。

Kaggle Tutorial on this competition

tkmさんのkaggle 入門動画が共有されています。動画ではPorto Seguroコンペの概要、分析環境構築(GCP登録、Ubuntu設定、データ読込)、モデリング(ロジスティック回帰、Cross Validation、Grid Search、xgboost)、submit方法などがわかりやすくデモ形式で紹介されています。私はこの動画のおかげでkaggleに入門することができたのでとても感謝しています。
この他にも、BigQueryでkaggle入門動画がyoutubeに上がっています。こちらはPorto Seguroコンペと直接関係はないのですが、私がBigQueryを使うきっかけとなった動画で「BigQuery気になってるけど、まだ使ったことない」という方にとてもオススメです。
個人的にとてもお世話になった動画たちなので目立つリンクを貼っておきます。

www.youtube.com

www.youtube.com

上位解法概要

上位解法はLightGBMとNNのアンサンブル解法が目立ちます。テーブルコンペの定石であるLightGBMにモデルの多様性を意識したNNモデルを用意できるかが一つポイントだったのかもしれません。NNのモデリングに関しては1st ~ 3rdで全く解法が異なっているため読んでいて楽しかったです。今回も1st ~ 3rdまでの解法をまとめたいと思います。

f:id:copypaste_ds:20190203154110p:plain

1st place solution: 1st place with representation learning | Kaggle

使用したモデルはLightGBM×1, NN×5(これとは別に特徴量抽出用にNN×5)で各モデルのパラメータ設定は表のとおりです。全てのモデルで準備する特徴量は同じ(表のf0)で、予測モデルがNNの場合は前段にDAE(Denosing autoencoder)による特徴量抽出をしています。予測時には6つのモデルの平均値を求めたそうです。

f:id:copypaste_ds:20190203205513p:plain

DAEで表現学習をする前に、最低限の加工としてcalc系のカラムを全て除外し、カテゴリ変数をダミー化しています。(特徴量抽出が好きじゃないので特徴量抽出は基本的にNNに任せたらしい) DAEのノイズはswap noiseで一定の確率で別レコードの値で入れ替えることでノイズとしたそうです。(画像データのようにflip, rotateなどはできず、gaussian, uniform noiseも不適切と考えたため) またスケーリングはRankGaussを使用したそうです。RankGaussの説明はこのサイトが参考になります。DAEの学習にはtrainとtestの両方を使用しており、testが多かったことは好都合だったとのこと。ただしprivate LB scoreにはあまり効果がなかったようです。

2nd place solution: 三个臭皮匠's approach(s) | Kaggle

public5位からshake upにより2位入賞。localCVとpublicのスコアが一致していたため shake up は予想外だったとのこと。trust CVで、localCVの結果が改善された時だけsubmitしていたそうです。 コメント欄を含めると他のチームメンバーの解法も共有されていますが、ここではチームリーダーのLittle Boatさん解法をまとめます。LightGBMとNNのモデルを作成したようです。(どちらもkernelでスクリプトが吸収されているので詳細を知りたい方はそちらも是非)

LightGBM: 2nd Place Lightgbm Solution | Kaggle

以下の特徴量を追加して予測を行ったようです。

  • 各レコードの欠損数
  • カテゴリ変数をOne Hot encoding
  • ind系特徴量を全て連結しカテゴリ変数とする
  • カテゴリ変数のカウント情報

NN: 2nd place solution NN model | Kaggle

以下の特徴量を追加して予測を行ったようです。

  • 各レコードの欠損数
  • 変数の相互作用項(効果的だったもののみ)
  • ind、reg, car系特徴量を全て文字列で連結しカテゴリ変数とする
  • 特徴量を3グループに分割しxgboostで2グループを説明変数、1グループを目的変数として学習した後、予測値を特徴量として追加
  • カテゴリ変数のカウント情報追加
  • 2つの特徴量を用いて一方をグループのキーに、一方を集計対象とみなして基本統計量(mean, std, max, min, median)を算出(有効だった組合せのみ)

モデルの構造は、各カテゴリを6次元にembeddingしたものと量的変数をconcateした後に、Dense(512) -> PReLU -> BN -> Dropout(0.75) -> Dense(64) -> PReLU -> BN -> Dropout(0.5) -> Dense(1) するというもの。スケーリングには StandardScalerを使用していました。

3nd place solution: 3rd place solution | Kaggle

public1074位からshake upで3位入賞した解法です。shake upを狙っていたそうですが本人もさすがに驚いたようです。
モデルはlightgbm×1とNN×1のアンサンブルと非常にシンプルで、正直モデルの強みや特徴がどこにあるのかよくわかりませんでした。モデリングの特徴を箇条書きでまとめます。

  1. calc系のカラムを除外し、LightGBMのCV scoreを確認しながら他にも9つのカラムを除外する
  2. カテゴリ変数をOne Hot Encoding
  3. LightGBMのパイパーパラメータは表現力を抑える(lambda_l1=10, num_leaves=24など)
  4. NNは3層で Dense(4096) _> Dropout(0.5) -> Dense(1024) -> Dropout(0.5) -> Dense(256) で、ネットワーク構造はあまり重要ではなかったとのこと

その他の上位解法

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

順位 リンク
8th Approach(3rd in public and 8th in private) | Kaggle
8th Taylor-made NN for 0.285 PLB (part of solution of 8º) | Kaggle
9th 9th place solution | Kaggle
12th 12th place solution | Kaggle
17th 17th solution - seed selection, NN, ensembling.. | Kaggle
18th 18th Place Solution - Careful Ensembling + Resampling Diversity | Kaggle
20th genetic algorithm solution (20th place) - very long read | Kaggle
21th Keep It Simple (21st solution & *mea culpa* for missing gold) | Kaggle
23th 23rd place solution | Kaggle
25th Safe Driver Prediction top 1%, LightGBM (0.29132) | Kaggle
29th Solution 1178 Public / 29 Private | Kaggle
31st 31th/5k+ with 1 single model without stacking and blending possible, wow! | Kaggle
35th 35th place solution | Kaggle
37th 39th solution | Kaggle
39th 39th solution utilizing Pr(is_test|X) by adversarial validation | Kaggle

日本語記事

Kaggleで世界11位になったデータ解析手法〜Sansan高際睦起の模範コードに学ぶ - エンジニアHub|若手Webエンジニアのキャリアを考える!

株式会社Sansanのデータサイエンティストが、Port Seguroコンペを題材に前処理・特徴量抽出・予測モデルについて紹介しています。コードや考え方を初心者でわかるようにQ&A形式で説明されていてわかりやすいです。

世界一のデータサイエンティストを目指して 〜Kaggle参加レポート〜 - Kysmo’s Tech Blog

株式会社キスモの役員の方がPort Seguroコンペの参加記録を書いています。こちらもQ&A形式で説明されていて非常にわかりやすいです。

OptunaとLightGBMを使って、Kaggle過去コンペにsubmitする | Enigmo Life

Port Seguroコンペのデータを使ってOptunaでLightGBMのパラメータチューニングをしている記事です。

おわりに

今回はPort Seguroコンペについてまとめました。テーブルコンペでもNNが積極的に使用されていることは驚きでした。一方でNNのモデリングは1st ~ 3rdで全く異なることから察するに、NNの定石はまだできていないのかもしれません。他のコンペの解法も眺めながら傾向を掴んでいきたいです。次回はメルカリコンペかAvitoあたりをまとめるつもりですのでそちらも読んでいただけると励みになります。

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 してみるのも面白いかもしれません。
続編はマイペースに書いていこうと思いますが、もしかしたらめんどくさくなるかもしれません。。めんどくさくなってしまったらごめんなさい。

signate 公園コンペ で8位でした。

自己紹介

はじめまして。最近、copypasteとしてtwitter, signate, kaggle を始めたものです。
ブログ執筆にも前々から興味はあったのですが、書くネタが思いつかない&書くのが面倒 という理由で一歩踏み出せずにいました。(「アウトプットは大事!」という話は何度か耳にしましたがどうしても半信半疑..)
ただ、コンペに参加する中で様々なcompetitorさんやengineerさんのブログやtweet(その他にもyoutube, kaggle kernel, 書籍などなど)に助けられたので、少しでも還元できればという気持ちと、頭の整理、備忘録も兼ねてブログデビューを決めました。
今後もデータ分析周りのトピックで何かしら書いていけたらな〜と思っております。よろしくおねがいします。

コンペ概要

コンペのテーマは「国立公園の観光宿泊者数予測」でした。
予測対象は下記8つの国立公園で、公園ごとに1年間(2017-01-01 ~ 2017-12-31)の観光者数を日毎に予測します。
学習データの期間は 2015-01-01 ~ 2016-12-31 なので、2年間のログデータを使用して直近1年間を予測することになります。

実用性を重要視した問題設定で、前日以前のデータを元に翌日以降の観光者数を予測するモデルを作る必要がありました。(当たり前といえば当たり前ですが)
要は、kaggle で時折見られる評価データを用いた特徴抽出(train, testをconcatした上で統計量算出など)は禁止されていました。
私としては実問題に近いタスクのほうが取り組む意欲が湧くので、このような問題設定はありがたかったです。

目的変数は公園毎・日毎の観光者数で、評価指標はMAEでした。 公園毎に目的変数の推移や値域が異なっていたので、"公園毎の性質の違いをどうモデルに組み込むか"も一つ重要な点だったのかもしれません。
目的が明確、データの欠損もない、公園毎のサンプル数も均一、データサイズも小さい、ので初学者には取り組みやすいテーマ&データだった印象です。唯一やりにくいと感じたのはサンプル数が若干少ないことくらいでしょうか。(学習セットは公園毎に2年×365日=730サンプル程度)

ちなみに賞金は無く、上位三名には懸賞として国立公園招待券がプレゼントされます。
私は沖縄の公園を満喫するべく、1位目指して毎日公園のことを考え続けました。(初参加&初優勝って誰もが憧れる展開じゃないですか??)
まぁ結果的に大差で負けてしまったわけですが。。

データ概要

データは観光者数のログデータに加えて、以下の外部データが与えられました。

  • SNSデータ(株式会社ホットリンク)
  • ロケーション付SNSデータ(株式会社ナイトレイ)
  • メッシュ型流動人口データ(株式会社Agoop)
  • 公共交通検索ログデータ(ジョルダン株式会社)
  • 国別月別来訪者数集計データ(株式会社コロプラ
  • 気象データ(気象庁
  • 積雪気象観測データ(防災科学技術研究所(NIED))
  • 公共交通検索ログデータ(ジョルダン株式会社)

気象データ(温度、湿度、天気など)や SNSデータもあり、データのリストを見たとき「面白そうだな〜、いろいろ活用方法が考えられそうだな〜」なんて思った記憶があります。(結局一つも使いこなせず、使用しませんでしたが...)
公共交通検索ログデータに至ってはダウンロードすらしておらず、中身すら確認していなかったことにコンペ終了後に気が付きました。
twitter情報によるといくつかの公園では特徴量として有効だったそうで間抜けな自分を恨みます。分析を始める前にしっかりとデータを確認することが大事ですね。。

解法

私の解法を端的に述べると「前年の観光者数を特徴量にしたlightgbm」です。
唯一工夫した点は、とても強引なOver Samplingくらいでしょうか。
取り組んだ順に解法をまとめたいと思います。

public LB: 6000台 [1サブで優勝を狙うも撃沈]

記念すべき最初のサブミットで上位を狙おうとしていました。(1サブで優勝って誰もが憧れる展開じゃないですか??)
結果、しょーもないバグで失敗に終わりました。バグの内容も覚えていません。
そもそも1サブで優勝なんて私にはできるわけないのです。

public LB: 2100台 [ベースラインモデル完成]

バグを修正したところ2100台でした。
モデル構築の方針ですが、私は「前年のログデータから予測日と類似した日付を参照し、類似日の情報を使って予測するモデル」を構築しようと考えました。
具体的には、2017-01-22の予測時には2016-01-22の観光者数を参考にすると良いだろうと考えたわけです。(2016-01-22と2015-01-22は類似していると考える)

この方針でモデル構築するために、サンプルを以下のように準備しました。

  • 学習データ
    • 2015 -> 2016 (2015年の履歴を参考に、2016年の観光者数を予測)
    • サンプル数は365(日)×8(公園数)= 2920になる。
  • 評価データ
    • 2016 -> 2017 (2016年の履歴を参考に、2017年の観光者数を予測)

このサンプルの作り方だと2015年のデータが準備できず(2014年のデータがないため)、サンプル数が半減する不利益がありますが、今回は類似日の目的変数が特徴量として使用できる利益を重視しました。
次に類似日の定義ですが、今回は以下の項目が一致する日付を類似日とし、項目ごとにgroupbyした基本統計量(頻度、最小、最大、平均、パーセンタイル)由来の特徴量を作成しました。

  • 曜日
  • 週数(weekofyear)
  • 週末フラグ
  • 祝日フラグ
  • 休日フラグ
  • 最近傍の休日までの日数
  • 連休フラグ
  • 連休何日目か
  • 月×曜日
  • 月×週数
  • 月×週数
  • ....などなど

カレンダー情報の取得にはjpholidayを使いました。
予測時は目的変数に対数スケール変換(np.log1p)をしたほうが良かったです。
あとはLightGBMに突っ込んでおしまい。
LightGBM は MAE を直接最適化できるので便利ですね。

public LB: 1900台 [CV探索]

次に有効なCV探しをしました。
ここまでは特に何も考えずランダムに5-Foldしていたのですが、コンペではCVの切り方が大事と聞いていたのでいくつか試しました。(実務でも勿論大事なわけですが)
以下が試したものです。

  • Random Kfold
  • Stratified Kfold
    • groupに公園を指定
    • groupに月を指定
    • groupにweekofyearを指定
    • groupに公園×weekofyearを指定

結局、weekofyearをgroupにした Repeated Stratified Kfold にしました。5Fold×5ですね。
TimeSeriesSplitについては、1年通してまんべんなく学習させたい気持ちが強かったので試しませんでした。(groupに月を指定したStratified Kfoldがうまく行かなかったことも理由の一つ)
CVを変えただけでもスコアが伸びたので、データの渡し方の重要さを学びました。 (納得いくCVは結局作れなかったのですが...)

public LB: 1700~1800台 [特徴量探索]

過去の目的変数の集計値が有効なら、それを加工したデータの集計値も有効だろうと考えました。
具体的には移動平均や季節性の排除などをして、時系列データを加工しました。
加工した後は、これまでと同様に基本統計量由来の特徴量を作成します。

さらに、364,365,366日のラグ特徴量も追加しました。

public LB: 1600 ~ 1700台 [Over samplingに挑戦]

サンプル数が少ないことが気になったので、Over Samplingしました。
かなり強引ですが時系列を無視して、サンプルを水増ししました。
試した組み合わせは色々です。
2015.5年は2015年と2016年の中点をとって作成しました。

  • 2016 -> 2015
  • 2015 -> 2015.5
  • 2015.5 -> 2016
  • 2015.5 -> 2015
  • 2016 -> 2015.5
  • ....(2015.2や2015.7など作って試行錯誤)

結局、日光以外の7公園は 2015->2016, 2016->2015, 2015.5->2016で学習し、日光は2015->2016, 2015.5->2016で学習しました。
日光を別にモデリングした理由は、日光だけ2015年と2016年で目的変数の分布が異なっていたためです。
2015年から2016年にかけて増加傾向が見られたためか、逆向きのOver Samplingは逆効果でした。

public LB: 1500台 [各種パラメータ調整]

特徴量選択やLightGBMのハイパラチューニングなどしました。
ちょうどoptunaが公開された時期だったので少し試してみましたが、結局手動チューニングが一番よかったです。
kaggle kernelからLGBMのパラメータを拝借して微調整するだけで十分でした。
optunaの使い方が悪かっただけかも(ほとんど枝刈りしていて全然探索してもらえませんでした。)

終結

最終スコアは public LB:1583.5, pribete: 1827.9 でした。
1位の方は puplic: 1371.6 pribate:1585.2 なので public, pribate共に大敗です。

f:id:copypaste_ds:20181221234640p:plain

その他に試したこと

  • 公園毎にモデル構築
  • 天気を考慮した時系列加工(台風などを考慮したかった)
  • ノイズを加えたOver Sampling
  • stacking
  • 公園ごとに学習サンプルに重み付け

試してみたかったこと

  • 時系列データ用の特徴抽出ライブラリ
  • 外部データの使用
    • 3カラムのデータだけでも試したいことが沢山あったので外部データにあまり目が行きませんでした。

他の方の解法

実際に取り組んだコンペの別解法はとても勉強になります。
2つともサンプル作成方法から私と異なっていて驚きました。
kaggleのようにチームが組めたなら、このようなモデルとアンサンブルすると良いのかもしれませんね。
自分にはなかったアイディアが多くあるので次に活かそうと思います。

おわりに

はじめてコンペを完走できて嬉しい反面、悔しい気持ちも強いです。
自分の立ち位置が少しわかった気がするので、次はさらに上位を目指して頑張ろうと思います。
最後にブログデビューの後押しをしてくださったupuraさんに感謝申し上げます。
次回はkaggleについて何か書けたらな〜と思っております。
アウトプットの大事さを肌で感じられると良いな〜