オットセイの経営日誌

データサイエンス系ベンチャーを経営してます。経営のこと、趣味のことつぶやきます。

Kaggleコンペ初心者が命削りながらなんとかメダル圏内に滑り込んだ話 (IEEE-CIS Fraud Detection)

前回のブログ記事投稿から約1ヶ月。この1ヶ月はKaggleのIEEE-CIS Fraud Detectionに人生を捧げると決めてブログを休んでいましたが、10/4にコンペが終了しました。

結果は、6381の参加チーム中、532位でした。上位10%に入ることができ、初Kaggle本気参戦で銅メダルを獲得することができました。

しかし、2週間ほど前からあらゆる試行錯誤を繰り返してもPublic LBが上がらず、所謂「このKaggleコンペ何もわからない」状態に陥り、非常に苦しい思いをした記憶が強いです。

ということで、本記事はKaggleで初メダル圏内を目指そう、という方を読者に想定して、自分のやったことを書きます。

メダルを既に獲得されている方、ましてKaggle Expert以上の方で万が一本記事にたどり着かれた場合は、さくっと離脱いただくか、笑って眺めていただければと思います。

1. 目的を明らかにし、自分の能力や環境を客観的に評価して、目的に到達するための戦略を立てる

いきなり自己啓発本みたいな言葉を並べましたが、Kaggleも一つの競技である以上、何を目的としてやっていて、それに到達するための自分の能力や環境はどうで、だからこんな戦略をとるべきだ、ということを考えないで戦いに臨むのは愚かだと考えました。

自分の場合、目的は以下2点でした。

  • データサイエンスコンペティションを開こうとしている者として、Kagglerのお気持ちを理解するために全力で戦う
  • 自分の全力を引き出せるように、無謀ではないが高い目標を設定する。具体的には銀メダルの獲得

データを眺めて法則を見出すのが好きな自分としては、本来はEDAをフラフラやって楽しみたいなという気持ちもあったのですが、上記がより重要度の高い目的としてあったので、本音は封印して、結果を出すことを重視しました。

一方で、自分の能力や環境は以下の通り捉えました。

  • Positiveな面
    • 大学で学ぶレベルの統計・機械学習の知識はそこそこある
    • 大学院や仕事で英語に触れていたのでそこそこ読める
    • コンサル業界にいたことがあるので調査力(ググり力)はそこそこある
  • Negativeな面
    • Kaggle初本気参戦なので、全体として何をやれば良いか、その中で何に力を入れるのがスコアupのために重要か、何も分かっていない
    • プログラムを書き始めて歴が浅いので、何でもない処理を書くのにも他の参加者に比べてもたつく可能性
    • スタートアップを経営しながらの参戦なので、(多分)他の参加者に比べて潤沢な時間を投入できるわけではない

半ば必然的に導かれた戦略は、
変に自分の能力に期待せず、徹底的に先人の知恵を調べ尽くし取り入れる。さらに時間が限られているので、スコアを上げるために重要な作業に集中的に時間を投入する
という身も蓋もないものとなりました。

結果から言えばこの戦略は間違っていて、先人の知恵ばかりに期待して自ら有効な特徴量のアイデアを探索する姿勢が弱いようでは、銀メダル圏内に到達できるほどKaggleは甘いものではなかったです。

しかし、自分の目的と能力・環境を照らして戦略を立てたこと自体は間違っていなかったと思います。

2. 全体として何をやれば良いか、その中で何に力を入れるのが重要か、特定する

大方針が決まったので、次に具体的にやるべき作業を理解する段に移ります。

まず、全体として何をやれば良いか把握するところからですが、偉大な先人が素晴らしいまとめをしてくださっています!

qiita.com

Kaggle界隈ではとても有名なupuraさんの記事ですね。この情報がなければ当て所なくKaggleに臨んでいたと思うので、ゾッとします。

さらに、主にTwitterやslack(kaggle-ja)で調べを進めた結果、自分なりの解釈ですが、Kaggleでは全体として以下をやれば良いのだと理解しました。

f:id:mhiro216:20191006125412p:plain

そして、もし投入できる時間が限られているのであれば、力を入れるべき工程は「特徴量エンジニアリング」「Discussion & Notebook 読み込み」だと理解しました。

情報を集める中で、スコアを上げるために特に重要なのは有効な特徴量を発見する特徴量エンジニアリングである、ということが多く言われていました。

一方でハイパラチューニングなどは、議論はそこかしこでなされていますが、実際のところ大きくスコアupに寄与することは少ない、と言われていた気がします。

さらに、Kaggleの良いところは、Discussion & Notebookがあることで、他の参加者と発見した知見を共有しながら戦えることです。

他の参加者の知見だけでなく、今回のようにデータの秘匿性が深く、カラム名がマスキングされているコンペでは、コンペ主催者から開示できる範囲のデータの補足説明がなされたりします。

実際今回のコンペでは、コンペ主催者からの説明はスコアupのための大きなヒントが含まれていました。

前述の通り先人の知恵に学ぶと決めたので、Discussion & Notebookは毎日チェックすることにしました。

あまりにDiscussionの内容を参考にしすぎたので、危うく落とし穴にハマりかけたこともありましたが。。。

以降、各プロセスでやったことを記しますが、明らかに特徴量エンジニアリングに重きを置いた内容となっており、他の工程がスッカスカなのはご了承ください。

3. 実験環境構築

早速データ分析に移りたいところですが、特徴量を追加・削除してモデルを作りスコアの変動を見るというプロセスを何百・何千回と繰り返すことになるので、その作業を行う環境を固定し、できるだけ効率的に実験を回せるように準備しておくことが長い目で見ると重要です。

実験のプロセスですが、大きく分けると以下の2つから成ります。

  1. 新たな仮説に基づく特徴量を追加してみて、スコアupに寄与するかどうか、全データを使わずにクイックに検証
  2. 検証結果が積み上がってきたら、全データを使ってCross Validation (CV)しながらモデルを作り、出力をsubmitしてLeader Board (LB)の変化を確認

一方で実験に使える環境は、一般的には以下の選択肢があると思います。

  • 個人のローカル環境
  • Kaggle NotebookやGoogle Colaboratoryなど無償のコンピュータリソース
  • GCPAWSなど有償のコンピュータリソース

結論から言えば、私は
1のクイックな検証には個人のローカル環境+Kaggle Notebookを、
2のモデル構築にはGCPのGCEを、
使いました。

理由は単純で、個人のローカル環境やKaggle Notebookは高々十数GBのメモリしかないので、特徴量を何千と作り探索しようとしていた私のやり方では、簡単にMemory Errorとなってしまうためです。

GCPを使う最大のデメリットは有償であることで、今回私は3万円ほど使いました(他の方のお話を聞いていると、決して高くないと思います)が、学生さんなどはなかなか難しいかと思います。

有償のリソースを使えない場合、メモリに配慮しながらKaggle Notebookなどを使い回していくことになりますが、それでも金メダルを獲得されている方はいらっしゃるので、テーブルデータであれば十分可能と思います(画像データではなかなか難しいと聞きます)。

で、GCPを使った実験環境の作り方ですが、これも偉大な先人であるtkmさんが手順を残してくださっています。

www.youtube.com

動画の手順と若干異なるのは、私はvimに慣れていないので、ローカルのPyCharm上でスクリプトを修正してgithubにcommitし、GCEにSSHで接続した後githubから修正したスクリプトをpullして実行する、という手順をとっていました。

いずれにせよ、tkmさんは他の動画でBigQueryでKaggleのデータを分析する手順も残してくださっていたり、非常に有益なコンテンツが多いので、食事中にテレビ番組を見るくらいならこちらの動画を流した方がinf倍有益かと思います。

4. EDA

ここからデータ分析に入るわけですが、コンペ参戦を決めたのがコンペ中盤だったこともあり、既に先人が多くのEDAを投稿してくださっていました。

そこで、ざっとEDA系のNotebookを確認した後は、先に特徴量エンジニアリングに着手してしまうこととし、特徴量のアイデアに行き詰まったら自分でEDAをしてみよう、という手順をとることにしました。

5. モデル構築手法選択

テーブルデータにおけるモデル構築手法は、大きく2つの選択肢があります。

テーブルデータでは、多くの場合勾配ブースティング系の手法の方がスコアが高くでるようです。

また、後に出てくるハイパーパラメータチューニングの方法はもちろんのこと、一部の特徴量の作り方についてもGBDTとNNで異なるので、投入できる時間が限られる今回は複数の手法に手を出すのは得策ではないと判断しました。

そこで今回は、LightGBM一本に絞って取り組むことにしました。

この戦略が良かったかどうかですが、最後に行うアンサンブルで、モデルの予測値を複数組み合わせて最終的な予測とするということをしますが、類似のモデルではなく全く独立に作られたモデルを混ぜた方がスコアが良くなることが一般的に知られています。

