Handoff: same model, INLA and Turing
Latte uses DynamicPPL's @model macro as its modelling surface. So does Turing.jl. A model you write for inla() can therefore be handed straight to Turing.sample for MCMC, with no rewriting in between.
This tutorial shows the handoff on a small Poisson model with an IID-Normal latent field, and compares the posteriors the two engines return.
The shared model is useful in a few situations:
When you suspect INLA's Laplace approximation might be off on an unusual model, a short NUTS run on the same
@modelis the most direct check.INLA is fast and approximate; HMC is slow and asymptotically exact. Keeping one model definition lets you pick the engine per problem.
Latte also exposes
hmc_laplace(lgm, y), which runs NUTS over the hyperparameters with an inner Laplace step on the latent field. It sits betweeninlaand full NUTS.
A small Poisson model
using Latte
using DynamicPPL: @model
using Distributions
using GaussianMarkovRandomFields: IIDModel
using Turing
using Random, Statistics
using CairoMakie
Random.seed!(20260424)
@model function iid_poisson(y, n)
τ ~ PCPrior.Precision(1.0, α = 0.01)
x ~ IIDModel(n)(τ = τ)
for i in eachindex(y)
y[i] ~ Poisson(exp(x[i]); check_args = false)
end
end
n = 15
true_x = randn(n) .* 0.4 .+ 0.8
y = rand.(Poisson.(exp.(true_x)))
first(y, 5)5-element Vector{Int64}:
3
1
5
3
5Fit with INLA
latte_from_dppl packages the DPPL model into a LatentGaussianModel that inla() understands. It is the same object tmb() and hmc_laplace() take.
model = iid_poisson(y, n)
lgm = latte_from_dppl(model; random = (:x,))
result = inla(lgm, y; progress = false)INLAResult:
Model: LatentGaussianModel{HyperparameterSpec{@NamedTuple{τ::Hyperparameter{Bijectors.TruncatedBijector{Float64, Float64}, :natural}}, @NamedTuple{}}, Latte.CachedDAGLatentModel{Latte.var"#_build_dag_latent##2#_build_dag_latent##3"{DynamicPPL.Model{typeof(Main.var"##976".iid_poisson), (:y, :n), (), (), Tuple{Vector{Int64}, Int64}, Tuple{}, DynamicPPL.DefaultContext, false}, Tuple{Symbol}, @NamedTuple{dims::Dict{Symbol, Int64}, classification::Dict{Symbol, Symbol}, edges::Dict{Symbol, Vector{Symbol}}, is_scalar::Dict{Symbol, Bool}}}, Nothing}, LinearlyTransformedObservationModel{ExponentialFamily{Distributions.Poisson, LogLink, Nothing, Nothing}, SparseArrays.SparseMatrixCSC{Float64, Int64}, Nothing}}
Hyperparameters: 1
Latent variables: 15
Mode: (τ=1.5845)
Convergence: ✓
Total time: 12.91 seconds
Exploration: 5 points (5 integration)
Model comparison metrics:
Deviance Information Criterion (DIC):
DIC: 48.24
Effective parameters (p_D): 1.23
Mean deviance (D̄): 47.0
Deviance at mode: 45.77
Marginal Log-Likelihood:
log p(y): -36.64
Watanabe-Akaike Information Criterion (WAIC):
WAIC: 59.2
Effective parameters (p_WAIC): 4.68
Log pointwise predictive density (lppd): -24.92
Conditional Predictive Ordinates (CPO):
LPML: -1159.65
Mean CPO: 0.0
Min CPO: 0.0
Unreliable observations: 15 / 15
Max failure score: 49.95
PIT computed: 15 values
PIT mean: 0.6344 (ideal: 0.5)
Approximation quality (KLD):
Max SKLD: 0.0106 (variable 1)
Mean SKLD: 0.0050
Use .hyperparameter_marginals, .latent_marginals, .accumulators for analysisRead the latent field back through the accessor, keyed by the name it has in the model (:x), rather than reaching into the result's fields:
x_inla = mean.(latent_marginals(result, :x))
first(x_inla, 5)5-element Vector{Float64}:
0.5711840374054646
0.0
1.0502608001967144
0.6450923524346533
1.0502608001967144Fit with Turing NUTS
Turing consumes the @model directly, so there is no packaging step here.
chain = sample(model, NUTS(), 2000; progress = false)
x_nuts = [mean(chain[Symbol("x[$i]")]) for i in 1:n];┌ Info: Found initial step size
└ ϵ = 0.45Compare posteriors
Latent posterior means from the two engines line up on the diagonal. Raw Makie here: an identity-reference-line scatter (the dashed y = x line spans the shared data range), which AoG's tidy-data idiom does not express cleanly.
fig = Figure(size = (500, 500))
ax = Axis(
fig[1, 1],
xlabel = "INLA posterior mean", ylabel = "Turing NUTS posterior mean",
title = "Latent posterior means (n = $n)"
)
scatter!(ax, x_inla, x_nuts)
lims = extrema(vcat(x_inla, x_nuts))
lines!(ax, [lims...], [lims...], color = :gray, linestyle = :dash)
fig
Quantitatively:
println("max |x_INLA − x_NUTS| = ", round(maximum(abs.(x_inla .- x_nuts)), digits = 3))
println("mean |x_INLA − x_NUTS| = ", round(mean(abs.(x_inla .- x_nuts)), digits = 3))max |x_INLA − x_NUTS| = 0.084
mean |x_INLA − x_NUTS| = 0.037Caveats
Sharing one @model keeps the specification identical across engines, but a few things are worth knowing before you lean on the comparison:
Constrained GMRF priors (Besag ICAR with sum-to-zero, rank-deficient random walks, and the like) do not sample cleanly under plain NUTS without a reparameterisation onto the constraint manifold. NUTS samples the unconstrained base density and ignores the constraint, which is not the posterior you specified. For a constrained prior,
inla(),tmb(), andhmc_laplace()impose the constraint via Kriging conditioning.Plain NUTS on a latent Gaussian model is usually slower than INLA, since the posterior geometry is awkward for a generic sampler. In practice, run
inla()first and turn tohmc_laplace()when you want a sampled hyperparameter posterior.Heavy tails in hyperparameter marginals (a precision under weak data, say) will differ between the two: INLA extrapolates the tail with a spline, while NUTS only reflects what it visited. Compare medians and credible intervals near the mode, where both are on firm ground.
References
The Turing.jl probabilistic programming language: a flexible Julia system for MCMC-based Bayesian inference. Latte shares its DynamicPPL @model surface, so the same model hands off to Turing.sample.
The modelling-language layer behind Turing and the @model surface Latte builds on: a performant runtime for dynamic probabilistic models.
The original INLA paper: deterministic approximate Bayesian inference for latent Gaussian models via nested Laplace approximations and numerical integration over the hyperparameters.
The No-U-Turn Sampler, the adaptive Hamiltonian Monte Carlo algorithm Turing's NUTS sampler and Latte's HMC-Laplace engine both run.
Hamiltonian Monte Carlo over the hyperparameters, with the latent Gaussian field marginalised by an embedded Laplace approximation and the gradient propagated through that inner solve. The method Latte's hmc_laplace engine implements.
This page was generated using Literate.jl.