EKF+BP与粒子滤波的轨迹估计:Matlab实现与调参经验详解
前一阵我接手一个Matlab仿真项目压缩包名称很长我记忆犹新基于扩展卡尔曼滤波算法的神经网络训练BP神经网络、扩展卡尔曼滤波EKFBP、粒子滤波PF轨迹估计研究。第一眼看过去我脑子里冒出一连串问题到底是拿BP网络去帮EKF做状态估计还是拿EKF去训练BP网络既然已经有扩展卡尔曼滤波为什么还要再加粒子滤波这几个方法全堆在一个标题里到底哪条才是主线把代码和数据文件通读一遍后问题才逐渐清晰。这类项目其实都是在做同一件事在只能拿到带噪声观测的条件下估计出一条运动轨迹背后的真实状态。扩展卡尔曼滤波是解析公式路线粒子滤波是蒙特卡洛采样路线BP神经网络则是典型的数据驱动工具。三者放在一起不是为了拼凑关键字而是为了处理“模型不准”和“噪声非高斯”这两个现实工程问题。这篇文章我打算把从这套项目里梳理出来的原理、Matlab实现链路和调参经验完整写一遍适合正在做目标跟踪、导航定位、组合导航或控制系统状态观测的工程师参考刚接触状态估计的学生也能跟着走通整个流程。1. 状态估计问题里EKF、BP和粒子滤波为什么总被放在一起1.1 从“用距离和角度猜位置”这个本质场景说起先建立一个直观场景。假设一个运动目标在二维平面里近似匀速直线运动我们在某个固定站点上只能测到两个量目标到站点的距离r以及视线方向与正北方向的夹角theta。这两个量测与目标状态位置和速度之间不是简单的线性关系r sqrt((px - sx)^2 (py - sy)^2) v_r theta atan2(py - sy, px - sx) v_theta这里的v_r和v_theta是量测噪声。从带噪声的非线性观测中实时恢复目标位置、速度就是典型的非线性状态估计问题。对这个具体问题扩展卡尔曼滤波EKF是最常用的一种解法因为它在每个滤波周期把非线性量测方程在当前估计点附近做一阶泰勒展开得到近似的线性系统后再套用卡尔曼滤波公式。整个过程计算量小迭代稳定后精度也能满足多数场景。粒子滤波PF走的是完全另一条路。它不对非线性函数做任何线性化也不假设状态噪声和量测噪声必须服从高斯分布而是用一批带权重的随机样本点去直接逼近后验概率分布。目标状态是位置就撒一堆位置样本状态是位置加速度就在四维空间里撒样本每个样本的权重代表“这个样本与真实状态有多契合”。1.2 一个容易被误解的“加法关系”标题里出现“EKFBP”实际工程里通常有两条不同的实现路线。一条是用BP神经网络去补偿模型误差也就是让EKF处理主线性化模型BP网络负责学习那些解析模型描述不出来的残差部分另一条是把BP网络的连接权重当作未知状态用EKF对网络权重进行在线训练这种方法常用于非线性动态系统辨识和自适应控制也被称为神经网络扩展卡尔曼滤波。把这个关系和粒子滤波放在一起就能看出这类项目想表达的真实结构先通过BP网络对外部输入或系统状态做特征映射再分别用EKF和PF完成轨迹状态估计最后比较两条技术路线在同等仿真条件下的表现。与其说三种算法是并列关系不如说它们各有分工BP管特征映射和模型补偿EKF管高斯假设下的快速递推PF管复杂分布下的稳健逼近。1.3 这套组合的典型应用场景从实际落地看这种组合常见于几类场景机器人和无人机在GPS信号不佳时的惯性/雷达组合导航汽车毫米波雷达目标跟踪以及工业控制系统里的状态软测量。这些场景有一个共同特点——部分系统机理可以用微分方程写出但方程里存在难以精确建模的非线性摩擦、空气阻力、传感器温度漂移等成分。用纯EKF会因为模型失配而发散用纯BP网络又无法保证滤波过程满足时间更新和量测更新的物理约束于是“先估计主运动趋势再用网络估计残差”就成了很自然的工程折中。2. 扩展卡尔曼滤波线性化不是公式套用而是误差控制问题2.1 卡尔曼滤波的线性假设和EKF的近似思路标准的卡尔曼滤波KF要求系统方程和量测方程都是线性的并且过程噪声、量测噪声都是高斯分布。状态更新只涉及矩阵乘法和协方差递推所以实时性非常好。真正做项目时会发现传感器量测方程几乎很少是纯线性的。雷达得到的是距离和角度视觉系统得到的是像素坐标这些量测与目标位置之间存在开方和三角运算此时KF不能直接使用必须采用扩展卡尔曼滤波EKF。EKF的核心动作是“在每一个工作点做线性化”。对过程方程x(k1) f(x(k), u(k)) w(k)对量测方程z(k) h(x(k)) v(k)计算雅可比矩阵F(k) ∂f/∂x | x(k) H(k) ∂h/∂x | x(k|k-1)然后用这两个线性化矩阵替入KF方程。如果系统的非线性程度较强或者滤波从错误的初始点出发一阶线性化带来的误差就可能被协方差递推不断放大最终表现为滤波结果发散。这是EKF最大的软肋也是后面要引入BP补偿和粒子滤波的重要原因。2.2 雅可比矩阵十个EKF发散九个是这里出了错我在调试这类代码时发现EKF最隐蔽的错误不是来自滤波器五个公式而是来自雅可比矩阵。以之前提到的距离-方位角量测为例系统状态为x [px; py; vx; vy]量测方程对状态求偏导后量测雅可比矩阵H是H [px/r, py/r, 0, 0; -py/r^2, px/r^2, 0, 0]其中r sqrt(px^2 py^2)。代码实现时最容易犯两个错一是把py/r^2前忘记加负号二是在角度接近正负90度时没处理符号跳变导致新息值瞬间异常。更隐蔽的是过程方程雅可比矩阵很多匀速模型实际状态转移是线性的但一旦加入控制量或转弯率就会变成非线性矩阵如果照抄匀速模型就漏掉了转弯率项。我的建议是写代码时先用符号推导验证一遍雅可比再在仿真里做一个“初值完全等于真值”的开环测试。如果开环状态下新息序列均值基本为零雅可比才算大概率正确。2.3 EKF调参过程噪声Q和量测噪声R的真实手感代码框架跑通之后最花时间的其实是过程噪声协方差矩阵Q和量测噪声协方差矩阵R的选择。Q不是越大越好也不是越小越好。Q过大滤波器会过于相信新量测估计曲线毛刺明显Q过小滤波器会过于信任模型目标一旦机动滤波结果就会长时间拉不回来。经验做法是先用传感器标定数据估计R的对角元素再根据目标最大加速度推算过程噪声的标准差。假设最大加速度为a_max采样间隔为dt则位置噪声方差可以粗略取Q_p (a_max * dt^2 / 2)^2速度噪声方差取Q_v (a_max * dt)^2。这组初始值通常能保证EKF不至于跑飞后续再根据新息序列的统计量微调。3. EKFBP的两种结合路线把神经网络放在滤波器的哪个环节3.1 路线一BP网络补偿模型残差实际系统里目标不可能永远匀速直线运动转弯、加减速、阵风扰动都会让名义运动模型失配。一个实用的EKFBP配置是用EKF处理“名义模型”的滤波同时训练一个BP网络输入为前几个时刻的状态估计增量或控制量输出为模型残差。具体数据流可以这样设计输入层u(k), x(k-1), x(k) 或经过归一化处理的相关量 输出层Δx(k)即 EKF状态预测与真实状态之间的残差在线滤波时先做EKF时间更新得到x_pred再叠加BP网络输出的残差修正量得到修正后的预测状态。使用这种结构时要注意BP网络输出的是残差不是全量状态。如果网络直接输出全量状态相当于把EKF退化成“噪声估计器前面的黑箱”滤波结果的物理解释就丢了。训练样本来自仿真理想情况下需要覆盖目标的各种运动模式否则网络只会对训练集中的运动模式有效。网络结构不必太复杂两三层的全连接网络通常就够了——在状态估计里特征工程的收益远大于堆网络层数。3.2 路线二把BP网络权重当作状态用EKF在线训练权重扩展卡尔曼滤波算法用于神经网络训练这个说法在标题里出现频率很高。它和传统梯度下降BP算法完全是两种训练范式。常规BP用误差反传更新权值而EKF训练法把网络所有权重集合写成一个大型状态向量w [w1; w2; ...; wm]网络的输出作为关于权重w的非线性函数EKF的“时间更新”负责权重的缓慢漂移“量测更新”则用期望输出与实际输出的误差调整所有权重。相比梯度下降EKF训练法能利用协方差矩阵在不同权值之间建立二阶关联信息收敛速度和参数估计稳定性往往更好尤其适合样本按时间顺序到达的在线学习问题。代价是协方差矩阵的维度等于权值总数网络一旦增大矩阵求逆的计算负担就会快速上升。工程上如果只是为了解决一个小规模的动态系统辨识任务把网络权重总数控制在几百个以内Matlab里直接实现EKF权重训练完全可行。网络再大的话建议换成无迹卡尔曼滤波或UKF训练或者使用mini-batch方式的递推最小二乘。3.3 混合实现时最容易踩的维度塌缩问题做EKFBP混合实现时我见过最多的问题是两个不同量纲的数据被直接塞进同一个滤波器或同一个网络。例如BP网络的输入包含位置和速度位置量级是几百米速度量级是几米每秒如果不做归一化处理网络训练的损失函数会被位置项主导速度项的拟合误差在反向传播中几乎起不到作用。在EKF端也类似有些初学者把BP输出残差直接加到状态向量上却忘了状态协方差也要同步增加对应的过程噪声。这会造成滤波器对修正后的状态过于自信协方差越走越小最终不再信任量测更新。正确做法是每次引入BP修正量时按照修正量本身的统计方差适当放大对应P矩阵元素避免滤波器“飘在半空中”。4. 粒子滤波不是“玄学采样”权重、提议分布和重采样4.1 重要性采样粒子权重不是随便给的粒子滤波的思想可以概括成一句话既然后验分布写不出解析式那就用很多随机样本点去近似它。每个粒子代表一种可能的状态粒子越密集的地方代表概率越大。实现时直接从真实后验采样通常做不到因此引入重要性采样从一个容易采样的提议分布q(x)中采样再按“真实分布/提议分布”的比例修正权重。标准滤波递推下每个粒子权重的更新公式可以化简成w(k,i) ∝ w(k-1,i) * p(z(k)|x(k,i))其中p(z|x)是量测似然。也就是说能解释当前量测的粒子权重会变大偏离量测太远的粒子权重会变小。这一步是粒子滤波里计算代价最大的地方因为每个粒子都要计算一次距离、角度与量测的接近程度。观测噪声R越小距离小但偏差大的粒子的权重会急剧区分为零量测似然对粒子的“筛选”越严格。4.2 重采样为什么必须做以及它带来的副作用如果不做重采样经过几轮递推后少数粒子会占据几乎所有权重其他粒子权重趋向于零这种现象叫粒子退化。粒子滤波的有效性可以用有效样本数Neff衡量Neff 1 / sum(w_i^2)当Neff小于粒子总数的某个阈值例如二分之一或三分之一时就需要触发重采样按权重比例复制高权重粒子、丢弃低权重粒子。最基础的是多项式重采样工程上更常用的是系统重采样它把随机抽取过程改成确定性层化抽取方差更小。但重采样也会带来新问题就是粒子多样性丢失。重采样后大量粒子变成同一个父粒子的拷贝如果过程噪声非常小这些粒子之后很难再散开就会出现粒子耗尽。解决方式是在重采样后给粒子添加一点人工抖动或者用正则粒子滤波的方法把离散粒子和连续核密度估计结合起来。4.3 粒子滤波的改进套路与EKF的先验引导纯粒子滤波的提议分布直接采用状态转移模型p(x(k)|x(k-1))这实现起来最简单但效率不高。如果量测非常精确而状态转移噪声很大大量粒子会落在量测似然很低的区域权重变得很小需要极多粒子才能维持精度。更聪明的做法是用一个EKF或UKF对每个粒子分别做一次局部预测把得到的后验分布作为粒子滤波的提议分布这就是EKF-PF或UKF-PF。这类方法把EKF的“单峰高斯近似”和粒子滤波的“多峰样本表示”结合起来EKF帮助粒子集中到更可能的状态区域粒子滤波再用样本分布表达非线性非高斯后验。标题里的“扩展卡尔曼滤波EKFBP、粒子滤波PF轨迹估计研究”很多代码版本其实就是在两种混合结构间做对比。第一种是BP作为模型补偿EKF作为主滤波第二种是EKF-PF嵌套滤波。两者都有实际工程价值而不是简单互相替代的关系。5. Matlab实现骨架从轨迹生成到算法对比5.1 先构造一个带“标准答案”的运动场景做状态估计研究无论用什么滤波器第一步都应该是先构建一个拥有标准答案的仿真场景。否则算法算出来的结果到底准不准根本没有对照基准。我习惯用匀速直线运动加非线性量测作为默认测试场景。状态设定为四维[px; py; vx; vy]目标初始位置[100; 50]速度[2; 1]采样周期dt 0.1s仿真时长50s。生成真值轨迹后再加上距离和方位角的非线性量测噪声。Matlab代码骨架如下% 生成匀速直线运动目标真值 dt 0.1; t 0:dt:50; nx length(t); px_true 100 2*t; py_true 50 1*t; truth [px_true; py_true; 2*ones(1,nx); 1*ones(1,nx)]; % 非线性量测距离和方位角 r_true sqrt(px_true.^2 py_true.^2); theta_true atan2(py_true, px_true); % 加噪声 R diag([10, (0.5*pi/180)^2]); r_noise sqrt(10) * randn(1,nx); theta_noise (0.5*pi/180) * randn(1,nx); z [r_true r_noise; theta_true theta_noise];这段代码有几个细节要留意真值生成时没有经过动态模型加噪这样后面评估的是“滤波算法对噪声量测的估计能力”而不是“滤波器对模型噪声的吸收能力”。随机噪声用randn直接写便于后续用rng控制随机种子。5.2 EKF与BP残差补偿的核心代码片段EKF的五个公式我不准备完整抄一遍而是给出核心循环里容易被忽略的部分。量测更新时新息要处理角度跳变y z(:,k) - hx; y(2) wrapToPi(y(2)); % 角度误差归一化到[-pi, pi] S H * P_pred * H R; K P_pred * H / S; x_est x_pred K * y; P_est (eye(4) - K * H) * P_pred * (eye(4) - K * H) K * R * K;wrapToPi是Matlab的Mapping Toolbox函数。如果不想依赖工具箱自己写一行也很快y(2) atan2(sin(y(2)), cos(y(2)));BP残差补偿的网络训练代码如下用Deep Learning Toolbox自带的feedforwardnet% 构造训练样本特征为历史时刻的量测和状态估计 X_train ...; % 每列一个样本 T_train ...; % 期望输出为 EKF状态残差 net feedforwardnet([10 5]); net.layers{1}.transferFcn tansig; net.layers{2}.transferFcn purelin; net.trainFcn trainlm; net.trainParam.epochs 200; net train(net, X_train, T_train);输入输出归一化非常关键。我一般用mapminmax函数先归一化训练完再用对应的设置反归一化[Xn, ps_in] mapminmax(X_train, -1, 1); [Tn, ps_out] mapminmax(T_train, -1, 1); net train(net, Xn, Tn);注意不要把训练输入里的量测值直接当成未来时间标签来用否则会造成时间泄漏训练效果虚高在线滤波时一测就崩。5.3 粒子滤波三步走的标准实现Matlab里实现标准粒子滤波同样不需要复杂工具箱基于系统重采样我习惯这样写核心循环% 粒子初始化 Np 2000; particles repmat(x_init, 1, Np) sqrt(P_init) * randn(4, Np); weights ones(1, Np) / Np; for k 2:length(t) % 1. 预测粒子按照状态转移方程传播 particles F * particles mvnrnd(zeros(4,1), Q, Np); % 2. 量测更新计算每个粒子的似然权重 innov z(:,k) - h(particles); innov(2,:) wrapToPi(innov(2,:)); likelihood exp(-0.5 * sum((innov ./ sqrt(diag(R))).^2, 1)); weights weights .* likelihood; weights weights / sum(weights); % 3. 重采样 Neff 1 / sum(weights.^2); if Neff Np * 0.6 indices systematic_resample(weights); particles particles(:, indices); weights ones(1, Np) / Np; end % 状态估计粒子加权均值 x_est_pf(:,k) sum(particles .* weights, 2); end关键点在于第2步中的似然计算。如果量测噪声包含相关项则应该用完整的马氏距离形式即innov * inv(R) * innov而不是简单地对两个分量除以各自标准差后求和。真实R矩阵非对角时后面那种近似会丢失分量间的相关性信息。5.4 统一评估接口一套代码对比三条曲线对比实验要公平必须保证三种方法在同一份量测数据上运行。建议把量测生成、噪声序列、初始状态估计都固定下来统一通过函数接口传递。例如可以定义x_est_ekf run_ekf(z); x_est_ekf_bp run_ekf_bp(z, net); x_est_pf run_pf(z, Np);画图时使用plot同时绘制真值、EKF结果、EKFBP结果、PF结果。不要只画某一段要画完整轨迹这样能立刻看到滤波收敛速度和曲线光滑度差异。6. 结果对比的量化指标与公平性设计6.1 用RMSE看误差也要用NIS看统计一致性单看估计轨迹和真值重合得很好不能完全说明算法有效。更常用的是累计均方根误差RMSERMSE_pos(k) sqrt(mean((px_est(k)-px_true(k)).^2 (py_est(k)-py_true(k)).^2))RMSE能反映精度高低但它会掩盖滤波器的“虚假自信”问题。滤波器协方差一旦收敛得过小RMSE可能很好看但新息统计量与理论分布完全不符。此时需要看归一化新息平方NISNIS(k) y(k) * inv(S(k)) * y(k)当滤波器实际工作正常时NIS的统计均值应近似等于量测维度dim_z。比如量测是距离和角度两维dim_z 2那么NIS均值应接近2。如果NIS长期明显小于2说明滤波器的协方差设置过大或量测噪声被高估如果NIS远大于2说明滤波器正在发散的边缘协方差没有正确反映真实误差。6.2 随机性管理与蒙特卡洛次数单次仿真的结果说服力不足尤其粒子滤波带有随机抽样过程就算固定粒子数和重采样算法两次运行之间的RMSE也会明显波动。最稳妥的做法是外层跑50到100次蒙特卡洛每次只改变目标初始相位或量测噪声种子最后统计RMSE的均值和方差。Matlab里用rng(seed)控制每次运行前的随机种子保证每次蒙特卡洛内三个算法面对的是完全相同的噪声实现。还要注意粒子滤波本身引入的随机性。如果只跑一次PF就宣称PF优于EKF很可能是运气好抽到了特殊种子。我在项目里会用同一个量测序列重新初始化不同的粒子种子连续跑几十次观察PF结果的方差是否在可接受范围。6.3 什么时候选EKFBP什么时候选PF这类对比做多了我慢慢总结出选择倾向如果系统模型大体准确只有小范围残差EKFBP是性价比很高的方案BP网络只需要很小的结构就能把残差拟合到可以接受的水平计算量基本还是EKF级别如果量测分布严重非高斯、目标运动模式存在强烈间歇性机动或者需要表达多模态的可能性PF的优势会显现出来但它对粒子数、提议分布和重采样策略都很敏感。工程上还有一个常被忽略的点EKFBP是“模型学习”的结合对训练数据覆盖范围有要求PF没有训练阶段但需要相对可靠的先验分布和系统模型。如果现场数据很少又无法写出系统模型这两类方法都不好用通常要先做系统辨识或使用无监督特征提取。7. 调参实战与踩坑记录7.1 初始协方差给得过于乐观导致滤波器“锁死”一次实验中我把初始协方差P0设成单位阵乘以0.01理由是“我对初始位置很有信心”。结果前几个测量周期内滤波器几乎不更新轨迹曲线明显偏离真值过了很久才慢慢拉回来。原因很简单P0太小会让卡尔曼增益K在前几步非常小滤波器不信任量测只能依赖早已不准确的状态预测。随后我把P0改成位置方差100、速度方差10状态在1秒内就收敛了。实际项目中如果初始状态来自粗略观测或人工给定P0对角线应比估计误差大一个数量级不要为了“让曲线平滑”而把P0设得太小。7.2 BP训练数据与在线状态分布不匹配我在第一次做EKFBP补偿时训练数据只用目标做匀速直线运动的前半段生成测试时却故意加了转弯运动结果BP残差补偿不但没有修正误差反而在转弯段输出了很大的错误修正量EKFBP的效果比纯EKF还差。回看原因训练输入特征的分布和在线场景分布严重不一致网络只能做外推而BP网络的外推能力非常弱。解决办法是训练样本里必须有足够丰富的运动模式至少包括弱机动、中等机动和急转弯三组数据并且做数据增强时把传感器偏移、增益误差等不同情况都扫一遍。另一个更稳妥的工程技巧是给BP输出加一个置信门限当输入特征落在训练数据分布之外时自动降低补偿量优先相信EKF本身。7.3 粒子滤波的粒子数不是越大越好很多人一上手就把粒子数设成50000觉得这样更精确。确实粒子越多蒙特卡洛近似误差越小但计算量线性增长很快。对于二维或四维空间轨迹估计粒子数2000到5000通常就能达到不错的精度再翻倍的好处非常有限。真正决定精度上限的往往是提议分布和重采样策略。我常用一个简单方法判断粒子数是否足够把粒子数减半看RMSE变化是否超过10%。如果超过说明粒子数还不够如果几乎不变则说明当前粒子数已经够用再增加就是在浪费算力。粒子数过少时可以在量测更新后观察有效粒子数Neff如果随便几步就触发重采样说明需要增加粒子数或改用EKF提议分布引导粒子向高似然区域集中。7.4 用固定随机种子做调试用多随机种子做结论调试代码阶段每次运行都使用同一个随机种子方便逐行对比不同参数的收敛效果等到最终下结论时必须关闭这种“好运模式”换成多种子统计。我见过不少研究者拿着一条最好的PF曲线和一条最差的EKF曲线做对比得出PF完全碾压EKF的结论这种对比方式很容易误导后续方案选型。更严谨的做法是记录所有蒙特卡洛次数的单次轨迹误差再画成箱线图。箱线图能直观展示中位数、四分位距和异常点比单独画RMSE下降曲线更能体现算法的稳定性。这也是为什么我在这套代码里会额外输出一份metrics.mat保存每次运行的全部误差序列而不是只保存均值结果。最后再分享一个压箱底的小技巧调试EKF与PF这类递推算法时不要总盯着误差曲线看要学会观察滤波器内部的新息序列和协方差轨迹。新息均值、NIS均值以及P矩阵对角线这几个量一旦出现异常往往比误差曲线提前好几个采样周期给出预警。把这些中间量全部打印出来调参会轻松一半。