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.

Part II: Gaussian Processes - Implementation and Practice

The Complete Emulation Workflow | Module 7: The Learnable Universe | ASTR 596

San Diego State University

“In theory, theory and practice are the same. In practice, they are not.”

Attributed to Yogi Berra (and many others)


Overview

Prerequisites: Complete Part I: GP Theory before proceeding. You should understand:

In Part II, you’ll learn:


Quick Recap: What You Learned in Part I

The Core Ideas

Gaussian Processes are:

The Prediction Formulas:

Given training data D={(xi,yi)}i=1N\mathcal{D} = \{(\mathbf{x}_i, y_i)\}_{i=1}^N and GP prior fGP(m,k)f \sim \mathcal{GP}(m, k):

μ(x)=m(x)+kT(K+σn2I)1(ym)σ2(x)=kkT(K+σn2I)1k\boxed{ \begin{align} \mu(\mathbf{x}_*) &= m(\mathbf{x}_*) + \mathbf{k}_*^T (\mathbf{K} + \sigma_n^2 \mathbf{I})^{-1} (\mathbf{y} - \mathbf{m}) \\[0.5em] \sigma^2(\mathbf{x}_*) &= k_{**} - \mathbf{k}_*^T (\mathbf{K} + \sigma_n^2 \mathbf{I})^{-1} \mathbf{k}_* \end{align} }

where:

The Key Kernels:

  1. Squared Exponential (SE): k(x,x)=σf2exp(xx222)k(\mathbf{x}, \mathbf{x}') = \sigma_f^2 \exp\left(-\frac{\|\mathbf{x} - \mathbf{x}'\|^2}{2\ell^2}\right) (infinitely smooth)

  2. Matérn-5/2: Twice differentiable, more realistic smoothness

  3. ARD (Automatic Relevance Determination): Per-dimension lengthscales d\ell_d

When to Use GPs:

Now let’s build one!


The Emulation Workflow: From Simulations to Predictions

Now that you understand GPs conceptually, let’s walk through the complete workflow you’ll implement in Part 2.

Step 1: Generate Training Data

Goal: Sample initial condition space intelligently to get informative training set.

Naive approach: Random sampling

Better approach: Latin Hypercube Sampling (LHS)

Best approach: Active learning (advanced)

For your project: Provided dataset uses LHS. You’ll add 50 more simulations where you choose (tests if you can identify gaps).

Step 2: Train the GP

Goal: Find hyperparameters θ=(σf2,,σn2)\boldsymbol{\theta} = (\sigma_f^2, \ell, \sigma_n^2) that best explain data.

Approach: Maximize marginal likelihood (evidence):

logp(yX,θ)=12yT(K+σn2I)1y12logK+σn2IN2log(2π)\log p(\mathbf{y} | X, \boldsymbol{\theta}) = -\frac{1}{2} \mathbf{y}^T (\mathbf{K} + \sigma_n^2 \mathbf{I})^{-1} \mathbf{y} - \frac{1}{2} \log |\mathbf{K} + \sigma_n^2 \mathbf{I}| - \frac{N}{2} \log(2\pi)

Three terms:

  1. Data fit: 12yT(K+σn2I)1y-\frac{1}{2} \mathbf{y}^T (\mathbf{K} + \sigma_n^2 \mathbf{I})^{-1} \mathbf{y} (how well does GP explain data?)

  2. Complexity penalty: 12logK+σn2I-\frac{1}{2} \log |\mathbf{K} + \sigma_n^2 \mathbf{I}| (Occam’s razor—prefer simpler explanations)

  3. Normalization: N2log(2π)-\frac{N}{2} \log(2\pi) (constant, doesn’t affect optimization)

Interpretation (Occam’s Razor):

Optimization:

Initialization heuristics (guess starting point):

Step 3: Make Predictions

Goal: Given new initial conditions x\mathbf{x}_*, predict bound fraction μ(x)\mu(\mathbf{x}_*) with uncertainty σ(x)\sigma(\mathbf{x}_*).

