【Keras/TensorFlow】ベイズ最適化でハイパーパラメータを調整する




ハイパーパラメーターの自動調整方法であるベイズ最適化について解説します。ハイパーパラメーターのチューニングを自動化することは、より短時間でいいモデルをつくるために必要不可欠な作業です。

ハイパーパラメーターの自動調節の方法は

  • GridSearch(グリッドサーチ)
  • RandomSearch(ランダムサーチ)
  • BayesianOptimization(ベイズ最適化)今回はこれ

の3つが主流です。上の2つについては別記事で紹介しておりますので、併せてご覧ください。

【Keras】RandomSearch(ランダムサーチ)でハイパーパラメータを調整する

2019年6月26日

【Keras】GridSearch(グリッドサーチ)でハイパーパラメータを調整する

2019年6月24日

ベイズ最適化とは?

ベイズ最適化とは、ガウス過程を利用して中身のアルゴリズムが見えないブラックボックス関数がどこで最大値を取るかを推測するものです。ハイパーパラメーターのチューニングにおいては、ブラックボックス関数=損失関数or正解率となります。(損失関数の場合は、最小化したいのでマイナスを掛けますが)

関数の中身は見えませんが、関数自体は存在するのでデータをインプットするとインプットデータが定義域に入っている限り何かしらのアウトプットが返ってきます。インプットを入れてアウトプットを何回か取り出すことで関数の形をぼんやりとでも推測し、最大値を取るx座標(ハイパーパラメーターたち)がどこらへんか目星を付けていくのです。

では、目星をつけると言いましたがどのようにつけるのでしょうか?それは予測される関数の信頼区間の最大値が最大のところを見に行く、あるいは期待値が最大の場所を見に行く方法などがあります。

詳細の説明はこちらに詳しく載っていたので、ぜひご覧ください。

機械学習のためのベイズ最適化入門(Slide shareのページに飛びます)

Python: BayesianOptimizationによるベイズ最適化

ベイズ最適化のメリット・デメリット

ハイパーパラメーターの自動調整には、他にグリッドサーチとランダムサーチがあります。これらと比較したときのメリット・デメリットを簡単に紹介します。

ベイズ最適化のメリット・デメリット面を大雑把に言えば、グリッドサーチとランダムサーチの中間のようなものです。

メリット

計算時間が比較的短い

最適解にたどり着きやすい

→総当たりではないのでグリッドサーチよりも計算時間は短く(検証する回数上限も指定できる)、ランダムに組み合わせている訳ではないのでランダムサーチより最適解にたどり着きやすくなります。

デメリット

離散値を取るハイパーパラメーターの調整に無駄な計算が発生する

→ベイズ最適化は連続値をとる関数の最大値を推定する手法です。そのため、離散値のハイパーパラメーターを調整する場合、異なる値であっても(例:3.4と3.5)関数内で離散値に変換されて同じ値(例:3)になります。つまり、無駄な計算が生じます。

手順①:bayesian-optimizationのインストール

今回は、bayesian-optimizationというライブラリを使用します。ターミナルにてこちらを入力してください。

pip install bayesian-optimization

手順②:ライブラリをインポート

import numpy as np
import pandas as pd
from pandas import Series, DataFrame
import sklearn
from sklearn.model_selection import train_test_split
import tensorflow as tf
from tensorflow import keras
import pathlib
from keras import optimizers
from tensorflow.keras import layers
from sklearn import datasets
from keras.models import Sequential
from keras.layers.core import Dense, Activation
from bayes_opt import BayesianOptimization

jupyter notebookなどPythonの開発環境でコードを書いていきます。まずはライブラリのインポートです。

手順③:データの準備

from sklearn.datasets import load_boston
boston = load_boston()
X = DataFrame(boston.data, columns=boston.feature_names)
Y = DataFrame(boston.target, columns=['Price'])

いつものようにボストンの住宅価格に関するデータをインポートします。sklearnに付属のデータです。説明変数Xと目的変数Yに分離します。

手順④:データの正規化

def norm(x):
    mean = np.mean(x)
    std = np.std(x)
    return (x - mean) / std
 
X = norm(X)
Y = norm(Y)

