Optuna+KerasでCNNのハイパーパラメータを最適化

#はじめに
2018/12/2にPreferred Networksからハイパーパラメータ自動最適化ツール「Optuna」が公開されました。本記事では、ざっくりと使い方を確認しながらfashion mnist分類問題を例にとって、この問題を解くためのCNNのハイパーパラメータを求めます。

#実行環境
Python:3.6.4
Keras:2.1.6
Optuna:0.4.0

#準備
[公式ドキュメント](https://optuna.readthedocs.io/en/stable/index.html)を参考にしています。
##Optunaのインストール
pipコマンドでインストールできます。

```
$ pip install optuna
```

##使い方
大まかな流れは以下の通り。
###①目的関数の設定
目的関数を定義します。この時、目的関数はtrialオブジェクトを引数にとり、最適化したいハイパーパラメータをこのtrialオブジェクトを使用して定義します。以下は1変数の2次関数を目的関数とした例です。

```Python
def objective(trial):
   x = trial.suggest_uniform('x', -10, 10)
   return (x - 2) ** 2
```

上記の例では、求めるべきパラメータ$x$を-10から10の連続的な範囲で最適化しています。
この他にもOptunaでは以下のようなハイパーパラメータを定義することができます。

```Python
# Categorical parameter
optimizer = trial.suggest_categorical('optimizer', ['MomentumSGD', 'Adam'])

# Int parameter
num_layers = trial.suggest_int('num_layers', 1, 3)

# Uniform parameter
dropout_rate = trial.suggest_uniform('dropout_rate', 0.0, 1.0)

# Loguniform parameter
learning_rate = trial.suggest_loguniform('learning_rate', 1e-5, 1e-2)

# Discrete-uniform parameter
drop_path_rate = trial.suggest_discrete_uniform('drop_path_rate', 0.0, 1.0, 0.1)
```

###②Study Objectの作成
Optunaではハイパーパラメータを求めるためにStudy Objectを作成する必要があります。
これは以下の1行で大丈夫です。

```Python
study = optuna.create_study()
```

###③最適化
②で作成したstudy objectのoptimizeメソッドによって目的関数内のハイパーパラメータを最適化します。

```Python
study.optimize(objective, n_trials=100)
```

出力例(*[公式チュートリアル](https://optuna.readthedocs.io/en/stable/tutorial/first.html)より引用)

```
[I 2018-05-09 10:03:22,469] Finished a trial resulted in value: 52.9345515866657. Current best value is 52.9345515866657 with parameters: {'x': -5.275613485244093}.
[I 2018-05-09 10:03:22,474] Finished a trial resulted in value: 32.82718929591965. Current best value is 32.82718929591965 with parameters: {'x': -3.7295016620924066}.
[I 2018-05-09 10:03:22,475] Finished a trial resulted in value: 46.89428737068025. Current best value is 32.82718929591965 with parameters: {'x': -3.7295016620924066}.
[I 2018-05-09 10:03:22,476] Finished a trial resulted in value: 100.99613064563654. Current best value is 32.82718929591965 with parameters: {'x': -3.7295016620924066}.
[I 2018-05-09 10:03:22,477] Finished a trial resulted in value: 110.56391159932272. Current best value is 32.82718929591965 with parameters: {'x': -3.7295016620924066}.
[I 2018-05-09 10:03:22,478] Finished a trial resulted in value: 42.486606942847395. Current best value is 32.82718929591965 with parameters: {'x': -3.7295016620924066}.
[I 2018-05-09 10:03:22,479] Finished a trial resulted in value: 1.130813338091735. Current best value is 1.130813338091735 with parameters: {'x': 3.063397074517198}.
...
[I 2018-05-09 10:03:23,431] Finished a trial resulted in value: 8.760381111220335. Current best value is 0.0026232243068543526 with parameters: {'x': 1.9487825780924659}.
```

###④結果の確認
学習の結果はstudy object内に格納されます。

```Python
#最適化したハイパーパラメータの確認
study.best_params

#最適化後の目的関数値
study.best_value

#全試行の確認
study.trials
```

#CNNのハイパーパラメータの探索
ここからは実際にKerasとOptunaを組み合わせてfashion mnistを解くためのCNNのハイパーパラメータを求めていきます。これより先のコードは[notebook形式でまとめている](https://drive.google.com/open?id=17r8YctZMu-3OaJmBgZrpz8iwDKW3beY6)ので、実際の挙動を確認したい方はご使用ください。

__※本記事では以下をハイパーパラメータとして設定しています。__
・畳み込み層の数(3,4,5,6,7)
・各畳込み層のフィルタ数(16, 32, 48, ... , 128)
・全結合層のユニット数(100,200,300,400,500)
・optimizer(Adam, RMSprop, SGD)
・活性化関数(relu, sigmoid, tanh)

##ライブラリのインポート、データの前処理
```Python
#ライブラリのインポート
import optuna
import keras.backend as K
from keras.datasets import fashion_mnist
from keras.layers import Convolution2D, Input, Dense, GlobalAveragePooling2D
from keras.models import Model
from keras.utils import to_categorical

#学習用データの前処理
(train_x, train_y), (test_x, test_y) = fashion_mnist.load_data()
print(train_x.shape, train_y.shape, test_x.shape, test_y.shape)

train_x = train_x.reshape(-1,28,28,1) / 255
test_x = test_x.reshape(-1,28,28,1) / 255
train_y = to_categorical(train_y)
test_y = to_categorical(test_y)
print(train_x.shape, train_y.shape, test_x.shape, test_y.shape)
```

##モデルの定義

```Python
def create_model(num_layer, activation, mid_units, num_filters):
    """
    num_layer : 畳込み層の数
    activation : 活性化関数
    mid_units : FC層のユニット数
    num_filters : 各畳込み層のフィルタ数
    """
    inputs = Input((28,28,1))
    x = Convolution2D(filters=num_filters[0], kernel_size=(3,3), padding="same", activation=activation)(inputs)
    for i in range(1,num_layer):
        x = Convolution2D(filters=num_filters[i], kernel_size=(3,3), padding="same", activation=activation)(x)
        
    x = GlobalAveragePooling2D()(x)
    x = Dense(units=mid_units, activation=activation)(x)
    x = Dense(units=10, activation="softmax")(x)
    
    model = Model(inputs=inputs, outputs=x)
    return model
```

##目的関数の定義
5エポック学習させた時の検証用データに対する正答率を目的関数に設定しています。

```Python
def objective(trial):
    #セッションのクリア
    K.clear_session()
    
    #最適化するパラメータの設定
    #畳込み層の数
    num_layer = trial.suggest_int("num_layer", 3, 7)
    
    #FC層のユニット数
    mid_units = int(trial.suggest_discrete_uniform("mid_units", 100, 500, 100))
    
    #各畳込み層のフィルタ数
    num_filters = [int(trial.suggest_discrete_uniform("num_filter_"+str(i), 16, 128, 16)) for i in range(num_layer)]
    
    #活性化関数
    activation = trial.suggest_categorical("activation", ["relu", "sigmoid", "tanh"])
    
    #optimizer
    optimizer = trial.suggest_categorical("optimizer", ["sgd", "adam", "rmsprop"])
                                          
    model = create_model(num_layer, activation, mid_units, num_filters)
    model.compile(optimizer=optimizer,
          loss="categorical_crossentropy",
          metrics=["accuracy"])
    
    history = model.fit(train_x, train_y, verbose=0, epochs=5, batch_size=128, validation_split=0.1)
    
    #検証用データに対する正答率が最大となるハイパーパラメータを求める
    return 1 - history.history["val_acc"][-1]
```

##最適化の実行
```Python
study = optuna.create_study()
study.optimize(objective, n_trials=100)
```

出力は以下のような感じです。

```
(中略)
[I 2018-12-04 16:03:27,951] Finished a trial resulted in value: 0.11699999968210861. Current best value is 0.09450000031789141 with parameters: {'num_layer': 6, 'mid_units': 400.0, 'num_filter_0': 80.0, 'num_filter_1': 48.0, 'num_filter_2': 80.0, 'num_filter_3': 96.0, 'num_filter_4': 96.0, 'num_filter_5': 80.0, 'activation': 'relu', 'optimizer': 'rmsprop'}.
[I 2018-12-04 16:04:27,360] Finished a trial resulted in value: 0.12449999952316282. Current best value is 0.09450000031789141 with parameters: {'num_layer': 6, 'mid_units': 400.0, 'num_filter_0': 80.0, 'num_filter_1': 48.0, 'num_filter_2': 80.0, 'num_filter_3': 96.0, 'num_filter_4': 96.0, 'num_filter_5': 80.0, 'activation': 'relu', 'optimizer': 'rmsprop'}.
```

##結果の確認

```Python
study.best_params
```

ハイパーパラメータは以下のように求まりました。

```
{'activation': 'relu',
 'mid_units': 400.0,
 'num_filter_0': 80.0,
 'num_filter_1': 48.0,
 'num_filter_2': 80.0,
 'num_filter_3': 96.0,
 'num_filter_4': 96.0,
 'num_filter_5': 80.0,
 'num_layer': 6,
 'optimizer': 'rmsprop'}
```

ちなみにこのときの目的関数値は

```Python
study.best_value
```

を実行して確認すると

```
0.09450000031789141
```
となりました。1-accuracyを目的関数としているので、正解率は9割ちょいということになります。これだけ適当にやって検証用データの精度9割ってすごいのでは…

#感想
とりあえずチュートリアルを見て、直感的にkerasと組み合わせてみましたが、これといったエラーもなくハイパーパラメータを計算することができました。Optunaの最適化がDefine by run方式のため、目的関数内でハイパーパラメータの数が変化しても何ら問題なく動いたことが個人的にめちゃくちゃ感動しました。