Ask-and-Tell 接口

Optuna 拥有一个 Ask-and-Tell 接口,为超参数优化提供了更灵活的接口。本教程解释了 Ask-and-Tell 接口有益的三个用例:

将 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)  # the objective

然后你尝试使用 Optuna 优化分类器的超参数 Csolver。当你直接引入 Optuna 时,你需要定义一个 objective 函数,该函数接受 trial 参数,并调用 trialsuggest_* 方法来采样超参数。

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)

这个接口不够灵活。例如,如果 objective 除了 trial 还需要额外的参数,你需要像 如何定义具有自己的参数的目标函数? 中那样定义一个类。Ask-and-tell 接口提供了一种更灵活的语法来优化超参数。以下示例与前面的代码块等效。

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

n_trials = 10
for _ in range(n_trials):
    trial = study.ask()  # `trial` is a `Trial` and not a `FrozenTrial`.

    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)  # tell the pair of trial and objective value

主要区别在于使用了两个方法:optuna.study.Study.ask()optuna.study.Study.tell()optuna.study.Study.ask() 创建一个可以采样超参数的 trial,而 optuna.study.Study.tell() 通过传入 trial 和目标值来结束该 trial。你可以将 Optuna 的超参数优化应用于你的原始代码,而无需 objective 函数。

如果你想使用剪枝器(pruner)加快优化速度,你需要像下面这样将 trial 的状态显式地传递给 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

# define study with hyperband pruner.
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)  # tell the pruned state
    else:
        score = clf.score(X_valid, y_valid)
        study.tell(trial, score)  # tell objective value

注意

optuna.study.Study.tell() 方法可以接受 trial 编号而不是 trial 对象。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")),
}

每次调用时,将 distributions 传递给 optuna.study.Study.ask() 方法。返回的 trial 包含建议的超参数。

study = optuna.create_study(direction="maximize")
n_trials = 10
for _ in range(n_trials):
    trial = study.ask(distributions)  # pass the pre-defined distributions.

    # two hyperparameters are already sampled from the pre-defined distributions
    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)

批量优化 (Batch Optimization)

Ask-and-tell 接口使我们能够优化批量目标函数,以实现更快的优化。例如,可并行化的评估、向量操作等。

以下目标函数接受批量超参数 xsys,而不是单个超参数对 xy,并计算整个向量的目标值。

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

在下面的示例中,一个批次中的超参数对数量是 \(10\),并且 batched_objective 被评估三次。因此,trial 的数量是 \(30\)。请注意,你需要在批量评估后存储 trial_numberstrial,以便调用 optuna.study.Study.tell() 方法。

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

for _ in range(3):
    # create batch
    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))

    # evaluate batched objective
    x_batch = np.array(x_batch)
    y_batch = np.array(y_batch)
    objectives = batched_objective(x_batch, y_batch)

    # finish all trials in the batch
    for trial_number, objective in zip(trial_numbers, objectives):
        study.tell(trial_number, objective)

提示

optuna.samplers.TPESampler 类可以接受一个布尔参数 constant_liar。建议在批量优化期间将此值设置为 True,以避免多个 worker 评估相似的参数配置。特别是当每个目标函数的评估成本较高、运行状态持续时间较长以及/或者 worker 数量较多时。

提示

optuna.samplers.CmaEsSampler 类可以接受一个 popsize 属性参数,该参数用作 CMA-ES 算法的初始种群大小。在批量优化的上下文中,可以将其设置为批量大小的倍数,以便从并行化操作中获益。

脚本总运行时间: (0 minutes 0.119 seconds)

由 Sphinx-Gallery 生成的图库