勾配ブースティング③ (ランダムサーチ)

ランダムサーチ

ランダムサーチは決められた候補値のランダムな組み合わせを決められた回数だけ検証し、その限られた回数の中で最も良い評価を得たパラメータの組み合わせを導き出す手法です。

グリッドサーチの欠点である計算コストの高さを検証回数を制限することで解消した一方、パラメータ全ての組み合わせを検証するわけではないので、必ずしも最適な組み合わせを見つけることができない点が特徴になります。

ランダムサーチ用のモデル構築

ランダムサーチを行うには、scikit-learnのRandomizedSearchCVクラスを使用します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xgb_reg_random = xgb.XGBRegressor()
from sklearn.model_selection import RandomizedSearchCV

params = {"booster": ["gbtree"],
"n_estimators":[10,30,50,100],
"max_depth":[2, 3, 4, 5, 6],
"learning_rate":[0.1,0.25,0.5,0.75,1.0],
"colsample_bytree":[0.1,0.25, 0,5, 0.75, 1.0],
"random_state":[0]}

k_fold = KFold(n_splits=5, shuffle=True, random_state=0)
random = RandomizedSearchCV(estimator=xgb_reg_random,param_distributions=params,scoring="r2",cv=k_fold,n_iter=30,random_state=0)

random.fit(X_train,y_train)

n_iterにはランダムサーチの検証回数を指定します。(12行目)

[実行結果(一部)]

グリッドサーチよりもかなり速く処理を完了させることができました。

ランダムサーチの結果

ランダムサーチで最も評価が高かった組み合わせとそのスコアを出力します。

[Google Colaboratory]

1
2
print(random.best_params_)
print(random.best_score_)

[実行結果]

グリッドサーチではスコアが0.89でしたが、ランダムサーチではスコアが0.88と少し精度が落ちてしまっています。

今回のランダムサーチは最適なパターンまでたどり着かなかったということになります。

テストデータでの評価

最後に、テストデータを使った評価を行います。

[Google Colaboratory]

1
2
3
4
5
y_test_pred = random.predict(X_test)
y_test_pred = np.expand_dims(y_test_pred, 1)

print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

R2スコアが0.82以上となっており、グリッドサーチの0.80より良い結果となります。

最終評価にはパラメータチューニングに関与していないテストデータを使用しているので、グリッドサーチよりランダムサーチの方が良い結果となることは十分にありえます。

この点を踏まえて、どの方法でパラメータチューニングを行うかはケースによって使い分ける必要があります。

勾配ブースティング② (グリッドサーチ)

グリッドサーチ

XgBoostには、10種類を超えるハイパーパラメータがあります。

最適なパラメータの探索を効率的に行う手法として、グリッドサーチがあります。

グリッドサーチとは、あらかじめパラメータの候補値を定義しておき、それら候補値の組み合わせを全通り検証し、最も良い評価結果を出した組み合わせがどれだったのかを調べる手法です。

グリッドサーチ用のモデル構築

グリッドサーチを行うには、scikit-learnのGridSearchCVクラスを使用します。

GridSearchCVクラスは、ハイパーパラメータのそれぞれの組み合わせを交差検証法で評価し、最も評価の高かった組み合わせでモデルを学習してくれるクラスです。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xgb_reg_grid = xgb.XGBRegressor()
from sklearn.model_selection import GridSearchCV

params = {"booster": ["gbtree"],
"n_estimators":[10,30,50,100],
"max_depth":[2, 3, 4, 5, 6],
"learning_rate":[0.1,0.25,0.5,0.75,1.0],
"colsample_bytree":[0.1,0.25, 0,5, 0.75, 1.0],
"random_state":[0]}

k_fold = KFold(n_splits=5, shuffle=True, random_state=0)
grid = GridSearchCV(estimator=xgb_reg_grid,param_grid=params,cv=k_fold,scoring="r2")

grid.fit(X_train,y_train)

指定したXgBoostのパラメータの意味は以下の通りです。(4~9行目)

  • booster
    決定木系モデルか線形モデルのどちらかを指定する。
  • n_estimators
    生成する決定木の数。
    ランダムフォレストでは決定木の数を増やして平均をとるので精度面に影響はなかったが、XgBoostの場合は決定木の数を増やすほどモデルが複雑になり過学習のリスクが高まるので注意。
  • max_depth
    決定木の層の最大の深さ。
  • learning_rate
    学習率。
    以前の決定木の誤りをどれだけ強く補正するかを指定する。
    補正を強くしすぎるとモデルが複雑になり、過学習のリスクが高まる。
  • colsample_bytree
    各決定木で使用する説明変数の割合。
    1未満に指定すると、その割合だけランダムに選択された説明変数を使用する。

