Skip to content

Commit b1097c6

Browse files
Ethan Chefacebook-github-bot
authored andcommitted
log values for HVKG using LogEHVI (#2974)
Summary: Pull Request resolved: #2974 HKVG to output log values when _log=True using the LogEHVI value function. Also adds a _log flag to InverseCostWeightedUtility to output utilities in log-space. if _log=True, it assumes that * current_value is in log-space * cost_aware_utility outputs in log-space (raises an error if cost_aware_utility does not have a _log flag or if its _log=False). Note that InverseCostWeightedUtility does a logarithmic transform for the inputted costs; assumes that inputted costs are in the original space. This is so that one does not need to make any direct modification to the cost fn, and because InverseCostWeightedUtility already does some pre-processing to the costs (e.g. clipping). tldr: HKVG assumes all of its inputs are logged, but InverseCostWeightedUtility does not. Reviewed By: SebastianAment Differential Revision: D80263869 fbshipit-source-id: 95c29aac348a5541bf79de3a27208896936e424b
1 parent 003f13c commit b1097c6

File tree

4 files changed

+279
-120
lines changed

4 files changed

+279
-120
lines changed

botorch/acquisition/cost_aware.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from __future__ import annotations
1313

14-
import warnings
1514
from abc import ABC, abstractmethod
1615
from collections.abc import Callable
1716

@@ -21,7 +20,6 @@
2120
IdentityMCObjective,
2221
MCAcquisitionObjective,
2322
)
24-
from botorch.exceptions.warnings import CostAwareWarning
2523
from botorch.models.deterministic import DeterministicModel
2624
from botorch.models.gpytorch import GPyTorchModel
2725
from botorch.sampling.base import MCSampler
@@ -112,7 +110,7 @@ def __init__(
112110
cost_model: DeterministicModel | GPyTorchModel,
113111
use_mean: bool = True,
114112
cost_objective: MCAcquisitionObjective | None = None,
115-
min_cost: float = 1e-2,
113+
log: bool = False,
116114
) -> None:
117115
r"""Cost-aware utility that weights increase in utility by inverse cost.
118116
For negative increases in utility, the utility is instead scaled by the
@@ -130,7 +128,8 @@ def __init__(
130128
un-transform predictions/samples of a cost model fit on the
131129
log-transformed cost (often done to ensure non-negativity). If the
132130
cost model is multi-output, then by default this will sum the cost
133-
across outputs.
131+
across outputs. NOTE: Keep in mind that cost_objective must output
132+
strictly positive values; forward will raise a ValueError otherwise.
134133
min_cost: A value used to clamp the cost samples so that they are not
135134
too close to zero, which may cause numerical issues.
136135
Returns:
@@ -147,7 +146,7 @@ def __init__(
147146
self.cost_model = cost_model
148147
self.cost_objective: MCAcquisitionObjective = cost_objective
149148
self._use_mean = use_mean
150-
self._min_cost = min_cost
149+
self._log = log
151150

152151
def forward(
153152
self,
@@ -202,18 +201,21 @@ def forward(
202201
cost = none_throws(sampler)(cost_posterior)
203202
cost = self.cost_objective(cost)
204203

205-
# Ensure non-negativity of the cost
206-
if torch.any(cost < -1e-7):
207-
warnings.warn(
208-
"Encountered negative cost values in InverseCostWeightedUtility",
209-
CostAwareWarning,
210-
stacklevel=2,
204+
# Ensure that costs are positive
205+
if not torch.all(cost > 0.0):
206+
raise ValueError(
207+
"Costs must be strictly positive. Consider clamping cost_objective."
211208
)
212-
# clamp (away from zero) and sum cost across elements of the q-batch -
213-
# this will be of shape `num_fantasies x batch_shape` or `batch_shape`
214-
cost = cost.clamp_min(self._min_cost).sum(dim=-1)
209+
210+
# sum costs along q-batch
211+
cost = cost.sum(dim=-1)
215212

216213
# compute and return the ratio on the sample level - If `use_mean=True`
217214
# this operation involves broadcasting the cost across fantasies.
218-
# We multiply by the cost if the deltas are <= 0, see discussion #2914
219-
return torch.where(deltas > 0, deltas / cost, deltas * cost)
215+
if self._log:
216+
# if _log is True then input deltas are in log space
217+
# so original deltas cannot be <= 0
218+
return deltas - torch.log(cost)
219+
else:
220+
# We multiply by the cost if the deltas are <= 0, see discussion #2914
221+
return torch.where(deltas > 0, deltas / cost, deltas * cost)

botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@
3232
from botorch.acquisition.decoupled import DecoupledAcquisitionFunction
3333
from botorch.acquisition.knowledge_gradient import ProjectedAcquisitionFunction
3434
from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction
35+
from botorch.acquisition.multi_objective.logei import qLogExpectedHypervolumeImprovement
3536
from botorch.acquisition.multi_objective.monte_carlo import (
3637
qExpectedHypervolumeImprovement,
3738
)
3839
from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective
39-
from botorch.exceptions.errors import UnsupportedError
40+
from botorch.exceptions.errors import BotorchError, UnsupportedError
4041
from botorch.exceptions.warnings import NumericsWarning
4142
from botorch.models.deterministic import PosteriorMeanModel
4243
from botorch.models.model import Model
@@ -47,6 +48,7 @@
4748
from botorch.utils.multi_objective.box_decompositions.non_dominated import (
4849
FastNondominatedPartitioning,
4950
)
51+
from botorch.utils.safe_math import logdiffexp, logmeanexp
5052
from botorch.utils.transforms import (
5153
average_over_ensemble_models,
5254
match_batch_shape,
@@ -91,6 +93,7 @@ def __init__(
9193
current_value: Tensor | None = None,
9294
use_posterior_mean: bool = True,
9395
cost_aware_utility: CostAwareUtility | None = None,
96+
log: bool = False,
9497
) -> None:
9598
r"""q-Hypervolume Knowledge Gradient.
9699
@@ -133,6 +136,9 @@ def __init__(
133136
[Daulton2023hvkg]_ for details.
134137
cost_aware_utility: A CostAwareUtility specifying the cost function for
135138
evaluating the `X` on the objectives indicated by `evaluation_mask`.
139+
log: If True, then returns the log of the HVKG value. If True, then it
140+
expects current_value to be in log-space and cost_aware_utility to
141+
output log utilities.
136142
"""
137143
if sampler is None:
138144
# base samples should be fixed for joint optimization over X, X_fantasies
@@ -170,6 +176,8 @@ def __init__(
170176
self.cost_aware_utility = cost_aware_utility
171177
self._cost_sampler = None
172178

179+
self._log = log
180+
173181
@property
174182
def cost_sampler(self):
175183
if self._cost_sampler is None:
@@ -242,6 +250,7 @@ def forward(self, X: Tensor) -> Tensor:
242250
objective=self.objective,
243251
sampler=self.inner_sampler,
244252
use_posterior_mean=self.use_posterior_mean,
253+
log=self._log,
245254
)
246255

247256
# make sure to propagate gradients to the fantasy model train inputs
@@ -259,9 +268,23 @@ def forward(self, X: Tensor) -> Tensor:
259268
values = value_function(X=X_fantasies.reshape(shape)) # num_fantasies x b
260269

261270
if self.current_value is not None:
262-
values = values - self.current_value
271+
if self._log:
272+
values = logdiffexp(self.current_value, values)
273+
else:
274+
values = values - self.current_value
263275

264276
if self.cost_aware_utility is not None:
277+
if self._log:
278+
# check whether cost_aware_utility has a _log flag
279+
# raises an error if it does not or if _log is False
280+
if (
281+
not hasattr(self.cost_aware_utility, "_log")
282+
or not self.cost_aware_utility._log
283+
):
284+
raise BotorchError(
285+
"Cost-aware HVKG has _log=True and requires cost_aware_utility"
286+
"to output log utilities."
287+
)
265288
values = self.cost_aware_utility(
266289
# exclude pending points
267290
X=X_actual[..., :q, :],
@@ -271,7 +294,10 @@ def forward(self, X: Tensor) -> Tensor:
271294
)
272295

273296
# return average over the fantasy samples
274-
return values.mean(dim=0)
297+
if self._log:
298+
return logmeanexp(values, dim=0)
299+
else:
300+
return values.mean(dim=0)
275301

276302
def get_augmented_q_batch_size(self, q: int) -> int:
277303
r"""Get augmented q batch size for one-shot optimization.
@@ -329,6 +355,7 @@ def __init__(
329355
valfunc_cls: type[AcquisitionFunction] | None = None,
330356
valfunc_argfac: Callable[[Model], dict[str, Any]] | None = None,
331357
use_posterior_mean: bool = True,
358+
log: bool = False,
332359
**kwargs: Any,
333360
) -> None:
334361
r"""Multi-Fidelity q-Knowledge Gradient (one-shot optimization).
@@ -376,6 +403,9 @@ def __init__(
376403
valfunc_argfac: An argument factory, i.e. callable that maps a `Model`
377404
to a dictionary of kwargs for the terminal value function (e.g.
378405
`best_f` for `ExpectedImprovement`).
406+
log: If True, then returns the log of the HVKG value. If True, then it
407+
expects current_value to be in log-space and cost_aware_utility to
408+
output log utilities.
379409
"""
380410

381411
super().__init__(
@@ -392,6 +422,7 @@ def __init__(
392422
current_value=current_value,
393423
use_posterior_mean=use_posterior_mean,
394424
cost_aware_utility=cost_aware_utility,
425+
log=log,
395426
)
396427
self.project = project
397428
if kwargs.get("expand") is not None:
@@ -465,6 +496,7 @@ def forward(self, X: Tensor) -> Tensor:
465496
valfunc_cls=self.valfunc_cls,
466497
valfunc_argfac=self.valfunc_argfac,
467498
use_posterior_mean=self.use_posterior_mean,
499+
log=self._log,
468500
)
469501

470502
# make sure to propagate gradients to the fantasy model train inputs
@@ -481,9 +513,24 @@ def forward(self, X: Tensor) -> Tensor:
481513
)
482514
values = value_function(X=X_fantasies.reshape(shape)) # num_fantasies x b
483515
if self.current_value is not None:
484-
values = values - self.current_value
516+
if self._log:
517+
# Assumes current value is in log-space
518+
values = logdiffexp(self.current_value, values)
519+
else:
520+
values = values - self.current_value
485521

