Hyperasを使ったKerasハイパーパラメータチューニング

ディープラーニングをしているとハイパーパラメータが何か最適かを悩むことが多いです。
ハイパーパラメータの探索には、ランダムサーチやグリッドサーチなど様々な方法があります。[記事「ハイパーパラメータ自動調整いろいろ」](https://qiita.com/cvusk/items/1f3b178f34c39beb29ff)にわかりやすく書かれていて、勉強させていただきました。
今回は、[Hyperas](https://github.com/maxpumperla/hyperas)を使ってKerasのハイパーパラメータを自動チューニングします。

# 環境
前提としてpyenvが入っているくらいでしょうか。pyenvインストールと設定については記事[「UbuntuにpyenvとvenvでPython開発環境構築」](https://qiita.com/FukuharaYohei/items/2820f7bda6678ce26ea7)を参照ください。

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.11|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.6.8|pyenv上でpython3.6.8を使っています<br>パッケージはvenvを使って管理しています|

# インストール
pipでhyperasをインストール。venvの仮想環境を使っています。バージョンは0.41です。

```bash
pip install hyperas
```

# プログラム
Jupyter Labで動かしています。プログラムは[GitHub](https://github.com/YoheiFukuhara/keras-for-beginner/blob/master/Keras07_hyperas.ipynb)に置いています。
今回のモデルの意味に関しては、記事[「【Keras入門(1)】単純なディープラーニングモデル定義」](https://qiita.com/FukuharaYohei/items/579cc0184885b4ba75df)を参照ください。

## 1. ライブラリインポート
Hyperas関連ライブラリをインポートします。

```python
from hyperopt import Trials, STATUS_OK, tpe, rand
from hyperas import optim
from hyperas.distributions import choice, uniform
```

## 2. 訓練データ関数
訓練データを返す関数を作ります。今回はテスト用の乱数です。
通常はファイルを読み込んだり、訓練だけでなくテストデータも分割して渡します。

```python
def data():
    import numpy as np
    x_train = np.random.rand(128, 2)
    y_train = (np.sum(x_train, axis=1) > 1.0) * 1
    y_train = y_train.reshape(128,1)
    
    return x_train, y_train
```

## 3. モデル関数
データを受け取ってモデル定義と訓練実行の関数です。
通常だと評価も実行するかと思います。

```python
def create_model(x_train, y_train):
    # Sequentialモデル使用(Sequentialモデルはレイヤを順に重ねたモデル)
    model = Sequential()

    # 結合層
    model.add(Dense({{choice([4, 8, 16, 32, 64, 128])}}, input_dim=2, activation="tanh"))
    
    model.add(Dropout({{uniform(0, 1)}}))

    # 結合層：入力次元を省略すると自動的に前の層の出力次元数を引き継ぐ
    model.add(Dense(1, activation="sigmoid"))

    # モデルをコンパイル
    model.compile(loss="binary_crossentropy",
                  optimizer={{choice(['adam', 'sgd'])}},
                  metrics=["accuracy"])
    
    result = model.fit(x_train, y_train, epochs=3, validation_split=0.2, verbose=0)
    
    # 普通はここでevaluateしてその結果を使うが、今回は簡易的に訓練時の結果を使用
    
    validation_acc = np.amax(result.history['val_acc']) 
    print('Best validation acc of epoch:', validation_acc)
    
    # ここでreturnするlossの値を最小化するように探索する
    return {'loss': -validation_acc, 'status': STATUS_OK, 'model': model}
```

### 3.1. choice
上記のchoiceの詳細です。`{{choice([4, 8, 16, 32, 64, 128])}}`を使ってレイヤ数を選択させています。

```python
model.add(Dense({{choice([4, 8, 16, 32, 64, 128])}}, input_dim=2, activation="tanh"))
```

### 3.2. uniform
上記のuniformの詳細です。`{{uniform(0, 1)}}`とすることでDropoutの数値を0から1までの間の数で探索します。

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


## 4. チューニング実行
チューニングを実行して、最も結果が良かったパラメータとモデルを受け取ります。

- max_evals : ハイパーパラメータを探索する回数です。回数が多いほどいい値を探索してくれるかもしれませんが、その半面時間がかかります。
- eval_space : Trueを渡すことで、1番目のリターンパラメータにわかりやすい値が返ってきます。デフォルトはFalseで、Falseで`choice`を使った場合に対して指定した順の値が返ってきます。今回の例`{{choice([4, 8, 16, 32, 64, 128])}}`で0の場合は4が最も良かったDenseの値です。
- notebook_name : 今回は、Jupyterで動かしているのでパラメータ"notebook_name"に自身のノートブック名を渡しています

```python
best_run, best_model = optim.minimize(model=create_model,
                                      data=data,
                                      algo=tpe.suggest,
                                      max_evals=3,
                                      eval_space=True,
                                      notebook_name='Keras07_hyperas', # This is important!
                                      trials=Trials())
```

### 実行時出力
実行時の出力です。

```
>>> Imports:
#coding=utf-8

try:
    from tensorflow.keras.models import Sequential
except:
    pass

try:
    from tensorflow.keras.layers import Dense, Dropout
except:
    pass

try:
    from hyperopt import Trials, STATUS_OK, tpe, rand
except:
    pass

try:
    from hyperas import optim
except:
    pass

try:
    from hyperas.distributions import choice, uniform
except:
    pass

try:
    import numpy as np
except:
    pass

>>> Hyperas search space:

def get_space():
    return {
        'Dense': hp.choice('Dense', [8, 16, 32, 64, 128]),
        'Dropout': hp.uniform('Dropout', 0, 1),
        'optimizer': hp.choice('optimizer', ['adam', 'sgd']),
    }

>>> Data
  1: 
  2: import numpy as np
  3: x_train = np.random.rand(128, 2)
  4: y_train = (np.sum(x_train, axis=1) > 1.0) * 1
  5: y_train = y_train.reshape(128,1)
  6: 
  7: 
  8: 
  9: 
>>> Resulting replaced keras model:

  1: def keras_fmin_fnct(space):
  2: 
  3:     # Sequentialモデル使用(Sequentialモデルはレイヤを順に重ねたモデル)
  4:     model = Sequential()
  5: 
  6:     # 結合層
  7:     model.add(Dense(space['Dense'], input_dim=2, activation="tanh"))
  8:     
  9:     model.add(Dropout(space['Dropout']))
 10: #    model.add(Dropout(0.2))
 11: 
 12:     # 結合層：入力次元を省略すると自動的に前の層の出力次元数を引き継ぐ
 13:     model.add(Dense(1, activation="sigmoid"))
 14: 
 15:     # モデルをコンパイル
 16:     model.compile(loss="binary_crossentropy",
 17:                   optimizer=space['optimizer'],
 18:                   metrics=["accuracy"])
 19:     
 20:     result = model.fit(x_train, y_train, epochs=30, validation_split=0.2, verbose=0)
 21:     
 22:     
 23:     # 普通はここでevaluateしてその結果を使うが、今回は簡易的に訓練時の結果を使用
 24:     
 25:     validation_acc = np.amax(result.history['val_acc']) 
 26:     print('Best validation acc of epoch:', validation_acc)
 27:     
 28:     return {'loss': -validation_acc, 'status': STATUS_OK, 'model': model}
 29: 
  0%|          | 0/3 [00:00<?, ?it/s, best loss: ?]
WARNING:tensorflow:From Instructions for updating:
Use tf.cast instead.
Best validation acc of epoch:
0.88461536
Best validation acc of epoch:
0.9230769
Best validation acc of epoch:
0.7692308
100%|██████████| 3/3 [00:08<00:00,  3.04s/it, best loss: -0.9230769276618958]
```

**この出力は大事なので消さないようにしましょう。**例えば`DROPOUT`の変数を複数最適化しようとすると、最後に出力する最適値がどちらの変数かわからなくなります。基本的に時間がかかる処理なので、最後に無駄にしないように、コンソールに出力する値はとっておきましょう。

```
{'Dropout': 0.42522861686845626,
 'Dropout_1': 0.23316134447477344}
```

## 5. 評価
チューニングして最も結果が良かったモデルを使って評価もできます。

```python
x_test, y_test = data()
print("Evalutation of best performing model:")
print(best_model.evaluate(x_test, y_test))
```

```
Evalutation of best performing model:
128/128 [==============================] - 0s 99us/sample - loss: 0.4846 - acc: 0.9141
[0.4846123978495598, 0.9140625]
```

## 6. ハイパーパラメータ表示
best_runに最も結果が良かったハイパーパラメータが入っています。

```python
print("Best performing model chosen hyper-parameters:")
print(best_run)
```

```
Best performing model chosen hyper-parameters:
{'Dense': 3, 'Dropout': 0.21280043312755825, 'optimizer': 0}
```

# 複雑なパターンのチューニング
試していませんが、[「Kerasだってハイパーパラメータチューニングできるもん。【hyperas】」](https://qiita.com/wataoka/items/f46224ccccc5321543bd#tutorial3-%E8%A4%87%E9%9B%91%E3%81%AA%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)の記事にあるように複雑なチューニングも可能なようです。

```python
if {{choice(['three', 'four', 'five'])}} == 'three':
        pass
elif {{choice(['three', 'four', 'five'])}} == 'four':
    model.add(Dense(100))
    model.add(Activation({{choice(['linear', 'relu', 'sigmoid'])}}))
    model.add(Dropout({{uniform(0, 1)}}))
elif {{choice(['three', 'four', 'five'])}} == 'five':
    model.add(Dense(200))
    model.add(Activation({{choice(['linear', 'relu', 'sigmoid'])}}))
    model.add(Dropout({{uniform(0, 1)}}))
    model.add(Dense(100))
    model.add(Activation({{choice(['linear', 'relu', 'sigmoid'])}}))
    model.add(Dropout({{uniform(0, 1)}}))
```

※2019/8/15追記
上記の方法だけでは失敗しました。こんなおまじないを書かないとうまくいきませんでした。変数のディクショナリを作る箇所のバグっぽいです。

```python
# (layer = {{choice([['three', 'four', 'five'])}})
```


# Google Colaboratoryの場合
[Keras Hyperparameter Tuning in Google Colab using Hyperas](https://towardsdatascience.com/keras-hyperparameter-tuning-in-google-colab-using-hyperas-624fa4bbf673)に書いてあるとおりにすればできる？試せていないです。

# メモリ不足によるエラー対策
[GitHubのIssue](https://github.com/maxpumperla/hyperas/issues/16)を見てモデル出力をやめ、以下のコードを追加することで解決するかもしれません。私がやったときは、他に原因があったので、下記の方法でメモリ削減が達成できるのか調べられていません。

```python
import gc
from tensorflow.python.keras import backend as K

K.clear_session()
gc.collect()
```