指定したGridSearchCVのパラメータの意味は以下の通りです。(12行目)

  • estimator
    検証で使用するモデル。
  • param_grid
    パラメータ名と値の一覧。
  • cv
    交差検証でのデータセットの分割方法
  • scoring
    評価手法。

定義したgridにデータをfitすることで、グリッドサーチが開始されます。(13行目)

[実行結果(一部)]

ハイパーパラメータの全組み合わせ × 交差検証の分割数だけ学習・評価が行われるため、計算に時間がかかります。

グリッドサーチの結果

どの組み合わせが最適だったのか、結果を確認します。

最も評価が高かった組み合わせとそのスコアを出力します。

[Google Colaboratory]

1
2
print(grid.best_params_)
print(grid.best_score_)

[実行結果]

交差検証でのスコアが0.89ととても良い評価となりました。

テストデータでの評価

最後に、テストデータを使った評価を行います。

[Google Colaboratory]

1
2
3
4
5
y_test_pred = grid.predict(X_test)
y_test_pred = np.expand_dims(y_test_pred, 1)

print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

R2スコアが0.8以上となっており、こちらもかなりよい結果です。

今回は実質4種類のパラメータの最適な組み合わせを調べましたが、XgBoostにはこの他にも多くのハイパーパラメータがあります。

それぞれのパラメータの意味を確認し、グリッドサーチを試してみることでよりよい結果にすることができるかもしれません。

勾配ブースティング① (モデルの構築・評価)

勾配ブースティング決定木は、ランダムフォレストと同じように、アンサンブル法を採用しているアルゴリズムです。

ランダムフォレストが複数の決定木を並列に扱い平均を求める手法だったのに対して、勾配ブースティングでは逐次的に決定木を生成します。

具体的に説明すると、1つ前の決定木の誤りを修正して次の決定木を生成するということを繰り返していきます。

高い精度を誇る一方、ハイパーパラメータ設定の影響を受けやすいため、パラメータの調整には注意が必要です。

勾配ブースティング決定木の代表的なものは次の2つです。

  • XgBoost
  • LightGBM

モデルの構築・評価

勾配ブースティング決定木のモデルを構築し、評価を行います。

評価方法として、k分割交差検証法を使用します。

[Google Colaboratory]

1
2
3
4
5
import xgboost as xgb

xgb_reg = xgb.XGBRegressor(random_state=0)
k_fold = KFold(n_splits=5, shuffle=True, random_state=0)
xgb_scores = cross_val_score(xgb_reg, X, y, cv=k_fold, scoring="r2")

[実行結果]

精度評価スコア

精度評価スコアを表示します。

[Google Colaboratory]

1
2
print(f"各分割のスコア:{xgb_scores}")
print(f"平均スコア:{np.mean(xgb_scores)}")

[実行結果]

平均スコアが0.83ととても良い精度となりました。

次回はより高い精度を目指して、ハイパーパラメータの調整を行います。

ランダムフォレスト② (決定木の数を変更)

ハイパーパラメータで生成する決定木の数を変更してみます。

決定木の数を変更

n_estimatorsの値を10→3に変更し、ランダムフォレストのモデルを構築します。

[Google Colaboratory]

1
rf_change_param = RandomForestRegressor(n_estimators=3, max_depth=20, random_state=0).fit(X_train,y_train)

[実行結果]

残差プロット (n_estimators=3)

予測値を算出し、残差プロットを表示します。

[Google Colaboratory]

1
2
3
4
5
6
7
y_train_pred = rf_change_param.predict(X_train)
y_test_pred = rf_change_param.predict(X_test)

y_train_pred = np.expand_dims(y_train_pred, 1)
y_test_pred = np.expand_dims(y_test_pred, 1)

residual_plot(y_train_pred, y_train, y_test_pred, y_test)

[実行結果]

決定木の数が10本のときとあまり変化は見られません。

精度評価スコア (n_estimators=3)

精度評価スコアを表示します。

[Google Colaboratory]

1
2
3
4
print("訓練データスコア")
get_eval_score(y_train,y_train_pred)
print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

決定木の数が10本(前回記事)のときとほぼ変わらない結果となっています。

