【Python】勾配ブースティングを実装してみる

# はじめに
本記事ではPythonで勾配ブースティングの実装を行います。データはscikit-learnのサンプルで登録されているデータセットを使用しています。

# 勾配ブースティングとは
勾配ブースティングは教師あり学習の1種です。繰り返し決定木を学習させてモデルの精度を高める手法となっており、アンサンブル学習に含まれます。なお、分類・回帰の両方で使用することができます。

- `アンサンブル学習`
複数のアルゴリズムを用いた学習
- `分類`
データがどのグループに属するかを分類する手法
- `回帰`
連続した数値の予測する手法

## 決定木とは
決定木とはデータを基に樹形図を作成し、予測を行うアルゴリズムです。

- 分類で使用する場合のイメージ

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/c562ddfd-d2ae-3d94-805b-7972489d60e2.png)
　
- 回帰で使用する場合のイメージ

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/06e3f43a-cdff-10dd-0259-7388360ea7e2.png)

## 勾配ブースティングの流れ
勾配ブースティングは以下の流れで行います。

1. 学習用データから決定木を作成する
2. 決定木の予測値と実際の値の誤差を算出する
3. 誤差を小さくする決定木を再作成する
4. 2と4を繰り返しモデルの精度を高める

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/79955775-c003-e7f6-0d29-cb2da4a41309.png)

# Python実行環境
python 3.10.4
Windows 11 Home

# 勾配ブースティング（分類）の実装
乳がんのデータセット「load_breast_cancer」を使用して勾配ブースティングの分類を行います。

## 使用するデータの構造
使用するデータの説明変数と目的変数は以下の通りとなっています。

- 説明変数
`load_breast_cancer`の`dataset.data`を使用します。
データの中身は以下で確認することができます。（乳房塊の微細針吸引物のデジタル画像に含まれる細胞核の特徴が入っている模様）

    ```python:説明変数に使うデータの確認
    import pandas as pd
    from sklearn.datasets import load_breast_cancer
    
    dataset = load_breast_cancer()
    
    # 1.データの行数、列数を確認する
    print(dataset.data.shape)
    
    # 2.データのカラムを確認する
    print(dataset.feature_names)
    
    # 3.データのサンプルを表示する
    sanple_df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
    print(sanple_df.head(10))
    ```
    
    ```:実行結果
    「1.データの行数、列数を確認する」の実行結果（569行30列）
    (569, 30)
    
    「2.データのカラムを確認する」の実行結果
    ['mean radius' 'mean texture' 'mean perimeter' 'mean area'
     'mean smoothness' 'mean compactness' 'mean concavity'
     'mean concave points' 'mean symmetry' 'mean fractal dimension'
     'radius error' 'texture error' 'perimeter error' 'area error'
     'smoothness error' 'compactness error' 'concavity error'
     'concave points error' 'symmetry error' 'fractal dimension error'
     'worst radius' 'worst texture' 'worst perimeter' 'worst area'
     'worst smoothness' 'worst compactness' 'worst concavity'
     'worst concave points' 'worst symmetry' 'worst fractal dimension']
    
    「3.データのサンプルを表示する」の実行結果
    mean radius  mean texture  ...  worst symmetry  worst fractal dimension
    0        17.99         10.38  ...          0.4601                  0.11890
    1        20.57         17.77  ...          0.2750                  0.08902
    2        19.69         21.25  ...          0.3613                  0.08758
    3        11.42         20.38  ...          0.6638                  0.17300
    4        20.29         14.34  ...          0.2364                  0.07678
    5        12.45         15.70  ...          0.3985                  0.12440
    6        18.25         19.98  ...          0.3063                  0.08368
    7        13.71         20.83  ...          0.3196                  0.11510
    8        13.00         21.82  ...          0.4378                  0.10720
    9        12.46         24.04  ...          0.4366                  0.20750
    
    ```
　