Algorithm (from conditioning formulas):

  1. Compute kernel vectors:

    • k=k(Xtrain,x)\mathbf{k}_* = k(X_{\rm train}, \mathbf{x}_*) (training-test covariances)

    • k=k(x,x)k_{**} = k(\mathbf{x}_*, \mathbf{x}_*) (test-test covariance, usually 1)

  2. Compute predictive mean:

    μ(x)=m(x)+kT(K+σn2I)1(ym)\mu(\mathbf{x}_*) = m(\mathbf{x}_*) + \mathbf{k}_*^T (\mathbf{K} + \sigma_n^2 \mathbf{I})^{-1} (\mathbf{y} - \mathbf{m})

    Note: We typically standardize training targets to zero mean (m=0\mathbf{m} = 0) during training, then de-standardize predictions for reporting. This simplifies computation while preserving generality.

  3. Compute predictive variance:

    σ2(x)=kkT(K+σn2I)1k\sigma^2(\mathbf{x}_*) = k_{**} - \mathbf{k}_*^T (\mathbf{K} + \sigma_n^2 \mathbf{I})^{-1} \mathbf{k}_*

Efficient implementation (precompute during training):

Step 1: Cholesky factorization

Ky=K+σn2I=LLT\mathbf{K}_y = \mathbf{K} + \sigma_n^2 \mathbf{I} = \mathbf{L} \mathbf{L}^T

where L\mathbf{L} is lower-triangular (Cholesky factor).

Step 2: Precompute regression weights Solve Lβ=y\mathbf{L} \boldsymbol{\beta} = \mathbf{y}, then solve LTα=β\mathbf{L}^T \boldsymbol{\alpha} = \boldsymbol{\beta} to get:

α=Ky1y\boldsymbol{\alpha} = \mathbf{K}_y^{-1} \mathbf{y}

Step 3: Fast prediction (whitened form).

For any test point x\mathbf{x}_*:

v=L1k(solve triangular system)μ(x)=kTα(dot product)σ2(x)=kv2(variance reduction)\boxed{ \begin{align} \mathbf{v} &= \mathbf{L}^{-1} \mathbf{k}_* \quad \text{(solve triangular system)} \\[0.5em] \mu(\mathbf{x}_*) &= \mathbf{k}_*^T \boldsymbol{\alpha} \quad \text{(dot product)} \\[0.5em] \sigma^2(\mathbf{x}_*) &= k_{**} - \|\mathbf{v}\|^2 \quad \text{(variance reduction)} \end{align} }

Why this works:

Computational complexity summary:

OperationComplexityWhen NeededYour Project (N=250,M=106N=250, M=10^6)
Training (one-time)O(N3)O(N^3)Cholesky + hyperparameter optimizationFast for N250N \sim 250 on modern CPUs/GPUs (seconds to minutes for optimization)
Marginal predictionsO(MN)O(MN)Mean/variance at MM independent points (typical)~seconds for M=106M=10^6
Joint covarianceO(NM2)O(NM^2)Full predictive covariance for MM test points via MM triangular solves (rarely needed)Use batches: Mbatch104M_{\rm batch} \sim 10^4
Joint samplingO(M2N+M3)O(M^2 N + M^3)Drawing correlated samples for visualizationOnly for small M<1000M < 1000

Key insight: Marginal predictions scale linearly with MM → You can predict at millions of test points efficiently!

For your project: Training is instant (N=250N=250), predictions are fast. Focus on getting the kernel right, not optimizing speed.