従って、使う手法を一本に絞ったことはアンサンブルで不利になりますが、一方でLightGBM一本でも上位Solutionでは銀メダル圏内に到達する相当のスコアが出ていたので、やはり次の特徴量エンジニアリングが勝負の分かれ目だったと思います。

6. 特徴量エンジニアリング(Feature Engineering)

戦いのスタートにあたり、良さげなNotebookを見つけ、Forkします(こういうことができるのがスターターに優しくて良いですね)。

私はKonstantin YakovlevさんのNotebookを元にしました。

選んだ理由は、スコアが良いこともそうですが、コードがシンプルでわかりやすく、今後コードを改変して特徴量を追加・削除していくときに混乱しづらいと思ったためでした。

ちなみにこのKonstantin Yakovlevさん、大量のDiscussion & Notebookで参加者に知見を振りまきながら、2位以下を突き放して圧勝されています。

スコアでも教育目的でも多大な貢献をされていて、プラットフォーマーのKaggle的にはこれ以上の貢献はないでしょうね。いつかこの域に達したいものです。

次に、Discussionを眺めながら良さげな特徴量をピックアップし、追加・削除しながらスコアupに寄与するか検証していきました。

以下、私が試行錯誤した特徴量を書き連ねます。本コンペのDiscussionを元に試したものが多いですが、過去コンペで有効だった特徴量も調べて投入しています。

具体的な実装については、長くなるので別記事にまとめることとして、ここでは頭出しだけします。

主に参考にしたDiscussion:

