From 51fd1e24cebce0b09072d27c5a0eaa8c7af945a9 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Wed, 12 Oct 2022 20:20:57 +0000 Subject: [PATCH 01/14] Distrax to numpyro --- examples/barycentres.pct.py | 19 +++++++------- examples/classification.pct.py | 32 +++++++++++------------ examples/graph_kernels.pct.py | 8 +++--- examples/haiku.pct.py | 8 +++--- examples/kernels.pct.py | 17 ++++++------ examples/regression.pct.py | 15 ++++++----- examples/tfp_integration.pct.py | 6 ++--- examples/yacht.pct.py | 10 +++---- gpjax/config.py | 11 ++++---- gpjax/gps.py | 46 ++++++++++++++------------------- gpjax/likelihoods.py | 24 +++++++++-------- gpjax/parameters.py | 16 +++++------- 12 files changed, 105 insertions(+), 107 deletions(-) diff --git a/examples/barycentres.pct.py b/examples/barycentres.pct.py index b3e0738d..b24c36cc 100644 --- a/examples/barycentres.pct.py +++ b/examples/barycentres.pct.py @@ -7,7 +7,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -18,18 +18,19 @@ # # Gaussian Processes Barycentres # # In this notebook we'll give an implementation of . In this work, the existence of a Wasserstein barycentre between a collection of Gaussian processes is proven. When faced with trying to _average_ a set of probability distributions, the Wasserstein barycentre is an attractive choice as it enables uncertainty amongst the individual distributions to be incorporated into the averaged distribution. When compared to a naive _mean of means_ and _mean of variances_ approach to computing the average probability distributions, it can be seen that Wasserstein barycentres offer significantly more favourable uncertainty estimation. +# +# %% import typing as tp -import distrax as dx import jax import jax.numpy as jnp import jax.random as jr import jax.scipy.linalg as jsl import matplotlib.pyplot as plt +import numpyro import optax as ox -# %% import gpjax as gpx key = jr.PRNGKey(123) @@ -136,7 +137,7 @@ def sqrtm(A: jnp.DeviceArray): def wasserstein_barycentres( distributions: tp.List[dx.Distribution], weights: jnp.DeviceArray ): - covariances = [d.covariance() for d in distributions] + covariances = [d.covariance_matrix for d in distributions] cov_stack = jnp.stack(covariances) stack_sqrt = jax.vmap(sqrtm)(cov_stack) @@ -156,7 +157,7 @@ def step(covariance_candidate: jnp.DeviceArray, i: jnp.DeviceArray): # %% weights = jnp.ones((n_datasets,)) / n_datasets -means = jnp.stack([d.mean() for d in posterior_preds]) +means = jnp.stack([d.mean for d in posterior_preds]) barycentre_mean = jnp.tensordot(weights, means, axes=1) step_fn = jax.jit(wasserstein_barycentres(posterior_preds, weights)) @@ -167,7 +168,7 @@ def step(covariance_candidate: jnp.DeviceArray, i: jnp.DeviceArray): ) -barycentre_process = dx.MultivariateNormalFullCovariance( +barycentre_process = numpyro.distributions.MultivariateNormal( barycentre_mean, barycentre_covariance ) @@ -186,9 +187,9 @@ def plot( ci_alpha: float = 0.2, linewidth: float = 1.0, ): - mu = dist.mean() - sigma = dist.stddev() - ax.plot(xtest, dist.mean(), linewidth=linewidth, color=color, label=label) + mu = dist.mean + sigma = jnp.sqrt(dist.covariance_matrix.diagonal()) + ax.plot(xtest, dist.mean, linewidth=linewidth, color=color, label=label) ax.fill_between( xtest.squeeze(), mu - sigma, mu + sigma, alpha=ci_alpha, color=color ) diff --git a/examples/classification.pct.py b/examples/classification.pct.py index 0b10a9b2..876da984 100644 --- a/examples/classification.pct.py +++ b/examples/classification.pct.py @@ -7,7 +7,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -20,7 +20,6 @@ # In this notebook we demonstrate how to perform inference for Gaussian process models with non-Gaussian likelihoods via maximum a posteriori (MAP) and Markov chain Monte Carlo (MCMC). We focus on a classification task here and use [BlackJax](https://github.com/blackjax-devs/blackjax/) for sampling. import blackjax -import distrax as dx # %% import jax @@ -28,6 +27,7 @@ import jax.random as jr import jax.scipy as jsp import matplotlib.pyplot as plt +import numpyro import optax as ox from jaxtyping import Array, Float @@ -101,8 +101,8 @@ predictive_dist = likelihood(latent_dist, map_estimate) -predictive_mean = predictive_dist.mean() -predictive_std = predictive_dist.stddev() +predictive_mean = predictive_dist.mean +predictive_std = jnp.sqrt(predictive_dist.variance) fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(x, y, "o", label="Observations", color="tab:red") @@ -139,7 +139,7 @@ # The Laplace approximation improves uncertainty quantification by incorporating curvature induced by the marginal log-likelihood's Hessian to construct an approximate Gaussian distribution centered on the MAP estimate. # Since the negative Hessian is positive definite, we can use the Cholesky decomposition to obtain the covariance matrix of the Laplace approximation at the datapoints below. # %% -f_map_estimate = posterior(D, map_estimate)(x).mean() +f_map_estimate = posterior(D, map_estimate)(x).mean jitter = 1e-6 @@ -150,7 +150,7 @@ L_inv = jsp.linalg.solve_triangular(L, I(D.n), lower=True) H_inv = jsp.linalg.solve_triangular(L.T, L_inv, lower=False) -laplace_approximation = dx.MultivariateNormalFullCovariance(f_map_estimate, H_inv) +laplace_approximation = numpyro.distributions.MultivariateNormal(f_map_estimate, H_inv) from gpjax.kernels import cross_covariance, gram @@ -161,26 +161,26 @@ def predict( - laplace_at_data: dx.Distribution, + laplace_at_data: numpyro.distributions.Distribution, train_data: Dataset, test_inputs: Float[Array, "N D"], jitter: int = 1e-6, -) -> dx.Distribution: +) -> numpyro.distributions.Distribution: """Compute the predictive distribution of the Laplace approximation at novel inputs. Args: laplace_at_data (dict): The Laplace approximation at the datapoints. Returns: - dx.Distribution: The Laplace approximation at novel inputs. + numpyro.distributions.Distribution: The Laplace approximation at novel inputs. """ x, n = train_data.X, train_data.n t = test_inputs n_test = t.shape[0] - mu = laplace_at_data.mean().reshape(-1, 1) - cov = laplace_at_data.covariance() + mu = laplace_at_data.mean.reshape(-1, 1) + cov = laplace_at_data.covariance_matrix Ktt = gram(prior.kernel, t, params["kernel"]) Kxx = gram(prior.kernel, x, params["kernel"]) @@ -214,7 +214,7 @@ def predict( ) covariance += I(n_test) * jitter - return dx.MultivariateNormalFullCovariance( + return numpyro.distributions.MultivariateNormal( jnp.atleast_1d(mean.squeeze()), covariance ) @@ -226,8 +226,8 @@ def predict( predictive_dist = likelihood(latent_dist, map_estimate) -predictive_mean = predictive_dist.mean() -predictive_std = predictive_dist.stddev() +predictive_mean = predictive_dist.mean +predictive_std = predictive_dist.variance**0.5 fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(x, y, "o", label="Observations", color="tab:red") @@ -274,7 +274,7 @@ def predict( # %% # Adapted from BlackJax's introduction notebook. num_adapt = 500 -num_samples = 500 +num_samples = 200 mll = jax.jit(posterior.marginal_log_likelihood(D, constrainer, negative=False)) @@ -338,7 +338,7 @@ def one_step(state, rng_key): latent_dist = posterior(D, ps)(xtest) predictive_dist = likelihood(latent_dist, ps) - samples.append(predictive_dist.sample(seed=key, sample_shape=(10,))) + samples.append(predictive_dist.sample(key, sample_shape=(10,))) samples = jnp.vstack(samples) diff --git a/examples/graph_kernels.pct.py b/examples/graph_kernels.pct.py index 0069ad4e..656983fb 100644 --- a/examples/graph_kernels.pct.py +++ b/examples/graph_kernels.pct.py @@ -7,7 +7,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -88,7 +88,7 @@ } fx = f(true_params)(x) -y = fx.sample(seed=key).reshape(-1, 1) +y = fx.sample(key).reshape(-1, 1) D = gpx.Dataset(X=x, y=y) @@ -145,8 +145,8 @@ initial_dist = likelihood(posterior(D, params)(x), params) predictive_dist = likelihood(posterior(D, learned_params)(x), learned_params) -initial_mean = initial_dist.mean() -learned_mean = predictive_dist.mean() +initial_mean = initial_dist.mean +learned_mean = predictive_dist.mean rmse = lambda ytrue, ypred: jnp.sum(jnp.sqrt(jnp.square(ytrue - ypred))) diff --git a/examples/haiku.pct.py b/examples/haiku.pct.py index b2434433..9e014412 100644 --- a/examples/haiku.pct.py +++ b/examples/haiku.pct.py @@ -7,7 +7,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -19,6 +19,7 @@ # # In this notebook we demonstrate how GPJax can be used in conjunction with [Haiku](https://github.com/deepmind/dm-haiku) to build deep kernel Gaussian processes. Modelling data with discontinuities is a challenging task for regular Gaussian process models. However, as shown in , transforming the inputs to our Gaussian process model's kernel through a neural network can offer a solution to this. +# %% import typing as tp import distrax as dx @@ -31,7 +32,6 @@ from chex import dataclass from scipy.signal import sawtooth -# %% import gpjax as gpx from gpjax.kernels import Kernel @@ -179,8 +179,8 @@ def forward(x): latent_dist = posterior(D, final_params)(xtest) predictive_dist = likelihood(latent_dist, final_params) -predictive_mean = predictive_dist.mean() -predictive_std = predictive_dist.stddev() +predictive_mean = predictive_dist.mean +predictive_std = jnp.sqrt(predictive_dist.variance) fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(x, y, "o", label="Observations", color="tab:red") diff --git a/examples/kernels.pct.py b/examples/kernels.pct.py index adfea990..062ca11c 100644 --- a/examples/kernels.pct.py +++ b/examples/kernels.pct.py @@ -7,7 +7,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -19,6 +19,7 @@ # # In this guide, we introduce the kernels available in GPJax and demonstrate how to create custom ones. +# %% import distrax as dx import jax import jax.numpy as jnp @@ -29,7 +30,6 @@ from jaxtyping import Array, Float from optax import adam -# %% import gpjax as gpx key = jr.PRNGKey(123) @@ -64,7 +64,7 @@ prior = gpx.Prior(kernel=k) params, _, _, _ = gpx.initialise(prior, key).unpack() rv = prior(params)(x) - y = rv.sample(sample_shape=10, seed=key) + y = rv.sample(key, sample_shape=(10,)) ax.plot(x, y.T, alpha=0.7) ax.set_title(k.name) @@ -208,13 +208,12 @@ def _initialise_params(self, key: jr.PRNGKey) -> dict: # To define a bijector here we'll make use of the `Lambda` operator given in Distrax. This lets us convert any regular Jax function into a bijection. Given that we require $\tau$ to be strictly greater than $4.$, we'll apply a [softplus transformation](https://jax.readthedocs.io/en/latest/_autosummary/jax.nn.softplus.html) where the lower bound is shifted by $4.$. +import numpyro + # %% from gpjax.config import add_parameter -bij_fn = lambda x: jax.nn.softplus(x + jnp.array(4.0)) -bij = dx.Lambda(bij_fn) - -add_parameter("tau", bij) +add_parameter("tau", numpyro.distributions.transforms.SoftplusTransform()) # %% [markdown] # ### Using our polar kernel @@ -263,8 +262,8 @@ def _initialise_params(self, key: jr.PRNGKey) -> dict: # %% posterior_rv = likelihood(circlular_posterior(D, final_params)(angles), final_params) -mu = posterior_rv.mean() -one_sigma = posterior_rv.stddev() +mu = posterior_rv.mean +one_sigma = jnp.sqrt(posterior_rv.variance) # %% fig = plt.figure(figsize=(10, 8)) diff --git a/examples/regression.pct.py b/examples/regression.pct.py index 62f319e4..51735685 100644 --- a/examples/regression.pct.py +++ b/examples/regression.pct.py @@ -6,7 +6,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -86,9 +86,9 @@ parameter_state = gpx.initialise(prior, key) prior_dist = prior(parameter_state.params)(xtest) -prior_mean = prior_dist.mean() -prior_std = jnp.sqrt(prior_dist.covariance().diagonal()) -samples = prior_dist.sample(seed=key, sample_shape=20).T +prior_mean = prior_dist.mean +prior_std = jnp.sqrt(prior_dist.covariance_matrix.diagonal()) +samples = prior_dist.sample(key, sample_shape=(20,)).T plt.plot(xtest, samples, color="tab:blue", alpha=0.5) plt.plot(xtest, prior_mean, color="tab:orange") @@ -196,12 +196,15 @@ # # Equipped with the posterior and a set of optimised hyperparameter values, we are now in a position to query our GP's predictive distribution at novel test inputs. To do this, we use our defined `posterior` and `likelihood` at our test inputs to obtain the predictive distribution as a `Distrax` multivariate Gaussian upon which `mean` and `stddev` can be used to extract the predictive mean and standard deviatation. +# %% +latent_dist + # %% latent_dist = posterior(D, final_params)(xtest) predictive_dist = likelihood(latent_dist, final_params) -predictive_mean = predictive_dist.mean() -predictive_std = predictive_dist.stddev() +predictive_mean = predictive_dist.mean +predictive_std = jnp.sqrt(predictive_dist.covariance_matrix.diagonal()) # %% [markdown] # With the predictions and their uncertainty acquired, we illustrate the GP's performance at explaining the data $\mathcal{D}$ and recovering the underlying latent function of interest. diff --git a/examples/tfp_integration.pct.py b/examples/tfp_integration.pct.py index ba26c468..7e6534a6 100644 --- a/examples/tfp_integration.pct.py +++ b/examples/tfp_integration.pct.py @@ -7,7 +7,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -202,8 +202,8 @@ def run_chain(key, state): predictive_dist = likelihood(posterior(D, learned_params)(xtest), learned_params) -mu = predictive_dist.mean() -sigma = predictive_dist.stddev() +mu = predictive_dist.mean +sigma = jnp.sqrt(predictive_dist.variance) # %% [markdown] # Finally, we plot the learned posterior predictive distribution evaluated at the test points defined above. diff --git a/examples/yacht.pct.py b/examples/yacht.pct.py index bb5683a1..b549ff27 100644 --- a/examples/yacht.pct.py +++ b/examples/yacht.pct.py @@ -1,11 +1,12 @@ # --- # jupyter: # jupytext: +# custom_cell_magics: kql # text_representation: # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -17,13 +18,12 @@ # # In this notebook we'll demonstrate the use of GPJax on a benchmark UCI regression problem. Such tasks are commonly used within the research community to benchmark and evaluate new techniques against those already present in the literature. Much of the code contained in this notebook can be adapted to applied problems concerning datasets other than the one presented here. +# %% import jax.numpy as jnp import jax.random as jr import matplotlib.pyplot as plt import numpy as np import optax as ox - -# %% import pandas as pd from jax import jit from sklearn.metrics import mean_squared_error, r2_score @@ -153,8 +153,8 @@ latent_dist = posterior(training_data, learned_params)(scaled_Xte) predictive_dist = likelihood(latent_dist, learned_params) -predictive_mean = predictive_dist.mean() -predictive_stddev = predictive_dist.stddev() +predictive_mean = predictive_dist.mean +predictive_stddev = jnp.sqrt(predictive_dist.variance) # %% [markdown] # ## Evaluation diff --git a/gpjax/config.py b/gpjax/config.py index 13695d9e..7a191503 100644 --- a/gpjax/config.py +++ b/gpjax/config.py @@ -1,9 +1,10 @@ -import distrax as dx -import jax.numpy as jnp import jax.random as jr +import numpyro.distributions as npd from ml_collections import ConfigDict __config = None +import distrax as dx +import jax.numpy as jnp Identity = dx.Lambda(forward=lambda x: x, inverse=lambda x: x) Softplus = dx.Lambda( @@ -45,8 +46,8 @@ def get_defaults() -> ConfigDict: # Default bijections config.transformations = transformations = ConfigDict() - transformations.positive_transform = Softplus - transformations.identity_transform = Identity + transformations.positive_transform = npd.transforms.SoftplusTransform() + transformations.identity_transform = npd.transforms.IdentityTransform() # Default parameter transforms transformations.lengthscale = "positive_transform" @@ -63,7 +64,7 @@ def get_defaults() -> ConfigDict: return __config -def add_parameter(param_name: str, bijection: dx.Bijector) -> None: +def add_parameter(param_name: str, bijection: npd.transforms.Transform) -> None: """Add a parameter and its corresponding transform to GPJax's config file. Args: diff --git a/gpjax/gps.py b/gpjax/gps.py index db13f2f5..909a94fd 100644 --- a/gpjax/gps.py +++ b/gpjax/gps.py @@ -2,10 +2,10 @@ from abc import abstractmethod, abstractproperty from typing import Dict -import distrax as dx import jax.numpy as jnp import jax.random as jr import jax.scipy as jsp +import numpyro.distributions as npd from chex import dataclass from jaxtyping import Array, Float @@ -31,16 +31,16 @@ class AbstractGP: """Abstract Gaussian process object.""" - def __call__(self, *args: tp.Any, **kwargs: tp.Any) -> dx.Distribution: + def __call__(self, *args: tp.Any, **kwargs: tp.Any) -> npd.Distribution: """Evaluate the Gaussian process at the given points. Returns: - dx.Distribution: A multivariate normal random variable representation of the Gaussian process. + npd.Distribution: A multivariate normal random variable representation of the Gaussian process. """ return self.predict(*args, **kwargs) @abstractmethod - def predict(self, *args: tp.Any, **kwargs: tp.Any) -> dx.Distribution: + def predict(self, *args: tp.Any, **kwargs: tp.Any) -> npd.Distribution: """Compute the latent function's multivariate normal distribution.""" raise NotImplementedError @@ -76,7 +76,7 @@ def __rmul__(self, other: AbstractLikelihood): def predict( self, params: dict - ) -> tp.Callable[[Float[Array, "N D"]], dx.Distribution]: + ) -> tp.Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the GP's prior mean and variance. Args: params (dict): The specific set of parameters for which the mean function should be defined for. @@ -84,16 +84,13 @@ def predict( tp.Callable[[Array], Array]: A mean function that accepts an input array for where the mean function should be evaluated at. The mean function's value at these points is then returned. """ - def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs n_test = t.shape[0] μt = self.mean_function(t, params["mean_function"]) Ktt = gram(self.kernel, t, params["kernel"]) Ktt += I(n_test) * self.jitter - - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(μt.squeeze()), Ktt - ) + return npd.MultivariateNormal(jnp.atleast_1d(μt.squeeze()), Ktt) return predict_fn @@ -118,7 +115,7 @@ class AbstractPosterior(AbstractGP): jitter: tp.Optional[float] = DEFAULT_JITTER @abstractmethod - def predict(self, *args: tp.Any, **kwargs: tp.Any) -> dx.Distribution: + def predict(self, *args: tp.Any, **kwargs: tp.Any) -> npd.Distribution: """Predict the GP's output given the input.""" raise NotImplementedError @@ -141,7 +138,7 @@ class ConjugatePosterior(AbstractPosterior): def predict( self, train_data: Dataset, params: dict - ) -> tp.Callable[[Float[Array, "N D"]], dx.Distribution]: + ) -> tp.Callable[[Float[Array, "N D"]], npd.Distribution]: """Conditional on a set of training data, compute the GP's posterior predictive distribution for a given set of parameters. The returned function can be evaluated at a set of test inputs to compute the corresponding predictive density. Args: @@ -149,7 +146,7 @@ def predict( params (dict): A dictionary of parameters that should be used to compute the posterior. Returns: - tp.Callable[[Array], dx.Distribution]: A function that accepts an input array and returns the predictive distribution as a `distrax.MultivariateNormalFullCovariance`. + tp.Callable[[Array], npd.Distribution]: A function that accepts an input array and returns the predictive distribution as a `numpyro.distributions.MultivariateNormal`. """ x, y, n = train_data.X, train_data.y, train_data.n @@ -168,7 +165,7 @@ def predict( # w = L⁻¹ (y - μx) w = jsp.linalg.solve_triangular(L, y - μx, lower=True) - def predict(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs n_test = t.shape[0] μt = self.prior.mean_function(t, params["mean_function"]) @@ -185,9 +182,7 @@ def predict(test_inputs: Float[Array, "N D"]) -> dx.Distribution: covariance = Ktt - jnp.matmul(L_inv_Kxt.T, L_inv_Kxt) covariance += I(n_test) * self.jitter - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(mean.squeeze()), covariance - ) + return npd.MultivariateNormal(jnp.atleast_1d(mean.squeeze()), covariance) return predict @@ -227,8 +222,9 @@ def mll( L = jnp.linalg.cholesky(Sigma) # p(y | x, θ), where θ are the model hyperparameters: - marginal_likelihood = dx.MultivariateNormalTri( - jnp.atleast_1d(μx.squeeze()), L + + marginal_likelihood = npd.MultivariateNormal( + jnp.atleast_1d(μx.squeeze()), scale_tril=L ) # log p(θ) @@ -263,7 +259,7 @@ def _initialise_params(self, key: jnp.DeviceArray) -> Dict: def predict( self, train_data: Dataset, params: dict - ) -> tp.Callable[[Float[Array, "N D"]], dx.Distribution]: + ) -> tp.Callable[[Float[Array, "N D"]], npd.Distribution]: """Conditional on a set of training data, compute the GP's posterior predictive distribution for a given set of parameters. The returned function can be evaluated at a set of test inputs to compute the corresponding predictive density. Note, to gain predictions on the scale of the original data, the returned distribution will need to be transformed through the likelihood function's inverse link function. Args: @@ -271,7 +267,7 @@ def predict( params (dict): A dictionary of parameters that should be used to compute the posterior. Returns: - tp.Callable[[Array], dx.Distribution]: A function that accepts an input array and returns the predictive distribution as a `distrax.MultivariateNormalFullCovariance`. + tp.Callable[[Array], npd.Distribution]: A function that accepts an input array and returns the predictive distribution as a `numpyro.distributions.MultivariateNormal`. """ x, n = train_data.X, train_data.n @@ -279,7 +275,7 @@ def predict( Kxx += I(n) * self.jitter Lx = jnp.linalg.cholesky(Kxx) - def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs n_test = t.shape[0] Ktx = cross_covariance(self.prior.kernel, t, x, params["kernel"]) @@ -296,9 +292,7 @@ def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: covariance = Ktt - jnp.matmul(Lx_inv_Kxt.T, Lx_inv_Kxt) covariance += I(n_test) * self.jitter - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(mean.squeeze()), covariance - ) + return npd.MultivariateNormal(jnp.atleast_1d(mean.squeeze()), covariance) return predict_fn @@ -324,7 +318,7 @@ def marginal_log_likelihood( if not priors: priors = copy_dict_structure(self._initialise_params(jr.PRNGKey(0))) - priors["latent"] = dx.Normal(loc=0.0, scale=1.0) + priors["latent"] = npd.Normal(loc=0.0, scale=1.0) def mll(params: dict): params = transform(params=params, transform_map=transformations) diff --git a/gpjax/likelihoods.py b/gpjax/likelihoods.py index 2793126a..e9ac93c4 100644 --- a/gpjax/likelihoods.py +++ b/gpjax/likelihoods.py @@ -1,9 +1,9 @@ import abc from typing import Any, Callable, Dict, Optional -import distrax as dx import jax.numpy as jnp import jax.scipy as jsp +import numpyro.distributions as npd from chex import dataclass from jaxtyping import Array, Float @@ -66,16 +66,18 @@ def link_function(self) -> Callable: Callable: A link function that maps the predictive distribution to the likelihood function. """ - def link_fn(x, params: dict) -> dx.Distribution: - return dx.Normal(loc=x, scale=params["obs_noise"]) + def link_fn(x, params: dict) -> npd.Distribution: + return npd.Normal(loc=x, scale=params["obs_noise"]) return link_fn - def predict(self, dist: dx.Distribution, params: dict) -> dx.Distribution: + def predict(self, dist: npd.Distribution, params: dict) -> npd.Distribution: """Evaluate the Gaussian likelihood function at a given predictive distribution. Computationally, this is equivalent to summing the observation noise term to the diagonal elements of the predictive distribution's covariance matrix..""" n_data = dist.event_shape[0] - noisy_cov = dist.covariance() + I(n_data) * params["likelihood"]["obs_noise"] - return dx.MultivariateNormalFullCovariance(dist.mean(), noisy_cov) + noisy_cov = ( + dist.covariance_matrix + I(n_data) * params["likelihood"]["obs_noise"] + ) + return npd.MultivariateNormal(dist.mean, noisy_cov) @dataclass @@ -94,8 +96,8 @@ def link_function(self) -> Callable: Callable: A probit link function that maps the predictive distribution to the likelihood function. """ - def link_fn(x, params: Dict) -> dx.Distribution: - return dx.Bernoulli(probs=inv_probit(x)) + def link_fn(x, params: Dict) -> npd.Distribution: + return npd.Bernoulli(probs=inv_probit(x)) return link_fn @@ -115,9 +117,9 @@ def moment_fn( return moment_fn - def predict(self, dist: dx.Distribution, params: dict) -> Any: - variance = jnp.diag(dist.covariance()) - mean = dist.mean() + def predict(self, dist: npd.Distribution, params: dict) -> Any: + variance = jnp.diag(dist.covariance_matrix) + mean = dist.mean return self.predictive_moment_fn(mean.ravel(), variance, params) diff --git a/gpjax/parameters.py b/gpjax/parameters.py index 889c90e7..b44c7e5d 100644 --- a/gpjax/parameters.py +++ b/gpjax/parameters.py @@ -4,10 +4,10 @@ from copy import deepcopy from warnings import warn -import distrax as dx import jax import jax.numpy as jnp import jax.random as jr +import numpyro.distributions as npd from chex import dataclass from jaxtyping import Array, Float @@ -15,8 +15,6 @@ from .types import PRNGKeyType from .utils import merge_dictionaries -Identity = dx.Lambda(forward=lambda x: x, inverse=lambda x: x) - ################################ # Base operations @@ -134,7 +132,7 @@ def recursive_bijectors(ps, bs) -> tp.Tuple[tp.Dict, tp.Dict]: transform_type = transform_set[key] bijector = transform_set[transform_type] else: - bijector = Identity + bijector = npd.transforms.IdentityTransform() warnings.warn( f"Parameter {key} has no transform. Defaulting to identity transfom." ) @@ -155,10 +153,10 @@ def build_transforms(params: tp.Dict) -> tp.Tuple[tp.Dict, tp.Dict]: """ def forward(bijector): - return bijector.forward + return bijector.__call__ def inverse(bijector): - return bijector.inverse + return bijector.inv bijectors = build_bijectors(params) @@ -198,7 +196,7 @@ def transform(params: tp.Dict, transform_map: tp.Dict) -> tp.Dict: # Priors ################################ def log_density( - param: Float[Array, "D"], density: dx.Distribution + param: Float[Array, "D"], density: npd.Distribution ) -> Float[Array, "1"]: if type(density) == type(None): log_prob = jnp.array(0.0) @@ -263,9 +261,9 @@ def prior_checks(priors: dict) -> dict: ) else: warnings.warn("Placing unit Gaussian prior on latent function.") - priors["latent"] = dx.Normal(loc=0.0, scale=1.0) + priors["latent"] = npd.Normal(loc=0.0, scale=1.0) else: - priors["latent"] = dx.Normal(loc=0.0, scale=1.0) + priors["latent"] = npd.Normal(loc=0.0, scale=1.0) return priors From 11b0d9e9700ceae8b78474b9660dcf7f272f3319 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Thu, 13 Oct 2022 10:56:35 +0000 Subject: [PATCH 02/14] Update numpyro --- examples/barycentres.pct.py | 17 +++-- examples/classification.pct.py | 30 ++++----- examples/collapsed_vi.pct.py | 27 ++++---- examples/regression.pct.py | 26 +++----- examples/uncollapsed_vi.pct.py | 18 +++-- gpjax/__init__.py | 2 +- gpjax/config.py | 10 --- gpjax/gps.py | 2 +- gpjax/likelihoods.py | 4 +- gpjax/variational_families.py | 118 +++++++++++++++------------------ gpjax/variational_inference.py | 19 ++---- setup.py | 2 +- 12 files changed, 116 insertions(+), 159 deletions(-) diff --git a/examples/barycentres.pct.py b/examples/barycentres.pct.py index b24c36cc..1a3e3c21 100644 --- a/examples/barycentres.pct.py +++ b/examples/barycentres.pct.py @@ -100,24 +100,23 @@ def fit_gp(x: jnp.DeviceArray, y: jnp.DeviceArray): D = gpx.Dataset(X=x, y=y) likelihood = gpx.Gaussian(num_datapoints=n) posterior = gpx.Prior(kernel=gpx.RBF()) * likelihood - params, trainables, constrainers, unconstrainers = gpx.initialise( + parameter_state = gpx.initialise( posterior, key - ).unpack() - params = gpx.transform(params, unconstrainers) + ) + params, trainables, bijectors = parameter_state.unpack() + params = gpx.unconstrain(params, bijectors) objective = jax.jit( - posterior.marginal_log_likelihood(D, constrainers, negative=True) + posterior.marginal_log_likelihood(D, negative=True) ) opt = ox.adam(learning_rate=0.01) learned_params, training_history = gpx.fit( objective=objective, - trainables=trainables, - params=params, + parameter_state=parameter_state, optax_optim=opt, n_iters=1000, ).unpack() - learned_params = gpx.transform(learned_params, constrainers) return likelihood(posterior(D, learned_params)(xtest), learned_params) @@ -135,7 +134,7 @@ def sqrtm(A: jnp.DeviceArray): def wasserstein_barycentres( - distributions: tp.List[dx.Distribution], weights: jnp.DeviceArray + distributions: tp.List[numpyro.distributions.Distribution], weights: jnp.DeviceArray ): covariances = [d.covariance_matrix for d in distributions] cov_stack = jnp.stack(covariances) @@ -180,7 +179,7 @@ def step(covariance_candidate: jnp.DeviceArray, i: jnp.DeviceArray): # %% def plot( - dist: dx.Distribution, + dist: numpyro.distributions.Distribution, ax, color: str = "tab:blue", label: str = None, diff --git a/examples/classification.pct.py b/examples/classification.pct.py index 876da984..9e22f3e7 100644 --- a/examples/classification.pct.py +++ b/examples/classification.pct.py @@ -19,8 +19,6 @@ # # In this notebook we demonstrate how to perform inference for Gaussian process models with non-Gaussian likelihoods via maximum a posteriori (MAP) and Markov chain Monte Carlo (MCMC). We focus on a classification task here and use [BlackJax](https://github.com/blackjax-devs/blackjax/) for sampling. -import blackjax - # %% import jax import jax.numpy as jnp @@ -30,7 +28,7 @@ import numpyro import optax as ox from jaxtyping import Array, Float - +import blackjax import gpjax as gpx from gpjax.utils import I @@ -72,34 +70,31 @@ # To begin we obtain a set of initial parameter values through the `initialise` callable, and transform these to the unconstrained space via `transform` (see the [regression notebook](https://gpjax.readthedocs.io/en/latest/nbs/regression.html)). We also define the negative marginal log-likelihood, and JIT compile this to accelerate training. # %% parameter_state = gpx.initialise(posterior) -params, trainable, constrainer, unconstrainer = parameter_state.unpack() -params = gpx.transform(params, unconstrainer) +params, trainable, bijectors = parameter_state.unpack() +params = gpx.unconstrain(params, bijectors) -mll = jax.jit(posterior.marginal_log_likelihood(D, constrainer, negative=True)) +mll = jax.jit(posterior.marginal_log_likelihood(D, negative=True)) # %% [markdown] # We can obtain a MAP estimate by optimising the marginal log-likelihood with Obtax's optimisers. # %% opt = ox.adam(learning_rate=0.01) -unconstrained_params, training_history = gpx.fit( +learned_params, training_history = gpx.fit( mll, - params, - trainable, + parameter_state, opt, n_iters=500, ).unpack() -negative_Hessian = jax.jacfwd(jax.jacrev(mll))(unconstrained_params)["latent"][ +negative_Hessian = jax.jacfwd(jax.jacrev(mll))(learned_params)["latent"][ "latent" ][:, 0, :, 0] - -map_estimate = gpx.transform(unconstrained_params, constrainer) # %% [markdown] # From which we can make predictions at novel inputs, as illustrated below. # %% -latent_dist = posterior(D, map_estimate)(xtest) +latent_dist = posterior(D, learned_params)(xtest) -predictive_dist = likelihood(latent_dist, map_estimate) +predictive_dist = likelihood(latent_dist, learned_params) predictive_mean = predictive_dist.mean predictive_std = jnp.sqrt(predictive_dist.variance) @@ -139,7 +134,7 @@ # The Laplace approximation improves uncertainty quantification by incorporating curvature induced by the marginal log-likelihood's Hessian to construct an approximate Gaussian distribution centered on the MAP estimate. # Since the negative Hessian is positive definite, we can use the Cholesky decomposition to obtain the covariance matrix of the Laplace approximation at the datapoints below. # %% -f_map_estimate = posterior(D, map_estimate)(x).mean +f_map_estimate = posterior(D, learned_params)(x).mean jitter = 1e-6 @@ -224,7 +219,7 @@ def predict( # %% latent_dist = predict(laplace_approximation, D, xtest) -predictive_dist = likelihood(latent_dist, map_estimate) +predictive_dist = likelihood(latent_dist, learned_params) predictive_mean = predictive_dist.mean predictive_std = predictive_dist.variance**0.5 @@ -276,7 +271,7 @@ def predict( num_adapt = 500 num_samples = 200 -mll = jax.jit(posterior.marginal_log_likelihood(D, constrainer, negative=False)) +mll = jax.jit(posterior.marginal_log_likelihood(D, negative=False)) adapt = blackjax.window_adaptation( blackjax.nuts, mll, num_adapt, target_acceptance_rate=0.65 @@ -334,7 +329,6 @@ def one_step(state, rng_key): ps["kernel"]["lengthscale"] = states.position["kernel"]["lengthscale"][i] ps["kernel"]["variance"] = states.position["kernel"]["variance"][i] ps["latent"] = states.position["latent"][i, :, :] - ps = gpx.transform(ps, constrainer) latent_dist = posterior(D, ps)(xtest) predictive_dist = likelihood(latent_dist, ps) diff --git a/examples/collapsed_vi.pct.py b/examples/collapsed_vi.pct.py index f80431b2..fe48d87b 100644 --- a/examples/collapsed_vi.pct.py +++ b/examples/collapsed_vi.pct.py @@ -6,7 +6,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -86,22 +86,21 @@ # %% [markdown] # We now train our model akin to a Gaussian process regression model via the `fit` abstraction. Unlike the regression example given in the [conjugate regression notebook](https://gpjax.readthedocs.io/en/latest/nbs/regression.html), the inducing locations that induce our variational posterior distribution are now part of the model's parameters. Using a gradient-based optimiser, we can then _optimise_ their location such that the evidence lower bound is maximised. # %% -params, trainables, constrainers, unconstrainers = gpx.initialise(sgpr, key).unpack() +parameter_state = gpx.initialise(sgpr, key) +params, trainables, bijectors = parameter_state.unpack() -loss_fn = jit(sgpr.elbo(D, constrainers, negative=True)) +loss_fn = jit(sgpr.elbo(D, negative=True)) optimiser = ox.adam(learning_rate=0.005) -params = gpx.transform(params, unconstrainers) +params = gpx.unconstrain(params, bijectors) learned_params, training_history = gpx.fit( objective=loss_fn, - params=params, - trainables=trainables, + parameter_state=parameter_state, optax_optim=optimiser, n_iters=2000, ).unpack() -learned_params = gpx.transform(learned_params, constrainers) # %% [markdown] # We show predictions of our model with the learned inducing points overlayed in grey. @@ -109,10 +108,10 @@ latent_dist = q.predict(D, learned_params)(xtest) predictive_dist = likelihood(latent_dist, learned_params) -samples = latent_dist.sample(seed=key, sample_shape=20) +samples = latent_dist.sample(key, sample_shape=(20, )) -predictive_mean = predictive_dist.mean() -predictive_std = predictive_dist.stddev() +predictive_mean = predictive_dist.mean +predictive_std = jnp.sqrt(predictive_dist.variance) fig, ax = plt.subplots(figsize=(12, 5)) @@ -166,15 +165,15 @@ # %% full_rank_model = gpx.Prior(kernel=gpx.RBF()) * gpx.Gaussian(num_datapoints=D.n) -fr_params, fr_trainables, fr_constrainers, fr_unconstrainers = gpx.initialise( +fr_params, fr_trainables, fr_bijectors = gpx.initialise( full_rank_model, key ).unpack() -fr_params = gpx.transform(fr_params, fr_unconstrainers) -mll = jit(full_rank_model.marginal_log_likelihood(D, fr_constrainers, negative=True)) +fr_params = gpx.unconstrain(fr_params, fr_bijectors) +mll = jit(full_rank_model.marginal_log_likelihood(D, negative=True)) # %timeit mll(fr_params).block_until_ready() # %% -sparse_elbo = jit(sgpr.elbo(D, constrainers, negative=True)) +sparse_elbo = jit(sgpr.elbo(D, negative=True)) # %timeit sparse_elbo(params).block_until_ready() # %% [markdown] diff --git a/examples/regression.pct.py b/examples/regression.pct.py index 51735685..1e976025 100644 --- a/examples/regression.pct.py +++ b/examples/regression.pct.py @@ -146,20 +146,21 @@ # We can now unpack the `ParameterState` to receive each of the four components listed above. # %% -params, trainable, constrainer, unconstrainer = parameter_state.unpack() +params, trainable, bijectors = parameter_state.unpack() pp.pprint(params) # %% [markdown] # To motivate the purpose of `constrainer` and `unconstrainer` more precisely, notice that our model hyperparameters $\{\ell^2, \sigma^2, \alpha^2 \}$ are all strictly positive. To ensure more stable optimisation, it is strongly advised to transform the parameters onto an unconstrained space first via `transform`. # %% -params = gpx.transform(params, unconstrainer) +# params = gpx.constrain(params, bijectors) +pp.pprint(params) # %% [markdown] # To train our hyperparameters, we optimising the marginal log-likelihood of the posterior with respect to them. We define the marginal log-likelihood with `marginal_log_likelihood` on the posterior. # %% -mll = jit(posterior.marginal_log_likelihood(D, constrainer, negative=True)) +mll = jit(posterior.marginal_log_likelihood(D, negative=True)) mll(params) # %% [markdown] # Since most optimisers (including here) minimise a given function, we have realised the negative marginal log-likelihood and just-in-time (JIT) compiled this to accelerate training. @@ -170,10 +171,9 @@ # %% opt = ox.adam(learning_rate=0.01) inference_state = gpx.fit( - mll, - params, - trainable, - opt, + objective = mll, + parameter_state = parameter_state, + optax_optim = opt, n_iters=500, ) @@ -182,23 +182,13 @@ # %% final_params, training_history = inference_state.unpack() - -# %% [markdown] -# -# The exact value of our learned parameters is often useful in answering certain questions about the underlying process. To obtain these values, we untransfom our trained unconstrained parameters back to their original constrained space with `transform` and `constrainer`. - -# %% -final_params = gpx.transform(final_params, constrainer) -pp.pprint(final_params) +final_params # %% [markdown] # ## Prediction # # Equipped with the posterior and a set of optimised hyperparameter values, we are now in a position to query our GP's predictive distribution at novel test inputs. To do this, we use our defined `posterior` and `likelihood` at our test inputs to obtain the predictive distribution as a `Distrax` multivariate Gaussian upon which `mean` and `stddev` can be used to extract the predictive mean and standard deviatation. -# %% -latent_dist - # %% latent_dist = posterior(D, final_params)(xtest) predictive_dist = likelihood(latent_dist, final_params) diff --git a/examples/uncollapsed_vi.pct.py b/examples/uncollapsed_vi.pct.py index 6ad8d60a..fca98678 100644 --- a/examples/uncollapsed_vi.pct.py +++ b/examples/uncollapsed_vi.pct.py @@ -7,9 +7,9 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: -# display_name: Python 3.10.0 ('base') +# display_name: Python 3.9.7 ('gpjax') # language: python # name: python3 # --- @@ -137,10 +137,15 @@ # # Since Optax's optimisers work to minimise functions, to maximise the ELBO we return its negative. # %% -params, trainables, constrainers, unconstrainers = gpx.initialise(svgp, key).unpack() -params = gpx.transform(params, unconstrainers) +parameter_state = gpx.initialise(svgp, key) +params, trainables, bijectors = parameter_state.unpack() +params = gpx.unconstrain(params, bijectors) -loss_fn = jit(svgp.elbo(D, constrainers, negative=True)) +loss_fn = svgp.elbo(D, negative=True) + +# %% +b = gpx.abstractions.get_batch(D, 64, key) +loss_fn(params, b) # %% [markdown] # ### Mini-batching @@ -152,8 +157,7 @@ inference_state = gpx.fit_batches( objective=loss_fn, - params=params, - trainables=trainables, + parameter_state=parameter_state, train_data=D, optax_optim=optimiser, n_iters=4000, diff --git a/gpjax/__init__.py b/gpjax/__init__.py index 661c5cb8..2e83fc69 100644 --- a/gpjax/__init__.py +++ b/gpjax/__init__.py @@ -47,7 +47,7 @@ __description__ = "Didactic Gaussian processes in JAX" __url__ = "https://github.com/thomaspinder/GPJax" __contributors__ = "https://github.com/thomaspinder/GPJax/graphs/contributors" -__version__ = "0.4.13" +__version__ = "0.5.0" __all__ = [ diff --git a/gpjax/config.py b/gpjax/config.py index da585a9b..fa7be8fa 100644 --- a/gpjax/config.py +++ b/gpjax/config.py @@ -6,16 +6,6 @@ import distrax as dx import jax.numpy as jnp -Softplus = dx.Lambda( - forward=lambda x: jnp.log(1 + jnp.exp(x)), - inverse=lambda x: jnp.log(jnp.exp(x) - 1.0), -) - -# TODO: Remove this once 'FillTriangular' is added to Distrax. -FillTriangular = dx.Chain([tfb.FillTriangular()]) - -Identity = dx.Lambda(forward=lambda x: x, inverse=lambda x: x) - def get_defaults() -> ConfigDict: """Construct and globally register the config file used within GPJax. diff --git a/gpjax/gps.py b/gpjax/gps.py index 7ac672b4..4d23a1c7 100644 --- a/gpjax/gps.py +++ b/gpjax/gps.py @@ -39,7 +39,7 @@ class AbstractGP: """Abstract Gaussian process object.""" - def __call__(self, *args: tp.Any, **kwargs: tp.Any) -> npd.Distribution: + def __call__(self, *args: Any, **kwargs: Any) -> npd.Distribution: """Evaluate the Gaussian process at the given points. Args: diff --git a/gpjax/likelihoods.py b/gpjax/likelihoods.py index e58d2d3d..0585ba96 100644 --- a/gpjax/likelihoods.py +++ b/gpjax/likelihoods.py @@ -33,7 +33,7 @@ class AbstractLikelihood: num_datapoints: int # The number of datapoints that the likelihood factorises over. name: Optional[str] = "Likelihood" - def __call__(self, *args: Any, **kwargs: Any) -> dx.Distribution: + def __call__(self, *args: Any, **kwargs: Any) -> npd.Distribution: """Evaluate the likelihood function at a given predictive distribution. Args: @@ -46,7 +46,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> dx.Distribution: return self.predict(*args, **kwargs) @abc.abstractmethod - def predict(self, *args: Any, **kwargs: Any) -> dx.Distribution: + def predict(self, *args: Any, **kwargs: Any) -> npd.Distribution: """Evaluate the likelihood function at a given predictive distribution. Args: diff --git a/gpjax/variational_families.py b/gpjax/variational_families.py index 0a939fd8..dc02fa94 100644 --- a/gpjax/variational_families.py +++ b/gpjax/variational_families.py @@ -16,11 +16,11 @@ import abc from typing import Any, Callable, Dict, Optional -import distrax as dx import jax.numpy as jnp import jax.scipy as jsp from chex import dataclass from jaxtyping import Array, Float +import numpyro.distributions as npd from .config import get_defaults from .gps import Prior @@ -32,11 +32,21 @@ DEFAULT_JITTER = get_defaults()["jitter"] +def kl_divergence(p, q): + N = p.mean.shape[0] + iS1 = jnp.linalg.inv(q.covariance_matrix) + diff = q.mean - p.mean + tr_term = jnp.trace(jnp.matmul(iS1, p.covariance_matrix)) + det_term = jnp.log(jnp.linalg.det(q.covariance_matrix) / jnp.linalg.det(p.covariance_matrix)) + quad_term = diff.T @ jnp.linalg.inv(q.covariance_matrix) @ diff + return 0.5 * (tr_term + det_term + quad_term - N) + + @dataclass class AbstractVariationalFamily: """Abstract base class used to represent families of distributions that can be used within variational inference.""" - def __call__(self, *args: Any, **kwargs: Any) -> dx.Distribution: + def __call__(self, *args: Any, **kwargs: Any) -> npd.Distribution: """For a given set of parameters, compute the latent function's prediction under the variational approximation. Args: @@ -61,7 +71,7 @@ def _initialise_params(self, key: PRNGKeyType) -> Dict: raise NotImplementedError @abc.abstractmethod - def predict(self, *args: Any, **kwargs: Any) -> dx.Distribution: + def predict(self, *args: Any, **kwargs: Any) -> npd.Distribution: """Predict the GP's output given the input. Args: @@ -142,12 +152,11 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: Kzz += I(m) * self.jitter Lz = jnp.linalg.cholesky(Kzz) - qu = dx.MultivariateNormalTri(jnp.atleast_1d(mu.squeeze()), sqrt) - pu = dx.MultivariateNormalTri(jnp.atleast_1d(μz.squeeze()), Lz) - - return qu.kl_divergence(pu) + qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) + pu = npd.MultivariateNormal(jnp.atleast_1d(μz.squeeze()), scale_tril=Lz) + return kl_divergence(qu, pu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distribution]: + def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as: @@ -158,7 +167,7 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi params (Dict): The set of parameters that are to be used to parameterise our variational approximation and GP. Returns: - Callable[[Float[Array, "N D"]], dx.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. + Callable[[Float[Array, "N D"]], npd.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. """ mu = params["variational_family"]["moments"]["variational_mean"] sqrt = params["variational_family"]["moments"]["variational_root_covariance"] @@ -170,7 +179,7 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi Lz = jnp.linalg.cholesky(Kzz) μz = self.prior.mean_function(z, params["mean_function"]) - def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs n_test = t.shape[0] Ktt = gram(self.prior.kernel, t, params["kernel"]) @@ -197,9 +206,7 @@ def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: ) covariance += I(n_test) * self.jitter - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(mean.squeeze()), covariance - ) + return npd.MultivariateNormal(jnp.atleast_1d(mean.squeeze()), covariance) return predict_fn @@ -230,12 +237,13 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: sqrt = params["variational_family"]["moments"]["variational_root_covariance"] m = self.num_inducing - qu = dx.MultivariateNormalTri(jnp.atleast_1d(mu.squeeze()), sqrt) - pu = dx.MultivariateNormalDiag(jnp.zeros(m)) - - return qu.kl_divergence(pu) + qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) + pu = npd.MultivariateNormal( + jnp.zeros(m), jnp.eye(m) + ) # TODO: Implement diagonal MVGaussian. + return kl_divergence(qu, pu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distribution]: + def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as @@ -246,7 +254,7 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi params (Dict): The set of parameters that are to be used to parameterise our variational approximation and GP. Returns: - Callable[[Float[Array, "N D"]], dx.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. + Callable[[Float[Array, "N D"]], npd.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. """ mu = params["variational_family"]["moments"]["variational_mean"] sqrt = params["variational_family"]["moments"]["variational_root_covariance"] @@ -257,7 +265,7 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi Kzz += I(m) * self.jitter Lz = jnp.linalg.cholesky(Kzz) - def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs n_test = t.shape[0] Ktt = gram(self.prior.kernel, t, params["kernel"]) @@ -281,9 +289,7 @@ def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: ) covariance += I(n_test) * self.jitter - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(mean.squeeze()), covariance - ) + return npd.MultivariateNormal(jnp.atleast_1d(mean.squeeze()), covariance) return predict_fn @@ -360,12 +366,12 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: Kzz += I(m) * self.jitter Lz = jnp.linalg.cholesky(Kzz) - qu = dx.MultivariateNormalTri(jnp.atleast_1d(mu.squeeze()), sqrt) - pu = dx.MultivariateNormalTri(jnp.atleast_1d(μz.squeeze()), Lz) + qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) + pu = npd.MultivariateNormal(jnp.atleast_1d(μz.squeeze()), scale_tril=Lz) - return qu.kl_divergence(pu) + return kl_divergence(qu, pu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distribution]: + def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as @@ -378,7 +384,7 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi params (Dict): The set of parameters that are to be used to parameterise our variational approximation and GP. Returns: - Callable[[Float[Array, "N D"]], dx.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. + Callable[[Float[Array, "N D"]], npd.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. """ natural_vector = params["variational_family"]["moments"]["natural_vector"] natural_matrix = params["variational_family"]["moments"]["natural_matrix"] @@ -408,7 +414,7 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi Lz = jnp.linalg.cholesky(Kzz) μz = self.prior.mean_function(z, params["mean_function"]) - def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs Ktt = gram(self.prior.kernel, t, params["kernel"]) Kzt = cross_covariance(self.prior.kernel, z, t, params["kernel"]) @@ -433,9 +439,7 @@ def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + jnp.matmul(Ktz_Kzz_inv_L, Ktz_Kzz_inv_L.T) ) - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(mean.squeeze()), covariance - ) + return npd.MultivariateNormal(jnp.atleast_1d(mean.squeeze()), covariance) return predict_fn @@ -486,12 +490,8 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: Returns: Float[Array, "1"]: The KL-divergence between our variational approximation and the GP prior. """ - expectation_vector = params["variational_family"]["moments"][ - "expectation_vector" - ] - expectation_matrix = params["variational_family"]["moments"][ - "expectation_matrix" - ] + expectation_vector = params["variational_family"]["moments"]["expectation_vector"] + expectation_matrix = params["variational_family"]["moments"]["expectation_matrix"] z = params["variational_family"]["inducing_inputs"] m = self.num_inducing @@ -510,12 +510,12 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: Kzz += I(m) * self.jitter Lz = jnp.linalg.cholesky(Kzz) - qu = dx.MultivariateNormalTri(jnp.atleast_1d(mu.squeeze()), sqrt) - pu = dx.MultivariateNormalTri(jnp.atleast_1d(μz.squeeze()), Lz) + qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) + pu = npd.MultivariateNormal(jnp.atleast_1d(μz.squeeze()), scale_tril=Lz) - return qu.kl_divergence(pu) + return kl_divergence(qu, pu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distribution]: + def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as @@ -528,14 +528,10 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi params (Dict): The set of parameters that are to be used to parameterise our variational approximation and GP. Returns: - Callable[[Float[Array, "N D"]], dx.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. + Callable[[Float[Array, "N D"]], npd.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. """ - expectation_vector = params["variational_family"]["moments"][ - "expectation_vector" - ] - expectation_matrix = params["variational_family"]["moments"][ - "expectation_matrix" - ] + expectation_vector = params["variational_family"]["moments"]["expectation_vector"] + expectation_matrix = params["variational_family"]["moments"]["expectation_matrix"] z = params["variational_family"]["inducing_inputs"] m = self.num_inducing @@ -554,7 +550,7 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], dx.Distributi Lz = jnp.linalg.cholesky(Kzz) μz = self.prior.mean_function(z, params["mean_function"]) - def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs Ktt = gram(self.prior.kernel, t, params["kernel"]) Kzt = cross_covariance(self.prior.kernel, z, t, params["kernel"]) @@ -579,9 +575,7 @@ def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + jnp.matmul(Ktz_Kzz_inv_sqrt, Ktz_Kzz_inv_sqrt.T) ) - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(mean.squeeze()), covariance - ) + return npd.MultivariateNormal(jnp.atleast_1d(mean.squeeze()), covariance) return predict_fn @@ -611,20 +605,18 @@ def _initialise_params(self, key: PRNGKeyType) -> Dict: self.prior._initialise_params(key), { "variational_family": {"inducing_inputs": self.inducing_inputs}, - "likelihood": { - "obs_noise": self.likelihood._initialise_params(key)["obs_noise"] - }, + "likelihood": {"obs_noise": self.likelihood._initialise_params(key)["obs_noise"]}, }, ) def predict( self, train_data: Dataset, params: Dict - ) -> Callable[[Float[Array, "N D"]], dx.Distribution]: + ) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs. Args: params (Dict): The set of parameters that are to be used to parameterise our variational approximation and GP. Returns: - Callable[[Float[Array, "N D"]], dx.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. + Callable[[Float[Array, "N D"]], npd.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. """ x, y = train_data.X, train_data.y @@ -658,11 +650,9 @@ def predict( Lz_inv_Kzx_diff = jsp.linalg.cho_solve((L, True), jnp.matmul(Lz_inv_Kzx, diff)) # Kzz⁻¹ Kzx (y - μx) - Kzz_inv_Kzx_diff = jsp.linalg.solve_triangular( - Lz.T, Lz_inv_Kzx_diff, lower=False - ) + Kzz_inv_Kzx_diff = jsp.linalg.solve_triangular(Lz.T, Lz_inv_Kzx_diff, lower=False) - def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs Ktt = gram(self.prior.kernel, t, params["kernel"]) Kzt = cross_covariance(self.prior.kernel, z, t, params["kernel"]) @@ -684,9 +674,7 @@ def predict_fn(test_inputs: Float[Array, "N D"]) -> dx.Distribution: + jnp.matmul(L_inv_Lz_inv_Kzt.T, L_inv_Lz_inv_Kzt) ) - return dx.MultivariateNormalFullCovariance( - jnp.atleast_1d(mean.squeeze()), covariance - ) + return npd.MultivariateNormal(jnp.atleast_1d(mean.squeeze()), covariance) return predict_fn diff --git a/gpjax/variational_inference.py b/gpjax/variational_inference.py index 16e1e604..97ec0b19 100644 --- a/gpjax/variational_inference.py +++ b/gpjax/variational_inference.py @@ -104,9 +104,7 @@ def elbo_fn(params: Dict, batch: Dataset) -> Float[Array, "1"]: return elbo_fn - def variational_expectation( - self, params: Dict, batch: Dataset - ) -> Float[Array, "N 1"]: + def variational_expectation(self, params: Dict, batch: Dataset) -> Float[Array, "N 1"]: """Compute the expectation of our model's log-likelihood under our variational distribution. Batching can be done here to speed up computation. Args: @@ -119,15 +117,14 @@ def variational_expectation( x, y = batch.X, batch.y # q(f(x)) + print(self.variational_family.predict(params)) predictive_dist = vmap(self.variational_family.predict(params))(x[:, None]) mean = predictive_dist.mean().val.reshape(-1, 1) variance = predictive_dist.variance().val.reshape(-1, 1) # log(p(y|f(x))) log_prob = vmap( - lambda f, y: self.likelihood.link_function( - f, params["likelihood"] - ).log_prob(y) + lambda f, y: self.likelihood.link_function(f, params["likelihood"]).log_prob(y) ) # ≈ ∫[log(p(y|f(x))) q(f(x))] df(x) @@ -176,9 +173,7 @@ def elbo_fn(params: Dict) -> Float[Array, "1"]: Kzz = gram(self.prior.kernel, z, params["kernel"]) Kzz += I(m) * self.variational_family.jitter Kzx = cross_covariance(self.prior.kernel, z, x, params["kernel"]) - Kxx_diag = vmap(self.prior.kernel, in_axes=(0, 0, None))( - x, x, params["kernel"] - ) + Kxx_diag = vmap(self.prior.kernel, in_axes=(0, 0, None))(x, x, params["kernel"]) μx = self.prior.mean_function(x, params["mean_function"]) Lz = jnp.linalg.cholesky(Kzz) @@ -224,12 +219,10 @@ def elbo_fn(params: Dict) -> Float[Array, "1"]: diff = y - μx # L⁻¹ A (y - μx) - L_inv_A_diff = jsp.linalg.solve_triangular( - L, jnp.matmul(A, diff), lower=True - ) + L_inv_A_diff = jsp.linalg.solve_triangular(L, jnp.matmul(A, diff), lower=True) # (y - μx)ᵀ (Iσ² + Q)⁻¹ (y - μx) - quad = (jnp.sum(diff**2) - jnp.sum(L_inv_A_diff**2)) / noise + quad = (jnp.sum(diff ** 2) - jnp.sum(L_inv_A_diff ** 2)) / noise # 2 * log N(y; μx, Iσ² + Q) two_log_prob = -n * jnp.log(2.0 * jnp.pi * noise) - log_det_B - quad diff --git a/setup.py b/setup.py index 79fa846c..13af1caa 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ def parse_requirements_file(filename): "jaxlib>=0.1.47", "optax", "chex", - "distrax>=0.1.2", + "numpyro", "tensorflow-probability>=0.16.0", "tqdm>=4.0.0", "ml-collections==0.1.0", From b40f883bf59f28b86980b8406a1d6bf046bc01a2 Mon Sep 17 00:00:00 2001 From: Daniel Dodd Date: Fri, 14 Oct 2022 14:57:40 +0100 Subject: [PATCH 03/14] Improve kl divergence efficiency. --- gpjax/variational_families.py | 139 +++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/gpjax/variational_families.py b/gpjax/variational_families.py index dc02fa94..7e9a5877 100644 --- a/gpjax/variational_families.py +++ b/gpjax/variational_families.py @@ -18,9 +18,9 @@ import jax.numpy as jnp import jax.scipy as jsp +import numpyro.distributions as npd from chex import dataclass from jaxtyping import Array, Float -import numpyro.distributions as npd from .config import get_defaults from .gps import Prior @@ -32,16 +32,6 @@ DEFAULT_JITTER = get_defaults()["jitter"] -def kl_divergence(p, q): - N = p.mean.shape[0] - iS1 = jnp.linalg.inv(q.covariance_matrix) - diff = q.mean - p.mean - tr_term = jnp.trace(jnp.matmul(iS1, p.covariance_matrix)) - det_term = jnp.log(jnp.linalg.det(q.covariance_matrix) / jnp.linalg.det(p.covariance_matrix)) - quad_term = diff.T @ jnp.linalg.inv(q.covariance_matrix) @ diff - return 0.5 * (tr_term + det_term + quad_term - N) - - @dataclass class AbstractVariationalFamily: """Abstract base class used to represent families of distributions that can be used within variational inference.""" @@ -154,9 +144,11 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) pu = npd.MultivariateNormal(jnp.atleast_1d(μz.squeeze()), scale_tril=Lz) - return kl_divergence(qu, pu) + return kld_dense_dense(qu, pu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: + def predict( + self, params: Dict + ) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as: @@ -235,15 +227,13 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: """ mu = params["variational_family"]["moments"]["variational_mean"] sqrt = params["variational_family"]["moments"]["variational_root_covariance"] - m = self.num_inducing qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) - pu = npd.MultivariateNormal( - jnp.zeros(m), jnp.eye(m) - ) # TODO: Implement diagonal MVGaussian. - return kl_divergence(qu, pu) + return kld_dense_white(qu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: + def predict( + self, params: Dict + ) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as @@ -369,9 +359,11 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) pu = npd.MultivariateNormal(jnp.atleast_1d(μz.squeeze()), scale_tril=Lz) - return kl_divergence(qu, pu) + return kld_dense_dense(qu, pu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: + def predict( + self, params: Dict + ) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as @@ -490,8 +482,12 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: Returns: Float[Array, "1"]: The KL-divergence between our variational approximation and the GP prior. """ - expectation_vector = params["variational_family"]["moments"]["expectation_vector"] - expectation_matrix = params["variational_family"]["moments"]["expectation_matrix"] + expectation_vector = params["variational_family"]["moments"][ + "expectation_vector" + ] + expectation_matrix = params["variational_family"]["moments"][ + "expectation_matrix" + ] z = params["variational_family"]["inducing_inputs"] m = self.num_inducing @@ -513,9 +509,11 @@ def prior_kl(self, params: Dict) -> Float[Array, "1"]: qu = npd.MultivariateNormal(jnp.atleast_1d(mu.squeeze()), scale_tril=sqrt) pu = npd.MultivariateNormal(jnp.atleast_1d(μz.squeeze()), scale_tril=Lz) - return kl_divergence(qu, pu) + return kld_dense_dense(qu, pu) - def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: + def predict( + self, params: Dict + ) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the predictive distribution of the GP at the test inputs t. This is the integral q(f(t)) = ∫ p(f(t)|u) q(u) du, which can be computed in closed form as @@ -530,8 +528,12 @@ def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribut Returns: Callable[[Float[Array, "N D"]], npd.Distribution]: A function that accepts a set of test points and will return the predictive distribution at those points. """ - expectation_vector = params["variational_family"]["moments"]["expectation_vector"] - expectation_matrix = params["variational_family"]["moments"]["expectation_matrix"] + expectation_vector = params["variational_family"]["moments"][ + "expectation_vector" + ] + expectation_matrix = params["variational_family"]["moments"][ + "expectation_matrix" + ] z = params["variational_family"]["inducing_inputs"] m = self.num_inducing @@ -605,7 +607,9 @@ def _initialise_params(self, key: PRNGKeyType) -> Dict: self.prior._initialise_params(key), { "variational_family": {"inducing_inputs": self.inducing_inputs}, - "likelihood": {"obs_noise": self.likelihood._initialise_params(key)["obs_noise"]}, + "likelihood": { + "obs_noise": self.likelihood._initialise_params(key)["obs_noise"] + }, }, ) @@ -650,7 +654,9 @@ def predict( Lz_inv_Kzx_diff = jsp.linalg.cho_solve((L, True), jnp.matmul(Lz_inv_Kzx, diff)) # Kzz⁻¹ Kzx (y - μx) - Kzz_inv_Kzx_diff = jsp.linalg.solve_triangular(Lz.T, Lz_inv_Kzx_diff, lower=False) + Kzz_inv_Kzx_diff = jsp.linalg.solve_triangular( + Lz.T, Lz_inv_Kzx_diff, lower=False + ) def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: t = test_inputs @@ -679,6 +685,81 @@ def predict_fn(test_inputs: Float[Array, "N D"]) -> npd.Distribution: return predict_fn +# TODO: Abstract these out to a KL divergence that accepts a linear operator to facilate structured covarainces other than dense. +def kld_dense_dense( + q: npd.MultivariateNormal, p: npd.MultivariateNormal +) -> Float[Array, "1"]: + """Kullback-Leibler divergence KL[q(x)||p(x)] between two dense covariance Gaussian distributions + q(x) = N(x; μq, Σq) and p(x) = N(x; μp, Σp). + + Args: + q (npd.MultivariateNormal): A multivariate Gaussian distribution. + p (npd.MultivariateNormal): A multivariate Gaussian distribution. + + Returns: + Float[Array, "1"]: The KL divergence between the two distributions. + """ + + q_mu = q.loc + q_sqrt = q.scale_tril + n = q_mu.shape[-1] + + p_mu = p.loc + p_sqrt = p.scale_tril + + diag = jnp.diag(q_sqrt) + + # Trace term tr(Σp⁻¹ Σq), and alpha for Mahalanobis term + alpha = jsp.linalg.solve_triangular(p_sqrt, p_mu - q_mu, lower=True) + trace = jnp.sum(jnp.square(jsp.linalg.solve_triangular(p_sqrt, q_sqrt, lower=True))) + + # Mahalanobis term: μqᵀ Σp⁻¹ μq + mahalanobis = jnp.sum(jnp.square(alpha)) + + # log|Σq| + logdet_qcov = jnp.sum(jnp.log(jnp.square(diag))) + two_kl = mahalanobis - n - logdet_qcov + trace + + # log|Σp| + log_det_pcov = jnp.sum(jnp.log(jnp.square(jnp.diag(q_sqrt)))) + two_kl += log_det_pcov + + return two_kl / 2.0 + + +def kld_dense_white(q: npd.MultivariateNormal) -> Float[Array, "1"]: + """Kullback-Leibler divergence KL[q(x)||p(x)] between a dense covariance Gaussian distribution + q(x) = N(x; μq, Σq), and white indenity Gaussian p(x) = N(x; 0, I). + + This is useful for variational inference with a whitened variational family. + + Args: + q (npd.MultivariateNormal): A multivariate Gaussian distribution. + + Returns: + Float[Array, "1"]: The KL divergence between the two distributions. + """ + + q_mu = q.loc + q_sqrt = q.scale_tril + n = q_mu.shape[-1] + + diag = jnp.diag(q_sqrt) + + # Trace term tr(Σp⁻¹ Σq), and alpha for Mahalanobis term: + alpha = q_mu + trace = jnp.sum(jnp.square(q_sqrt)) + + # Mahalanobis term: μqᵀ Σp⁻¹ μq + mahalanobis = jnp.sum(jnp.square(alpha)) + + # log|Σq| (no log|Σp| as this is just zero!) + logdet_qcov = jnp.sum(jnp.log(jnp.square(diag))) + two_kl = mahalanobis - n - logdet_qcov + trace + + return two_kl / 2.0 + + __all__ = [ "AbstractVariationalFamily", "AbstractVariationalGaussian", From 70a400e4a3a113ebe91f76a3310709d50d828af6 Mon Sep 17 00:00:00 2001 From: Daniel Dodd Date: Fri, 14 Oct 2022 19:07:48 +0100 Subject: [PATCH 04/14] Fix variational expectation vmap. --- gpjax/quadrature.py | 7 +++---- gpjax/variational_inference.py | 33 ++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/gpjax/quadrature.py b/gpjax/quadrature.py index 8312f6e5..839c6b9d 100644 --- a/gpjax/quadrature.py +++ b/gpjax/quadrature.py @@ -26,7 +26,7 @@ def gauss_hermite_quadrature( fun: Callable, mean: Float[Array, "N D"], - var: Float[Array, "N D"], + sd: Float[Array, "N D"], deg: Optional[int] = DEFAULT_NUM_GAUSS_HERMITE_POINTS, *args, **kwargs @@ -36,15 +36,14 @@ def gauss_hermite_quadrature( Args: fun (Callable): The function for which quadrature should be applied to. mean (Float[Array, "N D"]): The mean of the Gaussian distribution that is used to shift quadrature points. - var (Float[Array, "N D"]): The variance of the Gaussian distribution that is used to scale quadrature points. + sd (Float[Array, "N D"]): The standard deviation of the Gaussian distribution that is used to scale quadrature points. deg (int, optional): The number of quadrature points that are to be used. Defaults to 20. Returns: Float[Array, "N"]: The evaluated integrals value. """ gh_points, gh_weights = np.polynomial.hermite.hermgauss(deg) - stdev = jnp.sqrt(var) - X = mean + jnp.sqrt(2.0) * stdev * gh_points + X = mean + jnp.sqrt(2.0) * sd * gh_points W = gh_weights / jnp.sqrt(jnp.pi) return jnp.sum(fun(X, *args, **kwargs) * W, axis=1) diff --git a/gpjax/variational_inference.py b/gpjax/variational_inference.py index 97ec0b19..7d701fd0 100644 --- a/gpjax/variational_inference.py +++ b/gpjax/variational_inference.py @@ -104,7 +104,9 @@ def elbo_fn(params: Dict, batch: Dataset) -> Float[Array, "1"]: return elbo_fn - def variational_expectation(self, params: Dict, batch: Dataset) -> Float[Array, "N 1"]: + def variational_expectation( + self, params: Dict, batch: Dataset + ) -> Float[Array, "N 1"]: """Compute the expectation of our model's log-likelihood under our variational distribution. Batching can be done here to speed up computation. Args: @@ -115,20 +117,21 @@ def variational_expectation(self, params: Dict, batch: Dataset) -> Float[Array, Array: The expectation of the model's log-likelihood under our variational distribution. """ x, y = batch.X, batch.y + link_fn = self.likelihood.link_function - # q(f(x)) - print(self.variational_family.predict(params)) - predictive_dist = vmap(self.variational_family.predict(params))(x[:, None]) - mean = predictive_dist.mean().val.reshape(-1, 1) - variance = predictive_dist.variance().val.reshape(-1, 1) + # variational distribution q(f(.)) = N(f(.); μ(.), Σ(., .)) + q = self.variational_family(params) + + # μ(x) and √diag(Σ(x, x)) + (mean, sd), _ = vmap(lambda x_i: q(x_i).tree_flatten())(x[:, None]) # log(p(y|f(x))) - log_prob = vmap( - lambda f, y: self.likelihood.link_function(f, params["likelihood"]).log_prob(y) - ) + log_prob = vmap(lambda f, y: link_fn(f, params["likelihood"]).log_prob(y)) # ≈ ∫[log(p(y|f(x))) q(f(x))] df(x) - expectation = gauss_hermite_quadrature(log_prob, mean, variance, y=y) + expectation = gauss_hermite_quadrature( + log_prob, mean.reshape(-1, 1), sd.reshape(-1, 1), y=y + ) return expectation @@ -173,7 +176,9 @@ def elbo_fn(params: Dict) -> Float[Array, "1"]: Kzz = gram(self.prior.kernel, z, params["kernel"]) Kzz += I(m) * self.variational_family.jitter Kzx = cross_covariance(self.prior.kernel, z, x, params["kernel"]) - Kxx_diag = vmap(self.prior.kernel, in_axes=(0, 0, None))(x, x, params["kernel"]) + Kxx_diag = vmap(self.prior.kernel, in_axes=(0, 0, None))( + x, x, params["kernel"] + ) μx = self.prior.mean_function(x, params["mean_function"]) Lz = jnp.linalg.cholesky(Kzz) @@ -219,10 +224,12 @@ def elbo_fn(params: Dict) -> Float[Array, "1"]: diff = y - μx # L⁻¹ A (y - μx) - L_inv_A_diff = jsp.linalg.solve_triangular(L, jnp.matmul(A, diff), lower=True) + L_inv_A_diff = jsp.linalg.solve_triangular( + L, jnp.matmul(A, diff), lower=True + ) # (y - μx)ᵀ (Iσ² + Q)⁻¹ (y - μx) - quad = (jnp.sum(diff ** 2) - jnp.sum(L_inv_A_diff ** 2)) / noise + quad = (jnp.sum(diff**2) - jnp.sum(L_inv_A_diff**2)) / noise # 2 * log N(y; μx, Iσ² + Q) two_log_prob = -n * jnp.log(2.0 * jnp.pi * noise) - log_det_B - quad From 9c1607c352d8abf0f071939585af38f7ad3f309b Mon Sep 17 00:00:00 2001 From: Daniel Dodd Date: Sat, 15 Oct 2022 10:26:00 +0100 Subject: [PATCH 05/14] Fix dense kl bug. --- gpjax/variational_families.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gpjax/variational_families.py b/gpjax/variational_families.py index 7e9a5877..96d9bfa9 100644 --- a/gpjax/variational_families.py +++ b/gpjax/variational_families.py @@ -709,11 +709,11 @@ def kld_dense_dense( diag = jnp.diag(q_sqrt) - # Trace term tr(Σp⁻¹ Σq), and alpha for Mahalanobis term - alpha = jsp.linalg.solve_triangular(p_sqrt, p_mu - q_mu, lower=True) + # Trace term tr(Σp⁻¹ Σq) trace = jnp.sum(jnp.square(jsp.linalg.solve_triangular(p_sqrt, q_sqrt, lower=True))) # Mahalanobis term: μqᵀ Σp⁻¹ μq + alpha = jsp.linalg.solve_triangular(p_sqrt, p_mu - q_mu, lower=True) mahalanobis = jnp.sum(jnp.square(alpha)) # log|Σq| @@ -721,7 +721,7 @@ def kld_dense_dense( two_kl = mahalanobis - n - logdet_qcov + trace # log|Σp| - log_det_pcov = jnp.sum(jnp.log(jnp.square(jnp.diag(q_sqrt)))) + log_det_pcov = jnp.sum(jnp.log(jnp.square(jnp.diag(p_sqrt)))) two_kl += log_det_pcov return two_kl / 2.0 From c1eb3463e7e54d951969657b0a7d3fa951754346 Mon Sep 17 00:00:00 2001 From: Daniel Dodd Date: Sat, 15 Oct 2022 11:27:06 +0100 Subject: [PATCH 06/14] Fix bijection bug. --- gpjax/parameters.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gpjax/parameters.py b/gpjax/parameters.py index 257d3115..93afa59d 100644 --- a/gpjax/parameters.py +++ b/gpjax/parameters.py @@ -178,7 +178,7 @@ def constrain(params: Dict, bijectors: Dict) -> Dict: Returns: Dict: A transformed parameter set. The dictionary is equal in structure to the input params dictionary. """ - map = lambda param, trans: trans.__call__(param) + map = lambda param, trans: trans.inv(param) return jax.tree_util.tree_map(map, params, bijectors) @@ -194,7 +194,7 @@ def unconstrain(params: Dict, bijectors: Dict) -> Dict: Dict: A transformed parameter set. The dictionary is equal in structure to the input params dictionary. """ - map = lambda param, trans: trans.inv(param) + map = lambda param, trans: trans.__call__(param) return jax.tree_util.tree_map(map, params, bijectors) @@ -202,7 +202,9 @@ def unconstrain(params: Dict, bijectors: Dict) -> Dict: ################################ # Priors ################################ -def log_density(param: Float[Array, "D"], density: npd.Distribution) -> Float[Array, "1"]: +def log_density( + param: Float[Array, "D"], density: npd.Distribution +) -> Float[Array, "1"]: """Compute the log density of a parameter given a distribution. Args: From 709c4405b1958843c1faaa68f3f766c4cef32526 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sat, 15 Oct 2022 20:11:47 +0000 Subject: [PATCH 07/14] Numpyro mwe --- docs/requirements.txt | 1 + examples/uncollapsed_vi.pct.py | 7 ++----- gpjax/parameters.py | 4 ++-- paper.bib => paper/paper.bib | 0 paper.md => paper/paper.md | 0 paper/paper.pdf | Bin 0 -> 209198 bytes setup.py | 1 - tests/test_config.py | 11 +++++------ tests/test_gp.py | 20 ++++++++++---------- tests/test_likelihoods.py | 28 ++++++++++++++-------------- tests/test_parameters.py | 20 ++++++++++---------- tests/test_variational_families.py | 14 +++++++------- 12 files changed, 51 insertions(+), 55 deletions(-) rename paper.bib => paper/paper.bib (100%) rename paper.md => paper/paper.md (100%) create mode 100644 paper/paper.pdf diff --git a/docs/requirements.txt b/docs/requirements.txt index 9f284740..f8305d6e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,6 +6,7 @@ jinja2 nbsphinx>=0.8.0 nb-black==1.0.7 matplotlib==3.3.3 +tensorflow-probability>=0.16.0 sphinx-copybutton networkx>=2.0.0 pandoc diff --git a/examples/uncollapsed_vi.pct.py b/examples/uncollapsed_vi.pct.py index fca98678..2133c4b9 100644 --- a/examples/uncollapsed_vi.pct.py +++ b/examples/uncollapsed_vi.pct.py @@ -139,8 +139,6 @@ # %% parameter_state = gpx.initialise(svgp, key) params, trainables, bijectors = parameter_state.unpack() -params = gpx.unconstrain(params, bijectors) - loss_fn = svgp.elbo(D, negative=True) # %% @@ -165,7 +163,6 @@ batch_size=128, ) learned_params, training_history = inference_state.unpack() -learned_params = gpx.transform(learned_params, constrainers) # %% [markdown] # ## Predictions # @@ -176,8 +173,8 @@ latent_dist = q(learned_params)(xtest) predictive_dist = likelihood(latent_dist, learned_params) -meanf = predictive_dist.mean() -sigma = predictive_dist.stddev() +meanf = predictive_dist.mean +sigma = predictive_dist.variance ** 0.5 fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(x, y, "o", alpha=0.15, label="Training Data", color="tab:gray") diff --git a/gpjax/parameters.py b/gpjax/parameters.py index 93afa59d..6a8d8c33 100644 --- a/gpjax/parameters.py +++ b/gpjax/parameters.py @@ -285,9 +285,9 @@ def prior_checks(priors: Dict) -> Dict: if "latent" in priors.keys(): latent_prior = priors["latent"] if latent_prior is not None: - if latent_prior.name != "Normal": + if not isinstance(latent_prior, npd.Normal): warnings.warn( - f"A {latent_prior.name} distribution prior has been placed on" + f"A {type(latent_prior)} distribution prior has been placed on" " the latent function. It is strongly advised that a" " unit Gaussian prior is used." ) diff --git a/paper.bib b/paper/paper.bib similarity index 100% rename from paper.bib rename to paper/paper.bib diff --git a/paper.md b/paper/paper.md similarity index 100% rename from paper.md rename to paper/paper.md diff --git a/paper/paper.pdf b/paper/paper.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bd501dd071641b3c7bff33ee96c619c46b7474cc GIT binary patch literal 209198 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 z,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.16.0", "tqdm>=4.0.0", "ml-collections==0.1.0", "jaxtyping>=0.0.2", diff --git a/tests/test_config.py b/tests/test_config.py index b2198a1c..2dcf5d6d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,26 +1,25 @@ from ml_collections import ConfigDict -from tensorflow_probability.substrates.jax import bijectors as tfb - +import numpyro.distributions as npd from gpjax.config import add_parameter, get_defaults def test_add_parameter(): - add_parameter("test_parameter", tfb.Identity()) + add_parameter("test_parameter", npd.transforms.IdentityTransform()) config = get_defaults() assert "test_parameter" in config.transformations assert "test_parameter_transform" in config.transformations assert config.transformations["test_parameter"] == "test_parameter_transform" - assert isinstance(config.transformations["test_parameter_transform"], tfb.Bijector) + assert isinstance(config.transformations["test_parameter_transform"], npd.transforms.Transform) def test_add_parameter(): config = get_defaults() - add_parameter("test_parameter", tfb.Identity()) + add_parameter("test_parameter", npd.transforms.IdentityTransform()) config = get_defaults() assert "test_parameter" in config.transformations assert "test_parameter_transform" in config.transformations assert config.transformations["test_parameter"] == "test_parameter_transform" - assert isinstance(config.transformations["test_parameter_transform"], tfb.Bijector) + assert isinstance(config.transformations["test_parameter_transform"], npd.transforms.Transform) def test_get_defaults(): diff --git a/tests/test_gp.py b/tests/test_gp.py index 5a940f53..c99242e8 100644 --- a/tests/test_gp.py +++ b/tests/test_gp.py @@ -1,9 +1,9 @@ import typing as tp -import distrax as dx import jax.numpy as jnp import jax.random as jr import pytest +import numpyro.distributions as npd from gpjax import Dataset, initialise from gpjax.gps import ( @@ -32,9 +32,9 @@ def test_prior(num_datapoints): x = jnp.linspace(-3.0, 3.0, num_datapoints).reshape(-1, 1) predictive_dist = prior_rv_fn(x) - assert isinstance(predictive_dist, dx.Distribution) - mu = predictive_dist.mean() - sigma = predictive_dist.covariance() + assert isinstance(predictive_dist, npd.Distribution) + mu = predictive_dist.mean + sigma = predictive_dist.covariance_matrix assert mu.shape == (num_datapoints,) assert sigma.shape == (num_datapoints, num_datapoints) @@ -75,10 +75,10 @@ def test_conjugate_posterior(num_datapoints): x = jnp.linspace(-3.0, 3.0, num_datapoints).reshape(-1, 1) predictive_dist = predictive_dist_fn(x) - assert isinstance(predictive_dist, dx.Distribution) + assert isinstance(predictive_dist, npd.Distribution) - mu = predictive_dist.mean() - sigma = predictive_dist.covariance() + mu = predictive_dist.mean + sigma = predictive_dist.covariance_matrix assert mu.shape == (num_datapoints,) assert sigma.shape == (num_datapoints, num_datapoints) @@ -117,10 +117,10 @@ def test_nonconjugate_posterior(num_datapoints, likel): x = jnp.linspace(-3.0, 3.0, num_datapoints).reshape(-1, 1) predictive_dist = predictive_dist_fn(x) - assert isinstance(predictive_dist, dx.Distribution) + assert isinstance(predictive_dist, npd.Distribution) - mu = predictive_dist.mean() - sigma = predictive_dist.covariance() + mu = predictive_dist.mean + sigma = predictive_dist.covariance_matrix assert mu.shape == (num_datapoints,) assert sigma.shape == (num_datapoints, num_datapoints) diff --git a/tests/test_likelihoods.py b/tests/test_likelihoods.py index 57f0ea09..e9b48f32 100644 --- a/tests/test_likelihoods.py +++ b/tests/test_likelihoods.py @@ -1,8 +1,8 @@ import typing as tp -import distrax as dx import jax.numpy as jnp import jax.random as jr +import numpyro.distributions as npd import pytest from gpjax.likelihoods import ( @@ -39,8 +39,8 @@ def test_predictive_moment(n): pred_mom_fn = lhood.predictive_moment_fn params, _, _ = initialise(lhood, key).unpack() rv = pred_mom_fn(fmean, fvar, params) - mu = rv.mean() - sigma = rv.variance() + mu = rv.mean + sigma = rv.variance assert isinstance(lhood.predictive_moment_fn, tp.Callable) assert mu.shape == (n,) assert sigma.shape == (n,) @@ -57,7 +57,7 @@ def test_link_fns(lik: AbstractLikelihood, n: int): x = jnp.linspace(-3.0, 3.0).reshape(-1, 1) l_eval = link_fn(x, params) - assert isinstance(l_eval, dx.Distribution) + assert isinstance(l_eval, npd.Distribution) @pytest.mark.parametrize("noise", [0.1, 0.5, 1.0]) @@ -65,33 +65,33 @@ def test_call_gaussian(noise): key = jr.PRNGKey(123) n = 10 lhood = Gaussian(num_datapoints=n) - dist = dx.MultivariateNormalFullCovariance(jnp.zeros(n), jnp.eye(n)) + dist = npd.MultivariateNormal(jnp.zeros(n), covariance_matrix=jnp.eye(n)) params = {"likelihood": {"obs_noise": noise}} l_dist = lhood(dist, params) - assert (l_dist.mean() == jnp.zeros(n)).all() + assert (l_dist.mean == jnp.zeros(n)).all() noise_mat = jnp.diag(jnp.repeat(noise, n)) - assert (l_dist.covariance() == jnp.eye(n) + noise_mat).all() + assert (l_dist.covariance_matrix == jnp.eye(n) + noise_mat).all() l_dist = lhood.predict(dist, params) - assert (l_dist.mean() == jnp.zeros(n)).all() + assert (l_dist.mean == jnp.zeros(n)).all() noise_mat = jnp.diag(jnp.repeat(noise, n)) - assert (l_dist.covariance() == jnp.eye(n) + noise_mat).all() + assert (l_dist.covariance_matrix == jnp.eye(n) + noise_mat).all() def test_call_bernoulli(): n = 10 lhood = Bernoulli(num_datapoints=n) - dist = dx.MultivariateNormalFullCovariance(jnp.zeros(n), jnp.eye(n)) + dist = npd.MultivariateNormal(jnp.zeros(n), covariance_matrix=jnp.eye(n)) params = {"likelihood": {}} l_dist = lhood(dist, params) - assert (l_dist.mean() == 0.5 * jnp.ones(n)).all() - assert (l_dist.variance() == 0.25 * jnp.ones(n)).all() + assert (l_dist.mean == 0.5 * jnp.ones(n)).all() + assert (l_dist.variance == 0.25 * jnp.ones(n)).all() l_dist = lhood.predict(dist, params) - assert (l_dist.mean() == 0.5 * jnp.ones(n)).all() - assert (l_dist.variance() == 0.25 * jnp.ones(n)).all() + assert (l_dist.mean == 0.5 * jnp.ones(n)).all() + assert (l_dist.variance == 0.25 * jnp.ones(n)).all() @pytest.mark.parametrize("lik", [Gaussian, Bernoulli]) diff --git a/tests/test_parameters.py b/tests/test_parameters.py index f384d046..4679fdf4 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -1,7 +1,7 @@ import typing as tp -import distrax as dx import jax.numpy as jnp +import numpyro.distributions as npd import jax.random as jr import pytest from tensorflow_probability.substrates.jax import distributions as tfd @@ -164,7 +164,7 @@ def test_checks(num_datapoints): priors = prior_checks(incomplete_priors) assert "latent" in priors.keys() assert "variance" not in priors.keys() - assert isinstance(priors["latent"], dx.Normal) + assert isinstance(priors["latent"], npd.Normal) def test_structure_priors(): @@ -189,7 +189,7 @@ def recursive_fn(d1, d2, fn: tp.Callable[[tp.Any], tp.Any]): assert v -@pytest.mark.parametrize("latent_prior", [dx.Laplace(0.0, 1.0), tfd.Laplace(0.0, 1.0)]) +@pytest.mark.parametrize("latent_prior", [npd.Laplace(0.0, 1.0), npd.Laplace(0.0, 1.0)]) def test_prior_checks(latent_prior): priors = { "kernel": {"lengthscale": None, "variance": None}, @@ -199,7 +199,7 @@ def test_prior_checks(latent_prior): } new_priors = prior_checks(priors) assert "latent" in new_priors.keys() - assert new_priors["latent"].name == "Normal" + assert isinstance(new_priors["latent"], npd.Normal) priors = { "kernel": {"lengthscale": None, "variance": None}, @@ -208,7 +208,7 @@ def test_prior_checks(latent_prior): } new_priors = prior_checks(priors) assert "latent" in new_priors.keys() - assert new_priors["latent"].name == "Normal" + assert isinstance(new_priors["latent"], npd.Normal) priors = { "kernel": {"lengthscale": None, "variance": None}, @@ -219,7 +219,7 @@ def test_prior_checks(latent_prior): with pytest.warns(UserWarning): new_priors = prior_checks(priors) assert "latent" in new_priors.keys() - assert new_priors["latent"].name == "Laplace" + assert isinstance(new_priors["latent"], npd.Laplace) ######################### @@ -233,8 +233,8 @@ def test_output(num_datapoints, likelihood): assert isinstance(bijectors, dict) for k, v1, v2 in recursive_items(bijectors, bijectors): - assert isinstance(v1.forward, tp.Callable) - assert isinstance(v2.inverse, tp.Callable) + assert isinstance(v1.__call__, tp.Callable) + assert isinstance(v2.inv, tp.Callable) unconstrained_params = unconstrain(params, bijectors) assert ( @@ -252,5 +252,5 @@ def test_output(num_datapoints, likelihood): a_bijectors = build_bijectors(augmented_params) assert "test_param" in list(a_bijectors.keys()) - assert a_bijectors["test_param"].forward(jnp.array([1.0])) == 1.0 - assert a_bijectors["test_param"].inverse(jnp.array([1.0])) == 1.0 + assert a_bijectors["test_param"](jnp.array([1.0])) == 1.0 + assert a_bijectors["test_param"].inv(jnp.array([1.0])) == 1.0 diff --git a/tests/test_variational_families.py b/tests/test_variational_families.py index e6f86827..5def2d16 100644 --- a/tests/test_variational_families.py +++ b/tests/test_variational_families.py @@ -1,7 +1,7 @@ import typing as tp from mimetypes import init -import distrax as dx +import numpyro.distributions as npd import jax.numpy as jnp import jax.random as jr import pytest @@ -128,10 +128,10 @@ def test_variational_gaussians( assert isinstance(predictive_dist_fn, tp.Callable) predictive_dist = predictive_dist_fn(test_inputs) - assert isinstance(predictive_dist, dx.Distribution) + assert isinstance(predictive_dist, npd.Distribution) - mu = predictive_dist.mean() - sigma = predictive_dist.covariance() + mu = predictive_dist.mean + sigma = predictive_dist.covariance_matrix assert isinstance(mu, jnp.ndarray) assert isinstance(sigma, jnp.ndarray) @@ -194,10 +194,10 @@ def test_collapsed_variational_gaussian(n_test, n_inducing, n_datapoints, point_ assert isinstance(predictive_dist_fn, tp.Callable) predictive_dist = predictive_dist_fn(test_inputs) - assert isinstance(predictive_dist, dx.Distribution) + assert isinstance(predictive_dist, npd.Distribution) - mu = predictive_dist.mean() - sigma = predictive_dist.covariance() + mu = predictive_dist.mean + sigma = predictive_dist.covariance_matrix assert isinstance(mu, jnp.ndarray) assert isinstance(sigma, jnp.ndarray) From 171fdd964877fa6182a3049016444a376ecc86c0 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sat, 15 Oct 2022 20:13:59 +0000 Subject: [PATCH 08/14] Update workflow --- .github/workflows/workflow-master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-master.yml b/.github/workflows/workflow-master.yml index eeab66cc..da8acf75 100644 --- a/.github/workflows/workflow-master.yml +++ b/.github/workflows/workflow-master.yml @@ -5,7 +5,7 @@ on: - "master" pull_request: branches: - - "master" + - "*" jobs: codecov: name: Codecov Workflow From e96b918724bc9dce31a3af929a18b5770c8aca7a Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sat, 15 Oct 2022 20:16:10 +0000 Subject: [PATCH 09/14] Drop distrax ref --- gpjax/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gpjax/config.py b/gpjax/config.py index fa7be8fa..88592bdc 100644 --- a/gpjax/config.py +++ b/gpjax/config.py @@ -3,8 +3,6 @@ from ml_collections import ConfigDict __config = None -import distrax as dx -import jax.numpy as jnp def get_defaults() -> ConfigDict: From 1252d46768917ec60bc23e2da3d1f82ebca4d0a7 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sat, 15 Oct 2022 20:20:01 +0000 Subject: [PATCH 10/14] Drop TFP dependency --- tests/test_parameters.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 4679fdf4..94c377bd 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -4,7 +4,6 @@ import numpyro.distributions as npd import jax.random as jr import pytest -from tensorflow_probability.substrates.jax import distributions as tfd from gpjax.gps import Prior from gpjax.kernels import RBF @@ -57,7 +56,7 @@ def test_non_conjugate_initialise(): @pytest.mark.parametrize("x", [-1.0, 0.0, 1.0]) def test_lpd(x): val = jnp.array(x) - dist = tfd.Normal(loc=0.0, scale=1.0) + dist = npd.Normal(loc=0.0, scale=1.0) lpd = log_density(val, dist) assert lpd is not None assert log_density(val, None) == 0.0 @@ -81,7 +80,7 @@ def test_recursive_complete(lik): posterior = Prior(kernel=RBF()) * lik(num_datapoints=10) params, _, _ = initialise(posterior, jr.PRNGKey(123)).unpack() priors = {"kernel": {}} - priors["kernel"]["lengthscale"] = tfd.HalfNormal(scale=2.0) + priors["kernel"]["lengthscale"] = npd.HalfNormal(scale=2.0) container = copy_dict_structure(params) complete_priors = recursive_complete(container, priors) for ( @@ -90,7 +89,7 @@ def test_recursive_complete(lik): v2, ) in recursive_items(params, complete_priors): if k == "lengthscale": - assert isinstance(v2, tfd.HalfNormal) + assert isinstance(v2, npd.HalfNormal) else: assert v2 == None @@ -109,10 +108,10 @@ def test_prior_evaluation(): } priors = { "kernel": { - "lengthscale": tfd.Gamma(1.0, 1.0), - "variance": tfd.Gamma(2.0, 2.0), + "lengthscale": npd.Gamma(1.0, 1.0), + "variance": npd.Gamma(2.0, 2.0), }, - "likelihood": {"obs_noise": tfd.Gamma(3.0, 3.0)}, + "likelihood": {"obs_noise": npd.Gamma(3.0, 3.0)}, } lpd = evaluate_priors(params, priors) assert pytest.approx(lpd) == -2.0110168 @@ -147,8 +146,8 @@ def test_incomplete_priors(): } priors = { "kernel": { - "lengthscale": tfd.Gamma(1.0, 1.0), - "variance": tfd.Gamma(2.0, 2.0), + "lengthscale": npd.Gamma(1.0, 1.0), + "variance": npd.Gamma(2.0, 2.0), }, } container = copy_dict_structure(params) @@ -172,8 +171,8 @@ def test_structure_priors(): params, _, _ = initialise(posterior, jr.PRNGKey(123)).unpack() priors = { "kernel": { - "lengthscale": tfd.Gamma(1.0, 1.0), - "variance": tfd.Gamma(2.0, 2.0), + "lengthscale": npd.Gamma(1.0, 1.0), + "variance": npd.Gamma(2.0, 2.0), }, } structured_priors = structure_priors(params, priors) From df530f6bdc1b56a95fe62bdb648bc4e2e7718b9e Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sat, 15 Oct 2022 20:36:15 +0000 Subject: [PATCH 11/14] Update docs --- docs/conf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d84d66c6..f7e060ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -103,10 +103,12 @@ def find_version(*file_paths): bibtex_bibfiles = ["refs.bib"] bibtex_style = "unsrt" bibtex_reference_style = "author_year" +nb_execution_mode = "auto" nbsphinx_allow_errors = True nbsphinx_custom_formats = { ".pct.py": ["jupytext.reads", {"fmt": "py:percent"}], } +jupyter_execute_notebooks = "cache" # Latex commands # mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" @@ -183,6 +185,11 @@ def find_version(*file_paths): "logo_only": True, "show_toc_level": 2, "repository_url": "https://github.com/thomaspinder/GPJax/", + "launch_buttons": { + "binderhub_url": "https://mybinder.org", + "colab_url": "https://colab.research.google.com", + "notebook_interface": "jupyterlab", + }, "use_repository_button": True, "use_sidenotes": True, # Turns footnotes into sidenotes - https://sphinx-book-theme.readthedocs.io/en/stable/content-blocks.html } @@ -199,10 +206,10 @@ def find_version(*file_paths): # } + # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] - html_static_path = ["_static"] html_css_files = ["custom.css"] From 0d73ef8c247b6b503a8eafae9d20dfc37751cc86 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sat, 15 Oct 2022 21:28:48 +0000 Subject: [PATCH 12/14] Remove Distrax from notebooks --- examples/haiku.pct.py | 1 - examples/intro_to_gps.pct.py | 57 +++++++++++++++++------------------- examples/kernels.pct.py | 49 +++++++++++++++++++------------ 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/examples/haiku.pct.py b/examples/haiku.pct.py index 9e014412..42d8820b 100644 --- a/examples/haiku.pct.py +++ b/examples/haiku.pct.py @@ -22,7 +22,6 @@ # %% import typing as tp -import distrax as dx import haiku as hk import jax import jax.numpy as jnp diff --git a/examples/intro_to_gps.pct.py b/examples/intro_to_gps.pct.py index dc758e99..76f487e7 100644 --- a/examples/intro_to_gps.pct.py +++ b/examples/intro_to_gps.pct.py @@ -1,11 +1,12 @@ # --- # jupyter: # jupytext: +# custom_cell_magics: kql # text_representation: # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.11.5 +# jupytext_version: 1.11.2 # kernelspec: # display_name: Python 3.9.7 ('gpjax') # language: python @@ -105,15 +106,19 @@ # $$ # We can plot three different parameterisations of this density. -import distrax as dx - # %% +import numpyro.distributions as npd import jax.numpy as jnp import matplotlib.pyplot as plt +from utils import confidence_ellipse +import jax.random as jr +import pandas as pd +import seaborn as sns + -ud1 = dx.Normal(0.0, 1.0) -ud2 = dx.Normal(-1.0, 0.5) -ud3 = dx.Normal(0.25, 1.5) +ud1 = npd.Normal(0.0, 1.0) +ud2 = npd.Normal(-1.0, 0.5) +ud3 = npd.Normal(0.25, 1.5) xs = jnp.linspace(-5.0, 5.0, 500) @@ -121,10 +126,10 @@ for d in [ud1, ud2, ud3]: ax.plot( xs, - d.prob(xs), - label=f"$\mathcal{{N}}({{{float(d.mean())}}},\ {{{float(d.stddev())}}}^2)$", + jnp.exp(d.log_prob(xs)), + label=f"$\mathcal{{N}}({{{float(d.mean)}}},\ {{{float(jnp.sqrt(d.variance))}}}^2)$", ) - ax.fill_between(xs, jnp.zeros_like(xs), d.prob(xs), alpha=0.2) + ax.fill_between(xs, jnp.zeros_like(xs), jnp.exp(d.log_prob(xs)), alpha=0.2) ax.legend(loc="best") # %% [markdown] @@ -151,19 +156,15 @@ # $$ # Three example parameterisations of this can be visualised below where $\rho$ determines the correlation of the multivariate Gaussian. -import jax.random as jr - # %% -from utils import confidence_ellipse - key = jr.PRNGKey(123) -d1 = dx.MultivariateNormalDiag(jnp.zeros(2), scale_diag=jnp.array([1.0, 1.0])) -d2 = dx.MultivariateNormalTri( - jnp.zeros(2), jnp.linalg.cholesky(jnp.array([[1.0, 0.9], [0.9, 1.0]])) +d1 = npd.MultivariateNormal(jnp.zeros(2), jnp.eye(2)) +d2 = npd.MultivariateNormal( + jnp.zeros(2), scale_tril=jnp.linalg.cholesky(jnp.array([[1.0, 0.9], [0.9, 1.0]])) ) -d3 = dx.MultivariateNormalTri( - jnp.zeros(2), jnp.linalg.cholesky(jnp.array([[1.0, -0.5], [-0.5, 1.0]])) +d3 = npd.MultivariateNormal( + jnp.zeros(2), scale_tril=jnp.linalg.cholesky(jnp.array([[1.0, -0.5], [-0.5, 1.0]])) ) dists = [d1, d2, d3] @@ -181,15 +182,15 @@ titles = [r"$\rho = 0$", r"$\rho = 0.9$", r"$\rho = -0.5$"] for a, t, d in zip([ax0, ax1, ax2], titles, dists): - d_prob = d.prob(jnp.hstack([xx.reshape(-1, 1), yy.reshape(-1, 1)])).reshape( + d_prob = jnp.exp(d.log_prob(jnp.hstack([xx.reshape(-1, 1), yy.reshape(-1, 1)])).reshape( xx.shape - ) - cntf = a.contourf(xx, yy, d_prob, levels=20, antialiased=True, cmap="Reds") + )) + cntf = a.contourf(xx, yy, jnp.exp(d_prob), levels=20, antialiased=True, cmap="Reds") for c in cntf.collections: c.set_edgecolor("face") a.set_xlim(-2.75, 2.75) a.set_ylim(-2.75, 2.75) - samples = d.sample(seed=key, sample_shape=(5000,)) + samples = d.sample(key, sample_shape=(5000,)) xsample, ysample = samples[:, 0], samples[:, 1] confidence_ellipse( xsample, ysample, a, edgecolor="#3f3f3f", n_std=1.0, linestyle="--", alpha=0.8 @@ -236,19 +237,15 @@ # joint distribution $p(\mathbf{x}, \mathbf{y})$ quantifies the probability of two events, one # from $p(\mathbf{x})$ and another from $p(\mathbf{y})$, occurring at the same time. We visualise this idea below. -import pandas as pd - # %% -import seaborn as sns - n = 1000 -x = dx.Normal(loc=0.0, scale=1.0).sample(seed=key, sample_shape=(n,)) +x = npd.Normal(loc=0.0, scale=1.0).sample(key, sample_shape=(n,)) key, subkey = jr.split(key) -y = dx.Normal(loc=0.25, scale=0.5).sample(seed=subkey, sample_shape=(n,)) +y = npd.Normal(loc=0.25, scale=0.5).sample(subkey, sample_shape=(n,)) key, subkey = jr.split(subkey) -xfull = dx.Normal(loc=0.0, scale=1.0).sample(seed=subkey, sample_shape=(n * 10,)) +xfull = npd.Normal(loc=0.0, scale=1.0).sample(subkey, sample_shape=(n * 10,)) key, subkey = jr.split(subkey) -yfull = dx.Normal(loc=0.25, scale=0.5).sample(seed=subkey, sample_shape=(n * 10,)) +yfull = npd.Normal(loc=0.25, scale=0.5).sample(subkey, sample_shape=(n * 10,)) key, subkey = jr.split(subkey) df = pd.DataFrame({"x": x, "y": y, "idx": jnp.ones(n)}) diff --git a/examples/kernels.pct.py b/examples/kernels.pct.py index 062ca11c..ebb46ec5 100644 --- a/examples/kernels.pct.py +++ b/examples/kernels.pct.py @@ -20,15 +20,13 @@ # In this guide, we introduce the kernels available in GPJax and demonstrate how to create custom ones. # %% -import distrax as dx -import jax import jax.numpy as jnp import jax.random as jr import matplotlib.pyplot as plt -import tensorflow_probability.substrates.jax.bijectors as tfb from jax import jit from jaxtyping import Array, Float from optax import adam +import numpyro.distributions as npd import gpjax as gpx @@ -62,7 +60,7 @@ for k, ax in zip(kernels, axes.ravel()): prior = gpx.Prior(kernel=k) - params, _, _, _ = gpx.initialise(prior, key).unpack() + params, _, _ = gpx.initialise(prior, key).unpack() rv = prior(params)(x) y = rv.sample(key, sample_shape=(10,)) @@ -207,13 +205,31 @@ def _initialise_params(self, key: jr.PRNGKey) -> dict: # # To define a bijector here we'll make use of the `Lambda` operator given in Distrax. This lets us convert any regular Jax function into a bijection. Given that we require $\tau$ to be strictly greater than $4.$, we'll apply a [softplus transformation](https://jax.readthedocs.io/en/latest/_autosummary/jax.nn.softplus.html) where the lower bound is shifted by $4.$. - -import numpyro - # %% from gpjax.config import add_parameter +from jax.nn import softplus + + +class ShiftSoftplus(npd.transforms.Transform): + domain = npd.constraints.real + codomain = npd.constraints.real + + def __init__(self, low: Float[Array, "1"]) -> None: + super().__init__() + self.low = jnp.array(low) + + def __call__(self, x): + x -= self.low + return softplus(x) + self.low + + def _inverse(self, y): + return jnp.log(-jnp.expm1(-y)) + y -add_parameter("tau", numpyro.distributions.transforms.SoftplusTransform()) + def log_abs_det_jacobian(self, x, y, intermediates=None): + return -softplus(-x) + + +add_parameter("tau", ShiftSoftplus(4.)) # %% [markdown] # ### Using our polar kernel @@ -238,30 +254,25 @@ def _initialise_params(self, key: jr.PRNGKey) -> dict: circlular_posterior = gpx.Prior(kernel=PKern) * likelihood # Initialise parameters and corresponding transformations -params, trainable, constrainer, unconstrainer = gpx.initialise( - circlular_posterior, key -).unpack() +parameter_state = gpx.initialise(circlular_posterior, key) # Optimise GP's marginal log-likelihood using Adam -mll = jit(circlular_posterior.marginal_log_likelihood(D, constrainer, negative=True)) +mll = jit(circlular_posterior.marginal_log_likelihood(D, negative=True)) + learned_params, training_history = gpx.fit( mll, - params, - trainable, - adam(learning_rate=0.05), + parameter_state, + adam(learning_rate=0.01), n_iters=1000, ).unpack() -# Untransform learned parameters -final_params = gpx.transform(learned_params, constrainer) - # %% [markdown] # ### Prediction # # We'll now query the GP's predictive posterior at linearly spaced novel inputs and illustrate the results. # %% -posterior_rv = likelihood(circlular_posterior(D, final_params)(angles), final_params) +posterior_rv = likelihood(circlular_posterior(D, learned_params)(angles), learned_params) mu = posterior_rv.mean one_sigma = jnp.sqrt(posterior_rv.variance) From e84489ae1b09fbf119c7682db371a6d46e569842 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sat, 15 Oct 2022 21:40:31 +0000 Subject: [PATCH 13/14] Fix missing dep --- docs/index.md | 1 - examples/utils.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index a73af5aa..4c4fa24f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,7 +42,6 @@ To learn more, checkout the [regression notebook](https://gpjax.readthedocs.io/en/latest/examples/regression.html). ::: -Are you rendering --- diff --git a/examples/utils.py b/examples/utils.py index aa287445..b8bdecd1 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -1,7 +1,5 @@ import matplotlib.transforms as transforms import numpy as np -import tensorflow as tf -import utils as ut from matplotlib.patches import Ellipse From 40ffc83b5976c90265eb4c7511673e648cca69d7 Mon Sep 17 00:00:00 2001 From: Thomas Pinder Date: Sun, 16 Oct 2022 14:27:43 +0000 Subject: [PATCH 14/14] Resolve review comments --- examples/classification.pct.py | 13 ++++++------- examples/collapsed_vi.pct.py | 11 +++++------ gpjax/gps.py | 6 +++--- gpjax/likelihoods.py | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/examples/classification.pct.py b/examples/classification.pct.py index 9e22f3e7..fc401e21 100644 --- a/examples/classification.pct.py +++ b/examples/classification.pct.py @@ -25,7 +25,7 @@ import jax.random as jr import jax.scipy as jsp import matplotlib.pyplot as plt -import numpyro +import numpyro.distributions as npd import optax as ox from jaxtyping import Array, Float import blackjax @@ -71,7 +71,6 @@ # %% parameter_state = gpx.initialise(posterior) params, trainable, bijectors = parameter_state.unpack() -params = gpx.unconstrain(params, bijectors) mll = jax.jit(posterior.marginal_log_likelihood(D, negative=True)) @@ -145,7 +144,7 @@ L_inv = jsp.linalg.solve_triangular(L, I(D.n), lower=True) H_inv = jsp.linalg.solve_triangular(L.T, L_inv, lower=False) -laplace_approximation = numpyro.distributions.MultivariateNormal(f_map_estimate, H_inv) +laplace_approximation = npd.MultivariateNormal(f_map_estimate, H_inv) from gpjax.kernels import cross_covariance, gram @@ -156,18 +155,18 @@ def predict( - laplace_at_data: numpyro.distributions.Distribution, + laplace_at_data: npd.Distribution, train_data: Dataset, test_inputs: Float[Array, "N D"], jitter: int = 1e-6, -) -> numpyro.distributions.Distribution: +) -> npd.Distribution: """Compute the predictive distribution of the Laplace approximation at novel inputs. Args: laplace_at_data (dict): The Laplace approximation at the datapoints. Returns: - numpyro.distributions.Distribution: The Laplace approximation at novel inputs. + npd.Distribution: The Laplace approximation at novel inputs. """ x, n = train_data.X, train_data.n @@ -209,7 +208,7 @@ def predict( ) covariance += I(n_test) * jitter - return numpyro.distributions.MultivariateNormal( + return npd.MultivariateNormal( jnp.atleast_1d(mean.squeeze()), covariance ) diff --git a/examples/collapsed_vi.pct.py b/examples/collapsed_vi.pct.py index fe48d87b..e8b3ce15 100644 --- a/examples/collapsed_vi.pct.py +++ b/examples/collapsed_vi.pct.py @@ -87,14 +87,11 @@ # We now train our model akin to a Gaussian process regression model via the `fit` abstraction. Unlike the regression example given in the [conjugate regression notebook](https://gpjax.readthedocs.io/en/latest/nbs/regression.html), the inducing locations that induce our variational posterior distribution are now part of the model's parameters. Using a gradient-based optimiser, we can then _optimise_ their location such that the evidence lower bound is maximised. # %% parameter_state = gpx.initialise(sgpr, key) -params, trainables, bijectors = parameter_state.unpack() loss_fn = jit(sgpr.elbo(D, negative=True)) optimiser = ox.adam(learning_rate=0.005) -params = gpx.unconstrain(params, bijectors) - learned_params, training_history = gpx.fit( objective=loss_fn, parameter_state=parameter_state, @@ -102,6 +99,9 @@ n_iters=2000, ).unpack() +# %% +parameter_state.bijectors + # %% [markdown] # We show predictions of our model with the learned inducing points overlayed in grey. # %% @@ -168,13 +168,12 @@ fr_params, fr_trainables, fr_bijectors = gpx.initialise( full_rank_model, key ).unpack() -fr_params = gpx.unconstrain(fr_params, fr_bijectors) mll = jit(full_rank_model.marginal_log_likelihood(D, negative=True)) -# %timeit mll(fr_params).block_until_ready() +%timeit mll(fr_params).block_until_ready() # %% sparse_elbo = jit(sgpr.elbo(D, negative=True)) -# %timeit sparse_elbo(params).block_until_ready() +%timeit sparse_elbo(params).block_until_ready() # %% [markdown] # As we can see, the sparse approximation given here is around 50 times faster when compared against a full-rank model. diff --git a/gpjax/gps.py b/gpjax/gps.py index 4d23a1c7..1e3642af 100644 --- a/gpjax/gps.py +++ b/gpjax/gps.py @@ -110,7 +110,7 @@ def __rmul__(self, other: AbstractLikelihood): """ return self.__mul__(other) - def predict(self, params: dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: + def predict(self, params: Dict) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Compute the GP's prior mean and variance. Args: @@ -195,7 +195,7 @@ class ConjugatePosterior(AbstractPosterior): jitter: Optional[float] = DEFAULT_JITTER def predict( - self, train_data: Dataset, params: dict + self, train_data: Dataset, params: Dict ) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Conditional on a set of training data, compute the GP's posterior predictive distribution for a given set of parameters. The returned function can be evaluated at a set of test inputs to compute the corresponding predictive density. @@ -310,7 +310,7 @@ def _initialise_params(self, key: PRNGKeyType) -> Dict: return parameters def predict( - self, train_data: Dataset, params: dict + self, train_data: Dataset, params: Dict ) -> Callable[[Float[Array, "N D"]], npd.Distribution]: """Conditional on a set of training data, compute the GP's posterior predictive distribution for a given set of parameters. The returned function can be evaluated at a set of test inputs to compute the corresponding predictive density. Note, to gain predictions on the scale of the original data, the returned distribution will need to be transformed through the likelihood function's inverse link function. diff --git a/gpjax/likelihoods.py b/gpjax/likelihoods.py index 0585ba96..e9bf3173 100644 --- a/gpjax/likelihoods.py +++ b/gpjax/likelihoods.py @@ -116,12 +116,12 @@ def link_function(self) -> Callable: Callable: A link function that maps the predictive distribution to the likelihood function. """ - def link_fn(x, params: dict) -> npd.Distribution: + def link_fn(x, params: Dict) -> npd.Distribution: return npd.Normal(loc=x, scale=params["obs_noise"]) return link_fn - def predict(self, dist: npd.Distribution, params: dict) -> npd.Distribution: + def predict(self, dist: npd.Distribution, params: Dict) -> npd.Distribution: """Evaluate the Gaussian likelihood function at a given predictive distribution. Computationally, this is equivalent to summing the observation noise term to the diagonal elements of the predictive distribution's covariance matrix. Args: