👏

【機械学習】Optunaを使って効率よくハイパーパラメータを調整しよう

2023/05/04に公開

機械学習において、モデルのハイパーパラメータの調整は非常に大事です。
大事なんですが、手動で行うと、時間も手間もすごくかかりますよね。
ここでは、ハイパーパラメータを効率的に調整するOptunaについてご紹介するとともに、Pythonでの具体的な実装方法をご説明します。

Optunaとは

日本のPrefferdNetworks社が開発した、ハイパーパラメータの自動最適化フレームワークです。
Optunaは、ベイズ最適化を実装することで、パラメータの探索を効率的に行うことができます。

ベイズ最適化は、確率統計の理論の一つです。
具体的には、目的関数の値を評価するために、パラメータの値をサンプリングして目的関数の値を取得し、その値を元にパラメータの推定値を更新するというプロセスを繰り返します。このプロセスによって、より良いパラメータの候補が見つけていきます。
簡単に言うと、試行錯誤をしながら、最適なハイパーパラメータを探索していくイメージです。
https://www.preferred.jp/ja/projects/optuna/

GoogleColab には Optuna はインストールされていないため、!pip install optunaでインストールします

今回調整するハイパーパラメータについて

learning_rate(学習率)
モデルの重みを更新する際に使用されるステップサイズを指定するパラメータです。この値が小さいほど、モデルがより慎重に更新され、より正確な予測が可能になります。ただし、小さすぎる値を設定すると、収束に時間がかかる可能性があります。

num_leaves(決定木の葉の数)
決定木の複雑さを指定するパラメータです。この値が大きいほど、モデルはより複雑になり、過剰適合の可能性が高くなります。一方、この値が小さい場合、モデルは単純化され、未学習の可能性が高くなります。

min_data_in_leaf(葉に含まれる最小のサンプル数)
葉に含まれる最小のサンプル数を指定するパラメータです。この値が小さいほど、モデルはより複雑になり、過剰適合の可能性が高くなります。ただし、小さすぎる値を設定すると、未学習の可能性が高くなります。

min_sum_hessian_in_leaf(葉に含まれる最小のHessianの合計値)
決定木の学習に使用される二次微分(Hessian)の合計値の最小値を指定するパラメータです。この値が小さいほど、モデルはより複雑になり、過剰適合の可能性が高くなります。

bagging_fraction(バギングの使用率)
トレーニングデータのサブサンプルをランダムに選択して、決定木をトレーニングする際のサンプリング率を指定するパラメータです。この値が小さいほど、モデルのバリアンスが減少し、過剰適合の可能性が低くなります。

bagging_freq(バギングの適用頻度)
バギングを行う頻度を指定するパラメータです。この値を設定することで、決定木のトレーニングに使用するトレーニングデータのバリエーションを増やすことができます。

feature_fraction(特徴量の使用率)
トレーニングデータの特徴量のサブサンプルをランダムに選択して、決定木をトレーニングする際の特徴量の使用率を指定するパラメータです。この値が小さいほど、モデルのバリアンスが減少し、過剰適合の可能性が低くなります。

Optunaの実装

objective関数の用意

objectiveという関数を用意します。
その中で、調整したいハイパーパラメータや、モデルの訓練内容を書きます。

import optuna

def objective(trial):
    
    # 調整したいハイパーパラメータについて範囲を指定
    param = {
        'boosting_type': 'gbdt',    
        'objective': 'regression',      
        'metric': 'rmse',               
        'learning_rate': 
	trial.suggest_uniform('learning_rate', 0.01, 0.08),          
        'n_estimators': 100000,         
        'importance_type': 'gain',
        'num_leaves': 
	trial.suggest_int('num_leaves', 10, 100),
        'min_data_in_leaf': 
	trial.suggest_int('min_data_in_leaf', 5, 50),
        'min_sum_hessian_in_leaf': 
	trial.suggest_int('min_sum_hessian_in_leaf', 5, 50),
        'lambda_l1': 0,
        'lambda_l2': 0,
        'bagging_fraction': 
	trial.suggest_uniform('bagging_fraction', 0.1, 1.0),
        'bagging_freq': 
	trial.suggest_int('bagging_freq', 0, 10),
        'feature_fraction': 
	trial.suggest_uniform('feature_fraction', 0.1, 1.0),
        'random_seed': 42   
    }
    
    rmses = []
    
    # KFoldのオブジェクトを生成
    kf = KFold(n_splits=folds, shuffle=True, random_state=123) 

    # KFold CV
    for train_index, valid_index in kf.split(x_train):

        x_train_cv ,y_train_cv = 
	x_train.iloc[train_index], y_train.iloc[train_index]
        x_valid_cv ,y_valid_cv = 
	x_train.iloc[valid_index], y_train.iloc[valid_index]


        model = lgb.LGBMRegressor(**param)    

        model.fit(
            x_train_cv, 
            y_train_cv, 
            eval_set = 
	    [(x_train_cv, y_train_cv), (x_valid_cv, y_valid_cv)], 
            early_stopping_rounds = 100, 
            categorical_feature = category_list, 
            verbose = 500, 
        )

        y_pred_valid = 
	model.predict(x_valid_cv, num_iteration=model.best_iteration_)       

        # RMSEを算出
        temp_rmse_valid = 
	np.sqrt(mean_squared_error(y_valid_cv, y_pred_valid))

        # RMSEをリストにappend
        rmses.append(temp_rmse_valid)

    # CVのRMSEの平均値を目的関数として返す
    return np.mean(rmses)

Optunaの実行

  • create_study()でOptunaをインタンス化し、optimize()で実行します。
  • optimizeの引数に、先ほどのobjective関数と、試行回数(以下は30)を渡します。
folds = 5
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=30)

print('Number of finished trials:', len(study.trials))
print('Best trial:', study.best_trial.params)


途中、こんな感じで学習過程がわかります

Optunaの結果

  • 30回の試行後、以下のようなベストパラーメータが返ってきます。

Discussion