This vignette fits a Besag-York-Mollie (BYM) model to the Germany oral-cavity-cancer data (Besag, York, and Mollié 1991). Counts \(y_i\) in 544 districts follow \(y_i \sim \mathrm{Poisson}(E_i e^{\eta_i})\) with a structured intrinsic CAR (ICAR) component plus an unstructured iid component.
The example uses the low-level adlaplace pipeline and
the random_mult kernel. The generalized log-determinant and
rank of the ICAR precision are stored in the bundled
germany data; only the precision multipliers enter the AD
tape (Sørbye and Rue 2014). The intercept is omitted because the ICAR
precision is rank-deficient.
The germany object includes case and expected counts,
the district adjacency matrix, the scaled ICAR precision
Q_scaled, and the prec list for
random_mult. District boundaries are optional: when
terra is available, germany$map is loaded
from inst/extdata/germany_map.gpkg in the setup chunk.
Latent vector \(\gamma =
(u_{\mathrm{struct}}, u_{\mathrm{iid}})\) has length \(2n\): structured and iid effects on \(n\) areas. Observations use
poisson_obs with offset \(\log
E_i\).
n <- length(germany$Y)
n_theta <- 2L
n_gamma <- 2 * n
A_obs <- cbind(
Matrix::sparseMatrix(i = seq_len(n), j = seq_len(n), x = 1, dims = c(n, n)),
Matrix::sparseMatrix(i = seq_len(n), j = seq_len(n), x = 1, dims = c(n, n))
)
X_obs <- Matrix::Matrix(0, n, 0)
config <- list(
transform_theta = TRUE,
gamma = rep(0, n_gamma),
theta = log(c(1e-2, 0.1)),
offset = log(germany$E),
shards = adlaplace::ad_shards(A_obs, num_shards = 2L),
verbose = FALSE
)
data_obs <- adlaplace:::ad_data(
y = germany$Y,
A = A_obs,
X = X_obs,
theta_map = n_theta,
ad_kind = "observations",
ad_fun = "poisson_obs"
)
model_struct <- adlaplace:::ad_data(
gamma_map = Matrix::sparseMatrix(
i = seq_len(n), j = seq_len(n), x = 1,
dims = c(n_gamma, n)
),
theta_map = c(1L, n_theta),
ad_kind = "random",
ad_fun = "random_mult",
precision = germany$prec
)
model_iid <- adlaplace:::ad_data(
gamma_map = Matrix::sparseMatrix(
i = seq.int(n + 1L, length.out = n), j = seq_len(n), x = 1,
dims = c(n_gamma, n)
),
theta_map = c(2L, n_theta),
ad_kind = "random",
ad_fun = "random_diagonal",
precision = rep(1, n)
)Each model piece is taped separately with ad_fun_ptr().
The random_mult shard for the ICAR term is the slowest
step.
ptr_obs <- adlaplace::ad_fun_ptr(data = data_obs, config = config)
ptr_iid <- adlaplace::ad_fun_ptr(data = model_iid, config = config)
ptr_struct <- adlaplace::ad_fun_ptr(data = model_struct, config = config)
ad_fun_plain <- c(ptr_obs, ptr_struct, ptr_iid)
ad_pack <- adlaplace::ad_fun(ad_fun_plain, num_threads = num_threads)Per-shard log densities sum to the joint log density at a fixed parameter vector.
x_full <- c(config$gamma, config$theta)
shards <- seq.int(from = 0L, length.out = adlaplace:::n_groups(ad_fun_plain))
by_shard <- vapply(
shards,
function(s) {
adlaplace::joint_log_dens(ad_fun_plain, x_full, shards = s, negative = TRUE)
},
numeric(1)
)
res_fdf <- adlaplace::fun_obj_fdfh(
ad_pack,
config$theta, config$gamma,
inner = FALSE, verbose = FALSE
)
c(
sum(by_shard),
adlaplace::joint_log_dens(ad_fun_plain, x_full, negative = TRUE),
adlaplace::joint_log_dens(ad_pack, x_full, negative = TRUE),
res_fdf$f
)## [1] -1485.755 -1485.755 -1485.755 -1485.755
Gradients and Hessians on all shards should agree with a subset covering the full model.
log_all <- adlaplace::joint_log_dens(ad_fun_plain, x_full, negative = TRUE)
log_sub <- adlaplace::joint_log_dens(
ad_fun_plain, x_full,
shards = 0:2, negative = TRUE
)
grad_all <- adlaplace::grad(ad_fun_plain, x_full, negative = TRUE)
grad_sub <- adlaplace::grad(ad_fun_plain, x_full, shards = 0:2, negative = TRUE)
hess_all <- adlaplace::hessian(ad_fun_plain, x_full, negative = TRUE)
hess_sub <- adlaplace::hessian(ad_fun_plain, x_full, shards = 0:2, negative = TRUE)
rbind(
log_diff = log_all - res_fdf$f,
max_abs_grad_diff = max(abs(grad_all - res_fdf$grad)),
max_abs_hess_diff = max(abs(as.matrix(hess_all) - as.matrix(res_fdf$hessian))),
log_sub_diff = log_all - log_sub,
max_abs_grad_sub_diff = max(abs(grad_all - grad_sub)),
max_abs_hess_sub_diff = max(abs(as.matrix(hess_all) - as.matrix(hess_sub)))
)## [,1]
## log_diff 2.273737e-13
## max_abs_grad_diff 0.000000e+00
## max_abs_hess_diff 0.000000e+00
## log_sub_diff -7.527037e+02
## max_abs_grad_sub_diff 5.440000e+02
## max_abs_hess_sub_diff 1.000000e+02
Given hyperparameters \(\theta\),
inner_opt() maximizes over \(\gamma\).
x_outer <- config$theta
inner_res <- adlaplace::inner_opt(
parameters = log(c(1e-1, 1e-1)),
gamma = config$gamma,
ad_fun = ad_pack,
control = list(
maxit = 1000L,
report.level = 4,
report.freq = 5,
grad.tol = 1e-8,
cg.tol = 1e-6
),
deriv = TRUE,
verbose = FALSE
)## Beginning optimization
##
## iter f nrm_gr status radCG iter CG result
## 5 -353.988972 0.000002 Continuing - TR contract 2.000000 50 Exceeded max CG iters
## 10 -353.988972 0.000002 Continuing - TR contract 0.062500 50 Exceeded max CG iters
## 15 -353.988972 0.000002 Continuing - TR contract 0.001953 50 Exceeded max CG iters
## 20 -353.988972 0.000002 Continuing - TR contract 0.000061 50 Exceeded max CG iters
## 25 -353.988972 0.000002 Continuing - TR contract 0.000002 50 Exceeded max CG iters
## 30 -353.988972 0.000000 Continuing - TR expand 0.000000 16 Intersect TR bound
##
## Iteration has terminated
## 30 -353.988972 0.000000 Success
## [1] -353.989
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -0.37929 -0.10944 -0.06669 0.83530 -0.03906 507.12520
The same finite-difference idea applies to the full joint log density \(-\log p(\gamma, \theta \mid y)\) and its AD gradient and Hessian.
par(mfrow = c(4, 2), mar = c(2, 2, 2, 0), mgp = c(1, 0.5, 0))
x_here <- inner_res$full_parameters
Dpar_dens <- 96 # length(x_here) - 1L
Ngrid <- 11L
shards <- seq.int(from = 0L, length.out = adlaplace:::n_groups(ad_fun_plain))
par_grid <- matrix(x_here, nrow = Ngrid, ncol = length(x_here), byrow = TRUE)
Sx <- x_here[Dpar_dens] + seq(-0.1, 0.1, length.out = Ngrid)
SxD <- Sx[-1] - diff(Sx) / 2
par_grid[, Dpar_dens] <- Sx
x_list <- split(par_grid, row(par_grid))
dens <- vapply(
x_list,
adlaplace::joint_log_dens,
numeric(1),
ad_fun = ad_fun_plain,
shards = shards,
negative = FALSE
)
rbind(
dens[1:5],
dens[seq(to = length(dens), length.out = 5)]
)## 1 2 3 4 5
## [1,] 352.2540 352.8781 353.3638 353.7110 353.9194
## [2,] 353.9194 353.7105 353.3621 352.8739 352.2459
grad <- do.call(
cbind,
lapply(
x_list,
adlaplace::grad,
ad_fun = ad_fun_plain,
inner = FALSE,
shards = shards,
negative = FALSE
)
)
plot(Sx, grad[Dpar_dens, ], type = "l", ylab = "AD gradient")
points(SxD, diff(dens) / diff(Sx), pch = 16)
legend("topright", legend = c("AD", "finite diff"), lty = c(1, NA), pch = c(NA, 1), bty = "n")
hes <- array(
dim = c(length(x_here), length(x_here), Ngrid),
dimnames = list(NULL, NULL, NULL)
)
for (i in seq_len(Ngrid)) {
hes[, , i] <- as.matrix(adlaplace::hessian(
ad_fun_plain,
x_list[[i]],
inner = FALSE,
shards = shards,
negative = FALSE
))
}
grad_slope <- apply(grad, 1, diff) / mean(diff(Sx))
s_par2 <- sort(unique(c(Dpar_dens + 0:5, Dpar_dens + length(germany$Y), seq(to = length(x_here), length.out = 5))))
s_par2 <- s_par2[seq(1, min(c(prod(par("mfrow")) - 1L, length(s_par2))))]
for (Dpar2 in s_par2) {
plot(
Sx, hes[Dpar_dens, Dpar2, ],
type = "l",
ylab = paste0("H[", Dpar_dens, ",", Dpar2, "]"),
ylim = range(c(hes[Dpar_dens, Dpar2, ], grad_slope[, Dpar2]), na.rm = TRUE)
)
points(SxD, grad_slope[, Dpar2], pch = 16)
}log_lik_laplace() runs the inner optimizer and returns
the Laplace approximation to the marginal log likelihood.
res_noderiv <- adlaplace::log_lik_laplace(
x = x_outer,
ad_fun = ad_pack,
config = config,
deriv = FALSE
)## Beginning optimization
##
## iter f nrm_gr status radCG iter CG result
## 1 -1544.148145 10.761614 Continuing - TR expand 2.000000 50 Exceeded max CG iters
## 2 -1544.281934 9.892267 Continuing 2.000000 50 Exceeded max CG iters
## 3 -1544.300928 1.776112 Continuing 2.000000 50 Exceeded max CG iters
## 4 -1544.304080 1.751663 Continuing 2.000000 50 Exceeded max CG iters
## 5 -1544.304701 0.386926 Continuing 2.000000 50 Exceeded max CG iters
## 6 -1544.304829 0.372286 Continuing 2.000000 50 Exceeded max CG iters
## 7 -1544.304856 0.086241 Continuing 2.000000 50 Exceeded max CG iters
## 8 -1544.304862 0.081523 Continuing 2.000000 50 Exceeded max CG iters
## 9 -1544.304863 0.018936 Continuing 2.000000 50 Exceeded max CG iters
## 10 -1544.304864 0.017943 Continuing 2.000000 50 Exceeded max CG iters
##
## iter f nrm_gr status radCG iter CG result
## 11 -1544.304864 0.004168 Continuing 2.000000 50 Exceeded max CG iters
## 12 -1544.304864 0.003951 Continuing 2.000000 50 Exceeded max CG iters
## 13 -1544.304864 0.000916 Continuing 2.000000 50 Exceeded max CG iters
## 14 -1544.304864 0.000868 Continuing 2.000000 50 Exceeded max CG iters
## 15 -1544.304864 0.000201 Continuing 2.000000 50 Exceeded max CG iters
## 16 -1544.304864 0.000190 Continuing 2.000000 50 Exceeded max CG iters
## 17 -1544.304864 0.000190 Continuing - TR contract 1.000000 50 Exceeded max CG iters
## 18 -1544.304864 0.000190 Continuing - TR contract 0.500000 50 Exceeded max CG iters
## 19 -1544.304864 0.000190 Continuing - TR contract 0.250000 50 Exceeded max CG iters
## 20 -1544.304864 0.000190 Continuing - TR contract 0.125000 50 Exceeded max CG iters
##
## iter f nrm_gr status radCG iter CG result
## 21 -1544.304864 0.000190 Continuing - TR contract 0.062500 50 Exceeded max CG iters
## 22 -1544.304864 0.000190 Continuing - TR contract 0.031250 50 Exceeded max CG iters
## 23 -1544.304864 0.000190 Continuing - TR contract 0.015625 50 Exceeded max CG iters
## 24 -1544.304864 0.000190 Continuing - TR contract 0.007812 50 Exceeded max CG iters
## 25 -1544.304864 0.000190 Continuing - TR contract 0.003906 50 Exceeded max CG iters
## 26 -1544.304864 0.000190 Continuing - TR contract 0.001953 50 Exceeded max CG iters
## 27 -1544.304864 0.000190 Continuing - TR contract 0.000977 50 Exceeded max CG iters
## 28 -1544.304864 0.000190 Continuing - TR contract 0.000488 50 Exceeded max CG iters
## 29 -1544.304864 0.000190 Continuing - TR contract 0.000244 50 Exceeded max CG iters
## 30 -1544.304864 0.000190 Continuing - TR contract 0.000122 50 Exceeded max CG iters
##
## iter f nrm_gr status radCG iter CG result
## 31 -1544.304864 0.000190 Continuing - TR contract 0.000061 50 Exceeded max CG iters
## 32 -1544.304864 0.000190 Continuing - TR contract 0.000031 50 Exceeded max CG iters
## 33 -1544.304864 0.000190 Continuing - TR contract 0.000015 50 Exceeded max CG iters
## 34 -1544.304864 0.000190 Continuing - TR contract 0.000008 50 Exceeded max CG iters
## 35 -1544.304864 0.000190 Continuing - TR contract 0.000004 50 Exceeded max CG iters
## 36 -1544.304864 0.000190 Continuing - TR contract 0.000002 50 Exceeded max CG iters
## 37 -1544.304864 0.000190 Continuing - TR contract 0.000001 50 Exceeded max CG iters
## 38 -1544.304864 0.000190 Continuing - TR contract 0.000000 50 Exceeded max CG iters
## 39 -1544.304864 0.000190 Continuing - TR contract 0.000000 50 Intersect TR bound
## 40 -1544.304864 0.000190 Continuing - TR contract 0.000000 22 Intersect TR bound
##
## iter f nrm_gr status radCG iter CG result
## 41 -1544.304864 0.000190 Continuing - TR contract 0.000000 12 Intersect TR bound
## 42 -1544.304864 0.000190 Continuing - TR contract 0.000000 7 Intersect TR bound
## 43 -1544.304864 0.000190 Continuing - TR contract 0.000000 4 Intersect TR bound
## 44 -1544.304864 0.000190 Continuing - TR contract 0.000000 3 Intersect TR bound
##
## Iteration has terminated
## 44 -1544.304864 0.000190Radius of trust region is less than stop.trust.radius
res_deriv <- adlaplace::log_lik_laplace(
x = x_outer,
ad_fun = ad_pack,
config = config,
deriv = TRUE
)## Beginning optimization
##
## iter f nrm_gr status radCG iter CG result
## 1 -1544.148145 10.761614 Continuing - TR expand 2.000000 50 Exceeded max CG iters
## 2 -1544.281934 9.892267 Continuing 2.000000 50 Exceeded max CG iters
## 3 -1544.300928 1.776112 Continuing 2.000000 50 Exceeded max CG iters
## 4 -1544.304080 1.751663 Continuing 2.000000 50 Exceeded max CG iters
## 5 -1544.304701 0.386926 Continuing 2.000000 50 Exceeded max CG iters
## 6 -1544.304829 0.372286 Continuing 2.000000 50 Exceeded max CG iters
## 7 -1544.304856 0.086241 Continuing 2.000000 50 Exceeded max CG iters
## 8 -1544.304862 0.081523 Continuing 2.000000 50 Exceeded max CG iters
## 9 -1544.304863 0.018936 Continuing 2.000000 50 Exceeded max CG iters
## 10 -1544.304864 0.017943 Continuing 2.000000 50 Exceeded max CG iters
##
## iter f nrm_gr status radCG iter CG result
## 11 -1544.304864 0.004168 Continuing 2.000000 50 Exceeded max CG iters
## 12 -1544.304864 0.003951 Continuing 2.000000 50 Exceeded max CG iters
## 13 -1544.304864 0.000916 Continuing 2.000000 50 Exceeded max CG iters
## 14 -1544.304864 0.000868 Continuing 2.000000 50 Exceeded max CG iters
## 15 -1544.304864 0.000201 Continuing 2.000000 50 Exceeded max CG iters
## 16 -1544.304864 0.000190 Continuing 2.000000 50 Exceeded max CG iters
## 17 -1544.304864 0.000190 Continuing - TR contract 1.000000 50 Exceeded max CG iters
## 18 -1544.304864 0.000190 Continuing - TR contract 0.500000 50 Exceeded max CG iters
## 19 -1544.304864 0.000190 Continuing - TR contract 0.250000 50 Exceeded max CG iters
## 20 -1544.304864 0.000190 Continuing - TR contract 0.125000 50 Exceeded max CG iters
##
## iter f nrm_gr status radCG iter CG result
## 21 -1544.304864 0.000190 Continuing - TR contract 0.062500 50 Exceeded max CG iters
## 22 -1544.304864 0.000190 Continuing - TR contract 0.031250 50 Exceeded max CG iters
## 23 -1544.304864 0.000190 Continuing - TR contract 0.015625 50 Exceeded max CG iters
## 24 -1544.304864 0.000190 Continuing - TR contract 0.007812 50 Exceeded max CG iters
## 25 -1544.304864 0.000190 Continuing - TR contract 0.003906 50 Exceeded max CG iters
## 26 -1544.304864 0.000190 Continuing - TR contract 0.001953 50 Exceeded max CG iters
## 27 -1544.304864 0.000190 Continuing - TR contract 0.000977 50 Exceeded max CG iters
## 28 -1544.304864 0.000190 Continuing - TR contract 0.000488 50 Exceeded max CG iters
## 29 -1544.304864 0.000190 Continuing - TR contract 0.000244 50 Exceeded max CG iters
## 30 -1544.304864 0.000190 Continuing - TR contract 0.000122 50 Exceeded max CG iters
##
## iter f nrm_gr status radCG iter CG result
## 31 -1544.304864 0.000190 Continuing - TR contract 0.000061 50 Exceeded max CG iters
## 32 -1544.304864 0.000190 Continuing - TR contract 0.000031 50 Exceeded max CG iters
## 33 -1544.304864 0.000190 Continuing - TR contract 0.000015 50 Exceeded max CG iters
## 34 -1544.304864 0.000190 Continuing - TR contract 0.000008 50 Exceeded max CG iters
## 35 -1544.304864 0.000190 Continuing - TR contract 0.000004 50 Exceeded max CG iters
## 36 -1544.304864 0.000190 Continuing - TR contract 0.000002 50 Exceeded max CG iters
## 37 -1544.304864 0.000190 Continuing - TR contract 0.000001 50 Exceeded max CG iters
## 38 -1544.304864 0.000190 Continuing - TR contract 0.000000 50 Exceeded max CG iters
## 39 -1544.304864 0.000190 Continuing - TR contract 0.000000 50 Intersect TR bound
## 40 -1544.304864 0.000190 Continuing - TR contract 0.000000 22 Intersect TR bound
##
## iter f nrm_gr status radCG iter CG result
## 41 -1544.304864 0.000190 Continuing - TR contract 0.000000 12 Intersect TR bound
## 42 -1544.304864 0.000190 Continuing - TR contract 0.000000 7 Intersect TR bound
## 43 -1544.304864 0.000190 Continuing - TR contract 0.000000 4 Intersect TR bound
## 44 -1544.304864 0.000190 Continuing - TR contract 0.000000 3 Intersect TR bound
##
## Iteration has terminated
## 44 -1544.304864 0.000190Radius of trust region is less than stop.trust.radius
data.frame(
log_lik = c(res_noderiv$log_lik, res_deriv$log_lik),
neg_log_lik = c(res_noderiv$neg_log_lik, res_deriv$neg_log_lik),
inner_fval = c(res_deriv$opt$fval, res_deriv$opt$fval),
row.names = c("deriv = FALSE", "deriv = TRUE")
)## log_lik neg_log_lik inner_fval
## deriv = FALSE -1446.652 1446.652 -1544.305
## deriv = TRUE -1446.652 1446.652 -1544.305
## [1] -3.231589 -27.001930
outer_fn() and outer_gr() wrap
log_lik_laplace() for use with
stats::optim().
cache <- new.env(parent = emptyenv())
cache$gamma <- rep(0, n_gamma)
control_inner <- list(
maxit = 100L,
report.level = 0,
report.freq = 0
)
opt <- optim(
par = config$theta,
fn = adlaplace::outer_fn,
gr = adlaplace::outer_gr,
method = "L-BFGS-B",
lower = rep(-15, n_theta),
control = list(maxit = 200L, factr = 1e3, trace = 1, REPORT = 1),
config = config,
ad_fun = ad_pack,
cache = cache,
control_inner = control_inner
)## iter 1 value 1438.546746
## iter 2 value 1435.053755
## iter 3 value 1434.646017
## iter 4 value 1434.383745
## iter 5 value 1423.213290
## iter 6 value 1423.182386
## iter 7 value 1416.787819
## iter 8 value 1403.801739
## iter 9 value 1400.771909
## iter 10 value 1398.424226
## iter 11 value 1397.683740
## iter 12 value 1397.123150
## iter 13 value 1396.812895
## iter 14 value 1396.642743
## iter 15 value 1396.618185
## iter 16 value 1396.507839
## iter 17 value 1396.480000
## iter 18 value 1396.463077
## iter 19 value 1396.456788
## iter 20 value 1396.447679
## iter 21 value 1396.442234
## iter 22 value 1396.439843
## iter 23 value 1396.438560
## iter 24 value 1396.437934
## iter 25 value 1396.437930
## iter 26 value 1396.437616
## iter 27 value 1396.437458
## iter 28 value 1396.437380
## iter 29 value 1396.437341
## iter 30 value 1396.437321
## iter 31 value 1396.437320
## iter 32 value 1396.437310
## iter 33 value 1396.437306
## iter 34 value 1396.437304
## iter 35 value 1396.437303
## iter 36 value 1396.437302
## iter 37 value 1396.437302
## iter 38 value 1396.437302
## iter 39 value 1396.437302
## iter 40 value 1396.437302
## iter 41 value 1396.437302
## iter 42 value 1396.437302
## iter 43 value 1396.437302
## iter 44 value 1396.437302
## iter 45 value 1396.437302
## iter 46 value 1396.437302
## iter 47 value 1396.437302
## final value 1396.437302
## converged
res <- adlaplace::log_lik_laplace(
x = opt$par,
gamma = cache$gamma,
ad_fun = ad_pack,
config = config,
control = control_inner,
deriv = FALSE
)
u_hat <- res$opt$solution
eta_hat <- u_hat[seq_len(n)] + u_hat[seq.int(n + 1L, length.out = n)]
sd_hat <- exp(opt$par)
data.frame(
component = c("SD structured (ICAR)", "SD unstructured (iid)"),
estimate = sd_hat
)## component estimate
## 1 SD structured (ICAR) 2.003913e-01
## 2 SD unstructured (iid) 2.725064e-06
if (!is.null(germany$map)) {
germany$map$eta_hat <- eta_hat
terra::plot(
germany$map, "eta_hat",
main = expression(hat(eta) ~ "(adlaplace mode)")
)
} else {
par(mfrow = c(1, 1), mar = c(4, 4, 2, 1), mgp = c(2.2, 0.7, 0))
plot(
seq_len(n), eta_hat,
type = "h", xlab = "district", ylab = expression(hat(eta)),
main = "Total spatial effect (adlaplace mode; install terra for map)"
)
}## Warning: [is.lonlat] coordinates are out of range for lon/lat
The structured (ICAR) component typically dominates; the unstructured component is often near zero. Results are Laplace-approximate MLEs and conditional modes, not fully Bayesian posterior summaries.