Skip to content

Commit 449b911

Browse files
seanohaganfacebook-github-bot
authored andcommitted
Support different noise levels for different outputs in test functions (#2136)
Summary: Adds support for different noise levels for different objectives in a multiobjective test function Closes #2135 Modifies `BaseTestProblem` class to allow for `noise_std` to be a list of floats, where the length should be the number of objectives in a `MultiObjectiveTestProblem`. This way each component of the objective function can be subject to a separate noise level. Pull Request resolved: #2136 Reviewed By: sdaulton Differential Revision: D51791486 Pulled By: Balandat fbshipit-source-id: 234fd2f2a97ac62f6f2913ac163fe4950a784891
1 parent 21cdb44 commit 449b911

File tree

5 files changed

+254
-48
lines changed

5 files changed

+254
-48
lines changed

botorch/test_functions/base.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
from __future__ import annotations
1212

1313
from abc import ABC, abstractmethod
14-
from typing import List, Optional, Tuple
14+
from typing import List, Tuple, Union
1515

1616
import torch
17+
1718
from botorch.exceptions.errors import InputDataError
1819
from torch import Tensor
1920
from torch.nn import Module
@@ -26,11 +27,17 @@ class BaseTestProblem(Module, ABC):
2627
_bounds: List[Tuple[float, float]]
2728
_check_grad_at_opt: bool = True
2829

29-
def __init__(self, noise_std: Optional[float] = None, negate: bool = False) -> None:
30+
def __init__(
31+
self,
32+
noise_std: Union[None, float, List[float]] = None,
33+
negate: bool = False,
34+
) -> None:
3035
r"""Base constructor for test functions.
3136
3237
Args:
33-
noise_std: Standard deviation of the observation noise.
38+
noise_std: Standard deviation of the observation noise. If a list is
39+
provided, specifies separate noise standard deviations for each
40+
objective in a multiobjective problem.
3441
negate: If True, negate the function.
3542
"""
3643
super().__init__()
@@ -60,7 +67,8 @@ def forward(self, X: Tensor, noise: bool = True) -> Tensor:
6067
X = X if batch else X.unsqueeze(0)
6168
f = self.evaluate_true(X=X)
6269
if noise and self.noise_std is not None:
63-
f += self.noise_std * torch.randn_like(f)
70+
_noise = torch.tensor(self.noise_std, device=X.device, dtype=X.dtype)
71+
f += _noise * torch.randn_like(f)
6472
if self.negate:
6573
f = -f
6674
return f if batch else f.squeeze(0)
@@ -82,6 +90,7 @@ class ConstrainedBaseTestProblem(BaseTestProblem, ABC):
8290

8391
num_constraints: int
8492
_check_grad_at_opt: bool = False
93+
constraint_noise_std: Union[None, float, List[float]] = None
8594

8695
def evaluate_slack(self, X: Tensor, noise: bool = True) -> Tensor:
8796
r"""Evaluate the constraint slack on a set of points.
@@ -101,10 +110,11 @@ def evaluate_slack(self, X: Tensor, noise: bool = True) -> Tensor:
101110
corresponds to the constraint being feasible).
102111
"""
103112
cons = self.evaluate_slack_true(X=X)
104-
if noise and self.noise_std is not None:
105-
# TODO: Allow different noise levels for objective and constraints (and
106-
# different noise levels between different constraints)
107-
cons += self.noise_std * torch.randn_like(cons)
113+
if noise and self.constraint_noise_std is not None:
114+
_constraint_noise = torch.tensor(
115+
self.constraint_noise_std, device=X.device, dtype=X.dtype
116+
)
117+
cons += _constraint_noise * torch.randn_like(cons)
108118
return cons
109119

110120
def is_feasible(self, X: Tensor, noise: bool = True) -> Tensor:
@@ -147,13 +157,24 @@ class MultiObjectiveTestProblem(BaseTestProblem):
147157
_ref_point: List[float]
148158
_max_hv: float
149159

150-
def __init__(self, noise_std: Optional[float] = None, negate: bool = False) -> None:
160+
def __init__(
161+
self,
162+
noise_std: Union[None, float, List[float]] = None,
163+
negate: bool = False,
164+
) -> None:
151165
r"""Base constructor for multi-objective test functions.
152166
153167
Args:
154-
noise_std: Standard deviation of the observation noise.
168+
noise_std: Standard deviation of the observation noise. If a list is
169+
provided, specifies separate noise standard deviations for each
170+
objective.
155171
negate: If True, negate the objectives.
156172
"""
173+
if isinstance(noise_std, list) and len(noise_std) != len(self._ref_point):
174+
raise InputDataError(
175+
f"If specified as a list, length of noise_std ({len(noise_std)}) "
176+
f"must match the number of objectives ({len(self._ref_point)})"
177+
)
157178
super().__init__(noise_std=noise_std, negate=negate)
158179
ref_point = torch.tensor(self._ref_point, dtype=torch.float)
159180
if negate:

botorch/test_functions/multi_objective.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
import math
7777
from abc import ABC, abstractmethod
7878
from math import pi
79-
from typing import Optional
79+
from typing import List, Union
8080

8181
import torch
8282
from botorch.exceptions.errors import UnsupportedError
@@ -116,7 +116,11 @@ class BraninCurrin(MultiObjectiveTestProblem):
116116
_ref_point = [18.0, 6.0]
117117
_max_hv = 59.36011874867746 # this is approximated using NSGA-II
118118

119-
def __init__(self, noise_std: Optional[float] = None, negate: bool = False) -> None:
119+
def __init__(
120+
self,
121+
noise_std: Union[None, float, List[float]] = None,
122+
negate: bool = False,
123+
) -> None:
120124
r"""
121125
Args:
122126
noise_std: Standard deviation of the observation noise.
@@ -174,7 +178,7 @@ class DH(MultiObjectiveTestProblem, ABC):
174178
def __init__(
175179
self,
176180
dim: int,
177-
noise_std: Optional[float] = None,
181+
noise_std: Union[None, float, List[float]] = None,
178182
negate: bool = False,
179183
) -> None:
180184
r"""
@@ -334,7 +338,7 @@ def __init__(
334338
self,
335339
dim: int,
336340
num_objectives: int = 2,
337-
noise_std: Optional[float] = None,
341+
noise_std: Union[None, float, List[float]] = None,
338342
negate: bool = False,
339343
) -> None:
340344
r"""
@@ -600,7 +604,7 @@ class GMM(MultiObjectiveTestProblem):
600604

601605
def __init__(
602606
self,
603-
noise_std: Optional[float] = None,
607+
noise_std: Union[None, float, List[float]] = None,
604608
negate: bool = False,
605609
num_objectives: int = 2,
606610
) -> None:
@@ -926,7 +930,7 @@ def __init__(
926930
self,
927931
dim: int,
928932
num_objectives: int = 2,
929-
noise_std: Optional[float] = None,
933+
noise_std: Union[None, float, List[float]] = None,
930934
negate: bool = False,
931935
) -> None:
932936
r"""
@@ -1234,7 +1238,11 @@ class ConstrainedBraninCurrin(BraninCurrin, ConstrainedBaseTestProblem):
12341238
_ref_point = [80.0, 12.0]
12351239
_max_hv = 608.4004237022673 # from NSGA-II with 90k evaluations
12361240

1237-
def __init__(self, noise_std: Optional[float] = None, negate: bool = False) -> None:
1241+
def __init__(
1242+
self,
1243+
noise_std: Union[None, float, List[float]] = None,
1244+
negate: bool = False,
1245+
) -> None:
12381246
r"""
12391247
Args:
12401248
noise_std: Standard deviation of the observation noise.
@@ -1337,7 +1345,7 @@ class MW7(MultiObjectiveTestProblem, ConstrainedBaseTestProblem):
13371345
def __init__(
13381346
self,
13391347
dim: int,
1340-
noise_std: Optional[float] = None,
1348+
noise_std: Union[None, float, List[float]] = None,
13411349
negate: bool = False,
13421350
) -> None:
13431351
r"""

botorch/test_functions/synthetic.py

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@
4747
from __future__ import annotations
4848

4949
import math
50-
from typing import List, Optional, Tuple
50+
from typing import List, Optional, Tuple, Union
5151

5252
import torch
53+
from botorch.exceptions.errors import InputDataError
5354
from botorch.test_functions.base import BaseTestProblem, ConstrainedBaseTestProblem
5455
from botorch.test_functions.utils import round_nearest
5556
from torch import Tensor
@@ -64,13 +65,15 @@ class SyntheticTestFunction(BaseTestProblem):
6465

6566
def __init__(
6667
self,
67-
noise_std: Optional[float] = None,
68+
noise_std: Union[None, float, List[float]] = None,
6869
negate: bool = False,
6970
bounds: Optional[List[Tuple[float, float]]] = None,
7071
) -> None:
7172
r"""
7273
Args:
73-
noise_std: Standard deviation of the observation noise.
74+
noise_std: Standard deviation of the observation noise. If a list is
75+
provided, specifies separate noise standard deviations for each
76+
objective in a multiobjective problem.
7477
negate: If True, negate the function.
7578
bounds: Custom bounds for the function specified as (lower, upper) pairs.
7679
"""
@@ -802,7 +805,61 @@ def evaluate_true(self, X: Tensor) -> Tensor:
802805
# ------------ Constrained synthetic test functions ----------- #
803806

804807

805-
class ConstrainedGramacy(ConstrainedBaseTestProblem, SyntheticTestFunction):
808+
class ConstrainedSyntheticTestFunction(
809+
ConstrainedBaseTestProblem, SyntheticTestFunction
810+
):
811+
r"""Base class for constrained synthetic test functions."""
812+
813+
def __init__(
814+
self,
815+
noise_std: Union[None, float, List[float]] = None,
816+
constraint_noise_std: Union[None, float, List[float]] = None,
817+
negate: bool = False,
818+
bounds: Optional[List[Tuple[float, float]]] = None,
819+
) -> None:
820+
r"""
821+
Args:
822+
noise_std: Standard deviation of the observation noise. If a list is
823+
provided, specifies separate noise standard deviations for each
824+
objective in a multiobjective problem.
825+
constraint_noise_std: Standard deviation of the constraint noise.
826+
If a list is provided, specifies separate noise standard
827+
deviations for each constraint.
828+
negate: If True, negate the function.
829+
bounds: Custom bounds for the function specified as (lower, upper) pairs.
830+
"""
831+
self.constraint_noise_std = self._validate_constraint_noise(
832+
constraint_noise_std
833+
)
834+
SyntheticTestFunction.__init__(
835+
self, noise_std=noise_std, negate=negate, bounds=bounds
836+
)
837+
838+
def _validate_constraint_noise(
839+
self, constraint_noise_std
840+
) -> Union[None, float, List[float]]:
841+
"""
842+
Validates that constraint_noise_std has length equal to
843+
the number of constraints, if given as a list
844+
845+
Args:
846+
constraint_noise_std: Standard deviation of the constraint noise.
847+
If a list is provided, specifies separate noise standard
848+
deviations for each constraint.
849+
"""
850+
if (
851+
isinstance(constraint_noise_std, list)
852+
and len(constraint_noise_std) != self.num_constraints
853+
):
854+
raise InputDataError(
855+
"If specified as a list, length of constraint_noise_std "
856+
f"({len(constraint_noise_std)}) must match the "
857+
f"number of constraints ({self.num_constraints})"
858+
)
859+
return constraint_noise_std
860+
861+
862+
class ConstrainedGramacy(ConstrainedSyntheticTestFunction):
806863
r"""Constrained Gramacy test function.
807864
808865
This problem comes from [Gramacy2016]_. The problem is defined
@@ -835,31 +892,77 @@ def evaluate_slack_true(self, X: Tensor) -> Tensor:
835892
return torch.cat([-c1, -c2], dim=-1)
836893

837894

838-
class ConstrainedHartmann(Hartmann, ConstrainedBaseTestProblem):
895+
class ConstrainedHartmann(Hartmann, ConstrainedSyntheticTestFunction):
839896
r"""Constrained Hartmann test function.
840897
841898
This is a constrained version of the standard Hartmann test function that
842899
uses `||x||_2 <= 1` as the constraint. This problem comes from [Letham2019]_.
843900
"""
844901
num_constraints = 1
845902

903+
def __init__(
904+
self,
905+
dim: int = 6,
906+
noise_std: Union[None, float] = None,
907+
constraint_noise_std: Union[None, float, List[float]] = None,
908+
negate: bool = False,
909+
bounds: Optional[List[Tuple[float, float]]] = None,
910+
) -> None:
911+
r"""
912+
Args:
913+
dim: The (input) dimension.
914+
noise_std: Standard deviation of the observation noise.
915+
constraint_noise_std: Standard deviation of the constraint noise.
916+
If a list is provided, specifies separate noise standard
917+
deviations for each constraint.
918+
negate: If True, negate the function.
919+
bounds: Custom bounds for the function specified as (lower, upper) pairs.
920+
"""
921+
self._validate_constraint_noise(constraint_noise_std)
922+
Hartmann.__init__(
923+
self, dim=dim, noise_std=noise_std, negate=negate, bounds=bounds
924+
)
925+
846926
def evaluate_slack_true(self, X: Tensor) -> Tensor:
847927
return -X.norm(dim=-1, keepdim=True) + 1
848928

849929

850-
class ConstrainedHartmannSmooth(Hartmann, ConstrainedBaseTestProblem):
930+
class ConstrainedHartmannSmooth(Hartmann, ConstrainedSyntheticTestFunction):
851931
r"""Smooth constrained Hartmann test function.
852932
853933
This is a constrained version of the standard Hartmann test function that
854934
uses `||x||_2^2 <= 1` as the constraint to obtain smoother constraint slack.
855935
"""
856936
num_constraints = 1
857937

938+
def __init__(
939+
self,
940+
dim: int = 6,
941+
noise_std: Union[None, float] = None,
942+
constraint_noise_std: Union[None, float, List[float]] = None,
943+
negate: bool = False,
944+
bounds: Optional[List[Tuple[float, float]]] = None,
945+
) -> None:
946+
r"""
947+
Args:
948+
dim: The (input) dimension.
949+
noise_std: Standard deviation of the observation noise.
950+
constraint_noise_std: Standard deviation of the constraint noise.
951+
If a list is provided, specifies separate noise standard
952+
deviations for each constraint.
953+
negate: If True, negate the function.
954+
bounds: Custom bounds for the function specified as (lower, upper) pairs.
955+
"""
956+
self._validate_constraint_noise(constraint_noise_std)
957+
Hartmann.__init__(
958+
self, dim=dim, noise_std=noise_std, negate=negate, bounds=bounds
959+
)
960+
858961
def evaluate_slack_true(self, X: Tensor) -> Tensor:
859962
return -X.pow(2).sum(dim=-1, keepdim=True) + 1
860963

861964

862-
class PressureVessel(SyntheticTestFunction, ConstrainedBaseTestProblem):
965+
class PressureVessel(ConstrainedSyntheticTestFunction):
863966
r"""Pressure vessel design problem with constraints.
864967
865968
The four-dimensional pressure vessel design problem with four black-box
@@ -894,7 +997,7 @@ def evaluate_slack_true(self, X: Tensor) -> Tensor:
894997
)
895998

