Using another Optimizer

In this document we show how to use the likelihood method to obtain function handlers for the objective function, and gradient, (and hessian if there are no random efects in your model formulation). These handlers can then be parsed to an optimization algorithm of your own choice.

Simulate from the Ornstein-Uhlenbeck process


We use the common Ornstein-Uhlenbeck process to showcase the use of likelihood.

dXt = θ(μ − Xt) dt  + σX dBt

Ytk = Xtk + etk,   etk ∼ 𝒩(0, σY2) We first create data by simulating the process

# Simulate data using Euler Maruyama
set.seed(10)
theta=10; mu=1; sigma_x=1; sigma_y=1e-1
# 
dt.sim = 1e-3
t.sim = seq(0,1,by=dt.sim)
dw = rnorm(length(t.sim)-1,sd=sqrt(dt.sim))
x = 3
for(i in 1:(length(t.sim)-1)) {
  x[i+1] = x[i] + theta*(mu-x[i])*dt.sim + sigma_x*dw[i]
}

# Extract observations and add noise
dt.obs = 1e-2
t.obs = seq(0,1,by=dt.obs)
y = x[t.sim %in% t.obs] + sigma_y * rnorm(length(t.obs))

# Create data
.data = data.frame(
  t = t.obs,
  y = y
)

Construct model object


We now construct the ctsmTMB model object

# Create model object
obj = ctsmTMB$new()

# Set name of model (and the created .cpp file)
obj$setModelname("ornstein_uhlenbeck")

# Add system equations
obj$addSystem(
  dx ~ theta * (mu-x) * dt + sigma_x*dw
)

# Add observation equations
obj$addObs(
  y ~ x
)

# Set observation equation variances
obj$setVariance(
  y ~ sigma_y^2
)

# Specify algebraic relations
obj$setAlgebraics(
  theta ~ exp(logtheta),
  sigma_x ~ exp(logsigma_x),
  sigma_y ~ exp(logsigma_y)
)

# Specify parameter initial values and lower/upper bounds in estimation
obj$setParameter(
  logtheta   = log(c(initial = 5,    lower = 0,    upper = 20)),
  mu         = c(    initial = 0,    lower = -10,  upper = 10),
  logsigma_x = log(c(initial = 1e-1, lower = 1e-5, upper = 5)),
  logsigma_y = log(c(initial = 1e-1, lower = 1e-5, upper = 5))
)

# Set initial state mean and covariance
obj$setInitialState(list(x[1], 1e-1*diag(1)))

Estimation


We are in principle ready to call the estimate method to run the optimization scheme using the built-in optimization which uses stats::nlminb i.e.

fit = obj$estimate(.data)
## Building model...
## Compiling model...
## Checking data...
## Constructing objective function and derivative tables...
## ...took: 0.301 seconds.
## Minimizing the negative log-likelihood...
##   0:     921.77031:  1.60944  0.00000 -2.30259 -2.30259
##   1:     86.857795:  1.06416 0.614170 -1.83281 -1.97888
##   2:     31.849960:  1.43157 0.566368 -1.48426 -1.11793
##   3:     29.255774:  1.22062  1.42236 -1.18700 -1.48456
##   4:     2.6332703:  1.21162  1.06391 -0.839196 -1.50616
##   5:    -28.360004:  1.69842 0.789400 0.678990 -2.68208
##   6:    -31.783751:  1.72217 0.792252 0.505927 -2.70162
##   7:    -32.935798:  1.82814 0.794076 0.369331 -2.66980
##   8:    -33.931559:  1.93816 0.827823 0.404381 -2.54161
##   9:    -36.374390:  2.30299 0.846084 0.271620 -2.41241
##  10:    -37.306903:  2.33540 0.953594 0.250389 -2.39051
##  11:    -38.025359:  2.41802  1.01738 0.207467 -2.36220
##  12:    -38.962428:  2.84401  1.08562 0.118876 -2.21170
##  13:    -39.245448:  2.75222  1.09405 0.199654 -2.35933
##  14:    -39.316533:  2.76256  1.06843 0.205989 -2.34516
##  15:    -39.332674:  2.78162  1.08468 0.204880 -2.32578
##  16:    -39.343406:  2.81190  1.08012 0.197046 -2.32803
##  17:    -39.346207:  2.80311  1.07595 0.205052 -2.32692
##  18:    -39.346631:  2.80231  1.07756 0.202408 -2.32695
##  19:    -39.346636:  2.80291  1.07748 0.202473 -2.32692
##  20:    -39.346636:  2.80285  1.07748 0.202482 -2.32692
##  21:    -39.346636:  2.80285  1.07748 0.202481 -2.32692
##   Optimization finished!:
##             Elapsed time: 0.025 seconds.
##             The objective value is: -3.934664e+01
##             The maximum gradient component is: 7.2e-07
##             The convergence message is: relative convergence (4)
##             Iterations: 21
##             Evaluations: Fun: 29 Grad: 22
##             See stats::nlminb for available tolerance/control arguments.
## Returning results...
## Finished!

