forked from GPflow/GPflowOpt
/
scaling.py
228 lines (196 loc) · 9.73 KB
/
scaling.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# Copyright 2017 Joachim van der Herten
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from GPflow.param import DataHolder, AutoFlow, Parameterized
from GPflow.model import Model, GPModel
from GPflow import settings
import numpy as np
from .transforms import LinearTransform, DataTransform
from .domain import UnitCube
float_type = settings.dtypes.float_type
class DataScaler(GPModel):
"""
Model-wrapping class, primarily intended to assure the data in GPflow models is scaled. One DataScaler wraps one
GPflow model, and can scale the input as well as the output data. By default, if any kind of object attribute
is not found in the datascaler object, it is searched on the wrapped model.
The datascaler supports both input as well as output scaling, although both scalings are set up differently:
- For input, the transform is not automatically generated. By default, the input transform is the identity
transform. The input transform can be set through the setter property, or by specifying a domain in the
constructor. For the latter, the input transform will be initialized as the transform from the specified domain to
a unit cube. When X is updated, the transform does not change.
- If enabled: for output the data is always scaled to zero mean and unit variance. This means that if the Y property
is set, the output transform is first calculated, then the data is scaled.
By default, :class:`.Acquisition` objects will always wrap each model received. However, the input and output transforms
will be the identity transforms, and output normalization is switched off. It is up to the user (or
specialized classes such as the BayesianOptimizer to correctly configure the datascalers involved.
By carrying out the scaling at such a deep level in the framework, it is possible to keep the scaling
hidden throughout the rest of GPflowOpt. This means that, during implementation of acquisition functions it is safe
to assume the data is not scaled, and is within the configured optimization domain. There is only one exception:
the hyperparameters are determined on the scaled data, and are NOT automatically unscaled by this class because the
datascaler does not know what model is wrapped and what kernels are used. Should hyperparameters of the model be
required, it is the responsability of the implementation to rescale the hyperparameters. Additionally, applying
hyperpriors should anticipate for the scaled data.
"""
def __init__(self, model, domain=None, normalize_Y=False):
"""
:param model: model to be wrapped
:param domain: (default: None) if supplied, the input transform is configured from the supplied domain to
:class:`.UnitCube`. If None, the input transform defaults to the identity transform.
:param normalize_Y: (default: False) enable automatic scaling of output values to zero mean and unit
variance.
"""
# model sanity checks
assert (model is not None)
assert (isinstance(model, GPModel))
self._parent = None
# Wrap model
self.wrapped = model
# Initial configuration of the datascaler
n_inputs = model.X.shape[1]
n_outputs = model.Y.shape[1]
self._input_transform = (domain or UnitCube(n_inputs)) >> UnitCube(n_inputs)
self._normalize_Y = normalize_Y
self._output_transform = LinearTransform(np.ones(n_outputs), np.zeros(n_outputs))
# The assignments in the constructor of GPModel take care of initial re-scaling of model data.
super(DataScaler, self).__init__(model.X.value, model.Y.value, None, None, None, name=model.name+"_datascaler")
del self.kern
del self.mean_function
del self.likelihood
def __getattr__(self, item):
"""
If an attribute is not found in this class, it is searched in the wrapped model
"""
return self.wrapped.__getattribute__(item)
def __setattr__(self, key, value):
"""
If setting :attr:`wrapped` attribute, point parent to this object (the datascaler)
"""
if key is 'wrapped':
object.__setattr__(self, key, value)
value.__setattr__('_parent', self)
return
super(DataScaler, self).__setattr__(key, value)
def __eq__(self, other):
return self.wrapped == other
def __str__(self, prepend=''):
return self.wrapped.__str__(prepend)
@property
def input_transform(self):
"""
Get the current input transform
:return: :class:`.DataTransform` input transform object
"""
return self._input_transform
@input_transform.setter
def input_transform(self, t):
"""
Configure a new input transform. Data in the model is automatically updated with the new transform.
:param t: :class:`.DataTransform` object: the new input transform.
"""
assert(isinstance(t, DataTransform))
X = self.X.value
self._input_transform.assign(t)
self.X = X
@property
def output_transform(self):
"""
Get the current output transform
:return: :class:`.DataTransform` output transform object
"""
return self._output_transform
@output_transform.setter
def output_transform(self, t):
"""
Configure a new output transform. Data in the model is automatically updated with the new transform.
:param t: :class:`.DataTransform` object: the new output transform.
"""
assert (isinstance(t, DataTransform))
Y = self.Y.value
self._output_transform.assign(t)
self.Y = Y
@property
def normalize_output(self):
"""
:return: boolean, indicating if output is automatically scaled to zero mean and unit variance.
"""
return self._normalize_Y
@normalize_output.setter
def normalize_output(self, flag):
"""
Enable/disable automated output scaling. If switched off, the output transform becomes the identity transform.
If enabled, data will be automatically scaled to zero mean and unit variance. When the output normalization is
switched on or off, the data in the model is automatically adapted.
:param flag: boolean, turn output scaling on or off
"""
self._normalize_Y = flag
if not flag:
# Output normalization turned off. Reset transform to identity
self.output_transform = LinearTransform(np.ones(self.Y.value.shape[1]), np.zeros(self.Y.value.shape[1]))
else:
# Output normalization enabled. Trigger scaling.
self.Y = self.Y.value
# Methods overwriting methods of the wrapped model.
@property
def X(self):
"""
Returns the input data of the model, unscaled.
:return: :class:`.DataHolder`: unscaled input data
"""
return DataHolder(self.input_transform.backward(self.wrapped.X.value))
@property
def Y(self):
"""
Returns the output data of the wrapped model, unscaled.
:return: :class:`.DataHolder`: unscaled output data
"""
return DataHolder(self.output_transform.backward(self.wrapped.Y.value))
@X.setter
def X(self, x):
"""
Set the input data. Applies the input transform before setting the data of the wrapped model.
"""
self.wrapped.X = self.input_transform.forward(x.value if isinstance(x, DataHolder) else x)
@Y.setter
def Y(self, y):
"""
Set the output data. In case normalize_Y=True, the appropriate output transform is updated. It is then
applied on the data before setting the data of the wrapped model.
"""
value = y.value if isinstance(y, DataHolder) else y
if self.normalize_output:
self.output_transform.assign(~LinearTransform(value.std(axis=0), value.mean(axis=0)))
self.wrapped.Y = self.output_transform.forward(value)
def build_predict(self, Xnew, full_cov=False):
"""
build_predict builds the TensorFlow graph for prediction. Similar to the method in the wrapped model, however
the input points are transformed using the input transform. The returned mean and variance are transformed
backward using the output transform.
"""
f, var = self.wrapped.build_predict(self.input_transform.build_forward(Xnew), full_cov=full_cov)
return self.output_transform.build_backward(f), self.output_transform.build_backward_variance(var)
@AutoFlow((float_type, [None, None]))
def predict_y(self, Xnew):
"""
Compute the mean and variance of held-out data at the points Xnew
"""
f, var = self.wrapped.build_predict(self.input_transform.build_forward(Xnew))
f, var = self.likelihood.predict_mean_and_var(f, var)
return self.output_transform.build_backward(f), self.output_transform.build_backward_variance(var)
@AutoFlow((float_type, [None, None]), (float_type, [None, None]))
def predict_density(self, Xnew, Ynew):
"""
Compute the (log) density of the data Ynew at the points Xnew
"""
mu, var = self.build_predict(Xnew)
Ys = self.output_transform.build_forward(Ynew)
return self.likelihood.predict_density(mu, var, Ys)