ディープラーニングは各層の種類、活性化層の種類、オプティマイザの種類、オプティマイザのハイパーパラメータ、などなどたくさんあり、
手で最適化していくのは大変です。
そんなとき、
1. グリッドサーチ:パラメーターの候補の組み合わせパターンを全て調べる。
2. ランダマイズドサーチ:パラメーターの候補の組み合わせパターンを全て調べる。
3. その他賢いアルゴリズム系のサーチ:ベイジアン最適化、Tree-structured Parzen Estimator Approach(hyperopt)
といった、ものが使えます。
グリッドサーチはハイパーパラメータが少ない機械学習アルゴリズムでは有効ですが、ディープラーニングではあまりおすすめしません。実際には2.か3.を選ぶことになります。
賢い系のアルゴリズムはより有望そうなパラメータを試そうとしてくれます。
今回はhyperoptを使った例を書いてみます。
エッセンスはGpyopt(ベイジアン最適化)などほかのライブラリでも変わりません。
モデルのパラメタライズ
まず、モデルをガンガンパラメタライズしてチューニングしやすいようにします。
class MLP(chainer.Chain):
def __init__(self, n_units1, n_units2, n_units3, n_units4, n_out, layer_num, activate):
super(MLP, self).__init__(
l1=L.Linear(None, n_units1),
l2=L.Linear(None, n_units2),
l3=L.Linear(None, n_units3),
l4=L.Linear(None, n_units4),
lfinal=L.Linear(None, n_out),
)
self.layer_num = layer_num
if activate == 'relu':
self.act = F.relu
else:
self.act = F.sigmoid
def __call__(self, x):
h1 = self.act(self.l1(x))
h2 = self.act(self.l2(h1))
if self.layer_num == 3:
return self.lfinal(h2)
h3 = self.act(self.l3(h2))
if self.layer_num == 4:
return self.lfinal(h3)
h4 = self.act(self.l4(h3))
if self.layer_num == 5:
return self.lfinal(h4)
これで層数と活性化関数の種類、各層のユニット数が可変になりました。
トレーニング関数の作成
次に、トレーニング部分を関数化します。
ここで、オプティマイザの種類などチューニングしたい対象をパラメタライズする必要があります。
def main(params):
epoch = 40
gpu = 0
n_out = 10
batchsize = 100
n_units1 = params['n_units1']
n_units2 = params['n_units2']
n_units3 = params['n_units3']
n_units4 = params['n_units4']
layer_num = params['layer_num']
activate = params['activate']
optimizer_name = params['optimizer_name']
lr = params['lr']
model = L.Classifier(MLP(n_units1, n_units2, n_units3, n_units4, n_out, layer_num,
activate))
if gpu >= 0:
chainer.cuda.get_device(gpu).use()
model.to_gpu()
if optimizer_name == 'Adam':
optimizer = chainer.optimizers.Adam()
elif optimizer_name == 'AdaDelta':
optimizer = chainer.optimizers.AdaDelta()
else:
optimizer = chainer.optimizers.MomentumSGD(lr=lr)
optimizer.setup(model)
これによって、ディクショナリであるparamsを変えることによって、異なるパラメータセットを試すことができます。
今回はepoch数とバッチサイズなどは固定してみました。
hyperoptのfminでは関数の出力が最小になるようなパラメータの組み合わせを探索します。
ですので、モデルの性能がよければよいほどmain関数が小さい値を返す必要があります。
簡単には、validation lossを返せばよいです。
trainer.extend(
extensions.PlotReport(['main/loss', 'validation/main/loss'],
'epoch', file_name='loss.png'))
trainer.extend(
extensions.PlotReport(
['main/accuracy', 'validation/main/accuracy'],
'epoch', file_name='accuracy.png'))
trainer.run()
valid_data = trainer._extensions['PlotReport'].extension._data
loss_data = [data for i, data in valid_data['validation/main/loss']]
best10_loss = sorted(loss_data)[:10]
return sum(best10_loss) / 10
ここでは、最もLossが少なかったベスト10のロスを平均化して返しています。
validation lossもある程度ノイズを含んでいるので、ある程度平均化したほうが実力に近い、と勝手に思っています。
trainerからlossの履歴を取り出すために、私はplot_reportのエクステンションから値を無理やり引っ張りだしていますが、もっといいやり方があるかもしれません。
hyperopt実行部分
最後に探索空間を定義して、hyper_optのfminを叩きます。これだけ。
if __name__ == '__main__':
space = {'n_units1': scope.int(hp.quniform('n_units1', 100, 300, 50)),
'n_units2': scope.int(hp.quniform('n_units2', 100, 300, 50)),
'n_units3': scope.int(hp.quniform('n_units3', 100, 300, 50)),
'n_units4': scope.int(hp.quniform('n_units4', 100, 300, 50)),
'layer_num': scope.int(hp.quniform('layer_num', 3, 5, 1)),
'activate': hp.choice('activate',
('relu', 'sigmoid')),
'optimizer_name': hp.choice('optimizer_name',
('Adam', 'AdaDelta', 'MomentumSGD')),
'lr': hp.uniform('lr', 0.005, 0.02),
}
best = fmin(main, space, algo=tpe.suggest, max_evals=200)
print("best parameters", best)
hyperoptでint型のパラメータを探索したい場合、scope.intで囲めばよいです。
max_evalsは探索回数で、大きい数を指定するほど時間はかかりますが、よりよいパラメータを見つけてくれる可能性があります。
結果
params = {'n_units1': 200,
'n_units2': 200,
'n_units3': 100,
'n_units4': 200,
'layer_num': 3,
'activate': 'sigmoid',
'optimizer_name': 'AdaDelta',
'lr': 0.017,
}
がベストパラメータでした(accuracyは98.17%)。探索は20回しかやってません。
やっぱりchainerのMNISTのExampleのように、200ユニットの三層構成がよいみたいです。
scriptの全文は
https://gist.github.com/fukatani/76f530e34a51ae2c2e2ea359f0f06c56
に載せました。