見出し画像

XGBoostでグリッドサーチとクロスバリデーション1

気付きがあったので書いておきます。

多分みんな知ってるんだと思う。

Pythonでsklearn.datasetsにあるload.boston()の回帰をXGBoostを用いて行います。
XGBoostでは、DMatrixという目的変数と目標値が格納されたデータを作成します。testデータ2割、まずはdefaultで。

import xgboost as xgb
import matplotlib.pyplot as plt
from sklearn.datasets import load_boston
from sklearn import metrics as met
import sklearn as skl
import numpy as np

dataset = load_boston()
x = dataset.data
t = dataset.target

x_train, x_test, t_train, t_test = skl.model_selection.train_test_split(x, t, test_size=0.2, random_state=0)

dtrain = xgb.DMatrix(x_train, label=t_train, feature_names=dataset.feature_names)
dtest = xgb.DMatrix(x_test, label=t_test, feature_names=dataset.feature_names)
# 変数名マッチしてないと怒られる

param = {}
param['eta'] = 0.3
param['gamma'] = 0
param['max_depth'] = 6
# max_depthだけ分岐を許す

param['min_child_weight'] = 1
param['subsample'] = 1
param['colsample_bytree'] = 1

param['objective'] = 'reg:squarederror'
param['eval_metric'] = 'rmse'
# default

num_round = 10000
evals = [(dtest, 'eval'),(dtrain, 'train')]
# early_stoppingのためのevaluationはtrainデータでやるみたい。evalデータは値を見せるだけ。
evals_result = {}

bst = xgb.train(param, dtrain, num_boost_round=num_round, evals=evals, evals_result=evals_result, early_stopping_rounds=100)
train_pred = bst.predict(dtrain, ntree_limit=bst.best_ntree_limit)
test_pred = bst.predict(dtest, ntree_limit=bst.best_ntree_limit)

print('train_RMSE_score_is_{:.8f}, test_RMSE_score_is_{:.8f}'.format(np.sqrt(met.mean_squared_error(t_train, train_pred)), np.sqrt(met.mean_squared_error(t_test, test_pred))))
# 0.00116996, 4.62175620,  当然だがeval_metricが出してくる値と同じ
print('train_R2_score_is_{:.4f}, test_R2_score_is_{:.4f}'.format(met.r2_score(t_train, train_pred), met.r2_score(t_test, test_pred)))
# 1.0000, 0.7377 かなりoverfitting?
print('Best Score:{:.4f}, Iteration:{:d}, Ntree_Limit:{:d}'.format(bst.best_score, bst.best_iteration, bst.best_ntree_limit))
# Best Score:0.0012, Iteration:165, Ntree_Limit:166


plt.rcParams['figure.facecolor'] = 'white'

fig = plt.figure(figsize=(13, 5))
ax1 = plt.subplot(121)
ax1 = plt.plot(evals_result['eval']['rmse'], label = 'eval')
ax1 = plt.plot(evals_result['train']['rmse'], label = 'train')
plt.legend()
ax2 = plt.subplot(122)
ax2 = xgb.plot_importance(bst, ax=ax2)

画像1

eval_resultはeval_matricのTree数発展で並んだlist. dictになってるからkeyはドキュメント参照。
Feature importance目的変数が何回分岐に使われているかというF scoreを返す。

xgb.plot_tree(bst, num_trees=X)

でX番目のTreeの構造見れる。今回はmax_depth=6を入れてるから分岐が最大6つ発生している。
ちなみにnum_trees=0において、max_depth=6の木の構造は3回目の分岐までmax_depthが3の木と同じ。再現性あり。(それ以降はひとつ前の木に依存するので異なる。)

画像2

Tree0の構造は同じ

画像3


で、上の結果はtrain setのrmseが0.0012に対してtest setが4.6とかなりのoverfittingになっているのでハイパーパラメーターを検討する。


ハイパーパラメーターの調整

ハイパーパラメーターを調整する際にグリッドサーチとクロスバリデーション(CV)という手法がよく行われています。

ここ気付きポイントなのですが、

クロスバリデーションはパラメーターを決めるための指標ではなく、指定したパラメーターがどれだけ汎化性能があるかを示すための数字なので、グリッドサーチと組み合わせないと全く意味がない。

ということ。


xgb.cvを使ってCVだけをやってみます。

params = {}
num_round = 10000

bst_cv = xgb.cv(param, dtrain, num_boost_round=num_round, early_stopping_rounds=100, nfold=5, metrics='rmse')


print(bst_cv)
# bst_cvに対するメゾットがないので使いづらいがとりあえず行数=最適なTree数だと思う。

bst = xgb.train(param, dtrain, num_boost_round=num_round)
train_pred = bst.predict(dtrain)
test_pred = bst.predict(dtest)

