Note
Go to the end to download the full example code.
Ask-and-Tell Interface
Optuna has an Ask-and-Tell interface, which provides a more flexible interface for hyperparameter optimization. This tutorial explains three use-cases when the ask-and-tell interface is beneficial:
Apply Optuna to an existing optimization problem with minimum modifications
Let’s consider the traditional supervised classification problem; you aim to maximize the validation accuracy. To do so, you train LogisticRegression as a simple model.
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
Then you try to optimize hyperparameters C
and solver
of the classifier by using optuna.
When you introduce optuna naively, you define an objective
function
such that it takes trial
and calls suggest_*
methods of trial
to sample the hyperparameters:
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)
This interface is not flexible enough.
For example, if objective
requires additional arguments other than trial
,
you need to define a class as in
How to define objective functions that have own arguments?.
The ask-and-tell interface provides a more flexible syntax to optimize hyperparameters.
The following example is equivalent to the previous code block.
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
The main difference is to use two methods: optuna.study.Study.ask()
and optuna.study.Study.tell()
.
optuna.study.Study.ask()
creates a trial that can sample hyperparameters, and
optuna.study.Study.tell()
finishes the trial by passing trial
and an objective value.
You can apply Optuna’s hyperparameter optimization to your original code
without an objective
function.
If you want to make your optimization faster with a pruner, you need to explicitly pass the state of trial
to the argument of optuna.study.Study.tell()
method as follows:
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
Note
optuna.study.Study.tell()
method can take a trial number rather than the trial object.
study.tell(trial.number, y)
is equivalent to study.tell(trial, y)
.
Define-and-Run
The ask-and-tell interface supports both define-by-run and define-and-run APIs. This section shows the example of the define-and-run API in addition to the define-by-run example above.
Define distributions for the hyperparameters before calling the
optuna.study.Study.ask()
method for define-and-run API.
For example,
distributions = {
"C": optuna.distributions.FloatDistribution(1e-7, 10.0, log=True),
"solver": optuna.distributions.CategoricalDistribution(("lbfgs", "saga")),
}
Pass distributions
to optuna.study.Study.ask()
method at each call.
The retuned trial
contains the suggested hyperparameters.
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
The ask-and-tell interface enables us to optimize a batched objective for faster optimization. For example, parallelizable evaluation, operation over vectors, etc.
The following objective takes batched hyperparameters xs
and ys
instead of a single
pair of hyperparameters x
and y
and calculates the objective over the full vectors.
def batched_objective(xs: np.ndarray, ys: np.ndarray):
return xs**2 + ys
In the following example, the number of pairs of hyperparameters in a batch is \(10\),
and batched_objective
is evaluated three times.
Thus, the number of trials is \(30\).
Note that you need to store either trial_numbers
or trial
to call
optuna.study.Study.tell()
method after the batched evaluations.
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)
Total running time of the script: (0 minutes 0.140 seconds)