75

More than 5 years have passed since last update.

# Pythonでベイズ最適化を使ってハイパーパラメータを探索するライブラリ実装のメモ

Last updated at Posted at 2016-09-02

# はじめに

ベイズ最適化(参考：ベイズ最適化入門, 機械学習のためのベイズ最適化入門)を使うと、機械学習の時の各種Try&Errorで決めざるを得ないようなハイパーパラメータの探索を効率よく実施できる可能性があります。

## 今回使っている各種Version

• Python3.5
• numpy==1.11.1
• scikit-learn==0.17.1

# コード

bayesian_optimizer.py
``````from itertools import product
from sklearn.gaussian_process import GaussianProcess

# The MIT License (C) 2016 mokemokechicken

class BayesianOptimizer:
x_list = None
y_list = None
yielding_index = None
k_band = 5
verbose = False

def __init__(self, params):
self.params = params
self.keys = []
self.values = []
for k, v in sorted(self.params.items()):
self.keys.append(k)
self.values.append(v)

@property
def n_pattern(self):
return len(list(product(*self.values)))

def output(self, *args, **kwargs):
if self.verbose:
print(*args, **kwargs)

def supply_next_param(self, max_iter=None):
self.x_list = []
self.y_list = []
all_parameters = list(product(*self.values))  # [(0.01, [0, 0], 0, [10, 10]), (0.01, [0, 0], 0, [15, 15]), ...
index_space = [list(range(len(v))) for v in self.values]  # [[0], [0, 1, 2], [0], [0, 1, 2]]
all_index_list = list(product(*index_space))  # [(0, 0, 0, 0), (0, 0, 0, 1), ...

# examine 2 random points initially
idx = list(range(len(all_index_list)))
np.random.shuffle(idx)
searched_index_list = []
for index in idx[:2]:
param = self.to_param(all_parameters[index])
self.yielding_index = all_index_list[index]
searched_index_list.append(index)
yield param

# Bayesian Optimization
max_iter = int(min(max_iter or max(np.sqrt(self.n_pattern)*4, 20), self.n_pattern))  # 最大探索回数を適当に計算。
for iteration in range(max_iter):
k = 1 + np.exp(-iteration / max_iter * 3) * self.k_band  # kの値を徐々に減らして 探索重視 → 活用重視 にしてみる
gp = self.create_gp_and_fit(np.array(self.x_list), np.array(self.y_list))

mean_array, mse_array = gp.predict(all_index_list, eval_MSE=True)
next_index, acq_array = self.acquisition(mean_array, mse_array, k, excludes=searched_index_list)

self.output("--- Most Expected Predictions")
for acq, ps in sorted(zip(acq_array, all_parameters), reverse=True)[:3]:
self.output("%.2f: %s" % (acq, list(zip(self.keys, ps))))
self.output("--- Past Best Results")
for acq, vs, ps in self.best_results(3):
self.output("%.2f: %s" % (acq, list(zip(self.keys, vs))))

if next_index in searched_index_list:
break
searched_index_list.append(next_index)
self.yielding_index = all_index_list[next_index]
yield self.to_param(all_parameters[next_index])

@staticmethod
def create_gp_and_fit(x, y, max_try=100):
# この辺怪しい
theta0 = 0.1
for i in range(max_try+1):
try:
gp = GaussianProcess(theta0=theta0)
gp.fit(x, y)
return gp
except Exception as e:
theta0 *= 10
if i == max_try:
print(theta0)
raise e

def to_param(self, row):
return dict(zip(self.keys, row))

def report(self, score):
self.x_list.append(self.yielding_index)
self.y_list.append(score)

def best_results(self, n=5):
index_list = []
param_list = []
for xs in self.x_list:
values = [self.values[i][x] for i, x in enumerate(xs)]
index_list.append(values)
param_list.append(self.to_param(values))
return sorted(zip(self.y_list, index_list, param_list), reverse=True)[:n]

@staticmethod
def acquisition(mean_array, mse_array, k, excludes=None):
excludes = excludes or []
values = mean_array + np.sqrt(mse_array) * k
for_argmax = np.copy(values)
for ex in excludes:
for_argmax[ex] = -np.Inf
return np.argmax(for_argmax), values
``````

# 使い方

``````params = {
"parameter1": [10, 20, 25, 50],
"parameter2": ['p1', 'p2'],
...
}

bo = BayesianOptimizer(params)  # パラメータの組み合わせを辞書で渡す

for param in bo.supply_next_param():  # 次調べたほうが良いparamをdictで渡される
y = unknown_score_function(param)              # なんらかの方法でその param を評価する
bo.report(y)                                    # 評価値を教えてあげる

print(bo.best_results())                            # 一応一番良いScoreの値やparamは覚えてくれている
``````

# ２次元空間でのデモ

ちゃんと動くか Jupyter notebook上で試したものを以下に置いてます。
https://gist.github.com/mokemokechicken/7f3cf33c71d33bf0ec242c37cb3f2a75

## 探索

こんな感じで進んでいきます。

### 序盤

まずは全体的に空間を調べています。少し囲碁っぽいｗ。

### 中盤２

もう一つの良さそうな左下の山も丹念にチェック。真ん中上の本命周辺も結構網羅。

## 所感

• 200回の探索で50x50のパラメータ空間から良いポイントを見つけられました。必ずしも最善の地点を見つけられはしませんが、センスの良い探索をしてくれています。
• 今回のように探索空間が凸凹していると(?) sklearnのGaussianProcessの `fit()``Exception` が発生することがしばしばあります。 「`theta0``thetaU` を 大きくしたらどうだ」と言われるので、今のところそれで対応しています（それで良いのかよくわかってないです）。実際に機械学習のハイパーパラメータの評価のケースでは、結構ノイズも大きいのでこの問題はしばしば起こります。
• 今回の実装では、パラメータの「値」ではなく「配列上の位置」を使って共分散を計算しているので、文字列パラメータにも対応していますが、順序が無い値(例えば、 ['dog', 'cat', 'bird'] とか)だと少し変な感じです。

# さいごに

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
What you can do with signing up
75