Pythonでスタッキング学習を実装する【Kaggle】

# TL;DR

機械学習において単体の予測モデルで精度が頭打ちになった際、よく用いられる手法としてスタッキング学習があります。本記事では、過去のKaggleコンペティションである["Otto Group Product Classification Challenge"](https://www.kaggle.com/c/otto-group-product-classification-challenge)を題材にPythonでスタッキングモデルを実装し、マルチクラス分類タスクに挑戦します。

## コンペティション概要

商品データから9つのクラスのいずれに分類されるかを予測するマルチ分類タスクです。
`train.csv`は93個の特徴量と、目的変数である所属クラスのデータが格納されています。`test.csv`の特徴量から、各商品の所属するクラスを確率で予測することが目的です。評価指標にはMulti-Class Log-Lossが用いられます。

![スクリーンショット 2020-10-16 11.35.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342646/88fd0dc0-6bb2-e79e-bd5e-acd3d26e27a6.png)


# 準備

必要なライブラリをインポートします。

```python:In
import os, sys
import datetime
import warnings
warnings.filterwarnings('ignore')

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.neighbors import KNeighborsClassifier

import xgboost as xgb
from xgboost import XGBClassifier
```

## データの読み込み・前処理

```python:In
train = pd.read_csv('data/train.csv')
test = pd.read_csv('data/test.csv')
sample = pd.read_csv('data/sampleSubmission.csv')
```
```python:In
train.head()
```

![スクリーンショット 2020-10-16 10.46.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342646/8af039a2-6b5f-9f58-ba42-f688f78b909e.png)


目的変数の値が文字列になっているので、これを数値に変換します。

```python:In
le = LabelEncoder()
le.fit(train['target'])
train['target'] = le.transform(train['target'])
```

説明変数Xと目的変数yに分離します。XはNumPy配列に変換しておきます。

```python:In
X_train = train.drop(['id', 'target'], axis=1)
y_train = train['target'].copy()
X_test = test.drop(['id'], axis=1)

X_train = X_train.values
X_test = X_test.values
```

提出ファイル作成用に`test`の`id`フィールドを保持しておきます。

```
testIds = test['id'].copy()
```

趣旨から外れるのでここでは割愛しますが、データの分布を見ると値にかなり偏りが存在します。正規化などが良い手段のように思いましたが、試してみたところ最終的な精度に改善が見られなかったため、Row Dataのまま処理を進めることにします。

# 1層目モデルの定義

## モデル全体の構成

全体の構成として、1層目にRandomForestやGradient Boosting, KNNなどといった8つのモデルを定義します。1層目の各モデルの予測値を用いて、2層目のXGBoostによる予測を最終的な予測結果とします。

![スクリーンショット 2020-10-16 12.59.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342646/3a2883ed-b6b7-bde1-5fbb-89ebf92678ee.png)

## 分類器拡張クラスの定義

各1層目モデルに対する操作（定義・訓練・予測）を簡略化するため、分類器の拡張クラスを定義します。

```python:In
class ClfBuilder(object):
    def __init__(self, clf, params=None):
        self.clf = clf(**params)
    
    def fit(self, X, y):
        self.clf.fit(X, y)
    
    def predict(self, X):
        return self.clf.predict(X)
    
    def predict_proba(self, X):
        return self.clf.predict_proba(X)
```

## Out-of-Fold予測関数の定義

スタッキングでは2層目のモデルに1層目のモデルの予測値を利用します。2層目で既知のデータに対する過学習を防ぐため、1層目でOut-of-Foldによる予測値を算出してこれを2層目の学習に利用します。以下の実装では、`StratifiedKFold`で5分割の交差検証を行っています。

```python:In
NUM_CLASSES = 9

def get_base_model_preds(clf, X_train, y_train, X_test):
    print(clf.clf)
    
    N_SPLITS = 5
    oof_valid = np.zeros((X_train.shape[0], NUM_CLASSES))
    oof_test = np.zeros((X_test.shape[0], NUM_CLASSES))
    oof_test_skf = np.zeros((N_SPLITS, X_test.shape[0], NUM_CLASSES))
    
    skf = StratifiedKFold(n_splits=N_SPLITS)
    for i, (train_index, valid_index) in enumerate(skf.split(X_train, y_train)):
        print('[CV] {}/{}'.format(i+1, N_SPLITS))
        X_train_, X_valid_ = X_train[train_index], X_train[valid_index]
        y_train_, y_valid_ = y_train[train_index], y_train[valid_index]
        
        clf.fit(X_train_, y_train_)
        
        oof_valid[valid_index] = clf.predict_proba(X_valid_)
        oof_test_skf[i, :] = clf.predict_proba(X_test)
    
    oof_test[:] = oof_test_skf.mean(axis=0)
    return oof_valid, oof_test
```

## パラメータの設定

`ClfBuilder`関数に渡すパラメータをdict型で設定します。
（※ここではハイパーパラメータチューニング等は行いません）

```python:In
rfc_params = {
    'n_estimators': 100, 
    'max_depth': 10, 
    'random_state': 0, 
}
gbc_params = {
    'n_estimators': 50, 
    'max_depth': 10, 
    'random_state': 0, 
}
etc_params = {
    'n_estimators': 100, 
    'max_depth': 10,
    'random_state': 0, 
}
xgbc1_params = {
    'n_estimators': 100, 
    'max_depth': 10,
    'random_state': 0, 
}
knn1_params = {'n_neighbors': 4}
knn2_params = {'n_neighbors': 8}
knn3_params = {'n_neighbors': 16}
knn4_params = {'n_neighbors': 32}
```

1層目モデルのインスタンスを生成します。

```python:In
rfc = ClfBuilder(clf=RandomForestClassifier, params=rfc_params)
gbc = ClfBuilder(clf=GradientBoostingClassifier, params=gbc_params)
etc = ClfBuilder(clf=ExtraTreesClassifier, params=etc_params)
xgbc1 = ClfBuilder(clf=XGBClassifier, params=xgbc1_params)
knn1 = ClfBuilder(clf=KNeighborsClassifier, params=knn1_params)
knn2 = ClfBuilder(clf=KNeighborsClassifier, params=knn2_params)
knn3 = ClfBuilder(clf=KNeighborsClassifier, params=knn3_params)
knn4 = ClfBuilder(clf=KNeighborsClassifier, params=knn4_params)
```

# 1層目モデルの学習

先程定義した`get_base_model_preds`を利用して各1層目モデルの学習と、2層目モデルの学習・予測に利用する予測値の算出を行います。

```python:In
oof_valid_rfc, oof_test_rfc = get_base_model_preds(rfc, X_train, y_train, X_test)
oof_valid_gbc, oof_test_gbc = get_base_model_preds(gbc, X_train, y_train, X_test)
oof_valid_etc, oof_test_etc = get_base_model_preds(etc, X_train, y_train, X_test)
oof_valid_xgbc1, oof_test_xgbc1 = get_base_model_preds(xgbc1, X_train, y_train, X_test)
oof_valid_knn1, oof_test_knn1 = get_base_model_preds(knn1, X_train, y_train, X_test)
oof_valid_knn2, oof_test_knn2 = get_base_model_preds(knn2, X_train, y_train, X_test)
oof_valid_knn3, oof_test_knn3 = get_base_model_preds(knn3, X_train, y_train, X_test)
oof_valid_knn4, oof_test_knn4 = get_base_model_preds(knn4, X_train, y_train, X_test)
```

```:Out
RandomForestClassifier(max_depth=10, random_state=0)
[CV] 1/5
[CV] 2/5
[CV] 3/5
[CV] 4/5
[CV] 5/5
GradientBoostingClassifier(max_depth=10, n_estimators=50, random_state=0)
[CV] 1/5

(...略...)

[CV] 5/5
KNeighborsClassifier(n_neighbors=32)
[CV] 1/5
[CV] 2/5
[CV] 3/5
[CV] 4/5
[CV] 5/5
```

各分類器の予測結果を横並びに結合したものを、2層目に入力する予測値とします。

```python:In
X_train_base = np.concatenate([oof_valid_rfc, 
                               oof_valid_gbc, 
                               oof_valid_etc, 
                               oof_valid_xgbc1, 
                               oof_valid_knn1, 
                               oof_valid_knn2, 
                               oof_valid_knn3, 
                               oof_valid_knn4, 
                              ], axis=1)
X_test_base = np.concatenate([oof_test_rfc, 
                              oof_test_gbc, 
                              oof_test_etc, 
                              oof_test_xgbc1, 
                              oof_test_knn1, 
                              oof_test_knn2, 
                              oof_test_knn3, 
                              oof_test_knn4, 
                             ], axis=1)
```

# 2層目モデルの定義・学習

2層目モデルとしてXGBoostを利用します。パラメータを設定し、モデルのインスタンスを生成します。

```python:In
xgbc2_params = {
    'n_eetimators': 100, 
    'max_depth': 5, 
    'random_state': 42, 
}
xgbc2 = XGBClassifier(**xgbc2_params)
```

2層目モデルの学習を行います。

```python:In
xgbc2.fit(X_train_base, y_train)
```

# テストデータによる予測

学習した2層目モデルを用いてテストデータによる予測を行います。

```python:In
prediction = xgbc2.predict_proba(X_test_base)
```

予測結果を提出ファイル用のデータフレームに格納します。csv形式で出力し、提出します。

```python:In
columns = ['Class_1', 'Class_2', 'Class_3', 'Class_4', 'Class_5', 'Class_6', 'Class_7', 'Class_8', 'Class_9']
df_prediction = pd.DataFrame(prediction, columns=columns)
df_submission = pd.concat([testIds, df_prediction], axis=1)
```
```python:In
now = datetime.datetime.now()
timestamp = now.strftime('%Y%m%d-%H%M%S')
df_submission.to_csv('output/ensemble_{}.csv'.format(timestamp), index=False)
```

![スクリーンショット 2020-10-16 8.57.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342646/2928110b-7235-81a4-46fb-26be75b40265.png)

結果は`Score=0.43834`となりました。Late SubmissionなのでLeaderboardには載りませんが、載っていれば462/3507位で上位14%の成績でした。

## 1層目各モデルとの精度比較

スタッキングによる効果を確認するため、1層目の各モデルが算出したテストデータに対する予測値のスコアと比較してみましょう。

|Classifier|Score|
|:--|:--|
|Random Forest|0.95957|
|Gradient Boosting|0.49276|
|Extra Trees|1.34781|
|XGBoost-1|0.47799|
|KNN-1|1.94937|
|KNN-2|1.28614|
|KNN-3|0.93161|
|KNN-4|0.75685|

どの単体分類器よりもスタッキングによる予測の方が優れていることが確認できました！
今回は入力データの加工やハイパーパラメータのチューニングを行いませんでしたが、これらを実施することでさらに精度が向上するかもしれません。また、優勝モデルのように2層目を複数の分類器で構成するということも出来そうです。


# 参考文献・URL

* [『Kaggleで勝つデータ分析の技術』](https://www.amazon.co.jp/dp/B07YTDBC3Z)
* [Pythonでアンサンブル(スタッキング)学習 & 機械学習チュートリアル in Kaggle](https://qiita.com/hkthirano/items/2c35a81fbc95f0e4b7c1)
* [KAGGLE ENSEMBLING GUIDE](https://mlwave.com/kaggle-ensembling-guide/)