Ax makes it easy to evaluate performance of Bayesian optimization methods on synthetic problems through the use of benchmarking tools. This notebook illustrates how the benchmark suite can be used to easy test new methods on custom problems.
The first step is to define the benchmark problem. There are a collection of built-in useful benchmark problems, such as the classic Hartmann 6 optimization test problem:
from ax.benchmark.benchmark_problem import hartmann6
Or you can create a new problem. Benchmark problems can be defined by creating a BenchmarkProblem
object, as is done here for the constrained problem from Gramacy et al. (2016).
This entails defining a search space, optimization config, and the true optimal value of the benchmark.
import numpy as np
from ax.benchmark.benchmark_problem import BenchmarkProblem
from ax.core.objective import Objective
from ax.core.optimization_config import OptimizationConfig
from ax.core.outcome_constraint import ComparisonOp, OutcomeConstraint
from ax.core.parameter import ParameterType, RangeParameter
from ax.core.search_space import SearchSpace
from ax.metrics.noisy_function import NoisyFunctionMetric
# Create a Metric object for each function used in the problem
class GramacyObjective(NoisyFunctionMetric):
def f(self, x: np.ndarray) -> float:
return x.sum()
class GramacyConstraint1(NoisyFunctionMetric):
def f(self, x: np.ndarray) -> float:
return 1.5 - x[0] - 2 * x[1] - 0.5 * np.sin(2 * np.pi * (x[0] ** 2 - 2 * x[1]))
class GramacyConstraint2(NoisyFunctionMetric):
def f(self, x: np.ndarray) -> float:
return x[0] ** 2 + x[1] ** 2 - 1.5
# Create the search space and optimization config
search_space = SearchSpace(
parameters=[
RangeParameter(name="x1", parameter_type=ParameterType.FLOAT, lower=0.0, upper=1.0),
RangeParameter(name="x2", parameter_type=ParameterType.FLOAT, lower=0.0, upper=1.0),
]
)
# When we create the OptimizationConfig, we can define the noise level for each metric.
optimization_config=OptimizationConfig(
objective=Objective(
metric=GramacyObjective(
name="objective", param_names=["x1", "x2"], noise_sd=0.05
),
minimize=True,
),
outcome_constraints=[
OutcomeConstraint(
metric=GramacyConstraint1(name="constraint_1", param_names=["x1", "x2"], noise_sd=0.05),
op=ComparisonOp.LEQ,
bound=0,
relative=False,
),
OutcomeConstraint(
metric=GramacyConstraint2(name="constraint_2", param_names=["x1", "x2"], noise_sd=0.2),
op=ComparisonOp.LEQ,
bound=0,
relative=False,
),
],
)
# Create a BenchmarkProblem object
gramacy_problem = BenchmarkProblem(
name="Gramacy",
fbest=0.5998,
optimization_config=optimization_config,
search_space=search_space,
)
The Bayesian optimization methods to be used in benchmark runs are defined as a GenerationStrategy
, which is a list of model definitions and a specification of how many iterations to use each model for. Model definitions can be:
Models
registry with custom keyword arguments specified in GenerationStep
,ModelBridge
. Note that in the latter case, GenerationStrategy
cannot be serialized, so first approach is recommended wherever possible.
Here we construct a GenerationStrategy
that begins with 10 points from a non-scrambled Sobol sequence (we disable scrambling so all methods begin with the same initialization) and then switches to Bayesian optimization (using the Botorch model default of GP with noisy expected improvement) for any number of additional iterations.
from ax.modelbridge.generation_strategy import GenerationStrategy, GenerationStep
from ax.modelbridge.registry import Models
from ax.modelbridge.transforms.unit_x import UnitX
from ax.modelbridge.transforms.standardize_y import StandardizeY
strategy1 = GenerationStrategy(
name='GP+NEI',
steps=[
GenerationStep(model=Models.SOBOL, num_arms=10, model_kwargs={"scramble": False}),
GenerationStep(
model=Models.BOTORCH,
num_arms=-1, # Do not limit the number of arms this phase can generate.
model_kwargs={"transforms": [UnitX, StandardizeY]},
),
],
)
The Botorch generation step above could also be expressed using a custom factory function.
from ax.modelbridge.torch import TorchModelBridge
from ax.models.torch.botorch import BotorchModel
from ax.modelbridge.transforms.unit_x import UnitX
from ax.modelbridge.transforms.standardize_y import StandardizeY
def get_botorch_model(experiment, data, search_space):
m = BotorchModel() # This can be any implementation of TorchModel
return TorchModelBridge(
experiment=experiment,
search_space=search_space,
data=data,
model=m,
transforms=[UnitX, StandardizeY],
)
generation_step = GenerationStep(model=get_botorch_model, num_arms=10)
The get_botorch_model
factory function defined above is equivalent to using the built-in Models.BOTORCH
function, but was defined explicitly here to illustrate how custom models can be used in the benchmarking.
We can also easily create purely (quasi-)random strategies for comparison:
from ax.modelbridge.factory import get_sobol
strategy2 = GenerationStrategy(
name='Quasirandom',
steps=[
GenerationStep(model=Models.SOBOL, num_arms=10, model_kwargs={"scramble": False}),
GenerationStep(model=Models.SOBOL, num_arms=-1),
],
)
We now run the benchmarks, which using the BOBenchmarkingSuite object will run each of the supplied methods on each of the supplied problems. Note that this runs a real set of benchmarks and so will take several minutes to complete. Here we repeat each benchmark test 5 times; normally that would be increased to reduce variance in the results.
from ax.benchmark.benchmark_suite import BOBenchmarkingSuite
b = BOBenchmarkingSuite()
b.run(
num_runs=5, # Each benchmark task is repeated this many times
total_iterations=20, # The total number of iterations in each optimization
batch_size=2, # Number of synchronous parallel evaluations
bo_strategies=[strategy1, strategy2],
bo_problems=[hartmann6, gramacy_problem],
)
[INFO 06-24 18:24:34] ax.benchmark.benchmark_runner: Testing GP+NEI on Hartmann6: [INFO 06-24 18:24:34] ax.benchmark.benchmark_runner: Run 0 [INFO 06-24 18:42:38] ax.benchmark.benchmark_runner: Run 1 [INFO 06-24 18:59:08] ax.benchmark.benchmark_runner: Run 2 [INFO 06-24 19:17:03] ax.benchmark.benchmark_runner: Run 3 [INFO 06-24 19:36:28] ax.benchmark.benchmark_runner: Run 4 [INFO 06-24 19:55:43] ax.benchmark.benchmark_runner: Testing Quasirandom on Hartmann6: [INFO 06-24 19:55:43] ax.benchmark.benchmark_runner: Run 0 [INFO 06-24 19:55:44] ax.benchmark.benchmark_runner: Run 1 [INFO 06-24 19:55:45] ax.benchmark.benchmark_runner: Run 2 [INFO 06-24 19:55:45] ax.benchmark.benchmark_runner: Run 3 [INFO 06-24 19:55:46] ax.benchmark.benchmark_runner: Run 4 [INFO 06-24 19:55:46] ax.benchmark.benchmark_runner: Testing GP+NEI on Gramacy: [INFO 06-24 19:55:46] ax.benchmark.benchmark_runner: Run 0 /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal [INFO 06-24 22:04:42] ax.benchmark.benchmark_runner: Run 1 /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal /data/users/drfreund/fbsource/fbcode/buck-out/dev/gen/bento/kernels/bento_kernel_ae#link-tree/gpytorch/utils/cholesky.py:42: RuntimeWarning: A not p.d., added jitter of 1e-08 to the diagonal [INFO 06-25 00:18:51] ax.benchmark.benchmark_runner: Run 2 [INFO 06-25 02:17:40] ax.benchmark.benchmark_runner: Run 3 [INFO 06-25 03:58:55] ax.benchmark.benchmark_runner: Run 4 [INFO 06-25 05:55:26] ax.benchmark.benchmark_runner: Testing Quasirandom on Gramacy: [INFO 06-25 05:55:26] ax.benchmark.benchmark_runner: Run 0 [INFO 06-25 05:55:27] ax.benchmark.benchmark_runner: Run 1 [INFO 06-25 05:55:28] ax.benchmark.benchmark_runner: Run 2 [INFO 06-25 05:55:29] ax.benchmark.benchmark_runner: Run 3 [INFO 06-25 05:55:30] ax.benchmark.benchmark_runner: Run 4
<ax.benchmark.benchmark_runner.BOBenchmarkRunner at 0x7ff7aa7aa898>
Once the benchmark is finished running, we can generate a report that shows the optimization performance for each method, as well as the wall time spent in model fitting and in candidate generation by each method.
from IPython.core.display import HTML
report = b.generate_report(include_individual=False)
HTML(report)
Gramacy, R. B., Gray, G. A., Digabel, S. L., Lee, H. K. H., Ranjan, P., Wells, G., and Wild, S. M. Modeling an Augmented Lagrangian for Blackbox Constrained Optimization. Technometrics, 58(1): 1–11, 2016.