diff --git a/GPy/core/parameterization/variational.py b/GPy/core/parameterization/variational.py index ef4d974d..c51a8021 100644 --- a/GPy/core/parameterization/variational.py +++ b/GPy/core/parameterization/variational.py @@ -9,7 +9,10 @@ from parameterized import Parameterized from param import Param from transformations import Logexp -class VariationalPrior(object): +class VariationalPrior(Parameterized): + def __init__(self, name=None, **kw): + super(VariationalPrior, self).__init__(name=name, **kw) + def KL_divergence(self, variational_posterior): raise NotImplementedError, "override this for variational inference of latent space" @@ -19,7 +22,7 @@ class VariationalPrior(object): """ raise NotImplementedError, "override this for variational inference of latent space" -class NormalPrior(VariationalPrior): +class NormalPrior(VariationalPrior): def KL_divergence(self, variational_posterior): var_mean = np.square(variational_posterior.mean).sum() var_S = (variational_posterior.variance - np.log(variational_posterior.variance)).sum() @@ -30,6 +33,32 @@ class NormalPrior(VariationalPrior): variational_posterior.mean.gradient -= variational_posterior.mean variational_posterior.variance.gradient -= (1. - (1. / (variational_posterior.variance))) * 0.5 +class SpikeAndSlabPrior(VariationalPrior): + def __init__(self, variance = 1.0, pi = 0.5, name='SpikeAndSlabPrior', **kw): + super(VariationalPrior, self).__init__(name=name, **kw) + assert variance==1.0, "Not Implemented!" + self.pi = Param('pi', pi) + self.variance = Param('variance',variance) + self.add_parameters(self.pi, self.variance) + + def KL_divergence(self, variational_posterior): + mu = variational_posterior.mean + S = variational_posterior.variance + gamma = variational_posterior.binary_prob + var_mean = np.square(mu) + var_S = (S - np.log(S)) + var_gamma = (gamma*np.log(gamma/self.pi)).sum()+((1-gamma)*np.log((1-gamma)/(1-self.pi))).sum() + return var_gamma+ 0.5 * (gamma* (var_mean + var_S -1)).sum() + + def update_gradients_KL(self, variational_posterior): + mu = variational_posterior.mean + S = variational_posterior.variance + gamma = variational_posterior.binary_prob + + gamma.gradient -= np.log((1-self.pi)/self.pi*gamma/(1.-gamma))+(np.square(mu)+S-np.log(S)-1.)/2. + mu.gradient -= gamma*mu + S.gradient -= (1. - (1. / (S))) * gamma /2. + class VariationalPosterior(Parameterized): def __init__(self, means=None, variances=None, name=None, **kw): @@ -65,7 +94,7 @@ class NormalPosterior(VariationalPosterior): from ...plotting.matplot_dep import variational_plots return variational_plots.plot(self,*args) -class SpikeAndSlab(VariationalPosterior): +class SpikeAndSlabPosterior(VariationalPosterior): ''' The SpikeAndSlab distribution for variational approximations. ''' @@ -73,7 +102,7 @@ class SpikeAndSlab(VariationalPosterior): """ binary_prob : the probability of the distribution on the slab part. """ - super(SpikeAndSlab, self).__init__(means, variances, name) + super(SpikeAndSlabPosterior, self).__init__(means, variances, name) self.gamma = Param("binary_prob",binary_prob,) self.add_parameter(self.gamma) diff --git a/GPy/kern/__init__.py b/GPy/kern/__init__.py index 030cba66..5dd6e554 100644 --- a/GPy/kern/__init__.py +++ b/GPy/kern/__init__.py @@ -9,3 +9,4 @@ from _src.mlp import MLP from _src.periodic import PeriodicExponential, PeriodicMatern32, PeriodicMatern52 from _src.independent_outputs import IndependentOutputs, Hierarchical from _src.coregionalize import Coregionalize +from _src.ssrbf import SSRBF diff --git a/GPy/kern/_src/ssrbf.py b/GPy/kern/_src/ssrbf.py new file mode 100644 index 00000000..60df7225 --- /dev/null +++ b/GPy/kern/_src/ssrbf.py @@ -0,0 +1,252 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + + +from kern import Kern +import numpy as np +from ...util.linalg import tdot +from ...util.config import * +from ...util.caching import cache_this +from stationary import Stationary + +class SSRBF(Stationary): + """ + Radial Basis Function kernel, aka squared-exponential, exponentiated quadratic or Gaussian kernel + for Spike-and-Slab GPLVM + + .. math:: + + k(r) = \sigma^2 \exp \\bigg(- \\frac{1}{2} r^2 \\bigg) \ \ \ \ \ \\text{ where } r^2 = \sum_{i=1}^d \\frac{ (x_i-x^\prime_i)^2}{\ell_i^2} + + where \ell_i is the lengthscale, \sigma^2 the variance and d the dimensionality of the input. + + :param input_dim: the number of input dimensions + :type input_dim: int + :param variance: the variance of the kernel + :type variance: float + :param lengthscale: the vector of lengthscale of the kernel + :type lengthscale: array or list of the appropriate size (or float if there is only one lengthscale parameter) + :param ARD: Auto Relevance Determination. If equal to "False", the kernel is isotropic (ie. one single lengthscale parameter \ell), otherwise there is one lengthscale parameter per dimension. + :type ARD: Boolean + :rtype: kernel object + + .. Note: this object implements both the ARD and 'spherical' version of the function + """ + + def __init__(self, input_dim, variance=1., lengthscale=None, ARD=True, name='SSRBF'): + assert ARD==True, "Not Implemented!" + super(SSRBF, self).__init__(input_dim, variance, lengthscale, ARD, name) + + def K_of_r(self, r): + return self.variance * np.exp(-0.5 * r**2) + + def dK_dr(self, r): + return -r*self.K_of_r(r) + + def parameters_changed(self): + pass + + def Kdiag(self, X): + ret = np.empty(X.shape[0]) + ret[:] = self.variance + return ret + + #---------------------------------------# + # PSI statistics # + #---------------------------------------# + + def psi0(self, Z, posterior_variational): + ret = np.empty(posterior_variational.mean.shape[0]) + ret[:] = self.variance + return ret + + def psi1(self, Z, posterior_variational): + self._psi_computations(Z, posterior_variational.mean, posterior_variational.variance, posterior_variational.binary_prob) + return self._psi1 + + def psi2(self, Z, posterior_variational): + self._psi_computations(Z, posterior_variational.mean, posterior_variational.variance, posterior_variational.binary_prob) + return self._psi2 + + def dL_dpsi0_dmuSgamma(self, dL_dpsi0, Z, mu, S, gamma, target_mu, target_S, target_gamma): + pass + + + def dL_dpsi1_dmuSgamma(self, dL_dpsi1, Z, mu, S, gamma, target_mu, target_S, target_gamma): + self._psi_computations(Z, mu, S, gamma) + target_mu += (dL_dpsi1[:, :, None] * self._dpsi1_dmu).sum(axis=1) + target_S += (dL_dpsi1[:, :, None] * self._dpsi1_dS).sum(axis=1) + target_gamma += (dL_dpsi1[:,:,None] * self._dpsi1_dgamma).sum(axis=1) + + + def dL_dpsi2_dmuSgamma(self, dL_dpsi2, Z, mu, S, gamma, target_mu, target_S, target_gamma): + """Think N,num_inducing,num_inducing,input_dim """ + self._psi_computations(Z, mu, S, gamma) + target_mu += (dL_dpsi2[:, :, :, None] * self._dpsi2_dmu).reshape(mu.shape[0],-1,mu.shape[1]).sum(axis=1) + target_S += (dL_dpsi2[:, :, :, None] * self._dpsi2_dS).reshape(S.shape[0],-1,S.shape[1]).sum(axis=1) + target_gamma += (dL_dpsi2[:,:,:, None] *self._dpsi2_dgamma).reshape(gamma.shape[0],-1,gamma.shape[1]).sum(axis=1) + + def update_gradients_variational(self, dL_dKmm, dL_dpsi0, dL_dpsi1, dL_dpsi2, Z, posterior_variational): + self._psi_computations(Z, posterior_variational.mean, posterior_variational.variance, posterior_variational.binary_prob) + + #contributions from psi0: + self.variance.gradient = np.sum(dL_dpsi0) + + #from psi1 + self.variance.gradient += np.sum(dL_dpsi1 * self._dpsi1_dvariance) + self.lengthscale.gradient = (dL_dpsi1[:,:,None]*self._dpsi1_dlengthscale).reshape(-1,self.input_dim).sum(axis=0) + + + #from psi2 + self.variance.gradient += (dL_dpsi2 * self._dpsi2_dvariance).sum() + self.lengthscale.gradient += (dL_dpsi2[:,:,:,None] * self._dpsi2_dlengthscale).reshape(-1,self.input_dim).sum(axis=0) + + #from Kmm + self._K_computations(Z, None) + dvardLdK = self._K_dvar * dL_dKmm + var_len3 = self.variance / (np.square(self.lengthscale)*self.lengthscale) + + self.variance.gradient += np.sum(dvardLdK) + self.lengthscale.gradient += (np.square(Z[:,None,:]-Z[None,:,:])*dvardLdK[:,:,None]).reshape(-1,self.input_dim).sum(axis=0)*var_len3 + + + def gradients_Z_variational(self, dL_dKmm, dL_dpsi0, dL_dpsi1, dL_dpsi2, Z, posterior_variational): + self._psi_computations(Z, posterior_variational.mean, posterior_variational.variance, posterior_variational.binary_prob) + + #psi1 + grad = (dL_dpsi1[:, :, None] * self._dpsi1_dZ).sum(axis=0) + + #psi2 + grad += (dL_dpsi2[:, :, :, None] * self._dpsi2_dZ).sum(axis=0).sum(axis=1) + + grad += self.gradients_X(dL_dKmm, Z, None) + + return grad + + def gradients_q_variational(self, dL_dKmm, dL_dpsi0, dL_dpsi1, dL_dpsi2, Z, posterior_variational): + ndata = posterior_variational.mean.shape[0] + self._psi_computations(Z, posterior_variational.mean, posterior_variational.variance, posterior_variational.binary_prob) + #psi1 + grad_mu = (dL_dpsi1[:, :, None] * self._dpsi1_dmu).sum(axis=1) + grad_S = (dL_dpsi1[:, :, None] * self._dpsi1_dS).sum(axis=1) + grad_gamma = (dL_dpsi1[:,:,None] * self._dpsi1_dgamma).sum(axis=1) + #psi2 + grad_mu += (dL_dpsi2[:, :, :, None] * self._dpsi2_dmu).reshape(ndata,-1,self.input_dim).sum(axis=1) + grad_S += (dL_dpsi2[:, :, :, None] * self._dpsi2_dS).reshape(ndata,-1,self.input_dim).sum(axis=1) + grad_gamma += (dL_dpsi2[:,:,:, None] *self._dpsi2_dgamma).reshape(ndata,-1,self.input_dim).sum(axis=1) + + return grad_mu, grad_S, grad_gamma + + def gradients_X(self, dL_dK, X, X2=None): + #if self._X is None or X.base is not self._X.base or X2 is not None: + if X2==None: + _K_dist = X[:,None,:] - X[None,:,:] + _K_dist2 = np.square(_K_dist/self.lengthscale).sum(axis=-1) + dK_dX = self.variance*np.exp(-0.5 * self._K_dist2[:,:,None]) * (-2.*_K_dist/np.square(self.lengthscale)) + dL_dX = (dL_dK[:,:,None] * dK_dX).sum(axis=1) + else: + _K_dist = X[:,None,:] - X2[None,:,:] + _K_dist2 = np.square(_K_dist/self.lengthscale).sum(axis=-1) + dK_dX = self.variance*np.exp(-0.5 * self._K_dist2[:,:,None]) * (-_K_dist/np.square(self.lengthscale)) + dL_dX = (dL_dK[:,:,None] * dK_dX).sum(axis=1) + return dL_dX + + #---------------------------------------# + # Precomputations # + #---------------------------------------# + + @cache_this(1) + def _K_computations(self, X, X2): + """ + K(X,X2) - X is NxQ + Q -> input dimension (self.input_dim) + """ + if X2 is None: + self._X2 = None + + X = X / self.lengthscale + Xsquare = np.sum(np.square(X), axis=1) + self._K_dist2 = -2.*tdot(X) + (Xsquare[:, None] + Xsquare[None, :]) + else: + self._X2 = X2.copy() + + X = X / self.lengthscale + X2 = X2 / self.lengthscale + self._K_dist2 = -2.*np.dot(X, X2.T) + (np.sum(np.square(X), axis=1)[:, None] + np.sum(np.square(X2), axis=1)[None, :]) + self._K_dvar = np.exp(-0.5 * self._K_dist2) + + @cache_this(1) + def _psi_computations(self, Z, mu, S, gamma): + """ + Z - MxQ + mu - NxQ + S - NxQ + gamma - NxQ + """ + # here are the "statistics" for psi1 and psi2 + # Produced intermediate results: + # _psi1 NxM + # _dpsi1_dvariance NxM + # _dpsi1_dlengthscale NxMxQ + # _dpsi1_dZ NxMxQ + # _dpsi1_dgamma NxMxQ + # _dpsi1_dmu NxMxQ + # _dpsi1_dS NxMxQ + # _psi2 NxMxM + # _psi2_dvariance NxMxM + # _psi2_dlengthscale NxMxMxQ + # _psi2_dZ NxMxMxQ + # _psi2_dgamma NxMxMxQ + # _psi2_dmu NxMxMxQ + # _psi2_dS NxMxMxQ + + lengthscale2 = np.square(self.lengthscale) + + _psi2_Zhat = 0.5 * (Z[:, None, :] + Z[None, :, :]) # M,M,Q + _psi2_Zdist = 0.5 * (Z[:, None, :] - Z[None, :, :]) # M,M,Q + _psi2_Zdist_sq = np.square(_psi2_Zdist / self.lengthscale) # M,M,Q + _psi2_Z_sq_sum = (np.square(Z[:,None,:])+np.square(Z[None,:,:]))/lengthscale2 # MxMxQ + + # psi1 + _psi1_denom = S[:, None, :] / lengthscale2 + 1. # Nx1xQ + _psi1_denom_sqrt = np.sqrt(_psi1_denom) #Nx1xQ + _psi1_dist = Z[None, :, :] - mu[:, None, :] # NxMxQ + _psi1_dist_sq = np.square(_psi1_dist) / (lengthscale2 * _psi1_denom) # NxMxQ + _psi1_common = gamma[:,None,:] / (lengthscale2*_psi1_denom*_psi1_denom_sqrt) #Nx1xQ + _psi1_exponent1 = np.log(gamma[:,None,:]) -0.5 * (_psi1_dist_sq + np.log(_psi1_denom)) # NxMxQ + _psi1_exponent2 = np.log(1.-gamma[:,None,:]) -0.5 * (np.square(Z[None,:,:])/lengthscale2) # NxMxQ + _psi1_exponent = np.log(np.exp(_psi1_exponent1) + np.exp(_psi1_exponent2)) #NxMxQ + _psi1_exp_sum = _psi1_exponent.sum(axis=-1) #NxM + _psi1_exp_dist_sq = np.exp(-0.5*_psi1_dist_sq) # NxMxQ + _psi1_exp_Z = np.exp(-0.5*np.square(Z[None,:,:])/lengthscale2) # 1xMxQ + _psi1_q = self.variance * np.exp(_psi1_exp_sum[:,:,None] - _psi1_exponent) # NxMxQ + self._psi1 = self.variance * np.exp(_psi1_exp_sum) # NxM + self._dpsi1_dvariance = self._psi1 / self.variance # NxM + self._dpsi1_dgamma = _psi1_q * (_psi1_exp_dist_sq/_psi1_denom_sqrt-_psi1_exp_Z) # NxMxQ + self._dpsi1_dmu = _psi1_q * (_psi1_exp_dist_sq * _psi1_dist * _psi1_common) # NxMxQ + self._dpsi1_dS = _psi1_q * (_psi1_exp_dist_sq * _psi1_common * 0.5 * (_psi1_dist_sq - 1.)) # NxMxQ + self._dpsi1_dZ = _psi1_q * (- _psi1_common * _psi1_dist * _psi1_exp_dist_sq - (1-gamma[:,None,:])/lengthscale2*Z[None,:,:]*_psi1_exp_Z) # NxMxQ + self._dpsi1_dlengthscale = 2.*self.lengthscale*_psi1_q * (0.5*_psi1_common*(S[:,None,:]/lengthscale2+_psi1_dist_sq)*_psi1_exp_dist_sq + 0.5*(1-gamma[:,None,:])*np.square(Z[None,:,:]/lengthscale2)*_psi1_exp_Z) # NxMxQ + + + # psi2 + _psi2_denom = 2.*S[:, None, None, :] / lengthscale2 + 1. # Nx1x1xQ + _psi2_denom_sqrt = np.sqrt(_psi2_denom) + _psi2_mudist = mu[:,None,None,:]-_psi2_Zhat #N,M,M,Q + _psi2_mudist_sq = np.square(_psi2_mudist)/(lengthscale2*_psi2_denom) + _psi2_common = gamma[:,None,None,:]/(lengthscale2 * _psi2_denom * _psi2_denom_sqrt) # Nx1x1xQ + _psi2_exponent1 = -_psi2_Zdist_sq -_psi2_mudist_sq -0.5*np.log(_psi2_denom)+np.log(gamma[:,None,None,:]) #N,M,M,Q + _psi2_exponent2 = np.log(1.-gamma[:,None,None,:]) - 0.5*(_psi2_Z_sq_sum) # NxMxMxQ + _psi2_exponent = np.log(np.exp(_psi2_exponent1) + np.exp(_psi2_exponent2)) + _psi2_exp_sum = _psi2_exponent.sum(axis=-1) #NxM + _psi2_q = np.square(self.variance) * np.exp(_psi2_exp_sum[:,:,:,None]-_psi2_exponent) # NxMxMxQ + _psi2_exp_dist_sq = np.exp(-_psi2_Zdist_sq -_psi2_mudist_sq) # NxMxMxQ + _psi2_exp_Z = np.exp(-0.5*_psi2_Z_sq_sum) # MxMxQ + self._psi2 = np.square(self.variance) * np.exp(_psi2_exp_sum) # N,M,M + self._dpsi2_dvariance = 2. * self._psi2/self.variance # NxMxM + self._dpsi2_dgamma = _psi2_q * (_psi2_exp_dist_sq/_psi2_denom_sqrt - _psi2_exp_Z) # NxMxMxQ + self._dpsi2_dmu = _psi2_q * (-2.*_psi2_common*_psi2_mudist * _psi2_exp_dist_sq) # NxMxMxQ + self._dpsi2_dS = _psi2_q * (_psi2_common * (2.*_psi2_mudist_sq - 1.) * _psi2_exp_dist_sq) # NxMxMxQ + self._dpsi2_dZ = 2.*_psi2_q * (_psi2_common*(-_psi2_Zdist*_psi2_denom+_psi2_mudist)*_psi2_exp_dist_sq - (1-gamma[:,None,None,:])*Z[:,None,:]/lengthscale2*_psi2_exp_Z) # NxMxMxQ + self._dpsi2_dlengthscale = 2.*self.lengthscale* _psi2_q * (_psi2_common*(S[:,None,None,:]/lengthscale2+_psi2_Zdist_sq*_psi2_denom+_psi2_mudist_sq)*_psi2_exp_dist_sq+(1-gamma[:,None,None,:])*_psi2_Z_sq_sum*0.5/lengthscale2*_psi2_exp_Z) # NxMxMxQ + \ No newline at end of file diff --git a/GPy/models/__init__.py b/GPy/models/__init__.py index 3fcaffa8..83db4b8c 100644 --- a/GPy/models/__init__.py +++ b/GPy/models/__init__.py @@ -15,3 +15,4 @@ from mrd import MRD from gradient_checker import GradientChecker from gp_multioutput_regression import GPMultioutputRegression from sparse_gp_multioutput_regression import SparseGPMultioutputRegression +from ss_gplvm import SSGPLVM \ No newline at end of file diff --git a/GPy/models/ss_gplvm.py b/GPy/models/ss_gplvm.py new file mode 100644 index 00000000..f21da605 --- /dev/null +++ b/GPy/models/ss_gplvm.py @@ -0,0 +1,301 @@ +# Copyright (c) 2012, GPy authors (see AUTHORS.txt). +# Licensed under the BSD 3-clause license (see LICENSE.txt) + +import numpy as np +import itertools +from matplotlib import pyplot + +from ..core.sparse_gp import SparseGP +from .. import kern +from ..likelihoods import Gaussian +from ..inference.optimization import SCG +from ..util import linalg +from ..core.parameterization.variational import SpikeAndSlabPrior, SpikeAndSlabPosterior + +class SSGPLVM(SparseGP): + """ + Spike-and-Slab Gaussian Process Latent Variable Model + + :param Y: observed data (np.ndarray) or GPy.likelihood + :type Y: np.ndarray| GPy.likelihood instance + :param input_dim: latent dimensionality + :type input_dim: int + :param init: initialisation method for the latent space + :type init: 'PCA'|'random' + + """ + def __init__(self, Y, input_dim, X=None, X_variance=None, init='PCA', num_inducing=10, + Z=None, kernel=None, inference_method=None, likelihood=None, name='Spike-and-Slab GPLVM', **kwargs): + + if X == None: # The mean of variational approximation (mu) + from ..util.initialization import initialize_latent + X = initialize_latent(init, input_dim, Y) + self.init = init + + if X_variance is None: # The variance of the variational approximation (S) + X_variance = np.random.uniform(0,.1,X.shape) + + gamma = np.empty_like(X) # The posterior probabilities of the binary variable in the variational approximation + gamma[:] = 0.5 + + if Z is None: + Z = np.random.permutation(X.copy())[:num_inducing] + assert Z.shape[1] == X.shape[1] + + if likelihood is None: + likelihood = Gaussian() + + if kernel is None: + kernel = kern.SSRBF(input_dim) + + self.variational_prior = SpikeAndSlabPrior(pi=0.5) # the prior probability of the latent binary variable b + X = SpikeAndSlabPosterior(X, X_variance, gamma) + + SparseGP.__init__(self, X, Y, Z, kernel, likelihood, inference_method, name, **kwargs) + self.add_parameter(self.X, index=0) + + def parameters_changed(self): + super(SSGPLVM, self).parameters_changed() + self._log_marginal_likelihood -= self.variational_prior.KL_divergence(self.X) + + self.X.mean.gradient, self.X.variance.gradient, self.X.binary_prob.gradient = self.kern.gradients_q_variational(posterior_variational=self.X, Z=self.Z, **self.grad_dict) + + # update for the KL divergence + self.variational_prior.update_gradients_KL(self.X) + + def plot_latent(self, plot_inducing=True, *args, **kwargs): + pass + #return plot_latent.plot_latent(self, plot_inducing=plot_inducing, *args, **kwargs) + + def do_test_latents(self, Y): + """ + Compute the latent representation for a set of new points Y + + Notes: + This will only work with a univariate Gaussian likelihood (for now) + """ + assert not self.likelihood.is_heteroscedastic + N_test = Y.shape[0] + input_dim = self.Z.shape[1] + means = np.zeros((N_test, input_dim)) + covars = np.zeros((N_test, input_dim)) + + dpsi0 = -0.5 * self.output_dim * self.likelihood.precision + dpsi2 = self.dL_dpsi2[0][None, :, :] # TODO: this may change if we ignore het. likelihoods + V = self.likelihood.precision * Y + + #compute CPsi1V + if self.Cpsi1V is None: + psi1V = np.dot(self.psi1.T, self.likelihood.V) + tmp, _ = linalg.dtrtrs(self._Lm, np.asfortranarray(psi1V), lower=1, trans=0) + tmp, _ = linalg.dpotrs(self.LB, tmp, lower=1) + self.Cpsi1V, _ = linalg.dtrtrs(self._Lm, tmp, lower=1, trans=1) + + dpsi1 = np.dot(self.Cpsi1V, V.T) + + start = np.zeros(self.input_dim * 2) + + for n, dpsi1_n in enumerate(dpsi1.T[:, :, None]): + args = (self.kern, self.Z, dpsi0, dpsi1_n.T, dpsi2) + xopt, fopt, neval, status = SCG(f=latent_cost, gradf=latent_grad, x=start, optargs=args, display=False) + + mu, log_S = xopt.reshape(2, 1, -1) + means[n] = mu[0].copy() + covars[n] = np.exp(log_S[0]).copy() + + return means, covars + + def dmu_dX(self, Xnew): + """ + Calculate the gradient of the prediction at Xnew w.r.t Xnew. + """ + dmu_dX = np.zeros_like(Xnew) + for i in range(self.Z.shape[0]): + dmu_dX += self.kern.dK_dX(self.Cpsi1Vf[i:i + 1, :], Xnew, self.Z[i:i + 1, :]) + return dmu_dX + + def dmu_dXnew(self, Xnew): + """ + Individual gradient of prediction at Xnew w.r.t. each sample in Xnew + """ + dK_dX = np.zeros((Xnew.shape[0], self.num_inducing)) + ones = np.ones((1, 1)) + for i in range(self.Z.shape[0]): + dK_dX[:, i] = self.kern.dK_dX(ones, Xnew, self.Z[i:i + 1, :]).sum(-1) + return np.dot(dK_dX, self.Cpsi1Vf) + + def plot_steepest_gradient_map(self, fignum=None, ax=None, which_indices=None, labels=None, data_labels=None, data_marker='o', data_s=40, resolution=20, aspect='auto', updates=False, ** kwargs): + input_1, input_2 = significant_dims = most_significant_input_dimensions(self, which_indices) + + X = np.zeros((resolution ** 2, self.input_dim)) + indices = np.r_[:X.shape[0]] + if labels is None: + labels = range(self.output_dim) + + def plot_function(x): + X[:, significant_dims] = x + dmu_dX = self.dmu_dXnew(X) + argmax = np.argmax(dmu_dX, 1) + return dmu_dX[indices, argmax], np.array(labels)[argmax] + + if ax is None: + fig = pyplot.figure(num=fignum) + ax = fig.add_subplot(111) + + if data_labels is None: + data_labels = np.ones(self.num_data) + ulabels = [] + for lab in data_labels: + if not lab in ulabels: + ulabels.append(lab) + marker = itertools.cycle(list(data_marker)) + from GPy.util import Tango + for i, ul in enumerate(ulabels): + if type(ul) is np.string_: + this_label = ul + elif type(ul) is np.int64: + this_label = 'class %i' % ul + else: + this_label = 'class %i' % i + m = marker.next() + index = np.nonzero(data_labels == ul)[0] + x = self.X[index, input_1] + y = self.X[index, input_2] + ax.scatter(x, y, marker=m, s=data_s, color=Tango.nextMedium(), label=this_label) + + ax.set_xlabel('latent dimension %i' % input_1) + ax.set_ylabel('latent dimension %i' % input_2) + + from matplotlib.cm import get_cmap + from GPy.util.latent_space_visualizations.controllers.imshow_controller import ImAnnotateController + if not 'cmap' in kwargs.keys(): + kwargs.update(cmap=get_cmap('jet')) + controller = ImAnnotateController(ax, + plot_function, + tuple(self.X.min(0)[:, significant_dims]) + tuple(self.X.max(0)[:, significant_dims]), + resolution=resolution, + aspect=aspect, + **kwargs) + ax.legend() + ax.figure.tight_layout() + if updates: + pyplot.show() + clear = raw_input('Enter to continue') + if clear.lower() in 'yes' or clear == '': + controller.deactivate() + return controller.view + + def plot_X_1d(self, fignum=None, ax=None, colors=None): + """ + Plot latent space X in 1D: + + - if fig is given, create input_dim subplots in fig and plot in these + - if ax is given plot input_dim 1D latent space plots of X into each `axis` + - if neither fig nor ax is given create a figure with fignum and plot in there + + colors: + colors of different latent space dimensions input_dim + + """ + import pylab + if ax is None: + fig = pylab.figure(num=fignum, figsize=(8, min(12, (2 * self.X.shape[1])))) + if colors is None: + colors = pylab.gca()._get_lines.color_cycle + pylab.clf() + else: + colors = iter(colors) + plots = [] + x = np.arange(self.X.shape[0]) + for i in range(self.X.shape[1]): + if ax is None: + a = fig.add_subplot(self.X.shape[1], 1, i + 1) + elif isinstance(ax, (tuple, list)): + a = ax[i] + else: + raise ValueError("Need one ax per latent dimnesion input_dim") + a.plot(self.X, c='k', alpha=.3) + plots.extend(a.plot(x, self.X.T[i], c=colors.next(), label=r"$\mathbf{{X_{{{}}}}}$".format(i))) + a.fill_between(x, + self.X.T[i] - 2 * np.sqrt(self.X_variance.T[i]), + self.X.T[i] + 2 * np.sqrt(self.X_variance.T[i]), + facecolor=plots[-1].get_color(), + alpha=.3) + a.legend(borderaxespad=0.) + a.set_xlim(x.min(), x.max()) + if i < self.X.shape[1] - 1: + a.set_xticklabels('') + pylab.draw() + if ax is None: + fig.tight_layout(h_pad=.01) # , rect=(0, 0, 1, .95)) + return fig + + def getstate(self): + """ + Get the current state of the class, + here just all the indices, rest can get recomputed + """ + return SparseGP._getstate(self) + [self.init] + + def setstate(self, state): + self._const_jitter = None + self.init = state.pop() + SparseGP._setstate(self, state) + + +def latent_cost_and_grad(mu_S, kern, Z, dL_dpsi0, dL_dpsi1, dL_dpsi2): + """ + objective function for fitting the latent variables for test points + (negative log-likelihood: should be minimised!) + """ + mu, log_S = mu_S.reshape(2, 1, -1) + S = np.exp(log_S) + + psi0 = kern.psi0(Z, mu, S) + psi1 = kern.psi1(Z, mu, S) + psi2 = kern.psi2(Z, mu, S) + + lik = dL_dpsi0 * psi0 + np.dot(dL_dpsi1.flatten(), psi1.flatten()) + np.dot(dL_dpsi2.flatten(), psi2.flatten()) - 0.5 * np.sum(np.square(mu) + S) + 0.5 * np.sum(log_S) + + mu0, S0 = kern.dpsi0_dmuS(dL_dpsi0, Z, mu, S) + mu1, S1 = kern.dpsi1_dmuS(dL_dpsi1, Z, mu, S) + mu2, S2 = kern.dpsi2_dmuS(dL_dpsi2, Z, mu, S) + + dmu = mu0 + mu1 + mu2 - mu + # dS = S0 + S1 + S2 -0.5 + .5/S + dlnS = S * (S0 + S1 + S2 - 0.5) + .5 + return -lik, -np.hstack((dmu.flatten(), dlnS.flatten())) + +def latent_cost(mu_S, kern, Z, dL_dpsi0, dL_dpsi1, dL_dpsi2): + """ + objective function for fitting the latent variables (negative log-likelihood: should be minimised!) + This is the same as latent_cost_and_grad but only for the objective + """ + mu, log_S = mu_S.reshape(2, 1, -1) + S = np.exp(log_S) + + psi0 = kern.psi0(Z, mu, S) + psi1 = kern.psi1(Z, mu, S) + psi2 = kern.psi2(Z, mu, S) + + lik = dL_dpsi0 * psi0 + np.dot(dL_dpsi1.flatten(), psi1.flatten()) + np.dot(dL_dpsi2.flatten(), psi2.flatten()) - 0.5 * np.sum(np.square(mu) + S) + 0.5 * np.sum(log_S) + return -float(lik) + +def latent_grad(mu_S, kern, Z, dL_dpsi0, dL_dpsi1, dL_dpsi2): + """ + This is the same as latent_cost_and_grad but only for the grad + """ + mu, log_S = mu_S.reshape(2, 1, -1) + S = np.exp(log_S) + + mu0, S0 = kern.dpsi0_dmuS(dL_dpsi0, Z, mu, S) + mu1, S1 = kern.dpsi1_dmuS(dL_dpsi1, Z, mu, S) + mu2, S2 = kern.dpsi2_dmuS(dL_dpsi2, Z, mu, S) + + dmu = mu0 + mu1 + mu2 - mu + # dS = S0 + S1 + S2 -0.5 + .5/S + dlnS = S * (S0 + S1 + S2 - 0.5) + .5 + + return -np.hstack((dmu.flatten(), dlnS.flatten())) + +