From 302509d648c41239245fe98669df6c06d299fa24 Mon Sep 17 00:00:00 2001 From: Cristian Lara Date: Fri, 23 Aug 2024 12:50:32 -0700 Subject: [PATCH 1/3] Use mathjax svg renderer (#2481) Summary: We identified an issue where mathjax would fail to render certain math symbols. This happens on MacOS versions Ventura and later (13+) because of a change in the OS-supplied STIX fonts. We fix this here by changing the renderer from HTML+CSS (which tries to use local STIX fonts) to SVG. More details on the issue can be found here: https://meta.stackexchange.com/a/389308 | **Before** | image | | -------- | ------- | | **After** | image | ### Considerations The [official docs](https://docs.mathjax.org/en/stable/output.html#mathjax-output-formats) outline a couple considerations for using the SVG renderer, the biggest being lack of IE8 support. If IE8 support is crucial then we should consider alternate approaches. An alternative fix could be to upgrade our mathjax version as later versions may not be as reliant on local STIX fonts but the 3.0 update was a complete rewrite and appears to [not be a straightforward upgrade](https://docs.mathjax.org/en/latest/upgrading/v2.html). Pull Request resolved: https://github.com/pytorch/botorch/pull/2481 Reviewed By: Balandat Differential Revision: D61681247 Pulled By: CristianLara fbshipit-source-id: 8842997e140722d8e3577b734e6d42cf71caac03 --- website/siteConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/siteConfig.js b/website/siteConfig.js index a0472fa1c6..8329137916 100644 --- a/website/siteConfig.js +++ b/website/siteConfig.js @@ -74,7 +74,7 @@ const siteConfig = { 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js', // Mathjax for rendering math content `${baseUrl}js/mathjax.js`, - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js?config=TeX-AMS_HTML', + 'https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS_SVG', ], // CSS sources to load From 23ae29f5f81bb7cfe7ebdaa05cb14c9e581f567b Mon Sep 17 00:00:00 2001 From: Sait Cakmak Date: Fri, 23 Aug 2024 13:26:42 -0700 Subject: [PATCH 2/3] Rework botorch/acquisition/multi_objective directory structure (#2485) Summary: Pull Request resolved: https://github.com/pytorch/botorch/pull/2485 Moves the base classes into their own file to avoid circular imports. Previosly, we couldn't import the base class and the log acqfs in the same file without introducing a circular import. Also fixed typing of `eta`, as it cannot be `None`. Reviewed By: esantorella Differential Revision: D61721280 fbshipit-source-id: 359c179a3225cc642cd5489f3968e61063f53f19 --- .../acquisition/multi_objective/__init__.py | 26 +-- .../acquisition/multi_objective/analytic.py | 45 +----- botorch/acquisition/multi_objective/base.py | 153 ++++++++++++++++++ .../hypervolume_knowledge_gradient.py | 2 +- .../multi_objective/joint_entropy_search.py | 1 - botorch/acquisition/multi_objective/logei.py | 6 +- .../max_value_entropy_search.py | 5 +- .../multi_objective/monte_carlo.py | 100 +----------- .../multi_objective/multi_fidelity.py | 2 +- botorch/acquisition/multi_objective/parego.py | 4 +- docs/multi_objective.md | 119 +++++++------- sphinx/source/acquisition.rst | 15 +- .../multi_objective/test_analytic.py | 53 +----- test/acquisition/multi_objective/test_base.py | 111 +++++++++++++ .../acquisition/multi_objective/test_logei.py | 4 +- .../multi_objective/test_monte_carlo.py | 57 +------ 16 files changed, 368 insertions(+), 335 deletions(-) create mode 100644 botorch/acquisition/multi_objective/base.py create mode 100644 test/acquisition/multi_objective/test_base.py diff --git a/botorch/acquisition/multi_objective/__init__.py b/botorch/acquisition/multi_objective/__init__.py index 8359ac5058..3211bfe872 100644 --- a/botorch/acquisition/multi_objective/__init__.py +++ b/botorch/acquisition/multi_objective/__init__.py @@ -4,19 +4,23 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -from botorch.acquisition.multi_objective.analytic import ( - ExpectedHypervolumeImprovement, +from botorch.acquisition.multi_objective.analytic import ExpectedHypervolumeImprovement +from botorch.acquisition.multi_objective.base import ( MultiObjectiveAnalyticAcquisitionFunction, + MultiObjectiveMCAcquisitionFunction, ) from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import ( qHypervolumeKnowledgeGradient, qMultiFidelityHypervolumeKnowledgeGradient, ) +from botorch.acquisition.multi_objective.logei import ( + qLogExpectedHypervolumeImprovement, + qLogNoisyExpectedHypervolumeImprovement, +) from botorch.acquisition.multi_objective.max_value_entropy_search import ( qMultiObjectiveMaxValueEntropy, ) from botorch.acquisition.multi_objective.monte_carlo import ( - MultiObjectiveMCAcquisitionFunction, qExpectedHypervolumeImprovement, qNoisyExpectedHypervolumeImprovement, ) @@ -33,18 +37,20 @@ __all__ = [ + "ExpectedHypervolumeImprovement", "get_default_partitioning_alpha", + "IdentityMCMultiOutputObjective", + "MCMultiOutputObjective", + "MOMF", + "MultiObjectiveAnalyticAcquisitionFunction", + "MultiObjectiveMCAcquisitionFunction", "prune_inferior_points_multi_objective", "qExpectedHypervolumeImprovement", "qHypervolumeKnowledgeGradient", + "qLogExpectedHypervolumeImprovement", + "qLogNoisyExpectedHypervolumeImprovement", "qMultiFidelityHypervolumeKnowledgeGradient", - "qNoisyExpectedHypervolumeImprovement", - "MOMF", "qMultiObjectiveMaxValueEntropy", - "ExpectedHypervolumeImprovement", - "IdentityMCMultiOutputObjective", - "MCMultiOutputObjective", - "MultiObjectiveAnalyticAcquisitionFunction", - "MultiObjectiveMCAcquisitionFunction", + "qNoisyExpectedHypervolumeImprovement", "WeightedMCMultiOutputObjective", ] diff --git a/botorch/acquisition/multi_objective/analytic.py b/botorch/acquisition/multi_objective/analytic.py index 6b4c4aeb50..7e4f00e5d9 100644 --- a/botorch/acquisition/multi_objective/analytic.py +++ b/botorch/acquisition/multi_objective/analytic.py @@ -19,14 +19,14 @@ from __future__ import annotations -from abc import abstractmethod from itertools import product from typing import Optional import torch -from botorch.acquisition.acquisition import AcquisitionFunction +from botorch.acquisition.multi_objective.base import ( + MultiObjectiveAnalyticAcquisitionFunction, +) from botorch.acquisition.objective import PosteriorTransform -from botorch.exceptions.errors import UnsupportedError from botorch.models.model import Model from botorch.utils.multi_objective.box_decompositions.non_dominated import ( NondominatedPartitioning, @@ -36,45 +36,6 @@ from torch.distributions import Normal -class MultiObjectiveAnalyticAcquisitionFunction(AcquisitionFunction): - r"""Abstract base class for Multi-Objective batch acquisition functions.""" - - def __init__( - self, - model: Model, - posterior_transform: Optional[PosteriorTransform] = None, - ) -> None: - r"""Constructor for the MultiObjectiveAnalyticAcquisitionFunction base class. - - Args: - model: A fitted model. - posterior_transform: A PosteriorTransform (optional). - """ - super().__init__(model=model) - if posterior_transform is None or isinstance( - posterior_transform, PosteriorTransform - ): - self.posterior_transform = posterior_transform - else: - raise UnsupportedError( - "Only a posterior_transform of type PosteriorTransform is " - "supported for Multi-Objective analytic acquisition functions." - ) - - @abstractmethod - def forward(self, X: Tensor) -> Tensor: - r"""Takes in a `batch_shape x 1 x d` X Tensor of t-batches with `1` `d`-dim - design point each, and returns a Tensor with shape `batch_shape'`, where - `batch_shape'` is the broadcasted batch shape of model and input `X`. - """ - pass # pragma: no cover - - def set_X_pending(self, X_pending: Optional[Tensor] = None) -> None: - raise UnsupportedError( - "Analytic acquisition functions do not account for X_pending yet." - ) - - class ExpectedHypervolumeImprovement(MultiObjectiveAnalyticAcquisitionFunction): def __init__( self, diff --git a/botorch/acquisition/multi_objective/base.py b/botorch/acquisition/multi_objective/base.py new file mode 100644 index 0000000000..7d0d72873c --- /dev/null +++ b/botorch/acquisition/multi_objective/base.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +r""" +Base classes for multi-objective acquisition functions. +""" + + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Callable, Optional, Union + +import torch +from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin +from botorch.acquisition.multi_objective.objective import ( + IdentityMCMultiOutputObjective, + MCMultiOutputObjective, +) +from botorch.acquisition.objective import PosteriorTransform +from botorch.exceptions.errors import UnsupportedError +from botorch.models.model import Model +from botorch.models.transforms.input import InputPerturbation +from botorch.sampling.base import MCSampler +from torch import Tensor + + +class MultiObjectiveAnalyticAcquisitionFunction(AcquisitionFunction): + r"""Abstract base class for Multi-Objective batch acquisition functions.""" + + def __init__( + self, + model: Model, + posterior_transform: Optional[PosteriorTransform] = None, + ) -> None: + r"""Constructor for the MultiObjectiveAnalyticAcquisitionFunction base class. + + Args: + model: A fitted model. + posterior_transform: A PosteriorTransform (optional). + """ + super().__init__(model=model) + if posterior_transform is None or isinstance( + posterior_transform, PosteriorTransform + ): + self.posterior_transform = posterior_transform + else: + raise UnsupportedError( + "Only a posterior_transform of type PosteriorTransform is " + "supported for Multi-Objective analytic acquisition functions." + ) + + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Takes in a `batch_shape x 1 x d` X Tensor of t-batches with `1` `d`-dim + design point each, and returns a Tensor with shape `batch_shape'`, where + `batch_shape'` is the broadcasted batch shape of model and input `X`. + """ + pass # pragma: no cover + + def set_X_pending(self, X_pending: Optional[Tensor] = None) -> None: + raise UnsupportedError( + "Analytic acquisition functions do not account for X_pending yet." + ) + + +class MultiObjectiveMCAcquisitionFunction(AcquisitionFunction, MCSamplerMixin, ABC): + r"""Abstract base class for Multi-Objective batch acquisition functions. + + NOTE: This does not inherit from `MCAcquisitionFunction` to avoid circular imports. + + Args: + _default_sample_shape: The `sample_shape` for the default sampler. + """ + + _default_sample_shape = torch.Size([128]) + + def __init__( + self, + model: Model, + sampler: Optional[MCSampler] = None, + objective: Optional[MCMultiOutputObjective] = None, + constraints: Optional[list[Callable[[Tensor], Tensor]]] = None, + eta: Union[Tensor, float] = 1e-3, + X_pending: Optional[Tensor] = None, + ) -> None: + r"""Constructor for the `MultiObjectiveMCAcquisitionFunction` base class. + + Args: + model: A fitted model. + sampler: The sampler used to draw base samples. If not given, + a sampler is generated using `get_sampler`. + NOTE: For posteriors that do not support base samples, + a sampler compatible with intended use case must be provided. + See `ForkedRNGSampler` and `StochasticSampler` as examples. + objective: The MCMultiOutputObjective under which the samples are + evaluated. Defaults to `IdentityMCMultiOutputObjective()`. + constraints: A list of callables, each mapping a Tensor of dimension + `sample_shape x batch-shape x q x m` to a Tensor of dimension + `sample_shape x batch-shape x q`, where negative values imply + feasibility. + eta: The temperature parameter for the sigmoid function used for the + differentiable approximation of the constraints. In case of a float the + same eta is used for every constraint in constraints. In case of a + tensor the length of the tensor must match the number of provided + constraints. The i-th constraint is then estimated with the i-th + eta value. + X_pending: A `m x d`-dim Tensor of `m` design points that have + points that have been submitted for function evaluation + but have not yet been evaluated. + """ + super().__init__(model=model) + MCSamplerMixin.__init__(self, sampler=sampler) + if objective is None: + objective = IdentityMCMultiOutputObjective() + elif not isinstance(objective, MCMultiOutputObjective): + raise UnsupportedError( + "Only objectives of type MCMultiOutputObjective are supported for " + "Multi-Objective MC acquisition functions." + ) + if ( + hasattr(model, "input_transform") + and isinstance(model.input_transform, InputPerturbation) + and constraints is not None + ): + raise UnsupportedError( + "Constraints are not supported with input perturbations, due to" + "sample q-batch shape being different than that of the inputs." + "Use a composite objective that applies feasibility weighting to" + "samples before calculating the risk measure." + ) + self.add_module("objective", objective) + self.constraints = constraints + if constraints: + if type(eta) is not Tensor: + eta = torch.full((len(constraints),), eta) + self.register_buffer("eta", eta) + self.X_pending = None + if X_pending is not None: + self.set_X_pending(X_pending) + + @abstractmethod + def forward(self, X: Tensor) -> Tensor: + r"""Takes in a `batch_shape x q x d` X Tensor of t-batches with `q` `d`-dim + design points each, and returns a Tensor with shape `batch_shape'`, where + `batch_shape'` is the broadcasted batch shape of model and input `X`. Should + utilize the result of `set_X_pending` as needed to account for pending function + evaluations. + """ + pass # pragma: no cover diff --git a/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py b/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py index f43a55c090..8b017654b1 100644 --- a/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py +++ b/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py @@ -29,8 +29,8 @@ from botorch.acquisition.cost_aware import CostAwareUtility from botorch.acquisition.decoupled import DecoupledAcquisitionFunction from botorch.acquisition.knowledge_gradient import ProjectedAcquisitionFunction +from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction from botorch.acquisition.multi_objective.monte_carlo import ( - MultiObjectiveMCAcquisitionFunction, qExpectedHypervolumeImprovement, ) from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective diff --git a/botorch/acquisition/multi_objective/joint_entropy_search.py b/botorch/acquisition/multi_objective/joint_entropy_search.py index 1b8c167936..199a1dce32 100644 --- a/botorch/acquisition/multi_objective/joint_entropy_search.py +++ b/botorch/acquisition/multi_objective/joint_entropy_search.py @@ -25,7 +25,6 @@ from botorch import settings from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin from botorch.exceptions.errors import UnsupportedError - from botorch.models.model import Model from botorch.models.model_list_gp_regression import ModelListGP from botorch.models.utils import fantasize as fantasize_flag diff --git a/botorch/acquisition/multi_objective/logei.py b/botorch/acquisition/multi_objective/logei.py index 9a942a8c13..f9e6896773 100644 --- a/botorch/acquisition/multi_objective/logei.py +++ b/botorch/acquisition/multi_objective/logei.py @@ -14,7 +14,7 @@ import torch from botorch.acquisition.logei import TAU_MAX, TAU_RELU -from botorch.acquisition.multi_objective import MultiObjectiveMCAcquisitionFunction +from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective from botorch.models.model import Model from botorch.sampling.base import MCSampler @@ -60,7 +60,7 @@ def __init__( objective: Optional[MCMultiOutputObjective] = None, constraints: Optional[list[Callable[[Tensor], Tensor]]] = None, X_pending: Optional[Tensor] = None, - eta: Optional[Union[Tensor, float]] = 1e-2, + eta: Union[Tensor, float] = 1e-2, fat: bool = True, tau_relu: float = TAU_RELU, tau_max: float = TAU_MAX, @@ -333,7 +333,7 @@ def __init__( objective: Optional[MCMultiOutputObjective] = None, constraints: Optional[list[Callable[[Tensor], Tensor]]] = None, X_pending: Optional[Tensor] = None, - eta: Optional[Union[Tensor, float]] = 1e-3, + eta: Union[Tensor, float] = 1e-3, prune_baseline: bool = False, alpha: float = 0.0, cache_pending: bool = True, diff --git a/botorch/acquisition/multi_objective/max_value_entropy_search.py b/botorch/acquisition/multi_objective/max_value_entropy_search.py index b77da666d9..5df13ced74 100644 --- a/botorch/acquisition/multi_objective/max_value_entropy_search.py +++ b/botorch/acquisition/multi_objective/max_value_entropy_search.py @@ -20,17 +20,14 @@ from __future__ import annotations from math import pi - from typing import Callable, Optional, Union import torch from botorch.acquisition.max_value_entropy_search import qMaxValueEntropy +from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction from botorch.acquisition.multi_objective.joint_entropy_search import ( LowerBoundMultiObjectiveEntropySearch, ) -from botorch.acquisition.multi_objective.monte_carlo import ( - MultiObjectiveMCAcquisitionFunction, -) from botorch.models.converter import ( batched_multi_output_to_single_output, model_list_to_batched, diff --git a/botorch/acquisition/multi_objective/monte_carlo.py b/botorch/acquisition/multi_objective/monte_carlo.py index 0a82f9e2da..f0773f36db 100644 --- a/botorch/acquisition/multi_objective/monte_carlo.py +++ b/botorch/acquisition/multi_objective/monte_carlo.py @@ -26,19 +26,13 @@ from __future__ import annotations -from abc import ABC, abstractmethod from typing import Callable, Optional, Union import torch -from botorch.acquisition.acquisition import AcquisitionFunction, MCSamplerMixin -from botorch.acquisition.multi_objective.objective import ( - IdentityMCMultiOutputObjective, - MCMultiOutputObjective, -) -from botorch.exceptions.errors import UnsupportedError +from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction +from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective from botorch.exceptions.warnings import legacy_ei_numerics_warning from botorch.models.model import Model -from botorch.models.transforms.input import InputPerturbation from botorch.sampling.base import MCSampler from botorch.utils.multi_objective.box_decompositions.non_dominated import ( NondominatedPartitioning, @@ -57,92 +51,6 @@ from torch import Tensor -class MultiObjectiveMCAcquisitionFunction(AcquisitionFunction, MCSamplerMixin, ABC): - r"""Abstract base class for Multi-Objective batch acquisition functions. - - NOTE: This does not inherit from `MCAcquisitionFunction` to avoid circular imports. - - Args: - _default_sample_shape: The `sample_shape` for the default sampler. - """ - - _default_sample_shape = torch.Size([128]) - - def __init__( - self, - model: Model, - sampler: Optional[MCSampler] = None, - objective: Optional[MCMultiOutputObjective] = None, - constraints: Optional[list[Callable[[Tensor], Tensor]]] = None, - eta: Optional[Union[Tensor, float]] = 1e-3, - X_pending: Optional[Tensor] = None, - ) -> None: - r"""Constructor for the MCAcquisitionFunction base class. - - Args: - model: A fitted model. - sampler: The sampler used to draw base samples. If not given, - a sampler is generated using `get_sampler`. - NOTE: For posteriors that do not support base samples, - a sampler compatible with intended use case must be provided. - See `ForkedRNGSampler` and `StochasticSampler` as examples. - objective: The MCMultiOutputObjective under which the samples are - evaluated. Defaults to `IdentityMCMultiOutputObjective()`. - constraints: A list of callables, each mapping a Tensor of dimension - `sample_shape x batch-shape x q x m` to a Tensor of dimension - `sample_shape x batch-shape x q`, where negative values imply - feasibility. - eta: The temperature parameter for the sigmoid function used for the - differentiable approximation of the constraints. In case of a float the - same eta is used for every constraint in constraints. In case of a - tensor the length of the tensor must match the number of provided - constraints. The i-th constraint is then estimated with the i-th - eta value. - X_pending: A `m x d`-dim Tensor of `m` design points that have - points that have been submitted for function evaluation - but have not yet been evaluated. - """ - super().__init__(model=model) - MCSamplerMixin.__init__(self, sampler=sampler) - if objective is None: - objective = IdentityMCMultiOutputObjective() - elif not isinstance(objective, MCMultiOutputObjective): - raise UnsupportedError( - "Only objectives of type MCMultiOutputObjective are supported for " - "Multi-Objective MC acquisition functions." - ) - if ( - hasattr(model, "input_transform") - and isinstance(model.input_transform, InputPerturbation) - and constraints is not None - ): - raise UnsupportedError( - "Constraints are not supported with input perturbations, due to" - "sample q-batch shape being different than that of the inputs." - "Use a composite objective that applies feasibility weighting to" - "samples before calculating the risk measure." - ) - self.add_module("objective", objective) - self.constraints = constraints - if constraints: - if type(eta) is not Tensor: - eta = torch.full((len(constraints),), eta) - self.register_buffer("eta", eta) - self.X_pending = None - if X_pending is not None: - self.set_X_pending(X_pending) - - @abstractmethod - def forward(self, X: Tensor) -> Tensor: - r"""Takes in a `batch_shape x q x d` X Tensor of t-batches with `q` `d`-dim - design points each, and returns a Tensor with shape `batch_shape'`, where - `batch_shape'` is the broadcasted batch shape of model and input `X`. Should - utilize the result of `set_X_pending` as needed to account for pending function - evaluations. - """ - pass # pragma: no cover - - class qExpectedHypervolumeImprovement( MultiObjectiveMCAcquisitionFunction, SubsetIndexCachingMixin ): @@ -155,7 +63,7 @@ def __init__( objective: Optional[MCMultiOutputObjective] = None, constraints: Optional[list[Callable[[Tensor], Tensor]]] = None, X_pending: Optional[Tensor] = None, - eta: Optional[Union[Tensor, float]] = 1e-3, + eta: Union[Tensor, float] = 1e-3, fat: bool = False, ) -> None: r"""q-Expected Hypervolume Improvement supporting m>=2 outcomes. @@ -334,7 +242,7 @@ def __init__( objective: Optional[MCMultiOutputObjective] = None, constraints: Optional[list[Callable[[Tensor], Tensor]]] = None, X_pending: Optional[Tensor] = None, - eta: Optional[Union[Tensor, float]] = 1e-3, + eta: Union[Tensor, float] = 1e-3, fat: bool = False, prune_baseline: bool = False, alpha: float = 0.0, diff --git a/botorch/acquisition/multi_objective/multi_fidelity.py b/botorch/acquisition/multi_objective/multi_fidelity.py index 1430381503..9aec38ca01 100644 --- a/botorch/acquisition/multi_objective/multi_fidelity.py +++ b/botorch/acquisition/multi_objective/multi_fidelity.py @@ -46,7 +46,7 @@ def __init__( sampler: Optional[MCSampler] = None, objective: Optional[MCMultiOutputObjective] = None, constraints: Optional[list[Callable[[Tensor], Tensor]]] = None, - eta: Optional[Union[Tensor, float]] = 1e-3, + eta: Union[Tensor, float] = 1e-3, X_pending: Optional[Tensor] = None, cost_call: Optional[Callable[[Tensor], Tensor]] = None, ) -> None: diff --git a/botorch/acquisition/multi_objective/parego.py b/botorch/acquisition/multi_objective/parego.py index c6de972e9f..cddf3137d9 100644 --- a/botorch/acquisition/multi_objective/parego.py +++ b/botorch/acquisition/multi_objective/parego.py @@ -7,9 +7,7 @@ import torch from botorch.acquisition.logei import qLogNoisyExpectedImprovement, TAU_MAX, TAU_RELU -from botorch.acquisition.multi_objective.monte_carlo import ( - MultiObjectiveMCAcquisitionFunction, -) +from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective from botorch.acquisition.objective import GenericMCObjective from botorch.models.model import Model diff --git a/docs/multi_objective.md b/docs/multi_objective.md index 7e71241257..758dec4311 100644 --- a/docs/multi_objective.md +++ b/docs/multi_objective.md @@ -3,50 +3,54 @@ id: multi_objective title: Multi-Objective Bayesian Optimization --- -BoTorch provides first-class support for Multi-Objective (MO) Bayesian Optimization (BO) including implementations of -[`qLogNoisyExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement) (qLogNEHVI)[^qNEHVI][^LogEI], -[`qLogExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.logei.qLogExpectedHypervolumeImprovement) (qLogEHVI), +BoTorch provides first-class support for Multi-Objective (MO) Bayesian +Optimization (BO) including implementations of +[`qLogNoisyExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement) +(qLogNEHVI)[^qNEHVI][^LogEI], +[`qLogExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.logei.qLogExpectedHypervolumeImprovement) +(qLogEHVI), [`qLogNParEGO`](../api/acquisition.html#botorch.acquisition.multi_objective.parego.qLogNParEGO)[^qNEHVI], and analytic [`ExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.analytic.ExpectedHypervolumeImprovement) (EHVI) with gradients via auto-differentiation acquisition functions[^qEHVI]. -The goal in MOBO is learn the *Pareto front*: the set of optimal trade-offs, +The goal in MOBO is learn the _Pareto front_: the set of optimal trade-offs, where an improvement in one objective means deteriorating another objective. Botorch provides implementations for a number of acquisition functions specifically for the multi-objective scenario, as well as generic interfaces for implemented new multi-objective acquisition functions. ## Multi-Objective Acquisition Functions + MOBO leverages many advantages of BoTorch to make provide practical algorithms for computationally intensive and analytically intractable problems. For example, analytic EHVI has no known analytical gradient for when there are more than two objectives, but BoTorch computes analytic gradients for free via auto-differentiation, regardless of the number of objectives [^qEHVI]. -For analytic and MC-based MOBO acquisition functions such as qLogNEHVI, qLogEHVI, and -`qLogNParEGO`, BoTorch leverages GPU acceleration and quasi-second order methods for -acquisition optimization for efficient computation and optimization in many -practical scenarios [^qNEHVI][^qEHVI]. The MC-based acquisition functions -support using the sample average approximation for rapid convergence [^BoTorch]. +For analytic and MC-based MOBO acquisition functions such as qLogNEHVI, +qLogEHVI, and `qLogNParEGO`, BoTorch leverages GPU acceleration and quasi-second +order methods for acquisition optimization for efficient computation and +optimization in many practical scenarios [^qNEHVI][^qEHVI]. The MC-based +acquisition functions support using the sample average approximation for rapid +convergence [^BoTorch]. All analytic MO acquisition functions derive from -[`MultiObjectiveAnalyticAcquisitionFunction`](../api/acquisition.html#botorch.acquisition.multi_objective.analytic.MultiObjectiveAnalyticAcquisitionFunction) +[`MultiObjectiveAnalyticAcquisitionFunction`](../api/acquisition.html#botorch.acquisition.multi_objective.base.MultiObjectiveAnalyticAcquisitionFunction) and all MC-based acquisition functions derive from -[`MultiObjectiveMCAcquisitionFunction`](../api/acquisition.html#botorch.acquisition.multi_objective.monte_carlo.MultiObjectiveMCAcquisitionFunction). +[`MultiObjectiveMCAcquisitionFunction`](../api/acquisition.html#botorch.acquisition.multi_objective.base.MultiObjectiveMCAcquisitionFunction). These abstract classes easily integrate with BoTorch's standard optimization machinery. -`qLogNParEGO` supports optimization via random scalarizations. -In the batch setting, it uses a new random scalarization for each candidate -[^qEHVI]. Candidates are selected in a sequential greedy fashion, each with a -different scalarization, via the +`qLogNParEGO` supports optimization via random scalarizations. In the batch +setting, it uses a new random scalarization for each candidate [^qEHVI]. +Candidates are selected in a sequential greedy fashion, each with a different +scalarization, via the [`optimize_acqf_list`](../api/optim.html#botorch.optim.optimize.optimize_acqf_list) function. For a more in-depth example using these acquisition functions, check out the -[Multi-Objective Bayesian Optimization tutorial -notebook](../tutorials/multi_objective_bo). +[Multi-Objective Bayesian Optimization tutorial notebook](../tutorials/multi_objective_bo). ## Multi-Objective Utilities @@ -66,7 +70,7 @@ Pareto frontier for the minimization problem, and [Lacour17]_ is applied again to partition the space dominated by that Pareto frontier. Approximate box decompositions are also supported using the algorithm from [^Couckuyt2012]. See Appendix F.4 in [^qEHVI] for an analysis of approximate vs exact box -decompositions with EHVI. These box decompositions (approximate or exact) can +decompositions with EHVI. These box decompositions (approximate or exact) can also be used to efficiently compute hypervolumes. Additionally, variations on ParEGO can be trivially implemented using an @@ -77,39 +81,46 @@ The [`get_chebyshev_scalarization`](../api/utils.html#botorch.utils.multi_objective.scalarization.get_chebyshev_scalarization) convenience function generates these scalarizations. -[^qNEHVI]: S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian -Optimization of Multiple Noisy Objectives with Expected Hypervolume Improvement. -Advances in Neural Information Processing Systems 34, 2021. -[paper](https://arxiv.org/abs/2105.08195) - -[^LogEI]: S. Ament, S. Daulton, D. Eriksson, M. Balandat, and E. Bakshy. -Unexpected Improvements to Expected Improvement for Bayesian Optimization. Advances -in Neural Information Processing Systems 36, 2023. -[paper](https://arxiv.org/abs/2310.20708) "Log" variances of acquisition -functions, such as [`qLogNoisyExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement), -offer improved numerics compared to older counterparts such as -[`qNoisyExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.monte_carlo.qNoisyExpectedHypervolumeImprovement). - -[^qEHVI]: S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume -Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in Neural -Information Processing Systems 33, 2020. -[paper](https://arxiv.org/abs/2006.05078) - -[^BoTorch]: M. Balandat, B. Karrer, D. R. Jiang, S. Daulton, B. Letham, A. G. Wilson, -and E. Bakshy. BoTorch: A Framework for Efficient Monte-Carlo Bayesian Optimization. -Advances in Neural Information Processing Systems 33, 2020. -[paper](https://arxiv.org/abs/1910.06403) - -[^Yang2019]: K. Yang, M. Emmerich, A. Deutz, et al. Efficient computation of expected -hypervolume improvement using box decomposition algorithms. J Glob Optim 75, 2019. -[paper](https://arxiv.org/abs/1904.12672) - -[^Lacour17]: R. Lacour, K. Klamroth, C. Fonseca. A box decomposition algorithm to -compute the hypervolume indicator. Computers & Operations Research, -Volume 79, 2017. -[paper](https://www.sciencedirect.com/science/article/pii/S0305054816301538) - -[^Couckuyt2012]: I. Couckuyt, D. Deschrijver and T. Dhaene. Towards Efficient -Multiobjective Optimization: Multiobjective statistical criterions. IEEE Congress -on Evolutionary Computation, Brisbane, QLD, 2012. -[paper](https://ieeexplore.ieee.org/document/6256586) +[^qNEHVI]: + S. Daulton, M. Balandat, and E. Bakshy. Parallel Bayesian Optimization of + Multiple Noisy Objectives with Expected Hypervolume Improvement. Advances in + Neural Information Processing Systems 34, 2021. + [paper](https://arxiv.org/abs/2105.08195) + +[^LogEI]: + S. Ament, S. Daulton, D. Eriksson, M. Balandat, and E. Bakshy. Unexpected + Improvements to Expected Improvement for Bayesian Optimization. Advances in + Neural Information Processing Systems 36, 2023. + [paper](https://arxiv.org/abs/2310.20708) "Log" variances of acquisition + functions, such as + [`qLogNoisyExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.logei.qLogNoisyExpectedHypervolumeImprovement), + offer improved numerics compared to older counterparts such as + [`qNoisyExpectedHypervolumeImprovement`](../api/acquisition.html#botorch.acquisition.multi_objective.monte_carlo.qNoisyExpectedHypervolumeImprovement). + +[^qEHVI]: + S. Daulton, M. Balandat, and E. Bakshy. Differentiable Expected Hypervolume + Improvement for Parallel Multi-Objective Bayesian Optimization. Advances in + Neural Information Processing Systems 33, 2020. + [paper](https://arxiv.org/abs/2006.05078) + +[^BoTorch]: + M. Balandat, B. Karrer, D. R. Jiang, S. Daulton, B. Letham, A. G. Wilson, + and E. Bakshy. BoTorch: A Framework for Efficient Monte-Carlo Bayesian + Optimization. Advances in Neural Information Processing Systems 33, 2020. + [paper](https://arxiv.org/abs/1910.06403) + +[^Yang2019]: + K. Yang, M. Emmerich, A. Deutz, et al. Efficient computation of expected + hypervolume improvement using box decomposition algorithms. J Glob Optim + 75, 2019. [paper](https://arxiv.org/abs/1904.12672) + +[^Lacour17]: + R. Lacour, K. Klamroth, C. Fonseca. A box decomposition algorithm to compute + the hypervolume indicator. Computers & Operations Research, Volume 79, 2017. + [paper](https://www.sciencedirect.com/science/article/pii/S0305054816301538) + +[^Couckuyt2012]: + I. Couckuyt, D. Deschrijver and T. Dhaene. Towards Efficient Multiobjective + Optimization: Multiobjective statistical criterions. IEEE Congress on + Evolutionary Computation, Brisbane, QLD, 2012. + [paper](https://ieeexplore.ieee.org/document/6256586) diff --git a/sphinx/source/acquisition.rst b/sphinx/source/acquisition.rst index e35b68a146..2f6060058a 100644 --- a/sphinx/source/acquisition.rst +++ b/sphinx/source/acquisition.rst @@ -37,16 +37,9 @@ Monte-Carlo Acquisition Function API .. autoclass:: MCAcquisitionFunction :members: -Multi-Objective Analytic Acquisition Function API -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. currentmodule:: botorch.acquisition.multi_objective.analytic -.. autoclass:: MultiObjectiveAnalyticAcquisitionFunction - :members: - -Multi-Objective Monte-Carlo Acquisition Function API -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. currentmodule:: botorch.acquisition.multi_objective.monte_carlo -.. autoclass:: MultiObjectiveMCAcquisitionFunction +Base Classes for Multi-Objective Acquisition Function API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. automodule:: botorch.acquisition.multi_objective.base :members: @@ -72,7 +65,6 @@ Multi-Objective Analytic Acquisition Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: botorch.acquisition.multi_objective.analytic :members: - :exclude-members: MultiObjectiveAnalyticAcquisitionFunction Multi-Objective Hypervolume Knowledge Gradient Acquisition Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -93,7 +85,6 @@ Multi-Objective Monte-Carlo Acquisition Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: botorch.acquisition.multi_objective.monte_carlo :members: - :exclude-members: MultiObjectiveMCAcquisitionFunction .. automodule:: botorch.acquisition.multi_objective.logei :members: diff --git a/test/acquisition/multi_objective/test_analytic.py b/test/acquisition/multi_objective/test_analytic.py index ecb3365ca3..b9bec732e7 100644 --- a/test/acquisition/multi_objective/test_analytic.py +++ b/test/acquisition/multi_objective/test_analytic.py @@ -6,61 +6,12 @@ import torch -from botorch.acquisition.multi_objective.analytic import ( - ExpectedHypervolumeImprovement, - MultiObjectiveAnalyticAcquisitionFunction, -) -from botorch.acquisition.multi_objective.objective import IdentityMCMultiOutputObjective -from botorch.acquisition.objective import PosteriorTransform -from botorch.exceptions.errors import BotorchError, UnsupportedError -from botorch.posteriors import GPyTorchPosterior +from botorch.acquisition.multi_objective.analytic import ExpectedHypervolumeImprovement +from botorch.exceptions.errors import BotorchError from botorch.utils.multi_objective.box_decompositions.non_dominated import ( NondominatedPartitioning, ) from botorch.utils.testing import BotorchTestCase, MockModel, MockPosterior -from torch import Tensor - - -class DummyMultiObjectiveAnalyticAcquisitionFunction( - MultiObjectiveAnalyticAcquisitionFunction -): - def forward(self, X): - pass - - -class DummyPosteriorTransform(PosteriorTransform): - def evaluate(self, Y: Tensor) -> Tensor: - pass - - def forward(self, posterior: GPyTorchPosterior) -> GPyTorchPosterior: - pass - - -class TestMultiObjectiveAnalyticAcquisitionFunction(BotorchTestCase): - def test_abstract_raises(self): - with self.assertRaises(TypeError): - MultiObjectiveAnalyticAcquisitionFunction() - - def test_init(self): - mm = MockModel(MockPosterior(mean=torch.rand(2, 1))) - # test default init - acqf = DummyMultiObjectiveAnalyticAcquisitionFunction(model=mm) - self.assertTrue(acqf.posterior_transform is None) # is None by default - # test custom init - posterior_transform = DummyPosteriorTransform() - acqf = DummyMultiObjectiveAnalyticAcquisitionFunction( - model=mm, posterior_transform=posterior_transform - ) - self.assertEqual(acqf.posterior_transform, posterior_transform) - # test unsupported objective - with self.assertRaises(UnsupportedError): - DummyMultiObjectiveAnalyticAcquisitionFunction( - model=mm, posterior_transform=IdentityMCMultiOutputObjective() - ) - acqf = DummyMultiObjectiveAnalyticAcquisitionFunction(model=mm) - # test set_X_pending - with self.assertRaises(UnsupportedError): - acqf.set_X_pending() class TestExpectedHypervolumeImprovement(BotorchTestCase): diff --git a/test/acquisition/multi_objective/test_base.py b/test/acquisition/multi_objective/test_base.py new file mode 100644 index 0000000000..d880a62a86 --- /dev/null +++ b/test/acquisition/multi_objective/test_base.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +import torch +from botorch.acquisition.multi_objective.base import ( + MultiObjectiveAnalyticAcquisitionFunction, + MultiObjectiveMCAcquisitionFunction, +) +from botorch.acquisition.multi_objective.objective import ( + IdentityMCMultiOutputObjective, + MCMultiOutputObjective, +) +from botorch.acquisition.objective import IdentityMCObjective, PosteriorTransform +from botorch.exceptions.errors import UnsupportedError +from botorch.models.transforms.input import InputPerturbation +from botorch.posteriors import GPyTorchPosterior +from botorch.sampling.normal import SobolQMCNormalSampler +from botorch.utils.testing import BotorchTestCase, MockModel, MockPosterior +from torch import Tensor + + +class DummyMultiObjectiveAnalyticAcquisitionFunction( + MultiObjectiveAnalyticAcquisitionFunction +): + def forward(self, X): + pass + + +class DummyMultiObjectiveMCAcquisitionFunction(MultiObjectiveMCAcquisitionFunction): + def forward(self, X): + pass + + +class DummyPosteriorTransform(PosteriorTransform): + def evaluate(self, Y: Tensor) -> Tensor: + pass + + def forward(self, posterior: GPyTorchPosterior) -> GPyTorchPosterior: + pass + + +class DummyMCMultiOutputObjective(MCMultiOutputObjective): + def forward(self, samples, X=None): + if X is not None: + return samples[..., : X.shape[-2], :] + else: + return samples + + +class TestBaseMultiObjectiveAcquisitionFunctions(BotorchTestCase): + def test_abstract_raises(self): + with self.assertRaises(TypeError): + MultiObjectiveAnalyticAcquisitionFunction() + with self.assertRaises(TypeError): + MultiObjectiveMCAcquisitionFunction() + + def test_init_MultiObjectiveAnalyticAcquisitionFunction(self): + mm = MockModel(MockPosterior(mean=torch.rand(2, 1))) + # test default init + acqf = DummyMultiObjectiveAnalyticAcquisitionFunction(model=mm) + self.assertTrue(acqf.posterior_transform is None) # is None by default + # test custom init + posterior_transform = DummyPosteriorTransform() + acqf = DummyMultiObjectiveAnalyticAcquisitionFunction( + model=mm, posterior_transform=posterior_transform + ) + self.assertEqual(acqf.posterior_transform, posterior_transform) + # test unsupported objective + with self.assertRaises(UnsupportedError): + DummyMultiObjectiveAnalyticAcquisitionFunction( + model=mm, posterior_transform=IdentityMCMultiOutputObjective() + ) + acqf = DummyMultiObjectiveAnalyticAcquisitionFunction(model=mm) + # test set_X_pending + with self.assertRaises(UnsupportedError): + acqf.set_X_pending() + + def test_init_MultiObjectiveMCAcquisitionFunction(self): + mm = MockModel(MockPosterior(mean=torch.rand(2, 1), samples=torch.rand(2, 1))) + # test default init + acqf = DummyMultiObjectiveMCAcquisitionFunction(model=mm) + self.assertIsInstance(acqf.objective, IdentityMCMultiOutputObjective) + self.assertIsNone(acqf.sampler) + # Initialize the sampler. + acqf.get_posterior_samples(mm.posterior(torch.ones(1, 1))) + self.assertEqual(acqf.sampler.sample_shape, torch.Size([128])) + self.assertIsNone(acqf.X_pending) + # test custom init + sampler = SobolQMCNormalSampler(sample_shape=torch.Size([64])) + objective = DummyMCMultiOutputObjective() + X_pending = torch.rand(2, 1) + acqf = DummyMultiObjectiveMCAcquisitionFunction( + model=mm, sampler=sampler, objective=objective, X_pending=X_pending + ) + self.assertEqual(acqf.objective, objective) + self.assertEqual(acqf.sampler, sampler) + self.assertTrue(torch.equal(acqf.X_pending, X_pending)) + # test unsupported objective + with self.assertRaises(UnsupportedError): + DummyMultiObjectiveMCAcquisitionFunction( + model=mm, objective=IdentityMCObjective() + ) + # test constraints with input perturbation. + mm.input_transform = InputPerturbation(perturbation_set=torch.rand(2, 1)) + with self.assertRaises(UnsupportedError): + DummyMultiObjectiveMCAcquisitionFunction( + model=mm, constraints=[lambda Z: -100.0 * torch.ones_like(Z[..., -1])] + ) diff --git a/test/acquisition/multi_objective/test_logei.py b/test/acquisition/multi_objective/test_logei.py index ec3f532544..ea5fcc5a45 100644 --- a/test/acquisition/multi_objective/test_logei.py +++ b/test/acquisition/multi_objective/test_logei.py @@ -7,10 +7,8 @@ import itertools import torch +from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction from botorch.acquisition.multi_objective.logei import qLogExpectedHypervolumeImprovement -from botorch.acquisition.multi_objective.monte_carlo import ( - MultiObjectiveMCAcquisitionFunction, -) from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective from botorch.sampling.normal import IIDNormalSampler, SobolQMCNormalSampler from botorch.utils.multi_objective.box_decompositions.non_dominated import ( diff --git a/test/acquisition/multi_objective/test_monte_carlo.py b/test/acquisition/multi_objective/test_monte_carlo.py index d790dd4781..b29bd92bf1 100644 --- a/test/acquisition/multi_objective/test_monte_carlo.py +++ b/test/acquisition/multi_objective/test_monte_carlo.py @@ -16,12 +16,12 @@ from botorch import settings from botorch.acquisition import AcquisitionFunction from botorch.acquisition.cached_cholesky import _get_cache_root_not_supported_message +from botorch.acquisition.multi_objective.base import MultiObjectiveMCAcquisitionFunction from botorch.acquisition.multi_objective.logei import ( qLogExpectedHypervolumeImprovement, qLogNoisyExpectedHypervolumeImprovement, ) from botorch.acquisition.multi_objective.monte_carlo import ( - MultiObjectiveMCAcquisitionFunction, qExpectedHypervolumeImprovement, qNoisyExpectedHypervolumeImprovement, ) @@ -31,9 +31,7 @@ from botorch.acquisition.multi_objective.objective import ( GenericMCMultiOutputObjective, IdentityMCMultiOutputObjective, - MCMultiOutputObjective, ) -from botorch.acquisition.objective import IdentityMCObjective from botorch.exceptions.errors import BotorchError, UnsupportedError from botorch.exceptions.warnings import BotorchWarning, NumericsWarning from botorch.models import ( @@ -75,56 +73,7 @@ def evaluate(acqf: MultiObjectiveMCAcquisitionFunction, X: Tensor) -> Tensor: ) -class DummyMultiObjectiveMCAcquisitionFunction(MultiObjectiveMCAcquisitionFunction): - def forward(self, X): - pass - - -class DummyMCMultiOutputObjective(MCMultiOutputObjective): - def forward(self, samples, X=None): - if X is not None: - return samples[..., : X.shape[-2], :] - else: - return samples - - class TestMultiObjectiveMCAcquisitionFunction(BotorchTestCase): - def test_abstract_raises(self): - with self.assertRaises(TypeError): - MultiObjectiveMCAcquisitionFunction() - - def test_init(self): - mm = MockModel(MockPosterior(mean=torch.rand(2, 1), samples=torch.rand(2, 1))) - # test default init - acqf = DummyMultiObjectiveMCAcquisitionFunction(model=mm) - self.assertIsInstance(acqf.objective, IdentityMCMultiOutputObjective) - self.assertIsNone(acqf.sampler) - # Initialize the sampler. - acqf.get_posterior_samples(mm.posterior(torch.ones(1, 1))) - self.assertEqual(acqf.sampler.sample_shape, torch.Size([128])) - self.assertIsNone(acqf.X_pending) - # test custom init - sampler = SobolQMCNormalSampler(sample_shape=torch.Size([64])) - objective = DummyMCMultiOutputObjective() - X_pending = torch.rand(2, 1) - acqf = DummyMultiObjectiveMCAcquisitionFunction( - model=mm, sampler=sampler, objective=objective, X_pending=X_pending - ) - self.assertEqual(acqf.objective, objective) - self.assertEqual(acqf.sampler, sampler) - self.assertTrue(torch.equal(acqf.X_pending, X_pending)) - # test unsupported objective - with self.assertRaises(UnsupportedError): - DummyMultiObjectiveMCAcquisitionFunction( - model=mm, objective=IdentityMCObjective() - ) - # test constraints with input perturbation. - mm.input_transform = InputPerturbation(perturbation_set=torch.rand(2, 1)) - with self.assertRaises(UnsupportedError): - DummyMultiObjectiveMCAcquisitionFunction( - model=mm, constraints=[lambda Z: -100.0 * torch.ones_like(Z[..., -1])] - ) - def test_q_expected_hypervolume_improvement(self): for dtype in (torch.float, torch.double): with self.subTest(dtype=dtype): @@ -159,13 +108,13 @@ def test_fat_q_log_expected_hypervolume_improvement(self): def _test_q_expected_hypervolume_improvement( self, - acqf_class: type[AcquisitionFunction], + acqf_class: type[MultiObjectiveMCAcquisitionFunction], dtype: torch.dtype, acqf_kwargs: Optional[dict[str, Any]] = None, ): if acqf_kwargs is None: acqf_kwargs = {} - tkwargs = {"device": self.device, "dtype": dtype} + tkwargs: dict[str, Any] = {"device": self.device, "dtype": dtype} ref_point = [0.0, 0.0] t_ref_point = torch.tensor(ref_point, **tkwargs) pareto_Y = torch.tensor( From 831072cacb1351adf4ac7cf0c5326a7437cbe2b5 Mon Sep 17 00:00:00 2001 From: Sait Cakmak Date: Fri, 23 Aug 2024 13:26:42 -0700 Subject: [PATCH 3/3] Suppress non-log EI warnings in HVKG helpers (#2486) Summary: Pull Request resolved: https://github.com/pytorch/botorch/pull/2486 My first idea was to change this to use Log-EHVI, but this helper is used in the tutorial to compute the hypervolume, so we can't change the return value. Since this is not directly constructed by the user, raising a warning is not productive. This diff suppresses the warning. Reviewed By: Balandat Differential Revision: D61722790 fbshipit-source-id: df3fc52210f1724f74172fd4d7773df05b1c1080 --- .../hypervolume_knowledge_gradient.py | 34 ++++++++++------ .../test_hypervolume_knowledge_gradient.py | 40 ++++++++++--------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py b/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py index 8b017654b1..260085df77 100644 --- a/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py +++ b/botorch/acquisition/multi_objective/hypervolume_knowledge_gradient.py @@ -16,6 +16,7 @@ Learning, 2023. """ +import warnings from copy import deepcopy from typing import Any, Callable, Optional @@ -35,6 +36,7 @@ ) from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective from botorch.exceptions.errors import UnsupportedError +from botorch.exceptions.warnings import NumericsWarning from botorch.models.deterministic import PosteriorMeanModel from botorch.models.model import Model from botorch.sampling.base import MCSampler @@ -500,20 +502,26 @@ def _get_hv_value_function( if use_posterior_mean: model = PosteriorMeanModel(model=model) sampler = StochasticSampler(sample_shape=torch.Size([1])) # dummy sampler - base_value_function = qExpectedHypervolumeImprovement( - model=model, - ref_point=ref_point, - partitioning=FastNondominatedPartitioning( + with warnings.catch_warnings(): + warnings.filterwarnings( + message="qExpectedHypervolumeImprovement has known", + action="ignore", + category=NumericsWarning, + ) + base_value_function = qExpectedHypervolumeImprovement( + model=model, ref_point=ref_point, - Y=torch.empty( - (0, ref_point.shape[0]), - dtype=ref_point.dtype, - device=ref_point.device, - ), - ), # create empty partitioning - sampler=sampler, - objective=objective, - ) + partitioning=FastNondominatedPartitioning( + ref_point=ref_point, + Y=torch.empty( + (0, ref_point.shape[0]), + dtype=ref_point.dtype, + device=ref_point.device, + ), + ), # create empty partitioning + sampler=sampler, + objective=objective, + ) # ProjectedAcquisitionFunction requires this base_value_function.posterior_transform = None diff --git a/test/acquisition/multi_objective/test_hypervolume_knowledge_gradient.py b/test/acquisition/multi_objective/test_hypervolume_knowledge_gradient.py index a6d22a8550..04987b860e 100644 --- a/test/acquisition/multi_objective/test_hypervolume_knowledge_gradient.py +++ b/test/acquisition/multi_objective/test_hypervolume_knowledge_gradient.py @@ -4,6 +4,7 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. +import warnings from itertools import product from unittest import mock @@ -22,6 +23,7 @@ IdentityMCMultiOutputObjective, ) from botorch.exceptions.errors import UnsupportedError +from botorch.exceptions.warnings import NumericsWarning from botorch.models.deterministic import GenericDeterministicModel from botorch.models.gp_regression import SingleTaskGP from botorch.models.model_list_gp_regression import ModelListGP @@ -366,24 +368,26 @@ def test_evaluate_q_hvkg(self): for use_posterior_mean in (True, False): with mock.patch.object( ModelListGP, "fantasize", return_value=mfm - ) as patch_f: - with mock.patch( - NO, new_callable=mock.PropertyMock - ) as mock_num_outputs: - mock_num_outputs.return_value = 3 - qHVKG = acqf_class( - model=model, - num_fantasies=n_f, - objective=objective, - ref_point=ref_point, - num_pareto=num_pareto, - use_posterior_mean=use_posterior_mean, - **mf_kwargs, - ) - val = qHVKG(X) - patch_f.assert_called_once() - cargs, ckwargs = patch_f.call_args - self.assertEqual(ckwargs["X"].shape, torch.Size([1, 1, 1])) + ) as patch_f, mock.patch( + NO, new_callable=mock.PropertyMock + ) as mock_num_outputs, warnings.catch_warnings( + record=True + ) as ws: + mock_num_outputs.return_value = 3 + qHVKG = acqf_class( + model=model, + num_fantasies=n_f, + objective=objective, + ref_point=ref_point, + num_pareto=num_pareto, + use_posterior_mean=use_posterior_mean, + **mf_kwargs, + ) + val = qHVKG(X) + patch_f.assert_called_once() + cargs, ckwargs = patch_f.call_args + self.assertEqual(ckwargs["X"].shape, torch.Size([1, 1, 1])) + self.assertFalse(any(w.category is NumericsWarning for w in ws)) Ys = mean if use_posterior_mean else samples objs = objective(Ys.squeeze(1)).view(-1, num_pareto, num_objectives) if num_objectives == 2: