Skip to content

Commit 8d3c1c9

Browse files
esantorellafacebook-github-bot
authored andcommitted
Enable BoTorch-based BenchmarkProblems with MapMetric (facebook#4252)
Summary: Pull Request resolved: facebook#4252 * Enables BoTorchTestFunction to produce data with `steps` and `MapMetric`. The data is simply repeated, so this is not interesting for modeling, but it is useful for measuring runtime. * Makes outputs 2d in accordance with the docstring. Reviewed By: saitcakmak Differential Revision: D81689093 fbshipit-source-id: 4ca953647818cc3ece21f9c60df3808e613ce860
1 parent 7e8a9cc commit 8d3c1c9

4 files changed

Lines changed: 92 additions & 7 deletions

File tree

ax/benchmark/benchmark_test_functions/botorch_test.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class BoTorchTestFunction(BenchmarkTestFunction):
4444
``self.botorch_problem``.
4545
self.botorch_problem.dim + len(dummy_param_names) should equal the
4646
number of parameters in the ``params`` passed to ``evaluate_true``.
47+
n_steps: Number of data points produced per metric and per evaluation. 1
48+
if data is not time-series. If data is time-series, this will
49+
eventually become the number of values on a `MapMetric` for
50+
evaluations that run to completion.
4751
"""
4852

4953
outcome_names: Sequence[str]
@@ -82,7 +86,7 @@ def __post_init__(self) -> None:
8286
)
8387

8488
def tensorize_params(self, params: Mapping[str, int | float]) -> torch.Tensor:
85-
"""Converts parameters to a tensor.
89+
"""Converts parameters to a 1d tensor.
8690
8791
If modified bounds are provided, we normalize the parameters from the modified
8892
bounds to the unit cube, and then unnormalize to the original problem bounds.
@@ -114,8 +118,17 @@ def evaluate_true(self, params: Mapping[str, float | int]) -> torch.Tensor:
114118
f"Expected {expected_n_dims} parameters, got {len(params)}."
115119
)
116120
x = self.tensorize_params(params=params)
117-
objectives = self.botorch_problem(x).view(-1)
121+
# self.botorch_problem(x) has shape [n_metrics] if n_metrics > 1,
122+
# otherwise []. So `objectives` has shape [n_metrics]
123+
objectives = torch.atleast_1d(self.botorch_problem(x))
118124
if isinstance(self.botorch_problem, ConstrainedBaseTestProblem):
119-
constraints = self.botorch_problem.evaluate_slack_true(x).view(-1)
120-
return torch.cat([objectives, constraints], dim=-1)
121-
return objectives
125+
constraints = torch.atleast_1d(self.botorch_problem.evaluate_slack_true(x))
126+
metrics = torch.cat([objectives, constraints], dim=-1)
127+
else:
128+
metrics = objectives
129+
# shape (n_metrics, 1)
130+
metrics = metrics.unsqueeze(1)
131+
if self.n_steps == 1:
132+
return metrics
133+
# shape (n_metrics, n_steps)
134+
return metrics.repeat(1, self.n_steps)

ax/benchmark/problems/synthetic/from_botorch.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ def create_problem_from_botorch(
9898
dict[AuxiliaryExperimentPurpose, list[AuxiliaryExperiment]] | None
9999
) = None,
100100
n_dummy_dimensions: int = 0,
101+
use_map_metric: bool = False,
102+
n_steps: int = 1,
101103
) -> BenchmarkProblem:
102104
"""
103105
Create a ``BenchmarkProblem`` from a BoTorch ``BaseTestProblem``.
@@ -154,6 +156,12 @@ def create_problem_from_botorch(
154156
n_dummy_dimensions: If >0, the search space will be augmented
155157
with extra dimensions. The corresponding parameters will have no
156158
effect on function values.
159+
use_map_metric: Whether to use a ``BenchmarkMapMetric`` (rather than a
160+
``BenchmarkMetric``).
161+
n_steps: Number of steps (progression values) in each evaluation. The
162+
default of 1 reflects a normal synthetic function evaluation. A
163+
higher number results in repeating the evaluation and getting the
164+
same result ``n_steps`` times (before IID noise is added).
157165
158166
Example:
159167
>>> from ax.benchmark.benchmark_problem import create_problem_from_botorch
@@ -213,6 +221,7 @@ def create_problem_from_botorch(
213221
dummy_param_names={
214222
n for n in search_space.parameters if "embedding_dummy_" in n
215223
},
224+
n_steps=n_steps,
216225
)
217226

218227
if isinstance(test_problem, MultiObjectiveTestProblem):
@@ -224,12 +233,14 @@ def create_problem_from_botorch(
224233
observe_noise_sd=observe_noise_sd,
225234
outcome_names=test_function.outcome_names,
226235
ref_point=test_problem._ref_point,
236+
use_map_metric=use_map_metric,
227237
)
228238
else:
229239
optimization_config = get_soo_opt_config(
230240
outcome_names=test_function.outcome_names,
231241
lower_is_better=lower_is_better,
232242
observe_noise_sd=observe_noise_sd,
243+
use_map_metric=use_map_metric,
233244
)
234245

235246
optimal_value = (

ax/benchmark/tests/benchmark_test_functions/test_botorch_test_function.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def test_tensor_shapes(self) -> None:
9292
for name, result in evaluate_true_results.items():
9393
with self.subTest(name=name):
9494
self.assertEqual(result.dtype, torch.double)
95-
self.assertEqual(result.shape, torch.Size([expected_len[name]]))
95+
self.assertEqual(result.shape, torch.Size([expected_len[name], 1]))
9696

9797
def test_input_dimensions(self) -> None:
9898
test_function = self.botorch_test_functions["base Hartmann"]
@@ -115,3 +115,27 @@ def test_dummy_dimensions(self) -> None:
115115
test_function.evaluate_true(params=params),
116116
embedded_test_function.evaluate_true(params=embedded_params),
117117
)
118+
119+
def test_with_steps(self) -> None:
120+
ha_params = {f"x{i}": 0.5 for i in range(6)}
121+
br_params = {"x0": 0.0, "x1": 0.0}
122+
for name, botorch_problem, outcome_names, params in [
123+
("Unconstrained", Hartmann(dim=6), ["y"], ha_params),
124+
("Constrained", ConstrainedHartmann(dim=6), ["y", "c"], ha_params),
125+
("Moo", BraninCurrin(), ["y1", "y2"], br_params),
126+
]:
127+
for n_steps in [1, 3]:
128+
with self.subTest(name=name):
129+
test_function = BoTorchTestFunction(
130+
outcome_names=outcome_names,
131+
botorch_problem=botorch_problem,
132+
n_steps=n_steps,
133+
)
134+
self.assertEqual(test_function.n_steps, n_steps)
135+
result = test_function.evaluate_true(params=params)
136+
self.assertEqual(
137+
result.shape, torch.Size([len(outcome_names), n_steps])
138+
)
139+
if n_steps == 2:
140+
# data is simply repeated down the step dimension
141+
self.assertEqual(result[0, 0].item(), result[0, 1].item())

ax/benchmark/tests/problems/synthetic/test_from_botorch.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from itertools import product
99

1010
import torch
11-
from ax.benchmark.benchmark_metric import BenchmarkMetric
11+
from ax.benchmark.benchmark_metric import BenchmarkMapMetric, BenchmarkMetric
1212
from ax.benchmark.benchmark_problem import get_continuous_search_space
1313
from ax.benchmark.benchmark_test_functions.botorch_test import BoTorchTestFunction
1414
from ax.benchmark.problems.synthetic.from_botorch import (
@@ -342,3 +342,40 @@ def test_get_name(self) -> None:
342342
test_problem=Branin(), n_dummy_dimensions=24, observe_noise_sd=True
343343
)
344344
self.assertEqual(name, "Branin_observed_noise_26d")
345+
346+
def test_with_map_metric(self) -> None:
347+
with self.subTest("With default n_steps"):
348+
problem = create_problem_from_botorch(
349+
test_problem_class=Branin,
350+
test_problem_kwargs={},
351+
num_trials=1,
352+
use_map_metric=True,
353+
)
354+
self.assertIsInstance(
355+
problem.optimization_config.objective.metric, BenchmarkMapMetric
356+
)
357+
self.assertEqual(problem.test_function.n_steps, 1)
358+
359+
with self.subTest("With non-default n_steps"):
360+
n_steps = 4
361+
problem = create_problem_from_botorch(
362+
test_problem_class=Branin,
363+
test_problem_kwargs={},
364+
num_trials=1,
365+
use_map_metric=True,
366+
n_steps=4,
367+
)
368+
self.assertIsInstance(
369+
problem.optimization_config.objective.metric, BenchmarkMapMetric
370+
)
371+
self.assertEqual(problem.test_function.n_steps, n_steps)
372+
373+
with self.subTest("MOO"):
374+
problem = create_problem_from_botorch(
375+
test_problem_class=BraninCurrin,
376+
test_problem_kwargs={},
377+
num_trials=1,
378+
use_map_metric=True,
379+
)
380+
metric = next(iter(problem.optimization_config.metrics.values()))
381+
self.assertIsInstance(metric, BenchmarkMapMetric)

0 commit comments

Comments
 (0)