做非线性状态估计的同行,应该都被EKF(扩展卡尔曼滤波器)折磨过。算雅可比矩阵的时候,链式法则一层套一层,微分方程复杂一点,整个人就麻了。即使勉强把导数算出来了,遇到强非线性系统,一阶线性化的近似误差又会把滤波精度拖垮,调试的时候完全不知道是模型问题还是代码问题。后来我转到无迹卡尔曼滤波器算法(UKF),才算是把这个死结解开了——它不需要求导,精度却能达到二阶以上,在MATLAB里实现起来也比EKF直觉得多。这篇就把我从原理理解到代码实现,再到实际调参踩坑的完整过程整理出来,适合正在用Matlab做非线性状态评估、目标跟踪或导航解算的工程师参考。
1. 为什么EKF不够用了:无迹卡尔曼滤波器要解决的问题
1.1 非线性状态评估的现实场景
先说清楚什么叫非线性状态评估。你去跟踪一个转弯的无人机,观测量是雷达的方位角和距离,但状态量是直角坐标系下的位置和速度,从极坐标到直角坐标的转换方程是非线性的。你去估计电池的剩余电量(SOC),开路电压和SOC之间是个带滞回特性的曲线,同样是非线性的。再比如航天器再入大气层时的状态估计,气动加热导致的减速模型,非线性程度更是夸张。
在这些场景里,卡尔曼滤波的标准形式没法直接用,因为它整个推导过程都是建立在线性高斯模型基础上的。工程上最常用的补救办法就是EKF:把非线性函数在当前估计点做一阶泰勒展开,用雅可比矩阵替代线性卡尔曼滤波里的状态转移矩阵和观测矩阵。思路没错,但实际用起来的体验非常差。
1.2 EKF线性化方案的三个硬伤
第一个问题是雅可比矩阵动不动就算不出来。很多系统的状态方程是由查表、数值求解微分方程、甚至分段函数构成的,压根没有解析导数。就算你能手推导数,一个10维状态的雅可比矩阵,里面几十个偏导项,任何一个符号错了,滤波结果都能偏到姥姥家。我在MATLAB里用符号工具箱求过几次雅可比,符号表达式长得让人头皮发麻,最后还要转成数值函数,调试成本极高。
第二个问题是精度不够。一阶泰勒展开本质上是用切线代替曲线,只在展开点附近误差小。如果系统的非线性强度高,比如观测方程里有三角函数、平方项,或者状态更新间隔长,线性化之后的均值和真实均值之间会有明显偏差。更麻烦的是,线性化会系统性地低估协方差,因为所有高阶项都被截断了。协方差一旦被低估,卡尔曼增益就会算错,滤波器要么收敛得很慢,要么干脆发散。
第三个问题是EKF对不连续或不光滑的函数无能为力。现实中很多系统模型带有符号判断或者饱和限幅,导数在这些点不存在。硬要用EKF,就得人为修光滑,修出来的模型和真实系统之间的距离又变成新误差源。
1.3 UKF换了一个角度:分布传不动就传点
UKF能绕开EKF的这些问题,核心思想其实特别朴素:既然状态是随机变量,关心的本质上是它的概率分布,而不是状态函数本身长什么样。那我们就别去近似那个非线性函数了,直接近似状态的分布。怎么近似一个高斯分布呢?用一组精心挑选的点,也就是sigma点,去代表这个分布。把每个sigma点丢进非线性函数里跑一遍,跑出来的点群自然就携带了非线性变换后的信息,再对这些输出点做加权统计,就能得到变换后的均值和协方差。
这批做法的妙处在于,它完全绕开了雅可比矩阵,对系统模型没有光滑性的要求。而且sigma点的选取是二维精确的,什么意思呢?只要非线性函数在展开点附近有连续的二阶导数,UT变换得到的均值和协方差精度就能达到二阶,比EKF的一阶精度高一整个量级。如果非线性函数本身就是二次型的,UT变换甚至能精确复现真实结果。这个性质我在后面会用一个例子验证。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. UT变换的数学内功:sigma点怎么选、权重怎么定
2.1 sigma点构造公式的直觉解读
无迹变换(Unscented Transform,UT)的构造方法看起来是个公式,但背后逻辑其实很像你在一个团队里挑代表去参加调研:不能只让一个人去,因为他只代表自己的观点;得挑几个人,覆盖主流意见和不同极端情况,回来后综合大家的见闻估计整体情况。
公式层面上,给定n维状态向量x,均值是x̄,协方差矩阵是P。构造2n+1个sigma点:
- 第一个点是均值本身:X₀ = x̄
- 剩下的点沿P矩阵的各主轴方向对称散布:Xᵢ = x̄ + (√((n+λ)P))ᵢ,Xᵢ₊ₙ = x̄ - (√((n+λ)P))ᵢ,其中(√((n+λ)P))ᵢ表示矩阵(n+λ)P的Cholesky分解第i列
请注意,这里需要的是矩阵平方根。MATLAB里用chol((n+lambda)*P, 'lower')得到下三角矩阵,它的各列就是sigma点的散布方向。实际写代码的时候这也是个关键细节:用sqrtm做矩阵平方根在数值上远不如chol稳定,尤其是当P接近半正定或者维度较高时,sqrtm返回的结果可能出现微小负特征值,后面算协方差就容易崩。
2.2 三个参数 alpha、beta、kappa 到底在管什么
公式里的 λ = α²(n + κ) - n 是一个缩放参数,它决定了sigma点离均值有多远。三个参数各管一摊:
| 参数 | 典型值 | 作用 | 如果设错了会怎样 |
|---|---|---|---|
| α | 1e-3 ~ 1 | 控制sigma点相对均值的散布半径,越小越靠近中心 | 太小则sigma点过于集中,对强非线性高阶项捕捉变弱;太大则远离中心,对局部特性刻画不足 |
| β | 2(高斯分布最优) | 在权重中引入先验分布的高阶信息 | 非高斯场景下可能需要调整,但多数情况下默认2就好 |
| κ | 0 或 3-n | 次级缩放参数,主要用于保证协方差半正定 | 高维状态时设0可能让协方差失去正定性,可尝试设3-n |
权重公式也很固定:
- 均值的权重:W⁰ₘ = λ/(n+λ)
- 协方差的权重:W⁰_c = λ/(n+λ) + (1 - α² + β)
- 其余对称点权重:Wⁱₘ = Wⁱ_c = 1/[2(n+λ)]
注意到 W⁰_c 比 W⁰ₘ 多了一项(1-α²+β),这就是β发挥作用的地方。在高斯先验下,取β=2可以让四阶矩信息也匹配上,这就是为什么教科书里总是告诉你β取2,不是随便定的。
2.3 手算一个例子:从N(0,1)到y=x²
为了让你直观看到UT为什么比线性化强,我做一个最简单但非常有说服力的手算例子。假设x服从标准正态分布N(0,1),我们要估计y = x²的均值和方差。
先看解析结果。如果x~N(0,1),那么y=x²服从自由度为1的卡方分布,所以E[y]=1,Var[y]=2。这是标准答案。
再看EKF解法。EKF在x̄=0处线性化,y的近似是 y ≈ 0 + 2·x̄·(x-x̄) = 0,导数为0,因此预测均值是0,预测协方差也是0。和真实答案一比,直接错了。这暴露了一阶线性化的致命缺陷:线性化点恰好是函数导数为0的位置,所有信息都被截断了。
再看UT。取n=1,α=1,β=0,κ=2,则λ=1×(1+2)-1=2。sigma点和权重如下:
| sigma点 | 权重(均值) | 权重(协方差) | 非线性输出 y=x² |
|---|---|---|---|
| x₀=0 | 2/3 | 2/3 | 0 |
| x₁=√3 | 1/6 | 1/6 | 3 |
| x₂=-√3 | 1/6 | 1/6 | 3 |
加权后的均值 = 2/3×0 + 1/6×3 + 1/6×3 = 1,和解析值完全一致。加权后的协方差 = 2/3×(0-1)² + 1/6×(3-1)² + 1/6×(3-1)² = 2/3 + 4/6 + 4/6 = 2,也完全一致。
这个例子说明了一个很本质的东西:UT不是碰巧算对了,而是它的设计目标就是让样本均值和样本协方差在二阶及以下的多项式变换下精确成立。只要非线性函数能用泰勒级数展开到二阶,UT的结果就比EKF可靠得多。
3. MATLAB代码一步步拆:从公式到可运行实现
3.1 非增广UKF的算法流程
把UKF的原理变成MATLAB代码之前,先把整个滤波循环的逻辑理顺。UKF的标准流程分两步,和线性卡尔曼滤波的结构类似,只是内部的运算逻辑换成了sigma点传播。
预测步骤:由当前状态的均值和协方差生成sigma点,每个sigma点都通过状态方程传播,传播后的点加权合并,得到预测均值x_pred和预测协方差P_pred,最后加上过程噪声协方差Q。
更新步骤:以预测状态为基础再次生成sigma点(或者直接复用预测步生成的传播后sigma点,取决于实现细节),每个点通过观测方程传播,得到预测观测的均值z_pred、观测协方差Pzz和状态-观测互协方差Pxz,然后按标准卡尔曼增益公式计算K,更新状态和协方差。
3.2 核心函数代码与逐段说明
下面这个实现是经典的对称采样UKF,不增广噪声,适用于过程噪声和观测噪声都是加性高斯的情况。第一步是生成sigma点和权重的辅助函数:
matlab复制function [X, Wm, Wc] = generateSigmaPoints(x, P, lambda, alpha, beta)
% generateSigmaPoints 生成对称采样的sigma点和权重
% x: 当前状态均值,n维列向量
% P: 当前状态协方差矩阵,n×n
% lambda: 缩放参数 λ = alpha^2*(n+kappa)-n
% alpha, beta: UT变换参数
% 输出X: n×(2n+1)矩阵,每列是一个sigma点
% 输出Wm, Wc: 均值权重和协方差权重,行向量
n = numel(x);
X = zeros(n, 2*n + 1);
X(:, 1) = x;
% 使用Cholesky分解求矩阵平方根
P_sqrt = chol((n + lambda) * P, 'lower');
for i = 1:n
X(:, i+1) = x + P_sqrt(:, i);
X(:, i+1+n) = x - P_sqrt(:, i);
end
Wm = zeros(1, 2*n + 1);
Wc = zeros(1, 2*n + 1);
Wm(1) = lambda / (n + lambda);
Wc(1) = lambda / (n + lambda) + (1 - alpha^2 + beta);
for i = 2:2*n + 1
Wm(i) = 1 / (2 * (n + lambda));
Wc(i) = 1 / (2 * (n + lambda));
end
end
这里chol((n+lambda)*P, 'lower')返回下三角矩阵L,满足LL^T = (n+lambda)P。代码里还有一个隐蔽但重要的点:如果chol运行时报错说矩阵不是正定的,那说明当前的P矩阵已经出了数值问题,需要立刻去检查参数,而不是强行继续算。
然后是预测函数:
matlab复制function [x_pred, P_pred] = ukf_predict(x, P, f_func, Q, t, alpha, beta, kappa)
% ukf_predict UKF预测步骤
% f_func: 状态方程函数句柄,形式为 x_next = f_func(x, t)
% Q: 过程噪声协方差矩阵
% t: 当前时刻,用于时变模型
n = numel(x);
lambda = alpha^2 * (n + kappa) - n;
[X, Wm, Wc] = generateSigmaPoints(x, P, lambda, alpha, beta);
n_sigma = 2 * n + 1;
% 每个sigma点通过状态方程传播
Y = zeros(n, n_sigma);
for i = 1:n_sigma
Y(:, i) = f_func(X(:, i), t);
end
% 加权合并预测均值
x_pred = zeros(n, 1);
for i = 1:n_sigma
x_pred = x_pred + Wm(i) * Y(:, i);
end
% 加权合并预测协方差,并叠加过程噪声
P_pred = Q;
for i = 1:n_sigma
d = Y(:, i) - x_pred;
P_pred = P_pred + Wc(i) * (d * d');
end
end
最后是更新函数:
matlab复制function [x_upd, P_upd] = ukf_correct(x_pred, P_pred, h_func, z, R, alpha, beta, kappa)
% ukf_correct UKF更新步骤
% h_func: 观测方程函数句柄,形式为 z_hat = h_func(x)
% z: 当前时刻观测向量
% R: 观测噪声协方差矩阵
n = numel(x_pred);
m = numel(z);
lambda = alpha^2 * (n + kappa) - n;
[X, Wm, Wc] = generateSigmaPoints(x_pred, P_pred, lambda, alpha, beta);
n_sigma = 2 * n + 1;
% 每个sigma点通过观测方程传播
Z = zeros(m, n_sigma);
for i = 1:n_sigma
Z(:, i) = h_func(X(:, i));
end
% 预测观测均值
z_pred = zeros(m, 1);
for i = 1:n_sigma
z_pred = z_pred + Wm(i) * Z(:, i);
end
% 观测协方差和互协方差
Pzz = R;
Pxz = zeros(n, m);
for i = 1:n_sigma
dz = Z(:, i) - z_pred;
dx = X(:, 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
这套代码的模块化程度比较高,预测和更新分开,调试的时候可以单独验证某一部分的问题。另一个值得说明的设计是,两个函数都接收alpha、beta、kappa参数,但没有做成全局变量,这样在批量跑参数扫描的时候,可以很方便地在循环里修改UT参数。
3.3 用强非线性算例验证算法正确性
光有代码不行,得跑一个能说明问题的算例。我选一个经典的一维强非线性系统,来自Julier和Uhlmann的原论文,模型如下:
xₖ = 0.5·xₖ₋₁ + 25·xₖ₋₁/(1+xₖ₋₁²) + 8·cos(1.2·(k-1)) + wₖ₋₁
zₖ = xₖ²/20 + vₖ
其中w是过程噪声,方差Q=2;v是观测噪声,方差R=1。这个系统有两个吸引子,状态会在正负区域之间跳转,非线性强度很高,是EKF很容易翻车的场景。
验证脚本如下:
matlab复制%% UKF验证:非线性的双稳态系统估计
clear; clc; close all;
T = 50; % 仿真步数
x_true = zeros(1, T+1); % 真实状态
x_ukf = zeros(1, T+1); % UKF估计
P = 1; % 初始协方差
Q = 2; % 过程噪声方差
R = 1; % 观测噪声方差
% UT参数,实际工程中常用的组合
alpha = 1e-3;
beta = 2;
kappa = 0;
% 状态方程,这里的k是时间序号
f_func = @(x, k) 0.5*x + 25*x./(1+x.^2) + 8*cos(1.2*(k-1));
% 观测方程
h_func = @(x) x.^2 / 20;
x_true(1) = 0.1;
x_ukf(1) = 0.1;
for k = 1:T
% 生成真实状态和观测
w = sqrt(Q) * randn;
v = sqrt(R) * randn;
x_true(k+1) = f_func(x_true(k), k) + w;
z_meas = h_func(x_true(k+1)) + v;
% UKF预测和更新
[x_ukf(k+1), P] = ukf_predict(x_ukf(k), P, f_func, Q, k, alpha, beta, kappa);
[x_ukf(k+1), P] = ukf_correct(x_ukf(k+1), P, h_func, z_meas, R, alpha, beta, kappa);
end
%% 绘图对比
t_axis = 0:T;
figure;
plot(t_axis, x_true, 'k-', 'LineWidth', 1.5); hold on;
plot(t_axis, x_ukf, 'r--', 'LineWidth', 1.5);
legend('真实状态', 'UKF估计');
xlabel('时间步 k'); ylabel('状态值');
title('UKF强非线性状态估计结果');
grid on;
%% 计算均方根误差
rmse_ukf = sqrt(mean((x_true(2:end) - x_ukf(2:end)).^2));
fprintf('UKF RMSE = %.4f\n', rmse_ukf);
我实际跑过这个脚本,UKF的估计轨迹能很好地跟踪上真实状态,即使系统在正负吸引子之间切换时,滤波器也能在几步之内把自己拉回来。而同样的模型如果换成EKF,因为观测方程x²/20在x=0附近导数为0,初始估计很容易锁死在错误的符号上,一旦系统状态跳到另一个吸引子,EKF常常跟不上。
4. 参数调优与发散排查:我踩过的那些坑
4.1 协方差矩阵怎么突然就失去正定性了
用MATLAB跑UKF,最典型的报错是chol函数提示矩阵必须是正定的。这个错误出现的时机很刁钻,往往是程序已经运行了几十步之后才崩掉。一开始我以为是模型写错了,查了几天才发现是协方差矩阵在数值上慢慢退化,最终丢掉正定性。
罪魁祸首通常是两个。第一个是过程噪声Q给太小,导致预测步协方差增长不够,而更新步的观测噪声R又给得较大,导致P矩阵每一项都在缩小,最后精度不足,特征值变成负数。第二个是UT参数设置不当,尤其是κ取0且状态维度较高时,协方差权重里可能出现负值,数值上会让矩阵失去半正定保证。
解决办法分两步:一是检查Q和R的量级,确保它们和物理过程的噪声量级匹配,不要凭感觉往小了调;二是给P矩阵加一个微小扰动作为保底,比如每次更新后保证P = (P + P')/2 + 1e-12*eye(n),这能消除非对称误差并兜底数值下溢。
4.2 alpha、beta、kappa怎么配才不容易翻车
很多入门教程直接给推荐值:alpha=1e-3,beta=2,kappa=0。这个组合在各种场景下都比较稳,但不是万能的。我在调试中遇到的一个真实情况是,对一个高维系统(状态维度12),用kappa=0跑出来的结果时好时坏,协方差经常性非正定,后来改成kappa=3-n,问题立刻消失。
原因也简单:当n比较大时,lambda=alpha²(n+kappa)-n,如果kappa=0且alpha很小,lambda会接近于一个负值很大的数,这时候权重可能变得很大或者为负,数值稳定性就差。所以高维系统里,要么把kappa设为3-n来调整lambda,要么适当增大alpha到0.01甚至0.1,让sigma点分散一点。
还有个容易被忽略的细节:alpha太小会让sigma点非常靠近均值,如果系统模型的非线性函数在均值附近变化平缓,这种采样方式没问题;但如果函数在该处曲率很大,sigma点太近就捕捉不到非线性特征。所以遇到强非线性问题时,我一般会跑一个alpha扫描,从1e-3到0.5,观察RMSE变化曲线,选一个相对稳定的值。
4.3 滤波发散怎么判断、怎么救
滤波器发散和普通的估计误差大有区别。程序能正常跑完,但你画出的估计轨迹会出现一次大的跳变,然后系统再也回不到真实状态附近。这种发散比报错更难排查。
我判断发散的经验有两个。第一个是看新息序列(innovation,即z-z_pred)的统计特性。如果滤波器健康,新息应该近似零均值白噪声,并且它的实际协方差应该接近Pzz的理论值。如果新息连续出现同号大偏差,或者实测协方差比理论值大一个数量级以上,基本可以断定滤波器已经失去跟踪能力。
第二个是看协方差矩阵P的迹(trace)随时间的变化。滤波正常时,P的迹会收敛到一个相对稳定的水平。如果发现P的迹不断变小,一直缩到接近零,但估计误差却很大,说明P是“伪收敛”了,滤波器过于相信自己的估计。这时候的处理办法包括:增大初始P0告诉滤波器初始不确定度较大;适当增大Q告诉滤波器过程模型也不是完全可信;或者检查观测模型是否有明显的线性化误区,比如观测函数存在多值性而模型只表达了其中一个分支。
UKF虽然比EKF健壮,但也不是万能的。它的底层假设仍然是“状态后验分布近似高斯”,如果系统严重非高斯,比如量测野值很多,UKF照样会翻车。这种情况下就得考虑粒子滤波或者其他更重的手段了。
5. 从Demo走向工程:噪声匹配与状态扩维
5.1 Q/R矩阵的调法不是玄学
很多人跑Demo时随便设Q和R,跑通就完事。但一旦进入实际项目,Q和R的设定直接决定滤波效果上限。Q描述的是过程模型的误差,R描述的是传感器的误差。实际调参时我有几个习惯:
先做数据标定。采集一段静止状态下传感器的输出,直接计算观测噪声方差R,这一步只靠统计就能搞定,不要拍脑袋。后标定Q,在已知真实轨迹的测试数据上跑滤波器,通过残差诊断逐步调整Q的数值。如果估计结果滞后,说明Q偏小,滤波器过于依赖过时的状态;如果估计结果毛刺很多、来回抖动,说明Q偏大,滤波器被噪声带着走。
5.2 增广状态UKF与非增广UKF怎么选
之前给出的代码是非增广形式,假设过程噪声和观测噪声都是加性的。这个假设在很多系统里成立,但不是所有系统都成立。如果噪声本身是通过非线性系统进入状态方程的,比如噪声乘在状态变量上(乘性噪声),或者噪声经过了一个非线性执行机构,那么非增广形式的噪声协方差叠加就是近似处理,会低估真实不确定性。
这时候就要用增广状态UKF:把过程噪声和观测噪声作为额外的状态维度扩进sigma点生成过程,让噪声样本也经过完整的非线性函数传播。增广后状态维度从n变为n+n_w+n_v,sigma点数量从2n+1变成2(n+n_w+n_v)+1,计算量明显上去了。选择依据其实很直接:如果系统模型的非线性弱,噪声小,非增广完全够用;如果问题本身就很强非线性,或者噪声量级不可忽略,增广带来的精度提升是值得的。
5.3 在更多领域怎么套用这套框架
UKF的状态方程和观测方程都是用函数句柄传入的,这个设计让整套框架可以复用到任何能用数学表达式描述的系统里。我这里试过的场景包括:锂电池SOC估计(观测方程是OCV-SOC查表曲线)、带惯导误差模型的组合导航(状态维度15,观测是GPS位置速度)、以及车辆横摆角速度估计这种典型的非线性问题。
值得提醒的是,当状态维度升高后,UT变换的实现细节差异会变得明显。比如非局部效应问题:当n较大时,sigma点离均值平均距离变大,权重可能出现负值,这就需要用更精细的采样策略,比如尺度化UT或者球形采样。MATLAB里实现高维UKF时,我建议先用维度较低的场景验证代码正确性,再一步一步往上加状态,不要一上来就怼一个15维系统,不然出了问题根本不知道从哪查起。
从我个人使用经验来看,UKF在大部分非线性状态评估场景里是“性价比最高的滤波器”——实现难度低于粒子滤波,精度又显著优于EKF,对模型的友好程度很高。做滤波算法的,工具箱里放一套好用的UKF,能解决一大半日常问题。
