Skip to content

Commit 57ee46f

Browse files
authored
Merge pull request #258 from alan-turing-institute/gp
GP
2 parents 3706802 + 6a4ca01 commit 57ee46f

14 files changed

+777
-816
lines changed

autoemulate/emulators/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from ..model_registry import ModelRegistry
22
from .conditional_neural_process import ConditionalNeuralProcess
3+
from .gaussian_process import GaussianProcess
34
from .gaussian_process_mogp import GaussianProcessMOGP
5+
from .gaussian_process_mt import GaussianProcessMT
46
from .gaussian_process_sklearn import GaussianProcessSklearn
5-
from .gaussian_process_torch import GaussianProcessTorch
67
from .gradient_boosting import GradientBoosting
78
from .light_gbm import LightGBM
89
from .neural_net_sk import NeuralNetSk
@@ -29,13 +30,17 @@
2930
SupportVectorMachines().model_name, SupportVectorMachines, is_core=True
3031
)
3132
model_registry.register_model(
32-
GaussianProcessTorch().model_name, GaussianProcessTorch, is_core=True
33+
GaussianProcess().model_name, GaussianProcess, is_core=True
3334
)
3435
model_registry.register_model(
3536
ConditionalNeuralProcess().model_name, ConditionalNeuralProcess, is_core=True
3637
)
3738

