简介:资源为粒子滤波算法的MATLAB实现源码,面向目标跟踪、定位、传感器融合等非线性非高斯状态估计场景,适合需要学习粒子滤波原理、动手复现算法或开展相关课程设计的开发者与研究生。压缩包内共2个m文件,整体仅5KB,分别对应粒子滤波主程序与融合JPDA数据关联的多目标跟踪模块,代码精简且不依赖额外工具箱,便于直接运行和逐行阅读。目前已有737人学习下载。实现完整覆盖初始化、预测、权值更新与重采样四个核心步骤,并在多目标场景中引入联合概率数据关联,可通过全局似然优化解决观测与目标间的匹配歧义,应对目标交叉、遮挡等复杂情况。读者既可对照代码理解SIR滤波与系统重采样的具体实现,也能通过修改运动模型、观测模型或粒子数量,快速搭建自定义跟踪实验,是理解粒子滤波思想并掌握MATLAB工程实现的简洁入门资料。
1. 粒子滤波算法:为什么强非线性场景卡尔曼集体失灵,用MATLAB 50行就能跑通预研
做过目标跟踪、水声定位或组合导航的人应该都有过这种体验:系统模型稍微非线性一点,卡尔曼滤波的估计曲线就开始飘,EKF 展开到一阶泰勒级数后,遇到强非线性或非高斯噪声直接发散。而粒子滤波算法用几千个随机样本去“枚举”后验分布,不依赖线性化和高斯假设,反而在这种场景下稳得住。这篇文章把粒子滤波拆成三层讲:核心思想是什么、用 MATLAB 手写一个标准粒子滤波要怎么写、参数和坑在哪里。适合正在做课设、算法预研或想从卡尔曼滤波迁移到粒子滤波的工程师,文章最后会给出调参顺序和验证方法,照着跑一遍比读十篇综述都有用。
2. 从贝叶斯递推到 SIR-PF:粒子滤波的核心思想与 MATLAB 数据组织方式
2.1 为什么卡尔曼系在非线性场景会失效:线性化假设的边界
任何滤波问题本质都是贝叶斯递推:已知上一时刻后验分布 p(x_{t-1}|z_{1:t-1}),结合状态转移方程预测当前时刻的先验分布,再结合新的观测 z_t 更新得到后验分布。卡尔曼滤波能写出解析解,靠的是两条硬性假设:状态转移和观测模型都是线性高斯,初始分布是高斯。只要这两条成立,均值向量和协方差矩阵就足以完整描述整个分布。
现实中的模型很少这么乖。传感器观测可能是平方关系或三角函数关系,噪声可能是重尾分布。EKF 的思路是在当前状态点做一阶泰勒展开,把非线性函数近似成线性函数再去套卡尔曼公式。这个近似在弱非线性场景下够用,但一旦非线性强,展开点附近的线性化误差会随着递推不断累积,协方差矩阵被扭曲,最终滤波发散。UKF 用 sigma 点做无迹变换,比 EKF 对非线性更鲁棒,但它的前提仍然是后验分布可以用高斯近似。
粒子滤波直接放弃了解析解这条路。它用 N 个带权重的样本(粒子)x_t^i 以及对应权重 w_t^i 来离散近似后验分布:
p(x_t|z_{1:t}) ≈ Σ w_t^i · δ(x_t - x_t^i)
权重越大,代表这个粒子对应的状态越可信。这是典型的蒙特卡洛思想,它不需要任何线性化,也不需要高斯假设。更重要的是,粒子滤波能表征多模态分布。比如观测值是状态平方时,同一个观测可能对应正负两个状态,卡尔曼系会取平均后给出一个中间值,而粒子滤波可以同时保留两个峰。这是粒子滤波在很多硬场景下不可替代的根本原因。
提示:如果后验分布是单峰且接近高斯,卡尔曼滤波仍是最优选择,计算量低一个量级。粒子滤波不是替代品,而是非线性、非高斯场景下的兜底方案。
2.2 SIR-PF 的四个步骤:采样、加权、归一化、重采样
SIR 粒子滤波(Sampling Importance Resampling)是实践中最常见的粒子滤波形式,核心是一个按时间步循环的四步流程。第一步预测,也叫采样:把上一时刻的每个粒子代入状态转移方程,并加上过程噪声,生成一批新的粒子,代表“根据模型推出来的可能状态”。第二步加权,也叫更新:用当前观测计算每个粒子的似然值,也就是“如果这个粒子是真实状态,我有多大可能看到当前观测”,然后把似然值乘到粒子上一步的权重上。第三步归一化:让所有粒子权重之和等于 1,使权重具备概率意义。第四步重采样:按权重从现有粒子中重新抽取 N 个粒子,权重大的粒子会被复制多份,权重小的会被淘汰,随后所有权重重置为 1/N。
为什么必须有第四步?因为迭代几轮后,少数粒子的权重会趋于 1,绝大多数粒子权重趋于 0,有效粒子数骤降。这就是粒子退化现象。重采样把资源集中到高权重区域,算是给退化问题的一个粗暴解法。但重采样本身也有副作用,它会让粒子丧失多样性,后面第 5 章再细说。
这四步对应到 MATLAB 里非常直观:所有粒子本身就是一组 N 行向量,MATLAB 天然支持向量化计算,一次就能完成所有粒子的预测和加权,不需要 for 循环逐个处理粒子。这也是粒子滤波在 MATLAB 里特别容易上手的原因,代码量比 C++ 实现少一个数量级。
2.3 MATLAB 中的三件套:粒子矩阵、权重向量、似然函数
在 MATLAB 里组织粒子滤波程序,本质上只需要三个数据结构。粒子用一个 N×D 的矩阵存储,N 是粒子数,D 是状态维度;权重用一个 N×1 的向量存储,每个粒子对应一个 0 到 1 之间的权重值;状态转移函数、观测函数和似然函数写成函数句柄或者匿名函数。这个结构无论手写还是用工具箱都成立。
其中最容易绕晕的是似然函数和观测模型的关系。观测模型 z = h(x) + v 描述的是“状态 x 经过测量方程变成观测 z”;似然函数 L = p(z|x) 则是反过来问“给定状态 x,观测到 z 的概率”。在 MATLAB 代码里,这两个通常合并计算:先用 h(x) 得到预测观测值,再和真实观测做差得到残差,最后把残差代入高斯概率密度公式算出权重增量。
一个常见的错误是把状态方程和观测方程写反,或者忘记在预测步加过程噪声。粒子滤波里的过程噪声不是可选项,它决定了粒子能覆盖多大的状态空间,噪声太小会导致粒子根本不在真实状态附近,后续权重再准也无济于事,这一点在第 4 章会重点展开。
3. 用 MATLAB 手写标准粒子滤波:UNGM 模型的完整代码与参数读法
3.1 为什么选 UNGM 模型:平方观测带来多模态后验
理论讲得再多,不如一个能跑的例子。这篇选择单变量非平稳增长模型(UNGM),状态方程自带非线性项,观测方程是状态平方,这意味着单个观测值无法区分状态的符号,后验分布天然呈现多模态。卡尔曼系算法在这个模型上基本都会发散或给出错误均值,非常适合用来展示粒子滤波的独特能力。
模型方程写如下,w 和 v 分别是过程噪声和观测噪声,方差记为 q 和 r:
x(t) = 0.5·x(t-1) + 25·x(t-1)/(1+x(t-1)²) + 8·cos(1.2·(t-1)) + w
z(t) = x(t)²/20 + v
这里 x 是标量状态,z 是标量观测。由于观测是平方项,状态为正为负都能产生同样的观测,粒子滤波会同时维持正负两个粒子簇,这是卡尔曼滤波做不到的。
3.2 可运行代码:自适应重采样的 SIR-PF
把上面两个方程落成 MATLAB 脚本,核心循环只有 20 行。代码里加入了自适应重采样逻辑:只有有效粒子数 Neff 低于阈值时才做重采样,避免每一步重采样带来的多样性损失。
% SIR-PF 粒子滤波算法,UNGM 模型 % 状态方程: x(t)=0.5x+25x/(1+x^2)+8cos(1.2t)+w % 观测方程: z(t)=x^2/20+v % 参数: q=10 为过程噪声方差, r=1 为观测噪声方差 clear; clc; rng(1); T = 50; % 仿真步数 N = 1000; % 粒子数 q = 10; % 过程噪声方差 r = 1; % 观测噪声方差 x0 = 0.1; % 初始状态 % 1) 生成真实状态与观测,固定随机种子保证可复现 x_true = zeros(T,1); z = zeros(T,1); xt = x0; for t = 1:T xt = 0.5*xt + 25*xt/(1+xt^2) + 8*cos(1.2*(t-1)) + sqrt(q)*randn; x_true(t) = xt; z(t) = xt^2/20 + sqrt(r)*randn; end % 2) 初始化粒子与权重 xp = x0 + sqrt(q)*randn(N,1); % 从初始分布采样 w = ones(N,1)/N; % 权重初始化为等权 x_est = zeros(T,1); % 存储每一时刻的状态估计 hist_save = zeros(N,T); % 存储粒子,用于后验分布可视化 for t = 1:T % 3) 预测步:代入状态方程并加过程噪声 xp = 0.5*xp + 25*xp./(1+xp.^2) + 8*cos(1.2*(t-1)) + sqrt(q)*randn(N,1); % 4) 更新步:按观测似然更新权重 innov = z(t) - xp.^2/20; % 观测残差 likelihood = exp(-0.5*innov.^2/r); w = w .* likelihood; w = w / sum(w); % 归一化 % 5) 当前时刻状态估计:加权平均 x_est(t) = sum(xp .* w); hist_save(:,t) = xp; % 6) 自适应重采样:仅当有效粒子数不足时执行 Neff = 1/sum(w.^2); if Neff < N/2 [xp, w] = sys_resample(xp, w); end end figure; plot(1:T, x_true, 'k-', 1:T, x_est, 'r--', 'LineWidth', 1.2); legend('真实状态','PF估计'); grid on; xlabel('时间步'); ylabel('状态值'); % 系统重采样:分层采样,方差比多项式重采样更小 function [xr, wr] = sys_resample(x, w) N = numel(w); u = ((0:N-1)' + rand) / N; c = cumsum(w); idx = zeros(N,1); k = 1; for j = 1:N while c(k) < u(j) k = k + 1; end idx(j) = k; end xr = x(idx); wr = ones(N,1)/N; end代码里需要说明的是几个关键点。第一,生成真实状态时用的是单粒子迭代,一步一个噪声样本,这条轨迹就是后续滤波要追赶的“真值”。第二,预测步里xp./(1+xp.^2)用了数组除法,因为 xp 是 N×1 的向量,MATLAB 的./才能逐元素计算。第三,似然计算没有除以 sqrt(2πr),因为权重只关心相对大小,常数项不影响归一化结果,省掉能少一两次运算。第四,rng(1)固定了随机种子,保证每次运行结果一致,这是调参阶段最重要的习惯。
如果想看某一时刻的后验分布长什么样,可以用histogram(hist_save(:,25),'Normalization','pdf')绘制第 25 步的粒子分布。你会看到粒子可能分布在正负两个区域,这是平方观测下的正常现象,也是卡尔曼滤波看不到的图景。
3.3 代码逻辑与参数速查:改哪些量就能测不同场景
上面的代码有几个变量是调参入口,下表列出每个参数的含义和调整建议。
| 参数 | 代码位置 | 作用 | 调整建议 |
|---|---|---|---|
| T | 第 10 行 | 仿真总步数 | 调参阶段先用 50,验证稳定后加长 |
| N | 第 11 行 | 粒子数 | 一维状态 500~2000 够用,多维按第 4 章方法确定 |
| q | 第 12 行 | 过程噪声方差 | 决定预测步粒子扩散范围,太小会丢失目标 |
| r | 第 13 行 | 观测噪声方差 | 决定似然函数尖锐程度,太小会加速粒子退化 |
| Neff 阈值 | 第 33 行 | 重采样触发条件 | 常用 0.5N,也可改成 0.3N 减少重采样频率 |
把状态方程换成匀速运动模型,把观测方程换成目标方位角测量,这段代码就变成了目标跟踪场景的基础框架。粒子滤波的移植性就在于此,核心四步不动,换模型函数即可。
4. 粒子滤波参数怎么定:粒子数、噪声方差、重采样策略与工具箱分工
4.1 粒子数 N:先看状态维度再看 RMSE 收敛曲线
粒子数 N 是新手最容易陷入焦虑的参数。一维状态放 10000 个粒子,算得慢还看不出精度差异;六维状态放 500 个粒子,估计误差大到没法看。经验规律是:粒子数与状态空间维度和非线性强度相关,一维场景 500~2000 个通常够,三维跟踪场景 5000~10000 个是常见起步值。判断标准不是拍脑袋,而是画一条 RMSE 随 N 变化的收敛曲线。
具体做法是固定随机种子和噪声参数,分别用 N=200、500、1000、2000、5000 跑同一组数据,计算各自的均方根误差。误差随 N 增大快速下降然后进入平台期,平台期的起点就是性价比最高的粒子数。如果 N 增大一倍误差没有明显下降,再往上堆粒子数就是浪费算力。
提示:调参阶段先设 N=500 把逻辑跑通,确认曲线不发散后再加大 N。粒子滤波单步计算是 O(N) 复杂度,N 翻倍耗时近似翻倍,用大 N 调参会让你浪费大量等待时间。
4.2 过程噪声 q 与观测噪声 r:先验底盘的调参顺序
q 和 r 是对真实噪声方差的先验估计,它们直接决定预测步的扩散范围和更新步的权重分布形态。q 太小,粒子散布范围覆盖不了真实状态的变化,滤波会跟丢目标;q 太大,大量粒子浪费在无关区域,等效粒子数下降,精度反而变差。r 的作用类似:r 太小,似然函数非常尖锐,与观测稍微偏离的粒子权重迅速归零,退化加速;r 太大,观测信息被稀释,滤波器会对数据不敏感。
我一般的调参顺序是:先把 r 固定在观测噪声的量级,再把 q 设成真实过程扰动的 2~3 倍作为起点跑一轮。观察估计曲线是否滞后于真值,如果滞后明显,说明 q 偏小,粒子在预测步没跟上状态变化。观察权重退化速度,如果两步之内 Neff 就跌破阈值,说明 r 可能设得太小,可以适当增大 r 或改用更鲁棒的似然函数。q 和 r 不要同时调,一次只动一个,否则无法判断效果变化来自哪个参数。
4.3 重采样策略对比:多项式、系统、残差怎么选
重采样策略直接影响粒子多样性和计算量,常见的有三种。多项式重采样最直白,按权重归一化后的累积分布抽样,每次独立均匀采样,实现简单但采样方差较大。系统重采样把 [0,1] 区间等分成 N 份,每份内取一个随机点,再映射到累积分布上,方差更小,实现也简单,上面的代码用的就是这种。残差重采样先按权重整数部分确定复制次数,剩余部分再做一次多项式采样,计算效率高,但实现稍复杂。
| 策略 | 核心思路 | 方差 | 适用场景 |
|---|---|---|---|
| 多项式 | 独立均匀采样映射到累积分布 | 较大 | 快速原型验证 |
| 系统 | 分层随机采样再映射 | 最小 | 大多数场景的默认选择 |
| 残差 | 确定性复制加随机补采 | 较小 | 粒子数大、对效率敏感的场景 |
大多数场景直接选系统重采样,它的采样方差最小,且代码短。残差重采样在粒子数超过一万时效率优势更明显,但前提是你愿意多写一个函数。
4.4 工具箱 particleFilter 与手写代码的取舍
MATLAB 的 Navigation Toolbox 提供了现成的 particleFilter 类,适合快速验证和工程原型。基本用法是先定义状态转移函数和测量似然函数,然后创建对象、设置状态估计方法和重采样方法。
% 需要 Navigation Toolbox pf = particleFilter(@stateTransitionFcn, @measurementLikelihoodFcn); pf.StateEstimationMethod = 'mean'; % 用加权均值估计状态 pf.ResamplingMethod = 'systematic'; % 系统重采样 pf.NumStateVariables = 1; initialize(pf, 1000, x0, 'StateTransitionFcn', @stateTransitionFcn); predict(pf); % 预测步 correct(pf, z); % 更新步 state = pf.State; % 读取估计结果工具箱的好处是不需要自己写重采样函数,边界处理也更完善。但它有两个问题:需要额外工具箱授权,且内部机制被封装成了黑匣子,出问题之后不好定位是模型问题还是参数问题。我的建议是:手写代码用于学习和排错,工具箱用于工程交付或快速对比实验。两者对同一套模型参数的语义一致,学会手写再切工具箱几乎没有成本。
5. 粒子滤波 MATLAB 实现常见问题排查:退化、贫化、野值、工具箱报错
5.1 权重全挤在一个粒子上:Neff 降得太快怎么办
现象:运行几步后有效粒子数 Neff 骤降到个位数,绝大多数粒子权重接近 0,状态估计曲线开始剧烈跳动。
原因:观测似然函数过于尖锐,比如 r 设得比实际观测噪声小很多,导致粒子与观测稍有偏差权重就归零。这是粒子退化的直接表现,权重集中在极少数粒子上,分布近似失效。
解决:优先检查 r 是否过小,适当调大 r 使似然函数平滑一些。其次检查是否漏了重采样,如果重采样阈值设成 0 或条件写错,退化会一路累积。最后可以在似然函数里用“温度缩放”,将权重取幂到 1/α(α>1),人为展宽似然分布,这在野值较多的场景尤其有用。
5.2 重采样之后多样性反而丢了:粒子贫化
现象:重采样后粒子确实都集中在高权重区域,但几轮之后粒子之间几乎完全相同的状态,分布坍缩成几个孤立点,RMSE 反而比重采样前变大。
原因:重采样会复制高权重粒子、淘汰低权重粒子,反复执行后所有粒子退化成少量高权重粒子的复制品,多样性被摧毁。这个过程叫粒子贫化,和不做重采样的退化一样危险。
解决:在粒子分布的标准差太小或粒子重复率升高时,重采样后主动加一个小扰动xp = xp + h*std(xp)*randn(N,1),h 取 0.1 左右。这相当于给粒子一点“抖动”,保留探索能力。也可以降低重采样触发频率,比如把阈值从 N/2 改成 N/4,甚至有场景只在 Neff 低于 N/10 时才重采样。
5.3 过程噪声设太小:粒子跟不上真实状态
现象:估计曲线整体滞后于真实轨迹,尤其在状态快速变化段,粒子始终追不上真值,但你没改任何模型代码。
原因:q 值小于状态真实扰动的幅度,预测步给出的粒子散布范围太窄,真实状态没有落在粒子覆盖区域。粒子滤波本质上是用粒子“包围”真实状态,覆盖不到就更新不动。
解决:把 q 调大到真实扰动的 2~3 倍重跑一遍。调参时可以先给 q 一个偏大的值,观察滤波不发散后逐步缩小,直到精度和稳定性之间取得平衡。另一个常见做法是把 q 当成自适应参数,每个时间步根据状态估计的方差动态调整。
5.4 观测野值引发权重爆炸:估计值瞬间跳变
现象:某一时刻观测出现异常大值,估计曲线瞬间跳出一个尖峰,后续需要好几步才能恢复;严重时所有权重直接集中到极少数粒子上。
原因:观测野值导致所有粒子的似然几乎为 0,权重分布被一个相对最不坏的粒子垄断,整体估计跟着野值走。高斯似然对这一类异常值没有抵抗力。
解决:在似然函数里截断异常残差,比如当残差超过 3 倍标准差时把残差按 3 倍标准差处理,给权重一个下限。也可以把似然函数从高斯换成 t 分布,t 分布重尾特性天然容忍野值。工程里更稳妥的做法是叠加一个观测质量判断,可靠性低时直接把这次观测的权重更新系数调小。
5.5 工具箱报错:particleFilter 未定义与 API 差异
现象:运行pf = particleFilter(...)报错“未定义类 particleFilter”,或者工具箱调用时提示函数输入参数个数不对。
原因:前者大概率是没有安装 Navigation Toolbox,particleFilter 不是 MATLAB 基础函数,需要对应工具箱授权。后者通常是 MATLAB 版本差异导致对象属性名或方法签名不同,不同版本对 State 属性、predict 方法的参数要求略有差异。
解决:先执行ver('nav')确认工具箱是否安装。确认已安装后,在命令行输入doc particleFilter查看当前版本的接口文档,以文档为准调整代码。不想依赖工具箱的话,直接用第 3 章的代码是最稳妥的路线,核心逻辑完全自主可控。
6. 再进一步:自适应重采样与正则化扰动,用蒙特卡洛验证稳定性
6.1 自适应重采样阈值与正则化扰动
第 3 章代码里已经用了自适应重采样,阈值设在 N/2。实际工程中这个阈值不宜固定,粒子质量好时可以降低重采样频率,比如 N/4;粒子快速退化时则要提前触发。我的习惯是在每次更新后打印 Neff 序列,观察它在一轮仿真里的走势,再据此定阈值,而不是盲选。
正则化扰动是对抗粒子贫化的有效手段。重采样后,给每个粒子的状态加上一个尺度正比于当前粒子分布标准差的高斯扰动,尺度系数 h 取 0.05~0.2。h 太小起不到作用,h 太大又人为增大了估计方差。我的经验是从 0.1 起步,看 RMSE 和粒子分布散点图微调。更理论的做法是使用 MCMC 移动步骤或 EKF 建议分布,那相当于用局部线性化提高采样效率,属于进阶路线,新手先用扰动就足够。
6.2 用固定种子的蒙特卡洛实验验证参数
单次运行的 RMSE 有运气成分,随机种子不同结果可能完全不同。验证参数是否真的可靠,要用蒙特卡洛实验:保持真实状态轨迹和噪声种子不变,只改变粒子滤波的初始随机种子,重复跑多次,统计 RMSE 均值和方差。这样把参数优劣势从随机性中分离出来。实际做法是外层循环 M=50 次,每次重跑一遍滤波,记录每一时刻的误差序列,最后累加求均值。换参数时保证真实轨迹和观测序列一致,否则对比没有意义。
做蒙特卡洛实验时,我最深的教训是一开始直接把 N 拉到 5000 跑调参,改一次参数要等半天,后来改成先用 N=500 把所有逻辑和异常情况调通,再把 N 放大做最终验证。小 N 跑逻辑、大 N 出精度,这个顺序帮我省了大量无效等待。粒子滤波调试是一个反复看 Neff、看粒子分布、看误差曲线的过程,参数不是调出来的,是比出来的。希望这篇记录能帮你在自己的项目里少走几段弯路。
本文还有配套的精品资源,点击获取