はてな社内でKaggleハッカソンを行ないました(TakingDataリベンジマッチ編)

先週末、はてな社内でKaggleハッカソンを行ないました。丸一日、各自好きなKaggleのコンペに取り組んで、得られた知見を共有するという会です。

自分は以前TalkingDataというコンペに参加していたのですが、データサイズが結構大きく、一月くらいやってみたももの試行錯誤に四苦八苦してしまい、途中で離脱していました...。このハッカソンでは、そういったデータセットでも何とかできるようになろう!ということを目標にして参加しました。もちろん1日だけではさすがに時間が足りないので、ハッカソン前の10日くらいは定時後にちまちま作業をやっていました。

以下はハッカソン終了後に使った発表資料です。Kaggle上位の人にとっては当たり前のことしか書いてないかもしれませんが、社内でこういった知見をじわじわと貯めていくことが大事だと思っています。なお、ハッカソン終了後にAWSのでかいインスタンスを借りてやりなおしたところ、無事目標のTop 10%入り相当のスコアに到達しました!!

モチベーション

TalkingDataのデータセットについて

  • データサイズ
    • 学習用データサイズがかなり大きい(約1.8億件)
    • ちなみにテストセットも1800万件程度ある
  • 特徴量
    • カラム数自体は少ない、文字列ではなくハッシュ化された数値が与えられる
      • ip, app, device, os, channel, click_time
    • かなりスパース(特にip特徴量)。pd.get_dummiesとかやると即死
  • ラベル
    • is_attributed: アプリをダウンロードしたか(二値)
    • 正例が50万件以下、割合では0.02%程度でextremely unbalanced data
  • 評価指標はAUC

敗因分析

  • 主に2点
    • データの巨大さに負けて、特徴量エンジニアリングの工夫がなかなかできなかった
    • クロスバリデーションなしで学習が6時間くらいかかり、パラメーターチューニングも満足にできない
  • 以下の2つについて取り組んだ
    • Negative down sampling
    • Bayesian optimization

Negative down sampling

  • 正例と同じ分量になるだけ正例をサンプリングして学習データにする方法
  • 元々の学習データ全体と比べて1%以下のデータ量になり、試行錯誤がとてもやりやすい
    • 計算時間もメモリ量も
  • 負側のサンプリングは何回も別のセットを作ることができるので、その回数だけ学習器を作ってbagging(例: rank averaging)することで最終的な精度向上も目指すことも
  • TalkingData AdTracking Fraud Detection Challenge (1st place solution)

Bayesian optimization(モチベーション)

  • KaggleをやりだすようになってからLightGBMという決定木ベースのものを使うように
    • 使いやすさ(スケーリングなどが多少不要になる。欠損値もよしなに)や精度面でもよい
  • 一方で、これまで使うことが多かったSVMやロジステック回帰と比べて、ハイパーパラメータが多く、チューニングが大変になった
    • 学習率、葉の数、binの数、samplingのratioなどなど10くらいある
  • ハイパーパラメータで時間を食ってしまうため、試行錯誤の回数を増やせない
  • ベイズ最適化でチューニングの回数を減らせないか

ハイパーパラメータの例

それぞれのパラメータを4-5個の選択肢を用意して真面目にCVすると結構大変。

params = {
    'learning_rate': 0.01,
    'num_leaves': 255,  # 2^max_depth - 1
    'max_depth': 8,  # -1 means no limits
    'min_child_samples': 200,  # Minimum number of data need in a child(min_data_in_leaf)
    'max_bin': 255,  # Number of bucketed bin for feature values
    'subsample': 0.9,  # Subsample ratio of the training instance.
    'subsample_freq': 1,  # frequence of subsample, <=0 means no enable
    'colsample_bytree': 0.5,  # Subsample ratio of columns when constructing each tree.
    'min_child_weight': 0,  # Minimum sum of instance weight(hessian) needed in a child(leaf)
    'scale_pos_weight': 1,
    'reg_lambda': 0.0,  # L2 regularization term on weights
}

脱線: ハイパラチューニングでそこまで精度が劇的に上がることは少ないのでは?

  • それはその通り
  • しかし、チューニング結果を人が全て見れるわけではないケースも社内で増えつつある
    • 例: 異常検知などの再学習
  • 以下を同時に叶えたい
    • チューニングをミスって極端に悪くならないようにしたい
    • 多数のハイパラのグリッドサーチはメモリも計算時間もかかるからはしょりたい
  • Kaggleで多少精度が欲しいときに役に立つ…かも?

Baysian optimization(方法論)

  • ベイズ的最適化(Bayesian Optimization)の入門とその応用
  • 入力をハイパーパラメター、出力をスコア(例: AUCやloglossなど)とする関数回帰の問題と見なす
  • ガウス過程は関数回帰をやってくれる代表選手であり、関数空間の事後分布を計算できる
  • 次にどこハイパーパラメータを試行錯誤するか、事後分布の広がりが一番大きい点を優先的に探す
  • グリッドサーチやランダムサーチと比べて、データにどのような特徴があるを考えてハイパーパラメータの探索を行なうため、学習回数を減らせる
  • 補足: やってること、どこかで見たことありますよね
    • 多椀バンディットのUCBとかと考え方が似ている(し、実際に関係ある)

