simple examples using gamm as alternative to gam taken from help files for mgcv::gamm
# Use packaged data for vignette builds (stable across renders).
# To regenerate locally:
# set.seed(0)
# dat <- mgcv::gamSim(6, n = 500)
# dat$f2 <- sqrt(dat$f2)
# dat$f2 <- dat$f2 - dat$f2[which.min(abs(dat$x2 - 0.5))]
# dat$mu <- exp(-1 + 0 * dat$x1 + dat$f2 +
# rnorm(nlevels(dat$fac), sd = 0.25)[as.integer(dat$fac)])
# nbinom_sd <- 0.25
# dat$Z <- rgamma(nrow(dat), nbinom_sd^(-2), nbinom_sd^(-2))
# dat$y <- rpois(nrow(dat), lambda = dat$mu * dat$Z)
# save(dat, file = "data/gamm.RData")
data("gamm", package = "adlaplace")my_formula <-
adlaplace::nbinom(y, lower = 1e-9, init = 0.15) ~
x1 +
adlaplace::iwp(
x2,
ref_value = 0.5,
p = 2, knots = seq(0, 1, len = 21),
init = 1
) +
adlaplace::iid(fac, init = 0.25)md <- model_data(
data = dat,
formula = my_formula
)
config <- list(
transform_theta = TRUE,
shards = adlaplace::ad_shards(
md$data$A,
num_shards = 100
),
verbose = FALSE
)# 2 threads when this build has OpenMP; otherwise serial.
num_threads <- if (adlaplace::has_openmp()) 2L else 1L
config$num_threads <- num_threads
control_inner <- list(maxit = 100L, report.level = 0, report.freq = 0)
ad_fun <- adlaplace::ad_fun(md, config, num_threads = num_threads)res <- adlaplace::log_lik_laplace(
x = md$data$info$parameters$init,
ad_fun = ad_fun,
config = c(config, list(verbose = FALSE)),
control = control_inner,
deriv = TRUE
)
res$parameters## [1] 0.000000 0.000000 -1.897120 0.000000 -1.386294
## [1] -1108.412
## [1] -25.05191 -68.89198 -24.53787 -73.44516 -71.91246
md$data$info$parameters$lower[is.infinite(md$data$info$parameters$lower)] <- -5
md$data$info$parameters$upper[is.infinite(md$data$info$parameters$upper)] <- 5
cache <- new.env(parent = emptyenv())
cache$gamma <- rep(0, nrow(md$data$info$gamma)) # config$gamma not required; warm-start via cache
x0 <- md$data$info$parameters$init
adlaplace::outer_fn(
x = x0, cache = cache, config = config, ad_fun = ad_fun,
control_inner = control_inner
)## [1] 1108.412
adlaplace::outer_gr(
x = x0, cache = cache, config = config, ad_fun = ad_fun,
control_inner = control_inner
)## [1] -25.05191 -68.89198 -24.53787 -73.44516 -71.91246
outer_fit <- stats::optim(
par = md$data$info$parameters$init,
fn = adlaplace::outer_fn,
gr = adlaplace::outer_gr,
method = "L-BFGS-B",
control = list(
maxit = 200,
trace = 1,
factr = 1e3,
pgtol = 1e-12,
parscale = md$data$info$parameters$parscale
),
lower = md$data$info$parameters$lower,
upper = md$data$info$parameters$upper,
config = config,
ad_fun = ad_fun,
cache = cache,
control_inner = control_inner
)## iter 10 value 914.648624
## final value 914.512965
## converged
## [1] -0.0790839 0.8552411 -1.3604535 3.1965142 -1.9923480
outer_fit$summary <- format_parameters(
parameters = outer_fit$par, gamma = cache$gamma,
info = ad_fun@info
)
outer_fit$summary$parameters## term model label transform mle
## 1 x1 linear x1_linear FALSE -0.0790839
## 2 intercept intercept intercept FALSE 0.8552411
## 3 y nbinom y_nbinom_sd TRUE 0.2565444
## 4 x2 iwp x2_iwp TRUE 24.4471627
## 5 fac iid fac_iid TRUE 0.1363748
## [1] 914.513
## [1] 1.878867e-05 2.699211e-05 -1.901190e-05 1.562455e-05 -5.891609e-05
shards <- seq.int(from = 0, length.out = adlaplace:::n_groups(ad_fun@ptr))
xx <- c(
md$data$info$beta$init,
rep(0, nrow(md$data$info$gamma)),
adlaplace::apply_theta_log(md$data$info$theta, cols = "init")$init
)
by_shard <- mapply(
adlaplace::joint_log_dens,
shards = shards,
MoreArgs = list(x = xx, ad_fun = ad_fun, negative = FALSE),
SIMPLIFY = TRUE
)
by_shard[seq(to = length(by_shard), length.out = 5)]## [1] -943.521282 -863.374980 -48.336093 1.869423 88685.963409
Finite-difference checks along one outer coordinate validate the profile gradient, the determinant derivative, and the chain rule through the inner mode \(u(\beta, \theta)\).
par(mfrow = c(3, 2), mar = c(2, 2, 2, 0), mgp = c(1, 0.5, 0))
x1 <- outer_fit$par
Dpar <- 4 # length(x1)
Ngrid <- 7L
n_beta <- nrow(md$data$info$beta)
Sx <- x1[Dpar] + 0.5 * seq(-1, 1, length.out = Ngrid)
SxD <- Sx[-1] - diff(Sx) / 2
par_grid <- matrix(x1,
nrow = Ngrid, ncol = nrow(md$data$info$parameters),
byrow = TRUE
)
par_grid[, Dpar] <- Sx
scan_label <- md$data$info$parameters$label[Dpar]
res_scan <- lapply(
split(par_grid, row(par_grid)),
adlaplace::log_lik_laplace,
ad_fun = ad_fun,
config = config,
control = control_inner,
deriv = TRUE,
gamma = cache$gamma
)
SnegLik <- vapply(res_scan, `[[`, numeric(1), "neg_log_lik")
Sdet <- vapply(res_scan, function(r) r$extra$hessian$half_log_det, numeric(1))
grad_mat <- do.call(rbind, lapply(res_scan, `[[`, "grad"))
extra_df <- do.call(abind::abind, c(lapply(res_scan, `[[`, "deriv"), along = 3))
dU <- do.call(
abind::abind,
c(lapply(res_scan, function(r) as.matrix(r$extra$dU)), along = 3)
)
u_hat <- do.call(rbind, lapply(res_scan, function(r) r$opt$solution))
res_mid <- res_scan[[(Ngrid + 1L) %/% 2L]]
plot(Sx, SnegLik, xlab = scan_label, ylab = "-log lik")
plot(Sx, grad_mat[, Dpar], type = "l", xlab = scan_label, ylab = "grad")
points(SxD, diff(SnegLik) / diff(Sx), pch = 16)
legend("topright", legend = c("AD", "finite diff"), lty = c(1, NA), pch = c(NA, 1), bty = "n")
Du <- min(2L, ncol(u_hat))
plot(Sx, u_hat[, Du], type = "l", xlab = scan_label, ylab = paste0("u[", Du, "]"))
plot(
Sx, dU[Du, Dpar, ],
type = "l",
xlab = scan_label,
ylab = paste0("du[", Du, "]/d", scan_label)
)
points(SxD, diff(u_hat[, Du]) / diff(Sx), pch = 16)
plot(Sx, Sdet, xlab = scan_label, ylab = "half log det")
plot(
Sx, extra_df[Dpar, "d_det", ],
type = "l",
xlab = scan_label,
ylab = paste0("d log det / d", scan_label)
)
points(SxD, diff(Sdet) / diff(Sx), pch = 16)hessian_outer <- Matrix::forceSymmetric(res_mid$extra$hessian$outer)
hessian_ad <- adlaplace::hessian(
ad_fun,
res_mid$full_parameters,
inner = FALSE,
negative = TRUE
)
max(abs((hessian_outer - hessian_ad)@x), na.rm = TRUE)## [1] 2.16005e-11
The same finite-difference idea applies to the full joint log density \(-\log p(\beta, \gamma, \theta \mid y)\) and its AD gradient and Hessian.
par(mfrow = c(3, 2), mar = c(2, 2, 2, 0), mgp = c(1, 0.5, 0))
x_here <- c(
md$data$info$beta$init,
0.1 + rep(0, nrow(md$data$info$gamma)),
adlaplace::apply_theta_log(md$data$info$theta, cols = "init")$init
)
Dpar_dens <- 1 # length(x_here)
Ngrid <- 11L
shards <- seq.int(from = 0L, length.out = adlaplace:::n_groups(ad_fun@ptr))
par_grid <- matrix(x_here, nrow = Ngrid, ncol = length(x_here), byrow = TRUE)
Sx <- x_here[Dpar_dens] + 1 * seq(-1, 1, length.out = Ngrid)
SxD <- Sx[-1] - diff(Sx) / 2
par_grid[, Dpar_dens] <- Sx
x_list <- split(par_grid, row(par_grid))
dens <- mapply(
adlaplace::joint_log_dens,
x = x_list,
MoreArgs = list(ad_fun = ad_fun, negative = FALSE)
)
grad <- do.call(
cbind,
lapply(
x_list,
adlaplace::grad,
ad_fun = ad_fun,
inner = FALSE,
shards = shards,
negative = FALSE
)
)
plot(Sx, dens)
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,
x_list[[i]],
inner = FALSE,
shards = shards,
negative = FALSE
))
}
grad_slope <- apply(grad, 1, diff) / mean(diff(Sx))
for (Dpar2 in c(Dpar_dens, 1L, length(x_here) - 2L, length(x_here) - 1L)) {
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)
}res <- adlaplace::log_lik_laplace(
x = outer_fit$par,
gamma = cache$gamma,
ad_fun = ad_fun,
config = config,
control = control_inner,
deriv = FALSE
)
newx <- data.frame(x2 = seq(0, 1, len = 101))
sim <- adlaplace::sim_fit(
x = newx,
data = md,
fit = res,
n = 500L
)
matplot(newx$x2, sim, type = "l", col = "#00000010", lty = 1)
points(dat$x2, dat$f2, col = "red", cex = 0.1)