library(brms)
Goal: Explore a joint teaching approach, where I teach students both how to code in R in order to solve a real interesting problem rather than super basic stuff.
We want to sample from a distribution. We know it’s density function, but imagine we don’t have a function that generates random samples. There are many ways to do it, but here we’ll illustrate MCMC methods:
We’ll also take an opportunity to see how we might optimize the code.
Metropolis Monte Carlo
Version 1: Naive implementation
Purposefully bad slow code, but works. We have our standard components:
- a pdf to evaluate (f)
- an initial value
- a proposal distribution
- a fixed number of mcmc samples to target
We iterate with a while loop and append accepted samples to a vector x until the length of the vector matches the number of samples.
<- function(f, init, proposal_dist, samples) {
mcmc_metropolis1 <- init
x while (length(x) < samples) {
<- proposal_dist(1, x[length(x)])
x_ <- f(x[length(x)])
f1 <- f(x_)
f2 if (runif(1) <= f2/f1) {
<- c(x, x_)
x else {
} <- c(x, x[length(x)])
x
}
}
x
}
# TODO: check if can be done with distributional
<- function(mu, kappa) {
dist_vonmises function(x) brms::dvon_mises(x, mu = mu, kappa = kappa)
}
<- function(sd) {
dist_norm function(n, x) rnorm(n, mean = x, sd = sd)
}
<- mcmc_metropolis1(
out f = dist_vonmises(0, 10),
init = 0.5,
proposal_dist = dist_norm(sd = 0.25),
samples = 2000
)
here is our mcmc trace:
plot(out, type = "l")
and the resulting distribution of samples, with the theoretical density on top:
plot(density(out))
curve(dist_vonmises(0, 10)(x), add = TRUE, col = "red")
it runs fast because this is a really simple problem, but we can optimize our function a lot. Here are a few different ways to code the same algorithm:
<- function(f, init, proposal_dist, samples) {
mcmc_metropolis2 <- numeric(samples)
out <- f(init)
f1 for (i in seq_len(samples)) {
<- init
out[i] <- proposal_dist(1, init)
x <- f(x)
f2 if (runif(1) <= f2/f1) {
<- x
init <- f2
f1
}
}
out
}
<- function(f, init, proposal_dist, samples) {
mcmc_metropolis3 <- function(current, ...) {
next_sample <- proposal_dist(1, current)
x_ <- f(current)
f1 <- f(x_)
f2 if (runif(1) <= f2/f1) x_ else current
}Reduce(next_sample, numeric(samples-1), init = init, accumulate = TRUE)
}
<- function(f, init, proposal_dist, samples) {
mcmc_metropolis4 <- function(current, ...) {
next_sample <- proposal_dist(1, current)
x_ <- f(current)
f1 <- f(x_)
f2 if (runif(1) <= f2/f1) x_ else current
}
<- vector("list", samples)
out for (i in seq.int(samples)) {
<- init
out[[i]] <- next_sample(init)
init
}
out }
There are some speed differences among those, but they are all substantially better than the original.
::mark(
benchmcmc_metropolis1(
f = function(x) exp(10 * cos(x)),
init = 0.5,
proposal_dist = dist_norm(sd = 0.25),
samples = 10000
),mcmc_metropolis2(
f = function(x) exp(10 * cos(x)),
init = 0.5,
proposal_dist = dist_norm(sd = 0.25),
samples = 10000
),mcmc_metropolis3(
f = function(x) exp(10 * cos(x)),
init = 0.5,
proposal_dist = dist_norm(sd = 0.25),
samples = 10000
), mcmc_metropolis4(
f = function(x) exp(10 * cos(x)),
init = 0.5,
proposal_dist = dist_norm(sd = 0.25),
samples = 10000
), check = FALSE
)
Warning: Some expressions had a GC in every iteration; so filtering is
disabled.
# A tibble: 4 × 6
expression min median `itr/sec` mem_alloc `gc/sec`
<bch:expr> <bch:t> <bch:t> <dbl> <bch:byt> <dbl>
1 mcmc_metropolis1(f = function(x)… 102.7ms 120.3ms 7.66 382MB 80.4
2 mcmc_metropolis2(f = function(x)… 12.2ms 12.8ms 76.6 122KB 7.86
3 mcmc_metropolis3(f = function(x)… 17.9ms 18.7ms 51.1 349KB 9.83
4 mcmc_metropolis4(f = function(x)… 16.1ms 16.7ms 58.7 129KB 9.79
An alternative to the metropolis algorithm is rejection sampling:
<- function(n, f, max_f, proposal_fun, ...) {
rejection_sampling stopifnot(is.numeric(n), length(n) == 1, n > 0)
stopifnot(is.numeric(max_f), length(max_f) == 1 | length(max_f) == n, max_f > 0)
<- function(n, f, max_f, proposal_fun, ..., acc = c()) {
inner if (length(acc) > n) {
return(acc[seq_len(n)])
}<- proposal_fun(n)
x <- stats::runif(n) * max_f
y <- y < f(x, ...)
accept inner(n, f, max_f, proposal_fun, ..., acc = c(acc, x[accept]))
}
inner(n, f, max_f, proposal_fun, ...)
}