Estimating the likelihood with a particle filter, and using it to fit a parameter
How a bootstrap particle filter gives an estimate of the data likelihood as a by-product, why that estimate is noisy and unbiased only on the natural scale, and how I used it to grid-search one parameter of a stochastic SEIR model.
University lab From my coursework in the MSc in Statistics and Machine Learning at Linköping University, rewritten as a note to review later.
Here was my problem in the lab: I had a simulator-style epidemic model with one unknown parameter, and I wanted the value that best explains the data. Maximum likelihood needs . For a linear Gaussian model the Kalman filter hands you that number exactly. For a nonlinear model there’s no formula. The nice surprise: a particle filter gives a random estimate of it almost for free. I used this to pick the transmission parameter of a simplified stochastic SEIR model fitted to daily ICU admissions in Stockholm in 2020. The course was clear that the model was a toy, so the point here is the method, not the epidemiology.
The model in one paragraph
The hidden state holds the number of susceptible (), exposed () and infectious () people, plus a log contact rate that does a Gaussian random walk. Each day, binomial draws move people , with the probability growing with , where is a transmission parameter. The observation is the number of new ICU cases, drawn as with a small probability . So the observations are small non-negative counts. That’s one reason a Gaussian observation model would be a poor fit: it allows negative and non-integer values, and its spread doesn’t grow with the level the way count data does.
The likelihood estimate
Write the likelihood with the chain rule, . In a bootstrap particle filter, at step the particles are (approximately) draws from , before we look at . So the average of their unnormalized weights estimates the one-step predictive density:
Multiplying over time:
In code it’s one extra line in the filter loop, done in log space for safety. The is easy to forget. It’s what turns the sum of weights into an average.
import numpy as np
def log_mean_exp(logw):
c = logw.max()
return c + np.log(np.mean(np.exp(logw - c)))
# inside the bootstrap particle filter loop, after computing log-weights:
# logZ += log_mean_exp(logw) # log of (1/N) * sum_i w_t^i
# W = np.exp(logw - logw.max()); W /= W.sum()
It’s random, so look at its spread
Run the filter twice and you get two different values. In the lab I first ran the filter 20 times with at the default and drew a box plot of the log-likelihood estimates. Seeing that spread is important before using the estimate to compare parameter values: a difference smaller than the spread doesn’t mean much.
The variance shrinks as grows, and grows with the length of the series. A rough rule from the theory is that the variance of scales like , so a longer series needs more particles for the same precision.
Unbiased, but on which scale?
A nice and slightly surprising fact: is an unbiased estimate of the likelihood, , for any . This is what makes particle MCMC methods work.
But is not unbiased for . Because is concave, Jensen’s inequality gives
So when you average several values, as I did, the result is biased downward, and the bias is bigger where the estimate is noisier. To estimate the log-likelihood itself from runs, the log of the average is better: , computed with log-mean-exp. For finding the maximum it often doesn’t matter much, but if the noise level changes a lot across parameter values, the downward bias can shift where the peak appears.
Grid search over one parameter
With an estimated likelihood in hand, the simplest way to fit one parameter is a grid. I used 50 values of between 0.1 and 0.9, and for each one averaged over 30 runs with particles. The averaged curve peaked at with an average log-likelihood of about . For comparison, a single run at the default with gave about . So the data clearly preferred faster transmission than the default, within this toy model.
A few things that help with this kind of search:
- Average or increase . A single run per grid point gives a jagged curve with a misleading argmax.
- Reuse random numbers. Using the same random seed at every grid point (common random numbers) makes neighbouring estimates noisy in a similar way, so their differences are less noisy. Resampling breaks this partly, because a small change in can change which particles get copied.
- Beyond one parameter. Grids get expensive fast. Then you’d move to particle MCMC (which relies on the unbiasedness above) or to methods that optimize a smoothed likelihood estimate.
Key takeaways
- A bootstrap particle filter estimates as the average unnormalized weight; multiply over time to get .
- Don’t forget the , and do everything in log space.
- is unbiased for the likelihood, but is biased downward; average on the natural scale if you want the log-likelihood.
- Check the spread of the estimate before comparing parameter values; average runs, use more particles, or use common random numbers.
- A grid over one parameter is a fine first step for a simulator-based model.
Resources
- 732A80 Time Series and Sequence Learning, Linköping University (course lab 3, autumn 2020).
- Naesseth, Lindsten & Schön, “Elements of Sequential Monte Carlo”, 2019. https://arxiv.org/abs/1903.04797
- Doucet & Johansen, “A Tutorial on Particle Filtering and Smoothing: Fifteen years later”, 2009/2011. https://www.stats.ox.ac.uk/~doucet/doucet_johansen_tutorialPF2011.pdf
- Folkhälsomyndigheten report on covid-19 estimates for the Stockholm region (basis of the SEIR model used in the lab). https://www.folkhalsomyndigheten.se/publicerat-material/publikationsarkiv/e/estimates-of-the-peak-day-and-the-number-of-infected-individuals-during-the-covid-19-outbreak-in-the-stockholm-region-sweden-february—april-2020/