ennbo 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- enn/enn/enn.py +71 -31
- enn/enn/enn_fit.py +26 -24
- enn/enn/enn_normal.py +3 -2
- enn/enn/enn_params.py +13 -0
- enn/enn/enn_util.py +40 -12
- enn/turbo/base_turbo_impl.py +53 -7
- enn/turbo/lhd_only_impl.py +7 -0
- enn/turbo/morbo_trust_region.py +189 -0
- enn/turbo/no_trust_region.py +65 -0
- enn/turbo/proposal.py +11 -2
- enn/turbo/turbo_config.py +48 -4
- enn/turbo/turbo_enn_impl.py +46 -21
- enn/turbo/turbo_gp.py +9 -1
- enn/turbo/turbo_mode_impl.py +11 -2
- enn/turbo/turbo_one_impl.py +163 -24
- enn/turbo/turbo_optimizer.py +246 -58
- enn/turbo/turbo_trust_region.py +8 -10
- enn/turbo/turbo_utils.py +116 -26
- enn/turbo/turbo_zero_impl.py +5 -0
- {ennbo-0.1.0.dist-info → ennbo-0.1.2.dist-info}/METADATA +5 -4
- ennbo-0.1.2.dist-info/RECORD +29 -0
- ennbo-0.1.0.dist-info/RECORD +0 -27
- {ennbo-0.1.0.dist-info → ennbo-0.1.2.dist-info}/WHEEL +0 -0
- {ennbo-0.1.0.dist-info → ennbo-0.1.2.dist-info}/licenses/LICENSE +0 -0
enn/turbo/turbo_one_impl.py
CHANGED
|
@@ -7,33 +7,85 @@ if TYPE_CHECKING:
|
|
|
7
7
|
from numpy.random import Generator
|
|
8
8
|
|
|
9
9
|
from .base_turbo_impl import BaseTurboImpl
|
|
10
|
-
from .turbo_config import
|
|
10
|
+
from .turbo_config import TurboOneConfig
|
|
11
11
|
from .turbo_utils import gp_thompson_sample
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class TurboOneImpl(BaseTurboImpl):
|
|
15
|
-
def __init__(self, config:
|
|
15
|
+
def __init__(self, config: TurboOneConfig) -> None:
|
|
16
16
|
super().__init__(config)
|
|
17
17
|
self._gp_model: Any | None = None
|
|
18
|
-
self._gp_y_mean: float = 0.0
|
|
19
|
-
self._gp_y_std: float = 1.0
|
|
18
|
+
self._gp_y_mean: float | Any = 0.0
|
|
19
|
+
self._gp_y_std: float | Any = 1.0
|
|
20
20
|
self._fitted_n_obs: int = 0
|
|
21
21
|
|
|
22
|
+
def _as_2d(self, a: np.ndarray) -> np.ndarray:
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
a = np.asarray(a, dtype=float)
|
|
26
|
+
if a.ndim == 1:
|
|
27
|
+
return a.reshape(-1, 1)
|
|
28
|
+
if a.ndim == 2:
|
|
29
|
+
return a.T
|
|
30
|
+
raise ValueError(a.shape)
|
|
31
|
+
|
|
32
|
+
def _broadcast_gp_mean_std(self, num_metrics: int) -> tuple[np.ndarray, np.ndarray]:
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
num_metrics = int(num_metrics)
|
|
36
|
+
if num_metrics <= 0:
|
|
37
|
+
raise ValueError(num_metrics)
|
|
38
|
+
gp_y_mean = np.asarray(self._gp_y_mean, dtype=float).reshape(-1)
|
|
39
|
+
gp_y_std = np.asarray(self._gp_y_std, dtype=float).reshape(-1)
|
|
40
|
+
if gp_y_mean.size == 1 and num_metrics != 1:
|
|
41
|
+
gp_y_mean = np.full(num_metrics, float(gp_y_mean[0]), dtype=float)
|
|
42
|
+
if gp_y_std.size == 1 and num_metrics != 1:
|
|
43
|
+
gp_y_std = np.full(num_metrics, float(gp_y_std[0]), dtype=float)
|
|
44
|
+
if gp_y_mean.shape != (num_metrics,) or gp_y_std.shape != (num_metrics,):
|
|
45
|
+
raise ValueError((gp_y_mean.shape, gp_y_std.shape, num_metrics))
|
|
46
|
+
return gp_y_mean, gp_y_std
|
|
47
|
+
|
|
48
|
+
def _unstandardize(self, y_std_2d: np.ndarray) -> np.ndarray:
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
y_std_2d = np.asarray(y_std_2d, dtype=float)
|
|
52
|
+
if y_std_2d.ndim != 2:
|
|
53
|
+
raise ValueError(y_std_2d.shape)
|
|
54
|
+
num_metrics = int(y_std_2d.shape[1])
|
|
55
|
+
gp_y_mean, gp_y_std = self._broadcast_gp_mean_std(num_metrics)
|
|
56
|
+
return gp_y_mean.reshape(1, -1) + gp_y_std.reshape(1, -1) * y_std_2d
|
|
57
|
+
|
|
22
58
|
def get_x_center(
|
|
23
59
|
self,
|
|
24
60
|
x_obs_list: list,
|
|
25
61
|
y_obs_list: list,
|
|
26
62
|
rng: Generator,
|
|
63
|
+
tr_state: Any = None,
|
|
27
64
|
) -> np.ndarray | None:
|
|
28
65
|
import numpy as np
|
|
29
66
|
import torch
|
|
67
|
+
import warnings
|
|
30
68
|
|
|
31
69
|
from .turbo_utils import argmax_random_tie
|
|
32
70
|
|
|
33
71
|
if len(y_obs_list) == 0:
|
|
34
72
|
return None
|
|
35
73
|
if self._gp_model is None:
|
|
36
|
-
|
|
74
|
+
if len(y_obs_list) <= 1:
|
|
75
|
+
x_array = np.asarray(x_obs_list, dtype=float)
|
|
76
|
+
y_array = np.asarray(y_obs_list, dtype=float)
|
|
77
|
+
if y_array.ndim == 2:
|
|
78
|
+
if self._config.tr_type == "morbo" and tr_state is not None:
|
|
79
|
+
scores = tr_state.scalarize(y_array, clip=True)
|
|
80
|
+
else:
|
|
81
|
+
scores = y_array[:, 0]
|
|
82
|
+
best_idx = argmax_random_tie(scores, rng=rng)
|
|
83
|
+
return x_array[best_idx]
|
|
84
|
+
return super().get_x_center(x_obs_list, y_obs_list, rng, tr_state)
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
"TurboOneImpl.get_x_center requires a fitted GP model for 2+ observations; "
|
|
87
|
+
"call prepare_ask() first."
|
|
88
|
+
)
|
|
37
89
|
if self._fitted_n_obs != len(x_obs_list):
|
|
38
90
|
raise RuntimeError(
|
|
39
91
|
f"GP fitted on {self._fitted_n_obs} obs but get_x_center called with {len(x_obs_list)}"
|
|
@@ -41,11 +93,38 @@ class TurboOneImpl(BaseTurboImpl):
|
|
|
41
93
|
|
|
42
94
|
x_array = np.asarray(x_obs_list, dtype=float)
|
|
43
95
|
x_torch = torch.as_tensor(x_array, dtype=torch.float64)
|
|
96
|
+
try:
|
|
97
|
+
from gpytorch.utils.warnings import GPInputWarning
|
|
98
|
+
except Exception: # pragma: no cover
|
|
99
|
+
GPInputWarning = None
|
|
100
|
+
|
|
44
101
|
with torch.no_grad():
|
|
45
|
-
|
|
46
|
-
|
|
102
|
+
if GPInputWarning is None:
|
|
103
|
+
posterior = self._gp_model.posterior(x_torch)
|
|
104
|
+
else:
|
|
105
|
+
# We intentionally evaluate the GP posterior at the training inputs
|
|
106
|
+
# (observed points) when choosing the center. GPyTorch warns about this
|
|
107
|
+
# in debug mode, but it's expected for our usage.
|
|
108
|
+
with warnings.catch_warnings():
|
|
109
|
+
warnings.filterwarnings(
|
|
110
|
+
"ignore",
|
|
111
|
+
message=r"The input matches the stored training data\..*",
|
|
112
|
+
category=GPInputWarning,
|
|
113
|
+
)
|
|
114
|
+
posterior = self._gp_model.posterior(x_torch)
|
|
115
|
+
mu_std = posterior.mean.cpu().numpy()
|
|
116
|
+
|
|
117
|
+
mu = self._unstandardize(self._as_2d(mu_std))
|
|
118
|
+
|
|
119
|
+
# For morbo: scalarize mu values
|
|
120
|
+
if self._config.tr_type == "morbo" and tr_state is not None:
|
|
121
|
+
scalarized = tr_state.scalarize(mu, clip=False)
|
|
122
|
+
best_idx = argmax_random_tie(scalarized, rng=rng)
|
|
123
|
+
else:
|
|
124
|
+
if mu.shape[1] != 1:
|
|
125
|
+
raise ValueError(mu.shape)
|
|
126
|
+
best_idx = argmax_random_tie(mu[:, 0], rng=rng)
|
|
47
127
|
|
|
48
|
-
best_idx = argmax_random_tie(mu, rng=rng)
|
|
49
128
|
return x_array[best_idx]
|
|
50
129
|
|
|
51
130
|
def needs_tr_list(self) -> bool:
|
|
@@ -56,7 +135,7 @@ class TurboOneImpl(BaseTurboImpl):
|
|
|
56
135
|
num_arms: int,
|
|
57
136
|
x_obs_list: list,
|
|
58
137
|
draw_initial_fn: Callable[[int], np.ndarray],
|
|
59
|
-
get_init_lhd_points_fn: Callable[[int], np.ndarray
|
|
138
|
+
get_init_lhd_points_fn: Callable[[int], np.ndarray],
|
|
60
139
|
) -> np.ndarray | None:
|
|
61
140
|
if len(x_obs_list) == 0:
|
|
62
141
|
return get_init_lhd_points_fn(num_arms)
|
|
@@ -102,18 +181,22 @@ class TurboOneImpl(BaseTurboImpl):
|
|
|
102
181
|
self._gp_y_mean = gp_y_mean_fitted
|
|
103
182
|
if gp_y_std_fitted is not None:
|
|
104
183
|
self._gp_y_std = gp_y_std_fitted
|
|
105
|
-
|
|
184
|
+
lengthscales = None
|
|
106
185
|
if self._gp_model is not None:
|
|
107
|
-
|
|
186
|
+
lengthscale = (
|
|
108
187
|
self._gp_model.covar_module.base_kernel.lengthscale.cpu()
|
|
109
188
|
.detach()
|
|
110
189
|
.numpy()
|
|
111
|
-
.ravel()
|
|
112
190
|
)
|
|
191
|
+
if lengthscale.ndim == 3:
|
|
192
|
+
lengthscale = lengthscale.mean(axis=0)
|
|
193
|
+
lengthscales = lengthscale.ravel()
|
|
113
194
|
# First line helps stabilize second line.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
195
|
+
lengthscales = lengthscales / lengthscales.mean()
|
|
196
|
+
lengthscales = lengthscales / np.prod(
|
|
197
|
+
np.power(lengthscales, 1.0 / len(lengthscales))
|
|
198
|
+
)
|
|
199
|
+
return self._gp_model, gp_y_mean_fitted, gp_y_std_fitted, lengthscales
|
|
117
200
|
|
|
118
201
|
def select_candidates(
|
|
119
202
|
self,
|
|
@@ -123,17 +206,63 @@ class TurboOneImpl(BaseTurboImpl):
|
|
|
123
206
|
rng: Generator,
|
|
124
207
|
fallback_fn: Callable[[np.ndarray, int], np.ndarray],
|
|
125
208
|
from_unit_fn: Callable[[np.ndarray], np.ndarray],
|
|
209
|
+
tr_state: Any = None,
|
|
126
210
|
) -> np.ndarray:
|
|
211
|
+
import numpy as np
|
|
212
|
+
|
|
127
213
|
if self._gp_model is None:
|
|
214
|
+
if self._fitted_n_obs >= 2:
|
|
215
|
+
raise RuntimeError(
|
|
216
|
+
"TurboOneImpl.select_candidates requires a fitted GP model for 2+ observations; "
|
|
217
|
+
"call prepare_ask() first."
|
|
218
|
+
)
|
|
128
219
|
return fallback_fn(x_cand, num_arms)
|
|
129
220
|
|
|
221
|
+
if self._config.tr_type == "morbo" and tr_state is not None:
|
|
222
|
+
import gpytorch
|
|
223
|
+
import torch
|
|
224
|
+
|
|
225
|
+
from .turbo_utils import torch_seed_context
|
|
226
|
+
|
|
227
|
+
x_torch = torch.as_tensor(x_cand, dtype=torch.float64)
|
|
228
|
+
seed = int(rng.integers(2**31 - 1))
|
|
229
|
+
with (
|
|
230
|
+
torch.no_grad(),
|
|
231
|
+
gpytorch.settings.fast_pred_var(),
|
|
232
|
+
torch_seed_context(seed, device=x_torch.device),
|
|
233
|
+
):
|
|
234
|
+
posterior = self._gp_model.posterior(x_torch)
|
|
235
|
+
samples = posterior.sample(sample_shape=torch.Size([1]))
|
|
236
|
+
|
|
237
|
+
if samples.ndim == 2:
|
|
238
|
+
samples_std = samples[0].detach().cpu().numpy().reshape(-1, 1)
|
|
239
|
+
elif samples.ndim == 3:
|
|
240
|
+
samples_std = samples[0].detach().cpu().numpy().T
|
|
241
|
+
else:
|
|
242
|
+
raise ValueError(samples.shape)
|
|
243
|
+
|
|
244
|
+
y_samples = self._unstandardize(samples_std)
|
|
245
|
+
scores = tr_state.scalarize(y_samples, clip=False)
|
|
246
|
+
shuffled_indices = rng.permutation(len(scores))
|
|
247
|
+
shuffled_scores = scores[shuffled_indices]
|
|
248
|
+
top_k_in_shuffled = np.argpartition(-shuffled_scores, num_arms - 1)[
|
|
249
|
+
:num_arms
|
|
250
|
+
]
|
|
251
|
+
idx = shuffled_indices[top_k_in_shuffled]
|
|
252
|
+
return from_unit_fn(x_cand[idx])
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
np.asarray(self._gp_y_mean).ndim != 0
|
|
256
|
+
or np.asarray(self._gp_y_std).ndim != 0
|
|
257
|
+
):
|
|
258
|
+
raise ValueError("multi-output GP requires tr_type='morbo'")
|
|
130
259
|
idx = gp_thompson_sample(
|
|
131
260
|
self._gp_model,
|
|
132
261
|
x_cand,
|
|
133
262
|
num_arms,
|
|
134
263
|
rng,
|
|
135
|
-
self._gp_y_mean,
|
|
136
|
-
self._gp_y_std,
|
|
264
|
+
float(self._gp_y_mean),
|
|
265
|
+
float(self._gp_y_std),
|
|
137
266
|
)
|
|
138
267
|
return from_unit_fn(x_cand[idx])
|
|
139
268
|
|
|
@@ -141,12 +270,18 @@ class TurboOneImpl(BaseTurboImpl):
|
|
|
141
270
|
import torch
|
|
142
271
|
|
|
143
272
|
if self._gp_model is None:
|
|
144
|
-
|
|
273
|
+
raise RuntimeError(
|
|
274
|
+
"TurboOneImpl.estimate_y requires a fitted GP model; call prepare_ask() first."
|
|
275
|
+
)
|
|
145
276
|
x_torch = torch.as_tensor(x_unit, dtype=torch.float64)
|
|
146
277
|
with torch.no_grad():
|
|
147
278
|
posterior = self._gp_model.posterior(x_torch)
|
|
148
|
-
|
|
149
|
-
|
|
279
|
+
mu_std = posterior.mean.cpu().numpy()
|
|
280
|
+
|
|
281
|
+
mu = self._unstandardize(self._as_2d(mu_std))
|
|
282
|
+
if mu.shape[1] == 1:
|
|
283
|
+
return mu[:, 0]
|
|
284
|
+
return mu
|
|
150
285
|
|
|
151
286
|
def get_mu_sigma(self, x_unit: np.ndarray) -> tuple[np.ndarray, np.ndarray] | None:
|
|
152
287
|
import torch
|
|
@@ -156,8 +291,12 @@ class TurboOneImpl(BaseTurboImpl):
|
|
|
156
291
|
x_torch = torch.as_tensor(x_unit, dtype=torch.float64)
|
|
157
292
|
with torch.no_grad():
|
|
158
293
|
posterior = self._gp_model.posterior(x_torch)
|
|
159
|
-
mu_std = posterior.mean.cpu().numpy()
|
|
160
|
-
sigma_std = posterior.variance.cpu().numpy()
|
|
161
|
-
|
|
162
|
-
|
|
294
|
+
mu_std = posterior.mean.cpu().numpy()
|
|
295
|
+
sigma_std = posterior.variance.cpu().numpy() ** 0.5
|
|
296
|
+
|
|
297
|
+
mu_std_2d = self._as_2d(mu_std)
|
|
298
|
+
sigma_std_2d = self._as_2d(sigma_std)
|
|
299
|
+
mu = self._unstandardize(mu_std_2d)
|
|
300
|
+
_gp_y_mean, gp_y_std = self._broadcast_gp_mean_std(int(mu_std_2d.shape[1]))
|
|
301
|
+
sigma = gp_y_std.reshape(1, -1) * sigma_std_2d
|
|
163
302
|
return mu, sigma
|