做状态估计这行当久了,你会发现“卡尔曼”这个姓氏下面其实好几个分支。在目标跟踪、无人车定位、机器人导航这些场景里,最常见的取舍就是:系统太非线性,标准卡尔曼滤波器(KF)算不准;扩展卡尔曼滤波器(EKF)要手推雅可比矩阵,推错一步就发散;而最值得上手的平衡点,往往是无损卡尔曼滤波器(UKF)。无损卡尔曼滤波器用一组确定性采样点(sigma points)去逼近状态分布,不需要求导,也避免了EKF那种泰勒展开到一阶的近似误差。这篇文章我会把UKF的数学直觉、算法步骤、MATLAB代码实现和调参中踩过的坑,完整写清楚,方便你直接改参数复现到自己的项目里。
我最初接触UKF,是在一个雷达目标跟踪的模拟项目里。传感器给的是距离和方位角,系统模型又带大转弯,EKF的雅可比矩阵每次都要重新推导,稍微写错一个偏导,滤波器就直接“漂”到天边去。后来换到UKF,代码量反而少了一大截,精度还更好。这篇文章我不会往教材里那种抽象推导钻,而是直接站在工程实操角度,告诉你UKF是怎么运作的、为什么能work、代码怎么写、参数怎么调,以及哪些地方容易翻车。
1. 项目概览与核心需求分析
1.1 UKF在非线性滤波中的位置
先说清楚一个概念:卡尔曼滤波不是单指那一套线性公式,而是一个家族。这个家族里有三条主线:
- 标准卡尔曼滤波器(KF):只适用于线性高斯系统,即状态方程和观测方程都必须是线性的。这个限制在真实工程里太苛刻。
- 扩展卡尔曼滤波器(EKF):用泰勒展开把非线性模型线性化。需要求雅可比矩阵,模型弱非线性时效果不错,但强非线性时误差累积很严重,甚至导致滤波发散。
- 无损卡尔曼滤波器(UKF):不线性化模型,而是用一组精心选择的采样点去“代表”状态分布,然后把每个采样点直接扔进非线性模型里去传播。近似精度高,实现难度反而比EKF低。
从工程角度看,UKF本质上是介于EKF和粒子滤波之间的方案。粒子滤波要随机撒几千上万个粒子,计算量太大;EKF只用一个点和一阶导数,精度不够;UKF用十几个确定性采样点,就拿到了三阶精度(对高斯分布而言),在“算得快”和“算得准”之间取得了非常实用的平衡点。
1.2 典型应用场景与核心需求
UKF最常见的几个应用场景,基本都长一个样子:系统本身带强非线性,或者传感器观测方程不线性。
- 雷达/声呐目标跟踪:传感器观测到的是距离、方位角、俯仰角,但目标状态通常是笛卡尔坐标系下的位置速度,观测方程天然是非线性的。
- 无人车/无人机定位:运动模型里有航向角、角速度、姿态四元数,还不免有大转弯、强机动。
- 导航系统组合:惯性导航和全球卫星导航系统组合时,姿态更新方程、地磁观测方程都是典型的强非线性。
- 电池SOC估计或化学反应过程估计:这些系统的状态转移和观测关系很难写成线性形式。
这些场景对应的核心需求高度一致:不想手动推导复杂的雅可比矩阵;非线性很重时滤波不能发散;实时性要求不能等粒子滤波那种大计算量;最好代码结构通用,换模型不用重写整套算法。
UKF正好全占上了。这也是为什么它在一线工程里备受青睐,尤其是做无人系统和目标追踪的团队,UKF往往是落地的第一选择。
2. 滤波原理:为什么KF不够用,EKF又差在哪
2.1 从贝叶斯滤波到KF
我把逻辑打断一下,用尽可能直白的话讲讲滤波这件事的数学本质。所有状态滤波问题都能归结为一个贝叶斯滤波问题:我们已知上一步的状态估计,又拿到了当前时刻的传感器测量,希望求出一个“综合两者”的新状态。
如果系统是线性高斯,贝叶斯滤波是有解析解的,这个解就是卡尔曼滤波公式。原因是高斯分布经过线性变换后还是高斯分布,并且均值和方差能通过矩阵运算直接推出来,不需要任何近似。
可现实世界几乎不存在真正的线性系统。目标转弯时,位置随时间的变化是三角函数关系;雷达测到的距离和方位角,要转换成笛卡尔坐标位置,中间又隔着一层平方根和反正切。这些非线性一旦进入系统,高斯分布经过非线性函数映射后就不再是高斯分布,理论上最优的解析解也就不存在了。
2.2 EKF的线性化思路与它的致命弱点
EKF的思路很直接:既然非线性函数没有解析解,那我就在某个点附近做泰勒展开,只保留一阶项,用雅可比矩阵把非线性模型“掰成线性模型”,然后套用KF公式。
听起来挺合理,但在强非线性场景下问题很严重:
- 需要手动推导或者用数值方法求雅可比矩阵。这本身就是最容易出错的地方,多变量系统求偏导时,矩阵尺寸错一个、符号错一个,滤波结果直接乱掉。
- 只保留一阶项,高阶项全被丢弃。当系统非线性很强时,一阶近似偏离真实情况非常大,滤波器会积累偏差,最后发散。
- 雅可比矩阵是在单一状态点上计算的,它不能刻画状态分布的形状。哪怕状态估计的均值是准的,二阶矩(协方差)的传播也经常被严重低估,导致滤波器“过于自信”,测量更新不起作用。
所以EKF在工程里最大的问题是:你不确定它什么时候会突然失效,而且一旦失效,排查起来很痛苦。我在实际项目里也见过不少同事调了大半天EKF,最后发现是某个偏导公式写错了——这种事情在UKF里根本不会发生,因为UKF不需要任何导数。
3. 无迹变换(UT)与UKF算法核心
3.1 UT的核心思想:用采样点代替线性化
UKF的核心叫无迹变换(Unscented Transform, UT)。它回到一个很朴素的直觉:如果某个函数的解析变换太难算,那就不用整条曲线去近似函数,而是取函数定义域上几个有代表性的点,直接把这个函数作用在这些点上。
想象一个二维高斯分布,它是一团云团。你不用去推导“这团云经过非线性变换后会变成什么形状”,而是在这一团云里精准地抽取几个“代表点”(sigma点),然后把每个代表点分别扔进非线性函数中,得到一组变换后的点。这组点经过加权组合,就能逼近真实变换后的均值和协方差。
这个思想比EKF好的地方就在这儿:EKF是在“函数”上做近似(近似到一阶项),UT是在“分布”上做采样逼近;非线性函数的整体行为被采样点完整地带出来了,而不是只取一个点附近的切线。
3.2 sigma点的选取、权重计算与参数意义
假设系统状态是n维的,均值为x,协方差为P。UT要生成2n+1个sigma点:
- 第一个点:x
- 后面2n个点:x ± sqrt((n+λ)P) 的各列
关键参数有两个:
- λ = α²(n+κ) - n,是复合尺度参数。
- 其中α是sigma点围绕均值的扩散程度,一般取1e-3到1之间的一个小正数。α越小,点越靠近均值;α越大,点越往分布外围扩散。
- κ是次级尺度参数,通常取0即可,但为保证协方差矩阵半正定也可以取正值。
- β用来利用分布先验信息,对于高斯分布,β=2是最优的。
权重公式如下:
- W₀ᵐ = λ/(n+λ)
- W₀ᶜ = λ/(n+λ) + (1 - α² + β)
- Wᵢᵐ = Wᵢᶜ = 1/[2(n+λ)],i = 1,...,2n
注意W₀ᶜ和W₀ᵐ是不相等的,多出来的(1-α²+β)项是用于修正cos分布形状。这个修正项直接拿掉,协方差传播就会出现偏移。
为什么即使如此,这一套对高斯分布的近似可以做到三阶精度?因为sigma点选取本来就是根据高斯分布的矩匹配条件反推出来的,它精确保留了一阶矩和二阶矩,而协方差传播的截断误差比EKF的一阶近似误差小一个量级。
3.3 UKF滤波流程的完整逻辑
UT负责搞定“分布经过非线性函数后怎么变化”,而UKF整个滤波过程就是在这基础上套一个卡尔曼增益框架,分成五步。
- 生成sigma点:由当前状态均值x和协方差P生成2n+1个sigma点。
- 预测:把每个sigma点代入状态转移方程f,得到一组预测点;对这组预测点做加权平均得到先验均值x_pred,加权外积累加得到先验协方差P_pred。
- 重新生成sigma点(可选):根据x_pred和P_pred再生成一组新的sigma点。实际工程里我一般直接沿用预测后的sigma点,等价操作,能省一点事。
- 观测更新:把sigma点代入观测方程h,得到预测观测值z_pred;通过sigma点与预测观测值的交叉协方差算出卡尔曼增益K。
- 状态校正:用K、实际观测z和预测量测z_pred,更新出后验状态x_upd和后验协方差P_upd。
你会发现整个流程里根本没有出现“雅可比矩阵”这个词。这是UKF最让人省心的设计:只要你能写出状态转移函数和观测函数,滤波器就能用,模型怎么变都只改两个函数,不碰滤波器核心代码。
4. MATLAB实现:CTRV目标跟踪全程代码
4.1 场景设定与模型定义
为了把MATLAB代码落到可运行,我用一个典型的目标跟踪场景来讲:假设一辆目标车在做匀速转弯运动,状态向量取5维:
- px:x方向位置
- py:y方向位置
- v:速度大小
- ψ:航向角
- ω:转弯角速度
状态转移方程在CTRV(Constant Turn Rate and Velocity)模型下是这样的:
- 当|ω|较大时:px更新为px + v/ω * (sin(ψ+ωdt)-sinψ),py更新为py + v/ω * (-cos(ψ+ωdt)+cosψ)
- 当|ω|接近0时:px更新为px + vcosψdt,py更新为py + vsinψdt
- 航向角更新为ψ+ωdt,速度和角速度不变
观测方程模拟雷达/声呐,给的是距离r和方位角θ:
- r = sqrt(px² + py²)
- θ = atan2(py, px)
这两个式子放在EKF里要各求一套雅可比,在UKF里则完全不需要考虑求导这件事。
4.2 UKF核心函数代码实现
我把UKF的一次预测加更新写成MATLAB函数。这个函数是通用的,换任何模型只需要换传入的函数句柄f和h。
function [x_upd, P_upd] = ukf_step(x, P, f, h, Q, R, dt, z, param) % param = [alpha, beta, kappa] alpha = param(1); beta = param(2); kappa = param(3); n = length(x); nsp = 2*n + 1; lambda = alpha^2 * (n + kappa) - n; % 权重 Wm = zeros(nsp, 1); Wc = zeros(nsp, 1); Wm(1) = lambda / (n + lambda); Wc(1) = lambda / (n + lambda) + (1 - alpha^2 + beta); Wm(2:end) = 1 / (2 * (n + lambda)); Wc(2:end) = 1 / (2 * (n + lambda)); % 生成sigma点 A = chol((n + lambda) * P, 'lower'); X = zeros(n, nsp); X(:, 1) = x; for i = 1:n X(:, i+1) = x + A(:, i); X(:, i+1+n) = x - A(:, i); end % 预测步骤:sigma点通过状态方程 Xpred = zeros(n, nsp); for i = 1:nsp Xpred(:, i) = f(X(:, i), dt); end x_pred = Xpred * Wm; P_pred = Q; for i = 1:nsp d = Xpred(:, i) - x_pred; P_pred = P_pred + Wc(i) * (d * d'); end % 更新步骤:sigma点通过观测方程 m = size(R, 1); Z = zeros(m, nsp); for i = 1:nsp Z(:, i) = h(Xpred(:, i)); end z_pred = Z * Wm; Pzz = R; Pxz = zeros(n, m); for i = 1:nsp dz = Z(:, i) - z_pred; dx = Xpred(:, i) - x_pred; Pzz = Pzz + Wc(i) * (dz * dz'); Pxz = Pxz + Wc(i) * (dx * dz'); end K = Pxz / Pzz; x_upd = x_pred + K * (z - z_pred); P_upd = P_pred - K * Pzz * K'; end有几个细节这里必须说明一下。
为什么用chol而不是sqrtm?矩阵平方根在MATLAB里有sqrtm和chol两种方式。UKF公式里需要的是满足P = SS'的矩阵S,chol正好直接给下三角矩阵,而sqrtm计算的是真正的矩阵平方根,两者在数值上可能有明显差异。用chol实现sigma点生成是标准做法。
为什么P_pred初始直接赋值Q?因为UT生成的sigma点本身已经携带了P的信息,P_pred的初值只要加上过程噪声Q,然后逐点累加外积即可。写代码时初值写错成zeros(n),结果就是协方差明显偏小。
为什么K=Pxz/Pzz而不是inv(Pzz)?MATLAB里直接用反斜杠 / 求解线性方程,数值稳定性比显式求逆好很多,结果一样但不容易在某些病态条件下产生大误差。这是我调试了很久才养成的习惯。
4.3 模型函数定义
状态转移函数和观测函数单独写,方便复用。
function xp = f_ctrv(x, dt) px = x(1); py = x(2); v = x(3); psi = x(4); omega = x(5); if abs(omega) > 1e-6 pxp = px + v/omega * (sin(psi + omega*dt) - sin(psi)); pyp = py + v/omega * (-cos(psi + omega*dt) + cos(psi)); else pxp = px + v * cos(psi) * dt; pyp = py + v * sin(psi) * dt; end xp = [pxp; pyp; v; psi + omega*dt; omega]; end function z = h_radar(x) z = [sqrt(x(1)^2 + x(2)^2); atan2(x(2), x(1))]; end这个CTRV模型是个很好的测试用例,因为它自带一个条件分支:omega接近0时公式需要切换,否则会出现除以零的问题。这种切换在EKF里会让雅可比矩阵的推导变得更复杂,而在UKF里只是函数内部逻辑,滤波器完全无感。
4.4 主仿真脚本与运行结果分析
下面是完整主脚本。我生成一条带噪声的真实轨迹,再用UKF进行在线估计,然后计算位置RMSE验证效果。
clear; clc; close all; rng(42); dt = 0.1; T = 20; N = T / dt; time = 0:dt:T; % 真实初始状态 x0 = [0; 0; 8; pi/4; 0.3]; xt = x0; Xtruth = zeros(5, N+1); Xtruth(:, 1) = x0; for k = 1:N xt = f_ctrv(xt, dt); Xtruth(:, k+1) = xt; end % 观测噪声 R = diag([10, 0.1]); % 距离噪声sigma=3.16m,方位角噪声sigma=0.1rad Zmeas = zeros(2, N+1); for k = 1:N+1 Zmeas(:, k) = h_radar(Xtruth(:, k)) + [sqrt(R(1,1))*randn; sqrt(R(2,2))*randn]; end % 初始估计与协方差 x_hat = [0; 0.5; 6; pi/4+0.05; 0.3]; P_hat = diag([4, 4, 4, 0.1, 0.05]); % 过程噪声(调参主要动这里) Q = diag([2, 2, 3, 0.05, 0.01]); % UKF参数 param = [1e-3, 2, 0]; % 滤波循环 X_ukf = zeros(5, N+1); X_ukf(:, 1) = x_hat; P = P_hat; for k = 1:N z = Zmeas(:, k+1); [x_hat, P] = ukf_step(x_hat, P, @f_ctrv, @h_radar, Q, R, dt, z, param); X_ukf(:, k+1) = x_hat; end % 位置RMSE rmse_pos = sqrt(mean(sum((X_ukf(1:2, 2:end) - Xtruth(1:2, 2:end)).^2, 1))); fprintf('位置RMSE = %.3f m\n', rmse_pos); % 绘图 figure; plot(Xtruth(1,:), Xtruth(2,:), 'k-', 'LineWidth', 1.5); hold on; plot(X_ukf(1,:), X_ukf(2,:), 'r--', 'LineWidth', 1.5); scatter(Xtruth(1,1), Xtruth(2,1), 80, 'ko', 'filled'); xlabel('x (m)'); ylabel('y (m)'); legend('真实轨迹', 'UKF估计', '起始点'); axis equal; grid on; title('UKF 跟踪轨迹');跑完这段代码,正常应该能看到红色虚线轨迹和黑色实线轨迹基本贴合,位置RMSE大约在1到2米之间。考虑到距离观测噪声σ有约3米,这个精度说明UKF把多步观测信息有效融合起来了。
我还建议你把这套代码放到方波运动或急转弯场景里试一下。你会发现即目标突然转了一个大弯,UKF也只会短暂偏离一小段,一旦观测连续到来,它会很快被拉回真实轨迹。EKF在这种场景下如果角度更新不及时,容易出现持续偏置。
4.5 参数选择经验
alpha取1e-3时,sigma点非常靠近均值,适合噪声较小的系统;但如果初始化协方差很大且非线性很强,alpha过小可能导致采样点覆盖不足。工程上我通常会从alpha=1e-2起步,观察滤波是否稳定,再决定是否调小。
kappa我长期用0,只在P出现非正定问题时才尝试调到1或2。beta固定为2,因为系统噪声按高斯假设建模,这是理论最优值。
真正需要花心思调的是Q和R,尤其是Q。Q设置太小,滤波器会过分相信预测;Q设置太大,观测噪声会被放大。一个有效调试技巧是看新息序列(innovation sequence)的均值是否近似为零,如果长期为正或负,说明Q和R的比例失衡。
5. UKF的优势、缺陷与适用边界
5.1 与EKF对比的实战优势
我直接给结论,表格里列的是我实际使用中的体感对比,不是教科书空泛宣传。
| 对比项 | EKF | UKF |
|---|---|---|
| 雅可比矩阵 | 需要手动推导,易错 | 完全不需要 |
| 弱非线性下精度 | 一阶近似,尚可 | 接近最优 |
| 强非线性下稳定性 | 容易发散 | 相对稳定 |
| 计算量 | 小,但推导成本高 | 稍大,代码量反而少 |
| 换模型成本 | 要重推雅可比 | 只换f和h两个函数 |
| 编程调试难度 | 高 | 低 |
计算量方面,UKF每步要做2n+1次状态方程传播和2n+1次观测方程传播。对于5维系统就是每个周期跑11次模型函数,完全能接受。对n上到30以上才需要担心计算压力,但这在实时目标跟踪场景里不常见。
5.2 UKF的局限性与注意点
- “免求导”不等于“免思考”。UKF虽不需要导数,但状态维数、噪声矩阵、初始协方差仍要设计得当。维数爆炸时(比如几十维状态)sigma点数量线性增长,优势会被稀释。
- 协方差矩阵必须保持对称正定。UT本质是矩匹配,一旦P因数值误差失去正定性,chol会直接报错。我后面会讲怎么处理这个问题。
- 强非高斯分布下精度仍然有限。UKF本质是用高斯分布去近似真实分布,如果真实分布是多峰的,UKF会和KF一样失效,这时粒子滤波才是正解。
- 观测丢失时只能靠预测。这和所有贝叶斯滤波一样,观测模型或观测链路出问题时,滤波输出完全依赖运动模型,误差会随预测步数增加而增长。
5.3 什么时候该用什么滤波器
我给一套选型经验:
- 系统是线性的,老老实实KF,不要折腾。
- 系统弱非线性,比如小角度转弯、噪声较小,EKF够用且算得快。
- 系统强非线性、状态维数低于15、噪声近似高斯,UKF是最佳选择。
- 系统强非线性且状态分布明显多峰,或者初始化完全未知,上粒子滤波。
- 想省推导时间、希望代码通用性强,哪怕弱非线性我也会直接用UKF。
我的实际倾向是:在工程时间有限的项目里,UKF是默认首选,EKF只在计算资源极其紧张时才替换回来。毕竟调试EKF的雅可比所花费的时间,往往远不止UKF多出来的那一点点计算量。
6. 常见问题与调试技巧速查
6.1 五个最容易踩的坑
我在自己的调试经历里遇到过不少“症状相似、病因不同”的问题,整理成表应该对你有用。
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| chol(P)报错,提示矩阵非正定 | 协方差P出现非对称或负特征值 | 对P做对称化:P=(P+P')/2;再对P+eps*I做特征值裁剪 |
| 滤波越跑越“飘”,RMSE越来越大 | Q设置太小,滤波器过度信任模型 | 适当增大Q,尤其是速度项和角速度项 |
| 滤波结果波动剧烈,被噪声带着跑 | R设置太小,或Q设置太大 | 检查R是否与传感器实际精度匹配,调小Q |
| 初始几秒剧烈跳变 | 初始协方差P_hat设置不合理 | 把初始化摆置略保守,P_hat对应状态量纲适度放大 |
| 代码换成真实传感器数据后失效 | 真实数据含离群点,不满足高斯假设 | 在滤波前先做粗差剔除或检测,把离群测量直接丢弃 |
6.2 数值稳定性的几个硬操作
第一,P的对称化。协方差矩阵理论上必须对称,但数值运算中P_hat = P_pred - KPzzK'这一步可能累积微小非对称。我习惯在每个周期结束后加一行:
P = (P + P') / 2;这个操作开销极小,但能避免很多奇异问题。
第二,奇异值保护。当系统可观测性变差时,P的某个特征值可能趋于0,chol会报错。稳妥做法是检查min(eig(P)),低于阈值就给P加一个很小的单位阵增量,比如eps1e3eye(n)。
第三,角度归一化。CTRV模型里的航向角ψ是循环变量,直接做减法可能得到很大的残差,导致卡尔曼增益异常。工程上一定要对角度差做wrap到[-pi, pi]的处理。这个细节在很多入门代码里都被忽略了,而真实场景里它几乎一定会出问题。
6.3 快速验证滤波器是否正常的自检方法
要判断滤波没调错,最简单有效的方法是看新息序列(innovation)。正式定义是实际观测减去预测量测:
innovation = z - z_pred;收敛良好的UKF,新息序列应该在零附近波动,且标准差基本符合R对角线开方后的量级。如果新息长期偏向一侧,说明模型-噪声配置有系统偏差;如果新息方差远大于理论值,说明噪声参数被调太大或状态未收敛。
另一个好方法是用蒙特卡洛统计。固定轨迹和噪声种子,跑50次仿真,统计RMSE的均值和方差。如果均值比单次运行结果大很多,说明单次结果只是“运气好”,滤波性能并不稳定。这个方法能帮你区分到底是参数问题还是代码bug。
7. 实操总结与个人经验
7.1 一套稳健的UKF落地流程
结合这些年踩过的坑,我建议新项目按下面这套流程来推进:
- 先写纯函数f和h,用几个带解析解的测试点验证模型本身正确。
- 搭UKF核心框架,先在仿真数据上跑通,不要急着接真实传感器。
- 固定alpha、beta、kappa为常规值,alpha=1e-2,beta=2,kappa=0,先不去精细化调参。
- 用仿真的“真实状态”对比估计结果,算RMSE,确认滤波不发散。
- 再调Q和R的比例,核心目标是让新息序列接近零均值白噪声。
- 接入真实数据前做离群点检测,传感器偶尔跳一个野值,UKF全无防备时也会被带偏。
这套路径能帮你把“模型问题”、“参数问题”和“数据质量”三个变量分离开,定位起来快得多。
7.2 最后再分享两个我自己调试UKF时发现的小习惯
第一个习惯,是每次修改模型或参数后,固定随机种子重跑一遍。随机种子不固定时,哪怕只跑两次,RMSE差出百分之二三十也很正常,很容易让人误判改动的好坏。固定种子后,对比才有意义。
第二个习惯,是给UKF代码留一个“EKF对照开关”。调试阶段用一个简单的EKF做底,当UKF发散了能快速对比是模型写错还是滤波本身的问题。这招帮我排掉过好几次“以为UKF写错、其实是模型里面有个符号写反”的冤枉经历。
UKF不是万能药,但对我来说,它是“非线性状态估计问题”里性价比最高的那个解。你只要把f和h两个函数写对,滤波框架一套,剩下的大多数问题都集中在Q和R的调参上,这比反复研究雅可比矩阵要舒服得多了。希望这篇文章能帮你减少一些摸索时间,尤其是那些我在MATLAB代码里已经标出来的细节,真的都是实战换来的经验。