class_docstr: This method approximates the Gaussian process posterior using a multivariate Gaussian.
  
  The idea is that the posterior over the function-value vector F is
  approximated by a Gaussian, and the KL divergence is minimised between
  the approximation and the posterior.
  
  This implementation is equivalent to SVGP with X=Z, but is more efficient.
  The whitened representation is used to aid optimization.
  
  The posterior approximation is
  
  .. math::
  
     q(\mathbf f) = N(\mathbf f \,|\, \boldsymbol \mu, \boldsymbol \Sigma)
  
  For a use example see
  :doc:`../../../../notebooks/getting_started/classification_and_other_data_distributions`.
docstr: data = (X, Y) contains the input points [N, D] and the observations [N, P]
  kernel, likelihood, mean_function are appropriate GPflow objects
docstr_tok: ["data","=","(","X",",","Y",")","contains","the","input","points","[","N",",","D","]","and","the","observations","[","N",",","P","]","kernel",",","likelihood",",","mean_function","are","appropriate","GPflow","objects"]
code: def __init__(
      self,
      data: RegressionData,
      kernel: Kernel,
      likelihood: Likelihood,
      mean_function: Optional[MeanFunction] = None,
      num_latent_gps: Optional[int] = None,
  ):
      """
      data = (X, Y) contains the input points [N, D] and the observations [N, P]
      kernel, likelihood, mean_function are appropriate GPflow objects
      """
      if num_latent_gps is None:
          num_latent_gps = self.calc_num_latent_gps_from_data(
              data, kernel, likelihood
          )
      super().__init__(
          kernel, likelihood, mean_function, num_latent_gps
      )
  
      self.data = data_input_to_tensor(data)
      X_data, _Y_data = self.data
  
      static_num_data = X_data.shape[0]
      if static_num_data is None:
          q_sqrt_unconstrained_shape = (self.num_latent_gps, None)
      else:
          q_sqrt_unconstrained_shape = (
              self.num_latent_gps,
              triangular_size(static_num_data),
          )
      self.num_data = Parameter(
          tf.shape(X_data)[0], shape=[], dtype=tf.int32, trainable=False
      )
  
      # Many functions below don't like `Parameter`s:
      dynamic_num_data = tf.convert_to_tensor(self.num_data)
  
      self.q_mu = Parameter(
          tf.zeros((dynamic_num_data, self.num_latent_gps)),
          shape=(static_num_data, num_latent_gps),
      )
      q_sqrt = tf.eye(
          dynamic_num_data, batch_shape=[self.num_latent_gps]
      )
      self.q_sqrt = Parameter(
          q_sqrt,
          transform=triangular(),
          unconstrained_shape=q_sqrt_unconstrained_shape,
          constrained_shape=(
              num_latent_gps,
              static_num_data,
              static_num_data,
          ),
      )
  
imports: from typing import Optional
  import numpy
  import tensorflow
  from check_shapes import check_shapes, inherit_check_shapes
  import gpflow
  from ..None import posteriors
  from ..base import InputData, MeanAndVariance, Parameter, RegressionData
  from ..conditionals import conditional
  from ..config import default_float, default_jitter
  from ..kernels import Kernel
  from ..kullback_leiblers import gauss_kl
  from ..likelihoods import Likelihood
  from ..mean_functions import MeanFunction
  from ..utilities import assert_params_false, is_variable, triangular, triangular_size
  from .model import GPModel
  from .training_mixins import InternalDataTrainingLossMixin
  from .util import data_input_to_tensor
label_desc: Use image node_id 1 to create a new VGP_deprecated object from inherited base classes: GPModel, InternalDataTrainingLossMixin with example: obj = VGP_deprecated(data, kernel, likelihood, mean_function, num_latent_gps)