機械学習

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ならもう一方のパラメータを最適する...など)

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

参考

関連書籍

関連書籍






ottantacinqueをフォローする
データ分析とケモインフォ

コメント

  1. 伊藤 雅夫 より:

    当方、機械学習を勉強し始めたばかりです。いろいろなサイトの情報を参考にさせていただいております。
    1.複数回の試行をして、結果(model.score(df_X, df_y))が異なります。別のサイトで、
    sampler=optuna.samplers.TPESampler(seed=42)
    を知りました。
    具体的には
    study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=42))
    これにより、複数回の試行でも、同じ値を得ることができました。
    2.その上で、lambda有り無しをチェックしました。
    def objective(trial):
    study.optimize(objective, n_trials=100)

    確かに、結果は同じになりました。

    念の為に、codeを示します(理解の行き違いをなくすため、但し、小生のシステムはanaconda3,64bit Python 3.10,Bostonのデータはwarningに従い、ダウンロード元を変更しています)

    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
    from xgboost import XGBRegressor

    optuna.logging.disable_default_handler() # 非表示
    #optuna.logging.enable_default_handler() # 表示

    data_url = “http://lib.stat.cmu.edu/datasets/boston”
    raw_df = pd.read_csv(data_url, sep=”\s+”, skiprows=22, header=None)
    df_X = np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]])
    df_y = raw_df.values[1::2, 2]

    #def objective(trial,df_X,df_y): # <————-引数に ,df_X,df_y あり
    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

    #optuna.create_study()でoptuna.studyインスタンスを作る。
    study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=42))
    # <—— #複数の試行で、結果を同じにするため

    #studyインスタンスのoptimize()に作った関数を渡して最適化する。
    #study.optimize(lambda trial: objective(trial,df_X,df_y), n_trials=100) # <– lambdaあり
    study.optimize(objective, n_trials=100) # <—– lambdaなし

    params = study.best_params
    model = XGBRegressor(**params)

    # 再度学習を実施する
    print(f"best_value(cv): {study.best_value}")
    print(f"best_params : {study.best_params}")
    print(f'model : {model}')

    model.fit(df_X, df_y)
    best_score=model.score(df_X, df_y)
    print(f"best_score : {best_score}")
    #score_X_valid=model.score(X_valid, y_valid)
    #print(f"score(X_valid): {score_X_valid}")
    print(f"\n{'='*20}")

    出力結果は、下記(でも、同じにはならない気がします。なぜ異なるのかはわかりませんが)
    best_value(cv): 18.35950639764233
    best_params : {'max_depth': 3, 'min_child_weight': 5, 'gamma': 0.4103331140543905, 'subsample': 0.950028384919545, 'colsample_bytree': 0.9470860473190347, 'reg_alpha': 0.00011500631113721802, 'reg_lambda': 0.0003546282369289078, 'learning_rate': 0.12150384104074101}
    model : XGBRegressor(base_score=None, booster=None, colsample_bylevel=None,
    colsample_bynode=None, colsample_bytree=0.9470860473190347,
    enable_categorical=False, gamma=0.4103331140543905, gpu_id=None,
    importance_type=None, interaction_constraints=None,
    learning_rate=0.12150384104074101, max_delta_step=None,
    max_depth=3, min_child_weight=5, missing=nan,
    monotone_constraints=None, n_estimators=100, n_jobs=None,
    num_parallel_tree=None, predictor=None, random_state=None,
    reg_alpha=0.00011500631113721802, reg_lambda=0.0003546282369289078,
    scale_pos_weight=None, subsample=0.950028384919545,
    tree_method=None, validate_parameters=None, verbosity=None)
    best_score : 0.9749697299350986

    ====================

    何かサジェスチョンいただければ、ありがたいです。