Optuna使い方解説:Keras,Python機械学習にハイパーパラメータ自動調整を実装【ディープラーニング,ラズパイ】

ハイパーパラメータ最適化を自動的・効率的に探索する、フレームワーク「Optuna」を利用した、AIプログラムのサンプル・使用方法を、前回の記事で公開しました。
https://nine-num-98.blogspot.com/2020/03/ai-hyper-opt-01.html


公開したプログラムの内容について、以下解説します。

GitHub公開プログラム
https://github.com/kotetsu99/deep_regression_opt

公開プログラムですが、主に4つのプログラムが存在します。

(1) 学習プログラム:01-deep_regression_train.py
(2) モデル可視化プログラム:02-dnn_visualization.py
(3) 予測プログラム:03-deep_regression_predict.py
(4) データプロットプログラム:04-deep_regression_plot.py

このうち、(2)については以下の記事で解説しています。
https://nine-num-98.blogspot.com/2020/03/dnn-visualization.html

また(3)(4)については、以前に↓でご紹介した工数見積もりAIプログラムと同じです。
https://nine-num-98.blogspot.com/2019/11/ai-deep-learning.html


本記事では(1)について解説を取り上げます。

各設定ファイル読み込み

01-deep_regression_train.py について、主にハイパーパラメータ試行関連の処理を見ていきます。

# 設定ファイル読み込み
conf_file = 'dnn.conf'
config = configparser.ConfigParser()
config.read(conf_file, 'UTF-8')

# ログ設定ファイル読み込み
logging.config.fileConfig('logging.conf')
logger = logging.getLogger()

# 目的関数最小値の初期値定義
min_vmae = np.inf

まずは、設定ファイルの読み込みです。上記では、configparser というライブラリを用いて、以下2つの設定ファイルの読み込みを行っています。

・ハイパーパラメータを含む、プログラム全体の設定を行う「dnn.conf」
・ログ出力関連の設定を行う「logging.conf」

dnn.confについての詳細は、以前の記事に解説しましたので、こちらをご確認ください。
https://nine-num-98.blogspot.com/2020/03/ai-hyper-opt-01.html

dnn.confに記載のハイパーパラメータ情報の取得は、別に定義されている

set_hyperparameter()

という関数で実施しています。また「min_vmae = np.inf」という箇所ですが、これについては後述します。

main関数

本プログラムのメイン関数です。

def main():
    # 環境設定(ディスプレイの出力先をlocalhostにする)
    os.environ['DISPLAY'] = ':0'

    # コマンド引数確認
    if len(sys.argv) < 2:
        print('使用法: python deep_regression_train.py 保存ファイル名.h5')
        sys.exit()

    # 探索試行回数を設定
    n_trials = config.getint('Trials','trials')

    # 最適化探索(optunaのstudyオブジェクト定義)
    study = optuna.create_study(sampler=optuna.samplers.TPESampler())
    # optimizeに最適化すべき目的関数(objective)を渡す。これをn_trials回試行する。目的関>数の値が最小のものを探索する。
    study.optimize(outer_objective(), n_trials)

    # 最適だった試行回を表示
    logger.info('best_trial.number: ' + 'trial#' + str(study.best_trial.number))
    # 目的関数の最適(最小)値を表示
    logger.info('best_vmae: ' + str(study.best_value))

    # ハイパーパラメータをソートして表示
    logger.info('--- best hyperparameter ---')
    sorted_best_params = sorted(study.best_params.items(), key=lambda x : x[0])
    for i, k in sorted_best_params:
        logger.info(i + ' : ' + str(k))
    logger.info('------------')

Optunaライブラリで定義されているメソッドを呼び出して、ハイパーパラメータ最適化探索を行います。探索処理を終えたら、最後に最適だったニューラルネットワークのハイパーパラメータをログ出力・表示して終了するという流れです。

Optunaは、まずstudyというオブジェクトを定義し、study.optimizeメソッドで最適化探索を行うという、割とシンプルな記述で処理を実装できます。

最適化探索においては、「目的関数(objective)」というものを定義し、指定回数(n_trials)この目的関数値の計算を繰り返します。目的関数の定義、計算方法は後述しますが、ここではニューラルネットワークの精度を表す誤差値です。

