|
5 | 5 |
|
6 | 6 | # pyre-strict
|
7 | 7 |
|
8 |
| -from abc import ABC, abstractmethod |
| 8 | +from abc import ABC, abstractmethod, abstractproperty |
9 | 9 | from math import sqrt
|
10 |
| -from typing import Any, Optional, Union |
| 10 | +from typing import Any, Union |
11 | 11 |
|
12 | 12 | import torch
|
13 | 13 | from ax.core.arm import Arm
|
|
21 | 21 |
|
22 | 22 |
|
23 | 23 | 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 |
27 | 43 | def outcome_names(self) -> list[str]:
|
28 | 44 | """The names of the outcomes of the problem (in the order of the outcomes)."""
|
29 | 45 | pass # pragma: no cover
|
30 | 46 |
|
31 | 47 | 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 | + ... |
41 | 54 |
|
| 55 | + @abstractmethod |
42 | 56 | 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 | + ... |
63 | 62 |
|
64 | 63 | def run(self, trial: BaseTrial) -> dict[str, Any]:
|
65 | 64 | """Run the trial by evaluating its parameterization(s).
|
@@ -110,33 +109,26 @@ def run(self, trial: BaseTrial) -> dict[str, Any]:
|
110 | 109 | )
|
111 | 110 |
|
112 | 111 | 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() |
134 | 127 |
|
135 | 128 | run_metadata = {
|
136 | 129 | "Ys": Ys,
|
137 | 130 | "Ystds": Ystds,
|
138 | 131 | "outcome_names": self.outcome_names,
|
| 132 | + "Ys_true": Ys_true, |
139 | 133 | }
|
140 |
| - if Ys_true: # only add key if we actually have a ground truth |
141 |
| - run_metadata["Ys_true"] = Ys_true |
142 | 134 | return run_metadata
|
0 commit comments