Skip to content

Commit cfcdb3c

Browse files
esantorellafacebook-github-bot
authored andcommitted
Remove functionality for BenchmarkRunner without ground truth (facebook#2674)
Summary: Pull Request resolved: facebook#2674 Context: This is an alternative to D61431979. Note: There are benchmarks that do not use `BenchmarkRunner`, but I plan to have them all use `BenchmarkRunner` in the future. `BenchmarkRunner` technically supports benchmarks without a ground truth, but that functionality is never used, and there aren't any Ax benchmarks that are noisy *and* don't have a ground truth. It is not conceptually clear how such a case should be benchmarked, so it is better to not over-engineer for that need, which may never arise. Instead, benchmarks that lack a ground truth but are deterministic can be treated as noiseless problems with a ground truth, and we can reap support for problems without a ground truth. Also, `BenchmarkRunner` has some methods that must either be defined or not defined depending on whether there is a ground truth. They can't be abstract because they will not always be defined. With this change, we can make the ground-truth methods abstract and get rid of the rest. This PR: - Rewrites docstrings - Removes method `get_Y_Ystd` - Makes `get_Y_true` and other methods abstract - Removes functionality for the case where `get_Y_true` raises a `NotImplementedError` Reviewed By: ItsMrLin Differential Revision: D61483962
1 parent 2f70a50 commit cfcdb3c

File tree

1 file changed

+49
-57
lines changed

1 file changed

+49
-57
lines changed

ax/benchmark/runners/base.py

+49-57
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
# pyre-strict
77

8-
from abc import ABC, abstractmethod
8+
from abc import ABC, abstractmethod, abstractproperty
99
from math import sqrt
10-
from typing import Any, Optional, Union
10+
from typing import Any, Union
1111

1212
import torch
1313
from ax.core.arm import Arm
@@ -21,45 +21,44 @@
2121

2222

2323
class BenchmarkRunner(Runner, ABC):
24-
25-
@property
26-
@abstractmethod
24+
"""
25+
A Runner that produces both observed and ground-truth values.
26+
27+
Observed values equal ground-truth values plus noise, with the noise added
28+
according to the standard deviations returned by `get_noise_stds()`.
29+
30+
This runner does require that every benchmark has a ground truth, which
31+
won't necessarily be true for real-world problems. Such problems fall into
32+
two categories:
33+
- If they are deterministic, they can be used with this runner by
34+
viewing them as noiseless problems where the observed values are the
35+
ground truth. The observed values will be used for tracking the
36+
progress of optimization.
37+
- If they are not deterministc, they are not supported. It is not
38+
conceptually clear how to benchmark such problems, so we decided to
39+
not over-engineer for that before such a use case arrives.
40+
"""
41+
42+
@abstractproperty
2743
def outcome_names(self) -> list[str]:
2844
"""The names of the outcomes of the problem (in the order of the outcomes)."""
2945
pass # pragma: no cover
3046

3147
def get_Y_true(self, arm: Arm) -> Tensor:
32-
"""Function returning the ground truth values for a given arm. The
33-
synthetic noise is added as part of the Runner's `run()` method.
34-
For problems that do not have a ground truth, the Runner must
35-
implement the `get_Y_Ystd()` method instead."""
36-
raise NotImplementedError(
37-
"Must implement method `get_Y_true()` for Runner "
38-
f"{self.__class__.__name__} as it does not implement a "
39-
"`get_Y_Ystd()` method."
40-
)
48+
"""
49+
Return the ground truth values for a given arm.
50+
51+
Synthetic noise is added as part of the Runner's `run()` method.
52+
"""
53+
...
4154

55+
@abstractmethod
4256
def get_noise_stds(self) -> Union[None, float, dict[str, float]]:
43-
"""Function returning the standard errors for the synthetic noise
44-
to be applied to the observed values. For problems that do not have
45-
a ground truth, the Runner must implement the `get_Y_Ystd()` method
46-
instead."""
47-
raise NotImplementedError(
48-
"Must implement method `get_Y_Ystd()` for Runner "
49-
f"{self.__class__.__name__} as it does not implement a "
50-
"`get_noise_stds()` method."
51-
)
52-
53-
def get_Y_Ystd(self, arm: Arm) -> tuple[Tensor, Optional[Tensor]]:
54-
"""Function returning the observed values and their standard errors
55-
for a given arm. This function is unused for problems that have a
56-
ground truth (in this case `get_Y_true()` is used), and is required
57-
for problems that do not have a ground truth."""
58-
raise NotImplementedError(
59-
"Must implement method `get_Y_Ystd()` for Runner "
60-
f"{self.__class__.__name__} as it does not implement a "
61-
"`get_Y_true()` method."
62-
)
57+
"""
58+
Return the standard errors for the synthetic noise to be applied to the
59+
observed values.
60+
"""
61+
...
6362

6463
def run(self, trial: BaseTrial) -> dict[str, Any]:
6564
"""Run the trial by evaluating its parameterization(s).
@@ -110,33 +109,26 @@ def run(self, trial: BaseTrial) -> dict[str, Any]:
110109
)
111110

112111
for arm in trial.arms:
113-
try:
114-
# Case where we do have a ground truth
115-
Y_true = self.get_Y_true(arm)
116-
Ys_true[arm.name] = Y_true.tolist()
117-
if noise_stds is None:
118-
# No noise, so just return the true outcome.
119-
Ystds[arm.name] = [0.0] * len(Y_true)
120-
Ys[arm.name] = Y_true.tolist()
121-
else:
122-
# We can scale the noise std by the inverse of the relative sample
123-
# budget allocation to each arm. This works b/c (i) we assume that
124-
# observations per unit sample budget are i.i.d. and (ii) the
125-
# normalized weights sum to one.
126-
std = noise_stds_tsr.to(Y_true) / sqrt(nlzd_arm_weights[arm])
127-
Ystds[arm.name] = std.tolist()
128-
Ys[arm.name] = (Y_true + std * torch.randn_like(Y_true)).tolist()
129-
except NotImplementedError:
130-
# Case where we don't have a ground truth.
131-
Y, Ystd = self.get_Y_Ystd(arm)
132-
Ys[arm.name] = Y.tolist()
133-
Ystds[arm.name] = Ystd.tolist() if Ystd is not None else None
112+
# Case where we do have a ground truth
113+
Y_true = self.get_Y_true(arm)
114+
Ys_true[arm.name] = Y_true.tolist()
115+
if noise_stds is None:
116+
# No noise, so just return the true outcome.
117+
Ystds[arm.name] = [0.0] * len(Y_true)
118+
Ys[arm.name] = Y_true.tolist()
119+
else:
120+
# We can scale the noise std by the inverse of the relative sample
121+
# budget allocation to each arm. This works b/c (i) we assume that
122+
# observations per unit sample budget are i.i.d. and (ii) the
123+
# normalized weights sum to one.
124+
std = noise_stds_tsr.to(Y_true) / sqrt(nlzd_arm_weights[arm])
125+
Ystds[arm.name] = std.tolist()
126+
Ys[arm.name] = (Y_true + std * torch.randn_like(Y_true)).tolist()
134127

135128
run_metadata = {
136129
"Ys": Ys,
137130
"Ystds": Ystds,
138131
"outcome_names": self.outcome_names,
132+
"Ys_true": Ys_true,
139133
}
140-
if Ys_true: # only add key if we actually have a ground truth
141-
run_metadata["Ys_true"] = Ys_true
142134
return run_metadata

0 commit comments

Comments
 (0)