AI Knowledge Tree

Gaussian process regression

Treat the function as a draw from a GP prior, condition on data, and get closed-form posterior mean and variance.

A Gaussian process is a probability distribution over functions[1]. Place a GP prior on the unknown regression function, condition on observed data, and the posterior is again a Gaussian (over function values), with a closed-form mean and variance at every test point. GPs marry kernels with full Bayesian machinery, and they are the engine of Bayesian optimization.

A distribution over functions

Define a GP f∼GP(m,k)f \sim \mathcal{GP}(m, k) by:

  • a mean function m:X→Rm: \mathcal{X} \to \mathbb{R} (often m≡0m \equiv 0),
  • a covariance (kernel) function k:X×X→Rk: \mathcal{X} \times \mathcal{X} \to \mathbb{R} that is symmetric and positive semidefinite.

Drawing from the GP at any finite set of inputs X⋆=(x⋆1,…,x⋆m)X_\star = (x_{\star 1}, \dots, x_{\star m}) gives a finite-dimensional Gaussian vector:

f⋆=(f(x⋆1),…,f(x⋆m))∼N(m(X⋆), K(X⋆,X⋆)),\mathbf{f}_\star = (f(x_{\star 1}), \dots, f(x_{\star m})) \sim \mathcal{N}(m(X_\star),\ K(X_\star, X_\star)),

with K(X⋆,X⋆)ij=k(x⋆i,x⋆j)K(X_\star, X_\star)_{ij} = k(x_{\star i}, x_{\star j}). A function draw is a random vector at any chosen evaluation grid.

Conditioning on data

Suppose observations are yi=f(xi)+εiy_i = f(x_i) + \varepsilon_i with εi∼N(0,σn2)\varepsilon_i \sim \mathcal{N}(0, \sigma_n^2). Stack training inputs into XX and outputs into y\mathbf{y}. By the Gaussian conditional formula, the joint distribution of (y,f⋆)(\mathbf{y}, \mathbf{f}_\star) is Gaussian, and conditioning on y\mathbf{y} gives the posterior:

f⋆∣y∼N(μ⋆, Σ⋆),\mathbf{f}_\star \mid \mathbf{y} \sim \mathcal{N}(\boldsymbol\mu_\star,\ \Sigma_\star),

with

μ⋆=K⋆X (KXX+σn2I)−1y,Σ⋆=K⋆⋆−K⋆X (KXX+σn2I)−1KX⋆.\boldsymbol\mu_\star = K_{\star X}\,(K_{XX} + \sigma_n^2 I)^{-1}\mathbf{y}, \qquad \Sigma_\star = K_{\star\star} - K_{\star X}\,(K_{XX} + \sigma_n^2 I)^{-1} K_{X\star}.

The posterior mean is a linear combination of training outputs, with weights depending on the kernel. The posterior variance shrinks where training data is dense and grows where it is sparse, with no extra effort: calibrated uncertainty falls out for free.

Hyperparameters via marginal likelihood

The kernel kk has its own hyperparameters (e.g. RBF length scale ℓ\ell and amplitude σf\sigma_f). Choose them by maximizing the log marginal likelihood:

log⁡p(y)=−12y⊤(KXX+σn2I)−1y−12log⁡det⁡(KXX+σn2I)−n2log⁡(2π).\log p(\mathbf{y}) = -\tfrac{1}{2}\mathbf{y}^\top(K_{XX} + \sigma_n^2 I)^{-1}\mathbf{y} - \tfrac{1}{2}\log\det(K_{XX} + \sigma_n^2 I) - \tfrac{n}{2}\log(2\pi).

The first term rewards data fit; the second penalizes model complexity (the log-determinant grows when the kernel implies high flexibility). This is the Bayesian Occam's razor at work, with no separate validation set needed for hyperparameter selection[2].

Costs and scalability

The GP posterior costs O(n3)O(n^3) to compute (the inversion of KXX+σn2IK_{XX} + \sigma_n^2 I) and O(n2)O(n^2) memory. This is manageable up to a few thousand points and gets prohibitive past tens of thousands. Standard fixes:

  • Sparse approximations (inducing points, FITC, VFE).
  • Low-rank kernel approximations (Nyström, random Fourier features).
  • Structured kernels (e.g. product kernels on grids, exploited by Kronecker tricks).

GPs are not the right tool for very large datasets; they are the right tool when each data point is expensive and uncertainty matters.

Use cases

  • Bayesian optimization: the GP is the surrogate model whose posterior variance drives exploration (see the A3 lesson).
  • Spatial statistics (kriging): the original application, predicting mineral concentrations from a few drill holes.
  • Hyperparameter response surfaces: model training accuracy as a function of hyperparameters and use the GP posterior to decide what to try next.
  • Calibrated regression: any setting where decision-quality predictive variance matters more than raw accuracy.
A Gaussian-process posterior: the mean passes near the (noisy) observations and the credible band widens wherever there is no data.
A Gaussian-process posterior: the mean passes near the (noisy) observations and the credible band widens wherever there is no data.