print('train_RMSE_score_is_{:.4f}, test_RMSE_score_is_{:.4f}'.format(np.sqrt(met.mean_squared_error(t_train, train_pred)), np.sqrt(met.mean_squared_error(t_test, test_pred))))
print('train_R2_score_is_{:.4f}, test_R2_score_is_{:.4f}'.format(met.r2_score(t_train, train_pred), met.r2_score(t_test, test_pred)))

画像4

CVの結果、nfold回の学習が行われ、その過程における各Tree数でのmetricsの評価の平均値がbst_cvに表れています。xgb.trainと同じことをやっていますが、modelは作られないのでxgb.trainを実行する必要があります

CVにおける各学習で、trainデータとtestデータの分け方によってmetric最小を取るTree数が違いますが、metricの平均値は82本のTreeで最小になっていますね。

パラメーターはすべてdefaultで指定しており、CVによってパラメーターが変わるということはありません。なのでこのままtrainを回してもCVをしない時と全く同じ結果になります。


nfold-CV時のTree数を使って学習をしたいときは、それらしいメゾットがない気がするのでlen(bst_cv)でDataFrameの行数を代入してやればいいと思います。

ちなみにその結果は以下のようになり、Tree数を約半分に減らしてtrainデータに対するscoreが下がっているにも関わらず、testデータに対するscoreはほとんど変わっていません

# train_RMSE_score_is_0.0324, test_RMSE_score_is_4.6219
# train_R2_score_is_1.0000, test_R2_score_is_0.7377


Scikit-learn AIPによるXGBoostとクロスバリデーション

Scikit-learnっぽくXGBoostやるよ、みたいな関数があります。web検索してもこっちのコードばっかりヒットするが、GridSearchCVが使えるからみんなこっち使ってるんだと思う。

import xgboost as xgb
import matplotlib.pyplot as plt
from sklearn.datasets import load_boston
from sklearn import metrics as met
import numpy as np
from sklearn.model_selection import train_test_split, GridSearchCV

dataset = load_boston()
x = dataset.data
t = dataset.target

x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.2, random_state=0)

# sklearn AIPなのでDMatrix作る必要ない


mod = xgb.XGBRegressor(max_depth = 6, learning_rate = 0.3, n_estimators=10000, objective='reg:squarederror', gamma=0, min_child_weight=1, subsample=1, colsample_bytree=1)
# モデルを作成。xgb.trainにおけるparamの部分

params = {'max_depth': [6], 'learning_rate': [0.3]}
gscv = GridSearchCV(mod, params, cv = 5, refit=True,  scoring='r2', verbose = 1, n_jobs=1)
# GridSearchCVをこれからやるよっていう宣言だけ。グリッドサーチは.fitで実行される。
# 今回はparamsは固定

evallist = [(x_train, t_train)]
gscv.fit(x_train, t_train, eval_metric='rmse', eval_set=evallist, early_stopping_rounds=100)
# 学習を行う。evallistの値に対してRMSEで評価を行い、100round後も変化がなければ終了。

print(gscv.best_estimator_)

train_pred = gscv.predict(x_train)
test_pred = gscv.predict(x_test)

print('train_RMSE_score_is_{:.4f}, test_RMSE_score_is_{:.4f}'.format(np.sqrt(met.mean_squared_error(t_train, train_pred)), np.sqrt(met.mean_squared_error(t_test, test_pred))))
# 0.0012, 4.6218
print('train_R2_score_is_{:.4f}, test_R2_score_is_{:.4f}'.format(met.r2_score(t_train, train_pred), met.r2_score(t_test, test_pred)))
# 1.0000, 0.7377
print('Best Score:{:.4f}'.format(gscv.best_score_))
# Best Score:0.8928 (R2)

XGBRegressorでモデル作ったりGridSrearchCVでグリッドサーチとCVのパラメーター指定したりするが実際に実行するのは.fitの部分。

つまりXGBRegressorはXGBにおけるparam = {}の部分、
GridSearchCV+.fitはセットで、xgb.cvからxgb.trainまでを行っていることになります。

GridSearchCVのrefitをTrueにすると, CVの結果による最適なパラメーターを用いて、データ全体に対するmodelの作成を行います。
従ってrefitはxgb.trainの部分を行うかどうかを指定します。

CVだけやっても意味がないの本質はここにあって、
CVによって汎化性能が高い最適なパラメーターを探索することがCVの意義であるので、パラメーターを振らない状態でやっても出てくるデータは同じということです。

結果はxgb.cvの時と全く一緒。


まとめ

CVによって汎化性能が高い最適なパラメーターを探索することがCVの意義であるので、パラメーターを振らない状態でやっても出てくるデータは同じ
とにかくグリッドサーチとクロスバリデーションは一緒にやれ

文章にすると当たり障りのない感じになってしまう。。


そうなるとxgb.cvで求めた汎化性能が高いTree数とxgb.trainで求めたtrainデータに対してscoreがいいTree数のどちらを採用すればよいのかとか、またいろいろ考えることが出てきますが、長くなったので一旦切ります。

次はグリッドサーチとクロスバリデーションを一緒にやった結果を書く予定です。







いいなと思ったら応援しよう!