verilog書く人

自称ASIC設計者です。どなたかkaggle一緒に出ましょう。

chainerで作ったDeep Learningモデルのハイパーパラメータチューニングを自動化してみる

ディープラーニングは各層の種類、活性化層の種類、オプティマイザの種類、オプティマイザのハイパーパラメータ、などなどたくさんあり、
手で最適化していくのは大変です。

そんなとき、

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()

    # Setup an optimizer
    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'))

    # Run the training
    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

に載せました。