Optunaをサクッと動かす方法|ハイパーパラメータの自動最適化を実践
Optunaは、ハイパーパラメータの自動最適化を簡単に行える便利なフレームワークです。本記事では、Optunaでハイパーパラメータを調整するためのサンプルコード(テンプレート)を紹介し、私がよく使っている応用例について解説します。
Optunaとは
Optunaは、日本のPrefferdNetworkが開発したハイパーパラメータの最適値を探索するためのフレームワークです。機械学習やディープラーニングでは、性能を引き出すためにハイパーパラメータのチューニングが必要ですが、このパラメータを自動チューニングするツールがoptunaになります。
この記事では、私がOptunaを使う場合の使い方について解説します。
もしかしたら、制作者側が意図しない使い方かもしれませんが、「私はこんな使い方をしています」というものになります。
使い方(基本)
インストール
インストールはpip
で行うことができます
pip install optuna
テンプレート的なコード
以下のコードは私がOptunaを使う場合のテンプレート的なコードになります。このコードでは、ta * X + tb
にノイズの入ったデータからta
とtb
を推定するものです。
import numpy as np
import optuna
# 1. データ準備
n = 1000
ta = 2
tb = 3
X = np.random.uniform(0, 10, n)
Y = ta * X + tb + np.random.randn(n)
print(X)
print(Y)
# 2. パラメータを受け取り処理する関数
def f(a, b) :
y = a * X + b
res = np.sum((Y-y)**2)
return res
# 3. Optunaが最適化で呼び出す関数
def objective(trial):
a = trial.suggest_float('a', -10, 10)
b = trial.suggest_float('b', -10, 10)
loss = f(a, b)
return loss
# 4. Optunaの実行部分
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=100)
# 5. 結果の表示
print('best params:', study.best_params)
大まかな処理の流れは以下になります。
- データ準備
f(a,b)
で使うデータを準備する関数です。サンプルではY = ta*X + tb + εという直線上の点にノイズεを加えたデータを作成しています。 - パラメータを受け取り処理する関数
a,b
を受け取り、Y = ta*X + tb + εとの差の絶対値の二乗を計算しています(MSE)。戻り値が小さいほどYとa,b
から計算したyとの差が小さくなります。 - Optunaが最適化で呼び出す関数
Optunaの引数で与えるObjective
関数です。パラメータa,b
をOptunaが決定するためにsuggest_float
を呼び出しています。また、f(a,b)
を呼び出し、その結果をloss
として返しています(結果を数値として返すことが重要)。 - Optunaの実行部分
create_study
でオブジェクトを生成してoptimize
で最適化を行なっています。direction='minimize'
としているので「小さいほど良い結果」となり、n_trail=100
と設定しているので、100回繰り返しを行います。 - 結果の表示
最後に、ベストのパラメータを表示しています。
上記のプログラムを実行すると、おおよそa=2, b=3が結果として戻ります。線形回帰の代わりにOptunaで最適化をかけている例になりますが、基本はこの形で利用できるのでコードパターンとして覚えておくと良いです。
f()
の部分は、実際は、ディープラーニングのモデルの学習・推論や、lightGBMによる学習・予測などになります。
なお、suggest_xxxx
には以下のようなものが準備されています(代表的なもの)
関数名 | 内容 |
suggest_categorical | カテゴリ変数 |
suggest_float | 浮動小数点 |
suggest_int | 整数 |
応用編
1. 初期に探索するパラメータを設定する
Optunaでは初期の探索パターンを与えることが可能です。
# 3. Optunaの実行部分
study = optuna.create_study(direction='minimize')
study.enqueue_trial({'a': 1.0, 'b': 2.0})
study.enqueue_trial({'a': 2.0, 'b': 3.0})
study.enqueue_trial({'a': 3.0, 'b': 4.0})
study.optimize(objective, n_trials=10)
2. 続きから探索させる
Optunaを使う場合、f()
の処理がかなり重かったり、また、別の環境で実行して結果だけ移すということがあります(少なくとも、私は何度か経験しました)。例えば、学習などでは、数時間かけて学習・評価を行うことなんかもあるわけで、Optunaから呼び出して実行というのが結構難しかったりします。
私は、enqueue_trial
とペアになる結果を与えて、次のパラメータを予測させるという方法をとっていました。コードのイメージは以下のようになります。
ab = (({'a': -2.50919762305275, 'b': 9.014286128198323}, 435299.07726309635),
({'a': -8.466331566873603, 'b': -8.386458410932443},4875667.5918871))
cnt = 0
def objective(trial):
global cnt, ab
a = trial.suggest_float('a', -10, 10)
b = trial.suggest_float('b', -10, 10)
if cnt < len(ab):
loss = ab[cnt][1]
cnt += 1
return loss
print(f"a : {a}, b : {b}")
input()
loss = f(a, b)
return loss
study = optuna.create_study(direction='minimize')
for e in ab:
study.enqueue_trial(e[0])
study.optimize(objective, n_trials=10)
objective
関数では、保存されている事前の結果に対しては対応する結果をそのまま返し、新しいものについては、input()
で停止させています。表示されたa, b
をコピーして新しいパラメータで実行し、ab
に追加するというのを繰り返します。
面倒ですが、このやり方は意外と便利です。
乱数を初期化しても同じ結果にならないのがちょっと面倒ですが、とりあえずこの方法を使っていました。
なお、n_trail
はあらかじめ決めた試行回数をいれておいてください(3回目だから3とかしないように)。
まとめ
Optunaの簡単な使い方と、私が使っているTIPS的な部分いついて解説しました。Optunaの記事は多いのですでにどこかに書かれている情報かもしれませんが、参考になれば幸いです。