メモ帳

python, juliaで機械学習をやっていく

ハイパーパラメータ自動最適化ツール「Optuna」を更に便利に使えるラッパー関数をつくった

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でなくとも使えますし、本コードを少し変えれば他のフレームワークにも使えるようになると思うので、興味のある方はぜひお試し下さい。

ref: