Kaggle Masterへの道：後編 ～金メダルまでの軌跡とMLBコンペのソリューション～


# はじめに

こんにちは。(株) 日立製作所の [Lumada Data Science Lab.](https://www.hitachi.co.jp/products/it/lumada/about/ai/ldsl/index.html) の諸橋 政幸です。

2021年9月にKaggleコンペ「MLB Player Digital Engagement Forecasting」にて金メダルを獲得し、念願の「Kaggle Master」に昇格しました！
振り返りも兼ねて、これまでの取り組み内容をまとめました。
**【前編】分析コンペをはじめたきっかけ、国内コンペ参戦からKaggle初参戦までをご紹介**
**【後編】チームメンバ、金メダルチャレンジ、MLBコンペのソリューションをご紹介**

本編は**「後編」** です。

ちなみに、前編を読んでない方は下記リンクから是非読んでみてください。
⇒ **[Kaggle Masterへの道：前編 ～国内コンペ参戦からKaggle初参戦まで～](https://qiita.com/m-morohashi/items/72149edd2598ed2430be)**

<img width=600 alt="image01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323189/04b6a1b2-5fc9-c1f0-43d7-f3052b70d859.png">

# 頼もしいチームメンバ：齊藤拓磨さん

コンペの取り組み説明に入る前に、チームメンバの紹介をします。

**齊藤 拓磨さん (taksai)**
株式会社 PKSHA technology   アルゴリズムソリューション事業部

<img width=700 alt="image02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323189/f1dca789-131d-fdc7-6886-f16595254e63.png">

仕事を通して知り合ったのですが、その仕事が終わってしばらくしてからたまにプライベートで会ったりしてました。そんな中、齊藤さんも分析コンペをやりだしたことを知り、「チームを組んで参加してみないか？」という話に。チーム参加にしたことがなく憧れていたこともあり、組んでみることにしました。

齊藤さんは発想力・アイデアが凄い人で、とてもいい刺激をもらっています。
私の想像してなかった特徴量やアプローチをしてスコアを急激にアップさせたりと頼もしい存在です。
一緒に切磋琢磨してやってこられたことが、今回のKaggleでの金メダル取得につながったと確信しています！

# 金メダルへのチャレンジの軌跡

前編で書いたようにKaggle初参加で銀メダルをゲットしました。次のコンペでも銀が取れて意外にもあっさりとExpertになれました。
Master昇格条件は金1個＋銀2個が必要なので、あと金メダルを1個取ればいいという状態に。

しかし、ここからが大変でした。金と銀には想像以上の壁が。。。
そこで、先ほど紹介した齊藤さんとチームを組み、金メダルを明確に意識してコンペにチャレンジしました。

- **[1stチャレンジ] [NFL Big Data Bowl](https://www.kaggle.com/c/nfl-big-data-bowl-2020)**
    - コンペ概要: NFL（アメリカンフットボールのプロリーグ）において、ランプレイで獲得したヤード数を予測するコンペ
    - 順位： 32位／2,038チーム （銀メダル）
    - チームでのKaggle初参戦。2桁順位で銀。Kaggleでも戦えそうな手応えを得る。
    - NFLをよく知らなかった(ランプレイってなんぞや？というレベル)のでYouTubeで動画を見まくった。スポーツを題材としたコンペって何かワクワクする。
    - 初のコードコンペ(予測値ではなくコードを提出して未来データで評価するコンペ)。ここでの経験は後のMLBコンペに活きる。

- **[2ndチャレンジ] [2019 Data Science Bowl](https://www.kaggle.com/c/data-science-bowl-2019)**
    - コンペ概要: 子供向けゲームアプリにおいて、過去の行動履歴をもとに問題への正答結果（4段階のラベル）を予測するコンペ
    - 順位： 24位／3,493チーム （銀メダル）
    - 対象となるゲームアプリは実際にプレイできたので何度もプレイ。実際やってみると何が効きそうかが分かって特徴量作成に役立った。
    - 過去に行なったプレイ内容が後の結果に影響するので、いろいろな仮説を立てて検証していく過程がかなり楽しかった。

- **[3rdチャレンジ] [Indoor Location & Navigation](https://www.kaggle.com/c/indoor-location-navigation)**
    - コンペ概要: WiFiアクセスポイントのデータやスマートフォンのセンサデータから、ショッピングモール内の位置を推定するコンペ
    - 順位： 18位／1,170チーム （銀メダル）
    - 位置情報の推定というイメージしやすいタスク
    - 後処理がすごい効くコンペだった。その後処理を、コンペ開催中にコード付きで公開していてすごいと思った。
    - 中盤スコアが上がらず苦労したが、相対位置を予測するモデルを追加し、上記公開コードと組合せることで大幅にスコアアップできた。

ここまで3つチャレンジし、徐々に順位が上がっていたものの、金メダル争いには入れてなかったので「やはり金の壁は厚いなー」とあらためて痛感しました。

上位陣の解法を見ていくと、基本的なところは当然押さえつつも、他チームが見つけていない特徴量やモデルを見つけていました。**Discussionや公開Notebookは役に立つけど、金を取るためにはこれを超えるアイデアが必要。** いや、そんなの当たり前でそれを思い付けないから困ってるんだけども！

また、「これ思い付いたけど試さなかったな。。」とか、「これ試したけどうまく行かなかったよ？」みたいなこともありました。**思い付いたら全部試す精神**と、**思い付いたアイデアはうまく行くと思い込んで徹底的に試すこと** が重要なのかなと思いました。

前者は経験とセンスがものを言いそうだけど、後者は心構えと実装力という頑張れば何とかなる領域なので、とにかくここら辺に注意して進めていこうと思いました。

そして4回目のチャレンジ、、、

- **[4thチャレンジ] [MLB Player Digital Engagement Forecasting](https://www.kaggle.com/c/mlb-player-digital-engagement-forecasting)**
    - コンペ概要: MLB（メジャーリーグベースボール）の選手情報や試合結果などから、MLB選手のデジタルエンゲージメントを日次で予測するコンペ
    - 順位：  ？位／ 852チーム
    - 通称「MLBコンペ」。スポーツ系のコンペでデータ項目も理解しやすく比較的取り組みやすいコンペ。
    - とにかく思い付いたアイデアを書き溜めてほぼ全て試す。アイデアが的中し中盤に初の1桁台に。
    - 「コードコンペ」だったので実装面で難航。ここでNFLコンペでの経験が活きる。

このMLBコンペは2021/7/31に終わりましたが、未来データ(2021年8月の1ヶ月分)で評価するため、順位が確定するまで1ヶ月半待ちました。
この間は何もできることはなく、じっと提出したコードとモデルを信じて待つだけ。正直かなりストレスになりました。ずっとそわそわ。
ちなみに7/20時点での順位は4位だったと思います。(7/21～31の間は順位表が機能しないルールだったのでこの期間の順位変動は分かりません)

途中で中間発表が行われたのですが、
１回目の中間発表(8/12)：18位。。。。ぐあーー、マジで終わったと思いました。。
２回目は中間発表(8/26)： 9位。 お！金圏内に入ってる！
そして、、、
３回目の最終発表(9/10)：・・・**８位！！**

これでほぼ確定ですが、この時点ではあくまで暫定順位だったので、最終確定の9/15まではずっとドキドキしてました。
無事8位で金メダルが確定したときはガッツボーズしましたｗ

# MLBコンペ：ソリューション紹介

それぞれのコンペに思い入れがあるのですが、ここでは最後の「MLBコンペ」の解法について説明します。

あらためてコンペの概要を説明すると、MLBの選手情報や試合結果などから、日単位でMLB選手のデジタルエンゲージメントを予測するコンペになります。
デジタルエンゲージメントは4種（target1, target2, target3, target4）で、それぞれ0～100の連続値です。詳細は明かされていなかったのですが、おそらくtwitterやFacebookなどから選手の注目度や人気度を指標化したものではないかと思います。
また、選手情報などの固定のテーブルが4個、試合結果などの日次テーブルが11個あり、カラムは合計で数百はあったと思います。
あとは、処理時間にも制約があり、CPU／GPUのどちらを使ってもいいですが処理時間が6時間以内というルールでした。

### まずはベースラインを作成

とりあえずシンプルにLightGBMを使って回帰モデルを作成しました。目的変数が4個あったのでモデルも4個。
特別な工夫はせず、説明変数も選手情報テーブル1個と、日次テーブル1個だけを使いました。
バリデーションもこの時点ではランダムに5fold-cv。

### ベースライン作成後のアプローチ

Kaggleコンペは基本的に精度を追求すればいいのですが、本コンペのようなコードコンペではスクリプトを提出するため推論処理の実装についても留意する必要があります。
このため、「モデルの精度改善」に加えて、「推論処理のエラー回避」の工夫も盛り込みました。大まかに言うと、以下の6点が解法のポイントになります。

|No| 項目 | 解法ポイント |
|---|---|---|
|1 | モデルの精度改善 | ① 時系列データのvalidation設計 |
| |  | ② multi-task |
| |  | ③ LSTMによるlag-feature利用 |
|2 | 推論処理のエラー回避 | ④ 学習/推論の処理関数共通化 |
| |  | ⑤ くどいほどの例外処理の挿入 |
| |  | ⑥ 検証用ダミーデータ作成と動作テスト |

この1と2のバランスが難しく、実装方法をよく考えないと後半戦になるほど少しの特徴量やモデルの追加で大きなコード改修が必要でした。初期からそれを踏まえて実装したつもりでしたが、それでも後半は実装の困難さを理由に断念・妥協したアイデアもありました。この辺はまだまだ今後の課題だと思ってます。

## 解法ポイント①：時系列データのvalidation設計

オーソドックスに時系列を加味して、リークが起きないように学習データ(train)の後に検証データ(valid)が来るようにしました。
また、最終の評価期間が2021/8だったので、validをMLBシーズン中に限定しました。さらに月によるバラツキを抑制するため、5月/6月/7月の3パターンを用意しました。
さらに、同じvalidでも、trainの期間を変えたものを複数用意することで、アンサンブル時の汎化性能の向上を狙いました。

<img width=700 alt="image01.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323189/033244c7-3a19-7927-8285-a889e52159ed.png">

## 解法ポイント②：multi-task

目的変数は4つありますが、ある選手のtarget1が高いときは、他のtarget2-4も高い傾向が見られました。選手や日によって相関しないケースもありましたが、ざっくり言うと関係はありそうでした。
そこで、モデルをLightGBMからNN(keras)に切り替えて、4つの目的変数を同時に学習させることにしました。いわゆる「multi-task」で学習させました。
通常、テーブルデータだとLightGBMの方がNNよりも精度がよくなることが多いですが、このデータではNNに変えてmulti-taskにするだけで同程度かそれ以上になりました。

## 解法ポイント③：LSTMによるlag-feature利用

target1～4のラグ特徴量(前日や前々日の値)を使ってみたところ、精度がかなり良くなりました。これはDiscussionや公開Notebookでも共有されており、おそらく多くの人が使っていたと思います。
しかし、推論時は前日や前々日の目的変数の値が分からないため、このラグ特徴量を使うには「予測に予測を重ねる」ことになります。
このような再帰的な予測は精度悪化のリスクが高いため、リスクを回避するために使わないことにしました。

その代わり、LSTMを使うことで、シンプルな構造で、目的変数と説明変数のラグ特徴量を間接的にモデルに組み込むことにしました。
このアイデアが効いたおかげでスコアが大きく伸び、一気に順位が1桁台になりました。
ここが一番のブレークポイントでした。

なお、過去何日まで取り込むかについては、当初は1週間(7日)や1ヶ月(30日)がいいと思っていましたが、実験を重ねた結果「5日」としました。
ちなみに、LSTM部分をTransformerに置き換えたモデルも試しましたが、悪くはないけど良くもならなかったので不採用としました。

<img width=900 alt="image03.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323189/b7b98677-1988-be6e-33e0-7eedf5d7924b.png">

```
# define LSTM-model (keras)
def create_network_lstm(len_num,
                        len_cat,
                        timesteps,
                       ):
    ################# numeric
    input_num = Input(shape=(timesteps, len_num, ))
    x_num = TimeDistributed(Dense(300, activation="relu"))(input_num)
    x_num = TimeDistributed(BatchNormalization())(x_num)
    x_num = TimeDistributed(Dropout(0.2))(x_num)
    x_num = TimeDistributed(Dense(200, activation="relu"))(x_num)
    x_num = TimeDistributed(BatchNormalization())(x_num)
    x_num = TimeDistributed(Dropout(0.2))(x_num)
    x_num = TimeDistributed(Dense(200, activation="relu"))(x_num)
    
    ################# categorical
    input_cat = Input(shape=(timesteps, len_cat,))
    flag_first = 0
    for i in np.arange(len_cat):
        tmp_cat = input_cat[:, :, i]
        input_dim = len(dict_set[0][col_cat[i]])+1
        tmp_cat = Embedding(input_dim=input_dim, output_dim=int(input_dim/2))(tmp_cat)
        tmp_cat = Dropout(0.3)(tmp_cat)
        tmp_cat = TimeDistributed(Flatten())(tmp_cat)
        if flag_first==0:
            x_cat = tmp_cat
            flag_first = 1
        else:
            x_cat = Concatenate(axis=2)([x_cat, tmp_cat])
  
    ################# concatenate
    x = Concatenate(axis=2)([x_num, x_cat])

    ################# LSTM-layer
    x = Bidirectional(LSTM(200, return_sequences=True))(x)
    x = Bidirectional(LSTM(150, return_sequences=False))(x)

    ################# MLP
    x = Dense(256, activation="relu")(x)
    x = BatchNormalization()(x)
    x = Dropout(0.15)(x)
    x = Dense(128, activation="relu")(x)
    x = BatchNormalization()(x)
    x = Dropout(0.1)(x)
    x = Dense(64, activation="relu")(x)
    x = BatchNormalization()(x)
    x = Dropout(0.1)(x)
    out = Dense(4, activation="linear")(x)
    
    model = Model(inputs=[input_num, input_cat],
                  outputs=out,
                 )
    return model
```

## 解法ポイント④：学習/推論の処理関数共通化

学習時には数年分をバッチ処理でまとめて処理できますが、推論時は1日ごとの処理になります。テーブル数もカラム数も多く、学習と推論で別々の処理にするとメンテが大変で処理ミスが起きやすい状況でした。
そこで、標準化・欠損値処理・ラベルエンコーダーといった前処理や、テーブルごとの特徴量生成処理を関数化し、学習と推論で共通化しました。

<img width=800 alt="image03.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323189/081a5a16-8d71-3abb-5a58-a4d513bc0ada.png">

## 解法ポイント⑤：くどいほどの例外処理の挿入

「学習データにテーブルが毎日存在するものは推論にも必ず存在する」「学習データに欠損がないデータ項目は推論データにも欠損が無い」「学習データには推論に必要な選手IDが必ずある」「 学習データで選手IDが一意のテーブルは推論データでも一意」ということは必ずしも成立するわけではありません。
推論データがどうなっているかはコンペ参加者は知りようがないので、最悪の場合を想定し、どんなデータが来ても対応できるようにしないといけません。

実際、rostersテーブルは2021/5に1日だけテーブル自体が存在しませんでした。バッチ処理してると気付きませんが、1日ごとにストリーム処理するとテーブルが丸ごと存在しないため実装の仕方によってはエラーになります。
私はそれに気づかず、最初10回連続でサブミットエラーを起こしてサブミットを無駄にしました。。。

しかも、コード実行時のエラーメッセージは参加者に隠蔽されているため何故エラーが起きているのかを気付けない。。。ハマっている人が多くいました。
ただ、この経験から、初期のうちから例外処理を盛り込むだけ盛り込んだので、後半はエラーはほとんどありませんでした。

## 解法ポイント⑥：検証用ダミーデータの作成と動作検証

コンペ開催中は7/20までのデータしか配布されないため、7/21～8/31のデータで動くかどうかは本番の一発勝負となります。
エラーになったら一発退場（スコアレス）になるので、念のためこの期間のダミーデータを作成しました。
事前検証のシミュレータはコンペ参加者が有志で用意してくれていたので、それを利用しました（これは有難かった）。

これらを使うことで、「バッチ処理した場合とストリーム処理した場合で同じ予測値になること」「Kaggle Kernel上と手元のPCで同じ予測値になること」「処理時間が6h以内に収まること」を検証しました。
実際、これで何個か実装ミスが見つかりました（やってよかった）。

--
最終モデルとしては、上記のLSTMモデルに加えて、同じデータセットを使ってMLPモデルと、LigthGBMモデルも追加で作成し、LSTM:MLP:LigthGBM=5:1:1の配合でアンサンブルしました。

以上が「MLBコンペ」のソリューションになります。
あっと驚く解法ではないかもしれませんが、ここに至ったのも数多くの試行錯誤があり、試したけど効果がなかったものが裏に山ほどあります。思い付いたアイデアは都度メモして、ほぼすべてを試しました。とにかく報われてよかったです。

# これからも分析コンペを楽しみたいと思います！

無理だと思っていた金メダルをついに獲得し、「Kaggle Master」に昇格できました。
ここまで来れたのもチームメンバの齊藤さんと切磋琢磨してやってきたおかげだと思います。

次の目標は「Kaggle Grandmaster」ですが、金5個（うちソロ1個）という、とてつもなく険しい条件です。
あくまで趣味なので「楽しんで」やっていきたいなとは思っています。

長くなりましたが最後まで読んでいただき、ありがとうございました！