勾配ブースティング決定木（XGBoost, LightGBM, CatBoost）を実装してみた

* 製造業出身のデータサイエンティストがお送りする記事
* 今回は勾配ブースティング決定木の3つのアルゴリズム（XGBoost, LightGBM, CatBoost）を実装してみました。

##はじめに
###勾配ブースティング木とは
学習器に決定木を用いて、前回の決定木の予測値の誤差を次の決定木の予測値として学習していき、最終的には最初の予測結果と各決定木の予測誤差に重みをかけたものを足し合わせることで推論を行なっていく方法です。

* 学習器：決定木
* アンサンブル学習：ブースティング

![8511b217deea72f7f14ce54385f814aa-png.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743453/a916d7e5-4ebb-c851-8fb8-b50fca0e7a4d.png)

##XGBoost, LightGBM, CatBoostの比較
###枝の成長アルゴリズムについて
XGBoost, LightGBM, CatBoostでは、枝の成長アルゴリズムが異なります。下記に整理してみました。

|手法|枝の成長アルゴリズム概要|
|:---|:---|
|XGBoost|・指定した深さまで枝を成長させます　|
|LightGBM|・分岐毎に枝を成長させます<br/> ・不均衡なツリーを作ります　|
|CatBoost|・指定された深さまで枝を成長させます<br/> ・各深さの分岐の条件式が全て同じ決定木を使用しております<br/> ・ツリー構造が対称になります　|

![1_E006sjlIjabDJ3jNixRSnA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743453/2449285e-0846-8b0f-c392-4f89fd9ae7ee.png)

###分岐作成時のサンプリング方法について
XGBoost, LightGBM, CatBoostでは、分岐作成時のサンプリング方法が異なります。下記に整理してみました。

|手法|分岐作成時のサンプリング方法概要|
|:---|:---|
|XGBoost|・全てのデータを計算します　|
|LightGBM|・エラーが少ない分岐についてランダムサンプリングを行います　|
|CatBoost|・各分岐における各データについてエラーに応じた確率を求めます<br/> ・エラーが多いデータセットのみ計算します　|

###カテゴリ変数の扱いについて
XGBoost, LightGBM, CatBoostでは、カテゴリ変数の扱い方が異なります。下記に整理してみました。

|手法|カテゴリ変数の扱い方概要|
|:---|:---|
|XGBoost|・整数、文字列どちらもカテゴリ変数を処理できません<br/> ・数値のみ処理できます<br/> ・カテゴリ変数は事前にエンコーディングする必要があります　|
|LightGBM|・整数にエンコードされたカテゴリ変数は処理できます<br/> ・文字列のカテゴリ変数は処理できません<br/> ・文字列のカテゴリ変数は整数等へ事前にエンコードする必要があります　|
|CatBoost|・整数、文字列どちらもカテゴリ変数を処理できます　|

##XGBoostの実装
まずは、XGBoostをハイパーパラメータチューニングをせずに実装してみます。
今回もUCI Machine Learning Repositoryで公開されているボストン住宅の価格データを用いて予測モデルを構築します。

```php
# ライブラリーのインポート
import os

import pandas as pd
import numpy as np

import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

# ボストンの住宅価格データ
from sklearn.datasets import load_boston

# 前処理
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

# XGBoost
import xgboost as xgb

# 評価指標
from sklearn.metrics import r2_score
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error

# データセットの読込み
boston = load_boston()

# 説明変数の格納
df = pd.DataFrame(boston.data, columns = boston.feature_names)
# 目的変数の追加
df['MEDV'] = boston.target

# データの中身を確認
df.head()
```

![スクリーンショット 2020-11-09 20.33.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743453/2ffa1a41-98dd-2798-1f6d-8f3385bf5f75.png)

次にXGBoostの事前準備をします。

```php
# ランダムシード値
RANDOM_STATE = 10

# 学習データと評価データの割合
TEST_SIZE = 0.2

# 学習データと評価データを作成
x_train, x_test, y_train, y_test = train_test_split(df.iloc[:, 0:df.shape[1]-1],
                                                    df.iloc[:, df.shape[1]-1],
                                                    test_size=TEST_SIZE,
                                                    random_state=RANDOM_STATE)

# trainのデータセットの2割をモデル学習時のバリデーションデータとして利用する
x_train, x_valid, y_train, y_valid = train_test_split(x_train,
                                                      y_train,
                                                      test_size=TEST_SIZE,
                                                      random_state=RANDOM_STATE)

# XGBoostを利用するのに必要なフォーマットに変換
xgb_train = xgb.DMatrix(x_train, label=y_train, feature_names=boston.feature_names)
xgb_eval = xgb.DMatrix(x_valid, label=y_valid, feature_names=boston.feature_names)
xgb_test = xgb.DMatrix(x_test, label=y_test, feature_names=boston.feature_names)
```

次にハイパーパラメータの設定をします。

```php
# XGBoostのパラメータ設定
params = {
    'boosting_type': 'gbtree',
    'objective': 'reg:squarederror',
    'eval_metric': 'rmse',
    'eta': 0.03,
    'gamma': 0,
    'max_depth': 6,
    'min_child_weight': 1,
    'subsample': 1,
    'colsample_bytree': 1,
    'vervose': 0
}
```

あとは、モデルの学習と予測を行います。

```php
# XGBoost学習

# 学習の経過を保存する箱
evaluation_results = {}
evals = [(xgb_train, 'train'), (xgb_eval, 'eval')]
model = xgb.train(params,
                  xgb_train,
                  num_boost_round=500,
                  evals=evals,
                  evals_result=evaluation_results,
                  early_stopping_rounds=50,
                 )

# XGBoost推論
y_pred = model.predict(xgb_test, ntree_limit=model.best_ntree_limit)

# 評価
def calculate_scores(true, pred):
    """全ての評価指標を計算する

    Parameters
    ----------
    true (np.array)       : 実測値
    pred (np.array)       : 予測値

    Returns
    -------
    scores (pd.DataFrame) : 各評価指標を纏めた結果

    """
    scores = {}
    scores = pd.DataFrame({'R2': r2_score(true, pred),
                          'MAE': mean_absolute_error(true, pred),
                          'MSE': mean_squared_error(true, pred),
                          'RMSE': np.sqrt(mean_squared_error(true, pred))},
                           index = ['scores'])
    return scores

scores = calculate_scores(y_test, y_pred)
print(scores)
```

出力結果は下記のようになります。

```
              R2       MAE        MSE      RMSE
scores  0.885371  2.606647  11.987953  3.462362
```

##LightGBMの実装
LightGBMは下記に実装サンプルがありますので、そちらの記事を参考にしてください。

* [LightGBMのハイパーパラメータチューニング（Optuna）をしてみた](https://qiita.com/DS27/items/ec6a747977e57bb1837e)

##CatBoostの実装
最後に、CatBoostを実装します。

```php
# ライブラリーのインポート
import os

import pandas as pd
import numpy as np

import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

# ボストンの住宅価格データ
from sklearn.datasets import load_boston

# 前処理
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

# CatBoost
import catboost as  cb
from catboost import CatBoost, Pool

# 評価指標
from sklearn.metrics import r2_score
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error

# データセットの読込み
boston = load_boston()

# 説明変数の格納
df = pd.DataFrame(boston.data, columns = boston.feature_names)
# 目的変数の追加
df['MEDV'] = boston.target

# データの中身を確認
df.head()
```

![スクリーンショット 2020-11-09 20.33.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743453/2ffa1a41-98dd-2798-1f6d-8f3385bf5f75.png)

次にCatBoostの事前準備をします。

```php
# ランダムシード値
RANDOM_STATE = 10

# 学習データと評価データの割合
TEST_SIZE = 0.2

# 学習データと評価データを作成
x_train, x_test, y_train, y_test = train_test_split(df.iloc[:, 0:df.shape[1]-1],
                                                    df.iloc[:, df.shape[1]-1],
                                                    test_size=TEST_SIZE,
                                                    random_state=RANDOM_STATE)

# trainのデータセットの2割をモデル学習時のバリデーションデータとして利用する
x_train, x_valid, y_train, y_valid = train_test_split(x_train,
                                                    y_train,
                                                    test_size=TEST_SIZE,
                                                    random_state=RANDOM_STATE)

# CatBoostを利用するのに必要なフォーマットに変換
cb_train = Pool(x_train, y_train)
cb_eval = Pool(x_valid, y_valid)
cb_test = Pool(x_test, y_test)
```

次にハイパーパラメータの設定をします。

```php
# CatBoostのパラメータ設定
params = {
    'loss_function': 'RMSE',
    'num_boost_round': 1000,
    'early_stopping_rounds': 20
}
```

あとは、モデルの学習と予測を行います。

```php
# CatBoost学習

# 学習の経過を保存する箱
model = CatBoost(params)
model.fit(cb_train, eval_set=[cb_eval], verbose=False)

# CatBoost推論
y_pred = model.predict(cb_test)

# 評価
def calculate_scores(true, pred):
    """全ての評価指標を計算する

    Parameters
    ----------
    true (np.array)       : 実測値
    pred (np.array)       : 予測値

    Returns
    -------
    scores (pd.DataFrame) : 各評価指標を纏めた結果

    """
    scores = {}
    scores = pd.DataFrame({'R2': r2_score(true, pred),
                          'MAE': mean_absolute_error(true, pred),
                          'MSE': mean_squared_error(true, pred),
                          'RMSE': np.sqrt(mean_squared_error(true, pred))},
                           index = ['scores'])
    return scores

scores = calculate_scores(y_test, y_pred)
print(scores)
```

出力結果は下記のようになります。

```
              R2       MAE        MSE      RMSE
scores  0.875324  2.601039  13.038736  3.610919
```

##さいごに
最後まで読んで頂き、ありがとうございました。
今回は勾配ブースティング決定木の3つのアルゴリズム（XGBoost, LightGBM, CatBoost）を実装してみました。

訂正要望がありましたら、ご連絡頂けますと幸いです。