ハッカソンでの初期状態

  • LB(leaderboard)のprivateスコアが0.9691844
  • 特徴量は基本的なもの
    • ip, app, device, os, channel ,click_time
    • ['ip', 'device', 'os']でグルーピングしたときのappの異なり数など
      • この辺がめっちゃ効くので、工夫したい
  • 分類器はLightGBM。ハイパラチューニングはそんなに頑張ってない
    • が、メモリに乗る学習データがせいぜい8000万件、学習もCVなしで(確か)6時間かかる… 😇

初手

Negative Down Samplingをやってみる

  • 1.8億件の中から負例を正例と同数サンプリング
    • 学習データは約100万件程度まで落ちる
    • 特徴量は変えない
  • LightGBMを使って学習
    • めっちゃ早くなる(パラメータによるけど、1分くらいで終わる)
  • AUCが落ちる 😇 (0.95程度まで落下)

RankAveragingをやってみる

  • もしかしてbagging必須?!と思ってひとまずやってみる
  • rank averagingをやってみる
  • 若干上がるが、0.96に全く届かない…

教訓: aggregation系の特徴量が入っている場合は特徴量生成でサンプリングするとダメ

  • 「この事例と同じip/deviceを持っている事例の数」といったaggregation(sum/cumsum/ratio/{prev,next}_click_time_diffなど)系の操作が入る特徴量を使う場合、サンプリング後のデータフレームに対して特徴量を作っていたのがいけなかった
    • ipのようなsparseな特徴量の場合、サンプリングすると出る/出ないで結果が大きく変わってしまうため
  • ダメな例: 元データ => negative down sampling => 特徴量生成(10分) => 学習(1分)
  • よい例: 元データ => 特徴量生成(1時間) => negative down sampling => 学習(1分)

Negative down samplingのやり方を改善した結果

  • 学習データ100万件のみで元の精度と同じ結果までいけた
  • rank averagingをすることで多少上がった(0.97に乗るくらい)
    • とはいえ、割とすぐにサチる(5〜10くらいで十分)
  • 試行錯誤がとてもやりやすくなった 🎉
    • パラメータ変えたいときにも特徴量生成待たなくてよくなった
    • 特徴量足したいときにも新しく列を追加するだけでよくなった

特徴量生成

  • 特徴量を変えて実験をする度に毎回1時間待っているのはダルい
  • 列毎に結果を吐いて学習時に列と足し込んでいく形式でやると時間をかけずにやれて便利
  • BigQueryでこういった特徴量を作れるとよさそうですね
    • 30分程度でできるらしい。11th solution

Bayesian optimizationやってみる

  • 色々パッケージがあるけど、skoptを使ってみる
  • ベイズ最適化自体の計算は軽い
  • LightGBMのようにハイパーパラメータが多く、人手で結果を見てられない場合はまあまあ有用?
    • 例: dailyで再学習が必要な場合
    • 人手で結果を毎度見れるようなものでは普通のグリッドサーチで十分か
space = [
    Integer(3, 31, name='max_depth'),
    Integer(7, 1023, name='num_leaves'),
    Integer(50, 1000, name='min_child_samples'),
    Real(1, 999, "log-uniform", name='scale_pos_weight'),
    Real(0.6, 0.9, name='subsample'),
    Real(0.4, 0.9, name='colsample_bytree'),
    Real(0.000001, 0.1, "log-uniform", name='learning_rate'),
    Real(0.000001, 0.1, "log-uniform", name='reg_lambda')
]

res = gp_minimize(baysian_optimization_objective, space, n_calls=100)
print(res)
print(res.fun)
print(res.x)

ベイズ最適化所感

  • ベイズ最適化を使っただけで精度が上がった、というのはtalkingdataでは特になかった
  • cross-validationやってられん!という場合には使ってもよいかも
  • 方法論としては深層学習のチューニングにも使える

最終結果

  • LBのprivateスコアが0.9815761(525位/Top 13.2%)
    • あと一歩足りなかった…
  • negative down samplingで高速にiterationを回せるようになった後であれこれ追加した特徴量が一番効いた

その他面白いやつを共有

pickleの代わりにfeatherを使う

  • 特徴量を追加したDataFrameをpickleでファイルに保存したい
  • pickleのread/write時に結構なメモリ量を消費する
    • せっかく特徴量を作ったのにOOMで死んで水の泡 😇
  • featherだともっと省メモリ、高速に読み書きできる
    • 5分くらいかかっていたのが1分くらいで終わる

pandas芸

  • 特定の条件でグルーピングした上で、そのグループ内で次のクリック時間の計算を効率的に
  • SQLのwindow関数とかでも同じようなことができる
train_df['click_time'] = (train_df['click_time'].astype(np.int64, copy=False) // 10 ** 9).astype(np.int32, copy=False)
train_df['next_click'] = (train_df.groupby(['ip', 'app', 'device', 'os']).click_time.shift(-1) - train_df.click_time).astype(np.float32, copy=False)

validationデータの切り方

  • 学習データをシャッフルしてvalidationを作ると、validationでのAUCとtestでのAUCが結構違う
    • 時系列要素が強いデータなので
  • 最後の1日をvalidationにするとtestとのAUCの乖離が小さくなるので安心
  • 手元のvalidationとLBのスコアが乖離しないようにまずやりましょう

参考

前処理大全[データ分析のためのSQL/R/Python実践テクニック]

前処理大全[データ分析のためのSQL/R/Python実践テクニック]