行った特徴量エンジニアリング:

  • 既存のfeatureに対する処理
    • NANをfeatureの最小値より小さい値(-999など)で置換
      • LightGBMはNANがあるとnon-NANだけを分割し最後に残りのNANをいずれかのnodeに振り分ける。過学習につながるリスクがあるためNANは置換しておくのが一般的
    • binning
      • 量的変数を任意の境界値で区切りカテゴリ分け
      • 境界値は1000, 2000, 3000,,,などと一定間隔の値で決める方法もあれば出現頻度で区切る方法もある(histogramの作り方と同じ)
    • label encoding
      • string, category, object型の変数をint型に変換
    • outlier removal
      • 出現頻度が非常に低い値は-9999など(NANの置換に使った値とは違う値)で置換
      • 今回は異常値を見つける問題なので注意して使用すべき
    • TransactionAmtのlog1pをとる
  • 既存のfeatureをgroup単位でaggregation(例えば、card1のcategoryごとに平均をとって新たなfeature "card1_mean"とするなど)
    • sum, mean, std, min, max
    • frequency encoding
    • normalization
      • mean, stdを使ったnormalization、min, maxを使ったnormalization
    • target encoding
      • train dataのターゲット変数の値をgroup単位でmeanなどのaggregationを行うこと
      • やりすぎると過学習になる(未知のデータに対する精度が低くなる)ので初心者にはリスキー。私見ではこれを使わないで精度が上がる方法をまず探すべき
  • group自体を新たに生成
    • cardやaddrを結合してuser groupを生成し、aggregation
    • 今回は時系列データなので、TransactionDTを月/週/日/時間などに変換し、各々の単位でaggregation
    • 今回は時系列データなので、TransactionDTの前後X個のレコードについてaggregation (rolling window)
    • NANの数が同じ変数でグルーピング -> PCAに利用(実装の参考)
  • aggregationではない方法でfeatureを新たに生成
    • 量的変数の和・積・差・商
    • TransactionAmtの小数点以下(cent)のみを取り出す
    • lag/lead
      • 例えば、card1を使ったtransactionが1つ前に発生した時点までのTransactionDTの差(lag)、1つ後に発生した時点までの差(lead)
      • 実装の参考
    • pseudo-labeling
      • 一度予測を行い、予測値が0ないしは1に非常に近いtest dataのtargetに0ないしは1をlabelしてしまい、train dataに新たに追加
  • featureを縮約して新たなfeatureで代替(元のfeatureを残しておいても良いが、縮約したいというニーズがある以上普通は元のfeatureは削除する)
    • PCA
      • 量的変数をn次元に縮約して新たなfeatureとする
      • これが有効な理由は、決定木系アルゴリズムが軸に斜めの表現を不得手なため(参考
      • 実装の参考
    • LDA
      • LDAは文章をベクトル化するとき、出現する単語の出現回数で表現すると次元が極度に大きくなってしまうので、次元削減するときに使う手法
      • この発想をテーブルデータにも適用して、カテゴリ変数間の共起行列を作り、それに対してLDAを行い新たなfeatureとする
      • 参考

様々試した中、最も有効だった特徴量は、

cardやaddrを結合してuser groupを生成し、count, mean, stdなどでaggregation

でした。

アンサンブルする前のモデル単体で、Public LBは0.951+でした。

1st solutionでも同様にuser groupを生成してaggregationした特徴量を有効に使っていて、私との違いは、私はuser groupを特定する程度で止まっていたところを、1st solutionではuser個人を特定する程度までcardやaddrを結合した上で様々なaggregationを行なっていました。

user個人を特定することが重要だというのは以下のDiscussionを見てなんとなく気づいていたのですが、実験するところまで動けなかったのが敗因で、悔やまれます。

"The logic of our labeling is define reported chargeback on the card as fraud transaction (isFraud=1) and transactions posterior to it with either user account, email address or billing address directly linked to these attributes as fraud too. If none of above is reported and found beyond 120 days, then we define as legit transaction (isFraud=0).

ちなみに、大きくスコアをboostするfeatureをmagic featureと呼ぶそうです。コンペ中盤にKonstantin Yakovlevさんが「magic featureが3つあるよ」とおっしゃっているのを見て以降、ひたすらmagic featureを探していたのですが。。。

他にも「TransactionAmtの小数点以下(cent)のみを取り出し、aggregation」などもスコアupに寄与しましたが、user個人を特定した特徴量に比べると効果は小さく、本当にここで勝負が決まってしまったなという感じです。

7. 特徴量選択(Feature Selection)

特徴量を作成した後は、それが有効かどうか検証し、取り込むか否かを決める必要があります。

私は1変数追加するごとに一部データを使ってLightGBMでモデルを構築し、CVの結果をみて有効か判定していました。

LightGBMで検証するとそこそこ時間がかかるので、高速に検証を回したい場合はRidge回帰で代替する手法も一般的なようです。

また今回は採用しませんでしたが、LightGBMではfeature importanceを返してくれる関数が用意されているので、とりあえず大量に思いつく限りの特徴量を作って投入し、feature importanceが0より大きいものは全て採用する、という手法もあると思います(逆にfeature importanceが0より大きいものは、モデル単体の精度向上には寄与しているので、いくら小さくても除くべきではない。敢えてそれらを除いたモデルを作り、後でアンサンブルしたい場合は別)。

今回は大量にfeatureを作ればスコアが上がるというより、特定のmagic featureを見つけることが重要な気がして1変数ごとに検証する手法を採りましたが、これが良かったかどうかは分かりません。

ちなみに世の中にはboruta, NullImportancesなどのfeature selectionの手法もあるそうですが、なかなか扱いは難しいようです。

8. クロスバリデーション(Cross Validation)

Cross Validation (CV)が何かはここでは詳説しませんが、簡単に言えばTrain dataをさらにTrain, Validate dataに分割し、Test dataによる予測を行う前に精度を予測する手法です。

これがなぜ重要かというと、Public LBが計算される対象はTest dataの一部を切り出したもので、Test dataの中で分布に偏りがある部分を切り取っている可能性があったり、そもそも少ないデータ量が切り出されていたりで、最終的に順位決定に使われるPrivate LBとは必ずしも結果が相関しないことがあるからです。

尚、Public LBとPrivate LBが激しく変動することをShake up/downというようです。あまりにこれが大きいコンペでは参加者に混乱を巻き起こしますが、しかし毎回安定して勝っている方もいるので、CVをしっかり設計すれば安定して勝てるはずとも言えます。

それくらいCVは重要で、たとえPublic LBが多少下がっても"Trust your CV"してCVが良くなるモデルを採用していくのが望ましいとされています。

逆に言えば、"Trust your CV"できるCV Strategyを立てなければいけません。

CV StrategyってKFold以外に何があるの?と私も以前は思っていましたが、今回は以下の3つを検討しました。

  • KFold
    • Train dataをK個に分割。デフォルトでは連続するレコードのまとまりで分割するし、shuffle=Trueとすることでランダムな分割も可能
  • GroupKFold
    • 特定カラムのgroupごとにTrain dataを分割。今回のデータであればTransactionDTから算出したmonth別に分割するなど
  • TimeSeriesSplit

どのCVが良いのかですが、明確な判断基準はないものの、

CVのTrain dataとValidate dataの関係が、最後のPredictionでTrainするdataとTestするdataの関係と同様になるようにCVを設計する

のが重要なようです。

今回で言えば、Test dataはTrain dataより先の月のデータとなっていることがわかっていたため、TransactionDTから算出したmonth別に分割するGroupKFoldを採用することとしました。

9. ハイパーパラメータチューニング

LightGBM公式Documentに従ったチューニングを行うつもりでしたが、結局最後まで特徴量探索に時間をかけていたので、最初にForkしたNotebookの設定値を最後まで使っていました。

一応Optunaを使ってみたりもしたのですが、探索範囲が悪かったのか、広すぎたのか、CVを改善するパラメタは発見できず。

次回コンペの宿題ですね。

10. アンサンブル

アンサンブルは異なるモデルを複数作り、予測値の平均をとって最終的な予測とする手法です。

異なるモデルを複数作る方法は色々あって、

  • モデル構築手法を変える(LightGBM, XGBoost, CatBoost, NNで作った各モデルを混ぜるなど)
  • 特徴量を変える
  • ハイパーパラメータを変える
  • 乱数seedを変える
  • (邪道ですが)upされているNotebookの予測値と自分の予測値を混ぜる

などとありますが、今回はモデル構築をLightGBM一本でやっていて、かつ特徴量探索にほとんどの時間を振ったため、乱数seedを変えるseed averagingと、upされているNotebookの予測値と自分の予測値をsimple averaging / rank averagingする程度のことしかしませんでした。

ちなみにアンサンブルの各手法はこちらにまとまっています。

簡単なアンサンブルの結果、Public LBは0.952+まで到達しましたが、0.953の壁を破ることができず、Public LBでもPrivate LBでも銅メダル圏内に終わりました。

この0.953の壁が非常に高く、コンペ最後の1週間ほどは、努力が報われない辛さと不規則な生活によるダブルパンチでなかなかシンドイ日々を送っていました。。。

余談:毒入りKernel

余談ですが、コンペ終盤にPublic LBが0.9532のNotebookがupされました。これは当時銀メダル相当のNotebookで、一瞬色めき立ったものです。

しかし自分の予測値との相関をとってみると、初めの10万行は0.99+ですが、最後の10万行は0.09ほどしかないではありませんか! (今回のTest Datasetは初めの20%のレコードがPublic LB算出に使われ、残りの80%がPrivate LB算出に使われることはDiscussionから分かっていました)

つまりこのNotebookはPublic LBに過学習したもので、これをアンサンブルに取り入れてしまったりすると大きくPrivate LBを落としてしまうわけです。

TwitterではこのNotebookのことを「毒入りKernel」と呼んでいる方がいて、言い得て妙だなと思いました。

勝負事らしい騙し合いも一部では繰り広げられていましたという余談です。

感想

次回コンペに向けて

多くの時間を極端に特徴量エンジニアリングに振る戦略をとりましたが、それ自体は間違っていなかったと思っており、

Discussionを読み込む中で気になったものはすぐに実装に落として検証する、という姿勢を貫くことが次回コンペに向けては重要だと思いました。

後は、なんだかんだで投入した時間は平日夜は毎日2時間ほど、土日計16時間ほどに達していて、相当家庭に負担をかけてしまったなと思っており、

今回作成した特徴量作成のスクリプトなどを再利用できる形にして、効率的に戦うことも重視せねばと思いました。

匿名コンペについて雑感

今回のコンペはuserを特定し特徴量作成することが重要だったわけですが、当然ながらコンペのデータ提供者はuserデータを保持しており、匿名化されていたことになります。

つまり既に分かっている情報を解き明かすことに多くのKagglerの力が注がれていて、それって無駄じゃない?という考えも当然あります。

私も基本的には同意で、ただ難しいのは履歴データはデータが多ければ多いほど匿名性が落ちるので、userを仮名化していても、プライバシー保護しきれないリスクをデータ提供側が憂慮する気持ちも分かります。

加えて今回はFraudというsensitiveな情報が含まれるデータなので、k-匿名性を担保できない状態では、user情報を削除せざるを得なかったと想像します。

データサイエンスコンペは、仮に成果物が実務で役に立たなくとも参加者のスキルアップに繋がるし、何より1つの競技として面白く価値は高いと思うのですが、 一方で実務で役に立たないものに人生の多くを捧げている人もいることを思うと、資源が無駄使いされているようにも思います。

悩ましさを感じつつも、まずは不満の出ないコンペを開催することよりも議論を呼ぶコンペを開催することが正だと信じて、突き進もうと思います。