GPy/GPy/core/parameterization/transformations.py

427 lines
16 KiB
Python
Raw Normal View History

2013-05-01 17:11:13 +01:00
# Copyright (c) 2012, GPy authors (see AUTHORS.txt).
# Licensed under the BSD 3-clause license (see LICENSE.txt)
import numpy as np
from domains import _POSITIVE,_NEGATIVE, _BOUNDED
import weakref
import sys
2014-02-26 08:24:12 +00:00
_exp_lim_val = np.finfo(np.float64).max
_lim_val = 36.0
epsilon = np.finfo(np.float64).resolution
2013-05-01 17:11:13 +01:00
#===============================================================================
# Fixing constants
__fixed__ = "fixed"
FIXED = False
UNFIXED = True
#===============================================================================
2013-09-04 09:30:47 +01:00
class Transformation(object):
2013-06-04 17:03:29 +01:00
domain = None
2013-09-20 17:21:28 +01:00
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance or cls._instance.__class__ is not cls:
cls._instance = super(Transformation, cls).__new__(cls, *args, **kwargs)
2013-09-20 17:21:28 +01:00
return cls._instance
def f(self, opt_param):
2013-05-01 17:11:13 +01:00
raise NotImplementedError
def finv(self, model_param):
2013-05-01 17:11:13 +01:00
raise NotImplementedError
def gradfactor(self, model_param, dL_dmodel_param):
""" df(opt_param)_dopt_param evaluated at self.f(opt_param)=model_param, times the gradient dL_dmodel_param,
i.e.:
define
.. math::
\frac{\frac{\partial L}{\partial f}\left(\left.\partial f(x)}{\partial x}\right|_{x=f^{-1}(f)\right)}
"""
2013-05-01 17:11:13 +01:00
raise NotImplementedError
2013-05-08 15:34:25 +01:00
def initialize(self, f):
""" produce a sensible initial value for f(x)"""
2013-05-01 17:11:13 +01:00
raise NotImplementedError
2014-02-26 08:24:12 +00:00
def plot(self, xlabel=r'transformed $\theta$', ylabel=r'$\theta$', axes=None, *args,**kw):
assert "matplotlib" in sys.modules, "matplotlib package has not been imported."
import matplotlib.pyplot as plt
from ...plotting.matplot_dep import base_plots
x = np.linspace(-8,8)
base_plots.meanplot(x, self.f(x),axes=axes*args,**kw)
axes = plt.gca()
axes.set_xlabel(xlabel)
axes.set_ylabel(ylabel)
2013-05-01 17:11:13 +01:00
def __str__(self):
raise NotImplementedError
2014-02-12 17:11:55 +00:00
def __repr__(self):
return self.__class__.__name__
2013-05-01 17:11:13 +01:00
2013-09-04 09:30:47 +01:00
class Logexp(Transformation):
domain = _POSITIVE
2013-05-08 15:34:25 +01:00
def f(self, x):
return np.where(x>_lim_val, x, np.log1p(np.exp(np.clip(x, -_lim_val, _lim_val)))) + epsilon
2014-02-13 13:53:49 +00:00
#raises overflow warning: return np.where(x>_lim_val, x, np.log(1. + np.exp(x)))
2013-05-08 15:34:25 +01:00
def finv(self, f):
return np.where(f>_lim_val, f, np.log(np.exp(f+1e-20) - 1.))
def gradfactor(self, f, df):
return np.einsum('i,i->i', df, np.where(f>_lim_val, 1., 1. - np.exp(-f)))
2013-05-08 15:34:25 +01:00
def initialize(self, f):
if np.any(f < 0.):
print "Warning: changing parameters to satisfy constraints"
2013-05-08 15:34:25 +01:00
return np.abs(f)
def __str__(self):
2013-10-11 16:46:12 +01:00
return '+ve'
class NormalTheta(Transformation):
_instances = []
def __new__(cls, mu_indices, var_indices):
if cls._instances:
cls._instances[:] = [instance for instance in cls._instances if instance()]
for instance in cls._instances:
if np.all(instance().mu_indices==mu_indices, keepdims=False) and np.all(instance().var_indices==var_indices, keepdims=False):
return instance()
o = super(Transformation, cls).__new__(cls, mu_indices, var_indices)
cls._instances.append(weakref.ref(o))
return cls._instances[-1]()
def __init__(self, mu_indices, var_indices):
self.mu_indices = mu_indices
self.var_indices = var_indices
def f(self, theta):
# In here abs is only a trick to make sure the numerics are ok.
# The variance will never go below zero, but at initialization we need to make sure
# that the values are ok
# Before:
theta[self.var_indices] = np.abs(-.5/theta[self.var_indices])
theta[self.mu_indices] *= theta[self.var_indices]
return theta # which is now {mu, var}
def finv(self, muvar):
# before:
varp = muvar[self.var_indices]
muvar[self.mu_indices] /= varp
muvar[self.var_indices] = -.5/varp
return muvar # which is now {theta1, theta2}
def gradfactor(self, muvar, dmuvar):
mu = muvar[self.mu_indices]
var = muvar[self.var_indices]
#=======================================================================
# theta gradients
# This works and the gradient checks!
dmuvar[self.mu_indices] *= var
dmuvar[self.var_indices] *= 2*(var)**2
dmuvar[self.var_indices] += 2*dmuvar[self.mu_indices]*mu
#=======================================================================
return dmuvar # which is now the gradient multiplicator for {theta1, theta2}
def initialize(self, f):
if np.any(f[self.var_indices] < 0.):
print "Warning: changing parameters to satisfy constraints"
f[self.var_indices] = np.abs(f[self.var_indices])
return f
def __str__(self):
return "theta"
class NormalNaturalAntti(NormalTheta):
_instances = []
_logexp = Logexp()
def __new__(cls, mu_indices, var_indices):
if cls._instances:
cls._instances[:] = [instance for instance in cls._instances if instance()]
for instance in cls._instances:
if np.all(instance().mu_indices==mu_indices, keepdims=False) and np.all(instance().var_indices==var_indices, keepdims=False):
return instance()
o = super(Transformation, cls).__new__(cls, mu_indices, var_indices)
cls._instances.append(weakref.ref(o))
return cls._instances[-1]()
def __init__(self, mu_indices, var_indices):
self.mu_indices = mu_indices
self.var_indices = var_indices
def gradfactor(self, muvar, dmuvar):
mu = muvar[self.mu_indices]
var = muvar[self.var_indices]
#=======================================================================
# theta gradients
# This works and the gradient checks!
dmuvar[self.mu_indices] *= var
dmuvar[self.var_indices] *= 2*var**2#np.einsum('i,i,i,i->i', dmuvar[self.var_indices], [2], var, var)
#=======================================================================
return dmuvar # which is now the gradient multiplicator
def initialize(self, f):
if np.any(f[self.var_indices] < 0.):
print "Warning: changing parameters to satisfy constraints"
f[self.var_indices] = np.abs(f[self.var_indices])
return f
def __str__(self):
return "natantti"
class NormalEta(Transformation):
_instances = []
def __new__(cls, mu_indices, var_indices):
if cls._instances:
cls._instances[:] = [instance for instance in cls._instances if instance()]
for instance in cls._instances:
if np.all(instance().mu_indices==mu_indices, keepdims=False) and np.all(instance().var_indices==var_indices, keepdims=False):
return instance()
o = super(Transformation, cls).__new__(cls, mu_indices, var_indices)
cls._instances.append(weakref.ref(o))
return cls._instances[-1]()
def __init__(self, mu_indices, var_indices):
self.mu_indices = mu_indices
self.var_indices = var_indices
def f(self, theta):
theta[self.var_indices] = np.abs(theta[self.var_indices] - theta[self.mu_indices]**2)
return theta # which is now {mu, var}
def finv(self, muvar):
muvar[self.var_indices] += muvar[self.mu_indices]**2
return muvar # which is now {eta1, eta2}
def gradfactor(self, muvar, dmuvar):
mu = muvar[self.mu_indices]
#=======================================================================
# Lets try natural gradients instead: Not working with bfgs... try stochastic!
dmuvar[self.mu_indices] -= 2*mu*dmuvar[self.var_indices]
#=======================================================================
return dmuvar # which is now the gradient multiplicator
def initialize(self, f):
if np.any(f[self.var_indices] < 0.):
print "Warning: changing parameters to satisfy constraints"
f[self.var_indices] = np.abs(f[self.var_indices])
return f
def __str__(self):
return "eta"
class NormalNaturalThroughTheta(NormalTheta):
_instances = []
def __new__(cls, mu_indices, var_indices):
if cls._instances:
cls._instances[:] = [instance for instance in cls._instances if instance()]
for instance in cls._instances:
if np.all(instance().mu_indices==mu_indices, keepdims=False) and np.all(instance().var_indices==var_indices, keepdims=False):
return instance()
o = super(Transformation, cls).__new__(cls, mu_indices, var_indices)
cls._instances.append(weakref.ref(o))
return cls._instances[-1]()
def __init__(self, mu_indices, var_indices):
self.mu_indices = mu_indices
self.var_indices = var_indices
def gradfactor(self, muvar, dmuvar):
mu = muvar[self.mu_indices]
var = muvar[self.var_indices]
#=======================================================================
# This is just eta direction:
dmuvar[self.mu_indices] -= 2*mu*dmuvar[self.var_indices]
#=======================================================================
#=======================================================================
# This is by going through theta fully and then going into eta direction:
#dmu = dmuvar[self.mu_indices]
#dmuvar[self.var_indices] += dmu*mu*(var + 4/var)
#=======================================================================
return dmuvar # which is now the gradient multiplicator
def __str__(self):
return "natgrad"
class NormalNaturalThroughEta(NormalEta):
_instances = []
def __new__(cls, mu_indices, var_indices):
if cls._instances:
cls._instances[:] = [instance for instance in cls._instances if instance()]
for instance in cls._instances:
if np.all(instance().mu_indices==mu_indices, keepdims=False) and np.all(instance().var_indices==var_indices, keepdims=False):
return instance()
o = super(Transformation, cls).__new__(cls, mu_indices, var_indices)
cls._instances.append(weakref.ref(o))
return cls._instances[-1]()
def __init__(self, mu_indices, var_indices):
self.mu_indices = mu_indices
self.var_indices = var_indices
def gradfactor(self, muvar, dmuvar):
mu = muvar[self.mu_indices]
var = muvar[self.var_indices]
#=======================================================================
# theta gradients
# This works and the gradient checks!
dmuvar[self.mu_indices] *= var
dmuvar[self.var_indices] *= 2*(var)**2
dmuvar[self.var_indices] += 2*dmuvar[self.mu_indices]*mu
#=======================================================================
return dmuvar
def __str__(self):
return "natgrad"
2014-02-26 08:24:12 +00:00
class LogexpNeg(Transformation):
domain = _POSITIVE
def f(self, x):
return np.where(x>_lim_val, -x, -np.log(1. + np.exp(np.clip(x, -np.inf, _lim_val))))
#raises overflow warning: return np.where(x>_lim_val, x, np.log(1. + np.exp(x)))
def finv(self, f):
return np.where(f>_lim_val, 0, np.log(np.exp(-f) - 1.))
def gradfactor(self, f, df):
return np.einsum('i,i->i', df, np.where(f>_lim_val, -1, -1 + np.exp(-f)))
2014-02-26 08:24:12 +00:00
def initialize(self, f):
if np.any(f < 0.):
print "Warning: changing parameters to satisfy constraints"
return np.abs(f)
def __str__(self):
return '+ve'
2013-05-08 15:34:25 +01:00
2013-10-02 19:14:35 +01:00
class NegativeLogexp(Transformation):
domain = _NEGATIVE
2013-10-11 16:46:12 +01:00
logexp = Logexp()
def f(self, x):
2013-10-11 16:46:12 +01:00
return -self.logexp.f(x) # np.log(1. + np.exp(x))
def finv(self, f):
2013-10-11 16:46:12 +01:00
return self.logexp.finv(-f) # np.log(np.exp(-f) - 1.)
def gradfactor(self, f, df):
return np.einsum('i,i->i', df, -self.logexp.gradfactor(-f))
def initialize(self, f):
2013-10-11 16:46:12 +01:00
return -self.logexp.initialize(f) # np.abs(f)
def __str__(self):
2013-10-11 16:46:12 +01:00
return '-ve'
2013-09-04 09:30:47 +01:00
class LogexpClipped(Logexp):
max_bound = 1e100
2013-05-22 17:39:27 +01:00
min_bound = 1e-10
log_max_bound = np.log(max_bound)
log_min_bound = np.log(min_bound)
domain = _POSITIVE
_instances = []
2013-09-20 17:21:28 +01:00
def __new__(cls, lower=1e-6, *args, **kwargs):
if cls._instances:
cls._instances[:] = [instance for instance in cls._instances if instance()]
for instance in cls._instances:
if instance().lower == lower:
return instance()
o = super(Transformation, cls).__new__(cls, lower, *args, **kwargs)
cls._instances.append(weakref.ref(o))
return cls._instances[-1]()
def __init__(self, lower=1e-6):
2013-05-10 17:32:26 +01:00
self.lower = lower
2013-05-08 15:34:25 +01:00
def f(self, x):
exp = np.exp(np.clip(x, self.log_min_bound, self.log_max_bound))
f = np.log(1. + exp)
2013-05-22 17:16:10 +01:00
# if np.isnan(f).any():
# import ipdb;ipdb.set_trace()
2013-05-22 17:39:27 +01:00
return np.clip(f, self.min_bound, self.max_bound)
2013-05-08 15:34:25 +01:00
def finv(self, f):
return np.log(np.exp(f - 1.))
def gradfactor(self, f, df):
2013-05-20 10:11:27 +01:00
ef = np.exp(f) # np.clip(f, self.min_bound, self.max_bound))
2013-05-10 16:49:37 +01:00
gf = (ef - 1.) / ef
return np.einsum('i,i->i', df, gf) # np.where(f < self.lower, 0, gf)
def initialize(self, f):
if np.any(f < 0.):
print "Warning: changing parameters to satisfy constraints"
2013-05-01 17:11:13 +01:00
return np.abs(f)
def __str__(self):
2013-10-11 16:46:12 +01:00
return '+ve_c'
2013-05-01 17:11:13 +01:00
2013-09-04 09:30:47 +01:00
class Exponent(Transformation):
# TODO: can't allow this to go to zero, need to set a lower bound. Similar with negative Exponent below. See old MATLAB code.
domain = _POSITIVE
2013-05-08 15:34:25 +01:00
def f(self, x):
return np.where(x<_lim_val, np.where(x>-_lim_val, np.exp(x), np.exp(-_lim_val)), np.exp(_lim_val))
2013-05-08 15:34:25 +01:00
def finv(self, x):
2013-05-01 17:11:13 +01:00
return np.log(x)
def gradfactor(self, f, df):
return np.einsum('i,i->i', df, f)
2013-05-08 15:34:25 +01:00
def initialize(self, f):
if np.any(f < 0.):
print "Warning: changing parameters to satisfy constraints"
2013-05-01 17:11:13 +01:00
return np.abs(f)
def __str__(self):
2013-10-11 16:46:12 +01:00
return '+ve'
2013-05-01 17:11:13 +01:00
2013-09-04 09:30:47 +01:00
class NegativeExponent(Exponent):
domain = _NEGATIVE
2013-05-08 15:34:25 +01:00
def f(self, x):
2013-09-04 09:30:47 +01:00
return -Exponent.f(x)
def finv(self, f):
2013-09-04 09:30:47 +01:00
return Exponent.finv(-f)
def gradfactor(self, f, df):
return np.einsum('i,i->i', df, f)
2013-05-08 15:34:25 +01:00
def initialize(self, f):
2013-09-04 09:30:47 +01:00
return -Exponent.initialize(f) #np.abs(f)
2013-05-01 17:11:13 +01:00
def __str__(self):
2013-10-11 16:46:12 +01:00
return '-ve'
2013-05-01 17:11:13 +01:00
2013-09-04 09:30:47 +01:00
class Square(Transformation):
domain = _POSITIVE
2013-05-08 15:34:25 +01:00
def f(self, x):
return x ** 2
def finv(self, x):
return np.sqrt(x)
def gradfactor(self, f, df):
return np.einsum('i,i->i', df, 2 * np.sqrt(f))
2013-05-08 15:34:25 +01:00
def initialize(self, f):
return np.abs(f)
def __str__(self):
2013-10-11 16:46:12 +01:00
return '+sq'
2013-05-08 15:34:25 +01:00
2013-09-04 09:30:47 +01:00
class Logistic(Transformation):
domain = _BOUNDED
_instances = []
2013-09-20 17:21:28 +01:00
def __new__(cls, lower=1e-6, upper=1e-6, *args, **kwargs):
if cls._instances:
cls._instances[:] = [instance for instance in cls._instances if instance()]
for instance in cls._instances:
if instance().lower == lower and instance().upper == upper:
return instance()
o = super(Transformation, cls).__new__(cls, lower, upper, *args, **kwargs)
cls._instances.append(weakref.ref(o))
return cls._instances[-1]()
2013-05-08 15:34:25 +01:00
def __init__(self, lower, upper):
2013-05-01 17:11:13 +01:00
assert lower < upper
self.lower, self.upper = float(lower), float(upper)
self.difference = self.upper - self.lower
2013-05-08 15:34:25 +01:00
def f(self, x):
if (x<-300.).any():
x = x.copy()
x[x<-300.] = -300.
2013-05-08 15:34:25 +01:00
return self.lower + self.difference / (1. + np.exp(-x))
def finv(self, f):
2013-05-01 17:11:13 +01:00
return np.log(np.clip(f - self.lower, 1e-10, np.inf) / np.clip(self.upper - f, 1e-10, np.inf))
def gradfactor(self, f, df):
return np.einsum('i,i->i', df, (f - self.lower) * (self.upper - f) / self.difference)
def initialize(self, f):
if np.any(np.logical_or(f < self.lower, f > self.upper)):
print "Warning: changing parameters to satisfy constraints"
2014-02-07 15:16:05 +00:00
#return np.where(np.logical_or(f < self.lower, f > self.upper), self.f(f * 0.), f)
#FIXME: Max, zeros_like right?
return np.where(np.logical_or(f < self.lower, f > self.upper), self.f(np.zeros_like(f)), f)
2013-05-01 17:11:13 +01:00
def __str__(self):
2013-10-11 16:46:12 +01:00
return '{},{}'.format(self.lower, self.upper)
2013-05-01 17:11:13 +01:00
2014-02-07 15:16:05 +00:00