Note
Go to the end to download the full example code.
Sample a posterior with BlackJAX NUTS¶
Two inline FlatPPL modules give a JAX log density, and BlackJAX NUTS samples
it. The prior is Normal(0, 1) and one observation y = 1 has
Normal(theta, 1) noise, so the posterior has mean and variance 0.5. The
printed moments approach both. This page contains the complete workflow.
Install FlatPPL and BlackJAX using Installation. For positive scales, bounds or structured domains, continue with Fit a mixed model with automatic coordinate transforms, which uses automatic coordinate transforms.
Runtime budget: 60 s.
Write the model and its query¶
lawof collects the prior parameters. kernelof describes the observation
model; likelihoodof fixes the observed value. bayesupdate multiplies
prior and likelihood. The posterior binding can have any name: here steve.
Both inline modules share a context. Registering a logical filename lets the query load the model without creating a file. The query declares one record input and one scalar output.
def make_logdensity():
"""Compile the posterior log density of ``theta`` as a JAX callable."""
context = Context()
model = flatppl(
r"""
theta ~ Normal(mu = 0.0, sigma = 1.0)
y ~ Normal(mu = theta, sigma = 1.0)
prior = lawof((theta = theta))
forward_kernel = kernelof((y = y), theta)
L = likelihoodof(forward_kernel, (y = 1.0))
steve = bayesupdate(L, prior)
""",
context=context,
)
context.register("model.flatppl", model)
query = flatppl(
r"""
m = load_module("model.flatppl")
point = elementof(cartprod(theta = reals))
inputs = point
outputs = logdensityof(m.steve, point)
""",
context=context,
)
return query.compile()
Adapt and sample¶
BlackJAX needs a scalar log density and an initial point. This parameter is real-valued, so it already lives in unconstrained coordinates. Warmup tunes NUTS with BlackJAX’s default target acceptance of 0.8. Its returned parameters stay fixed during sampling. Separate keys keep the two phases independent.
lax.scan runs the transition loop in compiled code and collects the retained
draws. The Python function describes the loop once during JAX tracing.
def run(
*, warmup_steps: int = 512, num_samples: int = 4096, seed: int = 827
) -> tuple[jax.Array, jax.Array]:
"""Adapt NUTS, then return the retained ``theta`` draws and divergence flags."""
logdensity = make_logdensity()
warmup_key, sample_key = jax.random.split(jax.random.key(seed))
warmup = blackjax.window_adaptation(blackjax.nuts, logdensity)
(state, parameters), _ = warmup.run(
warmup_key, {"theta": jnp.float32(-2)}, num_steps=warmup_steps
)
sampler = blackjax.nuts(logdensity, **parameters)
@jax.jit
def sample(key: jax.Array, initial: Any) -> tuple[jax.Array, jax.Array]:
def step(current: Any, draw_key: jax.Array) -> tuple[Any, Any]:
updated, info = sampler.step(draw_key, current)
return updated, (updated.position["theta"], info.is_divergent)
return jax.lax.scan(step, initial, jax.random.split(key, num_samples))[1]
return sample(sample_key, state)
Compare with the known posterior¶
Completing the square gives Normal(0.5, sqrt(0.5)). Monte Carlo means and
variances approach 0.5; finite runs need not equal it. Check divergences too.
{'mean': 0.48674166202545166, 'variance': 0.516428530216217, 'divergences': 0}
Next, use Fit a Gaussian regression with runtime data for vector parameters and runtime data, or Fit a mixed model with automatic coordinate transforms for constrained scales, multiple chains and prediction.
Total running time of the script: (0 minutes 3.199 seconds)