LoginSignup
9
10

More than 3 years have passed since last update.

Pythonでめっちゃ簡単にできるベイズ最適化

Last updated at Posted at 2020-09-21

2020.09.22 この記事の続きになる記事を書きました。
scikit-optimizeのEarlyStopperで最適化を中断する

前置き

仕事でパラメータの最適化をすることがあるのと、職場で最適化問題の相談を受けることが多いので、めっちゃ簡単にベイズ最適化ができるscikit-optimizegp_minimizeについて、まとめておこうと思います。

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)

これだけで、サンプリングが回りだす。
とりあえず、今回は使いそうなパラメータだけ指定している。

最初のfuncxは最適化関数と探索空間を指定。
n_callsはサンプリング回数。
noiseは指定しないとガウス分布のノイズを載せて探索するので、必要に応じて変更。今回は0.0でノイズなしで評価する。
model_queue_sizegp_minimizescikit-learnGaussianProcessRegressorを使って、空間の評価値の予測と次のサンプリング点を探しているので、都度都度の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回のサンプリングで実空間とほぼ同じものを作りだすことができ、最適値も求めることができた。
○のマーカーは都度都度のサンプリング点、わかりにくいが、端っこをサンプリングし、空間の予測がある程度できてからは最小値付近を重点的にサンプリングしたように見える。
☆のマーカーが最終的に見つかった最適値。
Figure_1.png

Callbackの使い方

scikit-optimizeの使い方の記事は少ないながらもあるのに、今回これを書いたのは、実はCallbackの使い方を説明している日本語記事が全く見つからなかったから。
というわけで、Callbackの使い方は別記事にして、ここにリンクを貼ります。

2020.09.22 書きました。
scikit-optimizeのEarlyStopperで最適化を中断する

9
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10