学習がスムーズにいくように、訓練データ・テストデータをどちらも正規化します。ディープラーニングを行う際は、正規化を行わないと訓練が難しくなることがあります。これに関しては、TensorFlowの回帰チュートリアルにも記載してあります。

スケールや値の範囲が異なる特徴量を正規化するのは良い習慣です。特徴量の正規化なしでもモデルは収束するかもしれませんが、モデルの訓練はより難しくなり、結果として得られたモデルも入力で使われる単位に依存することになります。(TensorFlow公式チュートリアル)

手順⑤:データをHold Out(訓練用・テスト用に分ける)

train_x, test_x, train_y, test_y = train_test_split(X_normed, Y_normed)

sckit-learnのtrain_test_splitを利用して、データを訓練用とテスト用に分けます。

手順⑥:モデルをつくる

def buildModel(lr, batch_size):  
    
    #モデル用パラメーターをセット
    neurons = 10
    num_hidden_layers = 3
    
    #モデルをインスタンス化
    model = Sequential()
    
    #入力層
    model.add(Dense(neurons, activation="relu", input_shape=[len(train_x.keys(),)]))
    #隠れ層
    for i in np.arange(num_hidden_layers):
        model.add(Dense(neurons, activation="relu"))
    #出力層
    model.add(Dense(1))
    
    #最適化アルゴリズムの設定
    optimizer = optimizers.adam(lr)
    
    #損失関数と最適化アルゴリズムをセット
    model.compile(loss='mean_squared_error', optimizer=optimizer,
 metrics=['mean_absolute_error', 'mean_squared_error'])
    
    #訓練データでトレーニング
    model.fit(train_x, train_y,verbose=0, batch_size=int(batch_size))  
    
    #テストデータで精度を確認
    score = model.evaluate(test_x, test_y)
    
    #score[0]はloss
    #score[1]はmean_absolute_error
    #score[2]はmean_squered_error
    return score[0]*(-1)

そして、返り値がテストデータの評価値(この例は絶対二乗誤差)となる関数をつくります。我々はこの評価値を最小、あるいは最大にすることを最終目標に置いています。

また、関数の引数には最適化をしたいハイパーパラメーターを設定してください。この引数を自動で入れ替えていくことで最適化が行われます。

また、26行目を見てください。int()でキャストしているのは、小数を整数に変換するためです。ベイズ最適化は連続関数の最大値を推定する手法のため、小数が引数に入ってしまいます。学習率は小数でも構いませんが、バッチサイズは整数しか取れないので関数内でキャストする必要があります。

訓練データでfitして、テストデータでevaluateします。損失関数(最小二乗誤差)を最小化したいので、0番目の値を取り出します。ベイズ最適化は関数の戻り値の最大化を行うので、損失関数に-1を掛けてください。分類問題の正解率を戻り値としている場合は、数値が大きいほど良いので-1を掛ける必要はありません。

手順⑦:ベイズ最適化用の関数をつくる

def bayesOpt():
    pbounds = {
        'batch_size' : (2,8),
        'lr' : (0.001, 1.0)
    }
    optimizer = BayesianOptimization(f=buildModel, pbounds=pbounds)
    optimizer.maximize(init_points=5, n_iter=10, acq='ucb')
    return optimizer

さて、いよいよベイズ最適化です。調べたいハイパーパラメーターは辞書型で与えてください。関数buildModelで設定した引数と全く同じ名前をキーにし、範囲をタプル()で指定します。

この場合、batch_sizeは2から8、lr(学習率)は0.001から1.0の間で検証されます。

optimizerインスタンスを生成し、関数とハイパーパラメーターの辞書を渡します。このインスタンスのmaximizeメソッドを使うことで、ベイズ最適化が行われます。

init_pointsは、初期の点を何箇所置くかを指定します。ベイズ最適化は関数からアウトプットされた結果の点に対して仮の関数を想定しながら、最大値を探していくので、初期値として点が必要になります。

n_iterはハイパーパラメーター探索する回数を与えます。

acqは獲得関数といって、次の点をどのような基準で選ぶかを決める関数です。デフォルトはucbで、eiとpoiが選べます。詳細は上でも紹介した「機械学習のためのベイズ最適化入門」のスライドをご確認ください。

手順⑧:ベイズ最適化を実行し結果を確認

