ML_BearのKaggleな日常

元WEBマーケターのMLエンジニアがKaggleに挑戦する日々を綴ります

Kaggle Data Science Bowl 2019 参戦記 〜10万ドルの夢を見た話〜

これはなに?

  • Kaggleで10/24-1/23に開催されたData Science Bowl 2019コンペの参加記録です
  • 子供向けの教育アプリのログデータを元に、子供たちが課題をどれくらいの精度で解くことができるかを推定するタスクでした。
  • 優勝賞金10万ドルの大盤振る舞いなコンペで、個人で最高5位まで順位が上がったときにはなかなかいい夢を見ることができました。
  • ただ、評価指標の特性及びpublicLB(暫定順位)の算出に利用するデータ数不足などから、暫定順位(publicLB)と最終順位(privateLB)が激しく入れ替わるコンペでした。
  • 評価指標に振り回されてアタフタした挙げ句、public 17thからprivate 56thと大きく順位を下げるというあまりよろしくない結果に終わってしまったのですが、反省も込めてやったことのメモを残しておきます。

いい夢見ていたときのツイート

やったこと

コンペに参加するまで

  • 昨年10月のIEEEコンペで初めて金メダルを取れたのですが、コンペの締切が終了直前に延長されるなどのトラブルから、大変に疲弊していました。
  • なので、kaggleしばらくいいやと思って少しkaggleを休んでいましたが、12月に参加したKaggle Days Tokyo のオフラインコンペが楽しくてテーブルコンペ欲が復活してきました。(Kaggle Days Tokyo オフラインコンペ参戦記)
  • そこで、年末年始時間があったので軽くKernelやDiscussionを見てみると、以下のような工夫が何も言及されていなかったので、これやるだけでもまぁまぁ行けるかな、とか思って参加してみることにしました。
    • test-set内でAccuracyGroupを特定できるデータをtrainに利用する
    • targetを変えたモデルを活用する
      • accuracyそのもの
      • そもそも正解するかどうか
      • 4100(4110)イベントが何回起こるか

※ QWKは揺れる指標と聞いていたことが合ったので、ワンチャン揺れてソロ金あるかも、という打算が合ったのは書くまでもないと思います笑。

ワンチャンでソロ金取れたらええ感じやん〜、とか思ってた頃のツイート。これが3週間前とか信じられない。

コンペ参加直後

  • まずデータざっくり見た後、Kernelをベースにして基礎的な特徴量を作りました。
    • ベースkernel
    • installation_id全体でSUM取る、みたいなやつとかは当然抜きました。(最新版とかだと消えてるかも)
    • adjust_factorとかのあたりがよくわからなかったので無視しました。

CV構築

  • trainはtestに比べてやたらプレイ回数が多いユーザーが散見されたので削らないといけないと思っていました。
  • そのため、Adversarial Validation をもとにtestと乖離してそうな上位30%程度のログを特定し、以下の処理に活用しました。

    1. QWKのしきい最適化への活用
      • testと乖離していないtrainのデータから、testの分布に合わせて500回程度サンプリングを行って評価データセット群を作り、QWKのしきい値最適化を行いました。
      • 500って適当に決めたけど、そんな感じでサンプリングしている人は多かったイメージです。
      • trainとtestの分布がどれくらいずれてるかわからなかったのと、threshold optimizerの挙動が不安定に感じたので平均化したかった。
    2. モデルの評価への活用
      • 上記と同じデータセット群を用いて、RMSEの平均を取ってモデルの精度の確認を行いました。
    3. early_stoppingからの排除
      • 学習時のValidationSetから削除してearly_stoppingの参考にしないようにしました
      • TrainSetから消すテストもしてみましたが全然ダメだったので学習には使いました。
  • 上記方針はTrainをhold-outして適当に削ったもので手元で実験しながら決めました

    • testをうまく再現出来ていたか微妙なのですが、何もないよりはマシかなと信じてやってました。
    • IEEEコンペでチームメンバーがやってたのを学んでたので参考にしてやりました。

Data Augumentation

  • test-set内でAccuracyGroupを特定できるデータをtrainに利用しました。
  • 手元の数値は全面的に良くなるのですが、publicLBがなぜか下がっていました。
  • そのため、最後の最後で消してしまいました。-0.004ぐらいのロスでした。最終サブの片方では残せばよかった。

Private Dataset Probing

  • Assessment1回もやったことない人がどれくらいいるのか知りたかったので少しだけ行いました。
  • publicより結構多かったので、publicはあまり参考にしないようにしました。(結局最終的に参考にしたのですが)

Feature Engineering