基本的に決定木の数を増やしていくほど精度は上がっていきますが、その精度向上は徐々に収束していきます。

また、ランダムフォレストモデルでは対象データが膨大になったり、決定木の数や深さを増やすほど、処理時間が延びCPU負荷も増えますので注意が必要です。

交差検証法

交差検証法とは、テストデータと訓練データを入れ替えて複数回学習・評価を行い、それぞれのスコアの平均値をとって最終的なモデルの評価を決定する手法です。

(一方、データセットをあらかじめ訓練データとテストデータに分割する手法はホールドアウト法と呼ばれます。)

今回は交差検証法の手法の中でも、最もメジャーなK分割交差検証法を行ってみます。

K分割交差検証法

交差検証での分割方法を定義するために、scikit-learnのKFoldクラスを使用します。

KFoldのn_splitでデータの分割数を指定しています。(5行目)

またshuffleをTrueに指定すると、分割を行う前にデータセット内のデータ順序をシャッフルすることができます。

[Google Colaboratory]

1
2
3
4
5
6
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import KFold

rf_cv = RandomForestRegressor(n_estimators=3, max_depth=5, random_state=0)
k_fold = KFold(n_splits=5, shuffle=True, random_state=0)
rf_scores = cross_val_score(estimator=rf_cv, X=X, y=y, cv=k_fold, scoring="r2")

cross_val_scoreが交差検証を行う関数です。(6行目)

パラメータの意味は次の通りです。

  • estimator
    交差検証で使用するモデル
  • X
    fitするデータ(説明変数)
  • y
    fitするデータ(目的変数)
  • cv
    交差検証でのデータセットの分割方法
  • scoring
    評価手法

[実行結果]

精度評価スコア

精度評価スコアを確認します。

cross_val_scoreは、交差検証で得られたスコアを返します。

[Google Colaboratory]

1
2
print(f"各分割のスコア:{rf_scores}")
print(f"平均スコア:{np.mean(rf_scores)}")

[実行結果]

平均スコアは0.77となりました。

今回の交差検証では5回検証を行いましたが、各分割でのスコアの幅は0.66~0.86と広くなっています。

ホールドアウト法による1回の分割だけで、モデルを評価するのは不十分であることがよくわかります。

ランダムフォレスト① (構築・評価)

ランダムフォレスト

ランダムフォレストとは、決定木を複数生成して、それぞれの結果を総合的に判断して学習を行うアルゴリズムです。

単純な決定木ではテストデータに対して比較的高精度で予測はできていましたが、過学習に陥りやすいという問題がありました。

その問題を解消するためにランダムフォレストでは、それぞれの決定木で過学習が起きてしまうことを前提に異なる決定木モデルを複数生成し、それぞれの結果の平均をとることで、予測精度は保ちつつ過学習を抑制しようという仕組みとなっています。

複数のモデルを組み合わせて、高精度なモデルを作る手法のことをアンサンブル法と言います。

ランダムフォレストモデル構築

ランダムフォレストモデルの構築を行います。

scikit-learnRandomForestRegressorクラスを使用します。

[Google Colaboratory]

1
2
3
from sklearn.ensemble import RandomForestRegressor

rf = RandomForestRegressor(n_estimators=10, max_depth=20, random_state=0).fit(X_train,y_train)

n_estimatorsは、ランダムフォレストのハイパーパラメータの1つで、生成する決定木の数を指定します。(3行目)

[実行結果]

残差プロット

予測値を算出し、結果を残差プロットで表示します。

[Google Colaboratory]

1
2
3
4
5
6
7
y_train_pred = rf.predict(X_train)
y_test_pred = rf.predict(X_test)

y_train_pred = np.expand_dims(y_train_pred, 1)
y_test_pred = np.expand_dims(y_test_pred, 1)

residual_plot(y_train_pred, y_train, y_test_pred, y_test)

[実行結果]

一部大きく外れているものもありますが、かなり良く予測できています。

精度評価スコア

最後に精度評価スコアを表示します。

[Google Colaboratory]

1
2
3
4
print("訓練データスコア")
get_eval_score(y_train,y_train_pred)
print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

テストデータのR2スコアが0.75となり、単純な決定木データよりは良い結果となりました。

また、単純な決定木でmax_depthを20にした場合は過学習に陥っていましたが、ランダムフォレストでは10本の決定木の平均をとっているため、過学習には陥っていません。