Check it on arrays

python · numpy
import numpy as np

rng = np.random.default_rng(0)

# Toy 1D regression
def f_true(x): return np.sin(2 * x) + 0.3 * np.cos(7 * x)
n = 12
x_train = np.sort(rng.uniform(-3, 3, n))[:, None]
y_train = f_true(x_train).ravel() + 0.1 * rng.normal(size=n)

x_test = np.linspace(-4, 4, 200)[:, None]

def rbf(X1, X2, ell=1.0, sigma_f=1.0):
    d2 = np.sum(X1**2, 1)[:, None] + np.sum(X2**2, 1) - 2 * X1 @ X2.T
    return sigma_f**2 * np.exp(-0.5 * d2 / ell**2)

def gp_posterior(x_train, y_train, x_test, ell, sigma_f, sigma_n):
    Kxx = rbf(x_train, x_train, ell, sigma_f) + sigma_n**2 * np.eye(len(x_train))
    Kxs = rbf(x_train, x_test, ell, sigma_f)
    Kss = rbf(x_test, x_test, ell, sigma_f) + 1e-8 * np.eye(len(x_test))
    L = np.linalg.cholesky(Kxx)
    alpha = np.linalg.solve(L.T, np.linalg.solve(L, y_train))
    v = np.linalg.solve(L, Kxs)
    mu = Kxs.T @ alpha
    cov = Kss - v.T @ v
    return mu, np.sqrt(np.maximum(np.diag(cov), 0))

ell, sigma_f, sigma_n = 0.5, 1.0, 0.1
mu, sd = gp_posterior(x_train, y_train, x_test, ell, sigma_f, sigma_n)

# Reality check: posterior mean is close to the truth and uncertainty grows away from data
err_at_data = np.linalg.norm(gp_posterior(x_train, y_train, x_train, ell, sigma_f, sigma_n)[0] - y_train) / len(y_train)
print(f"in-sample RMSE: {err_at_data:.3f}")
print(f"posterior sd at first  test point: {sd[0]:.3f}  (far from data)")
print(f"posterior sd at center test point: {sd[len(sd) // 2]:.3f}  (near data)")
print(f"95% CI covers truth? "
      f"{np.mean(np.abs(mu - f_true(x_test).ravel()) <= 1.96 * sd):.2%}")

# Log marginal likelihood as a function of length scale
ells = np.linspace(0.1, 2.0, 25)
lmls = []
for L in ells:
    Kxx = rbf(x_train, x_train, L, sigma_f) + sigma_n**2 * np.eye(len(x_train))
    sign, logdet = np.linalg.slogdet(Kxx)
    alpha = np.linalg.solve(Kxx, y_train)
    lml = -0.5 * y_train @ alpha - 0.5 * logdet - 0.5 * len(x_train) * np.log(2 * np.pi)
    lmls.append(lml)
best_ell = ells[int(np.argmax(lmls))]
print(f"best ell by marginal likelihood: {best_ell:.2f}")

The GP posterior fits the training data, gives tight uncertainty near the samples and wide uncertainty far from them, and the marginal likelihood picks a reasonable length scale.

Exercises

Exercise 1. A noiseless GP (σn2=0\sigma_n^2 = 0) has a single training point x1=0x_1 = 0 with y1=1y_1 = 1, and an RBF kernel with σf=1\sigma_f = 1, ℓ=1\ell = 1, so k(x,x′)=exp⁡(−12(x−x′)2)k(x, x') = \exp(-\tfrac12 (x - x')^2). Compute the posterior mean and variance at a test point x⋆x_\star, and evaluate them at x⋆=0x_\star = 0.

Solution

Step 1. With one training point, KXX=k(0,0)=1K_{XX} = k(0, 0) = 1 and K⋆X=k(x⋆,0)=exp⁡(−12x⋆2)K_{\star X} = k(x_\star, 0) = \exp(-\tfrac12 x_\star^2). The posterior mean is

μ⋆=K⋆X KXX−1 y1=exp⁡ ⁣(−12x⋆2)⋅1⋅1=exp⁡ ⁣(−12x⋆2).\mu_\star = K_{\star X}\, K_{XX}^{-1}\, y_1 = \exp\!\left(-\tfrac12 x_\star^2\right) \cdot 1 \cdot 1 = \exp\!\left(-\tfrac12 x_\star^2\right).

Step 2. The posterior variance is

Σ⋆=K⋆⋆−K⋆XKXX−1KX⋆=1−exp⁡ ⁣(−12x⋆2)2=1−exp⁡(−x⋆2).\Sigma_\star = K_{\star\star} - K_{\star X} K_{XX}^{-1} K_{X\star} = 1 - \exp\!\left(-\tfrac12 x_\star^2\right)^2 = 1 - \exp(-x_\star^2).

