optunaとは
PFNにより公開されている最適化用のライブラリ。
TPE (Tree-structured Parzen Estimato)という、ベイズ最適化の一種を使って関数をいい感じで最適化するらしい。
XGBoost などのハイパーパラメータ最適化などによく用いられている印象。
TPEの理論面に関しては詳しく理解できていないので、以下の記事参照
(Optuna(TPE)のアルゴリズム理解:Part 1、Part 2、Part 3)
大まかな使い方
- objective(最適化したい関数)を作成(その中でパラメータやその探索範囲を指定する)
- optuna.create_studyでタスク(study)を作成
- study.optimizeを使い、作成した関数無いのパラメータを最適化
- study.best_paramsなどを使い結果を取得
ここではOptunaを使って、XGBoostのハイパーパラメータを最適化してみる。
コード
optunaのインストール
pip install optuna
データの準備
scikit-learn に用意されているボストンの住宅価格データセットを利用する
import numpy as np
import pandas as pd
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_boston
import optuna
import xgboost as xgb
boston = load_boston()
df_X = pd.DataFrame(boston.data, columns=boston.feature_names)
df_y = pd.DataFrame(boston.target,columns=['Price'])
目的関数を作成する
最小化したいスコアを返り値とする関数を定義する。
関数の中で最適化したいパラメータ(とその探索範囲)を trialオブジェクトで定義する。
最適化したいパラメータの指定方法は以下の通り
メソッド | 変数のサンプリング方法 |
---|---|
suggest_categorical(name, choices) | リスト(choices)の中からカテゴリ変数をサンプリング |
suggest_int(name, low, high[, step, log]) | low~high の範囲内で整数をサンプリング log:Trueで対数的な分布に変更 step:刻み幅を指定 |
suggest_float(name, low, high, *[, step, log]) | low〜high のの範囲内で少数値をサンプリング log:Trueで対数的な分布に変更 step:刻み幅を指定 |
suggest_uniform(name, low, high) | low~high の一様分布に従いサンプリング (sugget_float でlog, float を指定しない場合と同義) |
suggest_loguniform(name, low, high) | low~high の対数分布に従いサンプリング (sugget_float でlog=Trueとした場合と同義) |
suggest_discrete_uniform(name, low, high, q) | low〜high の離散一様分布からサンプリング (sugget_float でstepを指定した場合と同義) |
ここでは、5-fold交差検証での平均誤差を最適化したい指標として関数を定義する。
def objective(trial,df_X,df_y):
#評価するハイパーパラメータの値を規定
params ={
'max_depth':trial.suggest_int("max_depth",1,10),
'min_child_weight':trial.suggest_int('min_child_weight',1,5),
'gamma':trial.suggest_uniform('gamma',0,1),
'subsample':trial.suggest_uniform('subsample',0,1),
'colsample_bytree':trial.suggest_uniform('colsample_bytree',0,1),
'reg_alpha':trial.suggest_loguniform('reg_alpha',1e-5,100),
'reg_lambda':trial.suggest_loguniform('reg_lambda',1e-5,100),
'learning_rate':trial.suggest_uniform('learning_rate',0,1)}
model = xgb.XGBRegressor(n_estimators=100,
verbosity=0,
n_jobs=-1,
random_state=0,
**params)
#交差検証
scores = cross_val_score(model, df_X, df_y, scoring='neg_mean_squared_error',cv=5)
score_mean = -1 * np.mean(scores)
return score_mean
最適化を実行する
n_trialsで試行回数を指定する。
#optuna.create_study()でoptuna.studyインスタンスを作る。
study = optuna.create_study()
#studyインスタンスのoptimize()に作った関数を渡して最適化する。
study.optimize(lambda trial: objective(trial,df_X,df_y), n_trials=100)
#下記のような感じで計算過程が表示される
"""output
[I 2021-09-15 23:12:31,860] A new study created in memory with name: no-name-1c966f32-0143-4277-89a9-737785ecff73
[I 2021-09-15 23:12:32,538] Trial 0 finished with value: 21.953465964005716 and parameters: {'max_depth': 10, 'min_child_weight': 2, 'gamma': 0.7239605433055278, 'subsample': 0.43807900497034136, 'colsample_bytree': 0.2881615006892957, 'reg_alpha': 5.510000819738757e-05, 'reg_lambda': 61.06698592300022, 'learning_rate': 0.41400609846451486}. Best is trial 0 with value: 21.953465964005716.
[I 2021-09-15 23:12:33,586] Trial 1 finished with value: 20.380537482594693 and parameters: {'max_depth': 7, 'min_child_weight': 4, 'gamma': 0.022923877570006024, 'subsample': 0.9185493358622109, 'colsample_bytree': 0.6657960975739451, 'reg_alpha': 0.1270258763328784, 'reg_lambda': 31.66101345986972, 'learning_rate': 0.25690666578645205}. Best is trial 1 with value: 20.380537482594693.
[I 2021-09-15 23:12:34,523] Trial 2 finished with value: 20.995102527541384 and parameters: {'max_depth': 8, 'min_child_weight': 3, 'gamma': 0.08042752498848993, 'subsample': 0.8122586312076105, 'colsample_bytree': 0.6472068995951297, 'reg_alpha': 0.8193181134698155, 'reg_lambda': 22.35699024120949, 'learning_rate': 0.3523914200916791}. Best is trial 1 with value: 20.380537482594693.
····
"""
結果を表示する
#スコアを見る
print(study.best_params)
print(study.best_value)
"""
{'max_depth': 3, 'min_child_weight': 5, 'gamma': 0.025393721207504712, 'subsample': 0.8177130789878151, 'colsample_bytree': 0.7390507077447714, 'reg_alpha': 0.0013063429707847625, 'reg_lambda': 0.0014047293738148994, 'learning_rate': 0.12071521198832026}
best value: 18.026163732075332
"""
#それぞれの試行結果を見たい場合
# print(study.trials)
# 最適化したいパラメータを取り出して次の解析を行う場合
# params = study.best_params
# model = xgb.XGBRegressor(**params)
# model.fit(df_X, df_y)
最後に
ここでは触れないが、SQLiteなどDBを使うと最適化を終了しても途中から再開できたり、複数プロセスで並列処理ができるらしい。
また、単純に最適化するだけではなく分岐も入れられても最適化ができるとのこと。
(例えば、XGBoostとlightGBMの二つの予測器から一つを選択し、XGBoostならこのパラメータを使い、lightGBMならもう一方のパラメータを最適する...など)
また、ハイパーパラメータ以外のパラメータ最適化など色々と活用できそうだと感じた。
参考
関連書籍
関連書籍
リンク
リンク
リンク