決定木モデル⑤ (最小サンプル数を変更)

今回は、リーフノードのサンプル数を変更してみます。

最小サンプル数変更

リーフノードの最小サンプル数を変更するためには、min_samples_leafを設定します。

min_samples_leafのデフォルト値は1となっており、ノード数が1になるまで分岐をし続けることを意味します。

深さ(max_depth)は前回過学習に陥った20としたままで、最小サンプル数(min_samples_leaf)1から5に変更してみます。(1行目)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
tree_reg_samples_5 = DecisionTreeRegressor(max_depth=20, min_samples_leaf=5,random_state=0).fit(X_train,y_train)

y_train_pred = tree_reg_samples_5.predict(X_train)
y_test_pred = tree_reg_samples_5.predict(X_test)

y_train_pred = np.expand_dims(y_train_pred, 1)
y_test_pred = np.expand_dims(y_test_pred, 1)

residual_plot(y_train_pred, y_train, y_test_pred, y_test)

[実行結果]

中央下にいくつか外れ値がありますが、誤差の範囲が±10程度に集まっていてなかなかの結果になっていると思います。

また訓練データの誤差も適度にばらついていて、過学習の傾向が減っているようです。

精度評価スコア (max_depth=20, min_samples_leaf=5)

精度評価スコアを算出します。

[Google Colaboratory]

1
2
3
4
print("訓練データスコア")
get_eval_score(y_train,y_train_pred)
print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

リーフノードの最小サンプル数を5に制限したことで、テストデータのR2スコアが0.71となりこれまでで最も良い結果となりました。

また、訓練データのR2スコアが1.0から0.91となり、過学習傾向が軽減されていることが分かります。

決定木は過学習に陥りやすい傾向はありますが、深さや最小サンプル数などのハイパーパラメータを調整することで過学習をある程度抑えることができ、モデルの精度改善を行うことができます。

決定木モデル④ (深さを変更)

決定木の深さを定義するハイパーパラメータであるmax_depthを変更します。

深さの変更 (max_depth=5)

決定木の深さ(max_depth)を5に変更してみます。

[Google Colaboratory]

1
tree_reg_depth_5 = DecisionTreeRegressor(max_depth=5, random_state=0).fit(X_train,y_train)

次に予測値を出力し、残差プロットで可視化します。

[Google Colaboratory]

1
2
3
4
5
6
7
y_train_pred = tree_reg_depth_5.predict(X_train)
y_test_pred = tree_reg_depth_5.predict(X_test)

y_train_pred = np.expand_dims(y_train_pred, 1)
y_test_pred = np.expand_dims(y_test_pred, 1)

residual_plot(y_train_pred, y_train, y_test_pred, y_test)

[実行結果]

深さを5に変更したことにより、誤差のばらつきが少し小さくなりました。

精度評価スコア (max_depth=5)

精度評価スコアを表示します。

[Google Colaboratory]

1
2
3
4
print("訓練データスコア")
get_eval_score(y_train,y_train_pred)
print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

テストデータのR2スコアが0.7となり、精度が改善しました。

しかし、訓練データのR2スコアも0.85から0.92と上昇しています。

このように決定木の深さを深くするとモデルの精度が上がる一方、訓練データに過度に適合する過学習のリスクも上がってしまいます。

深さの変更 (max_depth=20)

今度は、決定木の深さ(max_depth)を20に変更してみます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
tree_reg_depth_20 = DecisionTreeRegressor(max_depth=20, random_state=0).fit(X_train,y_train)

y_train_pred = tree_reg_depth_20.predict(X_train)
y_test_pred = tree_reg_depth_20.predict(X_test)

y_train_pred = np.expand_dims(y_train_pred, 1)
y_test_pred = np.expand_dims(y_test_pred, 1)

residual_plot(y_train_pred, y_train, y_test_pred, y_test)

[実行結果]

精度評価スコア (max_depth=20)

精度評価スコアを表示します。

[Google Colaboratory]

1
2
3
4
print("訓練データスコア")
get_eval_score(y_train,y_train_pred)
print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

訓練データのR2スコアは1.0となり完全に適合していますが、テストデータのR2スコアは深さ5のときよりも下がっています。

これは過学習に陥っていると判断できます。

決定木モデル③ (評価)

前回構築した決定木モデルの評価を行います。

予測値算出

まずは予測値を算出します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
y_train_pred = tree_reg.predict(X_train)
y_test_pred = tree_reg.predict(X_test)

