the sequential factorization of the joint · particles and incremental weights · resampling · suspend and resume on a stack machine
2026-06-24
\[ \newcommand\given{{\,\vert\,}} \]
A 1-D random walk seen through noise: a latent state \(X_t\) takes a Gaussian step each tick, and each step emits one noisy reading \(y_t\).
This is a hidden Markov model: the latent states form a Markov chain and each reading depends only on the current state, so the joint factorizes as
\[p(X_{1:T}, y_{1:T}) = p(X_1)\,\prod_{t=2}^{T} p(X_t \given X_{t-1})\;\prod_{t=1}^{T} p(y_t \given X_t).\]
Single-site MH samples it by editing one \(X_t\) of a finished trace and re-running:
\(20{,}000\) edits \(\approx 33\) independent draws. The factors arrive in sequence: what would it take to use that?
Truncate that factorization at step \(t\): the first \(t\) states and readings define an intermediate target, the joint of everything seen so far.
\[\gamma_1\subseteq\gamma_2\subseteq\dots\subseteq\gamma_T=p(Y,X),\qquad \gamma_t(X_{1:t})=p(y_{1:t},\,X_{1:t}).\]
Free structure in the model
The sequential factorization is already there. Single-site MH ignores it; the particle filter is built around it.
Carry \(L\) partial execution particles. Resume them together until the program ends; at each observe:
def smc(program, L, seed=0):
g = np.random.default_rng(seed)
ms = [M(program, np.random.default_rng(int(s))) for s in g.integers(1, 2**62, L)]
while True:
outs = [resume(m) for m in ms] # advance each to its next observe
if outs[0][0] == "done": # programs finished: return final states
return np.array([o[1] for o in outs])
inc = np.array([o[1] for o in outs]) # incremental weights: log p(y_t | X_t)
W = np.exp(inc - inc.max()); W /= W.sum() # normalize
idx = g.choice(L, size=L, p=W) # resample: ancestors proportional to weight
ms = [ms[a].fork(np.random.default_rng(int(s))) # duplicate survivors, fresh rng each
for a, s in zip(idx, g.integers(1, 2**62, L))]The driver uses two operations the language must provide: resume (advance a particle to its next observe, then pause) and m.fork (duplicate a paused particle).
In step 2 a particle has just drawn its new state \(X_t\) from the transition prior and reached the \(t\)-th observe.
What is particle \(\ell\)’s incremental weight \(W_t^{(\ell)}\) there?
A) The full joint \(p(y_1,\dots,y_t, X_{1:t}^{(\ell)})\).
B) Just \(p(y_t \given X_t^{(\ell)})\).
C) The transition prior \(p(X_t \given X_{t-1})\).
D) The product \(\prod_{s\le t} p(y_s \given X_s^{(\ell)})\) over every reading so far.
After a few steps one particle holds almost all the weight and the rest are near zero. A classmate says: “just duplicate the heavy particle \(L\) times to refill the set, then keep going.”
Why does duplication alone not fix the degeneracy?
A) The \(L\) copies are identical: until the next sample makes them diverge (and the weights reset), they still act as one particle.
B) Duplication changes the target, so the estimate becomes biased and cannot be corrected.
C) It does fix it: \(L\) copies of the best particle is exactly the posterior.
D) The copies cannot be stored without re-running each from the start.
Make execution resumable as an explicit stack machine: a control stack C (operations left to do) and a value stack V (results). resume pops C until an observe should pause it; by then the distribution and the observed value are on V.
In your notebook, write the observek case so the machine pauses at the reading and can be resumed later. Hint: pop value and dist, add dist.log_prob(value) to m.log_w, push the value back, then return so the leftover C and V are the rest of the computation.
Reading
Focus while reading
Solutions
sample to diverge (and a weight reset) they act as one particle; B invents an uncorrectable bias, C calls the copies the posterior, D denies that copies can be stored.value and dist, add dist.log_prob(value) to m.log_w, push the value, then return: the leftover C and V are the rest of the computation, so resuming is just calling resume again.Introduction to Probabilistic Programming