Note
Go to the end to download the full example code.
Diagnose draws with ArviZ¶
ArviZ summarizes and diagnoses posterior draws held in a DataTree with
named variables and chain and draw dimensions. names and
shapes from flatppl.Module.logdensity() name the variables, and
constrain turns sampler coordinates into model parameters. Every sampler
page ends here; this one samples with BlackJAX NUTS and checks ArviZ’s
summary against the closed-form posterior of a Normal-Gamma model.
Runtime budget: 120 s.
The model and its closed-form posterior¶
tau | y ~ Gamma(a_n, b_n); mu has a Student-t marginal with mean
m_n and variance b_n / (k_n (a_n - 1)).
data = np.array([1.2, 0.4, 2.1, 1.7, 0.9, 1.5, 0.3, 1.1])
model = flatppl(
"""
tau ~ Gamma(shape = 3.0, rate = 2.0)
mu ~ Normal(mu = 0.0, sigma = 1.0 / sqrt(2.0 * tau))
y ~ iid(Normal(mu = mu, sigma = 1.0 / sqrt(tau)), 8)
observed = external(cartpow(reals, 8))
prior = lawof(record(mu = mu, tau = tau))
L = likelihoodof(kernelof(record(y = y), mu = mu, tau = tau), record(y = observed))
posterior = bayesupdate(L, prior)
"""
).set(observed=data)
density = model.logdensity("posterior")
a0, b0, k0, m0, n = 3.0, 2.0, 2.0, 0.0, data.size
mean = data.mean()
kn, an = k0 + n, a0 + n / 2
mn = (k0 * m0 + n * mean) / kn
bn = b0 + 0.5 * ((data - mean) ** 2).sum() + k0 * n * (mean - m0) ** 2 / (2 * kn)
exact = {"mu": (mn, bn / (kn * (an - 1))), "tau": (an / bn, an / bn**2)}
log_evidence = (
lgamma(an) - lgamma(a0) + a0 * log(b0) - an * log(bn)
+ 0.5 * log(k0 / kn) - n / 2 * log(2 * pi)
) # fmt: skip
Check the normalization¶
The free coordinates’ density integrates to the closed-form evidence.
mu_axis = jnp.linspace(-3.0, 5.0, 401)
log_tau_axis = jnp.linspace(-4.0, 3.0, 401)
grid = jax.vmap(
lambda m: jax.vmap(lambda t: density({"mu": m, "tau": t}))(log_tau_axis)
)(mu_axis)
total = np.trapezoid(
np.trapezoid(np.exp(np.asarray(grid)), np.asarray(log_tau_axis), axis=1),
np.asarray(mu_axis),
)
assert abs(np.log(total) - log_evidence) < 1e-6
Draw from four chains¶
BlackJAX NUTS with window adaptation, as in Sample and approximate a posterior with BlackJAX. The sampler statistics ride along for ArviZ.
chains, warmup, draws = 4, 400, 1000
starts = jax.vmap(
lambda key: jax.tree.map(
lambda x: x + 0.5 * jax.random.normal(key, jnp.shape(x)), density.init
)
)(jax.random.split(jax.random.key(1), chains))
def run_chain(key: jax.Array, start: Any) -> Any:
"""Adapt one chain, then keep its draws and sampler statistics."""
warmup_key, sample_key = jax.random.split(key)
adaptation = blackjax.window_adaptation(blackjax.nuts, density.fn)
(state, parameters), _ = adaptation.run(warmup_key, start, num_steps=warmup)
kernel = blackjax.nuts(density.fn, **parameters)
def step(state: Any, key: jax.Array) -> tuple[Any, Any]:
state, info = kernel.step(key, state)
stats = {"diverging": info.is_divergent, "energy": info.energy}
return state, (state.position, stats)
return jax.lax.scan(step, state, jax.random.split(sample_key, draws))[1]
free, stats = jax.jit(jax.vmap(run_chain))(
jax.random.split(jax.random.key(2), chains), starts
)
samples = jax.vmap(jax.vmap(density.constrain))(free)
Build the ArviZ data¶
One variable per name in density.names. A parameter of shape
density.shapes[name] keeps those trailing dimensions after
(chain, draw); both parameters here are scalars.
posterior = az.from_dict(
{
"posterior": {name: np.asarray(samples[name]) for name in density.names},
"sample_stats": {name: np.asarray(value) for name, value in stats.items()},
}
)
summary = az.summary(posterior, ci_prob=0.9, round_to="none")
print(summary.to_string())
mean sd eti90_lb eti90_ub ess_bulk ess_tail r_hat mcse_mean mcse_sd
mu 0.919051 0.268846 0.475752 1.359296 2460.121136 2235.982077 0.999856 0.005433 0.004612
tau 1.600658 0.612511 0.745355 2.748949 3012.758843 2659.248743 1.000553 0.011030 0.010007
Check the summary against the closed form¶
Each posterior mean lies within four of ArviZ’s Monte Carlo standard errors of the exact value, and each standard deviation within five percent. Rank normalized R-hat and bulk ESS confirm the chains mixed.
for name, (exact_mean, exact_variance) in exact.items():
row = {key: float(value) for key, value in summary.loc[name].items()}
assert abs(row["mean"] - exact_mean) < 4 * row["mcse_mean"], name
assert abs(row["sd"] / np.sqrt(exact_variance) - 1) < 0.05, name
assert row["r_hat"] < 1.01 and row["ess_bulk"] > 400, name
assert not np.asarray(stats["diverging"]).any()
Total running time of the script: (0 minutes 2.567 seconds)