Kaggleで世界11位になったデータ解析手法~Sansan高際睦起の模範コードに学ぶ

Kaggleの上位入賞者であるKaggle Grandmasterを獲得した、Sansan株式会社のデータサイエンティスト高際睦起さん。模範となるソースコードをもとに考え方や解析手法を教えていただきました。

Kaggleで世界11位になったデータ解析手法~Sansan高際睦起の模範コードに学ぶ

世界中のデータサイエンティストたちが集まり、企業や研究者が投稿したデータに対する高精度なモデルを競い合うプラットフォーム・Kaggle。メンバーは100万人を超えており、良問の多さや参加者のレベルの高さゆえに、機械学習を学ぶ者にとって優れた研鑽(けんさん)の場となっています。

日本国内にも数多くのKaggler(Kaggleに取り組む人)がいますが、上位入賞者であるKaggle Grandmaster1の称号を持つ日本人は4人しかいません(2018年8月2日時点)。そのひとりが、クラウド名刺管理サービス「Sansan」や、名刺アプリ「Eight」を提供するSansan株式会社のデータサイエンティスト高際睦起(たかぎわむつき)さんです。

今回は彼が11位に入賞したコンペ「Porto Seguro’s Safe Driver Prediction」にフォーカスを当て、分析精度を高めるためにどのような手法を用いたのかを解説していただきました。

1

高際 睦起(たかぎわ・むつき)
Data Strategy & Operation Center
R&D Group 研究員 博士(理学)
東北大学大学院理学研究科物理学専攻博士課程修了。Kaggle Grandmaster。
画像認識、機械学習を使った研究開発に従事中。京都ラボ勤務。

「Porto Seguro’s Safe Driver Prediction」とは?

──まず、コンペの概要について解説していただけますか?

高際:「Porto Seguro’s Safe Driver Prediction」は、車のドライバーが次の年に保険金請求したかどうかを予想するコンペです2。ドライバーの情報が匿名化されて与えられており、その情報を分析することで請求の有無(t={0(請求無), 1(請求有)})を予測する2クラス分類の問題でした。

データが記載されているCSVファイル3には、ind(18種)、reg(3種)、car(16種)、calc(20種)という合計57種類の変数が記載されており、それぞれの具体的な項目名(何を示す変数か)は隠されています。項目によっては欠損値4がありました。訓練データ中 P(t=1)=0.036 という非常にアンバランスなデータセットとなっています。

【技法1】前処理

──欠損値を含むカラムに対して、どのような対応を行いましたか?

高際:用いたのは、わりとオーソドックスな手法ばかりでした。例えば、平均値で欠損値を穴埋めしたり、「XGBoost」というツールを使ったり。XGBoostは非常に有名なツールで、多くのKagglerが使っている定番のものです。このツールは欠損値を欠損のまま入力に使えるため、このコンペではそれほど深く考えずにデータを突っ込みました。

2

また、このコンペではロジスティック回帰やNeural Networkなど複数の手法を用いていくつも学習器を作成し、それらのアンサンブル学習によって精度を上げたのですが、XGBoost以外の学習器ではカテゴリ変数5か数値変数*6かによって欠損値の扱い方を変えています。

カテゴリ変数の場合は、欠損値を「新たなカテゴリ」として扱いました。つまり、K個のカテゴリがあったら、欠損値もカテゴリとみなしたK+1個のOne Hot Encodingを行って特徴量にしています。

数値変数の場合は、平均値か中央値で欠損値を埋めて学習器に訓練をさせ、後でアンサンブルする際に良かった側の結果を選びました。また、新たな変数として「欠損値かどうか」の二値変数も特徴量に加えています。

# categorical features
for k in cols_category:
    ohe = pd.get_dummies(df[k])
    x = (ohe.values != 0)
    m = x.mean(axis=0)
    a = ((min_freq < m) & (m < 1-min_freq))
    X.append(x[:, a])
    cols.extend(['{}_{}'.format(k, i) for i in ohe.columns[a]])

    X.append(df[k].map(dict(zip(ohe.columns, m))).values)
    cols.append(k+'_freq')

    for c in ('ps_reg_02', 'ps_reg_03', 'ps_car_12', 'ps_car_13'):
        m = [df[c][x[:, i]].mean() for i in range(x.shape[1])]
        X.append(df[k].map(dict(zip(ohe.columns, m))).values)
        cols.append(''.join((k,'_mean(',c,')')))

# numeric features
X.append(df[['ps_reg_03', 'ps_car_14']].values == -1)
cols.extend(['ps_reg_03_-1', 'ps_car_14_-1'])

x = df[cols_numeric].values
x[x==-1] = np.nan
m = np.nanmedian(x, axis=0)
for i in range(x.shape[1]):
    x[np.isnan(x[:,i]),i] = m[i]
X.append(x)
cols.extend(cols_numeric)

▲高際さんの書いたコードより一部抜粋。カテゴリ変数と数値変数に対するさまざまな前処理。ここで作成した特徴量をRGFで用いたという。

