機械学習のパラメータチューニングフレームワークを比較してみた!

データサイエンスにおいて、最後の詰めで必要となるハイパーパラメータチューニング。その説明と実装例、比較までを簡単かつ簡潔に説明していきます!

1. ハイパーパラメータとは?

 機械学習モデルではあるデータを学習する際に、学習速度や方法、モデル独自のパラメータなど複数のパラメータを設定する必要があります。このパラメータ群をハイパーパラメータと呼びます。これをモデルの精度を高めるために、パラメーターを調整することをハイパーパラメータチューニングと呼びます。

 パラメータチューニングには大きく分けて「手動」で調整を行う方法と「自動」で調整を行う方法の2種類があります。手動の方は、自分で1つずつ値を調整しながら、モデルの精度を高めていく方法です。職人技なところがあります。自動の方は、ある設定した範囲のパラメータ空間の中で一番精度がよかったものを自動で選択する手法です。あまりパラメータチューニングの経験がなくても自動でチューニングを行って、ベストなパラメータを返してくれるので便利です。

2. 比較するハイパーパラメータチューニングフレームワーク

 今回紹介するパラメータチューニング関数は以下の4つです。概要、探索方法、メリット、デメリットをそれぞれ説明していきます。

2-a. GridSearchCV

 scikit-learnのパッケージにある交差検証とグリッドサーチを行うことができる関数です。通常、パラメータチューニング以前に偏ったデータによる学習の評価を回避するために、交差検証により、データセットの範囲内で偏りなくモデル評価する必要があります。グリッドサーチとは、各パラメータ範囲内の”全ての組み合わせ”を設定し計算する手法です。

メリット
  • 設定したパラメータの範囲を網羅的に探索するため、漏れがない。
  • 範囲が狭い、または、探索領域が少ない場合に有効。
  • 簡潔にコーディングができる。
デメリット
  • 設定したパラメータの範囲が広くなると計算量が膨大になってしまう。

2-b. RandomizedSearchCV

 scikit-learnのパッケージにある交差検証とランダムサーチを行うことができる関数です。ランダムサーチとは、各パラメータ範囲内で”ランダムな組み合わせ”を設定し計算する手法です。

メリット
  • 調整するパラメータの範囲が大きくなっても、計算量を抑えつつチューニングすることができる。
  • 範囲が狭い、または、探索領域が少ない場合に有効。
  • 簡潔にコーディングができる。
デメリット
  • 最終的な精度がぶれやすい。
  • パラメータ探索の網羅性がないため、漏れが出やすい。

2-c. BayesianOptimization

 BayesianOptimization(bayes_opt)パッケージにあるベイズ最適化を行うことができるフレームワークです。ベイズ最適化とは、未知の空間からガウス過程の事前分布を用いて、目標関数の最大または最小値を示すパラメータを特定する手法です。この手法は前の結果を使ってパラメータを特定するため、グリッドサーチやランダムサーチに比べて合理的と言えます。

メリット
  • 最適化と計算コストのバランスが取ることができる。
  • パラメータチューニングに独自の評価関数の設定などのカスタム性が良い。
デメリット
  • コード量が多くなる。
  • 自分で交差検証の関数を設ける必要がある。

2-d. Optuna

 2018年末に発表されたOptunaパッケージにあるTree-structured Parzen Estimatorというベイズ最適化アルゴリズムを用いてパラメータ最適化を行うことができる関数です。前のBayesianOptimizationとは分類としては同じですが、内部に採用されているアルゴリズムが異なります。詳しく知りたい方は外部リンクをどうぞ!

メリット
  • BayesianOptimizationより最適化と計算コストのバランスが取ることができ、広い範囲でパラメータ探索できる。
  • パラメータチューニングに独自の評価関数の設定などのカスタム性が良い。
デメリット
  • コード量が多くなる。
  • 自分で交差検証の関数を設ける必要がある。