- 目的変数
    `load_breast_cancer`の`dataset.target`を使用します。
    データの中身は以下で確認することができます。（悪性か良性かの情報が入っている）
    
    ```python:目的変数に使うデータの確認
    import pandas as pd
    from sklearn.datasets import load_breast_cancer
    
    dataset = load_breast_cancer()
    
    # 1.データの行数、列数を確認する
    print(dataset.target.shape)
    
    # 2.データの対応を確認する
    print(dataset.target_names)
    
    # 3.データのサンプルを表示する(先頭10件は全て0だったため、末尾を表示)
    sanple_df = pd.DataFrame(dataset.target, columns=['乳がん診断結果'])
    print(sanple_df.tail(10))
    ```
    
    ```:実行結果
    「1.データの行数、列数を確認する」の実行結果（569行1列）
    (569,)
    
    「2.データの対応を確認する」の実行結果（0が悪性、1が良性）
    ['malignant' 'benign']
    
    「3.データのサンプルを表示する」の実行結果
         乳がん診断結果
    559        1
    560        1
    561        1
    562        0
    563        0
    564        0
    565        0
    566        0
    567        0
    568        1
    ```

## 作成プログラム
Pythonで勾配ブースティングの分類を実装したプログラムは以下になります。

```python
from sklearn.datasets import load_breast_cancer 
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import xgboost as xgb
import pandas as pd
import numpy as np

def main():
    dataset = load_breast_cancer() #乳がんのデータセットを読み込む
    x_train, x_test, y_train, y_test = split_data_set(dataset)
    calculate_accuracy(dataset, x_train, x_test, y_train, y_test)

#乳がんのデータを学習用とテスト用に分割する
def split_data_set(dataset):
    x = dataset.data #説明変数
    y = dataset.target #目的変数

    #学習用データ（train）を8割、テストデータ（test）を2割に分ける
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, stratify = y)
    
    return x_train, x_test, y_train, y_test 

#勾配ブースティングを使用し、テストデータの正解率を算出する
def calculate_accuracy(dataset, x_train, x_test, y_train, y_test):
    model_XGB = make_model(x_train, y_train, dataset)
    pred_binary = predict_data(x_test, y_test, dataset, model_XGB)
    output_feature_importances(model_XGB)

    #正解データと予測データを比較して正解率を算出する
    score = accuracy_score(y_test, pred_binary)
    print("テストデータの正解率：" + str(score))

#勾配ブースティングを使用しモデルを作る
def make_model(x_train, y_train, dataset):
    dtrain = xgb.DMatrix(x_train, label=y_train, feature_names = list(dataset.feature_names))
    param = {'max_depth': 8,                    #決定木の最大深度
             'objective': 'binary:logistic'     #学習目的
             }
    num_round = 50 #何回学習を行うか
    model_XGB = xgb.train(param, dtrain, num_round)

    return model_XGB

#テストデータにモデルを適用し、悪性か良性かを予測する
def predict_data(x_test, y_test, dataset, model_XGB):
    dtest  = xgb.DMatrix(x_test, label=y_test, feature_names = list(dataset.feature_names))
    pred = model_XGB.predict(dtest)

    #model_XGB.predictは少数で値が返されるため、四捨五入し2値に変換する
    pred_binary = np.where(pred > 0.5, 1, 0)

    return pred_binary

#どの説明変数が重要だったかを出力する
def output_feature_importances(model_XGB):
    fscore_dict = model_XGB.get_fscore()
    feature_names = list(fscore_dict.keys())
    dataMatrix = list(fscore_dict.values())
    df = pd.DataFrame({"説明変数":feature_names, "重要度":dataMatrix})
    df = df.sort_values("重要度", ascending=False)
    print(df)

if __name__ == '__main__':
    main()
```

## 実行結果

- テストデータの正解率
    ```
    テストデータの正解率：0.9824561403508771
    ```
    約98％は正解（悪性のデータを正しく悪性、良性のデータを正しく良性と算出）となりました。よって今回作成したモデルの精度は高いと言えます。また、ランダムフォレストでの予測と比較すると正解率が2％向上したため、今回使用したデータに関してはランダムフォレストよりも勾配ブースティングの方が優れていることが分かりました。
　
- どの説明変数が重要だったかを出力した結果

    | 説明変数 | 重要度  |
    | ---- | ---- |
    | worst texture | 29.0 |
    | worst area | 25.0 |
    | area error | 19.0 |
    | mean concave points | 14.0 |
    | worst concavity | 12.0 |
    | 以降説明変数が続く | 以降重要度が続く |

    `worst texture`が悪性、良性の予測に最も影響していることが分かります。

※`train_test_split`で作られる学習用データとテストデータは毎回変わるため、テストデータの正解率、説明変数の重要度も毎回変化します


# 勾配ブースティング（回帰）の実装
カリフォルニア住宅価格のデータセット「fetch_california_housing」を使用して勾配ブースティングの回帰を行います。

## 使用するデータの構造
使用するデータの説明変数と目的変数は以下の通りとなっています。

- 説明変数
`fetch_california_housing`の`dataset.data`を使用します。
データの中身は以下で確認することができます。（地域をブロックごとに分けて算出した情報が入っている）

    ```python:説明変数に使うデータの確認
    import pandas as pd
    from sklearn.datasets import fetch_california_housing
    
    dataset = fetch_california_housing()
    
    # 1.データの行数、列数を確認する
    print(dataset.data.shape)
    
    # 2.データのカラムを確認する
    print(dataset.feature_names)
    
    # 3.データのサンプルを表示する
    sanple_df = pd.DataFrame(dataset.data, columns=dataset.feature_names)
    print(sanple_df.head(10))
    ```
    
    ```:実行結果
    「1.データの行数、列数を確認する」の実行結果（20640行8列）
    (20640, 8)
    
    「2.データのカラムを確認する」の実行結果
    ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude']
    
    「3.データのサンプルを表示する」の実行結果
       MedInc  HouseAge  AveRooms  AveBedrms  Population  AveOccup  Latitude  Longitude
    0  8.3252      41.0  6.984127   1.023810       322.0  2.555556     37.88    -122.23
    1  8.3014      21.0  6.238137   0.971880      2401.0  2.109842     37.86    -122.22
    2  7.2574      52.0  8.288136   1.073446       496.0  2.802260     37.85    -122.24
    3  5.6431      52.0  5.817352   1.073059       558.0  2.547945     37.85    -122.25
    4  3.8462      52.0  6.281853   1.081081       565.0  2.181467     37.85    -122.25
    5  4.0368      52.0  4.761658   1.103627       413.0  2.139896     37.85    -122.25
    6  3.6591      52.0  4.931907   0.951362      1094.0  2.128405     37.84    -122.25
    7  3.1200      52.0  4.797527   1.061824      1157.0  1.788253     37.84    -122.25
    8  2.0804      42.0  4.294118   1.117647      1206.0  2.026891     37.84    -122.26
    9  3.6912      52.0  4.970588   0.990196      1551.0  2.172269     37.84    -122.25
    
    ```
　
- 目的変数
    `fetch_california_housing`の`dataset.target`を使用します。
    データの中身は以下で確認することができます。（住宅価格の中央値が入っている）
    
    ```python:目的変数に使うデータの確認
    import pandas as pd
    from sklearn.datasets import fetch_california_housing
    
    dataset = fetch_california_housing()

    # 1.データの行数、列数を確認する
    print(dataset.target.shape)
    
    # 2.データの対応を確認する
    print(dataset.target_names)
    
    # 3.データのサンプルを表示する
    sanple_df = pd.DataFrame(dataset.target, columns=['住宅価格の中央値'])
    print(sanple_df.head(10))
    ```
    
    ```:実行結果
    「1.データの行数、列数を確認する」の実行結果（20640行1列）
    (20640,)
    
    「2.データの対応を確認する」の実行結果
    ['MedHouseVal']
    
    「3.データのサンプルを表示する」の実行結果
         住宅価格の中央値
    0       4.526
    1       3.585
    2       3.521
    3       3.413
    4       3.422
    5       2.697
    6       2.992
    7       2.414
    8       2.267
    9       2.611
    ```

## 作成プログラム
Pythonで勾配ブースティングの回帰を実装したプログラムは以下になります。

