グリッドサーチとOptunaを実際に使って比較してみた！

# はじめに
こんにちは。船井総合研究所の平尾です。

とある案件においてXGBoostのパラメータチューニングをグリッドサーチによって行っていました。グリッドサーチによる計算実行時間は約1時間と長かったです。

しかし、先輩社員に「Optuna」というツールの存在を教えてもらい、Optunaを用いたところ実行が約30秒で終わって非常に感動しました。

そこで今回は、scikit-learnのdiabetes(糖尿病データ)に対するモデル構築において、グリッドサーチとOptunaを実際に使って実行時間、精度を比較してみました。

# 各ツールの概要
### グリッドサーチ
グリッドサーチは、指定したハイパーパラメータの値の全ての組み合わせにおいてモデル構築を行う方法です。

以下は例です。
```
param_grid = {
    'learning_rate':[0.01, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30], 
    'max_depth': [2, 4, 6, 8, 10], 
    'min_child_weight':[0, 1, 2, 3, 4], 
    'subsample':[0.2, 0.4, 0.6, 0.8, 1],
    'colsample_bytree':[0.2, 0.4, 0.6, 0.8, 1], 
    'reg_lambda':[2, 4, 6, 8, 10],
}
```
組み合わせの総数は21875通りとなります。
実行時間が長くなるのも納得です。

変数として扱うパラメータの数が多いほど計算量が多くなり、実行時間も長くなります。

### Optuna
指定したハイパーパラメータの範囲の中で最適と思われる数値の組み合わせを作成し、
モデル学習を行うという流れを指定回数繰り返します。

以下は例です。

```
param = {
        'learning_rate':trial.suggest_float('learning_rate', 0.01, 0.30),
        'max_depth': trial.suggest_int('max_depth', 2, 10),
        'min_child_weight': trial.suggest_int('min_child_weight', 0, 4),
        'subsample': trial.suggest_float('subsample', 0.2, 1),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.2, 1),
        'reg_lambda': trial.suggest_int('reg_lambda', 2, 10),}
```
このように調整したいパラメータの範囲を指定します。

# データの準備
まずはモデル学習できるようにデータを準備します。

ライブラリをインポートします。