──欠損値の穴埋めにおいて、複数パターンを試した上で最終的にアンサンブル学習で精度を上げるというのはベーシックな手法なのでしょうか?

高際:そうですね。複数の方法でやってみて、アンサンブル学習で重み付きで混ぜるなり、混ぜても精度が上がらないものは捨てるなりしていくことが多いです。

──欠損値があるだけではなく、不均衡データであることもこのコンペの大きな特徴かと思います。どのような対応を行いましたか?

高際:評価指標が正解率である場合には不均衡データの対応は非常に厄介になるんですが、「Porto Seguro’s Safe Driver Prediction」では評価指標がジニ係数であったため、不均衡データ特有の対応法がそれほど必要ありませんでした。

XGBoostにscale_pos_weightという正例負例の重みを変えるパラメータがあるので、良い値を探索しました。今回のような評価指標で、訓練データと評価データとで分布が近い場合には、不均衡起因の問題は少ない印象があります。

──今回はあまり工夫が必要なかったとのことですが、他のコンペなどでは不均衡データに対して何かしらの処理をしなければ学習がうまくいかないケースもあると思います。その場合、どんな対処法をとることが多いですか?

高際:一般的には、問題設定(評価指標やデータの分布)を正しく理解した後、訓練データのOver-/Under-samplingや重みの変更などを試してみるのが妥当だと思います。

どういう対処法をとるのがいいかはデータサイエンティストの間でもよく議論の種になるんですが、結局のところデータや評価指標に依存するのでケースバイケースです。さまざまな手法を試してみなければ何が最適かは分かりません。アルゴリズムを作っては試してをくり返しながら、チューニングを続けていくことが多いですね。

【技法2】特徴抽出

──「Porto Seguro’s Safe Driver Prediction」ではバイナリ特徴やカテゴリカル特徴、単純な連続特徴などさまざまなデータが入り混じっていました。それらを学習モデルで使うために、どのような手順で特徴抽出していったのでしょうか?

高際:カテゴリ数が非常に大きいとか、数値変数に外れ値が目立つなどの場合はうまく特徴量化しづらいのですが、今回のデータはそういうことがなかったので扱いやすかったです。

XGBoostの学習では、バイナリ変数と数値変数はそのまま特徴量として利用し、カテゴリ変数はOne Hot Encodingしました。カテゴリ変数を、「頻度」「他の変数の平均値」のような数値変数に変換して特徴量に加えました。

Neural Network用はXGBoostとほぼ同じで、変数別に標準化(平均値を引いて分散で割る)するプロセスを入れています。

また、特徴抽出を行うツールとしてはscikit-learnPandasを使用しています。これらはKagglerがよく使う定番のものですね。

ちなみに、交差検証で精度を見る上で特徴量を作成するとき、知っている人にとっては当たり前ですが初心者にありがちなミスを話しておくと、平均値や中央値、分散などの統計値を利用するプロセスは交差確認の中で行わないと正しく精度を求められないことがあります。

──特徴抽出は各Kagglerの個性が出る作業かと思います。高際さんは、他の方々と比べてどのような個性を持っていますか?

高際:僕の場合、フィーチャーエンジニアリングはそれほど得意ではないです。特徴をコンピュータ自身に探してもらうようなタイプのコンペの方が得意ですね。

「Porto Seguro’s Safe Driver Prediction」の場合はデータの変数名が隠蔽されていたので、フィーチャーエンジニアリングがうまい人がなかなか活躍できなかったんじゃないかと思います。変数名をもとにして、効果がありそうな特徴を推測することが難しいからです。

僕はカラムが全て匿名化されているコンペの方が好きです。相対評価になりますが、そういうタイプのコンペの方が順位も上がりやすい傾向にある気がしますね。

3

【技法3】予測モデルの作成

──機械学習の手法を複数用いて数多くの予測モデルを作成したそうですが、試した手法を順に解説してもらえますか?

高際:時系列順に話すと、まずはロジスティック回帰を試しました。これは、どんなコンペでもとりあえず最初に試す定番の手法です。scikit-learnを使用したり、 Vowpal Wabbitによって変数間の相関を確認したりしました。

次に試したのはNeural Networkです。浅いNeural Networkを作り、ロジスティック回帰の精度と比較しました。徐々に層を追加して深くしながら、精度の変化を確認していったんです。このコンペの場合は、モデルの自由度を増やすとオーバーフィット(過学習)しやすい傾向が強かったので、パラメータチューニングはほどほどでやめました。

──Neural Networkはよく使われるライブラリがいくつもありますが、高際さんは何を愛用していますか?

高際:僕はKerasを使うことが多いです。データサイエンティストによってはTensorflowChainerなどを用いている人もいると思います。僕がKerasを採用するのは、Kerasの前身であるTheanoというライブラリを以前から使っていたからです。要するに、手になじんでいるんですね。