896999

897-
class WeldedBeamSO(SyntheticTestFunction, ConstrainedBaseTestProblem):
1000+
class WeldedBeamSO(ConstrainedSyntheticTestFunction):
8981001
r"""Welded beam design problem with constraints (single-outcome).
8991002
9001003
The four-dimensional welded beam design proble problem with six
@@ -950,7 +1053,7 @@ def evaluate_slack_true(self, X: Tensor) -> Tensor:
9501053
return -torch.stack([g1, g2, g3, g4, g5, g6], dim=-1)
9511054

9521055

953-
class TensionCompressionString(SyntheticTestFunction, ConstrainedBaseTestProblem):
1056+
class TensionCompressionString(ConstrainedSyntheticTestFunction):
9541057
r"""Tension compression string optimization problem with constraints.
9551058
9561059
The three-dimensional tension compression string optimization problem with
@@ -981,7 +1084,7 @@ def evaluate_slack_true(self, X: Tensor) -> Tensor:
9811084
return -constraints.clamp_max(100)
9821085

9831086

984-
class SpeedReducer(SyntheticTestFunction, ConstrainedBaseTestProblem):
1087+
class SpeedReducer(ConstrainedSyntheticTestFunction):
9851088
r"""Speed Reducer design problem with constraints.
9861089
9871090
The seven-dimensional speed reducer design problem with eleven black-box

0 commit comments

Comments
 (0)