import numpy as np

y_train_pred = np.expand_dims(y_train_pred, 1)
y_test_pred = np.expand_dims(y_test_pred, 1)

print(len(y_train_pred))
print(y_train_pred[:5])
print(len(y_test_pred))
print(y_test_pred[:5])

[実行結果]

可視化

散布図で予測値を可視化します。

[Google Colaboratory]

1
2
3
4
5
6
7
plt.scatter(y_train_pred, y_train, label="train")
plt.scatter(y_test_pred, y_test, label="test")
plt.xlabel("Pred")
plt.ylabel("True")
plt.title("Scatter Plot")
plt.legend()
plt.show()

[実行結果]

線形回帰とは異なる分布になっています。

グラフからは、何種類かの特定の値が予測値として出力されているようです。

予測値として出力される値のパターンはリーフノードの数に依存します。

前回出力した樹形図のリーフノード数は8個だったので、予測値のパターンも8種類ということになります。

残差プロット

次に実測値と予測値の誤差をプロットしてみます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
def residual_plot(y_train_pred, y_train, y_test_pred, y_test):
plt.scatter(y_train_pred, y_train_pred - y_train, label="train")
plt.scatter(y_test_pred, y_test_pred - y_test, label="test")
plt.plot([0, 50], [0,0] ,color="red")
plt.xlabel("Pred")
plt.ylabel("Pred - True")
plt.title("Residual Plot")
plt.legend()
plt.show()

residual_plot(y_train_pred, y_train, y_test_pred, y_test)

[実行結果]

右側に外れ値がいくつかありますが、誤差の範囲が±10程度と、まずまずの結果となっていると思います。

精度評価スコア

精度評価スコアを算出します。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np

def get_eval_score(y_true,y_pred):
mae = mean_absolute_error(y_true,y_pred)
mse = mean_squared_error(y_true,y_pred)
rmse = np.sqrt(mse)
r2score = r2_score(y_true,y_pred)

print(f" MAE = {mae}")
print(f" MSE = {mse}")
print(f" RMSE = {rmse}")
print(f" R2 = {r2score}")

print("訓練データスコア")
get_eval_score(y_train,y_train_pred)
print("テストデータスコア")
get_eval_score(y_test,y_test_pred)

[実行結果]

R2スコアが0.66であり、十分な精度となっていません。

次回は、ハイパーパラメータの設定で決定木の深さを変更し、スコアの向上を図ります。

決定木モデル② (データ準備・決定木モデル構築)

決定木モデルを構築するための、データを準備します。

データ準備

ボストンの住宅価格データを読み込みます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
from sklearn.datasets import load_boston
boston = load_boston()

import pandas as pd
df = pd.DataFrame(boston.data,columns=boston.feature_names)
df["MEDV"] = boston.target

display(df.head())

[実行結果]

説明変数を変数Xに、目的変数を変数yに代入します。

[Google Colaboratory]

1
2
3
4
5
X = df[boston.feature_names]
y = df[["MEDV"]]

display(X.head())
display(y.head())

[実行結果]

訓練データと検証データを7対3の割合で分割します。

[Google Colaboratory]

1
2
3
4
5
6
7
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3,random_state=0)

print(len(X_train))
display(X_train.head())
print(len(X_test))
display(X_test.head())

[実行結果]

以上で、データの準備は完了です。

決定木モデルの構築

決定木モデルを構築するには、scikit-learnDecisionTreeRegressorクラスを使用します。

max_depthは決定木の層の深さの上限を設定するパラメータで、今回は3階層(max_depth=3)の決定木にしています。

[Google Colaboratory]

1
2
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor(max_depth=3, random_state=0).fit(X_train,y_train)

これで決定木モデルの構築と学習が完了しました。

決定木モデルの描画

学習により生成された決定木を描画します。

[Google Colaboratory]

1
2
3
4
5
6
from sklearn import tree
import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(figsize=(20,8))
tree.plot_tree(tree_reg,fontsize=8)

[実行結果]

各ノードの中身に表示されている値は次のような意味となります。

  • X[n] <= m
    次のノードへの分岐条件
  • mae
    ノードの不純度(valueと実測値の平均二乗誤差)
  • samples
    ノードに含まれるデータ件数
  • value
    ノードに含まれるデータの平均値

末端になるノード(リーフノード)のvalueが予測値の候補となっています。

次回は、構築した決定木モデルの評価を行います。