データサイエンスにおいて、最後の詰めで必要となるハイパーパラメータチューニング。その説明と実装例、比較までを簡単かつ簡潔に説明していきます!
1. ハイパーパラメータとは?
機械学習モデルではあるデータを学習する際に、学習速度や方法、モデル独自のパラメータなど複数のパラメータを設定する必要があります。このパラメータ群をハイパーパラメータと呼びます。これをモデルの精度を高めるために、パラメーターを調整することをハイパーパラメータチューニングと呼びます。
パラメータチューニングには大きく分けて「手動」で調整を行う方法と「自動」で調整を行う方法の2種類があります。手動の方は、自分で1つずつ値を調整しながら、モデルの精度を高めていく方法です。職人技なところがあります。自動の方は、ある設定した範囲のパラメータ空間の中で一番精度がよかったものを自動で選択する手法です。あまりパラメータチューニングの経験がなくても自動でチューニングを行って、ベストなパラメータを返してくれるので便利です。
2. 比較するハイパーパラメータチューニングフレームワーク
今回紹介するパラメータチューニング関数は以下の4つです。概要、探索方法、メリット、デメリットをそれぞれ説明していきます。
2-a. GridSearchCV
scikit-learnのパッケージにある交差検証とグリッドサーチを行うことができる関数です。通常、パラメータチューニング以前に偏ったデータによる学習の評価を回避するために、交差検証により、データセットの範囲内で偏りなくモデル評価する必要があります。グリッドサーチとは、各パラメータ範囲内の”全ての組み合わせ”を設定し計算する手法です。
- 設定したパラメータの範囲を網羅的に探索するため、漏れがない。
- 範囲が狭い、または、探索領域が少ない場合に有効。
- 簡潔にコーディングができる。
- 設定したパラメータの範囲が広くなると計算量が膨大になってしまう。
2-b. RandomizedSearchCV
scikit-learnのパッケージにある交差検証とランダムサーチを行うことができる関数です。ランダムサーチとは、各パラメータ範囲内で”ランダムな組み合わせ”を設定し計算する手法です。
- 調整するパラメータの範囲が大きくなっても、計算量を抑えつつチューニングすることができる。
- 範囲が狭い、または、探索領域が少ない場合に有効。
- 簡潔にコーディングができる。
- 最終的な精度がぶれやすい。
- パラメータ探索の網羅性がないため、漏れが出やすい。
2-c. BayesianOptimization
BayesianOptimization(bayes_opt)パッケージにあるベイズ最適化を行うことができるフレームワークです。ベイズ最適化とは、未知の空間からガウス過程の事前分布を用いて、目標関数の最大または最小値を示すパラメータを特定する手法です。この手法は前の結果を使ってパラメータを特定するため、グリッドサーチやランダムサーチに比べて合理的と言えます。
- 最適化と計算コストのバランスが取ることができる。
- パラメータチューニングに独自の評価関数の設定などのカスタム性が良い。
- コード量が多くなる。
- 自分で交差検証の関数を設ける必要がある。
2-d. Optuna
2018年末に発表されたOptunaパッケージにあるTree-structured Parzen Estimatorというベイズ最適化アルゴリズムを用いてパラメータ最適化を行うことができる関数です。前のBayesianOptimizationとは分類としては同じですが、内部に採用されているアルゴリズムが異なります。詳しく知りたい方は外部リンクをどうぞ!
- BayesianOptimizationより最適化と計算コストのバランスが取ることができ、広い範囲でパラメータ探索できる。
- パラメータチューニングに独自の評価関数の設定などのカスタム性が良い。
- コード量が多くなる。
- 自分で交差検証の関数を設ける必要がある。
3. 比較条件
それぞれの手法を比較するためにある程度の条件を揃えていきたいと思います。使うデータセットは昔から機械学習のベンチマークとして用いられた手書き文字認識です。データセットはscikit-learnパッケージから直接データを取得することができ、以下のようなスペックです。
- クラス数:10
- クラスごとのサンプル数:~180
- 全体のサンプル数:1797
- 次元数:8×8=64
- 特徴量:0~16の整数
ランダムにトレーニングデータ:テストデータを7:3に分割し、トレーニングデータを交差検証で5分割にして評価していきます。学習モデルはXGboostというアンサンブル学習と決定木を組み合わせた手法を用います。探索範囲は、決定木の深さを2-100、識別器に通す回数を10-1000として学習させてみます。ランダムサーチ、ベイズ最適化の探索回数は約200回行います。グリッドサーチは探索範囲の目を少々荒くして他の探索回数と同等にして探索します。
4. 比較結果
結果は、以下の表1のようになりました。パラメータと探索時間は小数点以下を切り捨て、識別率は小数点以下3桁までを示しています。パラメータmax_depthはどのフレームワークでも一致して”2″となりました。パラメータn_estimatorsは400±50にはおさまっているものの、若干異なる結果を示していました。識別のカラムにおいて、赤いアンダーラインが引かれているところが、最も良い識別率です。テストデータの識別率ではBayesian Optimizationが最も高く、学習データの識別率ではOptunaが最も高い結果となりました。探索時間に関してはGridSearchCV、RandomizedSearchCVが同程度の計算時間がかかり、それらに比べてBayesian Optimization、Optunaは1.4倍ほど遅い結果となりました。これは、最適化のための計算過程の違いによるものと考えられます。
探索フレームワーク | パラメータ (max_depth) | パラメータ (n_estimators) | 識別率 (学習データ) | 識別率 (テストデータ) | 探索時間 [秒] |
GridSearchCV | 2 | 360 | 0.965 | 0.966 | 908 |
RandomizedSearchCV | 2 | 345 | 0.962 | 0.963 | 876 |
Bayesian Optimization | 2 | 437 | 0.884 | 0.976 | 1240 |
Optuna | 2 | 415 | 0.968 | 0.948 | 1397 |
5. 各ソースコード
実際に実行したソースコードを示します。
6-1. GridSearchCV
import xgboost as xgb
import os
import random
import time
import numpy as np
import pandas as pd
import warnings
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits
if __name__ == '__main__':
print(xgb)
digits = load_digits()
warnings.filterwarnings('ignore')
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)
params = {"objective":"multi:softmax","num_class":10, "tree_method": 'gpu_hist'}
print(params)
model = xgb.XGBClassifier(params)
start = time.time()
print(np.arange(2,100,10))
print(np.arange(10,1000,50))
grid_search = GridSearchCV(model, {"max_depth":np.arange(2,100,10), "n_estimators":np.arange(10,1000,50)}, cv=5, verbose=1, n_jobs=-1)
grid_search.fit(X_train, y_train)
print("Best_parameter : " , grid_search.best_params_)
print("精度: " , grid_search.best_score_)
elapsed_time = time.time() - start
pred = grid_search.predict(X_test)
print("テスト精度:", accuracy_score(y_test, pred))
print ("パラメータ探索時間:{0}".format(elapsed_time) + "[sec]")
result_df = pd.DataFrame(grid_search.cv_results_)
result_df.sort_values(by="rank_test_score", inplace=True)
print(result_df[["rank_test_score",
"params",
"mean_test_score"]])
result_df.to_csv('acc_grid_digits.csv')
6-2. RandomizedSearchCV
import os
import random
import time
import numpy as np
import pandas as pd
import warnings
from sklearn import model_selection
import xgboost as xgb
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import RandomizedSearchCV
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits
if __name__ == '__main__':
digits = load_digits()
warnings.filterwarnings('ignore')
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)
params = {"objective":"multi:softmax","num_class":10}
print(params)
model = xgb.XGBClassifier(params)
start = time.time()
params = {"max_depth":np.arange(2,100,1), "n_estimators":np.arange(10,1000,1)}
random_search = RandomizedSearchCV(model, params, cv=5, iid=True, return_train_score=False, n_iter=200, n_jobs=-1)
random_search.fit(X_train, y_train)
print("Best_parameter : " , random_search.best_params_)
print("精度: " , random_search.best_score_)
elapsed_time = time.time() - start
pred = random_search.predict(X_test)
print("テスト精度:", accuracy_score(y_test, pred))
print ("パラメータ探索時間:" + str(elapsed_time) + "[sec]")
result_df = pd.DataFrame(random_search.cv_results_)
result_df.sort_values(by="rank_test_score", inplace=True)
print(result_df[["rank_test_score",
"params",
"mean_test_score"]])
result_df.to_csv('acc_random_digits.csv')
6-3. Bayesian Optimization
import os
import random
import time
import numpy as np
import pandas as pd
import warnings
from sklearn import model_selection
import xgboost as xgb
from sklearn.model_selection import train_test_split
from bayes_opt import BayesianOptimization
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits
def xgb_cv(n_estimators, max_depth):
model = xgb.XGBClassifier(n_estimators=int(n_estimators), max_depth = int(max_depth), objective="multi:softmax", num_class=10)
score = cross_val_score(model, X_train, y_train, cv=5, scoring="r2").mean()
return score
bo = BayesianOptimization(
xgb_cv,
{'n_estimators': (10, 1000),
'max_depth': (2, 100)
},
verbose=2
)
if __name__ == '__main__':
digits = load_digits()
warnings.filterwarnings('ignore')
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)
start = time.time()
bo.maximize(n_iter=195)
print("Best_parameter : " , bo.max['params'])
print("精度: " , bo.max['target'])
elapsed_time = time.time() - start
model = xgb.XGBClassifier(n_estimators=int(bo.max['params']['n_estimators']), max_depth=int(bo.max['params']['max_depth']), objective="multi:softmax", num_class=3)
model.fit(X_train, y_train)
pred = model.predict(X_test)
print("テスト精度:", accuracy_score(y_test, pred))
print ("パラメータ探索時間:" + str(elapsed_time) + "[sec]")
result_df = pd.DataFrame(bo.res)
print(result_df)
result_df.to_csv('acc_bo_digits_all.csv')
res_target = result_df['target']
res_target.to_csv('acc_bo_digits.csv')
6-4. Optuna
import os
import random
import time
import numpy as np
from numpy.core.fromnumeric import size
import pandas as pd
import warnings
import xgboost as xgb
import optuna
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits
def objective(trial):
max_depth = trial.suggest_int("max_depth", 2, 100)
n_estimators = trial.suggest_int("n_estimators", 10, 1000)
model = xgb.XGBClassifier(max_depth=max_depth,
n_estimators = n_estimators,
objective = "multi:softmax",
num_class = 10)
score = cross_val_score(model, X_train, y_train, cv=5, n_jobs=-1)
print(score)
r2_mean = np.mean(score)
print(r2_mean)
acc_list.append(r2_mean)
return r2_mean
if __name__ == '__main__':
digits = load_digits()
warnings.filterwarnings('ignore')
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)
print(X.shape)
print(y.shape)
acc_list = []
start = time.time()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=200)
print("Best_parameter : " , study.best_params)
print("精度: " , study.best_value)
elapsed_time = time.time() - start
df = pd.DataFrame({'acc': acc_list})
df.to_csv('acc_optuna_digits.csv')
best_params = {
"max_depth": study.best_params["max_depth"],
"n_estimators": study.best_params["n_estimators"],
"objective": "multi:softmax",
"num_class":3
}
model = xgb.XGBClassifier(best_params)
model.fit(X_train, y_train)
pred = model.predict(X_test)
print("テスト精度:", accuracy_score(y_test, pred))
print ("パラメータ探索時間:" + str(elapsed_time) + "[sec]")
6. 最後に
いかがだったでしょうか?
どの探索フレームワークもある程度精度の良いパラメータを選択してくれることがわかりました。しかし、識別率のコンマ数桁の違いがKaggleでは大きな差を生みます。メリット・デメリットを考えて、この記事がフレームワークを選ぶ1つの参考資料になっていただければ幸いです。