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。您可以在不使用 objective 函数的情况下,将 Optuna 的超参数优化应用于您的原始代码。

如果您想通过 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 的示例。

在调用 optuna.study.Study.ask() 方法进行 define-and-run API 之前,为超参数定义分布。例如:

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,以避免多个工作节点评估相似的参数配置。特别是在每个目标函数评估成本很高且运行状态持续时间较长,以及/或工作节点数量较多的情况下。

提示

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

脚本总运行时间: (0 分钟 0.102 秒)

由 Sphinx-Gallery 生成的画廊