麻か辣なら辣が好き

でもどっちも好き。変な経歴のマーケター兼PMがあれこれ書くブログだよ。

Kaggle Microsoft Malware コンペ振り返り

これはなに?

  • 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があったので、基本的な特徴量はここから頂戴しました。

  • 今回、Train/Testで時系列でデータが分けられていました。さらに、このLBが完全時系列分断されている可能性があることを指摘しているKernelがあったため、特にバージョン系の特徴量の取扱に気をつけました。Trainデータ内の新し目のバージョンはまだマルウェアに感染していなくて、これから感染する、などの可能性もあるかなと思ったためです。

  • 具体的にはVersion系の扱いを以下の感じで処理しました
    • バージョン番号そのもの及び分割したものでTargetEncodingはしなかった
      • 古いバージョンだけ行ったり、丸めたバージョンで行ったりはしました。(が、あんまり効いてなかった気がします)
    • AvSigVersionは日付に変換して日付として処理を行った
      • 例: Train(or Test)の最新の日付までの差分(Latest AvSigVersion Date of TrainData - AvSigVersion Date)
      • CPMPさんのSolutionみてるとこの当たりにMagicFeatureあったみたいなのでたどり着けなくて悔しい
    • バージョンを古い順に並べ替えて、一番古いものから数えて出現頻度が何パーセンタイルに位置するか、なども効きました

Target Encoding

  • バージョン番号系以外はTargetEncodingを利用しました
  • Fearure Importance高めの特徴量では2個の掛け合わせでも行いました。
  • 個人的には効いていたと思っていましたが、CPMP Solutionでは効いてないと書いていたので、なにか間違っているかもしれません。

Cluster Distance Features

  • このKernel で出てきたクラスタ中心までの距離の特徴量もそこそこ使えました

  • 具体的には以下のようにして作って選別しました。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文脈でのトピックを算出

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
    • base:
    • 特徴量: 80個
      • 自分のLightGBMの特徴量をほぼそのまま使いたかった
      • が、GPUのメモリに載らなかったので上から80個選んだ
    • CV
      • NFFM: CV 0.73166 / pubLB: 0.690 / priLB: 0.628
      • XdeepFM: CV 0.72809 / pubLB: 0.692 / priLB: 0.660
        • これがPrivate最良モデル(金圏)ですが絶対に選ばないので後悔してません笑

その他の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を抜いたときにはホッとしました

  • とにかくデータが重かったので特徴量作成は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する等
    • 「どれくらい前処理(すれば|しなくても)良いんだろう?」って思ってた解だ。なるほど…!
  • magic feature
    • 同じEngineVersionにおけるMax AvSigVersion と AvSigVersionとの差
      • LB: +0.020 らしい。すげぇ…。
      • 言われてみれば意味合いはなんとなく分かる(がたどり着けない)ので、分析のセンスとTry&Errorの手数の重要さがわかる。
  • Models
    • Single
      • LightGBM: pubLB 0.698
      • Keras: pubLB 0.696
    • Blend
      • Keras (w/ LightGBM preds): pubLB 0.698 (priLB 0.683!)
  • TargetEncodingは効かなかった
  • 外部データも効かなかった

総括

  • 大荒れのコンペでしたが、自分的には色々勉強になったし銀メダルゲット出来たので総じて良いコンペでした。
    • Malwareするまでは `adversarial validation' の概念すらしらなかったので隔世の感です…!
  • また、文頭にも書きましたがKernelやDiscussionでのコミュニティの暖かさにも感激したコンペでした。
    • 1st Placeの人も書いてるように @cdeotte @ogrellier @cpmpml あたりの投稿が非常に参考になりました。
  • 一旦一区切りだけど次は金メダル取れるように頑張りたいです。