@@ -228,21 +228,19 @@ def forward(self, X: Tensor, num_samples: int = 1) -> Tensor:
228
228
229
229
230
230
class ConstrainedMaxPosteriorSampling (MaxPosteriorSampling ):
231
- r"""Sample from a set of points according to
232
- their max posterior value,
233
- which also likely meet a set of constraints
234
- c1(x) <= 0, c2(x) <= 0, ..., cm(x) <= 0
235
- c1, c2, ..., cm are black-box constraint functions
236
- Each constraint function is modeled by a seperate
237
- surrogate GP constraint model
238
- We sample points for which the posterior value
239
- for each constraint model <= 0,
240
- as described in https://doi.org/10.48550/arxiv.2002.08526
231
+ r"""Constrained max posterior sampling.
232
+
233
+ Posterior sampling where we try to maximize an objective function while
234
+ simulatenously satisfying a set of constraints c1(x) <= 0, c2(x) <= 0,
235
+ ..., cm(x) <= 0 where c1, c2, ..., cm are black-box constraint functions.
236
+ Each constraint function is modeled by a seperate GP model. We follow the
237
+ procedure as described in https://doi.org/10.48550/arxiv.2002.08526.
241
238
242
239
Example:
243
- >>> CMPS = ConstrainedMaxPosteriorSampling(model,
244
- constraint_model=ModelListGP(cmodel1, cmodel2,
245
- ..., cmodelm) # models w/ feature dim d=3
240
+ >>> CMPS = ConstrainedMaxPosteriorSampling(
241
+ model,
242
+ constraint_model=ModelListGP(cmodel1, cmodel2),
243
+ )
246
244
>>> X = torch.rand(2, 100, 3)
247
245
>>> sampled_X = CMPS(X, num_samples=5)
248
246
"""
@@ -254,82 +252,101 @@ def __init__(
254
252
objective : Optional [MCAcquisitionObjective ] = None ,
255
253
posterior_transform : Optional [PosteriorTransform ] = None ,
256
254
replacement : bool = True ,
257
- minimize_constraints_only : bool = False ,
258
255
) -> None :
259
256
r"""Constructor for the SamplingStrategy base class.
260
257
261
258
Args:
262
259
model: A fitted model.
263
- objective: The MCAcquisitionObjective under
264
- which the samples are evaluated.
260
+ objective: The MCAcquisitionObjective under which the samples are evaluated.
265
261
Defaults to `IdentityMCObjective()`.
266
- posterior_transform: An optional PosteriorTransform.
262
+ posterior_transform: An optional PosteriorTransform for the objective
263
+ function (corresponding to `model`).
267
264
replacement: If True, sample with replacement.
268
- constraint_model: either a ModelListGP where each submodel
269
- is a GP model for one constraint function,
270
- or a MultiTaskGP model where each task is one
271
- constraint function
272
- All constraints are of the form c(x) <= 0.
273
- In the case when the constraint model predicts
274
- that all candidates violate constraints,
275
- we pick the candidates with minimum violation.
276
- minimize_constraints_only: False by default, if true,
277
- we will automatically return the candidates
278
- with minimum posterior constraint values,
279
- (minimum predicted c(x) summed over all constraints)
280
- reguardless of predicted objective values.
265
+ constraint_model: either a ModelListGP where each submodel is a GP model for
266
+ one constraint function, or a MultiTaskGP model where each task is one
267
+ constraint function. All constraints are of the form c(x) <= 0. In the
268
+ case when the constraint model predicts that all candidates
269
+ violate constraints, we pick the candidates with minimum violation.
281
270
"""
271
+ if objective is not None :
272
+ raise NotImplementedError (
273
+ "`objective` is not supported for `ConstrainedMaxPosteriorSampling`."
274
+ )
275
+
282
276
super ().__init__ (
283
277
model = model ,
284
278
objective = objective ,
285
279
posterior_transform = posterior_transform ,
286
280
replacement = replacement ,
287
281
)
288
282
self .constraint_model = constraint_model
289
- self .minimize_constraints_only = minimize_constraints_only
283
+
284
+ def _convert_samples_to_scores (self , Y_samples , C_samples ) -> Tensor :
285
+ r"""Convert the objective and constraint samples into a score.
286
+
287
+ The logic is as follows:
288
+ - If a realization has at least one feasible candidate we use the objective
289
+ value as the score and set all infeasible candidates to -inf.
290
+ - If a realization doesn't have a feasible candidate we set the score to
291
+ the negative total violation of the constraints to incentivize choosing
292
+ the candidate with the smallest constraint violation.
293
+
294
+ Args:
295
+ Y_samples: A `num_samples x batch_shape x num_cand x 1`-dim Tensor of
296
+ samples from the objective function.
297
+ C_samples: A `num_samples x batch_shape x num_cand x num_constraints`-dim
298
+ Tensor of samples from the constraints.
299
+
300
+ Returns:
301
+ A `num_samples x batch_shape x num_cand x 1`-dim Tensor of scores.
302
+ """
303
+ is_feasible = (C_samples <= 0 ).all (
304
+ dim = - 1
305
+ ) # num_samples x batch_shape x num_cand
306
+ has_feasible_candidate = is_feasible .any (dim = - 1 )
307
+
308
+ scores = Y_samples .clone ()
309
+ scores [~ is_feasible ] = - float ("inf" )
310
+ if not has_feasible_candidate .all ():
311
+ # Use negative total violation for samples where no candidate is feasible
312
+ total_violation = (
313
+ C_samples [~ has_feasible_candidate ]
314
+ .clamp (min = 0 )
315
+ .sum (dim = - 1 , keepdim = True )
316
+ )
317
+ scores [~ has_feasible_candidate ] = - total_violation
318
+ return scores
290
319
291
320
def forward (
292
321
self , X : Tensor , num_samples : int = 1 , observation_noise : bool = False
293
322
) -> Tensor :
294
323
r"""Sample from the model posterior.
295
324
296
325
Args:
297
- X: A `batch_shape x N x d`-dim Tensor
298
- from which to sample (in the `N`
299
- dimension) according to the maximum
300
- posterior value under the objective.
326
+ X: A `batch_shape x N x d`-dim Tensor from which to sample (in the `N`
327
+ dimension) according to the maximum posterior value under the objective.
301
328
num_samples: The number of samples to draw.
302
329
observation_noise: If True, sample with observation noise.
303
330
304
331
Returns:
305
- A `batch_shape x num_samples x d`-dim
306
- Tensor of samples from `X`, where
307
- `X[..., i, :]` is the `i`-th sample.
332
+ A `batch_shape x num_samples x d`-dim Tensor of samples from `X`, where
333
+ `X[..., i, :]` is the `i`-th sample.
308
334
"""
309
- posterior = self .model .posterior (X , observation_noise = observation_noise )
310
- samples = posterior .rsample (sample_shape = torch .Size ([num_samples ]))
335
+ posterior = self .model .posterior (
336
+ X = X ,
337
+ observation_noise = observation_noise ,
338
+ # Note: `posterior_transform` is only used for the objective
339
+ posterior_transform = self .posterior_transform ,
340
+ )
341
+ Y_samples = posterior .rsample (sample_shape = torch .Size ([num_samples ]))
311
342
312
343
c_posterior = self .constraint_model .posterior (
313
- X , observation_noise = observation_noise
314
- )
315
- constraint_samples = c_posterior .rsample (sample_shape = torch .Size ([num_samples ]))
316
- valid_samples = constraint_samples <= 0
317
- if valid_samples .shape [- 1 ] > 1 : # if more than one constraint
318
- valid_samples = torch .all (valid_samples , dim = - 1 ).unsqueeze (- 1 )
319
- if (valid_samples .sum () == 0 ) or self .minimize_constraints_only :
320
- # if none of the samples meet the constraints
321
- # we pick the one that minimizes total violation
322
- constraint_samples = constraint_samples .sum (dim = - 1 )
323
- idcs = torch .argmin (constraint_samples , dim = - 1 )
324
- if idcs .ndim > 1 :
325
- idcs = idcs .permute (* range (1 , idcs .ndim ), 0 )
326
- idcs = idcs .unsqueeze (- 1 ).expand (* idcs .shape , X .size (- 1 ))
327
- Xe = X .expand (* constraint_samples .shape [1 :], X .size (- 1 ))
328
- return torch .gather (Xe , - 2 , idcs )
329
- # replace all violators with -infinty so it will never choose them
330
- replacement_infs = - torch .inf * torch .ones (samples .shape ).to (X .device ).to (
331
- X .dtype
344
+ X = X , observation_noise = observation_noise
332
345
)
333
- samples = torch .where ( valid_samples , samples , replacement_infs )
346
+ C_samples = c_posterior . rsample ( sample_shape = torch .Size ([ num_samples ]) )
334
347
335
- return self .maximize_samples (X , samples , num_samples )
348
+ # Convert the objective and constraint samples into a scalar-valued "score"
349
+ scores = self ._convert_samples_to_scores (
350
+ Y_samples = Y_samples , C_samples = C_samples
351
+ )
352
+ return self .maximize_samples (X = X , samples = scores , num_samples = num_samples )
0 commit comments