UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

Kaggleで銀メダルを獲得しました!

こんにちは、UUUMで主にデータ分析業務をしている成松です!

先日(3ヶ月ぐらい前)ソロで参加したElo Merchant Category Recommendationで銀メダルを獲得したので、今回はこちらの振り返りをしたいと思います!


コンペ概要

Eloというブラジルのカード会社が主催したもので、クレジットカードの購買データを使ってRoyalty Scoreを予測するという回帰問題でした。データとしては、各カードの属性が記載された学習用データと検証用データ、そしてカードそれぞれの購買履歴データが記載されたトランザクションデータが主に提供されました。

予測対象であるRoyalty Scoreは、0を中心とした正規分布のような分布と-33付近に外れ値が存在するという分布になっていました。

f:id:n_narimatsu:20190516140107p:plain

評価指標には比較的外れ値の影響を受けやすいRMSEが設定されていたため、外れ値をどう処理するかが非常に重要となりました。

問題設定が分かりやすく評価指標もシンプルで、データサイズもさほど大きくなかったので、初心者でも気軽に参加できたコンペだったのではないかと思います。


スコアの変遷

まず、コンペ終了までに私のスコアがどう変化したのかをご紹介します!

f:id:n_narimatsu:20190517122619p:plain

コンペの序盤 ~ 中盤にかけて運よくスコアを順調に下げることに成功したのですが、後半はほとんど何もできませんでした。


初サブミット!

コンペに参加してすぐにベースとなるモデルを作り始めました。テーブルコンペだったので、深く考えずLightGBMを使いました。また、特徴量はカード属性を表すカテゴリカルデータの他に、購買履歴のデータから購買頻度や金額などを集計し特徴量としてくわえました。

そうして作ったファーストモデルをKaggleに提出したところ、スコアは3.873でした。 この時点で、ほとんどの参加者は3.75 ~ 3.80にいて0.001の精度を競っていました。ですので、この3.873というスコアはめちゃくちゃ低かったです。


Kernelを写経

ファーストモデルを提出した後、このモデルを改良して順位をあげると躍起になっていました。ちょっと特徴量を加えたり、パラメータを調整すれば簡単にスコアが上がるだろうと考えていたのですが、ほとんど改善されませんでした。

そこで、自分のモデルを改良することを諦め、こちらのKernelを写経しました。 自分よりスコアが優れているモデルをそのまま写経したので、当たり前ですがスコアが上がりました!

この時点で順位は上位20%ぐらいだったと思います。


特徴量を大量生産

その後、モデルそのものの改良は一旦ストップし、特徴量を大量生産するフェーズに移行しました。始めのうちはローカルPCでゴリゴリ作っていたのですが、メモリが足りなくなる事や計算処理に時間がかかりすぎる事があり、途中からこちらの動画を参考にBigQueryを使ってました。

最終的には300個ぐらいの特徴量を作ったと思います。その中でも「どのカード(ユーザ)がどのお店で何回購入したか」のマトリックスをNMFで次元圧縮した特徴量はめちゃくちゃ効果的でした!

import pandas as pd
import numpy as np
from scipy.sparse import csr_matrix
from pandas.api.types import CategoricalDtype
from sklearn.decomposition import NMF


N_COMP = 20


# データ読み込み
feats = ["card_id", "merchant_id"]
h_trs = pd.read_csv("../input/historical_transactions.csv", usecols=feats)
h_trs = h_trs.dropna()


# sparse matrixの生成
card_merchant = h_trs.groupby(feats).size().reset_index()
card_merchant.columns = ["card_id", "merchant_id", "count_"]

card_c = CategoricalDtype(sorted(card_merchant.card_id.unique()), ordered=True)
merchant_c = CategoricalDtype(sorted(card_merchant.merchant_id.unique()), ordered=True)

row = card_merchant.card_id.astype(card_c).cat.codes
col = card_merchant.merchant_id.astype(merchant_c).cat.codes
sparse_matrix = csr_matrix((card_merchant["count_"], (row, col)), \
                           shape=(card_c.categories.size, merchant_c.categories.size))


# NMFによる次元削減
model = NMF(n_components=N_COMP, init='random', random_state=0) 
embedded = model.fit_transform(sparse_matrix)

df = pd.DataFrame(embedded, columns=["NMF_comp{}".format(i) for i in range(1, N_COMP+1)])
df["card_id"] = person_c.categories.values


この特徴量と他の300もの特徴量の組み合わせにより、順位が一気に5位まで上がりました!

f:id:n_narimatsu:20190516181823p:plain


アンサンブル

12月中旬あたりぐらいになると、新たな特徴量を作るためのネタが切れてしまいました。
新たな特徴量を作るぐらいしかスコア改善の方法が分からなかったので、1月中旬ぐらいまでかなり伸び悩みました。

悩んだ結果、モデルを複数作ってそれらをアンサンブルしようと決めました。 Kaggleでは、複数のモデルをアンサンブルすることで精度を上げるという手法がよく使われます。各モデルがもつ特有の弱点を他のモデルで補い合うことで精度が向上しやすいのではと思われます。

私の場合、複数個のモデルを作るにあたり下記のようなことを考えました。

  • 外れ値を予測する問題として扱ったらどうか?
  • 外れ値以外は、外れ値なしで学習したモデルの方が精度が良いのではないか?

最終的には2つの学習モデルを新たに作り、合計3つのモデルをアンサンブルしました。

f:id:n_narimatsu:20190517123746p:plain


コンペ終了

3つの学習器によるアンサンブルモデルを作った後も試行錯誤をしたり、raddarさんの神Kernelを参考にしたものの、これ以上スコアが上がることなく3ヶ月間のコンペが終了しました。

Private LBでは多少のShake Downをくらったものの、4129チーム中196位に踏みとどまり銀メダルを獲得しました!!!

f:id:n_narimatsu:20190517120336p:plain


まとめ

Eloコンペは、本気で最後までやりきった初めてのコンペだったので、無事に銀メダルを獲得できてよかったです!日々投稿され続けるKernelやDiscussionをキャッチアップし続けるのは骨が折れましたが、その分多くの学びを得ることができたと思います。今後はテーブルコンペに限らずいろんなコンペに参加して、2019年内にKaggle Masterになることを目標に頑張っていきたいと思います!

www.wantedly.com