試行毎に、ハイパーパラメータを変えながらニューラルネットワークを生成、目的関数値の計算を行います。ここで、目的関数値が最小であるニューラルネットワークが最適なハイパーパラメータを持つものとされます。

optimizeメソッドに、objective、n_trialsを渡すことで、上記の探索処理が実行されます。ここではoptimizeに、目的関数(objective)ではなく、outer_objective()という関数を渡していますが、これはobjectiveを戻り値とする高階関数です。これを渡している理由については後述します。

探索を終えた後、studyオブジェクトは最適値に関する情報をプロパティ変数として持っています。例えば、

・study.best_trial.number :最適だった試行回、
・study.best_params :最適とされたハイパーパラメータ

のような情報です。
# 詳細は、下記を参照
https://optuna.readthedocs.io/en/latest/reference/study.html#

ここまでに出てきたハイパーパラメータ、目的関数等の用語の概念については、以下の書籍が参考になります。


outer_objective,objective関数

main関数の中で、ハイパーパラメータ最適値探索の際に実行される関数です。study.optimize メソッドで呼び出されているのが、以下のouter_objective関数です。

# objective関数を内包する高階関数。objective関数呼び出し前に、種々の事前設定等を行う。
def outer_objective():
    # 学習モデルファイル保存先パス取得
    savefile = sys.argv[1]

    # データセットファイル取得
    c_file_path = config['File Path']
    dataset_file = c_file_path['dataset_file']
    # データをロード
    X, y, n_features, n_outputs = data_set(dataset_file)
    #print(X)
    #print(y)

    # ハイパーパラメータの調整設定読み込み
    n_bs, nb_epochs, nb_patience, val_min_delta, n_layer_range, mid_units_range, dropout_rate_range, activation_list, optimizer_list = set_hyperparameter()
    # 収束判定設定。以下の条件を満たすエポックがpatience回続いたら打切り。
    # val_loss(観測上最小値) - min_delta  < val_loss
    es_cb = keras.callbacks.EarlyStopping(monitor='val_loss', min_delta=val_min_delta, patience=nb_patience, verbose=1, mode='min')

    print('obj_loop_start')

outer_objective関数は、生成する学習モデルの保存先パス取得、学習データセットの読み込みを行った後、ニューラルネットワークのハイパーパラメータの読み込みを行っています。

学習データセット読み込み時には、データ前処理(正規化・標準化)を行っています。この処理についての解説は、以下の記事に記していますので、参考にされてください。
https://nine-num-98.blogspot.com/2019/12/ai-normalization.html

ハイパーパラメータは、set_hyperparameter() という別で定義している関数を用いて、設定ファイル(dnn.conf)から取得します。

上記の処理は1回実施すればよいので、繰り返し試行される次の目的関数(objective)の前に実行できるよう記述しています。outer_objectiveという高階関数を作っているのはそれが理由です。

    # 目的関数
    def objective(trial):
        # グローバル変数の目的関数最小値呼び出し
        global min_vmae

        # 中間層数の探索範囲設定
        n_layer = trial.suggest_int(*n_layer_range)
        # ユニット数の探索範囲設定
        mid_units = int(trial.suggest_discrete_uniform(*mid_units_range))
        # ドロップアウト率の探索範囲設定
        dropout_rate = trial.suggest_uniform(*dropout_rate_range)
        # 活性化関数の探索候補設定
        activation = trial.suggest_categorical(*activation_list)
        # 最適化アルゴリズムの探索候補設定
        optimizer = trial.suggest_categorical(*optimizer_list)

        # 各パラメータを画面出力
        logger.info("trial#" + str(trial.number) + ': ' +
        "n_layer= " + str(n_layer) + ', ' +
        "mid_units= " + str(mid_units) + ', ' +
        "dropout_rate= " + str(dropout_rate) + ', ' +
        "activation= " + str(activation) + ', ' +
        "optimizer= " + str(optimizer))

        # 学習モデルの構築と学習の開始
        model = create_model(n_features, n_outputs, n_layer, activation, mid_units, dropout_rate, optimizer)
        history = model.fit(X, y, verbose=0, epochs=nb_epochs, validation_split=0.1, batch_size=n_bs, callbacks=[es_cb])

        # 最小値探索(各エポックで得られた目的関数のうち最小値を返す)
        vmae = np.amin(history.history['val_mean_absolute_error'])

        # これまでの最小目的関数より小さい場合更新して、最適モデルとして保存
        if vmae < min_vmae:
            min_vmae = vmae
            model.save(savefile)

            # 損失関数の時系列変化をグラフ表示
            plot_loss(history)

        return vmae
  
    return objective

