Gaussian posterior#
These examples walk through Bayesian inference with Sequince’s standard
Sequential Monte Carlo (SMC) sampler. Start with gaussian.py to learn the
workflow, then run rosenbrock.py to explore a curved posterior and
customize mutation and resampling. Each script
contains its own prior, likelihood, and sampler setup. Edit the parameters near
the top of the script to experiment.
Setup and running#
Use Python 3.11 or later and install Sequince with the optional plotting and HDF5 dependencies:
python -m pip install 'sequince[corner,hdf5]'
For installation options, including using a development checkout, see “Getting started” in the main Sequince documentation.
Run the script from this directory:
python gaussian.py
The script writes to outdir/gaussian within this directory.
Running a script again replaces its previous outputs.
1. Define the target distribution#
Sequince needs functions that evaluate the log prior and log likelihood. Both
receive a batch of particles with shape (n_particles, n_dimensions) and a
StepContext, and return one value per particle, with shape
(n_particles,). Sequince passes the context when evaluating the target.
Use context.shared to read static model parameters supplied to smc.run
through shared_context. For example, both scripts define the prior as:
def log_prior(particles, context):
lower_bound = context.shared["lower_bound"]
upper_bound = context.shared["upper_bound"]
inside = np.all(
(particles >= lower_bound) & (particles <= upper_bound), axis=-1
)
log_density = -particles.shape[1] * np.log(upper_bound - lower_bound)
return np.where(inside, log_density, -np.inf)
The Gaussian likelihood reads mean and sigma from the same shared
mapping; the Rosenbrock likelihood reads its coefficients a and b.
This makes the target functions independent of module-level model settings.
For a parameter vector \(\boldsymbol{\theta}\) with \(d\) dimensions, the examples use independent uniform priors between -5 and 5:
Including the prior normalization is necessary for a correct evidence estimate.
In gaussian.py, the likelihood is a standard multivariate Gaussian,
including its normalization constant (the defaults are mean=0 and
sigma=1). The resulting posterior is a standard
Gaussian restricted to the prior box. Its mean is zero, and its covariance is
very close to the identity for these wide bounds. The script also computes the
analytic evidence, including the Gaussian probability mass inside the box.
2. Initialize particles from the prior#
Create a seeded NumPy generator, draw particles from the prior, and put them in
a ParticleState:
rng = np.random.default_rng(seed)
initial_state = ParticleState(
particles=rng.uniform(
lower_bound, upper_bound, size=(n_particles, n_dimensions)
),
)
When log_weights is omitted, ParticleState supplies equal normalized
weights, each with log weight \(\log w_i = -\log N\), where \(N\) is
the number of particles (n_particles). This is appropriate here
because the particles are drawn from the prior, the initial target distribution.
There is no need to set the weights explicitly. Supply log_weights when
constructing a state that already has nonuniform importance weights.
Keep using the same generator for the sampler. Changing the initialization to a
different distribution requires accounting for that proposal; see the main
API reference for make_standard_bayesian_smc and its log_proposal option.
3. Configure the sampler#
Use the prebuilt helper to assemble the standard Bayesian SMC workflow:
smc, posterior, metadata = make_standard_bayesian_smc(
log_prior=log_prior,
log_likelihood=log_likelihood,
target_ess_ratio=0.8,
mutation="minipcn",
mutation_n_steps=16,
)
The sampler moves through tempered distributions
where \(p\) is the prior and \(L\) is the likelihood. It starts at the prior with \(\beta=0\) and finishes at the posterior with \(\beta=1\). Adaptive tempering chooses increments using an effective sample size (ESS) criterion. Resampling concentrates particles in regions of higher weight, and mutation moves help the population explore each intermediate distribution.
The helper returns the SMC runner, a tempered posterior object, and initial
metadata. These scripts use the runner and metadata directly; the posterior
object is also available for further sampling with draw_additional_samples.
See “Configuring the runner” in the main User guide and the helper’s API
reference for resampling thresholds, mutation choices, and other options.
4. Run SMC#
Pass the initial population, generator, and helper metadata to the runner:
result = smc.run(
initial_state,
rng=rng,
shared_context={
"lower_bound": lower_bound,
"upper_bound": upper_bound,
"mean": mean,
"sigma": sigma,
},
metadata=metadata,
)
The runner makes shared_context available as context.shared in both
target functions. The Gaussian example supplies the bounds, mean, and standard
deviation shown above; the Rosenbrock example supplies bounds and its two
coefficients. Treat this mapping as read-only during the run.
The helper’s metadata is available as context.metadata and carries
evolving information such as the tempering parameter \(\beta\)
(context.metadata["beta"]). Sequince’s tempered posterior applies
\(\beta\) to the likelihood; the likelihood functions return their ordinary
log likelihood, without applying \(\beta\) themselves.
context.t identifies the current SMC iteration. The configured sampler
stops when it reaches the posterior. Logging shows progress and the running
evidence estimate.
See “Meta data & shared context” in the main Getting started guide and “Working with particle state” in the User guide for these interfaces.
5. Inspect and save the result#
result.final_state contains the final particles, their weights, and the log
evidence estimate. result.weighted_mean() and
result.weighted_covariance() compute posterior summaries using those weights.
For the Gaussian, compare the summaries and evidence with the known values.
Monte Carlo estimates fluctuate; agreement improves with adequate particles and
mixing, rather than being exact for a single run.
Each script saves:
corner.png: marginal and joint posterior plots.history.png: the tempering parameter and log evidence over iterations.posterior.npz: particles, normalized weights, and the log evidence estimate.result.h5: the SMC result saved usingresult.save_to_file.
The arrays can be loaded with numpy.load. Retain the weights when analyzing
the particles. See the main API reference for SMCResult for posterior sample
extraction, diagnostics, plotting, and result persistence.
Gaussian results#
Weighted marginal and joint posterior distributions for the Gaussian target.#
Adaptive temperature and accumulated log evidence at each SMC stage.#
1"""Sample a Gaussian posterior using Sequince. See README.rst."""
2
3import logging
4from math import erf, sqrt
5from pathlib import Path
6
7import matplotlib.pyplot as plt
8import numpy as np
9from sequince import ParticleState, make_standard_bayesian_smc
10from sequince.log import configure_logging
11from sequince.steps import StepContext
12
13# Edit these parameters to explore the example.
14n_dimensions = 2
15n_particles = 2000
16lower_bound, upper_bound = -5.0, 5.0
17mean, sigma = 0.0, 1.0
18seed = 42
19outdir = Path("outdir/gaussian")
20
21
22def log_prior(particles: np.ndarray, context: StepContext) -> np.ndarray:
23 """Evaluate a normalized uniform prior within the bounds.
24
25 Parameters
26 ----------
27 particles : numpy.ndarray, shape (n_particles, n_dimensions)
28 Batch of parameter vectors.
29 context : StepContext
30 Runtime context whose shared mapping contains the model parameters.
31
32 Returns
33 -------
34 log_density : numpy.ndarray, shape (n_particles,)
35 Log prior density, with negative infinity outside the bounds.
36 """
37 lower_bound = context.shared["lower_bound"]
38 upper_bound = context.shared["upper_bound"]
39 inside = np.all(
40 (particles >= lower_bound) & (particles <= upper_bound), axis=-1
41 )
42 log_density = -particles.shape[1] * np.log(upper_bound - lower_bound)
43 return np.where(inside, log_density, -np.inf)
44
45
46def log_likelihood(particles: np.ndarray, context: StepContext) -> np.ndarray:
47 """Evaluate a normalized Gaussian likelihood.
48
49 Parameters
50 ----------
51 particles : numpy.ndarray, shape (n_particles, n_dimensions)
52 Batch of parameter vectors.
53 context : StepContext
54 Runtime context whose shared mapping contains the model parameters.
55
56 Returns
57 -------
58 log_density : numpy.ndarray, shape (n_particles,)
59 Gaussian log density for each particle.
60 """
61 mean = context.shared["mean"]
62 sigma = context.shared["sigma"]
63 return -0.5 * np.sum(((particles - mean) / sigma) ** 2, axis=-1) - (
64 0.5 * particles.shape[1] * np.log(2.0 * np.pi * sigma**2)
65 )
66
67
68def main() -> None:
69 """Sample the posterior and save particles, plots, and the SMC result."""
70 configure_logging(level=logging.INFO)
71 rng = np.random.default_rng(seed)
72 outdir.mkdir(parents=True, exist_ok=True)
73
74 # ParticleState supplies equal weights, appropriate for prior draws.
75 initial_state = ParticleState(
76 particles=rng.uniform(
77 lower_bound, upper_bound, size=(n_particles, n_dimensions)
78 ),
79 )
80
81 # The helper combines adaptive tempering, resampling, and mutation moves.
82 smc, posterior, metadata = make_standard_bayesian_smc(
83 log_prior=log_prior,
84 log_likelihood=log_likelihood,
85 target_ess_ratio=0.8,
86 mutation="minipcn",
87 mutation_n_steps=16,
88 )
89 result = smc.run(
90 initial_state,
91 rng=rng,
92 shared_context={
93 "lower_bound": lower_bound,
94 "upper_bound": upper_bound,
95 "mean": mean,
96 "sigma": sigma,
97 },
98 metadata=metadata,
99 )
100
101 state = result.final_state
102 print(f"Estimated log evidence: {state.log_evidence:.4f}")
103 # Include the Gaussian mass within the finite prior bounds.
104 prior_mass = 0.5 * (
105 erf((upper_bound - mean) / (sigma * sqrt(2.0)))
106 - erf((lower_bound - mean) / (sigma * sqrt(2.0)))
107 )
108 log_evidence = n_dimensions * (
109 np.log(prior_mass) - np.log(upper_bound - lower_bound)
110 )
111 print(f"Analytic log evidence: {log_evidence:.4f}")
112 print(f"Untruncated Gaussian mean: {mean}; variance: {sigma**2}.")
113 print(f"Posterior mean: {result.weighted_mean()}")
114 print(f"Posterior covariance:\n{result.weighted_covariance()}")
115
116 fig = result.plot_corner()
117 fig.savefig(outdir / "corner.png")
118 plt.close(fig)
119
120 fig, axes = plt.subplots(2, 1, figsize=(6, 6))
121 result.plot_metadata_history("beta", ax=axes[0])
122 axes[0].set_ylabel("Tempering parameter beta")
123 result.plot_log_evidence(ax=axes[1])
124 fig.tight_layout()
125 fig.savefig(outdir / "history.png")
126 plt.close(fig)
127
128 # Keep weights with the particles so subsequent analysis remains valid.
129 np.savez(
130 outdir / "posterior.npz",
131 particles=state.particles,
132 weights=state.weights,
133 log_evidence=state.log_evidence,
134 )
135 result.save_to_file(outdir / "result.h5")
136
137
138if __name__ == "__main__":
139 main()
Continue with Rosenbrock posterior to explore a curved posterior and conditional resampling.