注意
前往页面底部下载完整的示例代码。
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 优化分类器的超参数 C
和 solver
。当你直接引入 Optuna 时,你需要定义一个 objective
函数,该函数接受 trial
参数,并调用 trial
的 suggest_*
方法来采样超参数。
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-run 和 define-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 接口使我们能够优化批量目标函数,以实现更快的优化。例如,可并行化的评估、向量操作等。
以下目标函数接受批量超参数 xs
和 ys
,而不是单个超参数对 x
和 y
,并计算整个向量的目标值。
def batched_objective(xs: np.ndarray, ys: np.ndarray):
return xs**2 + ys
在下面的示例中,一个批次中的超参数对数量是 \(10\),并且 batched_objective
被评估三次。因此,trial 的数量是 \(30\)。请注意,你需要在批量评估后存储 trial_numbers
或 trial
,以便调用 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)