Skip to content

Commit e20e2f3

Browse files
saitcakmakfacebook-github-bot
authored andcommitted
Implement qLogNParEGO (#2364)
Summary: Adds an implementation of qLogNParEGO that is compatible with Ax MBM. This constructs the Chebyshev scalarization before deferring to qLogNEI for remaining computations. The construction of the Chebyshev objective mirrors what was done in `_get_acqusition_func` for the legacy Ax model. Reviewed By: SebastianAment Differential Revision: D58122015
1 parent 5fbbf0e commit e20e2f3

File tree

5 files changed

+374
-0
lines changed

5 files changed

+374
-0
lines changed

botorch/acquisition/input_constructors.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
qLogNoisyExpectedHypervolumeImprovement,
7777
)
7878
from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective
79+
from botorch.acquisition.multi_objective.parego import qLogNParEGO
7980
from botorch.acquisition.multi_objective.utils import get_default_partitioning_alpha
8081
from botorch.acquisition.objective import (
8182
ConstrainedMCObjective,
@@ -1115,6 +1116,84 @@ def construct_inputs_qLogNEHVI(
11151116
}
11161117

11171118

1119+
@acqf_input_constructor(qLogNParEGO)
1120+
def construct_inputs_qLogNParEGO(
1121+
model: Model,
1122+
training_data: MaybeDict[SupervisedDataset],
1123+
scalarization_weights: Optional[Tensor] = None,
1124+
objective: Optional[MCMultiOutputObjective] = None,
1125+
X_pending: Optional[Tensor] = None,
1126+
sampler: Optional[MCSampler] = None,
1127+
X_baseline: Optional[Tensor] = None,
1128+
prune_baseline: Optional[bool] = True,
1129+
cache_root: Optional[bool] = True,
1130+
constraints: Optional[List[Callable[[Tensor], Tensor]]] = None,
1131+
eta: Union[Tensor, float] = 1e-3,
1132+
fat: bool = True,
1133+
tau_max: float = TAU_MAX,
1134+
tau_relu: float = TAU_RELU,
1135+
):
1136+
r"""Construct kwargs for the `qLogNoisyExpectedImprovement` constructor.
1137+
1138+
Args:
1139+
model: The model to be used in the acquisition function.
1140+
training_data: Dataset(s) used to train the model.
1141+
scalarization_weights: A `m`-dim Tensor of weights to be used in the
1142+
Chebyshev scalarization. If omitted, samples from the unit simplex.
1143+
objective: The MultiOutputMCAcquisitionObjective under which the samples are
1144+
evaluated before applying Chebyshev scalarization.
1145+
Defaults to `IdentityMultiOutputObjective()`.
1146+
X_pending: A `m x d`-dim Tensor of `m` design points that have been
1147+
submitted for function evaluation but have not yet been evaluated.
1148+
Concatenated into X upon forward call.
1149+
sampler: The sampler used to draw base samples. If omitted, uses
1150+
the acquisition functions's default sampler.
1151+
X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points
1152+
that have already been observed. These points are considered as
1153+
the potential best design point. If omitted, checks that all
1154+
training_data have the same input features and take the first `X`.
1155+
prune_baseline: If True, remove points in `X_baseline` that are
1156+
highly unlikely to be the best point. This can significantly
1157+
improve performance and is generally recommended.
1158+
constraints: A list of constraint callables which map a Tensor of posterior
1159+
samples of dimension `sample_shape x batch-shape x q x m`-dim to a
1160+
`sample_shape x batch-shape x q`-dim Tensor. The associated constraints
1161+
are considered satisfied if the output is less than zero.
1162+
eta: Temperature parameter(s) governing the smoothness of the sigmoid
1163+
approximation to the constraint indicators. For more details, on this
1164+
parameter, see the docs of `compute_smoothed_feasibility_indicator`.
1165+
fat: Toggles the use of the fat-tailed non-linearities to smoothly approximate
1166+
the constraints indicator function.
1167+
tau_max: Temperature parameter controlling the sharpness of the smooth
1168+
approximations to max.
1169+
tau_relu: Temperature parameter controlling the sharpness of the smooth
1170+
approximations to ReLU.
1171+
1172+
Returns:
1173+
A dict mapping kwarg names of the constructor to values.
1174+
"""
1175+
base_inputs = construct_inputs_qLogNEI(
1176+
model=model,
1177+
training_data=training_data,
1178+
objective=objective,
1179+
X_pending=X_pending,
1180+
sampler=sampler,
1181+
X_baseline=X_baseline,
1182+
prune_baseline=prune_baseline,
1183+
cache_root=cache_root,
1184+
constraints=constraints,
1185+
eta=eta,
1186+
fat=fat,
1187+
tau_max=tau_max,
1188+
tau_relu=tau_relu,
1189+
)
1190+
base_inputs.pop("posterior_transform", None)
1191+
return {
1192+
**base_inputs,
1193+
"scalarization_weights": scalarization_weights,
1194+
}
1195+
1196+
11181197
@acqf_input_constructor(qMaxValueEntropy)
11191198
def construct_inputs_qMES(
11201199
model: Model,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from typing import Callable, List, Optional, Union
7+
8+
import torch
9+
from botorch.acquisition.logei import qLogNoisyExpectedImprovement, TAU_MAX, TAU_RELU
10+
from botorch.acquisition.multi_objective.monte_carlo import (
11+
MultiObjectiveMCAcquisitionFunction,
12+
)
13+
from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
14+
from botorch.acquisition.objective import GenericMCObjective
15+
from botorch.models.model import Model
16+
from botorch.posteriors.fully_bayesian import MCMC_DIM
17+
from botorch.sampling.base import MCSampler
18+
from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization
19+
from botorch.utils.sampling import sample_simplex
20+
from botorch.utils.transforms import is_ensemble
21+
from torch import Tensor
22+
23+
24+
class qLogNParEGO(qLogNoisyExpectedImprovement, MultiObjectiveMCAcquisitionFunction):
25+
def __init__(
26+
self,
27+
model: Model,
28+
X_baseline: Tensor,
29+
scalarization_weights: Optional[Tensor] = None,
30+
sampler: Optional[MCSampler] = None,
31+
objective: Optional[MCMultiOutputObjective] = None,
32+
constraints: Optional[List[Callable[[Tensor], Tensor]]] = None,
33+
X_pending: Optional[Tensor] = None,
34+
eta: Union[Tensor, float] = 1e-3,
35+
fat: bool = True,
36+
prune_baseline: bool = False,
37+
cache_root: bool = True,
38+
tau_relu: float = TAU_RELU,
39+
tau_max: float = TAU_MAX,
40+
) -> None:
41+
r"""q-LogNParEGO supporting m >= 2 outcomes. This acquisition function
42+
utilizes qLogNEI to compute the expected improvement over Chebyshev
43+
scalarization of the objectives.
44+
45+
This is adapted from qNParEGO proposed in [Daulton2020qehvi]_ to utilize
46+
log-improvement acquisition functions of [Ament2023logei]_. See [Knowles2005]_
47+
for the original ParEGO algorithm.
48+
49+
This implementation assumes maximization of all objectives. If any of the model
50+
outputs are to be minimized, either an `objective` should be used to negate the
51+
model outputs or the `scalarization_weights` should be provided with negative
52+
weights for the outputs to be minimized.
53+
54+
Args:
55+
model: A fitted multi-output model, producing outputs for `m` objectives
56+
and any number of outcome constraints.
57+
NOTE: The model posterior must have a `mean` attribute.
58+
X_baseline: A `batch_shape x r x d`-dim Tensor of `r` design points
59+
that have already been observed. These points are considered as
60+
the potential best design point.
61+
scalarization_weights: A `m`-dim Tensor of weights to be used in the
62+
Chebyshev scalarization. If omitted, samples from the unit simplex.
63+
sampler: The sampler used to draw base samples. See `MCAcquisitionFunction`
64+
more details.
65+
objective: The MultiOutputMCAcquisitionObjective under which the samples are
66+
evaluated before applying Chebyshev scalarization.
67+
Defaults to `IdentityMultiOutputObjective()`.
68+
constraints: A list of constraint callables which map a Tensor of posterior
69+
samples of dimension `sample_shape x batch-shape x q x m'`-dim to a
70+
`sample_shape x batch-shape x q`-dim Tensor. The associated constraints
71+
are satisfied if `constraint(samples) < 0`.
72+
X_pending: A `batch_shape x q' x d`-dim Tensor of `q'` design points
73+
that have points that have been submitted for function evaluation
74+
but have not yet been evaluated. Concatenated into `X` upon
75+
forward call. Copied and set to have no gradient.
76+
eta: Temperature parameter(s) governing the smoothness of the sigmoid
77+
approximation to the constraint indicators. See the docs of
78+
`compute_(log_)smoothed_constraint_indicator` for details.
79+
fat: Toggles the logarithmic / linear asymptotic behavior of the smooth
80+
approximation to the ReLU.
81+
prune_baseline: If True, remove points in `X_baseline` that are
82+
highly unlikely to be the best point. This can significantly
83+
improve performance and is generally recommended. In order to
84+
customize pruning parameters, instead manually call
85+
`botorch.acquisition.utils.prune_inferior_points` on `X_baseline`
86+
before instantiating the acquisition function.
87+
cache_root: A boolean indicating whether to cache the root
88+
decomposition over `X_baseline` and use low-rank updates.
89+
tau_max: Temperature parameter controlling the sharpness of the smooth
90+
approximations to max.
91+
tau_relu: Temperature parameter controlling the sharpness of the smooth
92+
approximations to ReLU.
93+
"""
94+
MultiObjectiveMCAcquisitionFunction.__init__(
95+
self,
96+
model=model,
97+
sampler=sampler,
98+
objective=objective,
99+
constraints=constraints,
100+
eta=eta,
101+
)
102+
org_objective = self.objective
103+
# Create the composite objective.
104+
with torch.no_grad():
105+
Y_baseline = org_objective(model.posterior(X_baseline).mean)
106+
if is_ensemble(model):
107+
Y_baseline = torch.mean(Y_baseline, dim=MCMC_DIM)
108+
scalarization_weights = (
109+
scalarization_weights
110+
if scalarization_weights is not None
111+
else sample_simplex(
112+
d=Y_baseline.shape[-1], device=X_baseline.device, dtype=X_baseline.dtype
113+
).view(-1)
114+
)
115+
chebyshev_scalarization = get_chebyshev_scalarization(
116+
weights=scalarization_weights,
117+
Y=Y_baseline,
118+
)
119+
composite_objective = GenericMCObjective(
120+
objective=lambda samples, X=None: chebyshev_scalarization(
121+
org_objective(samples=samples, X=X), X=X
122+
),
123+
)
124+
qLogNoisyExpectedImprovement.__init__(
125+
self,
126+
model=model,
127+
X_baseline=X_baseline,
128+
sampler=sampler,
129+
# This overwrites self.objective with the composite objective.
130+
objective=composite_objective,
131+
X_pending=X_pending,
132+
constraints=constraints,
133+
eta=eta,
134+
fat=fat,
135+
prune_baseline=prune_baseline,
136+
cache_root=cache_root,
137+
tau_max=tau_max,
138+
tau_relu=tau_relu,
139+
)
140+
# Set these after __init__ calls so that they're not overwritten / deleted.
141+
# These are intended mainly for easier debugging & transparency.
142+
self._org_objective: MCMultiOutputObjective = org_objective
143+
self.chebyshev_scalarization: Callable[[Tensor, Optional[Tensor]], Tensor] = (
144+
chebyshev_scalarization
145+
)
146+
self.scalarization_weights: Tensor = scalarization_weights
147+
self.Y_baseline: Tensor = Y_baseline

sphinx/source/acquisition.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ Multi-Objective Predictive Entropy Search Acquisition Functions
108108
.. automodule:: botorch.acquisition.multi_objective.predictive_entropy_search
109109
:members:
110110

111+
ParEGO: Multi-Objective Acquisition Function with Chebyshev Scalarization
112+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
113+
.. automodule:: botorch.acquisition.multi_objective.parego
114+
:members:
115+
111116
The One-Shot Knowledge Gradient
112117
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
113118
.. automodule:: botorch.acquisition.knowledge_gradient
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from typing import Any, Dict, Optional
7+
8+
import torch
9+
from botorch.acquisition.logei import qLogNoisyExpectedImprovement
10+
from botorch.acquisition.multi_objective.objective import (
11+
IdentityMCMultiOutputObjective,
12+
WeightedMCMultiOutputObjective,
13+
)
14+
from botorch.acquisition.multi_objective.parego import qLogNParEGO
15+
from botorch.models.fully_bayesian import SaasFullyBayesianSingleTaskGP
16+
from botorch.models.gp_regression import SingleTaskGP
17+
from botorch.models.model import Model
18+
from botorch.models.model_list_gp_regression import ModelListGP
19+
from botorch.utils.testing import BotorchTestCase
20+
21+
22+
class TestqLogNParEGO(BotorchTestCase):
23+
def base_test_parego(
24+
self,
25+
with_constraints: bool = False,
26+
with_scalarization_weights: bool = False,
27+
with_objective: bool = False,
28+
model: Optional[Model] = None,
29+
) -> None:
30+
if with_constraints:
31+
assert with_objective, "Objective must be specified if constraints are."
32+
tkwargs: Dict[str, Any] = {"device": self.device, "dtype": torch.double}
33+
num_objectives = 2
34+
num_constraints = 1 if with_constraints else 0
35+
num_outputs = num_objectives + num_constraints
36+
model = model or SingleTaskGP(
37+
train_X=torch.rand(5, 2, **tkwargs),
38+
train_Y=torch.rand(5, num_outputs, **tkwargs),
39+
)
40+
scalarization_weights = (
41+
torch.rand(num_objectives, **tkwargs)
42+
if with_scalarization_weights
43+
else None
44+
)
45+
objective = (
46+
WeightedMCMultiOutputObjective(
47+
weights=torch.tensor([2.0, -0.5], **tkwargs), outcomes=[0, 1]
48+
)
49+
if with_objective
50+
else None
51+
)
52+
constraints = [lambda samples: samples[..., -1]] if with_constraints else None
53+
acqf = qLogNParEGO(
54+
model=model,
55+
X_baseline=torch.rand(3, 2, **tkwargs),
56+
scalarization_weights=scalarization_weights,
57+
objective=objective,
58+
constraints=constraints,
59+
prune_baseline=True,
60+
)
61+
self.assertEqual(acqf.Y_baseline.shape, torch.Size([3, 2]))
62+
# Scalarization weights should be set if given and sampled otherwise.
63+
if scalarization_weights is not None:
64+
self.assertIs(acqf.scalarization_weights, scalarization_weights)
65+
else:
66+
self.assertEqual(
67+
acqf.scalarization_weights.shape, torch.Size([num_objectives])
68+
)
69+
# Should sum to 1 since they're sampled from simplex.
70+
self.assertAlmostEqual(acqf.scalarization_weights.sum().item(), 1.0)
71+
# Original objective should default to identity.
72+
if with_objective:
73+
self.assertIs(acqf._org_objective, objective)
74+
else:
75+
self.assertIsInstance(acqf._org_objective, IdentityMCMultiOutputObjective)
76+
# Acqf objective should be the chebyshev scalarization compounded
77+
# with the original objective.
78+
test_samples = torch.rand(32, 5, num_outputs, **tkwargs)
79+
expected_objective = acqf.chebyshev_scalarization(
80+
acqf._org_objective(test_samples)
81+
)
82+
self.assertEqual(expected_objective.shape, torch.Size([32, 5]))
83+
self.assertAllClose(acqf.objective(test_samples), expected_objective)
84+
# Evaluate the acquisition function.
85+
self.assertEqual(acqf(torch.rand(5, 2, **tkwargs)).shape, torch.Size([1]))
86+
test_X = torch.rand(32, 5, 2, **tkwargs)
87+
acqf_val = acqf(test_X)
88+
self.assertEqual(acqf_val.shape, torch.Size([32]))
89+
# Check that we're indeed using qLogNEI.
90+
self.assertIs(
91+
acqf.forward.__code__, qLogNoisyExpectedImprovement.forward.__code__
92+
)
93+
self.assertAllClose(
94+
acqf_val, qLogNoisyExpectedImprovement.forward(acqf, X=test_X)
95+
)
96+
97+
def test_parego_simple(self) -> None:
98+
self.base_test_parego()
99+
100+
def test_parego_with_constraints_objective_weights(self) -> None:
101+
self.base_test_parego(
102+
with_constraints=True, with_objective=True, with_scalarization_weights=True
103+
)
104+
105+
def test_parego_with_ensemble_model(self) -> None:
106+
tkwargs: Dict[str, Any] = {"device": self.device, "dtype": torch.double}
107+
models = []
108+
for _ in range(2):
109+
model = SaasFullyBayesianSingleTaskGP(
110+
train_X=torch.rand(5, 2, **tkwargs),
111+
train_Y=torch.randn(5, 1, **tkwargs),
112+
train_Yvar=torch.rand(5, 1, **tkwargs) * 0.05,
113+
)
114+
mcmc_samples = {
115+
"lengthscale": torch.rand(4, 1, 2, **tkwargs),
116+
"outputscale": torch.rand(4, **tkwargs),
117+
"mean": torch.randn(4, **tkwargs),
118+
}
119+
model.load_mcmc_samples(mcmc_samples)
120+
models.append(model)
121+
self.base_test_parego(model=ModelListGP(*models))

0 commit comments

Comments
 (0)