Numerical stability:


  1. Keep shapes static under jit

    • Pre-allocate arrays inside compiled functions

    • Avoid dynamic shapes (e.g., no n = X.shape[0] that varies between calls)

  2. Standardize inputs and outputs

    • Z-score normalize each input dimension: (X - mean) / std

    • For outputs spanning orders of magnitude, normalize or log-transform

    • Store normalization constants, denormalize predictions when reporting

  3. Use log-space for positive hyperparameters (with bounds)

    • Ensures positivity: =exp(~)\ell = \exp(\tilde{\ell}), optimize over ~R\tilde{\ell} \in \mathbb{R}

    • Gradients often better behaved in log-space

    • Apply to lengthscales, variances, noise

    • Tip for robustness: Bound the log-space parameters to avoid extreme values:

      • Lengthscale: log[2,1]\log \ell \in [-2, 1] (roughly [0.13,2.7]×\ell \in [0.13, 2.7] \times input range) — prevents learning scales far outside data

      • Signal variance: logσf2[3,2]\log \sigma_f^2 \in [-3, 2] — prevents excessively small or large signal

      • Noise variance: logσn2[8,1]\log \sigma_n^2 \in [-8, -1] — prevents noise from dominating or disappearing

    • Implement bounded optimization via optax.clip_by_global_norm or reparameterize to constrained domain (e.g., sigmoid)

  4. Multiple optimizer restarts

    • Marginal likelihood is non-convex (many local optima)

    • Try 5-10 random initializations, keep best

    • Use heuristic initialization (see below) as starting point

  5. Precision considerations

    • Enable 64-bit in JAX: Set environment variable JAX_ENABLE_X64=True or use jax.config.update("jax_enable_x64", True) at start

    • Default: Use float64 for training and hyperparameter optimization

    • Speed optimization (optional): After validating Ky\mathbf{K}_y is well-conditioned, test float32 for predictions

    • Matérn-5/2 + float32: Often stable for N<1000N < 1000 with proper jitter (Matérn kernels numerically better-behaved than SE)

    • SE kernel + float32: More prone to numerical issues near singularities (requires higher jitter or float64)

    • Monitor condition number: cond(K_y) = max(eig)/min(eig) should be <1010< 10^{10} for float64, <106< 10^{6} for float32

    • Your project: Stick with float64 for N250N \sim 250 (no performance penalty, guaranteed stability)

  6. Batch predictions for memory safety

    • For MNM \gg N, predict in batches: M_batch ~ 10^4

    • Use vmap (vectorized map) for vectorization: jax.vmap(predict_fn)(X_test_batch)

  7. Deduplicate training inputs

    • Problem: Near-identical training points cause K\mathbf{K} to be nearly singular

    • Check: Compute pairwise distances: xixj\|\mathbf{x}_i - \mathbf{x}_j\| for all iji \neq j

    • Threshold: Flag duplicates if distance <108< 10^{-8} (numerical precision limit)

    • Causes: Accidentally running same simulation twice, numerical roundoff in parameter sampling

    • Symptom: Cholesky fails with “matrix not positive definite”

    • Fix: Remove exact duplicates, or add more jitter to diagonal

💡 Debugging breadcrumb: If Cholesky fails...

  1. Increase jitter: Try 1e-5 or 1e-4 (scale-aware formula above)

  2. Deduplicate inputs: Check for near-identical training points (distance < 1e-8)

  3. Standardize features: Z-score normalize all inputs before training

  4. Try Matérn-5/2 instead of SE (less prone to near-singularity)

  5. Check condition number: jnp.linalg.cond(K_y) should be <1010< 10^{10} for float64


### Step 4: Validate the Emulator

**Goal**: Check that GP predictions are accurate and well-calibrated.

**Metrics**:

1. **Predictive accuracy**:
   - RMSE: $\sqrt{\frac{1}{M} \sum_{i=1}^M (y_i - \mu(\mathbf{x}_i))^2}$ (how close are predictions?)
   - $R^2$ coefficient: fraction of variance explained

2. **Uncertainty calibration**:
   - **Coverage**: What fraction of test points fall within predicted 1-$\sigma$ bands?
     - Should be 68% if calibrated (for Gaussian residuals)
     - **Empirical coverage < nominal** (e.g., 60% within 1σ vs expected 68%) → **Overconfident** (error bars too narrow)
     - **Empirical coverage > nominal** (e.g., 80% within 1σ) → **Underconfident** (error bars too wide)
   - **Calibration plot**: Plot predicted std vs actual error, should align

3. **Negative log-likelihood (NLL)** on test set:
   $$\text{NLL} = -\frac{1}{M} \sum_{i=1}^M \log \mathcal{N}\big(y_i \mid \mu(\mathbf{x}_i), \sigma_*^2(\mathbf{x}_i) + \sigma_n^2\big)$$

   **Note**: Use **total predictive variance** $\sigma_*^2(\mathbf{x}_i) + \sigma_n^2$ for observed targets. For latent-function scoring, use epistemic variance $\sigma_*^2(\mathbf{x}_i)$ only.

   - Jointly measures accuracy + calibration
   - Lower is better