Step 3. At x⋆=0x_\star = 0: μ⋆=e0=1\mu_\star = e^0 = 1, recovering the observation exactly (a noiseless GP interpolates its data), and Σ⋆=1−1=0\Sigma_\star = 1 - 1 = 0, zero uncertainty at the observed location. Far from data (∣x⋆∣→∞|x_\star| \to \infty) the mean decays to the zero prior and the variance grows to the prior variance 11.

Exercise 2. In the log marginal likelihood −12y⊤A−1y−12log⁡det⁡A−n2log⁡(2π)-\tfrac12 \mathbf{y}^\top A^{-1}\mathbf{y} - \tfrac12 \log\det A - \tfrac{n}{2}\log(2\pi) with A=KXX+σn2IA = K_{XX} + \sigma_n^2 I, explain why the −12log⁡det⁡A-\tfrac12\log\det A term acts as a complexity penalty. (Consider how det⁡A\det A changes when the kernel is made more flexible.)

Solution

Step 1. AA is symmetric positive definite, so log⁡det⁡A=∑ilog⁡λi\log\det A = \sum_i \log \lambda_i where λi>0\lambda_i > 0 are its eigenvalues. The eigenvalues measure the variance the prior assigns along each principal direction of the data covariance.

Step 2. A more flexible kernel (larger amplitude σf\sigma_f, or a shorter length scale that lets nearby points vary more independently) inflates the prior variance, raising the eigenvalues λi\lambda_i, hence raising log⁡det⁡A\log\det A. The term −12log⁡det⁡A-\tfrac12\log\det A then becomes more negative, lowering the marginal likelihood.

Step 3. So a flexible model that could fit many datasets is charged for that flexibility, while the data-fit term −12y⊤A−1y-\tfrac12 \mathbf{y}^\top A^{-1}\mathbf{y} rewards explaining the actual y\mathbf{y}. Maximizing the sum balances the two, which is the Bayesian Occam's razor: it prefers the least flexible kernel that still fits, with no held-out validation set.

Exercise 3 (code). On a 1D toy regression, verify that a near-noiseless GP interpolates its training points (posterior mean matches yy, posterior standard deviation near zero at training inputs), and that the posterior standard deviation is larger at a point far from all data than at a point near the data.

Solution
python · numpy
import numpy as np

rng = np.random.default_rng(0)

def f_true(x):
    return np.sin(2 * x) + 0.3 * np.cos(7 * x)

n = 12
x_train = np.sort(rng.uniform(-3, 3, n))[:, None]
y_train = f_true(x_train).ravel() + 0.1 * rng.normal(size=n)

def rbf(X1, X2, ell=1.0, sigma_f=1.0):
    d2 = np.sum(X1**2, 1)[:, None] + np.sum(X2**2, 1) - 2 * X1 @ X2.T
    return sigma_f**2 * np.exp(-0.5 * d2 / ell**2)

def gp_posterior(x_train, y_train, x_test, ell, sigma_f, sigma_n):
    Kxx = rbf(x_train, x_train, ell, sigma_f) + sigma_n**2 * np.eye(len(x_train))
    Kxs = rbf(x_train, x_test, ell, sigma_f)
    Kss = rbf(x_test, x_test, ell, sigma_f) + 1e-8 * np.eye(len(x_test))
    L = np.linalg.cholesky(Kxx)
    alpha = np.linalg.solve(L.T, np.linalg.solve(L, y_train))
    v = np.linalg.solve(L, Kxs)
    mu = Kxs.T @ alpha
    cov = Kss - v.T @ v
    return mu, np.sqrt(np.maximum(np.diag(cov), 0))

# Near-noiseless GP interpolates the training points
mu_tr, sd_tr = gp_posterior(x_train, y_train, x_train, 0.5, 1.0, 1e-6)
assert np.allclose(mu_tr, y_train, atol=1e-3)
assert sd_tr.max() < 1e-2

# Uncertainty grows away from data
_, sd_far = gp_posterior(x_train, y_train, np.array([[10.0]]), 0.5, 1.0, 0.1)
_, sd_near = gp_posterior(x_train, y_train, x_train[n // 2:n // 2 + 1], 0.5, 1.0, 0.1)
print(f"sd far from data: {sd_far[0]:.3f}, sd near data: {sd_near[0]:.3f}")
assert sd_far[0] > sd_near[0]

The near-noiseless posterior passes through every training point with vanishing variance there, and the standard deviation at x⋆=10x_\star = 10 (about 1.01.0, the prior) dwarfs the standard deviation at an in-sample location (about 0.090.09). Calibrated uncertainty falls out of the same closed form, no extra fitting.

This closes the C2 margin-and-kernels track; the next subsection switches to a very different inductive bias: trees that split the input space along axis-aligned cuts.


References

  1. [1]Rasmussen, C. E., & Williams, C. K. I. (2006). Gaussian Processes for Machine Learning. MIT Press. The canonical reference for GP regression, the posterior formulas, and scalable approximations.↩
  2. [2]Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer. §6.4 derives the GP marginal likelihood and the Bayesian Occam's razor.↩