surrogates_interface 4.0.1__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.
- surrogates_interface/__init__.py +3 -0
- surrogates_interface/domains.py +107 -0
- surrogates_interface/openmdao.py +389 -0
- surrogates_interface/surrogates.py +1027 -0
- surrogates_interface/test/__init__.py +0 -0
- surrogates_interface/test/test_domains.py +44 -0
- surrogates_interface/test/test_init.py +12 -0
- surrogates_interface/test/test_pca_jacobian.py +137 -0
- surrogates_interface/test/test_smt.py +258 -0
- surrogates_interface/test/test_surrogates.py +22 -0
- surrogates_interface/test/test_tensorflow_and_openmdao.py +1220 -0
- surrogates_interface/test/test_transformers.py +617 -0
- surrogates_interface/transformers.py +816 -0
- surrogates_interface-4.0.1.dist-info/LICENSE +25 -0
- surrogates_interface-4.0.1.dist-info/METADATA +66 -0
- surrogates_interface-4.0.1.dist-info/RECORD +17 -0
- surrogates_interface-4.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module provides a model-agnostic interface to surrogate models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pickle
|
|
7
|
+
import warnings
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
from enum import IntEnum, auto
|
|
10
|
+
|
|
11
|
+
import h5py
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from surrogates_interface.domains import * # noqa: F401,F403
|
|
15
|
+
from surrogates_interface.transformers import * # noqa: F401,F403
|
|
16
|
+
|
|
17
|
+
# String type for h5py.
|
|
18
|
+
H5_STR = h5py.special_dtype(vlen=str)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ADMode(IntEnum):
|
|
22
|
+
"""
|
|
23
|
+
Enumeration used to select if the automatic differentiation must be applied in forward or reverse mode.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
#: Forward mode automatic differentiation.
|
|
27
|
+
FWD = auto()
|
|
28
|
+
#: Reverse mode automatic differentiation.
|
|
29
|
+
REV = auto()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SurrogateModel:
|
|
33
|
+
"""
|
|
34
|
+
Class that stores and evaluates an existing surrogate model.
|
|
35
|
+
|
|
36
|
+
This class cannot be used directly, because some of its functions are abstract.
|
|
37
|
+
For a TensorFlow model, the user should use `TensorFlowModel`.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
model : -
|
|
42
|
+
Surrogate model.
|
|
43
|
+
input_transformers : list-like of Transformer, optional
|
|
44
|
+
List of input transformers, which will be applied from the first till the last.
|
|
45
|
+
output_transformers : list-like of Transformer, optional
|
|
46
|
+
List of output transformers, which will be applied from the first till the last.
|
|
47
|
+
n_inputs : int
|
|
48
|
+
Number of inputs (that is, input features).
|
|
49
|
+
input_names : list-like of str, optional
|
|
50
|
+
Names of the inputs.
|
|
51
|
+
n_outputs : int
|
|
52
|
+
Number of outputs (that is, output features).
|
|
53
|
+
output_names : list-like of str, optional
|
|
54
|
+
Names of the outputs.
|
|
55
|
+
domain : GenericDomain
|
|
56
|
+
Input domain of the training data.
|
|
57
|
+
metadata : dict of array-like, optional
|
|
58
|
+
Metadata associated to the surrogate model.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
model,
|
|
64
|
+
input_transformers=(),
|
|
65
|
+
output_transformers=(),
|
|
66
|
+
input_names=(),
|
|
67
|
+
output_names=(),
|
|
68
|
+
n_inputs=None,
|
|
69
|
+
n_outputs=None,
|
|
70
|
+
domain=None,
|
|
71
|
+
metadata={},
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
model : -
|
|
79
|
+
Surrogate model.
|
|
80
|
+
input_transformers : list-like of Transformer, optional
|
|
81
|
+
List of input transformers, which will be applied from the first till the last.
|
|
82
|
+
output_transformers : list-like of Transformer, optional
|
|
83
|
+
List of output transformers, which will be applied from the first till the last.
|
|
84
|
+
input_names : list-like of str, optional
|
|
85
|
+
Names of the input channels.
|
|
86
|
+
If not provided, the inputs will be named `x_0`, `x_1`, ..., `x_n_inputs`.
|
|
87
|
+
output_names : list-like of str, optional
|
|
88
|
+
Names of the output channels.
|
|
89
|
+
If not provided, the outputs will be named `y_0`, `y_1`, ..., `y_n_outputs`.
|
|
90
|
+
n_inputs : int, optional
|
|
91
|
+
Number of inputs (that is, input features). If not provided it will be inferred from `input_names`.
|
|
92
|
+
It is used to compute the Jacobian and by the OpenMDAO wrappers.
|
|
93
|
+
n_outputs : int, optional
|
|
94
|
+
Number of outputs (that is, output features). If not provided it will be inferred from `output_names`.
|
|
95
|
+
It is used to compute the Jacobian and by the OpenMDAO wrappers.
|
|
96
|
+
domain : GenericDomain
|
|
97
|
+
Input domain of the training data.
|
|
98
|
+
metadata : dict of array-like, optional
|
|
99
|
+
Metadata associated to the surrogate model.
|
|
100
|
+
"""
|
|
101
|
+
self.model = model
|
|
102
|
+
self.input_transformers = input_transformers
|
|
103
|
+
self.output_transformers = output_transformers
|
|
104
|
+
self.domain = domain
|
|
105
|
+
self.metadata = {k: np.asarray(v) for k, v in metadata.items()}
|
|
106
|
+
if len(input_names) > 0:
|
|
107
|
+
self.input_names = input_names
|
|
108
|
+
if n_inputs is not None:
|
|
109
|
+
assert n_inputs == len(
|
|
110
|
+
input_names
|
|
111
|
+
), "The length of input_names must match n_inputs."
|
|
112
|
+
self.n_inputs = n_inputs
|
|
113
|
+
else:
|
|
114
|
+
self.n_inputs = len(input_names)
|
|
115
|
+
else:
|
|
116
|
+
if n_inputs is not None:
|
|
117
|
+
self.n_inputs = n_inputs
|
|
118
|
+
self.input_names = [f"x_{i}" for i in range(n_inputs)]
|
|
119
|
+
if len(output_names) > 0:
|
|
120
|
+
self.output_names = output_names
|
|
121
|
+
if n_outputs is not None:
|
|
122
|
+
assert n_outputs == len(
|
|
123
|
+
output_names
|
|
124
|
+
), "The length of output_names must match n_outputs."
|
|
125
|
+
self.n_outputs = n_outputs
|
|
126
|
+
else:
|
|
127
|
+
self.n_outputs = len(output_names)
|
|
128
|
+
else:
|
|
129
|
+
if n_outputs is not None:
|
|
130
|
+
self.n_outputs = n_outputs
|
|
131
|
+
self.output_names = [f"y_{i}" for i in range(n_outputs)]
|
|
132
|
+
|
|
133
|
+
def _save_model(self, file_name, *args, **kwargs):
|
|
134
|
+
"""
|
|
135
|
+
Save a generic surrogate model.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
file_name : str, path-like
|
|
140
|
+
File path where the model will be saved.
|
|
141
|
+
*args : list
|
|
142
|
+
Additional `args` are passed to the function that saves the model.
|
|
143
|
+
**kwargs : dict
|
|
144
|
+
Additional `kwargs` are passed to the function that saves the model.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
None.
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
# Protected because normally the user does not want to call it.
|
|
152
|
+
raise NotImplementedError()
|
|
153
|
+
|
|
154
|
+
def save_h5(self, model_path, extra_data_path=None, *args, **kwargs):
|
|
155
|
+
"""
|
|
156
|
+
Save the SurrogateModel object.
|
|
157
|
+
|
|
158
|
+
The model will be stored into 2 files, one for the surrogate model itself,
|
|
159
|
+
and the other for the extra data (input/output transformers, channel names, ...).
|
|
160
|
+
The file for the extra data is saved in HDF5 format.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
model_path : str
|
|
165
|
+
File path where the model will be saved.
|
|
166
|
+
extra_data_path : str, bytes, h5f.FileID, None
|
|
167
|
+
Name of file (bytes or str), or an instance of `h5f.FileID` to bind to an existing file identifier,
|
|
168
|
+
or a file-like object where the extra data will be saved.
|
|
169
|
+
If None (default), the extra data is appended to the model h5 file
|
|
170
|
+
*args : -
|
|
171
|
+
Additional `args` are passed to `_save_model()`, which saves the surrogate model itself.
|
|
172
|
+
**kwargs : -
|
|
173
|
+
Additional `kwargs` are passed to `_save_model()`, which saves the surrogate model itself.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
None.
|
|
178
|
+
"""
|
|
179
|
+
# Save the surrogate model.
|
|
180
|
+
self._save_model(model_path, *args, **kwargs)
|
|
181
|
+
|
|
182
|
+
# Test if the model file is in HDF5 format, and therefore we can append the extra data.
|
|
183
|
+
# This code is not covered because it raises the exception with TensorFlow 2.15, but does not with TensorFlow 2.10.
|
|
184
|
+
# The associated test is: test_tensorflow_and_openmdao._test_save_h5_extra_data_to_non_h5_model_error
|
|
185
|
+
if extra_data_path is None and not h5py.is_hdf5(model_path): # pragma: no cover
|
|
186
|
+
raise ValueError( # pragma: no cover
|
|
187
|
+
"The extra data cannot be appended to the model file, because it is not a hdf5 file. Please specify the extra_data_path argument." # pragma: no cover
|
|
188
|
+
) # pragma: no cover
|
|
189
|
+
|
|
190
|
+
extra_data_path = extra_data_path or model_path
|
|
191
|
+
# Save the extra data.
|
|
192
|
+
with h5py.File(
|
|
193
|
+
extra_data_path, ["x", "r+"][extra_data_path == model_path]
|
|
194
|
+
) as fid:
|
|
195
|
+
fid.create_dataset("model_type", data=self.__class__.__name__, dtype=H5_STR)
|
|
196
|
+
fid.create_dataset("input_names", data=self.input_names, dtype=H5_STR)
|
|
197
|
+
fid.create_dataset("output_names", data=self.output_names, dtype=H5_STR)
|
|
198
|
+
fid.create_dataset("n_inputs", data=self.n_inputs, dtype="i8")
|
|
199
|
+
fid.create_dataset("n_outputs", data=self.n_outputs, dtype="i8")
|
|
200
|
+
|
|
201
|
+
grp_meta = fid.create_group("metadata")
|
|
202
|
+
for key, val in self.metadata.items():
|
|
203
|
+
grp_meta.create_dataset(key, data=val)
|
|
204
|
+
|
|
205
|
+
grp_in = fid.create_group("input_transformers")
|
|
206
|
+
for i in range(len(self.input_transformers)):
|
|
207
|
+
group = grp_in.create_group(f"transformer_{i}")
|
|
208
|
+
self.input_transformers[i]._save_h5(group)
|
|
209
|
+
|
|
210
|
+
grp_out = fid.create_group("output_transformers")
|
|
211
|
+
for i in range(len(self.output_transformers)):
|
|
212
|
+
group = grp_out.create_group(f"transformer_{i}")
|
|
213
|
+
self.output_transformers[i]._save_h5(group)
|
|
214
|
+
|
|
215
|
+
grp_domain = fid.create_group("domain")
|
|
216
|
+
if self.domain:
|
|
217
|
+
self.domain._save_h5(grp_domain)
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _load_model(file_name, *args, **kwargs):
|
|
221
|
+
"""
|
|
222
|
+
Load a generic surrogate model.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
file_name : str
|
|
227
|
+
File path where the model was saved.
|
|
228
|
+
*args : list
|
|
229
|
+
Additional `args` are passed to the function that loads the model.
|
|
230
|
+
**kwargs : dict
|
|
231
|
+
Additional `kwargs` are passed to the function that loads the model.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
model : -
|
|
236
|
+
Surrogate model.
|
|
237
|
+
"""
|
|
238
|
+
# Protected because normally the user does not want to call it.
|
|
239
|
+
raise NotImplementedError()
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def load_h5(cls, model_path, extra_data_path=None, *args, **kwargs):
|
|
243
|
+
"""
|
|
244
|
+
Load a surrogate model.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
model_path : str
|
|
249
|
+
File path where the model was saved.
|
|
250
|
+
extra_data_path : str, None
|
|
251
|
+
File path where the extra data were saved.
|
|
252
|
+
If None, default, extra data is read from the model h5 file
|
|
253
|
+
*args : -
|
|
254
|
+
Additional `args` are passed to `_load_model()`, which loads the surrogate model itself.
|
|
255
|
+
**kwargs : -
|
|
256
|
+
Additional `kwargs` are passed to `_load_model()`, which saves the surrogate model itself.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
obj : SurrogateModel
|
|
261
|
+
The surrogate model that was saved.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
# Load extra data.
|
|
265
|
+
with h5py.File(extra_data_path or model_path, "r") as fid:
|
|
266
|
+
model_type = fid["model_type"].asstr()[()]
|
|
267
|
+
if cls.__name__ != model_type:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"The model type in the extra data file ({model_type}) does not match the current one ({cls.__name__})."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
input_names = [s for s in fid["input_names"].asstr()[()]]
|
|
273
|
+
output_names = [s for s in fid["output_names"].asstr()[()]]
|
|
274
|
+
if "n_inputs" in fid:
|
|
275
|
+
n_inputs = fid["n_inputs"][()]
|
|
276
|
+
elif len(input_names) > 0:
|
|
277
|
+
n_inputs = len(input_names)
|
|
278
|
+
else:
|
|
279
|
+
# Older versions did not include the number of inputs.
|
|
280
|
+
n_inputs = None
|
|
281
|
+
warnings.warn(
|
|
282
|
+
"This file does not contain the number of inputs. Some features may be unavailable."
|
|
283
|
+
)
|
|
284
|
+
if "n_outputs" in fid:
|
|
285
|
+
n_outputs = fid["n_outputs"][()]
|
|
286
|
+
elif len(output_names) > 0:
|
|
287
|
+
n_outputs = len(output_names)
|
|
288
|
+
else:
|
|
289
|
+
# Older versions did not include the number of outputs.
|
|
290
|
+
n_outputs = None
|
|
291
|
+
warnings.warn(
|
|
292
|
+
"This file does not contain the number of outputs. Some features may be unavailable."
|
|
293
|
+
)
|
|
294
|
+
metadata = {k: np.array(fid["metadata"][k]) for k in fid["metadata"]}
|
|
295
|
+
# Look for the correct transformer class based on the input transformer type.
|
|
296
|
+
input_transformers = [
|
|
297
|
+
globals()[fid["input_transformers"][name]["type"].asstr()[()]]._load_h5(
|
|
298
|
+
fid["input_transformers"][name]
|
|
299
|
+
)
|
|
300
|
+
for name in fid["input_transformers"]
|
|
301
|
+
]
|
|
302
|
+
# Look for the correct transformer class based on the output transformer type.
|
|
303
|
+
output_transformers = [
|
|
304
|
+
globals()[
|
|
305
|
+
fid["output_transformers"][name]["type"].asstr()[()]
|
|
306
|
+
]._load_h5(fid["output_transformers"][name])
|
|
307
|
+
for name in fid["output_transformers"]
|
|
308
|
+
]
|
|
309
|
+
# First, check if the domain exists, to allow loading files generated pre 2.2.0.
|
|
310
|
+
# Then, check if type exists, to determine whether there is actually a domain.
|
|
311
|
+
# Finally, look for the correct domain class based on the domain type.
|
|
312
|
+
if "domain" in fid and "type" in fid["domain"]:
|
|
313
|
+
domain = globals()[fid["domain"]["type"].asstr()[()]]._load_h5(
|
|
314
|
+
fid["domain"]
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
domain = None
|
|
318
|
+
|
|
319
|
+
# Load model.
|
|
320
|
+
model = cls._load_model(model_path, *args, **kwargs)
|
|
321
|
+
|
|
322
|
+
return cls(
|
|
323
|
+
model,
|
|
324
|
+
input_transformers=input_transformers,
|
|
325
|
+
output_transformers=output_transformers,
|
|
326
|
+
input_names=input_names,
|
|
327
|
+
output_names=output_names,
|
|
328
|
+
n_inputs=n_inputs,
|
|
329
|
+
n_outputs=n_outputs,
|
|
330
|
+
metadata=metadata,
|
|
331
|
+
domain=domain,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@abstractmethod
|
|
335
|
+
def _predict_model_output(self, x):
|
|
336
|
+
"""
|
|
337
|
+
Predict the surrogate model output. Typically, surrogates are trained on transformed input and output data,
|
|
338
|
+
therefore this function expects a transformed input and will return a transformed output.
|
|
339
|
+
"""
|
|
340
|
+
# Protected because normally the user does not want to call it.
|
|
341
|
+
# Any surrogate must be evaluated. That is why this method is abstract.
|
|
342
|
+
|
|
343
|
+
def predict_output(self, x):
|
|
344
|
+
r"""
|
|
345
|
+
Predict the complete model output, including all input and output transformations.
|
|
346
|
+
|
|
347
|
+
We assume that the input :math:`x` has undergone a series of transformation :math:`f_i`
|
|
348
|
+
|
|
349
|
+
.. math::
|
|
350
|
+
\tilde{x} = f_n \circ \cdots \circ f_2 \circ f_1 \circ x
|
|
351
|
+
|
|
352
|
+
Similarly, also the output :math:`y` has been transformed as
|
|
353
|
+
|
|
354
|
+
.. math::
|
|
355
|
+
\tilde{y} = g_m \circ \cdots \circ g_2 \circ g_1 \circ y
|
|
356
|
+
|
|
357
|
+
Then, a surrogate model :math:`s` has been trained on the transformed input and output.
|
|
358
|
+
|
|
359
|
+
.. math::
|
|
360
|
+
\hat{\tilde{y}} = s(\tilde{x}) \simeq \tilde{y}
|
|
361
|
+
|
|
362
|
+
The output in the original space is thus reconstructed as
|
|
363
|
+
|
|
364
|
+
.. math::
|
|
365
|
+
\hat{y} = g_1^{-1} \circ g_2^{-1} \circ \cdots \circ g_m^{-1} \circ \hat{\tilde{y}}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
x : array-like of shape (n_points, n_inputs)
|
|
371
|
+
Input data.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
y : array-like of shape (n_points, n_outputs)
|
|
376
|
+
Output data.
|
|
377
|
+
"""
|
|
378
|
+
# Check if the points are inside the domain of the training data.
|
|
379
|
+
if self.domain is not None:
|
|
380
|
+
x_in_domain = self.domain.in_domain(x)
|
|
381
|
+
if not np.all(x_in_domain):
|
|
382
|
+
# n_x_outside = x.shape[0] - np.count_nonzero(x_in_domain)
|
|
383
|
+
fraction_outside = 1.0 - np.count_nonzero(x_in_domain) / x.shape[0]
|
|
384
|
+
warnings.warn(
|
|
385
|
+
f"{fraction_outside:.1%} of points are outside of the input domain."
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Apply direct transformations to input.
|
|
389
|
+
x_transformed = x.copy()
|
|
390
|
+
for transformer in self.input_transformers:
|
|
391
|
+
# If possible do the transformation inplace.
|
|
392
|
+
try:
|
|
393
|
+
transformer.transform(x_transformed, inplace=True)
|
|
394
|
+
except ValueError:
|
|
395
|
+
# The PCA does not support inplace.
|
|
396
|
+
x_transformed = transformer.transform(x_transformed, inplace=False)
|
|
397
|
+
|
|
398
|
+
# Evaluate surrogate model.
|
|
399
|
+
y = self._predict_model_output(x_transformed)
|
|
400
|
+
|
|
401
|
+
# Apply inverse transformations to output.
|
|
402
|
+
for transformer in reversed(self.output_transformers):
|
|
403
|
+
# If possible do the inverse transformation inplace.
|
|
404
|
+
try:
|
|
405
|
+
transformer.inverse_transform(y, inplace=True)
|
|
406
|
+
except ValueError:
|
|
407
|
+
# The PCA does not support inplace.
|
|
408
|
+
y = transformer.inverse_transform(y, inplace=False)
|
|
409
|
+
return y
|
|
410
|
+
|
|
411
|
+
def _predict_model_jacobian(self, x, *args, **kwargs):
|
|
412
|
+
"""
|
|
413
|
+
Predict the surrogate model output and Jacobian.
|
|
414
|
+
|
|
415
|
+
Typically, surrogates are trained on transformed input and output data,
|
|
416
|
+
therefore this function expects a transformed input and will return a transformed output.
|
|
417
|
+
"""
|
|
418
|
+
# Protected because normally the user does not want to call it.
|
|
419
|
+
raise NotImplementedError()
|
|
420
|
+
|
|
421
|
+
def predict_output_and_jacobian(self, x, *args, **kwargs):
|
|
422
|
+
"""
|
|
423
|
+
Predict the complete model output and Jacobian, including all input and output transformations.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
x : array-like of shape (n_points, n_inputs)
|
|
428
|
+
Input data.
|
|
429
|
+
*args : -
|
|
430
|
+
Additional `args` are passed to `_predict_model_jacobian()`.
|
|
431
|
+
**kwargs : -
|
|
432
|
+
Additional `kwargs` are passed to `_predict_model_jacobian()`.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
y : array-like of shape (n_points, n_outputs)
|
|
437
|
+
Output.
|
|
438
|
+
jacobian : array-like of shape (n_points, n_outputs, n_inputs)
|
|
439
|
+
Jacobian.
|
|
440
|
+
"""
|
|
441
|
+
# Check if the points are inside the domain of the training data.
|
|
442
|
+
if self.domain is not None:
|
|
443
|
+
x_in_domain = self.domain.in_domain(x)
|
|
444
|
+
if not np.all(x_in_domain):
|
|
445
|
+
# n_x_outside = x.shape[0] - np.count_nonzero(x_in_domain)
|
|
446
|
+
fraction_outside = 1.0 - np.count_nonzero(x_in_domain) / x.shape[0]
|
|
447
|
+
warnings.warn(
|
|
448
|
+
f"{fraction_outside:.1%} of points are outside of the input domain."
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Apply direct transformations to input.
|
|
452
|
+
x_transformed = x.copy()
|
|
453
|
+
# The input transformation Jacobian starts as an identity matrix,
|
|
454
|
+
# repeated over all points.
|
|
455
|
+
input_transformation_jacobian = np.tile(np.eye(x.shape[1]), (x.shape[0], 1, 1))
|
|
456
|
+
for transformer in self.input_transformers:
|
|
457
|
+
# Get Jacobian of current input transformation.
|
|
458
|
+
in_jac = transformer.transform_gradient(x_transformed)
|
|
459
|
+
# Accumulate the input transformations Jacobian.
|
|
460
|
+
if in_jac.ndim == 2:
|
|
461
|
+
# If the Jacobian has shape (n_points, n_in), it means it the features are independent.
|
|
462
|
+
# Do the multiplication as if in_jac for each point is a diagonal matrix.
|
|
463
|
+
input_transformation_jacobian *= in_jac[:, np.newaxis, :]
|
|
464
|
+
|
|
465
|
+
else: # in_jac.ndim == 3:
|
|
466
|
+
# If the Jacobian has shape (n_points, n_out, n_in), it means that the features are dependent.
|
|
467
|
+
# Therefore the chain rule is a matrix multiplication.
|
|
468
|
+
input_transformation_jacobian = in_jac @ input_transformation_jacobian
|
|
469
|
+
|
|
470
|
+
# Apply the input transformation.
|
|
471
|
+
try:
|
|
472
|
+
transformer.transform(x_transformed, inplace=True)
|
|
473
|
+
except ValueError:
|
|
474
|
+
# The PCA does not support inplace.
|
|
475
|
+
x_transformed = transformer.transform(x_transformed, inplace=False)
|
|
476
|
+
|
|
477
|
+
# Predict the model output and Jacobian.
|
|
478
|
+
y, jacobian = self._predict_model_jacobian(x_transformed, *args, **kwargs)
|
|
479
|
+
|
|
480
|
+
# Apply the input transformation Jacobian via the chain rule.
|
|
481
|
+
jacobian = jacobian @ input_transformation_jacobian
|
|
482
|
+
|
|
483
|
+
# Apply the inverse output transformation Jacobian via the chain rule.
|
|
484
|
+
for transformer in reversed(self.output_transformers):
|
|
485
|
+
out_jac = transformer.inverse_transform_gradient(y)
|
|
486
|
+
|
|
487
|
+
# Accumulate the output transformations Jacobian.
|
|
488
|
+
if out_jac.ndim == 2:
|
|
489
|
+
# If the Jacobian has shape (n_points, n_out), it means it the features are independent.
|
|
490
|
+
# Do the multiplication as if out_jac for each point is a diagonal matrix.
|
|
491
|
+
jacobian *= out_jac[:, :, np.newaxis]
|
|
492
|
+
|
|
493
|
+
else: # out_jac.ndim == 3:
|
|
494
|
+
# If the Jacobian has shape (n_points, n_out, n_in), it means that the features are dependent.
|
|
495
|
+
# Therefore the chain rule is a matrix multiplication.
|
|
496
|
+
jacobian = out_jac @ jacobian
|
|
497
|
+
|
|
498
|
+
# Apply the inverse output transformation.
|
|
499
|
+
try:
|
|
500
|
+
transformer.inverse_transform(y, inplace=True)
|
|
501
|
+
except ValueError:
|
|
502
|
+
# The PCA does not support inplace.
|
|
503
|
+
y = transformer.inverse_transform(y, inplace=False)
|
|
504
|
+
return y, jacobian
|
|
505
|
+
|
|
506
|
+
def predict_jacobian(self, x, *args, **kwargs):
|
|
507
|
+
"""
|
|
508
|
+
Predict the complete model Jacobian, including all input and output transformations.
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
x : array-like of shape (n_points, n_inputs)
|
|
513
|
+
Input data.
|
|
514
|
+
*args : -
|
|
515
|
+
Additional `args` are passed to `_predict_model_jacobian()`.
|
|
516
|
+
**kwargs : -
|
|
517
|
+
Additional `kwargs` are passed to `_predict_model_jacobian()`.
|
|
518
|
+
|
|
519
|
+
Returns
|
|
520
|
+
-------
|
|
521
|
+
jacobian : array-like of shape (n_points, n_outputs, n_inputs)
|
|
522
|
+
Jacobian.
|
|
523
|
+
"""
|
|
524
|
+
_, jacobian = self.predict_output_and_jacobian(x, *args, **kwargs)
|
|
525
|
+
return jacobian
|
|
526
|
+
|
|
527
|
+
def _predict_model_vjp(self, x, dy):
|
|
528
|
+
r"""
|
|
529
|
+
Predict the surrogate model output and vector-Jacobian product.
|
|
530
|
+
|
|
531
|
+
Computes
|
|
532
|
+
|
|
533
|
+
.. math::
|
|
534
|
+
\begin{align}
|
|
535
|
+
\mathrm{d}\boldsymbol{x} &= J(\boldsymbol{x})^{\mathsf{T}} \mathrm{d}\boldsymbol{y}
|
|
536
|
+
\\
|
|
537
|
+
\mathrm{d}\boldsymbol{x}^{\mathsf{T}} &= \mathrm{d}\boldsymbol{y}^{\mathsf{T}} J(\boldsymbol{x})^{\mathsf{T}}
|
|
538
|
+
\end{align}
|
|
539
|
+
"""
|
|
540
|
+
# Protected because normally the user does not want to call it.
|
|
541
|
+
raise NotImplementedError()
|
|
542
|
+
|
|
543
|
+
def predict_vjp(self, x, dy):
|
|
544
|
+
"""
|
|
545
|
+
Predict the surrogate model vector-Jacobian product, including all input and output transformations.
|
|
546
|
+
|
|
547
|
+
Parameters
|
|
548
|
+
----------
|
|
549
|
+
x : array-like of shape (n_points, n_inputs)
|
|
550
|
+
Input data.
|
|
551
|
+
dy : array-like of shape (n_points, n_outputs)
|
|
552
|
+
Output variation (seed). This function is meant to work with OpenMDAO, where `dy` is a one-hot array,
|
|
553
|
+
which means that `dy` is 1 for a given point and output channel and 0 for all others.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
dx : array-like of shape (n_points, n_inputs)
|
|
558
|
+
Input variation.
|
|
559
|
+
"""
|
|
560
|
+
# Find the hot point and input channel.
|
|
561
|
+
idx = dy.nonzero()
|
|
562
|
+
try:
|
|
563
|
+
i_point = idx[0][0]
|
|
564
|
+
# i_out = idx[1][0]
|
|
565
|
+
except IndexError:
|
|
566
|
+
# Early exit if the seed is 0.
|
|
567
|
+
return np.zeros((dy.shape[0], self.n_inputs), dtype=dy.dtype)
|
|
568
|
+
|
|
569
|
+
# Check if the hot point is inside the domain of the training data.
|
|
570
|
+
if self.domain is not None:
|
|
571
|
+
x_in_domain = self.domain.in_domain(x[[i_point], :])
|
|
572
|
+
if not x_in_domain[0]:
|
|
573
|
+
warnings.warn("The hot point is outside of the input domain.")
|
|
574
|
+
|
|
575
|
+
# Take one input point, apply input transformations and accumulate the Jacobian of the transformations.
|
|
576
|
+
x_transformed = x[[i_point], :].copy()
|
|
577
|
+
input_transformation_jacobian = np.ones((1, self.n_inputs), dtype=x.dtype)
|
|
578
|
+
for transformer in self.input_transformers:
|
|
579
|
+
input_transformation_jacobian *= transformer.transform_gradient(
|
|
580
|
+
x_transformed
|
|
581
|
+
)
|
|
582
|
+
transformer.transform(x_transformed, inplace=True)
|
|
583
|
+
|
|
584
|
+
# Compute VJP for a single point.
|
|
585
|
+
# y_i has shape (1, n_outputs)
|
|
586
|
+
# dx_i has shape (1, n_inputs)
|
|
587
|
+
y_i, dx_i = self._predict_model_vjp(x_transformed, dy[[i_point], :])
|
|
588
|
+
|
|
589
|
+
# Apply input transformation jacobian via chain rule.
|
|
590
|
+
dx_i *= input_transformation_jacobian
|
|
591
|
+
|
|
592
|
+
# Apply inverse output transformations and accumulate their VJP.
|
|
593
|
+
for transformer in reversed(self.output_transformers):
|
|
594
|
+
dx_i *= transformer.inverse_transform_gradient(y_i) @ dy[i_point, :]
|
|
595
|
+
transformer.inverse_transform(y_i, inplace=True)
|
|
596
|
+
|
|
597
|
+
# Preallocate and assign dx.
|
|
598
|
+
dx = np.zeros((dy.shape[0], self.n_inputs), dtype=dy.dtype)
|
|
599
|
+
dx[[i_point], :] = dx_i
|
|
600
|
+
return dx
|
|
601
|
+
|
|
602
|
+
@abstractmethod
|
|
603
|
+
def _predict_model_jvp(self, x, dx):
|
|
604
|
+
r"""
|
|
605
|
+
Predict the surrogate model output and Jacobian-vector product.
|
|
606
|
+
|
|
607
|
+
Computes
|
|
608
|
+
|
|
609
|
+
.. math::
|
|
610
|
+
\mathrm{d}\boldsymbol{y} = J(\boldsymbol{x}) \mathrm{d}\boldsymbol{x}
|
|
611
|
+
"""
|
|
612
|
+
# Protected because normally the user does not want to call it.
|
|
613
|
+
raise NotImplementedError()
|
|
614
|
+
|
|
615
|
+
def predict_jvp(self, x, dx):
|
|
616
|
+
"""
|
|
617
|
+
Predict the surrogate model Jacobian-vector product, including all input and output transformations.
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
x : array-like of shape (n_points, n_inputs)
|
|
622
|
+
Input data.
|
|
623
|
+
dx : array-like of shape (n_points, n_inputs)
|
|
624
|
+
Input variation (seed). This function is meant to work with OpenMDAO, where `dx` is a one-hot array,
|
|
625
|
+
which means that `dx` is 1 for a given point and input channel and 0 for all others.
|
|
626
|
+
|
|
627
|
+
Returns
|
|
628
|
+
-------
|
|
629
|
+
dy : array-like of shape (n_points, n_outputs)
|
|
630
|
+
Output variation.
|
|
631
|
+
"""
|
|
632
|
+
# Find the hot point and input channel.
|
|
633
|
+
idx = dx.nonzero()
|
|
634
|
+
try:
|
|
635
|
+
i_point = idx[0][0]
|
|
636
|
+
# i_in = idx[1][0]
|
|
637
|
+
except IndexError:
|
|
638
|
+
# Early exit if the seed is 0.
|
|
639
|
+
return np.zeros((dx.shape[0], self.n_outputs), dtype=x.dtype)
|
|
640
|
+
|
|
641
|
+
# Check if the hot point is inside the domain of the training data.
|
|
642
|
+
if self.domain is not None:
|
|
643
|
+
x_in_domain = self.domain.in_domain(x[[i_point], :])
|
|
644
|
+
if not x_in_domain[0]:
|
|
645
|
+
warnings.warn("The hot point is outside of the input domain.")
|
|
646
|
+
|
|
647
|
+
# Take one input point, apply input transformations and accumulate JVP of the transformations.
|
|
648
|
+
x_transformed = x[[i_point], :].copy()
|
|
649
|
+
input_transformation_jvp = np.array([1.0], dtype=x.dtype)
|
|
650
|
+
for transformer in self.input_transformers:
|
|
651
|
+
input_transformation_jvp *= (
|
|
652
|
+
transformer.transform_gradient(x_transformed) @ dx[i_point, :]
|
|
653
|
+
)
|
|
654
|
+
transformer.transform(x_transformed, inplace=True)
|
|
655
|
+
|
|
656
|
+
# Compute JVP for a single point.
|
|
657
|
+
# y_i and dy_i have shape (1, n_outputs)
|
|
658
|
+
y_i, dy_i = self._predict_model_jvp(x_transformed, dx[[i_point], :])
|
|
659
|
+
|
|
660
|
+
# Apply input transformation JVP via chain rule.
|
|
661
|
+
dy_i *= input_transformation_jvp
|
|
662
|
+
|
|
663
|
+
# Apply inverse output transformations and accumulate their Jacobians.
|
|
664
|
+
for transformer in reversed(self.output_transformers):
|
|
665
|
+
dy_i *= transformer.inverse_transform_gradient(y_i)
|
|
666
|
+
transformer.inverse_transform(y_i, inplace=True)
|
|
667
|
+
|
|
668
|
+
# Preallocate and assign dy.
|
|
669
|
+
dy = np.zeros((dx.shape[0], self.n_outputs), dtype=dx.dtype)
|
|
670
|
+
dy[[i_point], :] = dy_i
|
|
671
|
+
return dy
|
|
672
|
+
|
|
673
|
+
def __eq__(self, other):
|
|
674
|
+
# TODO: check also the model.
|
|
675
|
+
if isinstance(other, self.__class__):
|
|
676
|
+
test = True
|
|
677
|
+
try:
|
|
678
|
+
np.testing.assert_equal(self.metadata, other.metadata)
|
|
679
|
+
except AssertionError:
|
|
680
|
+
test = False
|
|
681
|
+
if test:
|
|
682
|
+
for a, b in zip(self.input_transformers, other.input_transformers):
|
|
683
|
+
test = test and (a == b)
|
|
684
|
+
if test:
|
|
685
|
+
for a, b in zip(self.output_transformers, other.output_transformers):
|
|
686
|
+
test = test and (a == b)
|
|
687
|
+
if test:
|
|
688
|
+
for a, b in zip(self.input_names, other.input_names):
|
|
689
|
+
test = test and (a == b)
|
|
690
|
+
if test:
|
|
691
|
+
for a, b in zip(self.output_names, other.output_names):
|
|
692
|
+
test = test and (a == b)
|
|
693
|
+
test = test and (self.n_inputs == other.n_inputs)
|
|
694
|
+
test = test and (self.n_outputs == other.n_outputs)
|
|
695
|
+
test = test and (self.domain == other.domain)
|
|
696
|
+
|
|
697
|
+
return test
|
|
698
|
+
return False
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class TensorFlowModel(SurrogateModel):
|
|
702
|
+
"""
|
|
703
|
+
Class that stores and evaluates an existing TensorFlow `Model`. Typically used with a `Sequential` model.
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
@staticmethod
|
|
707
|
+
def _load_model(filepath, custom_objects=None, compile=True, options=None):
|
|
708
|
+
"""
|
|
709
|
+
Loads a TensorFlow model saved via `model.save()`.
|
|
710
|
+
Wrapper of `tf.keras.models.load_model`.
|
|
711
|
+
|
|
712
|
+
Parameters
|
|
713
|
+
----------
|
|
714
|
+
filepath: str, pathlib.Path, h5py.File
|
|
715
|
+
Path to the model.
|
|
716
|
+
custom_objects: dict
|
|
717
|
+
Dictionary mapping names (strings) to custom classes or
|
|
718
|
+
functions to be considered during deserialization.
|
|
719
|
+
compile: bool
|
|
720
|
+
`True` to compile the model after loading.
|
|
721
|
+
options: tf.saved_model.LoadOptions
|
|
722
|
+
Object that specifies options for loading from SavedModel.
|
|
723
|
+
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
tf.keras.Model
|
|
727
|
+
A Keras model instance. If the original model was compiled, and saved
|
|
728
|
+
with the optimizer, then the returned model will be compiled. Otherwise,
|
|
729
|
+
the model will be left uncompiled. In the case that an uncompiled model
|
|
730
|
+
is returned, a warning is displayed if the `compile` argument is set to `True`.
|
|
731
|
+
|
|
732
|
+
Raises
|
|
733
|
+
------
|
|
734
|
+
ImportError
|
|
735
|
+
If loading from an hdf5 file and h5py is not available.
|
|
736
|
+
IOError
|
|
737
|
+
In case of an invalid savefile.
|
|
738
|
+
"""
|
|
739
|
+
from tensorflow.keras.models import load_model
|
|
740
|
+
|
|
741
|
+
return load_model(filepath, custom_objects, compile, options)
|
|
742
|
+
|
|
743
|
+
def _save_model(self, filepath, overwrite=True, save_format="h5"):
|
|
744
|
+
"""
|
|
745
|
+
Saves the model to Tensorflow SavedModel or a single HDF5 file.
|
|
746
|
+
Wrapper of tf.keras.Model.save`.
|
|
747
|
+
|
|
748
|
+
Parameters
|
|
749
|
+
----------
|
|
750
|
+
filepath : str, path-like
|
|
751
|
+
Path to `SavedModel` or `H5` file to save the model.
|
|
752
|
+
overwrite : bool, optional
|
|
753
|
+
`True` to silently overwrite any existing file at the
|
|
754
|
+
target location, or provide the user with a manual prompt.
|
|
755
|
+
save_format : str, optional
|
|
756
|
+
Either `'keras'`, `'tf'` or `'h5'`, indicating whether to
|
|
757
|
+
save the model in the native Keras format (.keras), in the
|
|
758
|
+
TensorFlow SavedModel format (referred to as "SavedModel" below),
|
|
759
|
+
or in the legacy HDF5 format (.h5). Defaults to `'h5'`.
|
|
760
|
+
|
|
761
|
+
Returns
|
|
762
|
+
-------
|
|
763
|
+
None.
|
|
764
|
+
|
|
765
|
+
"""
|
|
766
|
+
self.model.save(filepath, overwrite=overwrite, save_format=save_format)
|
|
767
|
+
|
|
768
|
+
def _predict_model_output(self, x):
|
|
769
|
+
"""
|
|
770
|
+
Predict the model output.
|
|
771
|
+
|
|
772
|
+
Parameters
|
|
773
|
+
----------
|
|
774
|
+
x : array-like of shape (n_points, n_inputs)
|
|
775
|
+
Input data.
|
|
776
|
+
|
|
777
|
+
Returns
|
|
778
|
+
-------
|
|
779
|
+
y : array-like of shape (n_points, n_outputs)
|
|
780
|
+
Output data.
|
|
781
|
+
"""
|
|
782
|
+
# TODO: Add support also for other arguments of predict, like use_multiprocessing.
|
|
783
|
+
y = self.model.predict(x, batch_size=x.shape[0], verbose=0)
|
|
784
|
+
return y
|
|
785
|
+
|
|
786
|
+
def _predict_model_output_and_jacobian_rev(self, x):
|
|
787
|
+
"""
|
|
788
|
+
Predict the model Jacobian using reverse mode automatic differentiation,
|
|
789
|
+
assuming that all batches (i.e. points) are independent.
|
|
790
|
+
|
|
791
|
+
Parameters
|
|
792
|
+
----------
|
|
793
|
+
x : array-like of shape (n_points, n_inputs)
|
|
794
|
+
Input data.
|
|
795
|
+
|
|
796
|
+
Returns
|
|
797
|
+
-------
|
|
798
|
+
y : array-like of shape (n_points, n_outputs)
|
|
799
|
+
Output.
|
|
800
|
+
jacobian : array-like of shape (n_points, n_outputs, n_inputs)
|
|
801
|
+
Jacobian.
|
|
802
|
+
"""
|
|
803
|
+
import tensorflow as tf
|
|
804
|
+
|
|
805
|
+
input = tf.constant(x)
|
|
806
|
+
with tf.GradientTape() as tape:
|
|
807
|
+
tape.watch(input)
|
|
808
|
+
output = self.model(input)
|
|
809
|
+
jacobian = tape.batch_jacobian(output, input).numpy()
|
|
810
|
+
return output.numpy(), jacobian
|
|
811
|
+
|
|
812
|
+
def _predict_model_output_and_jacobian_fwd(self, x):
|
|
813
|
+
"""
|
|
814
|
+
Predict the model Jacobian using forward mode automatic differentiation,
|
|
815
|
+
assuming that all batches (i.e. points) are independent.
|
|
816
|
+
|
|
817
|
+
Parameters
|
|
818
|
+
----------
|
|
819
|
+
x : array-like of shape (n_points, n_inputs)
|
|
820
|
+
Input data.
|
|
821
|
+
|
|
822
|
+
Returns
|
|
823
|
+
-------
|
|
824
|
+
y : array-like of shape (n_points, n_outputs)
|
|
825
|
+
Output.
|
|
826
|
+
jacobian : array-like of shape (n_points, n_outputs, n_inputs)
|
|
827
|
+
Jacobian.
|
|
828
|
+
"""
|
|
829
|
+
import tensorflow as tf
|
|
830
|
+
|
|
831
|
+
# Preallocate jacobian array.
|
|
832
|
+
jacobian = np.zeros((x.shape[0], self.n_outputs, self.n_inputs))
|
|
833
|
+
# Transform the input into a Tensor, and convert to single precision.
|
|
834
|
+
input = tf.constant(x, dtype=tf.float32)
|
|
835
|
+
# Define the direction for the derivatives.
|
|
836
|
+
# The direction will be broadcasted over the points.
|
|
837
|
+
tangents = np.zeros((x.shape[1]), dtype=np.float32)
|
|
838
|
+
# We start from the first input.
|
|
839
|
+
tangents[0] = 1
|
|
840
|
+
with tf.autodiff.ForwardAccumulator(
|
|
841
|
+
input, tf.broadcast_to(tangents, x.shape)
|
|
842
|
+
) as acc:
|
|
843
|
+
output = self.model(input)
|
|
844
|
+
jacobian[:, :, 0] = acc.jvp(output).numpy()
|
|
845
|
+
# And proceed with the others.
|
|
846
|
+
for i_in in range(1, self.n_inputs):
|
|
847
|
+
# Reset the direction.
|
|
848
|
+
tangents[i_in - 1] = 0
|
|
849
|
+
# Set the current one.
|
|
850
|
+
tangents[i_in] = 1
|
|
851
|
+
with tf.autodiff.ForwardAccumulator(
|
|
852
|
+
input, tf.broadcast_to(tangents, x.shape)
|
|
853
|
+
) as acc:
|
|
854
|
+
output = self.model(input)
|
|
855
|
+
jacobian[:, :, i_in] = acc.jvp(output).numpy()
|
|
856
|
+
return output.numpy(), jacobian
|
|
857
|
+
|
|
858
|
+
def _predict_model_jacobian(self, x, ad_mode=None):
|
|
859
|
+
"""
|
|
860
|
+
Predict the model Jacobian, assuming that all batches (i.e. points) are independent.
|
|
861
|
+
|
|
862
|
+
Parameters
|
|
863
|
+
----------
|
|
864
|
+
x : array-like of shape (n_points, n_inputs)
|
|
865
|
+
Input data.
|
|
866
|
+
ad_mode : ADMode, optional
|
|
867
|
+
Automatic differentiation mode. Can be either `ADMode.FWD` or `ADMode.REV`.
|
|
868
|
+
If `None`, the choice depends on the number of input and output featurtes.
|
|
869
|
+
|
|
870
|
+
Returns
|
|
871
|
+
-------
|
|
872
|
+
y : array-like of shape (n_points, n_outputs)
|
|
873
|
+
Output.
|
|
874
|
+
jacobian : array-like of shape (n_points, n_outputs, n_inputs)
|
|
875
|
+
Jacobian.
|
|
876
|
+
"""
|
|
877
|
+
if ad_mode is None:
|
|
878
|
+
if self.n_outputs <= self.n_inputs:
|
|
879
|
+
ad_mode = ADMode.REV
|
|
880
|
+
else:
|
|
881
|
+
ad_mode = ADMode.FWD
|
|
882
|
+
if ad_mode is ADMode.REV:
|
|
883
|
+
y, jacobian = self._predict_model_output_and_jacobian_rev(x)
|
|
884
|
+
elif ad_mode is ADMode.FWD:
|
|
885
|
+
y, jacobian = self._predict_model_output_and_jacobian_fwd(x)
|
|
886
|
+
return y, jacobian
|
|
887
|
+
|
|
888
|
+
def _predict_model_vjp(self, x, dy):
|
|
889
|
+
"""
|
|
890
|
+
Compute output and vector-Jacobian product using reverse-mode
|
|
891
|
+
automatic differentiation, i.e. backpropagation.
|
|
892
|
+
|
|
893
|
+
Parameters
|
|
894
|
+
----------
|
|
895
|
+
x : array-like of shape (1, n_inputs)
|
|
896
|
+
Input data.
|
|
897
|
+
dy : array-like of shape (1, n_outputs)
|
|
898
|
+
Output variation.
|
|
899
|
+
|
|
900
|
+
Returns
|
|
901
|
+
-------
|
|
902
|
+
y : array-like of shape (1, n_outputs)
|
|
903
|
+
Output.
|
|
904
|
+
dx : array-like of shape (1, n_inputs)
|
|
905
|
+
Input variation.
|
|
906
|
+
"""
|
|
907
|
+
import tensorflow as tf
|
|
908
|
+
|
|
909
|
+
# Compute the Jacobian.
|
|
910
|
+
input = tf.constant(x)
|
|
911
|
+
with tf.GradientTape() as tape:
|
|
912
|
+
tape.watch(input)
|
|
913
|
+
output = self.model(input)
|
|
914
|
+
jacobian = tape.batch_jacobian(output, input).numpy()
|
|
915
|
+
# Compute the product.
|
|
916
|
+
dummy = jacobian.transpose(0, 2, 1) @ dy[:, :, np.newaxis]
|
|
917
|
+
dx = dummy[:, :, 0]
|
|
918
|
+
return output.numpy(), dx
|
|
919
|
+
|
|
920
|
+
def _predict_model_jvp(self, x, dx):
|
|
921
|
+
"""
|
|
922
|
+
Compute output and Jacobian-vector product using forward-mode
|
|
923
|
+
automatic differentiation.
|
|
924
|
+
|
|
925
|
+
Parameters
|
|
926
|
+
----------
|
|
927
|
+
x : array-like of shape (1, n_inputs)
|
|
928
|
+
Input data.
|
|
929
|
+
dx : array-like of shape (1, n_inputs)
|
|
930
|
+
Input variation.
|
|
931
|
+
|
|
932
|
+
Returns
|
|
933
|
+
-------
|
|
934
|
+
y : array-like of shape (1, n_outputs)
|
|
935
|
+
Output.
|
|
936
|
+
dy : array-like of shape (1, n_outputs)
|
|
937
|
+
Output variation.
|
|
938
|
+
"""
|
|
939
|
+
import tensorflow as tf
|
|
940
|
+
|
|
941
|
+
# Take the current point, and convert to float32 or otherwise
|
|
942
|
+
# ForwardAccumulator will crash.
|
|
943
|
+
input = tf.constant(x, dtype=tf.float32)
|
|
944
|
+
tangents = tf.constant(dx, dtype=tf.float32)
|
|
945
|
+
with tf.autodiff.ForwardAccumulator(input, tangents) as acc:
|
|
946
|
+
output = self.model(input)
|
|
947
|
+
dy = acc.jvp(output).numpy()
|
|
948
|
+
return output.numpy(), dy
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
class SMTModel(SurrogateModel):
|
|
952
|
+
"""
|
|
953
|
+
Class that stores and evaluates an existing SMT `SurrogateModel`.
|
|
954
|
+
"""
|
|
955
|
+
|
|
956
|
+
@staticmethod
|
|
957
|
+
def _load_model(filepath):
|
|
958
|
+
with open(filepath, "rb") as f:
|
|
959
|
+
return pickle.load(f)
|
|
960
|
+
# TODO: deal with C++ models.
|
|
961
|
+
|
|
962
|
+
def _save_model(self, filepath):
|
|
963
|
+
from smt.surrogate_models import IDW, RBF, RMTB, RMTC
|
|
964
|
+
|
|
965
|
+
if isinstance(self.model, (IDW, RBF, RMTB, RMTC)):
|
|
966
|
+
raise NotImplementedError()
|
|
967
|
+
else:
|
|
968
|
+
# Pure Python models are saved with pickle.
|
|
969
|
+
with open(filepath, "wb") as fid:
|
|
970
|
+
pickle.dump(self.model, fid)
|
|
971
|
+
|
|
972
|
+
def _predict_model_output(self, x):
|
|
973
|
+
"""
|
|
974
|
+
Predict the model output.
|
|
975
|
+
|
|
976
|
+
Parameters
|
|
977
|
+
----------
|
|
978
|
+
x : array-like of shape (n_points, n_inputs)
|
|
979
|
+
Input data.
|
|
980
|
+
|
|
981
|
+
Returns
|
|
982
|
+
-------
|
|
983
|
+
y : array-like of shape (n_points, n_outputs)
|
|
984
|
+
Output data.
|
|
985
|
+
"""
|
|
986
|
+
# Switch off printing.
|
|
987
|
+
print_save = self.model.options["print_prediction"]
|
|
988
|
+
self.model.options["print_prediction"] = False
|
|
989
|
+
# Predict output.
|
|
990
|
+
y = self.model.predict_values(x)
|
|
991
|
+
# Restore printing option.
|
|
992
|
+
self.model.options["print_prediction"] = print_save
|
|
993
|
+
|
|
994
|
+
return y
|
|
995
|
+
|
|
996
|
+
def _predict_model_jacobian(self, x):
|
|
997
|
+
"""
|
|
998
|
+
Predict the model Jacobian.
|
|
999
|
+
|
|
1000
|
+
Parameters
|
|
1001
|
+
----------
|
|
1002
|
+
x : array-like of shape (n_points, n_inputs)
|
|
1003
|
+
Input data.
|
|
1004
|
+
|
|
1005
|
+
Returns
|
|
1006
|
+
-------
|
|
1007
|
+
y : array-like of shape (n_points, n_outputs)
|
|
1008
|
+
Output.
|
|
1009
|
+
jacobian : array-like of shape (n_points, n_outputs, n_inputs)
|
|
1010
|
+
Jacobian.
|
|
1011
|
+
"""
|
|
1012
|
+
# Switch off printing.
|
|
1013
|
+
print_save = self.model.options["print_prediction"]
|
|
1014
|
+
self.model.options["print_prediction"] = False
|
|
1015
|
+
# Predict output.
|
|
1016
|
+
y = self.model.predict_values(x)
|
|
1017
|
+
# Predict Jacobian by looping over the inputs.
|
|
1018
|
+
jacobian = np.empty((x.shape[0], self.n_outputs, self.n_inputs))
|
|
1019
|
+
for i in range(self.n_inputs):
|
|
1020
|
+
jacobian[:, :, i] = self.model.predict_derivatives(x, i)
|
|
1021
|
+
# Restore printing option.
|
|
1022
|
+
self.model.options["print_prediction"] = print_save
|
|
1023
|
+
return y, jacobian
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
class SurrogateModelFamily:
|
|
1027
|
+
pass
|