メモ帳

python, juliaで機械学習をやっていく

keras tunerでtf.kerasのハイパーパラメータを探索する

keras tuner

2019年10月末にメジャーリリースされたkeras tunerを試してみたいと思います。 github.com

できること

機械学習モデルのハイパーパラメータの探索

対応フレームワーク・ライブラリ

  • tensorflow
  • sckit-learn

使用可能な探索アルゴリズム

参考: Hyper-parameter optimization algorithms: a short review | by Aloïs Bissuel | Criteo R&D Blog | Medium

その他

  • 探索履歴の保存・再読み込み
  • 分散処理

基本的な手続き

  1. 探索するパラメーターの範囲指定 (モデル内に直接書き込む)
  2. チューナーインスタンス生成 (探索手法の決定)
  3. 探索実行

目次

  1. 一連の手続きを紹介
  2. 探索パラメータの範囲をyamlで指定できるようにする

今回使用したコードはこちらのcolabから確認できます。

1. 一連の手続きを紹介

1.1 パラメーターの範囲指定

後述するtuner instanceの生成時にモデルを作成する関数を渡す必要があります。 なお、その関数はhpという引数をもっていなければいけません。 そして、モデルを定義する際にhpを使って、明示的にパラメーターの範囲を指定することでパラメータの探索が可能になります。 指定にはhp.Inthp.Choiceなどを用います。

from tensorflow import keras
from tensorflow.keras.layers import Dense

def build_model(hp):
    model = keras.Sequential()
    model.add(Dense(units=hp.Int('units',
                                min_value=32,
                                max_value=512,
                                step=32),
                    activation='relu'))
    model.add(Dense(3, activation='softmax'))
    model.compile(
        optimizer=keras.optimizers.Adam(
            hp.Choice('learning_rate',
                      values=[1e-2, 1e-3, 1e-4])),
        loss='sparse_categorical_crossentropy',
        metrics=['acc'])
    return model

1.2. tuner instanceの生成

最適化するモデルと探索手法などを指定します。今回は、hyperbandを使ってみます。

from kerastuner.tuners import Hyperband

tuner = Hyperband(
    build_model,
    objective='val_acc',
    max_epochs=5,
    directory='my_dir',
    project_name='tf',
    overwrite=False
    )

ここで、結果を保存するためにmy_dir/tfというディレクトリが作られます。 すでに結果が保存済みの場合に上記のコードを動かすと探索履歴がリロードされます。

また、範囲の確認もできます。

tuner.search_space_summary()
# >> Search space summary
#     |-Default search space size: 2
#     units (Int)
#     |-default: None
#     |-max_value: 512
#     |-min_value: 32
#     |-sampling: None
#     |-step: 32
#     learning_rate (Choice)
#     |-default: 0.01
#     |-ordered: True
#     |-values: [0.01, 0.001, 0.0001]

1.3. 探索

探索を実行します。.fit()への引数 (callbacksなど) はここでわたします。

# datasetの取得
from sklearn import datasets
from sklearn.model_selection import train_test_split
iris = datasets.load_iris()
x, val_x, y, val_y = train_test_split(iris.data, iris.target)

# 探索の実行
tuner.search(x, y,
             epochs=5,
             validation_data=(val_x, val_y))

すると、kerasのhistory()が表示されて探索の経過が確認できます。

1.4. 結果の確認

改めてtunerインスタンスを生成して、保存されている結果をリロードします。 ただし、build_model関数とdirectoryとproject_nameは探索時に指定したものと同じものを入れて下さい。他は違っていてもリロードできます。

注) overwriteをTrueにするとリロードはされません。

tuner = Hyperband(
    build_model,
    objective='val_acc',
    max_epochs=5,
    directory='my_dir',
    project_name='tf',
    overwrite=False
    )

リロードしたtunerインスタンスから探索の結果を確認します。 以下で、スコアの上位10件の学習結果とそのときのハイパーパラメータがprintされます。

tuner.results_summary()
# >>> Results summary
#     |-Results in my_dir/tf
#     |-Showing 10 best trials
#     |-Objective(name='val_loss', direction='min')

#     ...

# >>> Trial summary
#     |-Trial ID: 51036f17dcb4f6b1aa4cf11a720f1c30
#     |-Score: 0.9210526347160339
#     |-Best step: 0
#     Hyperparameters:
#     |-learning_rate: 0.001
#     |-tuner/bracket: 0
#     |-tuner/epochs: 5
#     |-tuner/initial_epoch: 0
#     |-tuner/round: 0
#     |-units: 448

1.a. sklearnのモデルのパラメータ探索

sklearn用のtunerインスタンスを使うことで可能になります。 なお、Optimizationは以下の様にkerasで使ったものとは別クラスなので注意が必要です。

  • kerasの場合: kerastunerからインポート
  • sklearnの場合: kerastuner.oraclesからインポート
from sklearn.ensemble import ExtraTreesClassifier

def build_model_sk(hp):
    model = ExtraTreesClassifier(
        n_estimators=hp.Int('n_estimators', 10, 50, step=10),
        max_depth=hp.Int('max_depth', 3, 10),
        criterion=hp.Choice('criterion', values=['gini', 'entropy']),
        random_state=222,
    )
    return model