**Diagnostics**:

- **Residual plots**: $(y_i - \mu(\mathbf{x}_i))$ vs $\mathbf{x}_i$ (should be random, no patterns)
- **QQ plots** (quantile–quantile plots): Normalized residuals $(y_i - \mu(\mathbf{x}_i)) / \sigma(\mathbf{x}_i)$ should be $\mathcal{N}(0,1)$
- **Learned hyperparameters**: Do lengthscales make physical sense?
- **Out-of-distribution (OOD) detection**: Flag test inputs with low training similarity:
  $$\text{OOD if } \max_i k(\mathbf{x}_*, \mathbf{x}_i) / \sigma_f^2 < \text{threshold}$$

  **Note**: The threshold (e.g., 0.05) should be validated on your data; it depends on learned hyperparameters ($\sigma_f^2$, $\ell$). Cross-validate by checking whether OOD-flagged points have poor predictive accuracy.

  **Interpretation**: Point $\mathbf{x}_*$ has weak correlation with ALL training points → extrapolation!

  **Action items**:

  - Annotate predictions as **low-trust** in plots (different color/marker)
  - Report OOD fraction in test set as quality metric
  - Consider running additional simulations in OOD regions before making scientific claims
  - **Rule**: Never publish conclusions based solely on OOD predictions without validation

  **For your project**: If >10% of test set is OOD, your training data doesn't cover parameter space adequately.

**Cross-validation**:

- Leave-one-out: Train on $N-1$ points, predict left-out point, repeat
- $k$-fold: Split data into $k$ chunks, train on $k-1$, test on 1, repeat
- Checks: Does GP generalize? Or just memorizing training data?

### Step 5: Use the Emulator for Science

**Now the payoff! You have a fast, accurate, uncertainty-aware emulator. Use it for:**

**Application 1: Parameter Space Exploration.**

- Evaluate $\mu(\mathbf{x})$ on dense grid (millions of points, takes seconds)
- Visualize: Which combinations of $(Q, N, a)$ lead to survival vs dissolution?
- Identify interesting regions for further study

**Application 2: Optimization.**

- Find $\mathbf{x}^* = \arg\max_{\mathbf{x}} \mu(\mathbf{x})$ (e.g., maximize cluster lifetime)
- Use gradient-based optimizer (JAX gives gradients!)
- Or: Bayesian optimization (sample where acquisition function is high)

**Application 3: Bayesian Parameter Inference.**

- Observe real cluster (e.g., Pleiades): $(R_{\rm core, obs}, \sigma_{v, obs}, M_{\rm obs})$
- Likelihood: $p(\text{data} | \mathbf{x}) = \mathcal{N}(\text{data} | \mu_{\rm emu}(\mathbf{x}), \sigma^2_{\rm emu}(\mathbf{x}))$
- Run MCMC (you know how!) using emulator for likelihood
- Infer: What initial conditions produced observed cluster?

:::{admonition} Critical: Using Emulator in MCMC Likelihoods
:class: danger

**Common mistake**: Double-counting or forgetting emulator uncertainty in inference!

When using your emulator inside MCMC, the total observational uncertainty is:

$$
\boxed{\Sigma_{\text{total}}(\mathbf{x}) = \Sigma_{\text{obs}} + \sigma_*^2(\mathbf{x})}
$$

where:

- $\Sigma_{\text{obs}}$: Measurement uncertainty from observations (telescope noise, etc.)
- $\sigma_*^2(\mathbf{x})$: Emulator epistemic uncertainty (from GP)

**The likelihood is**:
$$
p(\mathbf{y}_{\text{obs}} | \mathbf{x}) = \mathcal{N}(\mathbf{y}_{\text{obs}} | \mu_{\text{emu}}(\mathbf{x}), \Sigma_{\text{total}}(\mathbf{x}))
$$

**NOT**: $\mathcal{N}(\mathbf{y}_{\text{obs}} | \mu_{\text{emu}}(\mathbf{x}), \Sigma_{\text{obs}})$ ❌ (ignores emulator error!)

**NOT**: $\mathcal{N}(\mathbf{y}_{\text{obs}} | \mu_{\text{emu}}(\mathbf{x}), \sigma_*^2(\mathbf{x}))$ ❌ (ignores observation noise!)

