snovaのブログ

主にプログラミングやデジタルコンテンツについて書きます。最近はPython, Flutter, VRに興味があります。

Kerasでハイパーパラメータを自動調整したいならHyperas

イントロダクション

ニューラルネットの問題点として、ハイパーパラメータが多く、しかもチューニング次第で大きく精度が変わることが知られています。
自動調整にはいくつか方法がありますが、なんとなく敷居が高そうだなと感じていました。
そんなとき、以下の記事を見つけたので、MNISTで実践してみました。
Kerasだってハイパーパラメータチューニングできるもん。【hyperas】

目次

計算機環境

  • OS : Ubuntu 16.04
  • Python : 3.5.5
  • Keras : 2.2.0
  • TensorFlow : 1.9.0

インストール

公式に従って、pipでインストールします。
- hyperopt 公式

pip install hyperas

動作チェックは、

import hyperas

コマンドについて

モジュールのインポート

hyperoptモジュールも使用します。

from hyperopt import Trials, STATUS_OK, tpe, rand

from hyperas import optim
from hyperas.distributions import choice, uniform

選択

choiceコマンドを使います。

ex. ) 活性化関数にrelusigmoidのどちらかを選択するとき

model.add(Activation({{choice(['relu', 'sigmoid'])}}))

数値の自動調整

uniformコマンドを使います。

ex. ) Dropout率を0から1の間で調整するとき

model.add(Dropout({{uniform(0, 1)}}))

層の数を増やすとき

層の数を増やしたいときは、if文とchoiceを併用します。

ex. ) 2層目を追加するかどうか

if {{choice(['one', 'two'])}} == 'two':
    model.add(Dense(100))
    model.add(Activation('relu'))

自動調整のための関数選び

hyperoptモジュールを使って、自動調整を行っていますが、以下の2つの方法が用意されています。
tpe : TPEで自動調整
rand : ランダムサーチで自動調整

ex. ) TPEで自動調整するとき
条件 :
- 学習モデルは関数create_modelの返り値
- データは関数dataの返り値
- 最大5回調整

best_run, best_model = optim.minimize(model=create_model,
                                      data=data,
                                      algo=tpe.suggest,
                                      max_evals=5,
                                      trials=Trials())

hyperoptやTPEに関しては以下の記事を参照
- hyperoptって何してんの?
- Hyperoptなどのハイパーパラメータチューニングとその関連手法についてのメモ
- 予測モデルを使ったシミュレーションと最適解探索
- 機械学習モデルのハイパパラメータ最適化

テストしてみる

公式のサンプルでは、多層パーセプトロンのモデルを構築して、MNISTのハイパーパラメータを自動調整しています。
参考サイト : hyperas - github

サンプルコードをベースにして作ったコードは以下

import numpy as np

from hyperopt import Trials, STATUS_OK, tpe
from keras.datasets import mnist
from keras.layers.core import Dense, Dropout, Activation
from keras.models import Sequential
from keras.utils import np_utils

from hyperas import optim
from hyperas.distributions import choice, uniform


def data():
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train = x_train.reshape(60000, 784)
    x_test = x_test.reshape(10000, 784)
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')
    x_train /= 255
    x_test /= 255
    nb_classes = 10
    y_train = np_utils.to_categorical(y_train, nb_classes)
    y_test = np_utils.to_categorical(y_test, nb_classes)

    return x_train, y_train, x_test, y_test


def create_model(x_train, y_train, x_test, y_test):
    model = Sequential()
    model.add(Dense(512, input_shape=(784,)))
    model.add(Activation('relu'))
    model.add(Dropout({{uniform(0, 1)}}))
    model.add(Dense({{choice([256, 512, 1024])}}))
    model.add(Activation({{choice(['relu', 'sigmoid'])}}))
    model.add(Dropout({{uniform(0, 1)}}))

    if {{choice(['three', 'four'])}} == 'four':
        model.add(Dense(100))
        model.add({{choice([Dropout(0.5), Activation('linear')])}})
        model.add(Activation('relu'))

    model.add(Dense(10))
    model.add(Activation('softmax'))

    model.compile(loss='categorical_crossentropy', metrics=['accuracy'],
                  optimizer={{choice(['rmsprop', 'adam', 'sgd'])}})

    result = model.fit(x_train, y_train,
              batch_size={{choice([64, 128])}},
              epochs=2,
              verbose=2,
              validation_split=0.1)

    validation_acc = np.amax(result.history['val_acc']) 
    print('Best validation acc of epoch:', validation_acc)

    return {'loss': -validation_acc, 'status': STATUS_OK, 'model': model}


