"A thousand particles set out to represent the posterior. By step fifty, nine hundred and ninety-nine were carrying weight zero and one was doing all the work. That one was very tired."
An Overworked AI Agent
The big picture
Section 10.3 built the particle filter: represent the posterior by a cloud of weighted samples, push each through the dynamics, and reweight by how well it explains the measurement. Left to run, that scheme quietly self-destructs. The importance weights spread out over time until nearly every particle holds a negligible share and one lucky sample carries almost all of it. Your thousand-particle filter becomes a one-particle filter that still costs a thousand particles to run. This section explains that failure, called weight degeneracy, gives you the one number that detects it (the effective sample size), and introduces resampling, the correction that rescues the filter by culling low-weight particles and cloning high-weight ones. It then confronts the price of that rescue: resampling trades weight degeneracy for a second disease, sample impoverishment. Threading that needle is the whole art of a healthy particle filter: resample too rarely and one particle inherits the entire posterior, resample too often and every particle inherits the same value.
This section assumes the particle-filter mechanics and importance weights of Section 10.3, plus comfort with sampling, variance, and the categorical distribution from the Chapter 4 primer. We keep the notation of the chapter: a set of \(N\) particles \(\{\mathbf{x}_k^{(i)}\}\) with normalized weights \(\{w_k^{(i)}\}\) summing to one approximates the filtering posterior \(p(\mathbf{x}_k \mid \mathbf{z}_{1:k})\).
Why the weights collapse: the degeneracy problem
What happens. In sequential importance sampling, each new measurement multiplies every particle's weight by a likelihood factor and the set is renormalized. A theorem of Doucet and colleagues makes precise why this always ends badly: the variance of the importance weights can only grow with time, with no steady state where they stay balanced. The mechanism compounds rather than accumulates: the proposal distribution (almost always just the process model) never looks at the new measurement, so each likelihood factor carries genuine variance, and multiplying many such factors behaves like compounding interest, particles with a lucky run of high factors pull ahead multiplicatively, not additively. That is also why degeneracy bites hardest exactly where sensory AI needs particle filters most: high-dimensional states (a multi-joint pose, a full SLAM map) let the proposal drift further from the posterior with every added dimension, and precise sensors sharpen the likelihood enough that only a few particles land near it at all. Given enough steps, the normalized weight of a single particle converges to one and all others to zero.
Why it matters. A degenerate filter still produces a number, so like the mistuned Kalman filter discussed in Chapter 9 it fails silently. Every particle except one contributes nothing to the posterior estimate, yet you still pay the full compute cost of propagating all \(N\) of them through the dynamics and the likelihood at every step. Worse, the surviving particle almost never sits at the true state, so the estimate is both expensive and wrong, and its reported spread collapses to a point that lies to you about your own uncertainty. That miscalibration is the same failure the book returns to in Chapter 18.
Misconception: more particles fixes degeneracy
The instinctive fix for a degenerate filter is to add more particles, and it helps, briefly. It does not fix the underlying problem: the particle count needed to keep weight variance bounded grows exponentially with the state's dimension and with how far the proposal sits from the informative region of the posterior. A ten-dimensional pose-and-map state that degenerates after forty steps with a thousand particles will still degenerate, just a little later, with a hundred thousand, at a hundred times the cost. What scales is a better proposal, adaptive resampling tuned on \(\widehat{N}_{\text{eff}}\), or shrinking the sampled dimensions with the Rao-Blackwellization of Section 10.5, not brute-force particle count.
Measuring the damage: effective sample size
You cannot fix what you cannot detect, and the detector is the effective sample size (ESS), a single scalar that estimates how many of your \(N\) particles are actually pulling their weight. The standard approximation is
$$ \widehat{N}_{\text{eff}} = \frac{1}{\sum_{i=1}^{N} \left(w_k^{(i)}\right)^2}. $$How to read it. When all weights are equal, \(w^{(i)} = 1/N\), the sum of squares is \(1/N\) and \(\widehat{N}_{\text{eff}} = N\): every particle counts. When one weight is one and the rest are zero, the sum of squares is one and \(\widehat{N}_{\text{eff}} = 1\): the filter has degenerated to a single sample. The ratio \(\widehat{N}_{\text{eff}}/N\) is therefore a health gauge running from 1 (perfect) toward \(1/N\) (dead). It is cheap, it needs no ground truth, and it is the trigger every practical particle filter watches.
Key insight
Resampling does not make your estimate more accurate at the instant you apply it; it can only lose information, since it discards low-weight particles that still carried a sliver of probability. Its value is entirely preventive: by spending particles now on the high-probability regions, it keeps the filter from wasting all of them on dead samples later. This is why you resample only when ESS says you must, not on every step: each resampling is a small, deliberate loss of diversity taken to avoid a total loss of representation.
The fix: resampling as survival of the fittest
What it does. Resampling replaces the weighted particle set with a new set of \(N\) equally weighted particles, drawn with replacement so the expected number of copies of particle \(i\) is proportional to its weight \(N w^{(i)}\): high-weight particles get cloned several times, low-weight particles vanish. After resampling, all weights reset to \(1/N\), and the cloud concentrates where the posterior mass actually is. The catch is that cloning creates duplicates, several new particles sit at identical states, so the cloud's diversity drops. This is sample impoverishment, and it is most dangerous when process noise is small, because the dynamics barely separate the duplicates on the next step and the filter can collapse onto a single trajectory even though its weights look healthy.
How to draw the copies. The naive method, multinomial resampling, draws \(N\) independent uniforms and picks, for each, the particle whose cumulative weight interval it lands in. It is unbiased but injects the most Monte Carlo variance of any scheme; three better methods cut that variance by making the draws less independent:
- Stratified resampling partitions \([0,1)\) into \(N\) equal strata and draws one uniform inside each, guaranteeing coverage across the whole weight range.
- Systematic resampling goes further, drawing a single uniform \(u \sim \mathcal{U}[0, 1/N)\) and placing the \(N\) sample points on the regular grid \(u + i/N\); it is \(O(N)\), simplest to code, and has the lowest variance in practice, the default in most libraries.
- Residual resampling deterministically keeps \(\lfloor N w^{(i)} \rfloor\) copies of each particle, then draws the remainder multinomially, handling the integer part exactly.
All four are unbiased in expectation; they differ only in the variance they inject. For sensor filters running at hundreds of hertz, systematic resampling is almost always the right default.
import numpy as np
def effective_sample_size(w):
"""Estimated number of 'active' particles from normalized weights w."""
return 1.0 / np.sum(w**2)
def systematic_resample(w, rng):
"""Return indices of resampled particles. O(N), low variance."""
N = len(w)
positions = (rng.random() + np.arange(N)) / N # one jitter, regular grid
cumulative = np.cumsum(w)
cumulative[-1] = 1.0 # guard against round-off
return np.searchsorted(cumulative, positions)
def maybe_resample(particles, w, rng, thresh_ratio=0.5):
"""Adaptive resampling: only when ESS drops below thresh_ratio * N."""
N = len(w)
if effective_sample_size(w) < thresh_ratio * N:
idx = systematic_resample(w, rng)
return particles[idx], np.full(N, 1.0 / N), True
return particles, w, False # weights carried forward
systematic_resample spends a single random draw and one searchsorted pass, so it is \(O(N)\); maybe_resample fires only when effective_sample_size falls below half the particle count, carrying the weights forward untouched otherwise. These three functions are the entire degeneracy-control layer of a particle filter.Resampling fires only when the health gauge crosses a threshold, a ratio of \(0.5\) (half the particles effectively dead) is the common choice. Between triggers the weights simply accumulate, so a filter watching a well-modelled, slowly changing state may go many steps without resampling at all, preserving diversity for free.
In practice: a warehouse robot that teleported through a wall
An indoor delivery robot localized itself with a particle filter over a floor map, fusing wheel odometry with a lidar scan match, a classic setup revisited in Chapter 25. On long straight aisles the lidar saw two identical parallel walls, so two particle clusters, one at the true pose and one shifted by an aisle width, both explained the scans well. The team resampled on every step. Each resample randomly culled a few particles, and after a few hundred steps the plausible-but-wrong cluster was wiped out entirely by resampling noise, taking most of the true cluster's diversity down with it. When the robot reached a junction that finally disambiguated the two hypotheses, the correct particles were gone: the estimate snapped to the wrong aisle, and the robot planned a path straight through a shelving unit. The fix was two lines: compute ESS, and resample only when it drops below \(N/2\). Keeping diversity alive between triggers let both hypotheses survive until the geometry, not the random number generator, decided between them.
Keeping diversity alive: roughening and regularization
Adaptive resampling reduces how often you impoverish the cloud, but when process noise is genuinely tiny the duplicates still pile up. Two standard remedies inject diversity back. Roughening (also called jitter) adds a small zero-mean noise to each resampled particle, spreading the clones apart, with the noise scale typically tied to the inter-particle spacing so it shrinks as the cloud converges. The regularized particle filter makes this principled: instead of resampling from the discrete set, it resamples from a continuous kernel-density estimate built on the particles, so every child is a fresh, distinct sample. Both cost a little bias for robustness against collapse, worth reaching for whenever a low-noise system keeps degenerating despite adaptive resampling. A different route, deferring diversity loss by analytically marginalizing part of the state, is the subject of Section 10.5.
The right tool: resampling off the shelf
Rolling your own multinomial, stratified, systematic, and residual resamplers plus an ESS gate is roughly fifty lines to write, and, more to the point, fifty lines to get subtly wrong (the round-off guard on the cumulative sum is a classic silent bug). filterpy ships all four resamplers and the ESS helper, collapsing the whole degeneracy layer to about five lines:
from filterpy.monte_carlo import systematic_resample
from filterpy.monte_carlo import neff # effective sample size
if neff(weights) < N / 2: # ESS below half the particles
idx = systematic_resample(weights) # validated, O(N)
particles[:] = particles[idx]
weights.fill(1.0 / N) # reset to uniform
filterpy.monte_carlo. The library provides neff (effective sample size) and battle-tested systematic_resample, stratified_resample, residual_resample, and multinomial_resample, replacing about fifty lines of hand-written and easily mis-indexed sampling code with five.Research frontier: learning your way out of degeneracy
Roughening and regularization are decades-old patches; the 2023-2026 frontier attacks degeneracy at its root by making resampling differentiable or unnecessary. Entropy-regularized optimal-transport resampling, introduced by Corenflos and colleagues and extended in later differentiable particle filter work, replaces the hard resample-with-replacement step with a soft transport plan, so the whole filter, proposal included, trains end to end by backpropagation instead of hand-tuning. Stein variational particle filters sidestep resampling altogether, pushing particles apart with a repulsive force derived from a kernelized Stein discrepancy so diversity is maintained by construction, not by injected noise. A parallel line of neural-proposal particle filters, several aimed at the robotics and wearable pose-tracking problems this book covers, learn the proposal directly from the measurement, narrowing the proposal-posterior mismatch this section's degeneracy theorem names as the root cause.
When each choice is right
When to resample: almost always adaptively, on an ESS threshold near \(N/2\); resample every step only for a specific reason, since impoverishment then comes faster. Which scheme: systematic by default for its low variance and \(O(N)\) cost, residual to squeeze variance further, multinomial only as a textbook baseline. When to add roughening or a regularized filter: when process noise is small enough that resampled duplicates do not naturally separate, the tell being an ESS that looks healthy while the distinct-particle count quietly collapses. These knobs, together with the proposal design of Section 10.3, are the whole tuning surface of a particle filter, and they recur from the localization of Chapter 25 to the SLAM back ends of Chapter 52.
Exercise
Take the one-dimensional particle filter you built for Section 10.3 and run it for 300 steps with \(N = 1000\) particles.
- Disable resampling entirely and log \(\widehat{N}_{\text{eff}}\) at every step. Plot it and note how many steps pass before the effective sample size falls below 10.
- Enable adaptive systematic resampling at the \(N/2\) threshold and overlay the ESS trace. Count how many times resampling actually fired.
- Now shrink the process noise by a factor of 100 and count the number of distinct particle values after 300 steps, with and without roughening. Explain what the ESS gauge failed to warn you about.
Hint
ESS measures weight balance, not spatial diversity. A cloud of a thousand identical duplicates has perfect ESS and zero diversity, which is exactly the blind spot roughening exists to cover.
Self-check
- Weight degeneracy and sample impoverishment are opposite failures. Which one does resampling cure, and which one does it cause?
- Your filter reports \(\widehat{N}_{\text{eff}} = 950\) out of 1000 particles yet its estimate is badly wrong and barely moves. What is going on, and why did ESS not catch it?
- Why is systematic resampling usually preferred over multinomial resampling even though both are unbiased in expectation?
What's Next
In Section 10.5, we attack degeneracy from the other end. Instead of curing a collapsed cloud after the fact, Rao-Blackwellization shrinks the space the particles have to cover in the first place: it samples only the states that truly need it and solves the rest analytically with a Kalman filter riding on each particle. Fewer sampled dimensions means slower weight variance growth, so the same particle budget buys a far healthier filter.