Optunaのハイパーパラメータ最適化処理において核となる、目的関数(objective)をここで定義しています。

objective関数は、main関数の

study.optimize(outer_objective(), n_trials)

で渡されている、n_trialsの回数だけ実行されます。objective関数は引数としてtrialを取るのが決まりです。trialは「objective関数を1回呼び出して実行せよ」という意味を持っています。このobjective関数が実行されるたび、異なる構成のニューラルネットワークが生成・学習され、最終的にその目的関数値(vmae)を返します。

ニューラルネットワークのハイパーパラメータのうち、以下については毎回値を振りながら、モデルが生成されます。

・中間層数(n_layer): 1,2,3,4,5
・各中間層のニューロン数(mid_units): 10,15,20,25,30
・ドロップアウト率(dropout_rate): 0.0 ~ 0.1

これらの探索範囲のチューニングはdnn.confで設定しています。

次に学習モデルを実際に生成して、目的関数の計算を行います。目的関数ですが、これはニューラルネットワークの学習で計算される損失関数とは別のものです。計算方法は以下のように定義されます。

・目的関数:平均絶対誤差値
・損失関数:平均二乗誤差

モデル誤差を図るという点ではどちらも本質的に同じなのですが、前者は外れ値(非常に大きな値)の誤差が、比較的小さいです。回帰問題におけるモデル誤差の評価には、こちらが使われるのが一般的らしいです。

損失関数も同じでいいじゃないかとという気もしますが、平均絶対誤差値では絶対値が使われるため、勾配を求めるための微分ができません。このため一般的に平均二乗誤差が使われていると思われます。

また、目的関数はデータセット全体の1割(評価データ)、損失関数はそれ以外の9割のデータ(学習データ)を使って計算されます。

各試行毎に得られるモデルの目的関数値を、暫定最小値(min_vmae、初期値は無限大)と比較します。そして、これよりも小さい値となれば最小値を更新します。最終的に、この一番小さい目的関数値を出したものが、最適なニューラルネットワークモデルとして保存されます。

create_model関数


objective関数内で呼び出される、ニューラルネットワークモデル生成関数です。

def create_model(n_features, n_outputs, n_layer, activation, mid_units, dropout_rate, optimizer):
    # ニューラルネットワーク定義
    model = Sequential()

    # 中間層数、各ニューロン数、ドロップアウト率の定義
    for i in range(n_layer):
        model.add(Dense(mid_units, activation=activation, input_shape=(n_features,)))
        model.add(Dropout(dropout_rate))

    # 出力層を定義(ニューロン数は1個)
    model.add(Dense(units=n_outputs, activation='linear'))
    # 回帰学習モデル作成
    model.compile(loss='mean_squared_error', optimizer=optimizer, metrics=['mae'])
    # モデルを返す
    return model

objective関数が繰り返し試行されるたびに、ハイパーパラメータ(中間層数:n_layer、各層ニューロン数:mid_units、ドロップアウト率:dropout_rate…)が変更されます。
create_model関数は、これらを引数として各パラメータに従ったニューラルネットワークモデルを生成し、それを返します。

中間層までの層は、forループによる繰り返し処理で作成。最初の層は入力層となります。
出力層はニューロン1個(n_outputs)で、活性化関数は恒等関数です。

回帰問題であるため、学習モデル作成中に評価される損失関数は、平均二乗誤差としています。metrics(mae:平均絶対誤差)の設定がありますが、これは目的関数の値として使われます。


以上、プログラムの解説でした。set_hyperparameter() など、上記以外にも関数がありますが、比較的単純なのでコードのコメントを参照してもらえれば、大体は理解されるのではと思います。

スポンサーリンク