Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Standard SVGP

Standard SVGP with pyrox

The first notebook in the pyrox SVGP series: a textbook stochastic variational GP with MM inducing points living in the input space. Everything here is the reference that the next three notebooks will bend — notebook 2 trains the same model with mini-batches instead of full data, notebook 3 replaces the inducing points with a spherical-harmonic basis, notebook 4 warps the input space through a small neural network before the kernel sees it.

The goal for this notebook is narrow: set up the pyrox + optax scaffold that the later variants will reuse, and produce the canonical sanity-check plots — a monotone decreasing loss curve, inducing points migrating toward informative regions of the input, and a posterior mean + 2σ2\sigma band that collapses onto the training data and widens over the input gap.

Background

Why sparse?

The exact GP posterior at NN training points and N∗N_* test points costs O(N3)\mathcal{O}(N^3) once and O(N2N∗)\mathcal{O}(N^2 N_*) per prediction — the bottleneck being the solve against KXX+σ2IK_{XX} + \sigma^2 I. With NN in the thousands this is already uncomfortable; with NN in the hundreds of thousands it is impossible on a single device. Sparse variational GPs (Titsias 2009, Hensman et al. 2013, 2015) replace the data-sized Gram matrix by a small M×MM \times M system over MM inducing values u=f(Z)u = f(Z) at user-chosen inducing inputs Z∈RM×DZ \in \mathbb{R}^{M \times D}, with M≪NM \ll N.

The ELBO

A variational posterior q(u)=N(m,S)q(u) = \mathcal{N}(m, S) induces a marginal Gaussian over the latent function at any input through the prior conditional p(f∣u)p(f \mid u). The resulting variational lower bound on the log marginal likelihood is

L=∑n=1NEq(fn) ⁣[log⁡p(yn∣fn)]  −  KL ⁣(q(u) ∥ p(u)),\mathcal{L} = \sum_{n=1}^{N} \mathbb{E}_{q(f_n)}\!\bigl[\log p(y_n \mid f_n)\bigr] \;-\; \mathrm{KL}\!\bigl(q(u)\,\|\,p(u)\bigr),

with p(u)=N(0,KZZ+jitter I)p(u) = \mathcal{N}(0, K_{ZZ} + \text{jitter}\,I). For Gaussian observation noise the expected log-likelihood has a closed form, so the ELBO is analytic and cheap to differentiate through — exactly the path pyrox.gp.svgp_elbo exposes.

The whitened parameterisation

Raw q(u)q(u) couples the variational mean/covariance to the kernel through KZZK_{ZZ} — the loss landscape tilts as hyperparameters move. Hensman-et-al-2015 reparameterise in whitened coordinates vv with u=LZZ vu = L_{ZZ}\, v, LZZ=chol(KZZ+jitter I)L_{ZZ} = \mathrm{chol}(K_{ZZ} + \text{jitter}\,I), so the prior over vv is N(0,I)\mathcal{N}(0, I) and the KL term decouples from the kernel. This is pyrox.gp.WhitenedGuide and it is the only guide that is both cheap and numerically robust for gradient-based training — we use it throughout the trilogy.

Setup

/anaconda/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Toy data

A noisy 1-D sine with an extinction gap in the middle — N=500N = 500 training points spread either side of the gap. The gap is the cheap sanity check: any well-fit GP should show a posterior band that narrows on the data and widens visibly where it has none.

<Figure size 1000x350 with 1 Axes>

Kernel

The SVGP ELBO is a pure JAX function, so to train hyperparameters via optax we need the kernel to expose its scalars as equinox.Module array leaves — not hidden in the pyrox.Parameterized registry. Pattern A from exact_gp_regression.ipynb is the right tool: subclass the Kernel protocol with a plain equinox module and store unconstrained scalars on the log scale (so exp handles the positivity constraint for free).

The likelihood is GaussianLikelihood from pyrox — one field noise_var. Training noise_var directly would require a positivity projection; shipping a one-field wrapper that stores log_noise_var avoids the bookkeeping.

Model

M=20M = 20 inducing points — placed by a Latin-style random sweep across the input range so they cover both data lobes and the gap. The gap locations will move toward the lobes during training; that migration is the headline visual of the notebook.

Training loop

svgp_elbo is a scalar JAX function of (prior, guide, lik, X, y) — put all three modules into a tuple, take eqx.filter_value_and_grad of the negative ELBO, and let optax.adam drive the leaves. Only array leaves get gradients; the rest of each module (the solver field, callbacks, etc.) is left alone.

final  -ELBO          = -193.226
fitted variance       = 0.559
fitted lengthscale    = 0.515
fitted noise std      = 0.154  (truth 0.15)

Loss curve

The first ~50 steps swallow almost all of the loss drop — the remaining 700 steps fine-tune kernel hyperparameters and inducing positions through a shallow tail. A symlog y-axis keeps both phases readable on one plot.

<Figure size 1000x350 with 1 Axes>

Inducing-point migration

Snapshot of the inducing inputs at step 0 (random initialisation) and at the end of training. A well-fit SVGP drags inducing points toward regions where they reduce the ELBO most — typically toward data-rich areas where the posterior wants sharp resolution. Expect the gap to be sparsely populated and the two data lobes to be denser.

A visible artefact. The ELBO penalises sloppy coverage but does not penalise two inducing points landing on top of each other — the KL term handles that degeneracy silently via a correlated q(u). You’ll often see small pile-ups (e.g. two points coincident around x ≈ 1.4 in the fitted row below). Fixing it properly takes either a repulsive regulariser on Z or a different inducing parameterisation — exactly the door that notebook 3 opens with spherical-harmonic features, where “inducing points” become a fixed orthogonal basis and the pile-up failure mode can’t exist.

<Figure size 1000x260 with 1 Axes>

Posterior predictive

pyrox.gp.WhitenedGuide.predict(K_xz, K_zz_op, K_xx_diag) returns posterior mean and variance at any test set given the three predictive blocks from the sparse prior. Add the prior’s mean function (zero here) back on, then overlay the ±2σ\pm 2\sigma band on top of the training scatter.

<Figure size 1000x400 with 1 Axes>

A note on the gap. The posterior mean is smoother across the input gap than the truth (the true function has a local min near x ≈ -0.3 that the blue mean rounds off). This is not a bug — with only M=20M = 20 inducing points and a lengthscale fit at ℓ≈0.5\ell \approx 0.5 against a gap about 1.2 units wide, the model has no data and limited inducing density there, so the posterior relaxes toward the prior (zero-mean, wide band). Widening the band is the epistemically correct response; getting the mean exactly right over the gap would require either more inducing capacity or a stronger structural prior (e.g. periodic kernel).

Summary

  • SparseGPPrior(kernel, Z) + WhitenedGuide + GaussianLikelihood is the canonical SVGP triple.
  • svgp_elbo is a pure JAX scalar so optax + eqx.filter_value_and_grad trains everything jointly (kernel hyperparameters, inducing inputs, guide parameters, noise).
  • The fitted lengthscale and noise should be close to the true generating values; inducing points migrate toward data lobes.

Next: notebook 2 keeps this exact model but swaps the full-data ELBO for an unbiased mini-batch estimator — the main reason you’d reach for SVGP instead of an exact GP in the first place.