# Copyright 2016 James Hensman, Mark van der Wilk,
# Valentine Svensson, alexggmatthews,
# PabloLeon, fujiisoup
# Copyright 2017 Artem Artemev @awav
#
#
# 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.
import enum
import numpy as np
import tensorflow as tf
from .. import settings
from ..core.errors import GPflowError
from ..core.compilable import Build
from ..core.node import Node
from ..core.base import IPrior, ITransform
from .. import misc
from ..transforms import Identity
EMPTY_FEEDS = {}
class Parameter(Node):
"""
Parameter class is a cornerstone of the GPflow package. It wraps TensorFlow
variable and its prior and transformation building operations. In GPflow
computation graph the parameter is a leaf in the tree.
*Constrained* and *unconstrained* values of the parameter.
These definitions arise from optimization topic, constrained and unconstrained
optimization accordingly.
_Constrained_ value has acceptable value boundaries and user always works with
such values. It means that when you pass value to create parameter object, the
parameter assumes that this is _constrained_ value. When user reads value of
the parameter, the user gets _constrained_ value.
For example, variance cannot be negative, hence the constraint is [0, ∞).
```
param = gpflow.Param(1.0) # Here `1.0` is constrained value.
value = param.read_value() # `value` is constrained value too.
```
*Unconstrained* value doesn't have any limits. The necessity in this value
is caused by available set of TensorFlow optimizers. In fact, internal
parameter tensor is unconstrained and will be trained as unconstrained variable.
```
param = gpflow.Param(1.0) # `1.0` is constrained value.
param.parameter_tensor # unconstrained TensorFlow variable.
```
User can manage contraint type which will be used at a parameter. There is special
*transform* property and option in constructor for that. The user can pass one of
the implementations of `ITransform` interface. By default Identity transform is
applied, so that constrained has no difference from unconstrained, and variable
is simply considered as unconstrained.
```
param = gpflow.Param(1.0) # Identity transform is applied.
```
In example below, the parameter has exponential transform. It means that input
value is always must be positive [0, ∞), but internal unconstrained tensor has
no boundaries (∞, ∞).
```
param = gpflow.Param(1.0, transform=gpflow.transforms.Exp())
param.read_value()
# 1.0
gpflow.get_default_session().run(param.parameter_tensor)
# -1.0000005000290891e-06
```
*Parameter's shape*.
The parameter's shape is always fixed by default. It means that user is
not allowed to modify shape when parameter's tensors are built. User can
modify default behavior by setting up `fix_shape` option to `False`, the
parameter will be able to change its shape and internal parameter's tensor
will have floating shape - None. *NOTE: trainable parameters with floating
shape cannot be trained by a bunch of TensorFlow optimizers like RMSProp,
Adam as they require shape information for trainable variables in advance of
optimizer's tensors construction.*
:param value: Constrained input value. It can be a float, an integer,
a float or integer like list, numpy array or TensorFlow variable.
:param transform: Instance of GPflow.ITransform implementation.
:param prior: Instance of GPflow.IPrior implementation.
:param trainable: Boolean flag. It indicates whether variables
will be added to trainbles TensorFlow set or not.
:param dtype: Type of new parameter.
:param fix_shape: Default value is `True` and indicates that shape
of internal tensor will be the same as the shape of the input
variable and can not be changed. `False` will turn on floating
shape mode for the tensor.
:param name: Name of the parameter.
:raises: ValueError exception if value is not valid. In cases when
default graph used for building parameter differs from the graph
used during construction of priors or transformations, the GPflowError
exception is raised.
"""
class ParameterAttribute(enum.Enum):
PRIOR = 'prior'
TRANSFORM = 'transform'
TRAINABLE = 'trainable'
@property
def interface(self):
if self.value == self.PRIOR.value:
return IPrior
elif self.value == self.TRANSFORM.value:
return ITransform
def __init__(self, value, transform=None, prior=None,
trainable=True, dtype=None, fix_shape=True,
name=None):
self._externally_defined = False
self._fixed_shape = fix_shape
value = self._valid_input(value, dtype=dtype)
super().__init__(name)
self._init_parameter_defaults()
self._init_parameter_attributes(prior, transform, trainable)
self._init_parameter_value(value)
@property
def shape(self):
if self._externally_defined:
return tuple(self.parameter_tensor.shape.as_list())
return self._value.shape
@property
def dtype(self):
if self._externally_defined:
return self.parameter_tensor.dtype.as_numpy_dtype
return self._value.dtype
@property
def size(self):
"""The size of this parameter, equivalent to self.value.size"""
return np.multiply.reduce(self.shape, dtype=np.int32)
@property
def parameter_tensor(self):
return self._unconstrained_tensor
@property
def unconstrained_tensor(self):
return self._unconstrained_tensor
@property
def constrained_tensor(self):
return self._constrained_tensor
@property
def prior_tensor(self):
return self._prior_tensor
@property
def feeds(self):
return EMPTY_FEEDS
@property
def initializables(self):
if self._externally_defined:
return None
return [(self.parameter_tensor, self.is_initialized_tensor)]
@property
def initializable_feeds(self):
if self._externally_defined:
return EMPTY_FEEDS
return {self._initial_value_tensor: self._apply_transform(self._value)}
@property
def graph(self):
if self.parameter_tensor is None:
return None
return self.parameter_tensor.graph
@property
def fixed_shape(self):
return self._fixed_shape
@property
def value(self):
"""
The `value` property is simple alias for `read_value()` method called with
default arguments.
"""
return self.read_value()
@property
def is_initialized_tensor(self):
"""
:returns: Initialization status boolean tensor for parameter's variable.
"""
return self._is_initialized_tensor
def is_initialized(self, session):
if not isinstance(session, tf.Session):
raise ValueError('TensorFlow session expected for initializing test.')
if self.parameter_tensor is None:
return False
return session.run(self.is_initialized_tensor)
def fix_shape(self):
if self._fixed_shape:
return
if self.parameter_tensor is not None:
self.parameter_tensor.set_shape(self.shape)
self._fixed_shape = True
def anchor(self, session):
if not isinstance(session, tf.Session):
ValueError('TensorFlow Session expected when anchoring.')
if self.trainable:
value = self.read_value(session=session)
self.assign(value, session=session)
def is_built(self, graph):
if not isinstance(graph, tf.Graph):
raise ValueError('TensorFlow graph expected for build status testing.')
if self.graph and self.graph is not graph:
return Build.NOT_COMPATIBLE_GRAPH
elif self.prior_tensor is None:
return Build.NO
return Build.YES
@property
def trainable(self):
return self._trainable
@trainable.setter
def trainable(self, value):
self.set_trainable(value)
def set_trainable(self, value):
if not isinstance(value, bool):
raise ValueError('Fixed property value must be boolean.')
if self._externally_defined:
raise GPflowError('Externally defined parameter tensor is not modifiable.')
graph = self.graph
if graph is not None:
if self.trainable == value:
return
if value:
misc.add_to_trainables(self.parameter_tensor, graph)
else:
misc.remove_from_trainables(self.parameter_tensor, graph)
self._trainable = value
def assign(self, value, session=None, dtype=None, force=True):
if self._externally_defined:
raise GPflowError("Externally defined parameter tensor is not modifiable.")
value = self._valid_input(value, dtype)
if self.fixed_shape:
self._value[...] = value
else:
self._value = value.copy()
if self.is_built_coherence() is Build.YES:
session = self.enquire_session(session)
self.initialize(session=session, force=force)
def read_value(self, session=None):
if session is not None and not isinstance(session, tf.Session):
raise ValueError('TensorFlow session expected as an argument.')
if session is None and self._externally_defined:
raise GPflowError('Externally defined parameter requires session.')
elif session:
is_built = self.is_built_coherence(session.graph)
if is_built is Build.YES:
return self._read_parameter_tensor(session)
return self._value
def as_pandas_table(self):
column_names = ['class', 'prior', 'transform', 'trainable', 'shape', 'fixed_shape', 'value']
column_values = [self.__class__.__name__, str(self.prior), str(self.transform),
self.trainable, self.shape, self.fixed_shape, self.value.copy()]
column_values = [[value] for value in column_values]
df = misc.pretty_pandas_table([self.pathname], column_names, column_values)
return df
def tf_compilation_index(self):
"""
Takes out index from the parameter's tensor name. E.g. parameter tensor name is
GPR-0000/kern/lengthscales, the method for that parameter will return '0000' index.
"""
if self.parameter_tensor is None:
return None
name = self.parameter_tensor.name
return name.split('-', 1)[-1].split('/')[0]
def _valid_input(self, value, dtype=None):
if not misc.is_valid_param_value(value):
msg = 'The value must be either a tensorflow variable, an array or a scalar.'
raise ValueError(msg)
cast = not (dtype is None)
is_built = False
shape = None
if hasattr(self, '_value'): # The parameter has not initialized yet.
is_built = self.is_built_coherence() == Build.YES
shape = self.shape
inner_dtype = self.dtype
if dtype is not None and inner_dtype != dtype:
msg = 'Overriding parameter\'s type "{0}" with "{1}" is not possible.'
raise ValueError(msg.format(inner_dtype, dtype))
elif isinstance(value, np.ndarray) and inner_dtype != value.dtype:
msg = 'The value has different data type "{0}". Parameter type is "{1}".'
raise ValueError(msg.format(value.dtype, inner_dtype))
cast = False
dtype = inner_dtype
if misc.is_number(value):
value_type = np.result_type(value).type
num_type = misc.normalize_num_type(value_type)
dtype = num_type if dtype is None else dtype
value = np.array(value, dtype=dtype)
elif misc.is_list(value):
dtype = settings.float_type if dtype is None else dtype
value = np.array(value, dtype=dtype)
elif cast and not misc.is_tensor(value):
value = value.astype(dtype)
if shape is not None and self.fixed_shape and is_built and shape != value.shape:
msg = 'Value has different shape. Parameter shape {0}, value shape {1}.'
raise ValueError(msg.format(shape, value.shape))
return value
def _clear(self):
self.reset_name()
self._externally_defined = False
self._is_initialized_tensor = None
self._initial_value_tensor = None
self._unconstrained_tensor = None
self._constrained_tensor = None
self._prior_tensor = None
def _build(self):
unconstrained = self._build_parameter()
constrained = self._build_constrained(unconstrained)
prior = self._build_prior(unconstrained, constrained)
self._is_initialized_tensor = True
if not isinstance(unconstrained, tf.Tensor):
self._is_initialized_tensor = tf.is_variable_initialized(unconstrained)
self._unconstrained_tensor = unconstrained
self._constrained_tensor = constrained
self._prior_tensor = prior
def _build_parameter(self):
if self._externally_defined:
self._check_tensor_trainable(self.parameter_tensor)
return self.parameter_tensor
name = self._parameter_name()
tensor = misc.get_variable_by_name(name)
if tensor is not None:
raise GPflowError('Tensor with name "{name}" already exists, {tensor}.'
.format(name=name, tensor=tensor))
value = self._apply_transform(self._value)
shape = value.shape if self.fixed_shape else None
init = tf.placeholder(self.dtype, shape=shape, name='initial_unconstrained_value')
self._initial_value_tensor = init
if self.fixed_shape:
args = dict(trainable=self.trainable)
else:
args = dict(validate_shape=False, trainable=self.trainable)
variable = tf.get_variable(name, initializer=init, **args)
return variable
def _build_constrained(self, parameter_tensor):
if not misc.is_tensor(parameter_tensor): # pragma: no cover
raise GPflowError("Input must be a tensor.")
return self.transform.forward_tensor(parameter_tensor)
def _build_prior(self, unconstrained_tensor, constrained_tensor):
"""
Build a tensorflow representation of the prior density.
The log Jacobian is included.
"""
if not misc.is_tensor(unconstrained_tensor):
raise GPflowError("Unconstrained input must be a tensor.")
if not misc.is_tensor(constrained_tensor):
raise GPflowError("Constrained input must be a tensor.")
prior_name = 'prior'
if self.prior is None:
return tf.constant(0.0, settings.float_type, name=prior_name)
log_jacobian = self.transform.log_jacobian_tensor(unconstrained_tensor)
logp_var = self.prior.logp(constrained_tensor)
return tf.squeeze(tf.add(logp_var, log_jacobian, name=prior_name))
def _check_tensor_trainable(self, tensor):
is_trainable = misc.is_tensor_trainable(tensor)
if is_trainable != self.trainable:
tensor_status = 'trainable' if is_trainable else 'not trainable'
param_status = 'trainable' if self.trainable else 'not'
msg = 'Externally defined tensor is {0} whilst parameter is {1}.'
raise GPflowError(msg.format(tensor_status, param_status))
def _init_parameter_defaults(self):
self._is_initialized_tensor = None
self._initial_value_tensor = None
self._unconstrained_tensor = None
self._prior_tensor = None
self._constrained_tensor = None
def _init_parameter_value(self, value):
if misc.is_tensor(value):
is_trainable = misc.is_tensor_trainable(value)
if is_trainable != self.trainable:
status = 'trainable' if is_trainable else 'not trainable'
raise ValueError('Externally defined tensor is {0}.'.format(status))
self._fixed_shape = True
self._externally_defined = True
self._set_parameter_tensor(value)
else:
self._value = value.copy()
def _init_parameter_attributes(self, prior, transform, trainable):
if transform is None:
transform = Identity()
self.prior = prior # pylint: disable=W0201
self.transform = transform # pylint: disable=W0201
self.trainable = trainable # pylint: disable=W0201
def _read_parameter_tensor(self, session):
return session.run(self.constrained_tensor)
def _apply_transform(self, value):
return self.transform.backward(value)
def _parameter_name(self):
return misc.tensor_name(self.tf_pathname, 'unconstrained')
def _set_parameter_tensor(self, tensor):
self._unconstrained_tensor = tensor
def _set_parameter_attribute(self, attr, value):
if attr is self.ParameterAttribute.TRAINABLE:
self.set_trainable(value)
return
is_built = self.is_built_coherence(self.graph)
if is_built is Build.YES:
raise GPflowError('Parameter "{}" has already been compiled.'.format(self.pathname))
name = attr.value
if value is not None and not isinstance(value, attr.interface):
msg = 'Attribute "{0}" must implement interface "{1}".'
raise GPflowError(msg.format(name, attr.interface))
object.__setattr__(self, name, value)
def __setattr__(self, name, value):
try:
attr = self.ParameterAttribute[name.upper()]
self._set_parameter_attribute(attr, value)
return
except KeyError:
pass
object.__setattr__(self, name, value)
def __str__(self):
return str(self.as_pandas_table())
def _repr_html_(self):
return self.as_pandas_table()._repr_html_()
@property
def fixed(self):
raise NotImplementedError("`fixed` property is no longer supported. Please use `trainable` instead.")
@fixed.setter
def fixed(self, _):
raise NotImplementedError("`fixed` property is no longer supported. Please use `trainable` instead.")