3. 比較条件

 それぞれの手法を比較するためにある程度の条件を揃えていきたいと思います。使うデータセットは昔から機械学習のベンチマークとして用いられた手書き文字認識です。データセットはscikit-learnパッケージから直接データを取得することができ、以下のようなスペックです。

  • クラス数:10
  • クラスごとのサンプル数:~180
  • 全体のサンプル数:1797
  • 次元数:8×8=64
  • 特徴量:0~16の整数

 ランダムにトレーニングデータ:テストデータを7:3に分割し、トレーニングデータを交差検証で5分割にして評価していきます。学習モデルはXGboostというアンサンブル学習と決定木を組み合わせた手法を用います。探索範囲は、決定木の深さを2-100、識別器に通す回数を10-1000として学習させてみます。ランダムサーチ、ベイズ最適化の探索回数は約200回行います。グリッドサーチは探索範囲の目を少々荒くして他の探索回数と同等にして探索します。

4. 比較結果

 結果は、以下の表1のようになりました。パラメータと探索時間は小数点以下を切り捨て、識別率は小数点以下3桁までを示しています。パラメータmax_depthはどのフレームワークでも一致して”2″となりました。パラメータn_estimatorsは400±50にはおさまっているものの、若干異なる結果を示していました。識別のカラムにおいて、赤いアンダーラインが引かれているところが、最も良い識別率です。テストデータの識別率ではBayesian Optimizationが最も高く、学習データの識別率ではOptunaが最も高い結果となりました。探索時間に関してはGridSearchCV、RandomizedSearchCVが同程度の計算時間がかかり、それらに比べてBayesian Optimization、Optunaは1.4倍ほど遅い結果となりました。これは、最適化のための計算過程の違いによるものと考えられます。

探索フレームワークパラメータ
(max_depth)
パラメータ
(n_estimators)
識別率
(学習データ)
識別率
(テストデータ)
探索時間
[秒]
GridSearchCV23600.9650.966908
RandomizedSearchCV23450.9620.963876
Bayesian Optimization24370.8840.9761240
Optuna24150.9680.9481397
表1 パラメータ探索結果

5. 各ソースコード

実際に実行したソースコードを示します。

6-1. GridSearchCV

import xgboost as xgb
import os
import random
import time
import numpy as np
import pandas as pd
import warnings

from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits

if __name__ == '__main__':

    print(xgb)

    digits = load_digits()


    warnings.filterwarnings('ignore')
    X = digits.data
    y = digits.target
    X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)
    params = {"objective":"multi:softmax","num_class":10, "tree_method": 'gpu_hist'}
    print(params)
    model = xgb.XGBClassifier(params)

    start = time.time()
    print(np.arange(2,100,10))
    print(np.arange(10,1000,50))
    grid_search = GridSearchCV(model, {"max_depth":np.arange(2,100,10), "n_estimators":np.arange(10,1000,50)}, cv=5, verbose=1, n_jobs=-1)
    grid_search.fit(X_train, y_train)
    print("Best_parameter : " , grid_search.best_params_)
    print("精度: " , grid_search.best_score_)
    elapsed_time = time.time() - start

    pred = grid_search.predict(X_test)
    print("テスト精度:", accuracy_score(y_test, pred))

    print ("パラメータ探索時間:{0}".format(elapsed_time) + "[sec]")
    result_df = pd.DataFrame(grid_search.cv_results_)
    result_df.sort_values(by="rank_test_score", inplace=True)
    print(result_df[["rank_test_score", 
                        "params", 
                        "mean_test_score"]])
    result_df.to_csv('acc_grid_digits.csv')

6-2. RandomizedSearchCV

import os
import random
import time
import numpy as np
import pandas as pd
import warnings
from sklearn import model_selection
import xgboost as xgb

from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import RandomizedSearchCV
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits

if __name__ == '__main__':
    digits = load_digits()

    warnings.filterwarnings('ignore')
    X = digits.data
    y = digits.target
    X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)
    params = {"objective":"multi:softmax","num_class":10}
    print(params)
    model = xgb.XGBClassifier(params)

    start = time.time()
    params = {"max_depth":np.arange(2,100,1), "n_estimators":np.arange(10,1000,1)}
    random_search = RandomizedSearchCV(model, params, cv=5, iid=True, return_train_score=False, n_iter=200, n_jobs=-1)
    random_search.fit(X_train, y_train)
    print("Best_parameter : " , random_search.best_params_)
    print("精度: " , random_search.best_score_)
    elapsed_time = time.time() - start

    pred = random_search.predict(X_test)
    print("テスト精度:", accuracy_score(y_test, pred))

    print ("パラメータ探索時間:" + str(elapsed_time) + "[sec]")

    result_df = pd.DataFrame(random_search.cv_results_)
    result_df.sort_values(by="rank_test_score", inplace=True)
    print(result_df[["rank_test_score", 
                        "params", 
                        "mean_test_score"]])
    result_df.to_csv('acc_random_digits.csv')

