これはなに?
- MalwareコンペはKernelやDiscussionから学ぶべき点が非常に多いコンペで、個人的にとてもコミュニティに感謝していました。(おかげさまでソロ銀メダルも取れました)
- なので、終わったらコミュニティに恩返ししようと思ってKaggleのDiscussionに投下するべく Malware コンペの解法や工夫していた点をサラッと箇条書きにしていました。
- が、コンペの最終結果がKaggleの歴史的に見ても大荒れの結果(参考)になってしまって、真面目に英訳するのが面倒になったのでブログにサクッと起こしたものです。
- これから肉付けしたり加筆しようと思ってたものをほぼそのまま出したのでヌケモレ多数あります笑
Malwareコンペのきっかけ
- Malwareの前はEloコンペをやっていましたが、1月下旬ごろからコンペに飽きていました。(その後raddar神が降臨して面白くなるのですが笑)
- 気分転換がてらMalwareのデータ見たりして遊んでいましたが、上述の通りKernelやDiscussionが面白く勉強になっていました。
- Eloはソロ銀取れたのですが結構Shake Up/Downが大きいコンペで、Kaggleの実績がほとんどない自分はフロックっぽくて嫌でした。
- なので、フロック疑惑を晴らすのも兼ねて、Elo終了後の2週間でチャレンジすることにしました。
- フロック疑惑を晴らすために出場したのに、Elo以上のShake Up/Downだったのでフロック疑惑は晴らせなかったのが残念です。
- が、2個めの(ソロ)銀メダル取れたので、次に金メダル取ったらMasterになれます。そのステップアップが出来たのは嬉しかったです。
注意
- 以下の内容は全てCV/PubLBでの話です (PriLBの後追い検証はしてません)
- 雑記なのでCPMPさんのSolutionを読まれる方が勉強になると思います笑
特徴量エンジニアリング
ベース
- 良さそうなKernelがあったので、基本的な特徴量はここから頂戴しました。
https://t.co/mcmgLZCZKF
— ML_Bear (@MLBear2) 2019年2月10日
マルウェアのベースはこれが良さそうかな。シンプルだけど今んとこベストスコアでかっこいい。OuterJoin使ってTrain⇔Testの出現割合が偏ってたり、出現数少ない値を排除してる部分とかとても綺麗で参考になる。全カラムをカテゴリ扱い(FreqEncoding)してるので伸び代も多そう
- 今回、Train/Testで時系列でデータが分けられていました。さらに、このLBが完全時系列分断されている可能性があることを指摘しているKernelがあったため、特にバージョン系の特徴量の取扱に気をつけました。Trainデータ内の新し目のバージョンはまだマルウェアに感染していなくて、これから感染する、などの可能性もあるかなと思ったためです。
マルウェアコンペのLBが完全に時系列でPublic⇔Private分割されているという問題を指摘したKernel。ある日付(正確には推測の日付)以降のsubmissionを全部0にしてもPublic LBは全く影響を受けなかったとのこと…。これは難しそう…。https://t.co/RRSZrcN5rV
— ML_Bear (@MLBear2) 2019年2月13日
- 具体的にはVersion系の扱いを以下の感じで処理しました
- バージョン番号そのもの及び分割したものでTargetEncodingはしなかった
- 古いバージョンだけ行ったり、丸めたバージョンで行ったりはしました。(が、あんまり効いてなかった気がします)
- AvSigVersionは日付に変換して日付として処理を行った
- 例: Train(or Test)の最新の日付までの差分(Latest AvSigVersion Date of TrainData - AvSigVersion Date)
- CPMPさんのSolutionみてるとこの当たりにMagicFeatureあったみたいなのでたどり着けなくて悔しい
- バージョンを古い順に並べ替えて、一番古いものから数えて出現頻度が何パーセンタイルに位置するか、なども効きました
- バージョン番号そのもの及び分割したものでTargetEncodingはしなかった
Target Encoding
- バージョン番号系以外はTargetEncodingを利用しました
- Fearure Importance高めの特徴量では2個の掛け合わせでも行いました。
- 個人的には効いていたと思っていましたが、CPMP Solutionでは効いてないと書いていたので、なにか間違っているかもしれません。
Cluster Distance Features
- このKernel で出てきたクラスタ中心までの距離の特徴量もそこそこ使えました
https://t.co/X1NvYFnsiA
— ML_Bear (@MLBear2) February 10, 2019
KMeansでクラスタ中心を求め、中心までの距離を特徴量として加えるというどっかで見た特徴量を作ってるカーネル、なんだけど、マルウェア下調べの身としては強い特徴量の活かし方の方が参考になった。同じ特徴量からエンコーディングだけ変えて何度も追加したりするんだね。
- 具体的には以下のようにして作って選別しました。20回ぐらい回して2-3個使えたかなぐらいでした。
- LightGBMでFearure Importance高めの変数を20個ぐらいピックアップ
- そのうち7個をランダムで選んでKMeans→クラスタ中心までの距離計測&特徴量化
- LightGBMに投入して精度が下がらなかったら採用
KNN Features
- Home Credit 1st Place Solutionで使われていた
neighbors_target_mean_500
も作りました - 計算時間の都合で多くは作れなかったがいくつか採用した
LDA Features
- 以下のような感じでLDAした特徴量はそこそこ効いていました
- TrainデータとTestデータを結合
- Fearure Importance高めの特徴量を2個づつ取ってきて以下のように処理
- AVProductStatesIdentifierとCountryIdentifierの例
- 各 AVProductStatesIdentifier で CountryIdentifier が何回現れるかカウント
- LDAで AVProductStatesIdentifier のCountryIdentifier文脈でのトピックを算出
- AVProductStatesIdentifierとCountryIdentifierの例
Validation
以下のKernelを参考に似た感じでやりましたが最後まで苦戦していました。うまく行った人のやり方聞きたいです。
Models
Base Models
概要
- LightGBMはKernel Fork後自分でかなり書き換えましたが、FMは特徴量の入れ替えとコードの可読性改善以外ほとんどいじっていません。
- 時間的制約…、と言いたいところですがDeepFMとかほぼ知識なくいじれなかった…。
詳細
- LightGBM
- base: Kernel
- CV 0.7344 / pubLB: 0.697 / priLB: 0.645
- 特徴量: 180個ぐらい
- 120個ぐらい自分で足しました
- 20-30個ぐらい削ったと思います
- XDeepFM / NFFM
その他のModel
- Stacking用にRandomForest、LogisticRegression、XGBoost、CatBoostなど作りましたが結局使っていません。
Parameter Tuning
LightGBM
- パラメータチューニングのノウハウがあまりないので、Optunaを使いました。
- パラメータの意味は分かるが、どれくらい動かしたら良いかとかKaggleCoursera以上の知識がない
- 経験ある人なら簡単にたどり着けたなのかもしれませんが、自分では絶対に設定しないようなパラメータにたどり着けてとても有用でした。 (LB +0.003)
- 具体的なパラメータは以下のような感じでした
チューニング前
'num_leaves': 300, 'min_data_in_leaf': 75, 'boosting': 'gbdt', 'feature_fraction': 0.2, 'bagging_freq': 3, 'bagging_fraction': 0.8, 'lambda_l1': 0.1, 'lambda_l2': 0.1, 'min_child_weight': 50
チューニング後
'num_leaves': 1638, 'min_data_in_leaf': 1819, 'boosting': 'gbdt', 'feature_fraction': 0.202, 'lambda_l1': 50.300, 'lambda_l2': 41.923, 'min_child_weight': 8.56,
Stacking/Blending
- 最終的にFinal Best Scoreとなったものは以下3モデルのRankAveragingでした (CV: 0.73616 / pubLB: 0.700 / priLB: 0.649)
- LightGBM
- XDeepFM
- NFFM
- これ不要なんだけど気づきようがありませんでした…
- 抜いてたら金だった…
- PublicLB最良モデルは↑に加えて以下をStackingしたものでした (CV: -- / pubLB: 0.701 / priLB: 0.647)
- LightGBM*2
- XDeepFM (Kernel そのまま)
- NFFM (Kernel そのまま)
- RandomForest / LogisticRegression
- 最終Submitには上の2つを使いました
- PubLB最良のものはKernelのデータを使っているが、Kernelの特徴量は保守的な補正が入っていない
- なので、pubへのoverfitが怖かった。
- そのため、自分が作った保守的な特徴量だけのモデルのシンプルなRankAveragingをもう一つのサブとして選んだ
余談: その他工夫した点
- データが重かったのでモデルを組んだり特徴量のテストをする時はデータを1/4程度にサンプリングして使ってました
- Trainデータ全量でモデルを走らせたのはわずか1週間前でした
- この計算結果がコケたら諦める、というものが無事 pubLB 0.697 を出してアンサンブルKernelを抜いたときにはホッとしました
MalwareようやくアンサンブルKernel抜いたぞ…!って言ってもSingleModelで抜いたから後は突き放すだけなんだけど。それにしてもデータ重いいw#kaggle - because this RAM isn't going to use itself. https://t.co/8uH1H3BjpA
— ML_Bear (@MLBear2) March 8, 2019
- とにかくデータが重かったので特徴量作成はBigQueryを主としていました。
- 僕の本職がデジタルマーケティング / PM なのでpandasよりもSQLのほうが慣れてるというのもありますが…笑
- 以下の処理を自動化して寝ている間に特徴量テストが終わるように工夫した
- 寝る前までにクエリ書いて保存しておく
- テスターが以下の処理を勝手に行う
- BigQueryに並列でクエリ投げる
- pickleに保存する
- pickle読み出してLightGBMに投入して採否判断
- どうせLightGBMでコア数必要になる & とにかく時間がなかったので、データ処理は綺麗に書く方法を考えるより多コアで殴れるように工夫して書きました。
- read_gbq的なコマンドのBigQueryからのデータ転送が遅いので下記のように並列化していました
import os import glob import joblib import pandas as pd from google.cloud import bigquery from reduce_mem_usage import reduce_mem_usage def bq_load(query, feature_name, exec_date): file_path = './features/{}/df_{}.pickle'.format(exec_date, feature_name) bq_client = bigquery.Client.from_service_account_json('/path/to/xx.json') df = bq_client.query(query).to_dataframe() df = reduce_mem_usage(df) df.to_pickle(file_path) exec_date = '20190301' queries = {} for sql_file_path in sorted(glob.glob("./sql/{}/*".format(exec_date))): with open(sql_file_path) as f: query = f.read() feature_name = get_feature_name_from(query) queries[feature_name] = query r = joblib.Parallel(n_jobs=32)( joblib.delayed(bq_load)(query, feature_name, exec_date) for feature_name, query in queries.items() )
- 以下のような並列処理のメソッドも多用しました
import numpy as np import pandas as pd from functools import partial from sklearn.preprocessing import StandardScaler from multiprocessing import Pool, cpu_count def parallelize_dataframe(df, func, columnwise=False): num_partitions = cpu_count() num_cores = cpu_count() pool = Pool(num_cores) if columnwise: # 列分割の並列化 df_split = [df[col_name] for col_name in df.columns] df = pd.concat(pool.map(func, df_split), axis=1) else: # 行分割の並列化 df_split = np.array_split(df, num_partitions) df = pd.concat(pool.map(func, df_split)) pool.close() pool.join() return df def standard_scaling(sc, series): """ 列毎の正規化処理 """ 〜〜〜〜 return scaled_series sc = StandardScaler() numerical_cols = [hoge, fuga, ...] f = partial(standard_scaling, sc) df_scaled = parallelize_dataframe(df, f, columnwise=True)
CPMP Solutionからの学び
(あとで追記するかも)
- adversarial validation がしずらくなるようにデータを前処理した
- CPMPさんの曰く
- 当初のデータはAUC0.98でadversarial validation出来てしまう
- データを前処理して0.7以下に抑えた
- 例: EngineVersion を Freq Encodingする等
- 「どれくらい前処理(すれば|しなくても)良いんだろう?」って思ってた解だ。なるほど…!
- CPMPさんの曰く
- magic feature
- 同じEngineVersionにおけるMax AvSigVersion と AvSigVersionとの差
- LB: +0.020 らしい。すげぇ…。
- 言われてみれば意味合いはなんとなく分かる(がたどり着けない)ので、分析のセンスとTry&Errorの手数の重要さがわかる。
- 同じEngineVersionにおけるMax AvSigVersion と AvSigVersionとの差
- Models
- Single
- LightGBM: pubLB 0.698
- Keras: pubLB 0.696
- Blend
- Keras (w/ LightGBM preds): pubLB 0.698 (priLB 0.683!)
- Single
- TargetEncodingは効かなかった
- 外部データも効かなかった
総括
- 大荒れのコンペでしたが、自分的には色々勉強になったし銀メダルゲット出来たので総じて良いコンペでした。
- Malwareするまでは `adversarial validation' の概念すらしらなかったので隔世の感です…!
- また、文頭にも書きましたがKernelやDiscussionでのコミュニティの暖かさにも感激したコンペでした。
- 1st Placeの人も書いてるように @cdeotte @ogrellier @cpmpml あたりの投稿が非常に参考になりました。
- 一旦一区切りだけど次は金メダル取れるように頑張りたいです。