以下を行って、ベースから取ってきたものと合わせて1150個ぐらいになりました。ただ、実質3日もやってないのでこのあたりもっとやりたかった。

  • ベースのKernelにこれは効くでしょってやつを足していきました。
    • 同じタイトルの過去の成績、イベントカウント
    • 同じワールドの(以下同じ)
    • 途中のゲームの評価を詳しく
    • 課題ごとの correct rate
    • 4020/4025系イベントの集計
    • correct系の集計
    • clip length
  • 他に独自のものをいくつか足しました
    • target encoding
      • title (≒タイトルの難易度)
      • title x 何回目のトライか
    • 別のモデルで予測した値を特徴量として戻す
      • 予測したもの
        • accuracy(mean of correct)そのもの
        • 4100(4110)イベントが何回起こるか
        • そもそも正解するかどうか
      • これは特徴量として利用しましたが、最終アンサンブル時のStackingの1モデルとして使っても良かったかも。
        • keeeeei79さんはStackingのモデルとして利用したらしい。(正規化なども特にせず)
    • word2vec: event_id(+correct)を単語としてみなす → ユーザーごとにつなげて文章にする → event_id をベクトル化 → SWEM
      • 比較的よく効いてました
  • 捨てたやつ
    • PageRank
      • titleやevent_idの遷移をグラフ化 → AssessmentのAccuracyGroupを伝搬させtitleなどの重要度を算出 → ユーザーごとに集計
      • feature importancesで上位に上がってくるのですが(publicLB)スコアにはほぼ無風だったのでコードが煩雑にならないように捨てました
    • LDA
      • titleやevent_idの遷移をLDA → ユーザーごとに集計
      • これはimportanceも低かった

Feature Selection

  • Null Importancesで600個ぐらい削り、最終的には550個ぐらいの特徴量にしぼりました。
  • Kaggle Days Tokyo の senkin-san slide のP18の式を利用
    • gain_scoreがほんの少しだけマイナスのものまで使うとちょうどよかった

QWK threshold optimization

  • kernelと同じものをやりました
  • タイトルごとに最適化するのを何度もトライしましたが結局うまく行きませんでした。

Models

  • 特徴量はすべて同じ(LightGBM以外は正規化している)で以下のモデルを作りました。
    • LightGBMx3 (葉が多い/普通/少ない)
      • パラメータはoptunaで最適化しました
    • NN
      • 特徴量作成で参考にしたカーネルと同じです
      • 余談ですがBaseModelが結構キレイに作られていたので実装で参考にしました。
    • Random Forest
    • Ridge
  • LightGBM以外も意外と強かったので驚きました。
  • すべてのモデルでSeed Averagingを行って、アンサンブルを行いました。

Ensemble

  • StackingとWeightedAverageで迷いました。手元の実験ではStackingが強かったのですが、以下2つの理由で結局WeightedAverageにしました。(-0.002ぐらいのロスでした)
    1. publicLBの数字がWeightedAverageが強かった
    2. trainとprivateが乖離してるときに爆死するのが怖かった
  • WeightedAverageのウェイトはoptunaで探索するようにしました
  • Stackingは2段のつもりでした。
    • LightGBMx3+NN+Ridge+RF → Ridge
    • 4th solution見てると3段でやってるので驚きました(いつか試す)
  • petfinderの解法に出てきたrank化したけど(多分)あんまり関係なかった

最終結果

  • public: 17th (0.570) → private: 56th (0.551) (3500teams)

コンペを通しての感想

  • Shake downして疲れた
    • 一時はまぐれで5位まで順位を上げることが出来ました。
    • 捨てサブだったので自分でもまぐれだとわかっていたものの、夢と希望が膨らまざるを得ませんでした。
    • 最終的には1ページ目の外まで飛んでしまい、疲れました。
  • 自分よりpublicLBを信じて疲れた
    • コンペ開始直後はpublicLBは気にしないでおこうと心に誓っていたのですが、コンペ終了間近で判断に困ったときに、なにか心の拠り所が欲しくて結局見てしまいました。その結果、いくつか信念を曲げたことで-0.006程度のロスになってしまった。(結果論ですが)
    • コンペ中盤の余裕のあるときにポリシーを明文化して貼っておく、とかしたほうが良いかもしれない。
    • 振り回されて特徴量生成がおざなりになったりしたのも痛かったですし、何より、なんであんなに頑張ってた自分を信じてあげられなかったんだ、ととても悲しい気持ちになりました。
  • QWKに非常に手こずって疲れた
    • 様々な方法でQWKハック/安定化を試みたが大半は徒労に終わりました。
    • ただ、oofをサンプリングして平均化したしきい値を求める、など結果的にみんなやってた手法を自分で見つけられてよかった。
    • JackさんがQWKの直接の最適化をしていたらしく度肝を抜かれました。
  • コードコンペに慣れたけど疲れた
    • 今までのコンペはひたすらBQにSQL投げるマンだったのでpandas力上がって良かったです。
    • コンペ終盤はFastSubmissinで回すことを覚え、PDCAのサイクルが格段に早くなりました。やっぱりGCPのデカいインスタンス最高。
    • テストをちゃんと書いたので、いくつかのサブでミスが発見できて助かりました。

自分を信じられなかった人の末路

まとめ

感想が3つとも疲れたになってしまいましたw 各種トラブルを含む3週間チャレンジでとにかく疲れたのですが、まぁ楽しかったかなーと。

3月からウォルマートのコンペが始まるみたいなので、それまで色々勉強して準備して、また疲れる日々を過ごせれば嬉しいなと思います。

冷静に読んでみると、なにを言っているのか(ry な感想ですね笑。