Skip to content

Commit 5252709

Browse files
committed
Implement Surrogate class for fitting a GP on existing data from optimizations
1 parent 2f39aa5 commit 5252709

File tree

3 files changed

+462
-0
lines changed

3 files changed

+462
-0
lines changed

CADETProcess/optimization/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
from .cache import *
100100
from .results import *
101101
from .optimizationProblem import *
102+
from .surrogate import *
102103
from .parallelizationBackend import *
103104
from .optimizer import *
104105
from .scipyAdapter import COBYLA, TrustConstr, NelderMead, SLSQP
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
from functools import wraps
2+
from typing import Any, NoReturn
3+
4+
import numpy as np
5+
from sklearn.gaussian_process import GaussianProcessRegressor
6+
from sklearn.preprocessing import StandardScaler
7+
8+
from CADETProcess.dataStructure import Structure, Typed
9+
from CADETProcess.optimization import Population
10+
11+
12+
class SurrogateModel(Structure):
13+
"""
14+
Surrogate model for an evaluated population.
15+
16+
Attributes
17+
----------
18+
population : Population
19+
A population containing evaluated individuals.
20+
"""
21+
22+
population = Typed(ty=Population)
23+
24+
def __init__(
25+
self,
26+
population: Population,
27+
*args, **kwargs
28+
) -> NoReturn:
29+
"""
30+
Initialize the Surrogate Model class.
31+
32+
Parameters
33+
----------
34+
population : Population
35+
A population containing evaluated individuals.
36+
"""
37+
super().__init__(*args, population=population, **kwargs)
38+
39+
self.surrogates: dict[str, dict] = {}
40+
for eval_fun in [
41+
'objectives',
42+
'nonlinear_constraints',
43+
'nonlinear_constraints_violation',
44+
'meta_scores',
45+
]:
46+
self.surrogates[eval_fun] = {}
47+
self.surrogates[eval_fun]['gp']: GaussianProcessRegressor = None
48+
self.surrogates[eval_fun]['x_scaler']: StandardScaler = None
49+
self.surrogates[eval_fun]['y_scaler']: StandardScaler = None
50+
51+
self._update_surrogate_models()
52+
53+
def train_gp(
54+
self,
55+
X: np.ndarray,
56+
Y: np.ndarray
57+
) -> tuple[GaussianProcessRegressor, StandardScaler, StandardScaler]:
58+
"""
59+
Fit a Gaussian Process on scaled input and output.
60+
61+
Parameters
62+
----------
63+
X : np.ndarray
64+
Feature vectors of training data (also required for prediction).
65+
Y : np.ndarray
66+
Target values in training data (also required for prediction).
67+
68+
Returns
69+
-------
70+
tuple[GaussianProcessRegressor, StandardScaler, StandardScaler]
71+
A tuple containing the gaussian process regressor, as well as scalers for
72+
input and output dimensions.
73+
74+
"""
75+
X_scaler = StandardScaler().fit(X)
76+
Y_scaler = StandardScaler().fit(Y)
77+
78+
gpr = GaussianProcessRegressor()
79+
gpr.fit(X=X_scaler.transform(X), y=Y_scaler.transform(Y))
80+
81+
return gpr, X_scaler, Y_scaler
82+
83+
def _update_eval_fun_surrogate(
84+
self,
85+
eval_fun: str,
86+
surrogate: GaussianProcessRegressor,
87+
x_scaler: StandardScaler,
88+
y_scaler: StandardScaler
89+
) -> NoReturn:
90+
self.surrogates[eval_fun]['surrogate'] = surrogate
91+
self.surrogates[eval_fun]['x_scaler'] = x_scaler
92+
self.surrogates[eval_fun]['y_scaler'] = y_scaler
93+
94+
def _evaluate_surrogate(
95+
self,
96+
eval_fun: str,
97+
X: np.ndarray,
98+
return_std: bool = False,
99+
return_cov: bool = False,
100+
) -> np.ndarray:
101+
if return_std and return_cov:
102+
raise RuntimeError(
103+
"At most one of return_std or return_cov can be requested."
104+
)
105+
106+
surrogate = self.surrogates[eval_fun]['surrogate']
107+
x_scaler = self.surrogates[eval_fun]['x_scaler']
108+
y_scaler = self.surrogates[eval_fun]['y_scaler']
109+
110+
X_scaled = x_scaler.transform(X)
111+
112+
if return_std:
113+
_, Y_std_scaled = surrogate.predict(
114+
X_scaled, return_std=True
115+
)
116+
Y_std = Y_std_scaled * y_scaler.scale_
117+
return np.array(Y_std, ndmin=2)
118+
elif return_cov:
119+
_, Y_cov_scaled = surrogate.predict(
120+
X_scaled, return_cov=True
121+
)
122+
Y_cov = Y_cov_scaled * (y_scaler.scale_ ** 2)
123+
return np.array(Y_cov, ndmin=2)
124+
else:
125+
Y_scaled = surrogate.predict(X_scaled)
126+
Y = y_scaler.inverse_transform(np.array(Y_scaled, ndmin=2))
127+
return Y
128+
129+
def _update_surrogate_models(self) -> NoReturn:
130+
if self.population.n_f > 0:
131+
surrogate, x_scaler, y_scaler = self.train_gp(
132+
self.population.x, self.population.f
133+
)
134+
self._update_eval_fun_surrogate(
135+
'objectives', surrogate, x_scaler, y_scaler
136+
)
137+
if self.population.n_g > 0:
138+
surrogate, x_scaler, y_scaler = self.train_gp(
139+
self.population.x, self.population.g
140+
)
141+
self._update_eval_fun_surrogate(
142+
'nonlinear_constraints', surrogate, x_scaler, y_scaler
143+
)
144+
if self.population.n_g > 0:
145+
surrogate, x_scaler, y_scaler = self.train_gp(
146+
self.population.x, self.population.cv
147+
)
148+
self._update_eval_fun_surrogate(
149+
'nonlinear_constraints_violation', surrogate, x_scaler, y_scaler
150+
)
151+
if self.population.n_m > 0:
152+
surrogate, x_scaler, y_scaler = self.train_gp(
153+
self.population.x, self.population.m
154+
)
155+
self._update_eval_fun_surrogate(
156+
'meta_scores', surrogate, x_scaler, y_scaler
157+
)
158+
159+
def update(self, population: Population) -> NoReturn:
160+
"""
161+
Update the surrogate model with new population.
162+
163+
Parameters
164+
----------
165+
population : Population
166+
New population entries.
167+
"""
168+
self.population.update(population)
169+
self._update_surrogate_models()
170+
171+
def ensures2d(func):
172+
"""Decorate function to ensure X array is an ndarray with ndmin=2."""
173+
@wraps(func)
174+
def wrapper(
175+
self,
176+
X: np.ndarray,
177+
*args, **kwargs
178+
) -> Any:
179+
180+
X = np.array(X)
181+
X_2d = np.array(X, ndmin=2)
182+
183+
Y = func(self, X_2d, *args, **kwargs)
184+
Y_2d = Y.reshape((len(X_2d), -1))
185+
186+
# return an individual or a population depending on the length of X
187+
if X.ndim == 1:
188+
return Y_2d[0]
189+
else:
190+
return Y_2d
191+
192+
return wrapper
193+
194+
@ensures2d
195+
def estimate_objectives(self, X: np.ndarray) -> np.ndarray:
196+
"""
197+
Estimate the objective function values using the surrogate model.
198+
199+
Parameters
200+
----------
201+
X : np.ndarray
202+
The input samples.
203+
204+
Returns
205+
-------
206+
np.ndarray
207+
The estimated objective function values.
208+
"""
209+
return self._evaluate_surrogate('objectives', X)
210+
211+
@ensures2d
212+
def estimate_objectives_standard_deviation(self, X: np.ndarray) -> np.ndarray:
213+
"""
214+
Estimate the standard deviation of the objective function values.
215+
216+
Parameters
217+
----------
218+
X : np.ndarray
219+
The input samples.
220+
221+
Returns
222+
-------
223+
np.ndarray
224+
The standard deviation of the estimated objective function values.
225+
"""
226+
return self._evaluate_surrogate('objectives', X, return_std=True)
227+
228+
@ensures2d
229+
def estimate_nonlinear_constraints(self, X: np.ndarray) -> np.ndarray:
230+
"""
231+
Estimate the nonlinear constraint function values using the surrogate model.
232+
233+
Parameters
234+
----------
235+
X : np.ndarray
236+
The input samples.
237+
238+
Returns
239+
-------
240+
np.ndarray
241+
The estimated nonlinear constraints function values.
242+
"""
243+
return self._evaluate_surrogate('nonlinear_constraints', X)
244+
245+
@ensures2d
246+
def estimate_nonlinear_constraints_standard_deviation(
247+
self,
248+
X: np.ndarray
249+
) -> np.ndarray:
250+
"""
251+
Get the standard deviation of the estimated nonlinear constraint function.
252+
253+
Parameters
254+
----------
255+
X : np.ndarray
256+
The input samples.
257+
258+
Returns
259+
-------
260+
np.ndarray
261+
The standard deviation of the estimated nonlinear constraint function.
262+
"""
263+
return self._evaluate_surrogate('nonlinear_constraints', X, return_std=True)
264+
265+
@ensures2d
266+
def estimate_nonlinear_constraints_violation(self, X: np.ndarray) -> np.ndarray:
267+
"""
268+
Estimate the nonlinear constraints function violation using the surrogate model.
269+
270+
Parameters
271+
----------
272+
X : np.ndarray
273+
The input samples.
274+
275+
Returns
276+
-------
277+
out : np.ndarray
278+
The estimated nonlinear constraints violation function values.
279+
"""
280+
return self._evaluate_surrogate('nonlinear_constraints_violation', X)
281+
282+
@ensures2d
283+
def estimate_nonlinear_constraints_violation_standard_deviation(
284+
self,
285+
X: np.ndarray
286+
) -> np.ndarray:
287+
"""
288+
Get the standard deviation of the estimated nonlinear constraint violation function.
289+
290+
Parameters
291+
----------
292+
X : np.ndarray
293+
The input samples.
294+
295+
Returns
296+
-------
297+
np.ndarray
298+
The standard deviation of the estimated nonlinear constraint violation function.
299+
"""
300+
return self._evaluate_surrogate(
301+
'nonlinear_constraints_violation',
302+
X,
303+
return_std=True
304+
)
305+
306+
@ensures2d
307+
def estimate_check_nonlinear_constraints(self, X: np.ndarray) -> np.array:
308+
"""
309+
Estimate if nonlinear constraints were violated.
310+
311+
Parameters
312+
----------
313+
X : np.ndarray
314+
The input samples.
315+
316+
Returns
317+
-------
318+
np.array
319+
Boolean array indicating if X were valid, based on nonlinear constraint
320+
violation.
321+
"""
322+
CV = self.estimate_nonlinear_constraints_violation(X)
323+
return np.all(CV < 0, axis=1, keepdims=True)
324+
325+
@ensures2d
326+
def estimate_meta_scores(self, X: np.ndarray) -> np.ndarray:
327+
"""
328+
Estimate the meta scores using the surrogate model.
329+
330+
Parameters
331+
----------
332+
X : np.ndarray
333+
The input samples.
334+
335+
Returns
336+
-------
337+
np.ndarray
338+
The estimated meta scores.
339+
"""
340+
return self._evaluate_surrogate('meta_scores', X)
341+
342+
@ensures2d
343+
def estimate_meta_scores_standard_deviation(
344+
self,
345+
X: np.ndarray
346+
) -> np.ndarray:
347+
"""
348+
Get the standard deviation of the estimated meta scores.
349+
350+
Parameters
351+
----------
352+
X : np.ndarray
353+
The input samples.
354+
355+
Returns
356+
-------
357+
np.ndarray
358+
The standard deviation of the estimated meta scores.
359+
"""
360+
return self._evaluate_surrogate('meta_scores', X, return_std=True)

0 commit comments

Comments
 (0)