**Why this matters**:

- Ignoring emulator uncertainty → overconfident posteriors (claims too much precision)
- Double-counting → underconfident posteriors (claims too little)
- Correct accounting → honest uncertainty quantification

**In practice**:

```python
# Emulator prediction
mu_emu, var_emu = gp.predict(theta)  # var_emu = sigma_*^2

# Total variance
var_total = var_obs + var_emu  # Add in quadrature

# Likelihood
log_likelihood = -0.5 * (y_obs - mu_emu)**2 / var_total - 0.5 * jnp.log(2 * jnp.pi * var_total)
```

**Connection to Module 5**: This is Bayesian error propagation—combining uncertainties from multiple sources (observations + model).

Application 4: Experimental Design.


🎓 What We’ve Learned: Implementation Summary

The Complete Workflow

  1. Generate training data (LHS sampling)

  2. Train GP (maximize marginal likelihood)

  3. Make predictions (Gaussian conditioning)

  4. Validate (coverage, calibration)

  5. Use for science (exploration, inference, optimization)

Connection to Everything You’ve Learned

Module 1 (Statistics):

Module 2-3 (Stellar Systems):

Module 4 (Radiative Transfer):

Module 5 (Bayesian Inference):

Project 5 (JAX):

Next Lecture (Neural Networks):

The Philosophical Point

Machine learning is not magic. It’s principled probabilistic inference with mathematical foundations.

GPs exemplify this:

When you implement a GP, you’re not using a black box—you’re applying a century of mathematics (Kolmogorov, Wiener, Rasmussen) to solve a 21st-century astrophysics problem.

This is computational astrophysics in 2025!


🔮 Your Final Project

Next Lecture (Part 2: Implementation):

Your Final Project (Weeks 1-3):

Week 1: Core Emulation.

Week 2: Neural Network Comparison.

Week 3: Extension.

Success Criteria:

  1. Emulator predicts well (R2>0.9R^2 > 0.9 on test set)

  2. Uncertainty is calibrated (68% coverage at 1-σ\sigma)

  3. Can explain: When to use GP vs NN?

  4. Code is modular (easy to swap kernels, add data)

  5. Documentation is research-grade (hand to collaborator, they can use it)


📚 Preparation for Next Lecture

Before Part 2, you should:

  1. Review these notes: Make sure you understand every conceptual checkpoint

  2. Mathematical preparation: Can you derive the GP predictive formulas? Try it on paper.

  3. Computational thinking:

    • How would you implement kSE(x,x)k_{\text{SE}}(\mathbf{x}, \mathbf{x}') in JAX?

    • How to compute K\mathbf{K} for NN training points efficiently?

    • What’s the shape of all the matrices involved?

  4. Read (optional but recommended):

    • Rasmussen & Williams, Gaussian Processes for Machine Learning, Ch. 2.1-2.3

    • MacKay, Information Theory, Inference, and Learning Algorithms, Ch. 45

  5. Physical intuition:

    • For your N-body sims, which parameter do you expect matters most for bound fraction?

    • Sketch what you think the emulator function looks like: f(Q)f(Q) holding others fixed

    • What should lengthscales be approximately? (Guess based on parameter ranges)

  6. Deliverables checklist (for your final project report):

    • Report RMSE, R2R^2, NLL (with total variance σ2+σn2\sigma_*^2 + \sigma_n^2), and 1σ/2σ coverage

    • Include one residual plot and one QQ plot (quantile–quantile)

    • Include an OOD similarity histogram (flag extrapolation regions)

    • Discuss ARD lengthscales physically: After training, list Q,N,a\ell_Q, \ell_N, \ell_a and write one sentence per parameter interpreting physics:

      • Example: “Q\ell_Q small → bound fraction highly sensitive to virial ratio”

      • Example: “N\ell_N large → bound fraction insensitive to particle number for N>500N > 500

      • Example: “a\ell_a moderate → cluster scale directly affects tidal stripping timescale”

  7. Questions: Write down anything confusing. We’ll address in Part 2!


📖 Glossary of Key Terms

ARD (Automatic Relevance Determination): Kernel with per-dimension lengthscales d\ell_d, allowing the GP to learn which input dimensions matter most. Small d\ell_d → high sensitivity to dimension dd, large d\ell_d → low sensitivity (GP effectively ignores that dimension).

LHS (Latin Hypercube Sampling): Stratified sampling method ensuring uniform coverage in each dimension separately. Better than random sampling for space-filling experimental designs. Widely used for training surrogate models.

OOD (Out-of-Distribution): Test points far from training data, where the emulator extrapolates and uncertainty is high. Detect via kernel similarity: maxik(x,xi)/σf2<threshold\max_i k(\mathbf{x}_*, \mathbf{x}_i) / \sigma_f^2 < \text{threshold} (e.g., 0.05; validate empirically for your problem as threshold depends on learned hyperparameters).

ICM/LMC (Intrinsic Coregionalization Model / Linear Model of Coregionalization): Multi-output GP methods that model correlations between outputs via cross-covariance functions. Beyond scope of this course; see Álvarez et al. (2012) for details.

Epistemic Uncertainty: Uncertainty about the latent function ff (reducible with more training data). From GP: σ2(x)\sigma_*^2(\mathbf{x}). Also called “model uncertainty” or “knowledge uncertainty.”

Aleatoric Uncertainty: Irreducible noise in observations (measurement error, simulation stochasticity). From likelihood: σn2\sigma_n^2. Also called “data uncertainty.”

Marginal Likelihood (Evidence): p(yX,θ)p(\mathbf{y} | X, \boldsymbol{\theta}), the probability of observing training data given hyperparameters, integrating over all possible functions. Used for Type-II maximum likelihood hyperparameter optimization. Implements Occam’s razor automatically.

Kernel (Covariance Function): k(x,x)k(\mathbf{x}, \mathbf{x}'), measures correlation between function values at inputs x\mathbf{x} and x\mathbf{x}'. Encodes smoothness assumptions and prior beliefs about function structure.

Lengthscale (\ell): Characteristic distance in input space over which function values remain correlated. Small \ell → rapid variation (wiggly functions), large \ell → smooth variation (slowly varying functions). Units: same as input dimensions.

Signal Variance (σf2\sigma_f^2): Prior variance of function values (overall amplitude of variation). Units: output2^2.

Noise Variance (σn2\sigma_n^2): Observation noise level (aleatoric uncertainty). Units: output2^2.

Cholesky Factorization: Decomposition Ky=LLT\mathbf{K}_y = \mathbf{L} \mathbf{L}^T where L\mathbf{L} is lower-triangular. Numerically stable way to solve linear systems and compute determinants. Essential for GP implementation.

Jitter: Small constant added to kernel matrix diagonal to ensure numerical stability: Ky=K+(σn2+ϵjitter)I\mathbf{K}_y = \mathbf{K} + (\sigma_n^2 + \epsilon_{\rm jitter}) \mathbf{I}. Prevents Cholesky failures from near-singular matrices. Typical values: ϵjitter106\epsilon_{\rm jitter} \sim 10^{-6} (scale-aware).

SE (Squared Exponential) Kernel: Also called RBF (Radial Basis Function). Assumes infinitely smooth functions. Often too smooth for real physics.

Matérn Kernel: Family of kernels with tunable smoothness ν\nu. Matérn-5/2 is recommended default (twice differentiable, realistic for physics).

PSD (Positive Semi-Definite): Property required for valid kernels. Ensures covariance matrices have non-negative eigenvalues (variances can’t be negative).

MCMC (Markov Chain Monte Carlo): Sampling method for complex posterior distributions (from Module 5). GPs enable MCMC-based inference by providing fast, differentiable emulators of expensive simulators.

JAX: Just-In-Time compiled array computing library for Python with automatic differentiation. Makes GP implementation fast and enables gradient-based hyperparameter optimization.

JIT (Just-In-Time Compilation): Compilation technique that speeds up Python code to near-C performance. Use @jit decorator in JAX.

vmap (Vectorized Map): JAX function for automatic vectorization. Maps a function over batch dimension efficiently (avoids Python loops).


Now you’re ready to implement! 🚀

“The best way to understand a Gaussian Process is to implement one.” — Every computational scientist ever