Skip to content

Commit 2512c73

Browse files
authored
Feature/optimize over entire discrete search spaces (#86)
Implement the possibility to optimize over entire discrete search spaces Signed-off-by: tip2rng <petru.tighineanu@de.bosch.com>
1 parent f5e15f3 commit 2512c73

File tree

4 files changed

+91
-10
lines changed

4 files changed

+91
-10
lines changed

blackboxopt/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "4.12.0"
1+
__version__ = "4.13.0"
22

33
from parameterspace import ParameterSpace
44

blackboxopt/optimizers/botorch_base.py

+47-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Callable, Dict, Iterable, Optional, Tuple, Union
1010

1111
from gpytorch.models import ExactGP
12+
from parameterspace import ParameterSpace
1213

1314
from blackboxopt.base import (
1415
Objective,
@@ -44,6 +45,33 @@
4445
) from e
4546

4647

48+
def _get_numerical_points_from_discrete_space(space: ParameterSpace) -> np.ndarray:
49+
"""Retrieve all points from a discrete space in the numerical representation"""
50+
points_along_dimensions = []
51+
for parameter_name in space.get_parameter_names():
52+
parameter = space.get_parameter_by_name(parameter_name)[
53+
"parameter"
54+
] # type:ignore
55+
if isinstance(parameter, ps.IntegerParameter):
56+
bounds = (parameter.bounds[0], parameter.bounds[1] + 1)
57+
points_along_dimensions.append(
58+
[parameter.val2num(v) for v in range(*bounds)]
59+
)
60+
elif isinstance(parameter, ps.OrdinalParameter) or isinstance(
61+
parameter, ps.CategoricalParameter
62+
):
63+
points_along_dimensions.append(
64+
[parameter.val2num(v) for v in parameter.values]
65+
)
66+
else:
67+
raise ValueError(
68+
f"Only discrete parameters are allowed but got {parameter}"
69+
)
70+
points = np.meshgrid(*points_along_dimensions)
71+
points = [p.reshape((p.size, 1)) for p in points]
72+
return np.concatenate(points, axis=-1)
73+
74+
4775
def _acquisition_function_optimizer_factory(
4876
search_space: ps.ParameterSpace,
4977
af_opt_kwargs: Optional[dict],
@@ -52,7 +80,10 @@ def _acquisition_function_optimizer_factory(
5280
"""Prepare either BoTorch's `optimize_acqf_discrete` or `optimize_acqf` depending
5381
on whether the search space is fully discrete or not and set required defaults if
5482
not overridden by `af_opt_kwargs`. If any of the af optimizer specific required
55-
kwargs are set, this overrides the automatic discrete space detection.
83+
kwargs are set, this overrides the automatic discrete space detection. In case an
84+
exclusively discrete space is detected and `num_random_choices` is not specified
85+
in `af_opt_kwargs`, the discrete acquisition function optimizer is using all
86+
possible combinations in the discrete space.
5687
5788
Args:
5889
search_space: Search space used for optimization.
@@ -76,6 +107,7 @@ def _acquisition_function_optimizer_factory(
76107
or "raw_samples" in kwargs
77108
or space_has_continuous_parameters
78109
):
110+
# continuous AF optimization
79111
return functools.partial(
80112
optimize_acqf,
81113
q=1,
@@ -86,12 +118,20 @@ def _acquisition_function_optimizer_factory(
86118
**kwargs,
87119
)
88120

89-
choices = torch.Tensor(
90-
[
91-
search_space.to_numerical(search_space.sample())
92-
for _ in range(kwargs.pop("num_random_choices", 5_000))
93-
]
94-
).to(dtype=torch_dtype)
121+
if "num_random_choices" not in kwargs and not space_has_continuous_parameters:
122+
# Optimize over the entire discrete search space, if the number of random
123+
# choices is not specified
124+
choices = torch.from_numpy(
125+
_get_numerical_points_from_discrete_space(search_space)
126+
).to(torch_dtype)
127+
else:
128+
# Optimize over the desired number of samples from the discrete search space
129+
choices = torch.Tensor(
130+
[
131+
search_space.to_numerical(search_space.sample())
132+
for _ in range(kwargs["num_random_choices"])
133+
]
134+
).to(dtype=torch_dtype)
95135
return functools.partial(optimize_acqf_discrete, q=1, choices=choices, **kwargs)
96136

97137

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "blackboxopt"
3-
version = "4.12.0"
3+
version = "4.13.0"
44
description = "A common interface for blackbox optimization algorithms along with useful helpers like parallel optimization loops, analysis and visualization scripts."
55
readme = "README.md"
66
repository = "https://github.com/boschresearch/blackboxopt"

tests/optimizers/botorch_base_test.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# see the NOTICE file and/or the repository https://github.com/boschresearch/blackboxopt
33
#
44
# SPDX-License-Identifier: Apache-2.0
5-
5+
import itertools
66
from functools import partial
77

88
import parameterspace as ps
@@ -16,6 +16,7 @@
1616
from blackboxopt.optimizers.botorch_base import (
1717
SingleObjectiveBOTorchOptimizer,
1818
_acquisition_function_optimizer_factory,
19+
_get_numerical_points_from_discrete_space,
1920
)
2021
from blackboxopt.optimizers.testing import (
2122
ALL_REFERENCE_TESTS,
@@ -78,6 +79,14 @@ def test_acquisition_function_optimizer_factory_with_discrete_space():
7879

7980
assert af_opt.func == optimize_acqf_discrete # pylint: disable=no-member
8081

82+
af_opt = _acquisition_function_optimizer_factory(
83+
discrete_space,
84+
af_opt_kwargs={"num_random_choices": 50},
85+
torch_dtype=torch.float64,
86+
)
87+
88+
assert af_opt.func == optimize_acqf_discrete # pylint: disable=no-member
89+
8190

8291
def test_acquisition_function_optimizer_factory_with_mixed_space():
8392
mixed_space = ps.ParameterSpace()
@@ -155,3 +164,35 @@ def test_find_optimum_in_1d_discrete_space(seed):
155164
assert (
156165
sum(l == 0 for l in losses) > 5
157166
), "After figuring out the best of the three points, it should only propose that."
167+
168+
169+
def test_get_numerical_points_from_discrete_space():
170+
p0l, p0h = -5, 10
171+
p1 = ("small", "medium", "large")
172+
p2 = ("woof", "miaow", "moo")
173+
discrete_space = ps.ParameterSpace()
174+
p_integ = ps.IntegerParameter("integ", (p0l, p0h))
175+
discrete_space.add(p_integ)
176+
p_ordin = ps.OrdinalParameter("ordin", p1)
177+
discrete_space.add(p_ordin)
178+
p_categ = ps.CategoricalParameter("categ", p2)
179+
discrete_space.add(p_categ)
180+
181+
points = _get_numerical_points_from_discrete_space(discrete_space)
182+
assert (
183+
points.shape[0] == p_integ.num_values * p_ordin.num_values * p_categ.num_values
184+
)
185+
assert points.shape[-1] == len(discrete_space)
186+
for integ, ordin, categ in itertools.product(
187+
range(p_integ.bounds[0], p_integ.bounds[1] + 1), p_ordin.values, p_categ.values
188+
):
189+
assert (
190+
(
191+
points
192+
== discrete_space.to_numerical(
193+
dict(integ=integ, ordin=ordin, categ=categ)
194+
)
195+
)
196+
.all(axis=1)
197+
.any()
198+
), f"Point {integ}, {ordin}, {categ} belongs to the search space but is not returned by `_get_numerical_points_from_discrete_space`"

0 commit comments

Comments
 (0)