Inside the package we optimise the objective function with respect to the fixed parameters using the construction function handlers from TMB::MakeADFun and parsing them to stats::nlminb i.e.

nll = TMB::MakeADFun(...)
opt = stats::nlminb(start=nll$par, objective=nll$fn, grad=nll$gr, hessian=nll$he)

Extract function handlers


The likelihood method allows you to retrieve the nll object that holds the negative log-likelihood, and its derivatives. The method takes arguments similar to those of estimate.

nll = obj$likelihood(.data)
## Building model...
## Compiling model...
## Checking data...
## Constructing objective function...
## ...took: 0.3 seconds.
## Succesfully returned function handlers

The initial parameters (supplied by the user) are stored here

nll$par
##   logtheta         mu logsigma_x logsigma_y 
##   1.609438   0.000000  -2.302585  -2.302585

The objective function can be evaluted by

nll$fn(nll$par)
## [1] 921.7703

The gradient can be evaluted by

nll$gr(nll$par)
##          [,1]      [,2]      [,3]      [,4]
## [1,] 1382.854 -1557.574 -1191.374 -820.9253

The hessian can be evaluted by

nll$he(nll$par)
##           [,1]       [,2]       [,3]       [,4]
## [1,]  2226.705 -2859.2069 -1636.0560 -1136.3013
## [2,] -2859.207  1663.1457  2251.5926   865.5807
## [3,] -1636.056  2251.5926   905.1284  1486.3607
## [4,] -1136.301   865.5807  1486.3607   346.7711

We can now use these to optimize the function using e.g. stats::optim instead.

Extract parameter lower/upper bounds


You can extract the parameter bounds specified when calling setParameter() method by using the getParameters method (note that nll$par and pars$initial are identical).

pars = obj$getParameters()
print(pars)
##            type   estimate   initial     lower     upper
## logtheta   free  2.8028465  1.609438      -Inf  2.995732
## mu         free  1.0774801  0.000000 -10.00000 10.000000
## logsigma_x free  0.2024813 -2.302585 -11.51293  1.609438
## logsigma_y free -2.3269227 -2.302585 -11.51293  1.609438

Optimize manually using stats::optim

We supply the initial parameter values, objective function handler and gradient handler, and parameter bounds to optim.

opt = stats::optim(par=nll$par, 
                   fn=nll$fn, 
                   gr=nll$gr, 
                   method="L-BFGS-B", 
                   lower=pars$lower, 
                   upper=pars$upper)

Compare results between the two optimizers


Lets compare the results from using stats::optim with the extracted function handler versus the internal optimisation that uses stats::nlminb stored in fit:

# Estimated parameters
data.frame(external=opt$par, internal=fit$par.fixed)
##              external   internal
## logtheta    2.8028428  2.8028465
## mu          1.0774788  1.0774801
## logsigma_x  0.2024796  0.2024813
## logsigma_y -2.3269222 -2.3269227
# Neg. Log-Likelihood
data.frame(external=opt$value, internal=fit$nll)
##    external  internal
## 1 -39.34664 -39.34664
# Gradient components
data.frame(external=t(nll$gr(opt$par)), internal=t(nll$gr(fit$par.fixed)))
##        external      internal
## 1 -5.284383e-05 -7.872731e-08
## 2 -1.491876e-04  5.834973e-07
## 3 -4.592109e-05 -7.193738e-07
## 4 -2.656532e-05 -4.960521e-07