486522
if self.cost_aware_utility is not None:
523+
if self._log:
524+
# check whether cost_aware_utility has a _log flag
525+
# raises an error if it does not or if _log is False
526+
if (
527+
not hasattr(self.cost_aware_utility, "_log")
528+
or not self.cost_aware_utility._log
529+
):
530+
raise BotorchError(
531+
"Cost-aware HVKG has _log=True and requires cost_aware_utility"
532+
"to output log utilities."
533+
)
487534
values = self.cost_aware_utility(
488535
# exclude pending points
489536
X=X_actual[..., :q, :],
@@ -493,7 +540,7 @@ def forward(self, X: Tensor) -> Tensor:
493540
)
494541

495542
# return average over the fantasy samples
496-
return values.mean(dim=0)
543+
return logmeanexp(values, dim=0) if self._log else values.mean(dim=0)
497544

498545

499546
def _get_hv_value_function(
@@ -505,6 +552,7 @@ def _get_hv_value_function(
505552
valfunc_cls: type[AcquisitionFunction] | None = None,
506553
valfunc_argfac: Callable[[Model], dict[str, Any]] | None = None,
507554
use_posterior_mean: bool = False,
555+
log: bool = False,
508556
) -> AcquisitionFunction:
509557
r"""Construct value function (i.e. inner acquisition function).
510558
This is a method for computing hypervolume.
@@ -518,7 +566,13 @@ def _get_hv_value_function(
518566
action="ignore",
519567
category=NumericsWarning,
520568
)
521-
base_value_function = qExpectedHypervolumeImprovement(
569+
570+
base_value_function_class = (
571+
qLogExpectedHypervolumeImprovement
572+
if log
573+
else qExpectedHypervolumeImprovement
574+
)
575+
base_value_function = base_value_function_class(
522576
model=model,
523577
ref_point=ref_point,
524578
partitioning=FastNondominatedPartitioning(

0 commit comments

Comments
 (0)