if __name__ == '__main__':
    best_run, best_model = optim.minimize(model=create_model,
                                          data=data,
                                          algo=tpe.suggest,
                                          max_evals=5,
                                          trials=Trials())
    X_train, Y_train, X_test, Y_test = data()
    print("Evalutation of best performing model:")
    print(best_model.evaluate(X_test, Y_test))

    print("Best performing model chosen hyper-parameters:")
    print(best_run)
    
    # ソート
    print('--- sorted ---')
    sorted_best_run = sorted(best_run.items(), key=lambda x : x[0])
    for i, k in sorted_best_run:
        print(i + ' : ' + str(k))

このとき、関数datacreate_modelの返り値は順番に注意

結果 :

...

Evalutation of best performing model:
10000/10000 [==============================] - 0s 19us/step
[0.10739784885103582, 0.9693]
Best performing model chosen hyper-parameters:
{'Dropout': 0.03323327852409652, 'Activation': 1, 'Dense': 2, 'Dropout_1': 0.0886198698550964, 'optimizer': 0, 'batch_size': 1, 'add': 0, 'Dropout_2': 1}
--- sorted ---
Activation : 1
Dense : 2
Dropout : 0.03323327852409652
Dropout_1 : 0.0886198698550964
Dropout_2 : 1
add : 0
batch_size : 1
optimizer : 0

このままだと、読みにくいのでまとめると、

パラメータ
3層目の活性化関数 sigmoid
3層目のノード数 1024
2層目のDropout率 0.03323327852409652
3層目のDropout率 0.0886198698550964
4層目を追加するか? Yes
5層目にDropout層 or 活性化関数追加? ドロップアウト層を追加
バッチ数 128
最適化関数 rmsprop

使用時の注意

学習のたびに、出力の順番が入れ替わる

print(best_run)で出力される結果だけに注目すると、2回目の学習では以下のように出力されました。

...

Best performing model chosen hyper-parameters:
{'Dropout_2': 1, 'add': 0, 'optimizer': 0, 'Activation': 1, 'batch_size': 1, 'Dropout_1': 0.0886198698550964, 'Dense': 2, 'Dropout': 0.03323327852409652}

...

1回目ではDropoutの項目が一番目に出力されていましたが、2回目ではaddの項目が1番目に出力されています。

正直、読みにくいので、対処法を考えました。
print(type(best_run))で型を調べたら、<class 'dict'>だったので、ソートしてforで出力することに。

sorted_best_run = sorted(best_run.items(), key=lambda x : x[0])
for i, k in sorted_best_run:
    print(i + ' : ' + str(k))

結果 :

Activation : 1
Dense : 2
Dropout : 0.03323327852409652
Dropout_1 : 0.0886198698550964
Dropout_2 : 1
add : 0
batch_size : 1
optimizer : 0

relusigmoidのどちらを選択したのかを明示的に出力するには、コードを結構変えないといけないので、今回は割愛します。

コメントアウトしている行も自動調整されている?

Dropout層の追加する行をコメントアウトで挿入し、検証しました。
モデルを構築している部分のコードを以下のように変えます。

# before

...

model = Sequential()
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dropout({{uniform(0, 1)}}))
model.add(Dense({{choice([256, 512, 1024])}}))
model.add(Activation({{choice(['relu', 'sigmoid'])}}))
model.add(Dropout({{uniform(0, 1)}}))

if {{choice(['three', 'four'])}} == 'four':
    model.add(Dense(100))
    model.add({{choice([Dropout(0.5), Activation('linear')])}})
    model.add(Activation('relu'))
        
...
# after

...

model = Sequential()
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dropout({{uniform(0, 1)}}))
model.add(Dense({{choice([256, 512, 1024])}}))
model.add(Activation({{choice(['relu', 'sigmoid'])}}))
model.add(Dropout({{uniform(0, 1)}}))

# --- add --- #
# model.add(Dropout({{uniform(0, 1)}}))
# model.add(Dropout({{uniform(0, 1)}}))
# model.add(Dropout({{uniform(0, 1)}}))
# model.add(Dropout({{uniform(0, 1)}}))

if {{choice(['three', 'four'])}} == 'four':
    model.add(Dense(100))
    model.add({{choice([Dropout(0.5), Activation('linear')])}})
    model.add(Activation('relu'))

...

結果は、

# before

...

Best performing model chosen hyper-parameters:
{'Dropout': 0.03323327852409652, 'Activation': 1, 'Dense': 2, 'Dropout_1': 0.0886198698550964, 'optimizer': 0, 'batch_size': 1, 'add': 0, 'Dropout_2': 1}
--- sorted ---
Activation : 1
Dense : 2
Dropout : 0.03323327852409652
Dropout_1 : 0.0886198698550964
Dropout_2 : 1
add : 0
batch_size : 1
optimizer : 0
# after

...


