Optunaを使ったXGBoostのハイパーパラメータ最適化

optunaとは

PFNにより公開されている最適化用のライブラリ。
TPE (Tree-structured Parzen Estimato)という、ベイズ最適化の一種を使って関数をいい感じで最適化するらしい。
XGBoost などのハイパーパラメータ最適化などによく用いられている印象。

TPEの理論面に関しては詳しく理解できていないので、以下の記事参照
(Optuna(TPE)のアルゴリズム理解:Part 1Part 2Part 3

大まかな使い方

  1. objective(最適化したい関数)を作成(その中でパラメータやその探索範囲を指定する)
  2. optuna.create_studyでタスク(study)を作成
  3. study.optimizeを使い、作成した関数無いのパラメータを最適化
  4. 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ならもう一方のパラメータを最適する...など)

また、ハイパーパラメータ以外のパラメータ最適化など色々と活用できそうだと感じた。

参考

関連書籍

関連書籍