Skip to content

Commit 048f728

Browse files
authored
Adam scale adapter implementation (#132)
* Add Adam-based scale adapter (#130) * Add tests for adam_scale_adapter * Update namespace and manual files associated with adam_scale_adapter * Clarify scale_adapter() '...' documentation Fix inaccurate docstring that the wrapper only exposes learning_rate for the Adam adapter. In fact, '...' passes through to the underlying adapter constructor, so any of its arguments can be set via the wrapper. Addresses review comment on #132. * Update adaptation.R to correct the '...' documentation for scale_adapter, and update the manual file
1 parent 65b86ab commit 048f728

5 files changed

Lines changed: 313 additions & 6 deletions

File tree

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
export(adam_scale_adapter)
34
export(barker_proposal)
45
export(bimodal_barker_proposal)
56
export(chain_state)

R/adaptation.R

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@
33
#' @param algorithm String specifying algorithm to use. One of:
44
#' * "stochastic_approximation" to use a Robbins-Monro (1951) based scheme,
55
#' * "dual_averaging" to use dual-averaging scheme of Nesterov (2009).
6+
#' * "adam" to use the Adam optimizer of Kingma and Ba (2014) applied to the
7+
#' acceptance-rate residual, following the implementation in the `walnuts`
8+
#' library.
69
#' @param initial_scale Initial value to use for scale parameter. If not set
710
#' explicitly a proposal and dimension dependent default will be used.
811
#' @param target_accept_prob Target value for average accept probability for
912
#' chain. If not set a proposal dependent default will be used.
10-
#' @param ... Any additional algorithmic parameters to pass through to
11-
#' [dual_averaging_scale_adapter()] or [stochastic_approximation_scale_adapter()].
13+
#' @param ... Any additional algorithmic parameters to pass through to the
14+
#' selected adapter constructor: see [dual_averaging_scale_adapter()],
15+
#' [stochastic_approximation_scale_adapter()] or [adam_scale_adapter()] for
16+
#' the full list of parameters accepted by each. In practice, most users
17+
#' tuning the Adam adapter only need to adjust `learning_rate`; the
18+
#' moment-decay parameters `beta_1`, `beta_2`, `epsilon` and
19+
#' `learn_rate_decay` have sensible defaults that rarely need adjustment.
1220
#'
1321
#' @return List of functions with entries
1422
#' * `initialize`, a function for initializing adapter state and proposal
@@ -21,12 +29,15 @@
2129
#' * `state` a zero-argument function for accessing current values of adapter
2230
#' state variables.
2331
#'
24-
#' @seealso [dual_averaging_scale_adapter()], [stochastic_approximation_scale_adapter()]
32+
#' @seealso [dual_averaging_scale_adapter()],
33+
#' [stochastic_approximation_scale_adapter()], [adam_scale_adapter()]
2534
#'
2635
#' @references Nesterov, Y. (2009). Primal-dual subgradient methods for convex
2736
#' problems. _Mathematical Programming_, 120(1), 221-259.
2837
#' @references Robbins, H., & Monro, S. (1951). A stochastic approximation
2938
#' method. _The Annals of Mathematical Statistics_, 400-407.
39+
#' @references Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic
40+
#' optimization. _arXiv preprint_ arXiv:1412.6980.
3041
#'
3142
#' @export
3243
#'
@@ -43,6 +54,7 @@ scale_adapter <- function(
4354
adapter_function <- switch(algorithm,
4455
dual_averaging = dual_averaging_scale_adapter,
4556
stochastic_approximation = stochastic_approximation_scale_adapter,
57+
adam = adam_scale_adapter,
4658
stop(sprintf("Unrecognized algorithm choice %s", algorithm))
4759
)
4860
adapter_function(initial_scale, target_accept_prob, ...)
@@ -187,6 +199,116 @@ dual_averaging_scale_adapter <- function(
187199
)
188200
}
189201

202+
#' Create object to adapt proposal scale to coerce average acceptance rate
203+
#' using the Adam optimizer of Kingma and Ba (2014).
204+
#'
205+
#' Applies the Adam stochastic-gradient optimizer to the log of the proposal
206+
#' scale, using the acceptance-rate residual `target_accept_prob - accept_prob`
207+
#' as the (stochastic) gradient. This corresponds to treating
208+
#' `-0.5 * (accept_prob - target_accept_prob)^2` as the objective and is the
209+
#' same gradient signal used by [dual_averaging_scale_adapter()] and
210+
#' [stochastic_approximation_scale_adapter()], but plugged into the Adam
211+
#' update rule instead of a dual-averaging or Robbins-Monro schedule. Follows
212+
#' the reference implementation in the `walnuts` library (see references
213+
#' below).
214+
#'
215+
#' To match the stability provided by dual averaging, a learning-rate decay
216+
#' `learning_rate / t^learn_rate_decay` is applied to the per-iteration Adam
217+
#' step; a value of `learn_rate_decay = 0` recovers standard Adam, while
218+
#' `learn_rate_decay = 0.5` (the default here) is recommended by the `walnuts`
219+
#' authors for stable convergence.
220+
#'
221+
#' @inherit scale_adapter params return
222+
#'
223+
#' @param learning_rate Learning rate for Adam optimizer (the `alpha`
224+
#' hyperparameter in the original Adam paper). Controls the magnitude of the
225+
#' update applied to the log-scale on each iteration. Should be positive.
226+
#' Defaults to `0.05`, which is a practical setting that gives fast
227+
#' convergence of the acceptance-rate coercion within typical MCMC warm-up
228+
#' lengths; the original Adam paper uses `1e-3`.
229+
#' @param beta_1 Exponential decay rate for the first-moment estimate of the
230+
#' gradient (the `beta_1` hyperparameter in the original Adam paper). Should
231+
#' be in `[0, 1)`. Defaults to `0.9`.
232+
#' @param beta_2 Exponential decay rate for the second-moment (squared
233+
#' gradient) estimate (the `beta_2` hyperparameter in the original Adam
234+
#' paper). Should be in `[0, 1)`. Defaults to `0.999`.
235+
#' @param epsilon Small positive constant added to the square root of the
236+
#' second-moment estimate in the denominator of the Adam update for
237+
#' numerical stability. Defaults to `1e-8`.
238+
#' @param learn_rate_decay Exponent controlling the decay of the effective
239+
#' learning rate across iterations: on iteration `t` the effective learning
240+
#' rate is `learning_rate / t^learn_rate_decay`. Should be in `[0, 1]`. A
241+
#' value of `0` recovers standard Adam; the default of `0.5` matches the
242+
#' value recommended in the `walnuts` implementation.
243+
#'
244+
#' @references Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic
245+
#' optimization. _arXiv preprint_ arXiv:1412.6980.
246+
#' @references Reference implementation in `walnuts`:
247+
#' <https://github.com/flatironinstitute/walnuts/blob/main/include/walnuts/adam.hpp>
248+
#'
249+
#' @export
250+
#'
251+
#' @examples
252+
#' proposal <- barker_proposal()
253+
#' adapter <- adam_scale_adapter(
254+
#' initial_scale = 1., target_accept_prob = 0.4
255+
#' )
256+
#' adapter$initialize(proposal, chain_state(c(0, 0)))
257+
adam_scale_adapter <- function(
258+
initial_scale = NULL,
259+
target_accept_prob = NULL,
260+
learning_rate = 0.05,
261+
beta_1 = 0.9,
262+
beta_2 = 0.999,
263+
epsilon = 1e-8,
264+
learn_rate_decay = 0.5
265+
) {
266+
log_scale <- NULL
267+
m <- 0
268+
v <- 0
269+
beta_1_pow <- 1
270+
beta_2_pow <- 1
271+
initialize <- function(proposal, initial_state) {
272+
if (is.null(initial_scale)) {
273+
initial_scale <- proposal$default_initial_scale(initial_state$dimension())
274+
}
275+
log_scale <<- log(initial_scale)
276+
m <<- 0
277+
v <<- 0
278+
beta_1_pow <<- 1
279+
beta_2_pow <<- 1
280+
proposal$update(scale = initial_scale)
281+
}
282+
update <- function(proposal, sample_index, state_and_statistics) {
283+
if (is.null(target_accept_prob)) {
284+
target_accept_prob <- proposal$default_target_accept_prob()
285+
}
286+
accept_prob <- state_and_statistics$statistics$accept_prob
287+
# Gradient of the squared-error objective in log-scale space. Note the
288+
# sign: `grad = target - observed` combined with the `log_scale -= ...`
289+
# update below reproduces the "observed > target => scale increases"
290+
# behaviour of the other scale adapters.
291+
grad <- target_accept_prob - accept_prob
292+
beta_1_pow <<- beta_1_pow * beta_1
293+
beta_2_pow <<- beta_2_pow * beta_2
294+
m <<- beta_1 * m + (1 - beta_1) * grad
295+
v <<- beta_2 * v + (1 - beta_2) * grad^2
296+
m_hat <- m / (1 - beta_1_pow)
297+
v_hat <- v / (1 - beta_2_pow)
298+
effective_learning_rate <- learning_rate / sample_index^learn_rate_decay
299+
log_scale <<- log_scale - (
300+
effective_learning_rate * m_hat / (sqrt(v_hat) + epsilon)
301+
)
302+
proposal$update(scale = exp(log_scale))
303+
}
304+
list(
305+
initialize = initialize,
306+
update = update,
307+
finalize = NULL,
308+
state = function() list(log_scale = log_scale, m = m, v = v)
309+
)
310+
}
311+
190312
#' Create object to adapt proposal shape.
191313
#'
192314
#' @param type Type of shape adapter to use. One of:

man/adam_scale_adapter.Rd

Lines changed: 95 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/scale_adapter.Rd

Lines changed: 15 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-adaptation.R

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,83 @@ for (dimension in c(1L, 2L, 5L)) {
210210
)
211211
}
212212

213+
check_adam_scale_adapter_state <- function(adapter_state) {
214+
expect_named(adapter_state, c("log_scale", "m", "v"), ignore.order = TRUE)
215+
expect_length(adapter_state$log_scale, 1)
216+
expect_length(adapter_state$m, 1)
217+
expect_length(adapter_state$v, 1)
218+
}
219+
220+
for (target_accept_prob in c(0.2, 0.4, 0.6)) {
221+
for (initial_scale in c(0.5, 1., 2.)) {
222+
for (learning_rate in c(0.05, 0.1)) {
223+
test_that(
224+
sprintf(
225+
paste0(
226+
"Adam scale adapter works with target_accept_prob %.1f ",
227+
"initial_scale %.1f learning_rate %.2f"
228+
),
229+
target_accept_prob, initial_scale, learning_rate
230+
),
231+
{
232+
adapter <- scale_adapter(
233+
algorithm = "adam",
234+
initial_scale = initial_scale,
235+
target_accept_prob = target_accept_prob,
236+
learning_rate = learning_rate
237+
)
238+
check_adapter(adapter)
239+
proposal <- dummy_proposal_with_scale_parameter()
240+
adapter$initialize(proposal, chain_state(rep(0, dimension)))
241+
adapter_state <- adapter$state()
242+
check_adam_scale_adapter_state(adapter_state)
243+
# After a fresh initialisation, one update with accept_prob higher
244+
# than target should push the scale up (m_hat = grad, v_hat = grad^2
245+
# at t = 1, so the update is deterministic in sign).
246+
adapter$update(
247+
proposal,
248+
1,
249+
list(statistics = list(accept_prob = target_accept_prob + 0.1))
250+
)
251+
expect_type(adapter$state(), "list")
252+
expect_gt(proposal$parameters()$scale, initial_scale)
253+
# After re-initialisation, one update with accept_prob lower than
254+
# target should push the scale down.
255+
adapter$initialize(proposal, chain_state(rep(0, dimension)))
256+
adapter$update(
257+
proposal,
258+
1,
259+
list(statistics = list(accept_prob = target_accept_prob - 0.1))
260+
)
261+
expect_lt(proposal$parameters()$scale, initial_scale)
262+
# Long-run convergence: reuse shared helper. Re-initialise so the
263+
# convergence run starts from a clean adapter state.
264+
adapter$initialize(proposal, chain_state(rep(0, dimension)))
265+
check_scale_adapter_coerces_to_target_accept_prob(
266+
adapter, proposal, target_accept_prob, initial_scale
267+
)
268+
}
269+
)
270+
}
271+
}
272+
}
273+
274+
for (dimension in c(1L, 2L, 5L)) {
275+
test_that(
276+
sprintf(
277+
"Adam scale adapter with default args works in dimension %i",
278+
dimension
279+
),
280+
{
281+
check_scale_adapter_with_default_args_works(
282+
scale_adapter(algorithm = "adam"),
283+
dimension,
284+
check_adam_scale_adapter_state
285+
)
286+
}
287+
)
288+
}
289+
213290
for (dimension in c(1L, 2L, 5L)) {
214291
for (kappa in c(0.5, 0.6, 0.8)) {
215292
for (correlation in c(0.0, 0.5, 0.8)) {

0 commit comments

Comments
 (0)