1# Copyright (c) 2012-2014, GPy authors (see AUTHORS.txt).
2# Licensed under the BSD 3-clause license (see LICENSE.txt)
3
4import numpy as np
5from .gp import GP
6from .parameterization.param import Param
7from ..inference.latent_function_inference import var_dtc
8from .. import likelihoods
9from GPy.core.parameterization.variational import VariationalPosterior
10
11import logging
12logger = logging.getLogger("sparse gp")
13
14class SparseGP(GP):
15    """
16    A general purpose Sparse GP model
17
18    This model allows (approximate) inference using variational DTC or FITC
19    (Gaussian likelihoods) as well as non-conjugate sparse methods based on
20    these.
21
22    This is not for missing data, as the implementation for missing data involves
23    some inefficient optimization routine decisions.
24    See missing data SparseGP implementation in py:class:'~GPy.models.sparse_gp_minibatch.SparseGPMiniBatch'.
25
26    :param X: inputs
27    :type X: np.ndarray (num_data x input_dim)
28    :param likelihood: a likelihood instance, containing the observed data
29    :type likelihood: GPy.likelihood.(Gaussian | EP | Laplace)
30    :param kernel: the kernel (covariance function). See link kernels
31    :type kernel: a GPy.kern.kern instance
32    :param X_variance: The uncertainty in the measurements of X (Gaussian variance)
33    :type X_variance: np.ndarray (num_data x input_dim) | None
34    :param Z: inducing inputs
35    :type Z: np.ndarray (num_inducing x input_dim)
36    :param num_inducing: Number of inducing points (optional, default 10. Ignored if Z is not None)
37    :type num_inducing: int
38
39    """
40
41    def __init__(self, X, Y, Z, kernel, likelihood, mean_function=None, X_variance=None, inference_method=None,
42                 name='sparse gp', Y_metadata=None, normalizer=False):
43
44        #pick a sensible inference method
45        if inference_method is None:
46            if isinstance(likelihood, likelihoods.Gaussian):
47                inference_method = var_dtc.VarDTC(limit=3)
48            else:
49                #inference_method = ??
50                raise NotImplementedError("what to do what to do?")
51            print(("defaulting to ", inference_method, "for latent function inference"))
52
53        self.Z = Param('inducing inputs', Z)
54        self.num_inducing = Z.shape[0]
55
56        super(SparseGP, self).__init__(X, Y, kernel, likelihood, mean_function, inference_method=inference_method, name=name, Y_metadata=Y_metadata, normalizer=normalizer)
57
58        logger.info("Adding Z as parameter")
59        self.link_parameter(self.Z, index=0)
60        self.posterior = None
61
62    @property
63    def _predictive_variable(self):
64        return self.Z
65
66    def has_uncertain_inputs(self):
67        return isinstance(self.X, VariationalPosterior)
68
69    def set_Z(self, Z, trigger_update=True):
70        if trigger_update: self.update_model(False)
71        self.unlink_parameter(self.Z)
72        self.Z = Param('inducing inputs',Z)
73        self.link_parameter(self.Z, index=0)
74        if trigger_update: self.update_model(True)
75
76    def parameters_changed(self):
77        self.posterior, self._log_marginal_likelihood, self.grad_dict = \
78        self.inference_method.inference(self.kern, self.X, self.Z, self.likelihood,
79                                        self.Y_normalized, Y_metadata=self.Y_metadata,
80                                        mean_function=self.mean_function)
81        self._update_gradients()
82
83    def _update_gradients(self):
84        self.likelihood.update_gradients(self.grad_dict['dL_dthetaL'])
85        if self.mean_function is not None:
86            self.mean_function.update_gradients(self.grad_dict['dL_dm'], self.X)
87
88        if isinstance(self.X, VariationalPosterior):
89            #gradients wrt kernel
90            dL_dKmm = self.grad_dict['dL_dKmm']
91            self.kern.update_gradients_full(dL_dKmm, self.Z, None)
92            kerngrad = self.kern.gradient.copy()
93            self.kern.update_gradients_expectations(variational_posterior=self.X,
94                                                    Z=self.Z,
95                                                    dL_dpsi0=self.grad_dict['dL_dpsi0'],
96                                                    dL_dpsi1=self.grad_dict['dL_dpsi1'],
97                                                    dL_dpsi2=self.grad_dict['dL_dpsi2'])
98            self.kern.gradient += kerngrad
99
100            #gradients wrt Z
101            self.Z.gradient = self.kern.gradients_X(dL_dKmm, self.Z)
102            self.Z.gradient += self.kern.gradients_Z_expectations(
103                               self.grad_dict['dL_dpsi0'],
104                               self.grad_dict['dL_dpsi1'],
105                               self.grad_dict['dL_dpsi2'],
106                               Z=self.Z,
107                               variational_posterior=self.X)
108        else:
109            #gradients wrt kernel
110            self.kern.update_gradients_diag(self.grad_dict['dL_dKdiag'], self.X)
111            kerngrad = self.kern.gradient.copy()
112            self.kern.update_gradients_full(self.grad_dict['dL_dKnm'], self.X, self.Z)
113            kerngrad += self.kern.gradient
114            self.kern.update_gradients_full(self.grad_dict['dL_dKmm'], self.Z, None)
115            self.kern.gradient += kerngrad
116            #gradients wrt Z
117            self.Z.gradient = self.kern.gradients_X(self.grad_dict['dL_dKmm'], self.Z)
118            self.Z.gradient += self.kern.gradients_X(self.grad_dict['dL_dKnm'].T, self.Z, self.X)
119        self._Zgrad = self.Z.gradient.copy()
120
121    def to_dict(self, save_data=True):
122        """
123        Convert the object into a json serializable dictionary.
124
125        :param boolean save_data: if true, it adds the training data self.X and self.Y to the dictionary
126        :return dict: json serializable dictionary containing the needed information to instantiate the object
127        """
128        input_dict = super(SparseGP, self).to_dict(save_data)
129        input_dict["class"] = "GPy.core.SparseGP"
130        input_dict["Z"] = self.Z.tolist()
131        return input_dict
132
133    @staticmethod
134    def _format_input_dict(input_dict, data=None):
135        input_dict = GP._format_input_dict(input_dict, data)
136        input_dict["Z"] = np.array(input_dict["Z"])
137        return input_dict
138
139    @staticmethod
140    def _build_from_input_dict(input_dict, data=None):
141        input_dict = SparseGP._format_input_dict(input_dict, data)
142        return SparseGP(**input_dict)
143