Best performing model chosen hyper-parameters:
{'Dropout_2': 0.9662681038993752, 'Dropout_5': 0.8366666847115819, 'Dropout_3': 0.011106434718081704, 'add': 1, 'optimizer': 0, 'Dropout_1': 0.06765709934504838, 'Dense': 1, 'batch_size': 0, 'Dropout': 0.4970559482092457, 'Dropout_4': 0.9770005173795487, 'Activation': 1, 'Dropout_6': 0}
--- sorted ---
Activation : 1
Dense : 1
Dropout : 0.4970559482092457
Dropout_1 : 0.06765709934504838
Dropout_2 : 0.9662681038993752
Dropout_3 : 0.011106434718081704
Dropout_4 : 0.9770005173795487
Dropout_5 : 0.8366666847115819
Dropout_6 : 0
add : 1
batch_size : 0
optimizer : 0

Dropout_[n]の数は6個になっています。
もともとは2個だったので、4個増加していることがわかりました。
この数字は、コメントアウトで挿入したDropoutの行数と同じなので、コメントアウトされた行のハイパーパラメータの値も自動調整されているのではないかと考えることができます。

ただし、best_model.summary()で構築されたモデルを可視化してみると、

...

Layer (type)                 Output Shape              Param #   
=================================================================
dense_4 (Dense)              (None, 512)               401920    
_________________________________________________________________
activation_6 (Activation)    (None, 512)               0         
_________________________________________________________________
dropout_5 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_5 (Dense)              (None, 512)               262656    
_________________________________________________________________
activation_7 (Activation)    (None, 512)               0         
_________________________________________________________________
dropout_6 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_6 (Dense)              (None, 10)                5130      
_________________________________________________________________
activation_8 (Activation)    (None, 10)                0         
=================================================================
Total params: 669,706
Trainable params: 669,706
Non-trainable params: 0

...

となっており、学習モデルにDropout層が追加されているわけではないようです。
そのため、コメントアウト行のハイパーパラメータは自動調整されているが、使用上問題ないと思います。
(ただ、結果が見づらくなるだけ)

層を追加するか判断する出力結果がDropoutとして表示される

文の意味わからないかもしれないので、補足します。
ソートされた後の出力結果の6行目をよく見てみると、

...

--- sorted ---
Activation : 1
Dense : 2
Dropout : 0.03323327852409652
Dropout_1 : 0.0886198698550964
Dropout_2 : 1  # <- ココ!!
add : 0
batch_size : 1
optimizer : 0

Dropout層の自動調整は2回しかされていないはずなのに、Dropout_2というハイパーパラメータが現れています。
消去法で層の追加に関係するパラメータだと思ったのですが、確認してみました。

層を追加するか判断する行の有無で検証しました。
モデルを構築している部分のコードを以下のように変えます。

# before

...

model = Sequential()
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu'))
model.add(Dropout({{uniform(0, 1)}}))
model.add(Dense({{choice([256, 512, 1024])}}))
model.add(Activation({{choice(['relu', 'sigmoid'])}}))
model.add(Dropout({{uniform(0, 1)}}))

if {{choice(['three', 'four'])}} == 'four':
    model.add(Dense(100))
    model.add({{choice([Dropout(0.5), Activation('linear')])}})
    model.add(Activation('relu'))

model.add(Dense(10))
model.add(Activation('softmax'))

...
# after

...

model = Sequential()
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu'))
model.add(Dropout({{uniform(0, 1)}}))
model.add(Dense({{choice([256, 512, 1024])}}))
model.add(Activation({{choice(['relu', 'sigmoid'])}}))
model.add(Dropout({{uniform(0, 1)}}))

# delete if sentence
model.add(Dense(100))
model.add({{choice([Dropout(0.5), Activation('linear')])}})
model.add(Activation('relu'))

model.add(Dense(10))
model.add(Activation('softmax'))

...

結果 :

# before

...

Best performing model chosen hyper-parameters:
{'Dropout': 0.03323327852409652, 'Activation': 1, 'Dense': 2, 'Dropout_1': 0.0886198698550964, 'optimizer': 0, 'batch_size': 1, 'add': 0, 'Dropout_2': 1}
--- sorted ---
Activation : 1
Dense : 2
Dropout : 0.03323327852409652
Dropout_1 : 0.0886198698550964
Dropout_2 : 1
add : 0
batch_size : 1
optimizer : 0
# after

...

Best performing model chosen hyper-parameters:
{'Activation': 0, 'add': 0, 'batch_size': 0, 'optimizer': 1, 'Dense': 2, 'Dropout': 0.47268542196596874, 'Dropout_1': 0.7275581656084844}
--- sorted ---
Activation : 0
Dense : 2
Dropout : 0.47268542196596874
Dropout_1 : 0.7275581656084844
add : 0
batch_size : 0
optimizer : 1

if文がある行を削除した結果、Dropout_2の項目が消えています。
つまり、1つ余分に多いDropoutは層の追加に関係した項目だとわかりました。
ちなみに、なぜなのかは不明です。

まとめ

ディープになるとコードがグチャグチャになりそうだなと思ったので、工夫する必要がありそうです。

参考文献

Google Play and the Google Play logo are trademarks of Google LLC.