Preferred Network(PFN)が作ったハイパーパラメータ自動最適化ツール「Optuna」を超絶簡単に使うためのラーパー関数をつくりました。モデル名、モデルオブジェクト、引数名と型、範囲の5つをペタペタ書くだけでよしなに最適化してくれるようになりました。一度に複数のモデルに対してチューニングを行えます。
Optuna
2018年12月3日に公開されたPFN製のライブラリで、Gridsearchと違って総当りでハイパーパラメータを最適化するのではなく、ベイズ最適化アルゴリズムの一種を用いて最適なハイパーパラメータ領域を探索してみたいです。scikit-learnやtensorflowなど様々なフレームワークで使えます。
使い方
EvaluateFuncとObjectiveというラッパー関数を実装しました。中身ではなく、使い方を紹介します。
データセットを取得
Iris dataset を使用します。多クラス分類問題です。簡単のためにホールドアウト法で検証します。
from sklearn.model_selection import train_test_split from sklearn.datasets import load_iris iris = load_iris() X, y = iris.data, iris.target X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=0)
optuna用のパラメータ指定
最適化するモデルやハイパーパラメータなどの指定をしてみます。 以下のような範囲で精度が高くなるように一度に最適化します。
- Extra tree:
- n_estimators: 1から100までの整数
- max_depth: 1から100までの整数を5刻みに調整
- random_state: 128固定
- Ridge:
- alpha: 0.01から100までのfloatを対数スケールで
- Kneighbor:
- n_neighbors: 1から30までの整数
- algorithm: ball_treeかkd_treeのどちらか
from sklearn.ensemble import ExtraTreesClassifier from sklearn.linear_model import RidgeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score # モデル名とモデルオブジェクトの指定 trial_models = { 'Extra Trees': ExtraTreesClassifier, 'Ridge': RidgeClassifier, 'kneighbor': KNeighborsClassifier, } # 指定したモデルに設定するハイパーパラメータ名と値の型と範囲を指定します。 # 定数の場合はtupleに入れずにそのままvalueに指定します。 # 型の意味と範囲指定の書き方は以下のようになります。 # - int: integer. ex: ('int', 最小値, 最大値) # - uni: a uniform float sampling. ex: ('uni', 最小値, 最大値) # - log: a uniform float sampling on log scale. ex: ('log', 最小値, 最大値) # - dis: a discretized uniform float sampling. ex: ('dis', 最小値, 最大値, 間隔) # - cat: category. ex: ('cat', (文字列A, 文字列B, 文字列C, )) trial_condition = { 'Extra Trees': { 'n_estimators': ('int', 1, 100), 'max_depth': ('dis', 1, 100, 5), 'random_state': 128 }, 'Ridge': { 'alpha': ('log', 1e-2, 1e2) }, 'kneighbor': { 'n_neighbors': ('int', 1, 30), 'algorithm': ('cat', ('ball_tree', 'kd_tree')), } } # 最適化する指標の指定 score_metric = accuracy_score direction = 'maximize' # 最大化したい時は'maximize'、最小化したい時は'minimize'
Optunaの実行
準備完了です。Optunaを実行してハイパーパラメータを自動最適化します。
import optuna # 今回つくったラッパー関数 from optuna_sklearn import EvaluateFunc, Objective evaluate = EvaluateFunc(X_train, X_val, y_train, y_val, score_metric) objective = Objective(evaluate, trial_models, trial_condition) # studyを作成 study = optuna.create_study(direction=direction) # Create a new study. # 最適化を実行。 n_trialsで探索数を指定できる。 study.optimize(objective, n_trials=50)
結果
kneighborが勝ちました。ちょっと意外です。
# 最適解 print(study.best_params) # >>> {'classifier': 'kneighbor', 'kneighbor_n_neighbors': 19, 'kneighbor_algorithm': 'ball_tree'} print(study.best_value) # >>> 0.9736842105263158 print(study.best_trial) # >>> FrozenTrial(number=0, state=<TrialState.COMPLETE: 1>, value=0.9736842105263158, datetime_start=datetime.datetime(2019, 8, 27, 0, 6, 43, 125692), datetime_complete=datetime.datetime(2019, 8, 27, 0, 6, 43, 195718), params={'classifier': 'kneighbor', 'kneighbor_n_neighbors': 19, 'kneighbor_algorithm': 'ball_tree'}, distributions={'classifier': CategoricalDistribution(choices=('Extra Trees', 'Ridge', 'kneighbor')), 'kneighbor_n_neighbors': IntUniformDistribution(low=1, high=30), 'kneighbor_algorithm': CategoricalDistribution(choices=('ball_tree', 'kd_tree'))}, user_attrs={}, system_attrs={'_number': 0}, intermediate_values={}, params_in_internal_repr={'classifier': 2, 'kneighbor_n_neighbors': 19.0, 'kneighbor_algorithm': 0}, trial_id=0)
ベタ書きとの比較
得られたハイパーパラメータで改めて学習してみます。 ベタ書きで得られたスコアがoptunaで得られたスコアと同じになるのでoptuna内での学習に問題はなさそうですね。
clf = KNeighborsClassifier(n_neighbors=19, algorithm='ball_tree') clf.fit(X_train, y_train) y_pred = clf.predict(X_val) error = accuracy_score(y_val, y_pred) # >>> 0.9736842105263158
ラッパー関数の中身
以下のリンク先にあります。実質50行程度なのでコピーして使う想定です。 github.com
なお、N分割交差検証を行いたいなど、検証方法を変更したい場合は、上記リンク先のEvaluateFunc内を変更することで実現できます。
def EvaluateFunc(X_train, X_val, y_train, y_val, score_metric): def _evaluate_func(model_obj): """ evaluate model prediction. customize the followings if you want cross validation """ # N分割交差検証を行いたい場合はsklearn.model_selection.KFoldなど、 # をつかってここでデータを分割する。 # 学習 model_obj.fit(X_train, y_train) # validationデータでpredict y_pred = model_obj.predict(X_val) # 上記で指定した指標(score_metric)で性能を評価 error = score_metric(y_val, y_pred) # returnした値を最小化or最大化するように、 # Optunaがハイパーパラメータを最適化してくれる return error return _evaluate_func
Optunaはgridsearchより圧倒的に簡単に、同程度の精度が期待できるとても便利なツールなので使わない手はないと思います。Optuna自体はsklearnでなくとも使えますし、本コードを少し変えれば他のフレームワークにも使えるようになると思うので、興味のある方はぜひお試し下さい。