さらに、Neural NetworkにおけるAutoEncoderという手法も試しました。AutoEncoderによって学習させることでNeural Networkの最適化を行い、オーバーフィットを防いでみようと思ったんですが、これもあまりうまくいきませんでした。

次に試したのは、XGBoostという手法です。先ほども話したとおり、よく用いられる定番のものです。これは、決定木*7をたくさん作って識別性能を上げるTreeモデルと呼ばれる実装になっていて、似たような手法としてRegularized Greedy ForestRandom Forestなどがあります。これらはベースとなる理論は同じですが、最適化手法が違うんです。

最適化手法が違うと、出てくる結果にも差が出てきます。どの手法が良いかは一概には言えないですが、単体として性能が上がりやすいのはXGBoostです。アンサンブル学習をする際に複数の手法を混ぜることで精度が上がりやすくなるので、異なるアルゴリズムをいくつも用いて学習をさせることが多いですね。

XGBoostでは、最初は学習率は大きめの0.1~0.2~0.5くらい、木の数は少なめで固定、他のパラメータの最適に近い範囲を把握していきました。そうして当たりを付けた上で、サーチを精密化していきました。

また、XGBoostにおいてはどの特徴量を読み込むか試行錯誤しながら複数のパターンを試しています。各項目を手作業で入れたり抜いたりしながら、どれくらい精度が変化するかを見ていきました。

最終的には、このコンペにおいてはcalcという名前の付いたカラムは全く使っていません。試しているうちに、無い方が良い結果が出ると分かってきたからです。

cols_binary = [
    'ps_ind_06_bin', 'ps_ind_07_bin', 'ps_ind_08_bin', 'ps_ind_09_bin', 'ps_ind_11_bin', 'ps_ind_12_bin', 'ps_ind_13_bin', 'ps_ind_16_bin', 'ps_ind_17_bin', 'ps_ind_18_bin',
    'ps_car_08_cat',
    ]
cols_category = [
    # 'ps_ind_01', 'ps_ind_14', 'ps_ind_15', 'ps_reg_01', 'ps_reg_02', 'ps_car_15',
    # 'ps_ind_03', 'ps_car_11',
    'ps_ind_02_cat', 'ps_ind_04_cat', 'ps_ind_05_cat',
    'ps_car_01_cat', 'ps_car_02_cat', 'ps_car_03_cat', 'ps_car_04_cat', 'ps_car_05_cat', 'ps_car_06_cat', 'ps_car_07_cat', 'ps_car_09_cat', 'ps_car_10_cat', 'ps_car_11_cat',
    ]
cols_numeric = [
    'ps_ind_01', 'ps_ind_03', 'ps_ind_14', 'ps_ind_15',
    'ps_reg_01', 'ps_reg_02', 'ps_reg_03',
    'ps_car_11', 'ps_car_12', 'ps_car_13', 'ps_car_14', 'ps_car_15',
    ]

▲高際さんの書いたコードより一部抜粋。使用するカラムは最終的にこのようなラインナップになったという。

さらに、LightGBMやRegularized Greedy Forestsも試しました。前者は交差検証ではXGBoostより少し悪い程度でしたが、アンサンブルしたときの精度がそれほど良くなくて、最後まで残りませんでした。後者は交差確認ではLightGBMと同じくらいの精度でしたが、アンサンブルしたときわずかに精度が向上したため採用しました。

最終的に提出したのは、XGBoostとNeural Network、RGFを混ぜてアンサンブル学習させた結果です。

predictions = [
    '../nnet_boost/phase00/mlp1', '../nnet_boost/phase00/mlp2', '../nnet_boost/phase01/mlp1',
    '../nnet_boost/phase02/mlp0', '../nnet_boost/phase02/mlp1', '../nnet_boost/phase12/mlp1', '../nnet_boost/phase12/mlp3',
    '../ae/predictions/xgb4', '../ae/predictions/xgb6a',
    '../xgb/predictions/xgb5b',
    '../xgb/predictions/xgb6a', '../xgb/predictions/xgb6b', '../xgb/predictions/xgb6d', '../xgb/predictions/xgb6e',
    '../xgb/predictions/xgb8b', '../xgb/predictions/xgb8c',
    '../rgf/predictions/rgf_0', '../rgf/predictions/rgf_1', '../rgf/predictions/rgf_2',
    ]

▲高際さんの書いたコードより一部抜粋。アンサンブル学習に使用したモデル群はこちら。

──精度を上げるには、地道な試行錯誤が欠かせないんですね。ちなみに、学習フェーズはとても時間のかかるものかと思いますが、効率化のためにやっていることはありますか?

高際:最初の検討段階においては、全量データを読み込ませるのではなく、一部だけを使って試してみることがよくあります。例えば「Porto Seguro’s Safe Driver Prediction」では全データが約60万件くらいだったんですが、初期フェーズにおいては1万件程のデータだけを使って、それなりに精度が出るかを見ていました。

Kaggle初心者は何から始めるべき?

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く