Abnormally Distributed

統計解析担当のライフサイエンス研究者 -> データサイエンティスト@コンサル

Optunaの概要

仕事でOptunaを使う機会があったので、論文を軽く読んでまとめてみる。

arxiv.org

Optunaとは

OptunaはPFNが開発したハイパーパラメータのチューニングツールであり、近年、勾配ブースティングやニューラルネットワークのチューニングによく使われている。
既存のツールに対するOptunaの特徴として、下記3点が挙げられている。

  1. Define-by-runで動的にパラメータ探索空間を設定可能
  2. 効率的なsampling, pruningアルゴリズム
  3. 設定が簡単で、軽量な実験から分散処理による大規模な計算まで対応可能

1. Define-by-run

define-by-runは深層学習フレームワークの分類に使われる用語だが、筆者らはパラメータチューニングツールにも同様の概念を提唱している。
従来のツールはdefine-and-run、すなわちパラメータの探索範囲を最初に設定した上で、最適なパラメータの探索を行うものだった。

一方、define-by-runでは動的に探索範囲を設定することができる。これは例えばニューラルネットワークのレイヤー数とユニット数を同時にチューニングする際などに役に立つ。

2. 効率的なsampling, pruningアルゴリズム

パラメータ最適化の効率は探索戦略(探索すべきパラメータを決める)と性能評価戦略(学習曲線から現在探索中のパラメータの良さを判断し、捨てるかどうか判断する)により決まる。

論文には探索(サンプリング)アルゴリズムの詳細は記載されていないが、TPE (tree-based Parzen estimator)*1, CMA-ES(Covariance Matrix Adaptation - Evolution Strategy)*2, GP-BO(Bayesian optimization) *3などを用いているとのこと。 サンプリング手法にはパラメータの相関を利用するrelational samplingと独立にサンプルするindependent samplingがある。TPEはindependent sampling、CMA-ES, GP-BOはrelational samplingである。

また、性能評価については、見込みの悪い試行を途中で終わらせること(pruning、枝刈り)が重要である。 OptunaではAsynchronous Successive Halving (ASHA)*4と呼ばれるアルゴリズムを採用している。ASHAでは試行中のパラメータセットの暫定的な順位に基づき、分散処理の個々のworkerが非同期的にpruningを行う手法とのこと。

使い方

Optunaではパラメータの最適化をobjective functionの最小化・最大化の問題と捉える。
最適化のプロセスをstudy、objective functionの1回の評価をtrialと呼ぶ。 パラメータの探索空間はtrialオブジェクトのメソッドにより動的に構築される。

以下のコードでは(モデルのチューニングではないが)、簡単な例として2次関数の最小化を行っている。

import optuna

def objective(trial):
    x = trial.suggest_uniform('x', -10, 10)
    return (x - 2) ** 2

study = optuna.create_study()
study.optimize(objective, n_trials=100)

study.best_params  # E.g. {'x': 2.002108042}}



以下はより実践的な、TensorFlowでニューラルネットワークのパラメータチューニングを行う場合のコード抜粋。 完全なコードは下記リンク先参照。 https://colab.research.google.com/drive/1MQp5rn6Dxhdv4r9PuJOyGf1sdsS2PHF1

ここでは、隠れ層の数、ユニット数、weight decayといったモデルのパラメータに加え、optimizerの種類とそのパラメータも最適化している。 従来のツールでこうしたチューニングを行うためには、最初にかなり複雑なパラメータ探索範囲を記述する必要があるが、Optunaのdefine-by-runの思想により、簡潔に実装することができる。

def create_model(trial):
    # We optimize the numbers of layers, their units and weight decay parameter.
    n_layers = trial.suggest_int("n_layers", 1, 3)
    weight_decay = trial.suggest_loguniform("weight_decay", 1e-10, 1e-3)
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Flatten())
    for i in range(n_layers):
        num_hidden = int(trial.suggest_loguniform("n_units_l{}".format(i), 4, 128))
        model.add(
            tf.keras.layers.Dense(
                num_hidden,
                activation="relu",
                kernel_regularizer=tf.keras.regularizers.l2(weight_decay),
            )
        )
    model.add(
        tf.keras.layers.Dense(CLASSES, kernel_regularizer=tf.keras.regularizers.l2(weight_decay))
    )
    return model


def create_optimizer(trial):
    # We optimize the choice of optimizers as well as their parameters.
    kwargs = {}
    optimizer_options = ["RMSprop", "Adam", "SGD"]
    optimizer_selected = trial.suggest_categorical("optimizer", optimizer_options)
    if optimizer_selected == "RMSprop":
        kwargs["learning_rate"] = trial.suggest_loguniform("rmsprop_learning_rate", 1e-5, 1e-1)
        kwargs["decay"] = trial.suggest_uniform("rmsprop_decay", 0.85, 0.99)
        kwargs["momentum"] = trial.suggest_loguniform("rmsprop_momentum", 1e-5, 1e-1)
    elif optimizer_selected == "Adam":
        kwargs["learning_rate"] = trial.suggest_loguniform("adam_learning_rate", 1e-5, 1e-1)
    elif optimizer_selected == "SGD":
        kwargs["learning_rate"] = trial.suggest_loguniform("sgd_opt_learning_rate", 1e-5, 1e-1)
        kwargs["momentum"] = trial.suggest_loguniform("sgd_opt_momentum", 1e-5, 1e-1)

    optimizer = getattr(tf.optimizers, optimizer_selected)(**kwargs)
    return optimizer


def objective(trial):
    # Get MNIST data.
    train_ds, valid_ds = get_mnist()

    # Build model and optimizer.
    model = create_model(trial)
    optimizer = create_optimizer(trial)

    # Training and validating cycle.
    with tf.device("/cpu:0"):
        for _ in range(EPOCHS):
            learn(model, optimizer, train_ds, "train")

        accuracy = learn(model, optimizer, valid_ds, "eval")

    # Return last validation accuracy.
    return accuracy.result()

とりあえず、特徴量を手当たり次第に作ってみてLightGBMの投入し、Optunaでハイパーパラメータ調整する、なんて作業は 誰か他の人にやってもらいたい。。