Metropolis-Hastings over Execution Traces

MCMC by editing executions · independent and single-site proposals · addressing · the acceptance ratio

Javier Burroni

2026-06-22

MCMC and Metropolis-Hastings

MCMC builds a Markov chain whose stationary distribution is the posterior \(p(X \given y)\). We estimate any \(\mathbb{E}[r(X)]\) by averaging \(r\) along the chain, using only ratios of the unnormalized target \(p(y, X)\), never the evidence \(p(y)\).

Metropolis-Hastings is one recipe for such a chain. From the current state \(X\):

  1. propose \(X' \sim q(X' \given X)\);
  2. accept (\(X \leftarrow X'\)) with probability \(\min(1, \alpha)\), otherwise keep \(X\), where \[\alpha = \frac{p(X' \given y)\, q(X \given X')}{p(X \given y)\, q(X' \given X)}.\]

The state is an execution trace

In a PPL the chain’s state is the trace \(\mathcal{X}\): the random choices a run made. A proposal re-runs the program. Today we build two of them: independent (a whole fresh run from the prior) and single-site (change one choice, reuse the rest).

Question 1

In the likelihood-weighting activity you measured the ESS with a prior sitting far from the data:

(let [mu (sample (normal 0 1))] (observe (normal mu 1) 8.0) mu)

There run(program, seed) returns (value, log_w) with \(\log W = \log p(y \given X)\) (only observe contributes, since the prior is the proposal).

In the same notebook, implement independent MH and run it here. The chain barely moves: explain why, in terms of what MH does with a proposal that likelihood weighting would merely down-weight.

Independent MH

def independent_mh(program, S=20000, seed=0):
    rng = np.random.default_rng(seed)
    x, logw = run(program, seed=0)        # current run
    chain = []
    accept = 0
    for i in range(S):
        x2, logw2 = run(program, seed=i+1)  # propose a fresh run
        if np.log(rng.random()) < logw2 - logw:                  # accept w.p. min(1, W'/W)
            x, logw = x2, logw2
            accept += 1
        chain.append(x)
    print("acceptance rate:", accept / S)
    return np.array(chain)
  • Proposing from the prior cancels prior against proposal: \(\alpha = p(y \given X')/p(y \given X) = W'/W\), already computed by run.

Single-site MH

An eight bits model

Eight bits: each bit is \(1\) when a fresh uniform(0,1) falls below \(0.5\), else \(0\). We observe their sum near \(7\), a \(\mathcal{N}(7, 1)\) likelihood.

(let [b1 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      b2 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      b3 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      b4 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      b5 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      b6 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      b7 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      b8 (if (< (sample (uniform-continuous 0 1)) 0.5) 1 0)
      total (+ b1 b2 b3 b4 b5 b6 b7 b8)]
  (observe (normal 7 1) total)        ; soft evidence: the sum is near 7
  total)

Why single-site fits

Eight latent choices, one uniform per bit. A whole-run proposal must land all eight at once, and the prior rarely puts the sum near \(7\). Single-site MH instead flips one bit and reuses the other seven, so progress compounds bit by bit.

Next class

Reading

  • Chapter 4.3 (Sequential Monte Carlo)
  • Optional: Chapter 4.4 (Black-box variational inference)

Focus while reading

  • how each observe defines an intermediate target density
  • what a particle carries, and its incremental weight
  • why resampling beats the degeneracy that stalls MH
  • what pausing and resuming a run asks of the evaluator

Plan ahead for Friday

Friday moves to Chapter 5: recursion, higher-order functions, dynamic structure. Start reading now.

Answer key

Solutions

  • Q1: Freeform.