import kerastuner as kt
from kerastuner.tuners import Sklearn
from kerastuner.oracles import BayesianOptimization as OracleBayesianOptim
from sklearn import metrics
from sklearn import model_selection

tuner = Sklearn(
    hypermodel=build_model_sk,
    oracle=OracleBayesianOptim(
        objective=kt.Objective('score', 'max'),
        max_trials=50),
    scoring=metrics.make_scorer(metrics.accuracy_score),
    cv=model_selection.StratifiedKFold(5),
    directory='my_dir',
    project_name='sk',
    overwrite=True)

tuner.search(x, y)
# >>> Results summary
#     |-Results in my_dir/sk
#     |-Showing 10 best trials
#     |-Objective(name='score', direction='max')

#     ...

#     Trial summary
#     |-Trial ID: 94ea01baee4b398ca1c5cb35e6308856
#     |-Score: 0.9644268774703557
#     |-Best step: 0
#     Hyperparameters:
#     |-criterion: gini
#     |-max_depth: 3
#     |-n_estimators: 50

2. yamlでパラメータ範囲を指定

パラメータの範囲をかえるたびにコードを変更したり、別ファイルを作っていると、再現性を損なったり管理が面倒になるので、yamlで範囲やデフォルト値を指定して、モデル定義時に読み込む形をとりたいと思います。

まず、yamlで記述されたパラメータの探索範囲をdictに変換し、pythonで扱えるようにします。今回の例ではコンフィグをpython スクリプトに直接書き込みますが、実際は.yml形式の別ファイルから読み込んで下さい。

import yaml
configs_yaml = """
tuner_configs: # tunerインスタンス生成時に指定する値
    directory: my_dir
    project_name: tf_yaml2
    epochs: 5
hyperparams: # make_model内で指定する値
    fixed:           # 固定値
        num_dense: 2
        units: 64
    search:        # 探索パラメータ (hp.xxxにあわせて必要な値を記述する)
        units:       # hp.Intなので以下3つが必要
            min_value: 32
            max_value: 512
            step: 32
        learning_rate:   # hp.Choiceなので以下の1が必須
            values: 
                - 1.0e-2
                - 1.0e-3
                - 1.0e-4
"""
configs = yaml.safe_load(configs_yaml)
# >>> {'hyperparams': {'fixed': {'num_dense': 2, 'units': 64},
#   'search': {'learning_rate': {'values': [0.01, 0.001, 0.0001]},
#    'units': {'max_value': 512, 'min_value': 32, 'step': 32}}},
#  'tuner_configs': {'directory': 'my_dir',
#   'epochs': 5,
#   'project_name': 'tf_yaml2'}}

次に、hp.xxx形式の探索範囲指定をdictから行えるようにbuild_modelを変更します。 モデルの定義は# definition of model以下で行います。ただし、hp.xxxに対して_set_params関数を使って引数に必要な値を代入しています。 また、モデル定義の直前でhp.Fixedに固定値を入力しています。ここで固定値が入力されていれば、モデル定義内でhp.xxxを使ってパラメータの範囲を指定しても固定値が優先されて、探索は行われなくなります。ただし、探索範囲がないとモデルの定義ができないので、yamlには範囲を書いておく必要があります。

def build_model(configs, num_classes):
    def _set_fixed_params(hp):
        # set fixed params in hp
        fixed_params = configs.get('fixed', {})
        for key, val in fixed_params.items():
            try:
                hp.Fixed(key, val)
            except AttributeError: pass
        return hp

    def _set_params(name):
        # shortcut for input params range in hp.xxx
        params = configs.get('search')
        return dict(name=name, **params.get(name, {}))

    def _build_model(hp):
        hp = _set_fixed_params(hp)
        fixed_params = configs.get('fixed', {})

        # definition of model
        model = keras.Sequential()
        for i in range(fixed_params.get('num_dense')):
            model.add(Dense(units=hp.Int(**_set_params('units')),
                            activation='relu'))

        model.add(Dense(num_classes, activation='softmax'))
        model.compile(
            optimizer=keras.optimizers.Adam(
                hp.Choice(**_set_params('learning_rate'))),
            loss='sparse_categorical_crossentropy',
            metrics=['acc'])
        
        return model
    return _build_model

ついでに、tunerインスタンスの生成も関数にしておきます。

from kerastuner.tuners import Hyperband

def train(x, y, val_x, val_y, configs, overwrite=False):
    tuner_configs = configs.get('tuner_configs')
    tuner = Hyperband(
        build_model(configs.get('hyperparams'), 3),
        objective='val_acc',
        max_epochs=tuner_configs.get('epochs'),
        directory=tuner_configs.get('directory'),
        project_name=tuner_configs.get('project_name'),
        overwrite=True
        )
    return tuner.search(x, y,
                epochs=tuner_configs.get('epochs'),
                validation_data=(val_x, val_y))

あとは、以下の要領で、yamlのloadだけで様々なパラメータ探索が可能です。

configs = yaml.safe_load(configs_yaml)
train(x, y, val_x, val_y, configs, overwrite=False)

まとめ

パラメータ探索だとOptunaなど有名なものがいくつかありますが、少なくともtf.kerasに関してはkeras tunerは使いやすさの点で優位性があるのではないかと感じました!

ref