```
import pandas as pd
import numpy as np
import seaborn as sns
from sklearn.datasets import load_diabetes
```
データフレームを作成します。
(参考：[【Python】scikit-learnのdiabetes datasetを使って回帰分析をしてみる](https://zenn.dev/omochimaru/articles/7da562d0a143c1))

```
# データの読み込み
dia = load_diabetes()

# 目的変数
exp_data = pd.DataFrame(dia.data, columns=dia.feature_names)

# 説明変数
tar_data = pd.DataFrame(dia.target, columns=['target'])

# データを結合
df = pd.concat([exp_data, tar_data], axis=1)

df.rename({"s1":"tc", "s2":"ldl", "s3":"hdl", "s4":"tch", "s5":"ltg", "s6":"glu"}, axis=1, inplace=True)
df
```
dfは次のようになります。
![スクリーンショット 2025-04-23 155205.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3992991/0c0a74e4-cdce-4ecf-b55c-401b9498c7a6.png)

10個の説明変数と１つの目的変数からできています。
これを用いて、XGBoostのパラメータチューニングを行います。

# グリッドサーチの場合
コードは以下の通りです。(参考：[機械学習のパラメータチューニングフレームワークを比較してみた！](https://www.ai-lab.app/839/))

```
#必要なライブラリのインポート
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.metrics import root_mean_squared_error
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import r2_score

#目的変数と説明変数に分類し、トレーニングデータとテストデータに分割
X = df.iloc[:,:10]
y = df.iloc[:,10]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# 変数として扱うパラメータの値の設定
param_grid = {
    'learning_rate':[0.01, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30], 
    'max_depth': [2, 4, 6, 8, 10], 
    'min_child_weight':[0, 1, 2, 3, 4], 
    'subsample':[0.2, 0.4, 0.6, 0.8, 1],
    'colsample_bytree':[0.2, 0.4, 0.6, 0.8, 1], 
    'reg_lambda':[2, 4, 6, 8, 10],
}

# XGBoostの定数部分を指定。
base_model = xgb.XGBRegressor(
    objective='reg:squarederror',
    booster="gbtree",                             
    n_estimators=100,                         
)

#各パラメータの組み合わせにおいて交差検証
cv = KFold(n_splits=6, shuffle=True, random_state=42) 
grid_search = GridSearchCV(base_model, param_grid, cv=cv, scoring='neg_root_mean_squared_error')                     
grid_search.fit(X_train, y_train)

# 交差検定で最もスコアが良かったときのパラメータを用いてモデルを再学習
pred = grid_search.predict(X_test)
r2 = r2_score(y_test, pred)

#各結果の出力
print("最適パラメータ : " , grid_search.best_params_)
print("最適パラメータにおける交差検定でのRMSEの平均: " , -grid_search.best_score_)
print("最適パラメータにおける最終評価でのRMSE：", root_mean_squared_error(y_test, pred))
print(f"決定係数 (R^2): {r2}")
```
GridSearchCVを用いてグリッドサーチを行います。
各パラメータの組み合わせにおいて交差検定を行っています。
評価指標としてRMSEを用いています。

各パラメータの組み合わせにおける交差検定において
最も精度が良かった組み合わせを採用し、
テストデータを用いて最終的な精度を出力します。




# Optunaの場合
コードは以下の通りです。
(参考：[機械学習のパラメータチューニングフレームワークを比較してみた！](https://www.ai-lab.app/839/))
```
#必要なライブラリのインポート
import optuna
import xgboost as xgb
from sklearn.base import clone
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score, KFold
from sklearn.metrics import root_mean_squared_error
from sklearn.metrics import r2_score

#目的変数と説明変数に分類し、トレーニングデータとテストデータに分割
X = df.iloc[:,:10]
y = df.iloc[:,10]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

#XGBoostの定数部分を指定。
base_model = xgb.XGBRegressor(
    objective='reg:squarederror',
    booster="gbtree",                                
    n_estimators=100                       
)

#チューニングにおける関数
def objective(trial):
#各変数の範囲を指定
    param = {
        'learning_rate':trial.suggest_float('learning_rate', 0.01, 0.30),
        'max_depth': trial.suggest_int('max_depth', 2, 10),
        'min_child_weight': trial.suggest_int('min_child_weight', 0, 4),
        'subsample': trial.suggest_float('subsample', 0.2, 1),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.2, 1),
        'reg_lambda': trial.suggest_int('reg_lambda', 2, 10),}
    
    #base_modelに変数を組み込む
    model = clone(base_model).set_params(**param) 

    
    #交差検証
    cv = KFold(n_splits=6, shuffle=True, random_state=42) 
    score = cross_val_score(model, X_train, y_train, cv=cv, scoring='neg_root_mean_squared_error')
    score = -score #scoreを正にする
    mean_score = np.mean(score) #各回におけるscoreを平均
    return mean_score #scoreの平均を出力

# objective関数を用いてパラメータチューニング
study = optuna.create_study(direction='minimize',sampler=optuna.samplers.TPESampler(seed=0)) #結果の再現性を担保
study.optimize(objective, n_trials=100)
best_parameters = study.best_params
best_score = study.best_value

# 交差検定で最もスコアが良かったときのパラメータを用いてモデルを再学習
model = clone(base_model).set_params(**best_parameters) 
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
test_score = root_mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

#各結果の出力
print('最適パラメータにおける交差検定でのRMSEの平均：',best_score)
print('最適パラメータ :',best_parameters)
print('最適パラメータにおける最終評価でのRMSE：',test_score)
print(f"決定係数 (R^2): {r2}")
```
指定したハイパーパラメータの範囲の中で最適と思われる数値の組み合わせを作成し、モデル学習を行うという流れを100回繰り返します。
Optunaの場合においても、各パラメータの組み合わせにおいて交差検定を行い、
最も精度が良かった組み合わせを採用し、テストデータを用いて最終的な精度を出力します。
また、以下のコード
```
study = optuna.create_study(direction='minimize',sampler=optuna.samplers.TPESampler(seed=0)) #結果の再現性を担保
```
により、各実行において出力結果にばらつきがでないようにしています。
(参考：[Google ColaboratoryでOptunaを実行する方法（Studyの再現性を確保する）](https://daisuke20240310.hatenablog.com/entry/optuna2))
# 精度・実行時間比較

|       | グリッドサーチ | Optuna|
| :---- | :-- | :-- |
| **RMSE(交差検証)** | 56.5 | 57.4 |
| **RMSE(テスト)** | 54.1| 52.6 | 
| **決定係数** | 0.47 |0.50 | 
| **実行時間** | 50分40秒 | 9秒 |



RMSE(交差検証)：各回の交差検証において最もRMSEが低くなったときのその値。
RMSE(テスト)：各回の交差検証において最もRMSEが低くなったときのパラメータの組み合わせでモデルを再度作成し、そのモデルで測ったRMSEの値。
決定係数：再度作成したモデルが出力した予測データと、正解データを用いて計算した決定係数。
実行時間：上記コードを実行するのにかかった時間。

決定係数を見ればわかる通り、どちらの手法においても
良い精度は得られませんでした。
そもそもXGBoostとデータが合っていなかったのか、説明変数に
なにかしらの手を加えたほうが良かったのか、等々、
いろいろと考察した方が良さそうです。

実行時間に関しては、Optunaはグリッドサーチに比べて圧倒的に計算が早かったです。
Optunaでは100回試行、グリッドサーチでは約2万回試行なので、
それはそうであろうという結果でした。

# パラメータ比較

|       | グリッドサーチ | Optuna|
| :---- | :-- | :-- |
| **learning_rate** | 0.1 | 0.068 |
| **max_depth** | 2| 2 | 
| **min_child_weight** | 2 |4| 
| **subsample** | 0.6 | 0.22 |
| **colsample_bytree** | 0.2 | 0.85 |
| **reg_lambda** | 4 | 7 |

learning_rateとmax_depthにおいては、
グリッドサーチとOptunaとで近しいor等しい数値となりました。
それ以外はバラバラの数値となりました。