今回は、HyperoptでMNISTのハイパーパラメータの最適化を行いました。Hyperoptは前回試したHyperasの元となっているライブラリです。Hyperasはシンプルに使える反面やや扱いにくい部分(慣れていないだけかもしれませんが)もあったため、大元のHyperoptで書き直してみました。
データは前回同様KaggleのDigital Recognizer(MNIST)で、最適化するハイパーパラメータは以下です。
・Dropout率
・Dense層出力ユニット数
・batch_size数
・validation_splitの比率
Hyperoptの使い方:
ハイパーパラメータのディクショナリー設定:
まずハイパーパラメータのディクショナリーを以下のようなフォーマットで用意します。
params = { 'Dense_0': hp.choice('Dense_0', [128, 256, 512]), 'Dense_1': hp.choice('Dense_1', [64, 128, 256]), 'Dropout_0': hp.uniform('Dropout_0', 0.0, 1.0), 'Dropout_1': hp.uniform('Dropout_1', 0.0, 1.0), 'Dropout_2': hp.uniform('Dropout_2', 0.0, 1.0), 'Dropout_3': hp.uniform('Dropout_3', 0.0, 1.0), 'batch_size': hp.choice('batch_size', [16, 32, 64]), 'validation_split': hp.uniform('validation_split', 0.1, 0.3) }
離散値の場合はhp.choice()、連続値の場合はhp.uniform()を使います。このあたりはHyperasと同じような感じです。このほか乱数用のhp.randint()や正規分布用のhp.normal()などいくつかあります(ここに書いてあります)。
ハイパーパラメータの挿入と戻り値の設定:
次はモデルの構築です。MNISTデータの前処理をしておいてから、CNNを用いてMNIST分類モデルを構築します。そして最適化したい変数の部分(以下の場合:CNN層内のドロップアウト率)に、
model.add(Dropout(params['Dropout_0'], seed=seed))
という感じで挿入しておきます。
model.compile()、model.fit()したあと model.evaluate()でlossとaccを求めて、その値を戻り値とします。サンプルなどではlossのかわりにaccを評価値として次のfmin()関数に渡していますが、どちらがいいのかは不明。または、hist=fit()のhistoryからhist.history['val_loss'][-1]で最後のロス値を取得する方法でもいいのかもしれません(あるいは'val_acc')。尚、accを渡す場合はマイナスをかけて最大値を最小値に反転させておく必要があります。
またモデルなどその他の値やオブジェクトを渡すときはディクショナリーにするといいようです。ディクショナリーにする場合は、次のfmin()関数に値を渡すために'loss'と'status'のキーが最低含まれていないといけないようです。今回は追加でモデルも含めたので以下のような戻り値としました。あとでベストmodelを参照する場合は追加しておくといいと思います。
return {'loss': -acc, 'status': STATUS_OK, 'model': model}
最適化:
最後に、best=fmin()で最適なパラメータを見つけます。fmin()へモデルとハイパーパラメータディクショナリーを渡し、探索回数などを指定して最適化します。探索回数は多いほどいいと思いますが、それなりに時間はかかります(数時間とか)。
trialsには探索結果の記録が保持されるので後で参照します。
trials = Trials() best = fmin(fn=cnn_model, space=params, algo=tpe.suggest, max_evals=20, trials=trials, verbose=1, rstate=np.random.RandomState(seed))
fn:CNNモデル(前述の'loss', 'status', 'model'が戻り値)
space:パイパーパラメータのディクショナリー
algo:使用するアルゴリズム(TPEなのでこのまま)
max_evals:探索回数
trials:探索記録保持先
verbose:ログ出力
rstate:乱数固定
結果参照:
best=fmin()からは最適化されたパラメータのディクショナリーが出力されます。そのままだと、hp.choice()の場合リストのインデックス番号が返されるので、
space_eval(params, best)
で実際の値に変換出力してくれます(以下)。
{'Dense_0': 512, 'Dense_1': 256, 'Dropout_0': 0.19796353174591008, 'Dropout_1': 0.30328292011950164, 'Dropout_2': 0.7005074297830172, 'Dropout_3': 0.3974900176858912, 'batch_size': 64, 'validation_split': 0.16617354953831512}
あらかじめtrials=Trials()と定義しておけば、trialsの中に全ての情報が記録されるので、必要に応じて値やモデルを参照することができます。
trials.best_trial['result']
で以下が出力されます(複数回探索した中でのベストの結果)。lossはaccをマイナス反転したものなのでaccのこと、modelはそのときのベストモデル、statusは処理が無事完了なら'ok'。
{'loss': -0.9935714285714285, 'model': <keras.engine.sequential.Sequential at 0x7fc5c3da87f0>, 'status': 'ok'}
ベストモデルは、
best_model = trials.best_trial['result']['model']
によって参照することができるので、このモデルを使ってpredict()することができます。
まとめ:
前回のHyperasよりも使い勝手はよさそうです。それほど面倒なコーディングをすることもないので、個人的にはHyperoptのほうが便利かと。要は、パイパーパラメータディクショナリーとモデルを最適化関数に入れれば答えがでてくるということです。
詳しいドキュメントがないので(ここくらい)、細かな使い方はわからないのですが(ソースを読み解くしかないかも)、いろいろ応用できそうです。
今回はmax_evals=20で20回探索(NVIDIA GTX1060で49分)した結果、スコアは0.99257でした。まあまあの結果でしたが、実際100回以上(数時間)は回したほうがいいのかもしれません。
こちらのサイトでは様々なベイズ最適化ライブラリーを比較しており、時間的にはHyperoptが一番速いようです。10次元以下の最適化であればPyBOが優れているようで、それ以上の次元ではどれも遅くなるようです。また20次元や40次元になるとほとんどのライブラリが最適化できなくなるようで、Spearmintが20次元でも機能していたようです。
追記1:
その後100回(約6時間)回してみましたがスコアは0.99185という結果。validationセットでのスコアは0.995だったので向上しましたが、オーバーフィッティング気味だったったのか結果的にはいまいち。いずれにせよ0.992前後が限界という感じ。CNNの層を少し改造するか、kerasのImageDataGeneratorでデータ水増しした方がいいのかもしれません。
追記2:
validation_splitを0.2に固定して、Dropout率とDense層出力ユニット数だけをハイパーパラメータとして10回ほど探索すると0.99442まで向上しました。普通に考えてvalidation dataは少ないほどval_accは上がってしまうと思うので固定にしたほうがよさそうです。
関連:
GPyOpt: Digital Recognizer(MNIST) CNN Keras ハイパーパラメータ最適化
Kaggle Digital Recognizer(MNIST): Hyperopt + Data Augmentation
Kaggle Digital Decognizer(MNIST): Keras, fit_generator() + hyperopt