6-3. Bayesian Optimization

import os
import random
import time
import numpy as np
import pandas as pd
import warnings
from sklearn import model_selection
import xgboost as xgb

from sklearn.model_selection import train_test_split
from bayes_opt import BayesianOptimization
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits

def xgb_cv(n_estimators, max_depth):
    model = xgb.XGBClassifier(n_estimators=int(n_estimators), max_depth = int(max_depth), objective="multi:softmax", num_class=10)
    score = cross_val_score(model, X_train, y_train, cv=5, scoring="r2").mean()
    return score

bo = BayesianOptimization(
    xgb_cv, 
    {'n_estimators': (10, 1000), 
     'max_depth': (2, 100)
    },
    verbose=2
)

if __name__ == '__main__':

    digits = load_digits()

    warnings.filterwarnings('ignore')
    X = digits.data
    y = digits.target
    X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)

    start = time.time()

    bo.maximize(n_iter=195)

    print("Best_parameter : " , bo.max['params'])
    print("精度: " , bo.max['target'])
    elapsed_time = time.time() - start

    model = xgb.XGBClassifier(n_estimators=int(bo.max['params']['n_estimators']), max_depth=int(bo.max['params']['max_depth']), objective="multi:softmax", num_class=3)
    model.fit(X_train, y_train) 
    pred = model.predict(X_test)
    print("テスト精度:", accuracy_score(y_test, pred))

    print ("パラメータ探索時間:" + str(elapsed_time) + "[sec]")

    result_df = pd.DataFrame(bo.res)
    print(result_df)
    result_df.to_csv('acc_bo_digits_all.csv')
    res_target = result_df['target']
    res_target.to_csv('acc_bo_digits.csv')

6-4. Optuna

import os
import random
import time
import numpy as np
from numpy.core.fromnumeric import size
import pandas as pd
import warnings
import xgboost as xgb
import optuna

from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits

def objective(trial):

  max_depth = trial.suggest_int("max_depth", 2, 100)
  n_estimators = trial.suggest_int("n_estimators", 10, 1000)
  model = xgb.XGBClassifier(max_depth=max_depth, 
                            n_estimators = n_estimators, 
                            objective = "multi:softmax",
                            num_class = 10)
  score = cross_val_score(model, X_train, y_train, cv=5, n_jobs=-1)
  print(score)
  r2_mean = np.mean(score)
  print(r2_mean)
  acc_list.append(r2_mean)

  return r2_mean


if __name__ == '__main__':
  digits = load_digits()

  warnings.filterwarnings('ignore')
  X = digits.data
  y = digits.target
  X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.3, shuffle=True)
  print(X.shape)
  print(y.shape)

  acc_list = []

  start = time.time()
  study = optuna.create_study(direction='maximize')
  study.optimize(objective, n_trials=200)
  print("Best_parameter : " , study.best_params)
  print("精度: " , study.best_value)
  elapsed_time = time.time() - start


  df = pd.DataFrame({'acc': acc_list})
  df.to_csv('acc_optuna_digits.csv')

  best_params = {
        "max_depth": study.best_params["max_depth"],
        "n_estimators": study.best_params["n_estimators"],
        "objective": "multi:softmax",
        "num_class":3
    }

  model = xgb.XGBClassifier(best_params)
  model.fit(X_train, y_train)
  pred = model.predict(X_test)
  print("テスト精度:", accuracy_score(y_test, pred))

  print ("パラメータ探索時間:" + str(elapsed_time) + "[sec]")

6. 最後に

いかがだったでしょうか?

どの探索フレームワークもある程度精度の良いパラメータを選択してくれることがわかりました。しかし、識別率のコンマ数桁の違いがKaggleでは大きな差を生みます。メリット・デメリットを考えて、この記事がフレームワークを選ぶ1つの参考資料になっていただければ幸いです。

参考文献

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA