2020.09.22 この記事の続きになる記事を書きました。
scikit-optimizeのEarlyStopperで最適化を中断する
前置き
仕事でパラメータの最適化をすることがあるのと、職場で最適化問題の相談を受けることが多いので、めっちゃ簡単にベイズ最適化ができるscikit-optimize
のgp_minimize
について、まとめておこうと思います。
インストール
pipで簡単インストール
pip install scikit-optimize
ソースコード全文
今回のソースコード全文。
ここでは要所のみ、実際には描画用のコードがあったりする。
import numpy as np
from skopt import gp_minimize
def func(param=None):
ret = np.cos(param[0] + 2.34) + np.cos(param[1] - 0.78)
return -ret
if __name__ == '__main__':
x1 = (-np.pi, np.pi)
x2 = (-np.pi, np.pi)
x = (x1, x2)
result = gp_minimize(func, x,
n_calls=30,
noise=0.0,
model_queue_size=1,
verbose=True)
インポート周り
import numpy as np
from skopt import gp_minimize
numpyとgp_minimizeだけ。
転用される場合は必要に応じて変更。
最適化関数
def func(param=None):
ret = np.cos(param[0] + 0.34) + np.cos(param[1] - 0.78)
return -ret
今回は二つの入力を受け取り、それぞれのコサイン関数値の合計が最大化する入力値を求める問題とした。
分かりやすいから。
ただ、さすがに芸がなさすぎるので、入力値にオフセット(0.34, -0.78)を加えた。
最終的にこのオフセットを相殺する(-0.34, 0.78)が求められれば、最適化成功と言えるものである。
なお、gp_minimize
という名前の通り、最小化しかできないので、戻り値は-をつけて負値にすることで、最小化を最大化としている。
探索空間の設定
x1 = (-np.pi, np.pi)
x2 = (-np.pi, np.pi)
x = (x1, x2)
今回はコサイン関数と知っているので、二つの入力変数の空間は同じで、(-π ~ π)が取りうる範囲。
変数ごとに取りうる最小と最大をListかTupleで指定し、最後にひとつのListかTupleにまとめて渡す必要があるので、最後にひとつにまとめている。
最適化
result = gp_minimize(func, x,
n_calls=30,
noise=0.0,
model_queue_size=1,
verbose=True)
これだけで、サンプリングが回りだす。
とりあえず、今回は使いそうなパラメータだけ指定している。
最初のfunc
とx
は最適化関数と探索空間を指定。
n_calls
はサンプリング回数。
noise
は指定しないとガウス分布のノイズを載せて探索するので、必要に応じて変更。今回は0.0でノイズなしで評価する。
model_queue_size
はgp_minimize
はscikit-learn
のGaussianProcessRegressor
を使って、空間の評価値の予測と次のサンプリング点を探しているので、都度都度のGaussianProcessRegressor
のインスタンスを保持する数。
指定しないと全部残していくので、メモリをどんどん食いつぶしていく。今回は1を指定して、常に最新のみ保持するようにした。
verbose
はサンプリング中の標準出力をありにしているだけ。Falseを指定すると何も出ない。
この最適化の結果がresult
に返される。
resultの中身について
dir関数で中身を参照してみると下記のメンバーが存在している。
In [1]:dir(result)
Out[1]:
['fun',
'func_vals',
'models',
'random_state',
'space',
'specs',
'x',
'x_iters']
概ね、下記の解釈でよいと思われる。
メンバー | 値 | 解釈 |
---|---|---|
fun | -1.9999999997437237 | 最適化中のもっともよかった評価値 |
func_vals | array([ 0.72436992, -0.2934671 , ・・・・・ -1.99988708, -1.99654543]) | 最適化中の都度都度の関数の戻り値(評価値) |
models | list(GaussianProcessRegressor) | 先の説明の通り、保持したGaussianProcessRegressor のインスタンスのList |
random_state | RandomState(MT19937) at 0x22023222268 | 乱数のシード |
space | Space([ Real(low=-3.141592653589793, high=3.141592653589793, prior='uniform', transform='normalize'), Real(low=-3.141592653589793, high=3.141592653589793, prior='uniform', transform='normalize')]) |
探索空間のオブジェクト |
specs | (多いので省略) Dictionaryが入っている |
どうも最適化の仕様がまとめて入っているっぽい |
x | [-2.3399919472084387, 0.7798573940377893] | 最適化された入力変数の値 |
x_iters | (多いので省略) List |
都度都度、どんな値をサンプリングしたかが入っている |
結果のまとめ
最後にresult
に入っていた値を用いて、プロットしたやつを張り付けておく。
ちょぉっと見にくいが、左図は実際のヒートマップ、縦軸がx1
で横軸がx2
なので、縦軸には下の方、横軸には少し右のほうに最小値(正負反転したので本当は最大値)がある。
右図は最適化の結果ですが、ヒートマップの色自体もGaussianProcessRegressor
の予測値で作ったもの。
30回のサンプリングで実空間とほぼ同じものを作りだすことができ、最適値も求めることができた。
○のマーカーは都度都度のサンプリング点、わかりにくいが、端っこをサンプリングし、空間の予測がある程度できてからは最小値付近を重点的にサンプリングしたように見える。
☆のマーカーが最終的に見つかった最適値。
Callbackの使い方
scikit-optimizeの使い方の記事は少ないながらもあるのに、今回これを書いたのは、実はCallbackの使い方を説明している日本語記事が全く見つからなかったから。
というわけで、Callbackの使い方は別記事にして、ここにリンクを貼ります。
2020.09.22 書きました。
scikit-optimizeのEarlyStopperで最適化を中断する