```python
from sklearn.datasets import fetch_california_housing 
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error
import xgboost as xgb
import pandas as pd

def main():
    dataset = fetch_california_housing() #カリフォルニア住宅価格のデータセットを読み込む
    x_train, x_test, y_train, y_test = split_data_set(dataset)
    check_model_accuracy(dataset, x_train, x_test, y_train, y_test)

#乳がんのデータを学習用とテスト用に分割する
def split_data_set(dataset):
    x = dataset.data #説明変数
    y = dataset.target #目的変数

    #学習用データ（train）を8割、テストデータ（test）を2割に分ける
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
    
    return x_train, x_test, y_train, y_test 

#勾配ブースティングで作成したモデルの精度を確認する
def check_model_accuracy(dataset, x_train, x_test, y_train, y_test):
    model_XGB = make_model(x_train, y_train, dataset)
    pred = predict_data(x_test, y_test, dataset, model_XGB)
    output_feature_importances(model_XGB)

    #正解データと予測データを比較（平均絶対誤差）してモデルの精度を確認する
    score = mean_absolute_error(y_test, pred)
    print("平均絶対誤差：" + str(score))

#勾配ブースティングを使用しモデルを作る
def make_model(x_train, y_train, dataset):
    dtrain = xgb.DMatrix(x_train, label=y_train, feature_names = list(dataset.feature_names))
    param = {'max_depth': 8,                     #決定木の最大深度
             'objective': 'reg:squarederror'     #学習目的
             }
    num_round = 50 #何回学習を行うか
    model_XGB = xgb.train(param, dtrain, num_round)

    return model_XGB

#テストデータにモデルを適用し、住宅価格を予測する
def predict_data(x_test, y_test, dataset, model_XGB):
    dtest  = xgb.DMatrix(x_test, label=y_test, feature_names = list(dataset.feature_names))
    pred = model_XGB.predict(dtest)

    return pred

#どの説明変数が重要だったかを出力する
def output_feature_importances(model_XGB):
    fscore_dict = model_XGB.get_fscore()
    feature_names = list(fscore_dict.keys())
    dataMatrix = list(fscore_dict.values())
    df = pd.DataFrame({"説明変数":feature_names, "重要度":dataMatrix})
    df = df.sort_values("重要度", ascending=False)
    print(df)

if __name__ == '__main__':
    main()
```

## 実行結果

- 平均絶対誤差
    ```
    平均絶対誤差：0.3065048385948936
    ```
    平均絶対誤差は約0.31となりました。 平均絶対誤差は0に近いほど正解データと予測データの誤差が小さくモデルの精度が高いと言えます。そのため今回作成したモデルの精度は高いと言えそうです。また、ランダムフォレストでの予測と比較すると平均絶対誤差が約0.08減少したため、今回使用したデータに関してはランダムフォレストよりも勾配ブースティングの方が優れていることが分かりました。
　
- どの説明変数が重要だったかを出力した結果

    | 説明変数 | 重要度  |
    | ---- | ---- |
    | MedInc | 1670.0 |
    | Latitude | 1212.0 |
    | Longitude | 1146.0 |
    | AveOccup | 1112.0 |
    | HouseAge | 1032.0 |
    | 以降説明変数が続く | 以降重要度が続く |

    `MedInc`（ブロックの所得中央値）が住宅価格の予測に最も影響していることが分かります。

※`train_test_split`で作られる学習用データとテストデータは毎回変わるため、平均絶対誤差、説明変数の重要度も毎回変化します

# 終わりに
Pythonで勾配ブースティングを実装しました。
分類・回帰ともにランダムフォレストより勾配ブースティングの方が良い結果となったため、基本的には勾配ブースティングを優先して使うのが良さそうと感じました。ランダムフォレスト、勾配ブースティングのメリット・デメリットについては別途調査しようと思います。

# 参考
この記事は以下の動画・記事を参考にして執筆しました。

・[Pythonで勾配ブースティング(xgboost)を作ってみよう【Python機械学習#9】](https://youtu.be/1TxeX8KjGuk?si=PLh17JENZcmQH1Ac)

・[勾配ブースティング決定木 - 金融エンジニアリング・グループ](https://www.feg.co.jp/analysis_tech/%E5%8B%BE%E9%85%8D%E3%83%96%E3%83%BC%E3%82%B9%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E6%B1%BA%E5%AE%9A%E6%9C%A8/)

・[xgboostの使い方：irisデータで多クラス分類](https://qiita.com/predora005/items/19aebcf3aa05946c7cf4)

・[xgboost で Feature Importance を算出する。](https://qiita.com/sudachi0114/items/ebabef57bc19d5624682)

・[初心者のXGBoostを使った実装と説明](https://qiita.com/Bacchan/items/8ae978dcee2fc91dde1c)