Gibbs sampling: deriving full conditionals for a random-walk smoother
How to get each full conditional distribution from a joint posterior by completing the square, how a Gibbs sampler sweeps through them, and the indexing bug that quietly turns it into something else.
University lab From my coursework in the MSc in Statistics and Machine Learning at Linköping University, rewritten as a note to review later.
In the fourth computational statistics lab we had 50 days of noisy measurements and wanted a smooth curve through them, with uncertainty. The joint posterior over all 50 levels looked messy. But each level on its own, given all the others, turned out to be a plain normal you can sample directly. That’s the setting for Gibbs sampling: cycle through the parameters, drawing each one from its conditional, and the sequence of full vectors is a Markov chain with the joint posterior as its target. Our derivation was right. Our code had a bug that I want to remember.
The model
We observe (one per day) and assume each is a noisy version of an unknown smooth level :
The prior says the level changes slowly, like a random walk:
Here is a known variance (0.2 in the lab, for both noise and steps) and is a flat prior on the starting level.
Posterior, up to a constant
Bayes’ theorem: posterior likelihood prior. All factors are Gaussian, so
The first sum pulls each toward its observation; the second pulls neighbours toward each other. That tug of war is the smoothing.
Full conditionals by “keep only the terms with ”
For the conditional of given everything else, treat all other ‘s as constants and drop every factor that doesn’t involve . For an interior day () three squared terms survive:
A sum of squares equals plus terms without , where is the average of the . (That’s completing the square.) So with :
At the two ends only two terms survive ():
Nice and intuitive: each level is drawn around the average of its own observation and its neighbours, with less spread the more terms pin it down.
The sweep, and the bug
One Gibbs iteration updates in turn, always using the most recent value of every other component. So when updating in iteration , is the value just drawn in iteration , and is the value from iteration .
Our lab code stored the draws in a matrix with one row per iteration, initialised to zeros, and wrote the update for row using mu[t, i-1] and mu[t, i+1]. The first is fine. The second is still 0, because row hasn’t reached column yet, and nothing ever copied the previous row forward. So every iteration restarted from zeros, and the “chain” was really 1000 independent single sweeps from an all-zero start, with the right neighbour always 0. On top of that, the interior mean was divided by 2 instead of 3, which happened to partly compensate (with the right-hand neighbour stuck at 0, dividing by 3 would have pulled every level toward zero).
The fitted curve still looked smooth and plausible, which is the scary part. A trace plot of a single component and a comparison with an independent method (for example the exact posterior mean, which for this Gaussian model is a linear smoother you can compute directly) would have exposed it. The fix is to keep one current state vector, update it in place, and copy it into the output matrix after each full sweep:
gibbs_rw <- function(y, s2, n_iter = 1000) {
n <- length(y)
draws <- matrix(0, n_iter, n)
cur <- y # current state of the chain
for (t in 1:n_iter) {
for (i in 1:n) {
if (i == 1) { m <- (y[1] + cur[2]) / 2; v <- s2 / 2 }
else if (i == n) { m <- (y[n] + cur[n - 1]) / 2; v <- s2 / 2 }
else { m <- (y[i] + cur[i - 1] + cur[i + 1]) / 3; v <- s2 / 3 }
cur[i] <- rnorm(1, m, sqrt(v)) # in place: newest values are used
}
draws[t, ] <- cur # store the state after the sweep
}
draws
}
set.seed(5)
x <- 1:50
y <- 0.3 + 1.2 * (1 - exp(-x / 15)) + rnorm(50, sd = 0.3) # a noisy curve
draws <- gibbs_rw(y, s2 = 0.2)
post_mean <- colMeans(draws[-(1:100), ]) # drop a short burn-in
D <- diff(diag(50)) # first-difference matrix
exact <- solve(diag(50) + crossprod(D), y) # exact posterior mean
max(abs(post_mean - exact)) # about 0.03: just Monte Carlo noise
The exact mean comes from writing the posterior as one multivariate normal: its precision matrix is , where takes differences of neighbours, so its mean is .
Things to watch
- Correlated components mix slowly. Neighbouring ‘s are strongly tied, so one-at-a-time updates take small steps. Block updates (drawing all jointly from its multivariate normal) mix much faster for models like this.
- Burn-in. Drop early iterations if the start was far from the posterior; look at trace plots and autocorrelation of a few components.
- Gibbs is a special case of Metropolis-Hastings where every proposal is accepted, because proposing from the exact conditional makes the acceptance ratio equal to 1. See Metropolis-Hastings.
For Gibbs sampling with hidden labels (data augmentation) in a mixture model, see Gibbs sampling and normal mixtures.
Key takeaways
- Full conditionals come from the joint posterior: keep only factors that involve the parameter, then recognise the distribution (here, by completing the square).
- For this random-walk smoother, each is normal around the average of and its neighbours, with variance for terms.
- Each update must use the latest values of all other components. Storing iterations in rows without carrying the state forward silently breaks the chain.
- A plausible-looking result isn’t proof the sampler is right: check against a known answer when you can.
Resources
- 732A90 Computational Statistics, Linköping University, Lab 4 (2020).
- George Casella and Edward I. George, “Explaining the Gibbs sampler”, The American Statistician 46(3), 1992: https://doi.org/10.1080/00031305.1992.10475878
- Geof H. Givens and Jennifer A. Hoeting, Computational Statistics, 2nd ed., Wiley, 2013, Chapter 7.
- Andrew Gelman et al., Bayesian Data Analysis, 3rd ed., CRC Press, 2013, Chapter 11: http://www.stat.columbia.edu/~gelman/book/