39+
3840
# non-core models
41+
model_registry.register_model(
42+
GaussianProcessMT().model_name, GaussianProcessMT, is_core=False
43+
)
3944
model_registry.register_model(
4045
GaussianProcessSklearn().model_name, GaussianProcessSklearn, is_core=False
4146
)
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
import gpytorch
2+
import numpy as np
3+
import torch
4+
from sklearn.base import BaseEstimator
5+
from sklearn.base import RegressorMixin
6+
from sklearn.preprocessing._data import _handle_zeros_in_scale
7+
from sklearn.utils import check_array
8+
from sklearn.utils import check_X_y
9+
from sklearn.utils.validation import check_is_fitted
10+
from skorch.callbacks import LRScheduler
11+
from skorch.probabilistic import ExactGPRegressor
12+
13+
from autoemulate.emulators.gaussian_process_utils import EarlyStoppingCustom
14+
from autoemulate.emulators.gaussian_process_utils import PolyMean
15+
from autoemulate.emulators.neural_networks.gp_module import GPModule
16+
from autoemulate.utils import set_random_seed
17+
18+
19+
class GaussianProcess(RegressorMixin, BaseEstimator):
20+
"""Exact Gaussian Process emulator build with GPyTorch.
21+
22+
Batched Multi-Output GP, treating outputs independently.
23+
24+
Parameters
25+
----------
26+
mean_module : GP mean, defaults to gpytorch.means.ConstantMean() when None
27+
covar_module : GP covariance, defaults to gpytorch.kernels.RBFKernel() when None
28+
lr : learning rate, default=1e-1
29+
optimizer : optimizer, default=torch.optim.AdamW
30+
max_epochs : maximum number of epochs, default=30
31+
normalize_y : whether to normalize the target values, default=True
32+
device : device to use, defaults to "cuda" if available, otherwise "cpu"
33+
random_state : random seed, default=None
34+
"""
35+
36+
def __init__(
37+
self,
38+
# architecture
39+
mean_module=None,
40+
covar_module=None,
41+
# training
42+
lr=2e-1,
43+
optimizer=torch.optim.AdamW,
44+
max_epochs=50,
45+
normalize_y=True,
46+
# misc
47+
device="cpu",
48+
random_state=None,
49+
):
50+
self.mean_module = mean_module
51+
self.covar_module = covar_module
52+
self.lr = lr
53+
self.optimizer = optimizer
54+
self.max_epochs = max_epochs
55+
self.normalize_y = normalize_y
56+
self.device = device
57+
self.random_state = random_state
58+
59+
def _get_module(self, module, default_module, n_features, n_outputs):
60+
"""
61+
Get mean and kernel modules.
62+
63+
We can't default the modules in the constructor because 'fit' modifies them which
64+
fails scikit-learn estimator tests. Therefore, we deepcopy if module is given or return the default class
65+
if not.
66+
"""
67+
if module is None:
68+
return default_module
69+
if callable(module):
70+
# torch.Size is needed to specify the batch shape
71+
return module(n_features, torch.Size([n_outputs]))
72+
else:
73+
ValueError("module must be callable or None")
74+
75+
def fit(self, X, y):
76+
"""Fit the emulator to the data.
77+
78+
Parameters
79+
----------
80+
X : array-like of shape (n_samples, n_features)
81+
The input data.
82+
y : array-like of shape (n_samples, )
83+
The output data.
84+
Returns
85+
-------
86+
self : object
87+
Returns self.
88+
"""
89+
if self.random_state is not None:
90+
set_random_seed(self.random_state)
91+
92+
X, y = check_X_y(
93+
X,
94+
y,
95+
y_numeric=True,
96+
multi_output=True,
97+
dtype=np.float32,
98+
copy=True,
99+
ensure_2d=True,
100+
)
101+
self.y_dim_ = y.ndim
102+
self.n_features_in_ = X.shape[1]
103+
self.n_outputs_ = y.shape[1] if y.ndim > 1 else 1
104+
y = y.astype(np.float32)
105+
106+
# GP's work better when the target values are normalized
107+
if self.normalize_y:
108+
self._y_train_mean = np.mean(y, axis=0)
109+
self._y_train_std = _handle_zeros_in_scale(np.std(y, axis=0), copy=False)
110+
y = (y - self._y_train_mean) / self._y_train_std
111+
112+
# default modules
113+
default_mean_module = gpytorch.means.ConstantMean(
114+
batch_shape=torch.Size([self.n_outputs_])
115+
)
116+
117+
# combined RBF + constant kernel works well in a lot of cases
118+
rbf = gpytorch.kernels.RBFKernel(
119+
ard_num_dims=self.n_features_in_, # different lengthscale for each feature
120+
batch_shape=torch.Size([self.n_outputs_]), # batched multioutput
121+
# seems to work better when we initialize the lengthscale
122+
).initialize(lengthscale=torch.ones(self.n_features_in_) * 1.5)
123+
constant = gpytorch.kernels.ConstantKernel()
124+
combined = rbf + constant
125+
126+
default_covar_module = gpytorch.kernels.ScaleKernel(
127+
combined, batch_shape=torch.Size([self.n_outputs_])
128+
)
129+
130+
mean_module = self._get_module(
131+
self.mean_module, default_mean_module, self.n_features_in_, self.n_outputs_
132+
)
133+
covar_module = self._get_module(
134+
self.covar_module,
135+
default_covar_module,
136+
self.n_features_in_,
137+
self.n_outputs_,
138+
)
139+
140+
# wrapping in ScaleKernel is generally good, as it adds an outputscale parameter
141+
if not isinstance(covar_module, gpytorch.kernels.ScaleKernel):
142+
covar_module = gpytorch.kernels.ScaleKernel(
143+
covar_module, batch_shape=torch.Size([self.n_outputs_])
144+
)
145+
146+
# model
147+
self.model_ = ExactGPRegressor(
148+
GPModule,
149+
module__mean=mean_module,
150+
module__covar=covar_module,
151+
likelihood=gpytorch.likelihoods.MultitaskGaussianLikelihood(
152+
num_tasks=self.n_outputs_
153+
),
154+
max_epochs=self.max_epochs,
155+
lr=self.lr,
156+
optimizer=self.optimizer,
157+
callbacks=[
158+
(
159+
"lr_scheduler",
160+
LRScheduler(policy="ReduceLROnPlateau", patience=5, factor=0.5),
161+
),
162+
(
163+
"early_stopping",
164+
EarlyStoppingCustom(
165+
monitor="train_loss",
166+
patience=10,
167+
threshold=1e-3,
168+
load_best=True,
169+
),
170+
),
171+
],
172+
verbose=0,
173+
device=self.device,
174+
)
175+
self.model_.fit(X, y)
176+
self.is_fitted_ = True
177+
return self
178+
179+
def predict(self, X, return_std=False):
180+
"""Predict the output of the emulator.
181+
182+
Parameters
183+
----------
184+
X : array-like of shape (n_samples, n_features)
185+
The input data.
186+
return_std : bool, default=False
187+
Whether to return the standard deviation.
188+
189+
Returns
190+
-------
191+
y : array-like of shape (n_samples, )
192+
The predicted output.
193+
"""
194+
195+
# checks
196+
check_is_fitted(self)
197+
X = check_array(X, dtype=np.float32)
198+
199+
# predict
200+
mean, std = self.model_.predict(X, return_std=True)
201+
202+
# sklearn: regression models should return float64
203+
mean = mean.astype(np.float64)
204+
std = std.astype(np.float64)
205+
206+
# output shape should be same as input shape
207+
# when input dim is 1D, make sure output is 1D
208+
if mean.ndim == 2 and self.y_dim_ == 1:
209+
mean = mean.squeeze()
210+
std = std.squeeze()
211+
212+
# undo normalization
213+
if self.normalize_y:
214+
mean = mean * self._y_train_std + self._y_train_mean
215+
std = std * self._y_train_std
216+
217+
if return_std:
218+
return mean, std
219+
return mean
220+
221+
def get_grid_params(self, search_type="random"):
222+
"""Returns the grid parameters for the emulator."""
223+
224+
def rbf(n_features, n_outputs):
225+
return gpytorch.kernels.RBFKernel(
226+
ard_num_dims=n_features,
227+
batch_shape=n_outputs,
228+
).initialize(lengthscale=torch.ones(n_features) * 1.5)
229+
230+
def matern_5_2_kernel(n_features, n_outputs):
231+
return gpytorch.kernels.MaternKernel(
232+
nu=2.5,
233+
ard_num_dims=n_features,
234+
batch_shape=n_outputs,
235+
)
236+
237+
def matern_3_2_kernel(n_features, n_outputs):
238+
return gpytorch.kernels.MaternKernel(
239+
nu=1.5,
240+
ard_num_dims=n_features,
241+
batch_shape=n_outputs,
242+
)
243+
244+
def rq_kernel(n_features, n_outputs):
245+
return gpytorch.kernels.RQKernel(
246+
ard_num_dims=n_features,
247+
batch_shape=n_outputs,
248+
)
249+
250+
def rbf_plus_constant(n_features, n_outputs):
251+
return (
252+
gpytorch.kernels.RBFKernel(
253+
ard_num_dims=n_features,
254+
batch_shape=n_outputs,
255+
).initialize(lengthscale=torch.ones(n_features) * 1.5)
256+
+ gpytorch.kernels.ConstantKernel()
257+
)
258+
259+
# combinations
260+
def rbf_plus_linear(n_features, n_outputs):
261+
return gpytorch.kernels.RBFKernel(
262+
ard_num_dims=n_features,
263+
batch_shape=n_outputs,
264+
) + gpytorch.kernels.LinearKernel(
265+
ard_num_dims=n_features,
266+
batch_shape=n_outputs,
267+
)
268+
269+
def matern_5_2_plus_rq(n_features, n_outputs):
270+
return gpytorch.kernels.MaternKernel(
271+
nu=2.5,
272+
ard_num_dims=n_features,
273+
batch_shape=n_outputs,
274+
) + gpytorch.kernels.RQKernel(
275+
ard_num_dims=n_features,
276+
batch_shape=n_outputs,
277+
)
278+
279+
def rbf_times_linear(n_features, n_outputs):
280+
return gpytorch.kernels.RBFKernel(
281+
ard_num_dims=n_features,
282+
batch_shape=n_outputs,
283+
) * gpytorch.kernels.LinearKernel(
284+
ard_num_dims=n_features,
285+
batch_shape=n_outputs,
286+
)
287+
288+
# means
289+
def constant_mean(n_features, n_outputs):
290+
return gpytorch.means.ConstantMean(batch_shape=n_outputs)
291+
292+
def zero_mean(n_features, n_outputs):
293+
return gpytorch.means.ZeroMean(batch_shape=n_outputs)
294+
295+
def linear_mean(n_features, n_outputs):
296+
return gpytorch.means.LinearMean(
297+
input_size=n_features, batch_shape=n_outputs
298+
)
299+
300+
def poly_mean(n_features, n_outputs):
301+
return PolyMean(degree=2, input_size=n_features, batch_shape=n_outputs)
302+
303+
if search_type == "random":
304+
param_space = {
305+
"covar_module": [
306+
rbf,
307+
matern_5_2_kernel,
308+
matern_3_2_kernel,
309+
rq_kernel,
310+
rbf_plus_constant,
311+
rbf_plus_linear,
312+
rbf_times_linear,
313+
matern_5_2_plus_rq,
314+
],
315+
"mean_module": [
316+
constant_mean,
317+
zero_mean,
318+
linear_mean,
319+
poly_mean,
320+
],
321+
"optimizer": [torch.optim.AdamW, torch.optim.Adam],
322+
"lr": [5e-1, 1e-1, 5e-2, 1e-2],
323+
"max_epochs": [
324+
50,
325+
100,
326+
200,
327+
],
328+
}
329+
else:
330+
raise ValueError("search_type must be 'random'")
331+
332+
return param_space
333+
334+
@property
335+
def model_name(self):
336+
return self.__class__.__name__
337+
338+
def _more_tags(self):
339+
# TODO: is it really non-deterministic?
340+
return {"multioutput": True, "non_deterministic": True}

autoemulate/emulators/gaussian_process_torch.py renamed to autoemulate/emulators/gaussian_process_mt.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
from autoemulate.utils import set_random_seed
1717

1818

19-
class GaussianProcessTorch(RegressorMixin, BaseEstimator):
20-
"""Exact Gaussian Process emulator build with GPyTorch.
19+
class GaussianProcessMT(RegressorMixin, BaseEstimator):
20+
"""Exact Multi-task Gaussian Process emulator build with GPyTorch. This
21+
emulator is useful to model correlated outputs.
2122
2223
Parameters
2324
----------

0 commit comments

Comments
 (0)