エンジニアHubproduced by エン

若手Webエンジニアのための情報メディア

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

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

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

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

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

高際 睦起(たかぎわ・むつき)
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が使っている定番のものです。このツールは欠損値を欠損のまま入力に使えるため、このコンペではそれほど深く考えずにデータを突っ込みました。

また、このコンペではロジスティック回帰や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】予測モデルの作成

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

高際:時系列順に話すと、まずはロジスティック回帰を試しました。これは、どんなコンペでもとりあえず最初に試す定番の手法です。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初心者は何から始めるべき?

──「Kaggleをやってみたいけれど、何から始めたらいいのか分からない」という読者もいると思います。おすすめのトレーニング方法があれば、ぜひ教えてください。

高際:日本のKagglerが参加している「kaggler-ja」というSlackがあるので、まずはそれに入ってみるといいと思います。このSlackは用途ごとに複数のチャンネルに分かれていて、初心者が質問できるようなチャンネルもあれば、コンペ終了後に感想戦をやっているチャンネルもあり、有益な議論が交わされています。

初心者向けチャンネルでは初歩的な質問に対しても丁寧に答えてもらえますし、感想戦のチャンネルはかなりテクニカルな話が出てくるので一定以上のKaggleのスキルを持った方にも参考になるはずです。

f:id:blog-media:20180822104941p:plain▲「kaggler-ja」では数多くのKagglerたちが活発な議論を交わしている。各チャンネルをのぞいてみると、その熱量の高さに驚かされる。

それから情報収集という意味で言うと、各コンペのKernels*8やDiscussion*9を読んでみるのも良いです。有用な情報に溢れているので、それらを見ながら試せばたくさんのことを理解できると思います。

すでに終了している過去のコンペを、公開されているコードを参考にしながら解いてみるのも良い上達法です。上位入賞者のソースコードを見ながら、自分が書いたものと何が違うのかを見比べてみてください。そうしながら、上級者が何を考えてコードを書いているのかを学んでいけばいいと思います。気負わずいろいろなコンペに手を出してみて、少しずつ自分のスキルを上げていってください。

データサイエンティストを目指す若き人たちへ

──最後に、これからデータサイエンティストになりたいと思っている方々へメッセージをお願いします!

高際:これは主に学生に対して伝えたいことですが、データサイエンティストは基礎知識とテクニックのバランスがとても重要です。学生時代は、なるべく基礎知識の学習に時間をかけた方がいいと思います。

──ここでいう基礎知識とは、例えばどのようなものを指しますか?

高際:例えば数学的な知識です。多次元の概念や行列演算、確率などはデータサイエンティストの業務のなかで必ず使います。それらは感覚的に理解できるようになっておいた方がいいんじゃないでしょうか。

──高際さんはどのようにして、数学的な知識を身に付けたのですか?

高際:僕は大学時代に物理を専攻していて、機械学習を学んだのは働き出してからです。物理から得た知識やスキルは、業務のなかで非常に生きています。数学的な考え方と機械学習の知識の両方がバランスよくあったからこそ、いまの仕事ができているという部分はあるかもしれないですね。

──長期にわたり、普遍的に活かせるようなスキルを身に付けておくことが大事なんですね。今回は本当にためになるお話をありがとうございました!

取材・執筆:中薗昴/写真:鈴木智哉

技術監修:田中一樹(株式会社ディー・エヌ・エー)

関連記事

*1:Kaggleのコンペで5つのゴールドメダル(そのうち最低1つはソロゴールド。それ以外はチームでも可)を獲得した者に与えられる称号。

*2:「Porto Seguro’s Safe Driver Prediction」を出題したのは、ブラジルの保険会社であるPorto Seguro。同社は約20年間にわたりビジネスに機械学習を活用してきたが、より優れた分析モデルをKagglerに考案してもらうべく、コンペの主催を決めたのだという。

*3:Kaggleではコンペごとに企業や研究者の持つデータがCSVファイルなどの形式で提供される(最近は画像・動画データなどが提供されるケースも増えてきている)。そのデータを使用し、データサイエンティストたちは分析を進める。

*4:Kaggleでは提供されるデータが必ずしも完全な状態のものばかりではなく、所々“歯抜け”の状態になっているものもある。これを欠損値という。この値をどのように処理するかに、データサイエンティストの技量が問われる。学習の前段階でデータを整備する工程は「前処理」と呼ばれ、分析精度を上げるために極めて重要な作業だ。

*5:いくつかのカテゴリで示せるような変数のこと。例えば血液型や都道府県など。

*6:連続した数値で示せるような変数のこと。例えば身長や年収など。

*7:木の枝のようにフィーチャーを分割することにより分岐を増やし、対象を分類するアルゴリズム

*8:各データサイエンティストたちが作成した予測モデルのコードや解説が公開される機能。

*9:世界中のデータサイエンティストたちとコンペに関する議論やコミュニケーションができる機能。