This vignette demonstrates how Lanczos methods can be used to approximate standard errors and epsilon bias-correction for penalized likelihood models, without directly forming or inverting the Hessian matrix representing associations among random effects.
library(lanczosRTMB)
#> Loading required package: RTMB
library(RTMB)Simulate and fit a generalized linear mixed model
We first simulate data from a compound lognormal-Gamma process:
set.seed(123)
n = 30
n_sum = 3
# log-linked normal deviates
u = 0 + rnorm(n)
# Conditional gamma process
y = rgamma( n, shape = 1/0.5^2, scale = exp(u) * 0.5^2 )We then define a function to compute the joint likelihood, while also allowing us to calculate other outputs for later use:
nll = function(p){
sumexpu = sum(exp(p$u[seq_len(n_sum)]))
ADREPORT( sumexpu )
REPORT( sumexpu )
nll1 = dnorm(p$u, mean=p$mu, sd=exp(p$logsd), log=TRUE)
nll2 = dgamma(
y,
shape = 1/exp(2*p$logcv),
scale = exp(p$u) * exp(2*p$logcv),
log=TRUE
)
jnll = -1 * ( sum(nll1) + sum(nll2) )
if(what == "jnll") return(jnll)
if(what == "sumexpu") return(sumexpu)
if(what == "biascorr") return(jnll + p$eps * sumexpu)
}Finally, we fit this model as a log-linked generalized linear mixed model (GLMM):
# Build RTMB object
what = "jnll"
obj = RTMB::MakeADFun(
nll,
list(u=u, mu = 0, logsd = 0, logcv = 0, eps = 0),
map = list(eps = factor(NA)),
random = "u",
silent = TRUE
)
# optimize
opt = nlminb( obj$par, obj$fn, obj$gr )
opt
#> $par
#> mu logsd logcv
#> -0.1052225 -0.1326400 -0.5984998
#>
#> $objective
#> [1] 36.46907
#>
#> $convergence
#> [1] 0
#>
#> $iterations
#> [1] 13
#>
#> $evaluations
#> function gradient
#> 17 14
#>
#> $message
#> [1] "relative convergence (4)"Fit as a penalized likelihood model
Next, we show that the model can be refitted using penalized likelihood, conditional upon fixed values for variance parameters:
# Define RTMB object for penalized likelihood
newmap = list(
mu = factor(NA),
logsd = factor(NA),
logcv = factor(NA),
eps = factor(NA)
)
pen = RTMB::MakeADFun(
nll,
obj$env$parList(),
map = newmap,
silent = TRUE
)
# Re-optimize
opt_pen = nlminb( pen$par, pen$fn, pen$gr )We can then use stochastic trace estimation and the Lanczos method to approximate the log-determinant of the Hessian for random effects:
# log-determinant using Lanczos
Hq = make_Hq( GetTape(pen), opt_pen$par )
lanczos_logdet( Hq, k = 10, m = 3 )
#> [1] 44.4956 44.4956 44.4956
# log-determinant for marginal likelihood
H = obj$env$spHess(par = obj$env$last.par.best, random = TRUE)
Matrix::determinant( H )$modulus
#> [1] 44.4956
#> attr(,"logarithm")
#> [1] TRUEUsing this log-determinant, we can also compare the log-marginal likelihood using Lanczos with the full calculation:
# Marginal likelihoods using Lanczos
lanczos_nll( pen, k = 10, m = 10 )
#> nll sd_nll
#> 36.46907 0.00000
# Marginal likelihood using Laplace
opt$obj
#> [1] 36.46907Delta methods
Alternatively, we can apply Lanczos methods to standard errors for parameters. Here, we will sample the first three random effects
samples = lanczos_sample(
Hq = Hq,
q = c( rep(1,3), rep(0,n-3) ),
k = 30,
n = 1000,
orthogonalize = TRUE
)
#
sdrep = sdreport( obj, ignore.parm.uncertainty = TRUE )
as.list(sdrep, what = "Std. Error")$u[1:3]
#> [1] 0.4839019 0.4864751 0.4066186
apply( samples, MARGIN = 1, FUN = sd)[1:3]
#> [1] 0.4836432 0.4823133 0.4087638Alternatively, we can apply Lanczos methods to approximate the Hessian with respect to a derived quantity. This involves calculating the gradient for the derived quantity with respect to coefficients:
what = "sumexpu"
grad = RTMB::MakeADFun(
nll,
obj$env$parList(),
map = newmap,
silent = TRUE
)$gr( pen$par )[1,]We can then use this gradient to approximate samples from the Hessian matrix and use that to calculate a Monte Carlo estimator for standard errors:
samples = lanczos_sample(
Hq = Hq,
q = grad,
k = 30,
n = 1000,
orthogonalize = TRUE
)
# Compare SD of samples with the delta method
sumexpu1 = apply( samples, MARGIN = 2, FUN = \(v)pen$report(v)$sumexpu )
c( pen$report()$sumexpu, sd(sumexpu1) )
#> [1] 4.064121 0.974645
summary(sdrep)['sumexpu',]
#> Estimate Std. Error
#> 4.064121 1.194484Alternatively, we can calculate the standard error for a derived quantity using Lanczos within the delta method:
#
Var = lanczos_variance(
Hq = Hq,
q = grad,
k = 3
)
# Compare with the delta method
c( pen$report()$sumexpu, sqrt(Var) )
#> [1] 4.064121 1.194484
summary(sdrep)['sumexpu',]
#> Estimate Std. Error
#> 4.064121 1.194484Epsilon methods
Finally, we can use the gradient of the log-likelihood with respect to a dummy variable epsilon to correct for retransformation bias:
# One-sided finite difference
what = "biascorr"
phat = obj$env$parList()
phat$eps = 0.0001
pen_hi = RTMB::MakeADFun(
nll,
parameters = phat,
map = newmap,
silent = TRUE
)
# Re-optimize
opt_hi = optim(
pen_hi$par, pen_hi$fn, pen_hi$gr,
method = "L-BFGS-B", control = list(factr = 1e-2)
)
# Calculate marginal nll for finite-difference
nll_hi = lanczos_nll( pen_hi, k = 10, m = 10, seed = 123 )
nll_mid = lanczos_nll( pen, k = 10, m = 10, seed = 123 )
# Epsilon bias-correction estimator
sdrep = sdreport( obj, bias.correct = TRUE )
#
(nll_hi['nll'] - nll_mid['nll']) / (phat$eps)
#> nll
#> 4.733928
summary(sdrep)['sumexpu',]
#> Estimate Std. Error Est. (bias.correct) Std. (bias.correct)
#> 4.064121 1.625808 4.734025 NARuntime for this vignette: 2.94 secs