Skip to content

Commit 6e9c790

Browse files
authored
add support for multiple objectives in botorch to numerical (#88)
* add support for multiple objectives in botorch `to_numerical` --------- Signed-off-by: Grossberger Lukas (CR/AIR2.2) <Lukas.Grossberger@de.bosch.com>
1 parent 96d6ecb commit 6e9c790

File tree

5 files changed

+48
-24
lines changed

5 files changed

+48
-24
lines changed

blackboxopt/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "4.10.0"
1+
__version__ = "4.11.0"
22

33
from parameterspace import ParameterSpace
44

blackboxopt/optimizers/botorch_base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def _append_evaluations_to_data(
294294
X, Y = to_numerical(
295295
_evals,
296296
self.search_space,
297-
self.objective,
297+
objectives=[self.objective],
298298
batch_shape=self.batch_shape,
299299
torch_dtype=self.torch_dtype,
300300
)

blackboxopt/optimizers/botorch_utils.py

+15-12
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# SPDX-License-Identifier: Apache-2.0
55

66
import warnings
7-
from typing import Iterable, List, Optional, Tuple
7+
from typing import Iterable, List, Optional, Sequence, Tuple
88

99
import numpy as np
1010
import parameterspace as ps
@@ -13,6 +13,7 @@
1313

1414
from blackboxopt.base import ConstraintsError, Objective
1515
from blackboxopt.evaluation import Evaluation
16+
from blackboxopt.utils import get_loss_vector
1617

1718

1819
def filter_y_nans(
@@ -88,7 +89,7 @@ def impute_nans_with_constant(x: torch.Tensor, c: float = -1.0) -> torch.Tensor:
8889
def to_numerical(
8990
evaluations: Iterable[Evaluation],
9091
search_space: ps.ParameterSpace,
91-
objective: Objective,
92+
objectives: Sequence[Objective],
9293
constraint_names: Optional[List[str]] = None,
9394
batch_shape: torch.Size = torch.Size(),
9495
torch_dtype: torch.dtype = torch.float32,
@@ -102,7 +103,7 @@ def to_numerical(
102103
Args:
103104
evaluations: List of evaluations that were collected during optimization.
104105
search_space: Search space used during optimization.
105-
objective: Objective that was used for optimization.
106+
objectives: Objectives that were used for optimization.
106107
constraint_names: Name of constraints that are used for optimization.
107108
batch_shape: Batch dimension(s) used for batched models.
108109
torch_dtype: Type of returned tensors.
@@ -142,13 +143,15 @@ def to_numerical(
142143
dtype=torch_dtype,
143144
)
144145
X = X.reshape(*batch_shape + X.shape)
145-
Y = torch.tensor(
146-
np.array([[e.objectives[objective.name]] for e in evaluations], dtype=float),
147-
dtype=torch_dtype,
148-
)
149146

150-
if objective.greater_is_better:
151-
Y *= -1
147+
Y = torch.Tensor(
148+
[
149+
get_loss_vector(
150+
known_objectives=objectives, reported_objectives=e.objectives
151+
)
152+
for e in evaluations
153+
]
154+
).to(dtype=torch_dtype)
152155

153156
if constraint_names is not None:
154157
try:
@@ -163,12 +166,12 @@ def to_numerical(
163166
except KeyError as e:
164167
raise ConstraintsError(
165168
f"Constraint name {e} is not defined in input evaluations."
166-
)
167-
except TypeError:
169+
) from e
170+
except TypeError as e:
168171
raise ConstraintsError(
169172
f"Constraint name(s) {constraint_names} are not defined in input "
170173
+ "evaluations."
171-
)
174+
) from e
172175

173176
Y = Y.reshape(*batch_shape + Y.shape)
174177

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.10.0"
3+
version = "4.11.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_utils_test.py

+30-9
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_to_numerical(evaluations, search_space, greater_is_better):
5454

5555
objective = Objective(objective_name, greater_is_better)
5656

57-
X, Y = to_numerical(evaluations, search_space, objective)
57+
X, Y = to_numerical(evaluations, search_space, [objective])
5858

5959
assert X.dtype == torch.float32
6060
assert Y.dtype == torch.float32
@@ -82,7 +82,7 @@ def test_to_numerical_with_batch(evaluations, search_space):
8282
objective = Objective(objective_name, False)
8383

8484
batch_shape = torch.Size((1,))
85-
X, Y = to_numerical(evaluations, search_space, objective, batch_shape=batch_shape)
85+
X, Y = to_numerical(evaluations, search_space, [objective], batch_shape=batch_shape)
8686

8787
assert X.dtype == torch.float32
8888
assert Y.dtype == torch.float32
@@ -102,31 +102,31 @@ def test_to_numerical_raises_errors(search_space):
102102
objectives={objective_name: 0.64},
103103
)
104104
with pytest.raises(ValueError, match="Mismatch"):
105-
to_numerical([eval_err], search_space, objective)
105+
to_numerical([eval_err], search_space, [objective])
106106

107107
# parameter not within defined bounds -> invalid configuration
108108
eval_err = Evaluation(
109109
configuration={"x0": 0.1, "x1": False, "x2": "small", "fp": 0.5},
110110
objectives={objective_name: 0.64},
111111
)
112112
with pytest.raises(ValueError, match="not valid"):
113-
to_numerical([eval_err], search_space, objective)
113+
to_numerical([eval_err], search_space, [objective])
114114

115115
# conditional parameter should be active, but is inactive -> invalid configuration
116116
eval_err = Evaluation(
117117
configuration={"x0": 0.57, "x1": True, "x2": "small", "fp": 0.5},
118118
objectives={objective_name: 0.64},
119119
)
120120
with pytest.raises(ValueError, match="not valid"):
121-
to_numerical([eval_err], search_space, objective)
121+
to_numerical([eval_err], search_space, [objective])
122122

123123
# conditional parameter should be inactive, but is active -> invalid configuration
124124
eval_err = Evaluation(
125125
configuration={"x0": 0.57, "x1": False, "x2": "small", "cp": 0.3, "fp": 0.5},
126126
objectives={objective_name: 0.64},
127127
)
128128
with pytest.raises(ValueError, match="not valid"):
129-
to_numerical([eval_err], search_space, objective)
129+
to_numerical([eval_err], search_space, [objective])
130130

131131

132132
@pytest.mark.parametrize(
@@ -147,7 +147,7 @@ def test_to_numerical_with_constraints(
147147
_, Y = to_numerical(
148148
evaluations_with_constraints,
149149
search_space,
150-
objective,
150+
[objective],
151151
constraints,
152152
)
153153

@@ -172,7 +172,7 @@ def test_to_numerical_raises_error_on_wrong_constraints(
172172
to_numerical(
173173
evaluations_with_constraints,
174174
search_space,
175-
objective,
175+
[objective],
176176
constraint_names=["WRONG_NAME"],
177177
)
178178

@@ -190,11 +190,32 @@ def test_to_numerical_raises_error_on_wrong_constraints(
190190
to_numerical(
191191
evaluations,
192192
search_space,
193-
objective,
193+
[objective],
194194
constraint_names=[constraint_name_1],
195195
)
196196

197197

198+
def test_to_numerical_multiple_objectives(search_space):
199+
objectives = [
200+
Objective("score", greater_is_better=True),
201+
Objective("loss", greater_is_better=False),
202+
]
203+
204+
evaluations = [
205+
Evaluation(
206+
objectives={"score": 0.1, "loss": 0.1}, configuration=search_space.sample()
207+
)
208+
]
209+
210+
X, Y = to_numerical(evaluations, search_space, objectives)
211+
212+
assert X.dtype == torch.float32
213+
assert Y.dtype == torch.float32
214+
assert X.size() == (len(evaluations), len(search_space))
215+
assert Y.size() == (len(evaluations), len(objectives))
216+
assert torch.equal(Y, torch.Tensor([[-0.1, 0.1]]))
217+
218+
198219
def test_filter_y_nans():
199220
x1 = torch.tensor([[0.1], [0.7], [1.0]])
200221
y1 = torch.tensor([[0.8], [0.3], [0.5]])

0 commit comments

Comments
 (0)