Watson Studioで深層学習 Part2。HPO機能付きでExperimentsを試す


(2018-06-26) 以下の3点修正
* [Kaggle](https://www.kaggle.com/c/cifar-10/discussion/40237)の記事を参考にニューラルネットを変更しました。90%までいきませんでしたが、だいたい85%の精度がでるようになりました。
* 元のコードは何カ所はバグ(お恥ずかしい。。)があり、モデルの保存ができない状態だったものを修正しました。
* HPOパラメータはlearning_dateの1つだけにしました。
(2018-07-04) サンプルコードにバグがあったので記事を含めて修正
(2018-07-06) コードを何カ所か再改訂
(2018-08-15) コードと解説を全面的に改定

# はじめに

[Watson Studioで深層学習。KerasサンプルアプリをGPU+TensorBoardで動かす](https://qiita.com/makaishi2/items/1fc28f5039798a883aef)の続編です。
この記事を書いた時はまだ自分で作ったアプリでHPO機能を動かす方法とDefinitionの登録をGUIで行う方法がよくわかっていなかったのですが、両方ともある程度やり方がわかったので、最新状況に基づいた手順をまとめ直します。

実行後のサンプル画面はこちらです。
![スクリーンショット 2018-08-15 15.34.28.png](https://qiita-image-store.s3.amazonaws.com/0/206130/53b88418-8ee9-03cb-3442-318cbe24e151.png)


実行までのステップは基本的に前編と同じです。

* バケットの定義、学習用データのアップロード
* コーディング、テスト
* Defintion・Experimentの登録・実行

前回との違いは、コーディング・テスト以外の3ステップを全部GUIで行った点と、Experiment登録時にHPOパラメータの指定も行った点になります。

#  バケットの定義、学習データのアップロード
この部分に関しては、前回と手順の違いはありません。
前回の記述をそのまま引用します。

> Object Storageに「入力用バケット」「出力用バケット」の2つを用意し、「入力用バケット」に対しては、学習に使う3つのファイル(cifar-10-tf-train.pkl, cifar-10-tf-test.pkl, cifar-10-tf-valid.pkl)をアップロードします。
> 細かい手順は「Watson Studioのディープラーニング機能(DLaaS)を使ってみた」とまったく同じなので、そちらを参照されて下さい。
> ダウンロードするファイルも、この記事で紹介されている[box link](https://ibm.ent.box.com/v/NNM-sample-data)からダウンロードできます。

# コーディング
ここに関しては、前回と大きく異なっているので、最初から解説します。
前回はNNDで生成したコードを修正する形をとったのですが、今回はチュートリアルのサンプルアプリを雛形にまったく別に作り直しました。

それでは順を追って詳しく解説を行います。

## プログラム実行時のカレントディレクトリ設定
前回のアプリでは、pythonプログラムは親子の2階層の構造になっていて、子のプログラム呼出し時にカレントディレクトリは``os.environ["DATA_DIR"]``で渡される入力バケットに移動していました。
しかし、こういう作りにしてしまうと,HPOパラメータが渡されるインターフェースになっている``config.json``ファイル読み取りができないことがわかったので、カレントディレクトリを変更しないようにアプリを修正しました。
ついでに、そもそも親子2階層の構造もやめにしています。
学習データ読込時には``open(path.join(DATA_DIR, 'cifar-10-tf-train.pkl'), 'rb')``のような形でパス名付きでファイルにアクセスするようにしています。

## HPOパラメータ実装
今回の記事の肝になる箇所です。
まず、Kerasのモデル定義でHPO化したいパラメータについては、あらかじめ変数として外だししておきます。要は、普段モデル定義とか、fitコマンドで定数を直書きにしているところを変数に置き換えるということです。
今回の実装例では**learning_rate**(学習率と呼ばれる深層学習の重要なパラメータの一つ)と**nb_epoch**(学習の繰り返し回数)を外だしにしました。

```py3
# learning_rate
    opt_rms = optimizers.Adam(lr = learning_rate)
    model.compile(loss='categorical_crossentropy', optimizer=opt_rms, metrics=['accuracy'])

# nb_epoch
history = model.fit(
    train_data, train_label, batch_size=batch_size, epochs=nb_epoch, verbose=1,
    validation_data=(val_data, val_label), shuffle=True, callbacks[tensorboard])
```

次が、HPOパラメータの設定処理です。
該当するコードは以下のようになります。
デフォルト値を初期設定した後で、config.jsonファイルの読み取りを行い(このファイルの作成はExpermnets側が行う)、変数定義がこちらにあればその値を優先して上書きするようにします。

```
# 学習係数 デフォルト値
learning_rate = 0.001
nb_epoch = 2

import json

try:
    with open("config.json", 'r') as f:
        json_obj = json.load(f)
    nb_epoch = json_obj['nb_epoch']
    learning_rate = json_obj['learning_rate']
except:
    pass

print('nb_epoch: %d' % nb_epoch)
print('learning_rate: %s' % learning_rate)
```

## Objecctive変数値の書き出し

Experiment Builder上でHPO機能を利用したプログラムでは、実験定義情報の一つである「Objective」で指定した変数値(デフォルト値のaccuracy)をval_dict_list.jsonという名前のjsonファイルに書き出す必要があります。その実装例を以下に示します。

```py3
# 検証データで最終的なモデル精度計算
test_scores = model.evaluate(test_data, test_label, verbose=1)

# 検証データによる精度をaccuracyとする
accuracies = [test_scores[1]]

# now write the values out to a JSON formatted file for the 
training_out =[]
for i in range(len(accuracies)):
    training_out.append({'steps':(i+1) , 'accuracy':accuracies[i]})

result_file = '{}/val_dict_list.json'.format(os.environ['RESULT_DIR'])
with open(result_file, 'w') as f:
    json.dump(training_out, f)
```

## TensorBoard初期化
学習過程での認識率などをTensorBoard連携するための準備で必要です。
HPOパラメータを使う場合は、テストセッション毎に保存先サブディレクトリが別々になるように、os.environ["SUBID"]をパス名の中に含めるようにします。

```py3
import os
from keras.callbacks import TensorBoard

# Tensorboardデータ保存用パスの指定
tb_directory = os.environ["LOG_DIR"]+"/"+os.environ["SUBID"]+"/logs/tb"

# Tensorbaordオブジェクトの作成
tensorboard = TensorBoard(log_dir=tb_directory)
```

## 学習処理

最後は、学習時のfit関数呼出し部分の実装です。
fit関数の引数として``tensorbord``をコールバック関数として指定することがポイントです。

```py3
# コールバック関数にTensorboardを指定する

history = model.fit(
    train_data, train_label, batch_size=batch_size, epochs=nb_epoch, verbose=1,
    validation_data=(val_data, val_label), shuffle=True, callbacks=[tensorboard]
)
```

## zip化
最後に、cnn-cifar10.pyをcnn-cifar10.zipとしてzipファイルに固めます。
具体的には、次のコマンドで行いました。

```sh
$ zip cnn-cifar10.zip cnn-cifar10.py
```

ここで作ったzipファイルは、[Github](https://github.com/makaishi2/watson-ml-scikit-learn/raw/master/zip/cnn-cifar10.zip)にアップしておきました。

# Definition定義・Experiments定義
Definition定義は、前回はAPI呼出しで行った箇所ですが、GUIでできそうだったので、今回はExperiments定義とあわせてGUIで行いました。
具体的な手順は以下のとおりです。

Assetsの画面から「New Experiment」をクリック

下の画面で、
①Experiment名には適当な名前、
②Bucket設定が事前に準備した入力用、出力用Bucketを指定した後、
③画面右の「Add Training Definition」をクリック。

![スクリーンショット 2018-08-15 16.01.12.png](https://qiita-image-store.s3.amazonaws.com/0/206130/4ffddf1f-dc6c-e977-c537-2f5a9deb2665.png)


次のような画面が出てくるので、
①Defintion名は適当に
②Training source codeには先ほどzipに固めたpythonソース(**cnn-cifar10.zip**)をアップロード
③Frameworkは**tensorflow 1.5**
④Execution commandには以下のコマンドを指定。(学習時に実行されるコマンドになります)
``python3 cnn-cifar10.py cnn-cifar10.h5``
⑤Compute planは一番上の1/2 x NVDIA Testla K80を指定

![スクリーンショット 2018-08-15 16.07.28.png](https://qiita-image-store.s3.amazonaws.com/0/206130/c3f433c7-6777-d8ee-5734-c31af39eb507.png)

その下のHyperrparameter optimizer methodをデフォルトの「none」から「random」に変更すると、下の図のように入力項目が増えるので、パラメータを以下のように設定します。

* Number of optimizer steps -> **5**
(もっと大きな数も指定できますが、グラフが最大5個までしか書けないようなので、これが一番見栄えがよさそうです)

* Objective -> **accuracy** (デフォルト値)

* Maximaize or minimise -> **maximize** (デフォルト値)

更にその下のAdd hyperparameter のリンクをクリックします。

![スクリーンショット 2018-08-15 15.53.57.png](https://qiita-image-store.s3.amazonaws.com/0/206130/d2e28014-54dc-b603-a30c-b76824a00a2b.png)

この先は、下の画面の要領で、HPOの値を順次設定していきます。
(この操作はMacのSafariではできず、Firefoxにする必要があったので、注意して下さい)

Name: **nb_epoch**
Values: **50**

Name: **learning_rate**
Values: **0.1, 0.01, 0.001, 0.0001**

![スクリーンショット 2018-06-26 22.00.43.png](https://qiita-image-store.s3.amazonaws.com/0/206130/b1e27963-5e12-15e6-2a25-4959e32290ad.png)

下のような画面の状態で必要な「実験定義」の設定が全部終わっているので、画面右下の「Create」をクリックします。

![スクリーンショット 2018-08-15 16.21.47.png](https://qiita-image-store.s3.amazonaws.com/0/206130/cff84562-d1e9-9d23-2442-3837f7650bc1.png)

下の画面に遷移するので、「Create and run」をクリック

![スクリーンショット 2018-08-15 16.22.02.png](https://qiita-image-store.s3.amazonaws.com/0/206130/3050f73d-0c16-fcbb-8ed2-456566070dcf.png)

これで図のようにHPOジョブの投入が行われます。投入当初はこの図のように一つのジョブに見えますが、

![スクリーンショット 2018-08-15 17.18.08.png](https://qiita-image-store.s3.amazonaws.com/0/206130/c10b93be-77e5-5547-3e68-7e150e80df7f.png)

しばらくするとジョブ名に``_N``のサフィックスが追加され、更にしばらくすると、下の図のように複数のジョブに分割されます。

![スクリーンショット 2018-08-15 18.16.21.png](https://qiita-image-store.s3.amazonaws.com/0/206130/f812522e-636f-d517-c1b7-fa74e22c871e.png)

最終的には下の図のようになります。

![スクリーンショット 2018-08-15 20.19.19.png](https://qiita-image-store.s3.amazonaws.com/0/206130/3c5c96a3-51f7-6d99-3c81-d24e7deb7546.png)

また、**Compare Runs**のタブをクリックすると、下のようにに数のテストセッションそれぞれのパラメータ値とその時の結果のサマリー表が表示されることになります。

![スクリーンショット 2018-08-15 20.19.34.png](https://qiita-image-store.s3.amazonaws.com/0/206130/769c9825-55c9-4005-bf09-a48c8b329a0e.png)

最後にソースコード全体を添付します。

```py3
# CNN on CIFAR-10 
# Watson Studio Sample Program
# step 3
# HPO連携

# 学習繰り返し回数
#nb_epoch = 50
nb_epoch = 2

# 分類クラス数
num_classes = 10

# 1回の学習で何枚の画像を使うか
batch_size = 64

# 学習係数 デフォルト値
learning_rate = 0.001

import os
import sys
from os import path

# 入力データ用ディレクトリ
DATA_DIR = os.environ["DATA_DIR"]

# 出力データ用ディレクトリ
RESULT_DIR = os.environ["RESULT_DIR"]

# 出力ファイルパス (mainで設定される。変数名だけ定義)
model_result_path = None

# Main script

def main(model_result_name):
    # モデルファイルの保存先ディレクトリの作成
    model_result_dir  = path.join(RESULT_DIR, "model")
    os.makedirs(model_result_dir, exist_ok=True)
    
    # モデルファイルのパス名(グローバル変数に設定)
    global model_result_path
    model_result_path = path.join(model_result_dir, model_result_name) 

    print("Starting DL model training...")
    print("DATA_DIR: %s" % DATA_DIR)
    print("RESULT_DIR: %s" % RESULT_DIR)
    print("MODEL_RESULT_PATH: %s" % model_result_path)

if __name__ == "__main__":
    main(sys.argv[1])

print('model_result_path: %s' % model_result_path)

######### pickle データの読み込み ################
# 読み込みデータはDATA_DIR配下にある前提 

import pickle
from keras.utils import np_utils

with open(path.join(DATA_DIR, 'cifar-10-tf-train.pkl'), 'rb') as f:
    train_data, train_label = pickle.load(f)
    train_data = train_data.astype('float32')  
    train_label = np_utils.to_categorical(train_label, num_classes)

with open(path.join(DATA_DIR, 'cifar-10-tf-valid.pkl'), 'rb') as f:
    val_data, val_label = pickle.load(f)
    val_data = val_data.astype('float32')  
    val_label = np_utils.to_categorical(val_label, num_classes)

with open(path.join(DATA_DIR, 'cifar-10-tf-test.pkl'), 'rb') as f:
    test_data, test_label = pickle.load(f)
    test_data = test_data.astype('float32')  
    test_label = np_utils.to_categorical(test_label, num_classes)
#################################################

############ HPO パラメータ読み取り##############
import json
import os

try:
    with open("config.json", 'r') as f:
        json_obj = json.load(f)
    learning_rate = json_obj['learning_rate']
    nb_epoch = json_obj['nb_epoch']
except:
    pass

print('learning_rate: %s' % learning_rate)
print('nb_epoch: %d' % nb_epoch)

# SUBIDの取得関数
def getCurrentSubID():
    if "SUBID" in os.environ:
        return os.environ["SUBID"]
    else:
        return None
##############################################

########## TessorBloard初期化 #################
import os
from keras.callbacks import TensorBoard

# Tensorboardデータ保存用パスの指定
tb_directory = os.environ["LOG_DIR"]+"/"+os.environ["SUBID"]+"/logs/tb"

# Tensorbaordオブジェクトの作成
tensorboard = TensorBoard(log_dir=tb_directory)
##############################################

######### CNNモデル作成 ########################
# ここは普通のKerasコーディングでいい
# 正しいHPOパラメータの外だしだけ意識する (この例ではlearning_rate)

# 必要ライブラリのロード
from keras.layers import Conv2D, MaxPooling2D, Dropout, Flatten, Dense, Activation, BatchNormalization
from keras import Sequential, regularizers, optimizers

baseMapNum = 32
weight_decay = 1e-4

def cnn_model(x_train, num_classes):
    model = Sequential()
    model.add(Conv2D(baseMapNum, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay), input_shape=x_train.shape[1:]))
    model.add(Activation('relu'))
    model.add(BatchNormalization())
    model.add(Conv2D(baseMapNum, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(Activation('relu'))
    model.add(BatchNormalization())
    model.add(MaxPooling2D(pool_size=(2,2)))
    model.add(Dropout(0.2))

    model.add(Conv2D(2*baseMapNum, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(Activation('relu'))
    model.add(BatchNormalization())
    model.add(Conv2D(2*baseMapNum, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(Activation('relu'))
    model.add(BatchNormalization())
    model.add(MaxPooling2D(pool_size=(2,2)))
    model.add(Dropout(0.3))

    model.add(Conv2D(4*baseMapNum, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(Activation('relu'))
    model.add(BatchNormalization())
    model.add(Conv2D(4*baseMapNum, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
    model.add(Activation('relu'))
    model.add(BatchNormalization())
    model.add(MaxPooling2D(pool_size=(2,2)))
    model.add(Dropout(0.4))

    model.add(Flatten())
    model.add(Dense(num_classes, activation='softmax'))

    opt_rms = optimizers.Adam(lr = learning_rate)
    model.compile(loss='categorical_crossentropy', optimizer=opt_rms, metrics=['accuracy'])

    return model

# モデル生成
model = cnn_model(train_data, num_classes)
######################################################

################## 学習・評価 ##########################
# コールバック関数にtensorboardを連携

history = model.fit(
    train_data, train_label, batch_size=batch_size, epochs=nb_epoch, verbose=1,
    validation_data=(val_data, val_label), shuffle=True, callbacks=[tensorboard]
)

# 検証データで最終的なモデル精度計算
test_scores = model.evaluate(test_data, test_label, verbose=1)
print('test_scores: ', test_scores)
print('learning_rate: %s' % learning_rate)
print('nb_epoch: %d' % nb_epoch)

#######################################################
# 検証データによる精度をaccuracyとする
accuracies = [test_scores[1]]

# now write the values out to a JSON formatted file for the 
training_out =[]
for i in range(len(accuracies)):
    training_out.append({'steps':(i+1) , 'accuracy':accuracies[i]})

result_file = '{}/val_dict_list.json'.format(os.environ['RESULT_DIR'])
with open(result_file, 'w') as f:
    json.dump(training_out, f)

################# モデルの保存 ##########################
# 初期設定でmodel_result_pathが設定されている

print('Saving the model...')
model.save(model_result_path)
print("Model saved in file: %s" % model_result_path)
#######################################################
```