ハイパーパラメータを調整する場合、GridSearchやRandomSearchがよく紹介されているが、
GridSearchはめんどい。かといってRandomSearchは心もとない。
そんなとき、ベイズ最適化などの応用編が役に立ちそうだ。
Hyperoptはそのような応用編の1つ。公式から使い方をまとめる(訳しただけ)。
Hyperopt-sklearnもいずれ紹介したい。
fmin
基本となる関数だ。必要な項目は以下
- 最小化する目的関数
- 検索するハイパーパラメータの範囲
- (任意)検索したすべてのポイントの評価を格納するデータベース
- 使用する検索アルゴリズム
- 検索回数
単純化すると次のような実装になる。
from hyperopt import fmin, tpe, hp
best = fmin(object, space,algo=tpe.suggest,max_evals=100)
print(best)
戻り値(best)は、検索結果のうちobjectを最小にしたハイパーパラメータである。
最大化したいなら関数の戻り値にマイナス1をかければよい。
目的関数の定義
目的関数は単に値を返すだけでも機能するが、辞書型で返すこともできる。
keyは以下のとおり。
- status : (必須)hyperopt.STATUS_STRINGS 例)STATUS_OK
- loss : (必須)最小化しようとしている値
- attachments : 値は辞書型。大きな文字列を返したいとき、小さな文字列をkeyとして利用する。
- loss_variance : float - 確率的目的関数の不確実性
- true_loss : float - モデルの汎化誤差。組み込みのプロットルーチンを利用できる。
- true_loss_variance : float - 汎化誤差の不確実性
このように辞書型にして豊富な情報を返す場合、検索したすべてのポイントの評価を格納するデータベースが必要になるだろう。
検索したすべてのポイントの評価を格納するデータベース
次のようにTrialオブジェクトを利用する。
import pickle
import time
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials
def objective(x):
return {
'loss': x ** 2,
'status': STATUS_OK,
# -- store other results like this
'eval_time': time.time(),
'other_stuff': {'type': None, 'value': [0, 1, 2]},
# -- attachments are handled differently
'attachments':
{'time_module': pickle.dumps(time.time)}
}
trials = Trials()
best = fmin(objective,
space=hp.uniform('x', -10, 10),
algo=tpe.suggest,
max_evals=100,
trials=trials)
こうすることで、Trialオブジェクトから、検索中のobjectの戻り値を検査することができる。
- trials.trials - 検索のすべてを表す辞書のリスト
- trials.results - 検索中に 'objective'によって返された辞書のリスト
- trials.losses() - 損失の浮動小数点リスト(各 'ok' トライアルの)
- trials.statuses() - ステータス文字列のリスト
Trialオブジェクトを、MongDBとすることで、パラレルサーチができるようになる。
検索するハイパーパラメータの範囲
たとえばランダムフォレストのハイパーパラメータを検索する場合、次のように設定できる。
space = {
'criterion': hp.choice('dtree_criterion', ['gini', 'entropy']),
'max_depth': hp.choice('dtree_max_depth',
[None, hp.qlognormal('dtree_max_depth_int', 3, 1, 1)]),
'min_samples_split': hp.qlognormal('dtree_min_samples_split', 2, 1, 1),
}
hp.choice(label, options)
オプションの1つを返す。リストまたはタプル。 optionsの要素は、それ自体が確率的な式で [入れ子] にすることができる。
hp.randint(label, upper)
[0, upper) の範囲の乱数を返す。より遠い整数値と比較して、近くの整数値間の損失関数に相関がない場合に利用することを想定。例えばランダムシード。
hp.uniform(label, low, high)
low と high の間で均等に値を返す。
hp.quniform(label, low, high, q)
round(uniform(low, high) / q) * q のような値を返す。
離散値に適している。
hp.loguniform(label, low, high)
exp(uniform(low, high)) のような対数に一様に分布するように返す。
変数は区間 [exp(low), exp(high)] に制約される。
hp.qloguniform(label, low, high, q)
round(exp(uniform(low, high)) / q) * q のような値を返す。
離散値に適している。
hp.normal(label, mu, sigma)
平均ミューと標準偏差 sigma で正規分布している実際の値を返す。大小の制約はない。
hp.qnormal(label, mu, sigma, q)
round(normal(mu, sigma) / q) * q のような値を返す。
離散値に適している。
hp.lognormal(label, mu, sigma)
戻り値の対数が正規分布するように exp(normal(mu, sigma)) に従って描画された値を返す。変数は正の値に制限される。
hp.qlognormal(label, mu, sigma, q)
round(exp(normal(mu, sigma)) / q) * q のような値を返す。
離散値に適している。
パラレルサーチ
Hyperoptはパラレルサーチ機能を有しているが、これを活用するためにはMongoDBのインストールが必要。
まずはシェルでMongoDBを起動しておく
mongod --dbpath . --port 1234
実装は次のとおり
from hyperopt import fmin, tpe, hp
from hyperopt.mongoexp import MongoTrials
trials = MongoTrials('mongo://localhost:1234/foo_db/jobs', exp_key='exp1')
best = fmin(object, space, trials=trials, algo=tpe.suggest, max_evals=10)
fminを呼び出している間、次のコードをシェルで実行する。
hyperopt-mongo-worker --mongo=localhost:1234/foo_db --poll-interval=0.1
mongodbから作業項目をデキューし、object関数を評価し、結果をデータベースに保存し直します。 fmin関数が十分な数のハイパーパラメータを試した後、関数は戻り、上のスクリプトは終了する。
以降、同じfminを実行した場合、MongoDBを参照するので直ちに値を返す。max_evalsを増やしてfminを実行すると更なる検索を続ける。exp_keyを変更すれば全く新しい検索ができる。
ref