Ask-and-Tell インターフェース

Optuna には Ask-and-Tell インターフェースが用意されており、ハイパーパラメータ最適化においてより柔軟な操作が可能です。 本チュートリアルでは、Ask-and-Tell インターフェースが特に有効な3つのユースケースを解説します:

最小限の変更で既存の最適化問題に Optuna を適用

従来の教師あり分類問題を考えます。検証精度を最大化することが目的です。 この目的を達成するため、単純なモデルとして LogisticRegression を使用します。

import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

import optuna


X, y = make_classification(n_features=10)
X_train, X_test, y_train, y_test = train_test_split(X, y)

C = 0.01
clf = LogisticRegression(C=C)
clf.fit(X_train, y_train)
val_accuracy = clf.score(X_test, y_test)  # 目的関数

次に、Optuna を使用して分類器のハイパーパラメータ Csolver を最適化します。 Optuna を単純に導入する場合、trial を受け取り、ハイパーパラメータをサンプリングするために trialsuggest_* メソッドを呼び出す objective 関数を定義します:

def objective(trial):
    X, y = make_classification(n_features=10)
    X_train, X_test, y_train, y_test = train_test_split(X, y)

    C = trial.suggest_float("C", 1e-7, 10.0, log=True)
    solver = trial.suggest_categorical("solver", ("lbfgs", "saga"))

    clf = LogisticRegression(C=C, solver=solver)
    clf.fit(X_train, y_train)
    val_accuracy = clf.score(X_test, y_test)

    return val_accuracy


study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=10)

このインターフェースは柔軟性に欠けます。 例えば、objectivetrial 以外の追加引数を必要とする場合、 独自の引数を持つ目的関数の定義方法 で説明されているように、クラスを定義する必要があります。 Ask-and-Tell インターフェースは、ハイパーパラメータ最適化のためのより柔軟な構文を提供します。 以下の例は、前のコードブロックと同等の動作をします。

study = optuna.create_study(direction="maximize")

n_trials = 10
for _ in range(n_trials):
    trial = study.ask()  # `trial` は `FrozenTrial` ではなく `Trial` です。

    C = trial.suggest_float("C", 1e-7, 10.0, log=True)
    solver = trial.suggest_categorical("solver", ("lbfgs", "saga"))

    clf = LogisticRegression(C=C, solver=solver)
    clf.fit(X_train, y_train)
    val_accuracy = clf.score(X_test, y_test)

    study.tell(trial, val_accuracy)  # トライアルと目的関数値のペアを送信

主な違いは、optuna.study.Study.ask()optuna.study.Study.tell() の2つのメソッドを使用することです。 optuna.study.Study.ask() はハイパーパラメータをサンプリング可能なトライアルを作成し、 optuna.study.Study.tell()trial と目的関数値を渡してトライアルを終了させます。 objective 関数を使用せずに、元のコードに Optuna のハイパーパラメータ最適化を適用できます。

プルーナーを使用して最適化を高速化する場合、optuna.study.Study.tell() メソッドの引数に トライアルの状態を明確に渡す必要があります。

import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import SGDClassifier
from sklearn.model_selection import train_test_split

import optuna


X, y = load_iris(return_X_y=True)
X_train, X_valid, y_train, y_valid = train_test_split(X, y)
classes = np.unique(y)
n_train_iter = 100

# ハイパーバンドプルーナーを使用したスタディの定義
study = optuna.create_study(
    direction="maximize",
    pruner=optuna.pruners.HyperbandPruner(
        min_resource=1, max_resource=n_train_iter, reduction_factor=3
    ),
)

for _ in range(20):
    trial = study.ask()

    alpha = trial.suggest_float("alpha", 0.0, 1.0)

    clf = SGDClassifier(alpha=alpha)
    pruned_trial = False

    for step in range(n_train_iter):
        clf.partial_fit(X_train, y_train, classes=classes)

        intermediate_value = clf.score(X_valid, y_valid)
        trial.report(intermediate_value, step)

        if trial.should_prune():
            pruned_trial = True
            break

    if pruned_trial:
        study.tell(trial, state=optuna.trial.TrialState.PRUNED)  # プルーニング状態を送信
    else:
        score = clf.score(X_valid, y_valid)
        study.tell(trial, score)  # 目的関数値を送信