#ベイズ最適化の実行
result = bayesOpt()

result.res
#全結果
[{'target': -1.2880561426868589,
  'params': {'batch_size': 5.294149263352722, 'lr': 0.9704499214827824}},
 {'target': -3.4454049707397703,
  'params': {'batch_size': 2.8022964297906263, 'lr': 0.5844529019248895}},
.......(省略

result.max
#最も良かった結果の詳細
{'target': -1.0840585231781006,
 'params': {'batch_size': 6.277500126662448, 'lr': 0.46829416051585}}

ベイズ最適化を実行して結果を確認します。batch_sizeはint()でキャストしているので、実際は小数点以下切り捨てです。(四捨五入ではない)

計算中は過程が自動で更新表示されていくので、安心感があります。いいスコアを更新すると、そのスコアの文字色が紫色になるので大変見やすいです。

最も良かった結果は、result.maxに入っています。辞書型なので、このまま別の処理に流すことも出来ます。ベイズ最適化はここまでになります!

まとめ

総当たりするほどのコンピューターリソースは無いけど、それなりに良い値が欲しい!というときは大活躍なのがベイズ最適化です。とはいえ、数値のチューニングに特化したものなので最適化アルゴリズムや活性化関数などのチューニングには利用できません。

数値以外のハイパーパラメーターはグリッドサーチかランダムサーチが必要です。

それと、1層あたりのニューロン数を2~5000の間で最適化したいという場合も範囲が広すぎるので、相当な計算量が必要になってしまいます。この場合はランダムサーチで2〜5000の間をまんべんなく調べていくのが良いでしょう。

職人技的な要素が強いチューニングの作業ですが、AutoMLの普及にしたがって、仕事が終わった後もコンピューターに仕事をさせることができるようになりました。

ぜひ、ベイズ最適化をご活用ください!

補足:全ソースコード

各種ライブラリがインストールされていれば、そのまま動かせます。まずは体験してみてください。

import numpy as np
import pandas as pd
from pandas import Series, DataFrame
import sklearn
from sklearn.model_selection import train_test_split
import tensorflow as tf
from tensorflow import keras
import pathlib
from keras import optimizers
from tensorflow.keras import layers
from sklearn import datasets
from keras.models import Sequential
from keras.layers.core import Dense, Activation
from bayes_opt import BayesianOptimization

from sklearn.datasets import load_boston
boston = load_boston()
X = DataFrame(boston.data, columns=boston.feature_names)
Y = DataFrame(boston.target, columns=['Price'])


#正規化しておく
def norm(x):
    mean = np.mean(x)
    std = np.std(x)
    return (x - mean) / std

X_normed = norm(X)
Y_normed = norm(Y)

train_x, test_x, train_y, test_y = train_test_split(X_normed, Y_normed)

def buildModel(lr, batch_size=2):  
    
    #モデル用パラメーターをセット
    neurons = 10
    num_hidden_layers = 3
    
    #モデルをインスタンス化
    model = Sequential()
    
    #入力層
    model.add(Dense(int(neurons), activation="relu", input_shape=[len(train_x.keys(),)]))
    #隠れ層
    for i in np.arange(int(num_hidden_layers)):
        model.add(Dense(int(neurons), activation="relu"))
    #出力層
    model.add(Dense(1))
    
    #最適化アルゴリズムの設定
    optimizer = optimizers.adam(lr)
    
    #損失関数と最適化アルゴリズムをセット
    model.compile(loss='mean_squared_error', optimizer=optimizer, metrics=['mean_absolute_error', 'mean_squared_error'])
    
    #訓練データでトレーニング
    model.fit(train_x, train_y,verbose=0, batch_size=int(batch_size))  
    
    #テストデータで精度を確認
    score = model.evaluate(test_x, test_y)
    
    #score[0]はloss
    #score[1]はmean_absolute_error
    #score[2]はmean_squered_error
    return score[0]*(-1)

def bayesOpt():
    pbounds = {
        'batch_size' : (2,8),
        'lr' : (0.001, 1.0)
    }
    optimizer = BayesianOptimization(f=buildModel, pbounds=pbounds)
    optimizer.maximize(init_points=5, n_iter=10, acq='ucb')
    return optimizer

result = bayesOpt()