Note

optuna.study.Study.tell() メソッドは、トライアルオブジェクトの代わりにトライアル番号も受け付けます。 study.tell(trial.number, y)study.tell(trial, y) と同等です。

Define-and-Run

Ask-and-Tell インターフェースは、define-by-rundefine-and-run の両方の API をサポートしています。 このセクションでは、上記の define-by-run の例に加えて、define-and-run API の例を示します。

define-and-run API を使用する前に、 optuna.study.Study.ask() メソッドを呼び出す前にハイパーパラメータの分布を定義します。 例えば

distributions = {
    "C": optuna.distributions.FloatDistribution(1e-7, 10.0, log=True),
    "solver": optuna.distributions.CategoricalDistribution(("lbfgs", "saga")),
}

各呼び出し時に distributionsoptuna.study.Study.ask() メソッドに渡します。 返された trial には提案されたハイパーパラメータが含まれます。

study = optuna.create_study(direction="maximize")
n_trials = 10
for _ in range(n_trials):
    trial = study.ask(distributions)  # 事前に定義した分布を渡します。

    # ハイパーパラメータの 2 つは既に事前定義された分布からサンプリングされています
    C = trial.params["C"]
    solver = trial.params["solver"]

    clf = LogisticRegression(C=C, solver=solver)
    clf.fit(X_train, y_train)
    val_accuracy = clf.score(X_test, y_test)

    study.tell(trial, val_accuracy)

バッチ最適化

Ask-and-Tell インターフェースを使用すると、バッチ化された目的関数を最適化して最適化を高速化できます。 例えば、並列化可能な評価、ベクトル演算などです。

以下の目的関数は、単一のハイパーパラメータ xy のペアの代わりに、 バッチ化されたハイパーパラメータ xsys を受け取り、完全なベクトル上で目的関数を計算します。

def batched_objective(xs: np.ndarray, ys: np.ndarray):
    return xs**2 + ys

以下の例では、バッチ内のハイパーパラメータのペア数は \(10\) で、 batched_objective は 3 回評価されます。 したがって、トライアル数は \(30\) です。 バッチ評価後に optuna.study.Study.tell() メソッドを呼び出すには、 trial_numbers または trial のいずれかを保存する必要があることに注意してください。

batch_size = 10
study = optuna.create_study(sampler=optuna.samplers.CmaEsSampler())

for _ in range(3):
    # バッチを作成
    trial_numbers = []
    x_batch = []
    y_batch = []
    for _ in range(batch_size):
        trial = study.ask()
        trial_numbers.append(trial.number)
        x_batch.append(trial.suggest_float("x", -10, 10))
        y_batch.append(trial.suggest_float("y", -10, 10))

    # バッチ化された目的関数を評価
    x_batch = np.array(x_batch)
    y_batch = np.array(y_batch)
    objectives = batched_objective(x_batch, y_batch)

    # バッチ内のすべてのトライアルを終了
    for trial_number, objective in zip(trial_numbers, objectives):
        study.tell(trial_number, objective)

Tip

optuna.samplers.TPESampler クラスはブール型パラメータ constant_liar を 取ります。この値を True に設定すると、バッチ最適化中に 複数のワーカーが類似したパラメータ設定を評価するのを防ぐことができます。 特に、各目的関数評価が高価で、実行状態の持続時間が 長い場合や、ワーカー数が多い場合に有効です。

Tip

optuna.samplers.CmaEsSampler クラスは popsize 属性パラメータを 取り、これは CMA-ES アルゴリズムの初期集団サイズとして使用されます。 バッチ最適化の文脈では、並列化可能な操作の恩恵を受けるために、 バッチサイズの倍数に設定することができます。

Total running time of the script: (0 minutes 0.159 seconds)

Gallery generated by Sphinx-Gallery