1. 为什么无迹变换值得单独拿出来聊
非线性系统的状态估计一直是个让人头疼的问题。不管是做组合导航、目标跟踪,还是机器人定位,只要系统模型里出现了非线性函数,经典卡尔曼滤波那一套线性假设就直接失效了。工程上最常见的做法是扩展卡尔曼滤波(EKF),通过一阶泰勒展开把非线性函数局部线性化,然后套用标准卡尔曼滤波的框架。这个思路简单直接,但它有个绕不开的硬伤:雅可比矩阵。
雅可比矩阵的推导有多麻烦,做过EKF的人都有体会。模型稍微复杂一点,求导过程就能写满两页纸,而且一旦模型有不可导点或者强非线性区域,一阶线性化的误差会大到让滤波器发散。更现实的问题是,很多工程场景下模型是黑箱的,你根本拿不到解析形式的雅可比,数值差分求导又会引入额外误差和计算量。无迹变换(Unscented Transform,简称UT)就是冲着这个痛点来的。
无迹变换的核心思想其实非常朴素:与其费劲去线性化一个非线性函数,不如选一组有代表性的采样点,让这些点直接穿过非线性函数,然后从变换后的点集里恢复出均值和协方差。这组采样点就是所谓的Sigma点。它不近似函数本身,而是近似概率分布的统计特性。这个思路绕开了求导,天然支持黑箱模型和不可导函数,精度上通常能做到二阶甚至更高阶,对高斯输入而言,UT对均值和协方差的近似至少是二阶精度的,而EKF只有一阶。
这篇文章适合谁看?如果你正在做状态估计相关的项目,被EKF的雅可比推导折磨过,或者发现滤波器在强非线性场景下频繁发散,那UT和基于它的无迹卡尔曼滤波(UKF)就是你该认真考虑的工具。如果你只是听说过UT但没实际写过代码,这篇文章会从原理到实现到避坑,把整个链路讲透。我会用Python给出可以直接跑的代码,同时补充参数选择、数值稳定性处理和常见故障排查的经验。
需要先说明一点:UT本身不是一个完整的滤波器,它是一个“变换”工具,负责把随机变量通过非线性函数后的统计特性算出来。把它嵌到卡尔曼滤波的预测和更新步骤里,才构成UKF。理解UT是理解UKF的前提,所以这篇文章聚焦在UT本身,但会自然地延伸到它在滤波框架里的用法。
2. 无迹变换的核心原理拆解
2.1 Sigma点采样:用少量点抓住分布的关键信息
UT的第一步是选Sigma点。给定一个n维随机变量,均值为(\bar{x}),协方差为(P_{xx}),标准的对称采样策略会生成(2n+1)个Sigma点。为什么是(2n+1)?因为对每个维度,我们需要在均值两侧各取一个点来捕捉该维度的分布展宽,加上中心点本身,就是(2n+1)个。这个数量是线性的,不会随维度爆炸,这也是UT计算量可控的原因。
具体的采样公式如下。先对协方差矩阵做Cholesky分解或者矩阵平方根分解,得到(L),满足(LL^T = P_{xx})。然后:
- 中心点:(\chi_0 = \bar{x})
- 正向点:(\chi_i = \bar{x} + \sqrt{n+\kappa} \cdot L_i),(i=1,...,n)
- 负向点:(\chi_{i+n} = \bar{x} - \sqrt{n+\kappa} \cdot L_i),(i=1,...,n)
这里(L_i)是矩阵(L)的第(i)列,(\kappa)是一个尺度参数。(\sqrt{n+\kappa})这个因子决定了Sigma点离均值有多远。(\kappa)的取值直接影响采样点的散布范围,后面会专门讲怎么选。
我习惯把这个过程理解成“用探针去探测非线性函数的形状”。每个Sigma点就像一个探针,带着权重信息穿过函数,回来之后我们根据它们落点的分布反推出统计特性。中心点的权重最大,两侧点的权重较小,这样既能捕捉均值附近的集中趋势,又能感知分布边缘的非线性弯曲。
2.2 权重计算:均值权重和协方差权重的区别
Sigma点生成之后,每个点都对应两组权重:一组用于计算变换后的均值(W_i^{(m)}),一组用于计算变换后的协方差(W_i^{(c)})。这两组权重在中心点上是不同的,在两侧点上相同。这是UT里很容易被忽略的细节。
设(\lambda = \alpha^2(n+\kappa) - n),其中(\alpha)控制Sigma点离均值的散布程度,通常取一个很小的值如(1e-3)。参数(\kappa)通常取(0)或(3-n)。当采用比例修正的采样时,权重公式为:
- 中心点均值权重:(W_0^{(m)} = \lambda / (n+\lambda))
- 中心点协方差权重:(W_0^{(c)} = \lambda / (n+\lambda) + (1 - \alpha^2 + \beta))
- 两侧点权重:(W_i^{(m)} = W_i^{(c)} = 1 / (2(n+\lambda))),(i=1,...,2n)
(\beta)这个参数用来融入分布的先验知识。对高斯分布,(\beta=2)是最优的,因为高斯分布的四阶矩有已知形式,这个取值能让协方差的计算精度更高。如果分布明显非高斯,(\beta)可以调小,但实践中大部分场景还是默认(2)。
有个坑我得提前说:(\lambda)不能取太小,否则中心点的协方差权重(W_0^{(c)})可能变成负数,导致协方差矩阵非正定,滤波器直接崩。这个在后面排查部分会详细讲。
2.3 变换过程:从采样到统计量恢复
完整的一次UT变换分三步。第一步按上面的公式生成Sigma点集。第二步把每个Sigma点代入非线性函数(y = f(x)),得到变换后的点集(\gamma_i = f(\chi_i))。第三步用权重加权求和,恢复出变换后随机变量的均值(\bar{y})和协方差(P_{yy}):
- 均值:(\bar{y} = \sum_{i=0}^{2n} W_i^{(m)} \gamma_i)
- 协方差:(P_{yy} = \sum_{i=0}^{2n} W_i^{(c)} (\gamma_i - \bar{y})(\gamma_i - \bar{y})^T)
如果需要计算互协方差(比如在UKF的更新步骤里),还要加上交叉项:(P_{xy} = \sum_{i=0}^{2n} W_i^{(c)} (\chi_i - \bar{x})(\gamma_i - \bar{y})^T)。
整个过程没有任何求导操作,函数(f)可以是任意形式,只要你能对它求值就行。这一点对工程实现极其友好。我做项目的时候,很多模型是用查表或者数值仿真给出的,根本没有解析表达式,EKF在这种场景下基本没辙,而UT可以无缝使用。
2.4 精度来源:为什么它比一阶线性化强
UT精度的直观解释是:它用一组确定性的采样点来“统计”非线性变换的效果,而不是假设变换近似线性。对高斯输入,UT对均值的近似精度至少到二阶,协方差也到二阶,而EKF对均值的近似只有一阶。当非线性程度中等时,UT的估计误差通常比EKF小一个量级。
举个具体的例子。考虑一个二维极坐标到笛卡尔坐标的变换,角度分量有较大的不确定性。EKF在这个变换上做线性化时,角度项的一阶近似会丢失掉一部分信息,导致估计偏差。而UT的Sigma点会沿着角度方向展开,穿过cos和sin函数后被正确地映射到笛卡尔空间的弧形分布上,恢复出来的协方差能反映真实的弯曲形状。
还有一个更工程化的好处:UT天然处理加性噪声和非加性噪声。对于非加性噪声,我们可以把状态和噪声拼成一个增广向量,一起做UT变换。EKF处理非加性噪声时需要额外的雅可比项,推导容易出错,UT则统一处理,代码结构更干净。
3. 无迹变换的完整实现与参数选择
3.1 从零写一个UT函数
下面这段Python代码实现了标准UT变换,输入是均值向量、协方差矩阵、非线性函数和参数配置,输出是变换后的均值和协方差。我把它写成一个独立函数,方便嵌入到任何滤波框架里。
import numpy as np def unscented_transform(mean, cov, f, alpha=1e-3, beta=2.0, kappa=0.0): n = mean.shape[0] lam = alpha**2 * (n + kappa) - n # 权重计算 Wm = np.full(2*n + 1, 1.0 / (2*(n + lam))) Wc = np.full(2*n + 1, 1.0 / (2*(n + lam))) Wm[0] = lam / (n + lam) Wc[0] = lam / (n + lam) + (1 - alpha**2 + beta) # Sigma点生成 sigma_points = np.zeros((2*n + 1, n)) sigma_points[0] = mean # 矩阵平方根,用Cholesky保证数值稳定性 try: L = np.linalg.cholesky((n + lam) * cov) except np.linalg.LinAlgError: # 协方差非正定时的兜底处理 cov_reg = cov + np.eye(n) * 1e-9 L = np.linalg.cholesky((n + lam) * cov_reg) for i in range(n): sigma_points[i + 1] = mean + L[:, i] sigma_points[n + i + 1] = mean - L[:, i] # 变换后的点 transformed = np.array([f(pt) for pt in sigma_points]) if transformed.ndim == 1: transformed = transformed.reshape(-1, 1) # 恢复均值和协方差 mean_out = np.zeros(transformed.shape[1]) for i in range(2*n + 1): mean_out += Wm[i] * transformed[i] cov_out = np.zeros((transformed.shape[1], transformed.shape[1])) for i in range(2*n + 1): diff = transformed[i] - mean_out cov_out += Wc[i] * np.outer(diff, diff) return mean_out, cov_out这段代码有几个细节值得说。矩阵平方根我用的是Cholesky分解,它比直接用np.linalg.sqrtm快很多,而且数值性质更好。Cholesky要求矩阵正定,如果协方差矩阵因为数值误差出现非正定,会抛异常,所以我加了一个兜底的正则化处理。这个兜底在实际项目里非常有用,尤其是长时间运行的滤波器,协方差矩阵偶尔会因为浮点误差失去正定性,没有这个保护程序会直接崩掉。
3.2 参数(\alpha)、(\beta)、(\kappa)的选择逻辑
三个参数里,(\alpha)最需要小心。它决定了Sigma点离均值多远。(\alpha)越小,Sigma点越靠近均值,对局部非线性的捕捉更精细,但协方差权重(W_0^{(c)})会趋向负值,数值稳定性变差。(\alpha)越大,采样点越分散,能覆盖更大的分布范围,但可能采到非线性很强甚至不合理的区域。
实践中我一般把(\alpha)放在(1e-3)到(1)之间。对于强非线性系统,比如角度量参与的状态,(\alpha=1e-3)是常见起点。但如果发现滤波器收敛太慢或者协方差收缩过度,可以适当加大到(0.1)甚至(0.5),让采样点覆盖更宽。这个参数没有万能值,需要根据具体问题的非线性程度和状态维度试出来。
(\kappa)的作用是调整采样点相对于均值的比例。当状态维度(n)较大时,取(\kappa = 3 - n)能让(n+\kappa = 3),避免高维情况下采样点离均值过远。低维时(\kappa=0)是常用默认值。如果状态维度超过10,我建议直接取(\kappa=3-n),并配合较大的(\alpha),否则(n+\lambda)太小会导致Sigma点过度集中,失去采样意义。
(\beta)就比较省心了,高斯假设下取(2)几乎总是对的。它的作用是修正协方差中心点权重,让高斯分布的四阶矩被正确计入。只有当你明确知道分布有重尾或明显非高斯时,才需要调它,而且调整空间不大。
下表是我在不同场景下的参数配置经验值,可以直接参考:
| 场景类型 | 状态维度 | alpha | beta | kappa | 说明 |
|---|---|---|---|---|---|
| 低维弱非线性 | 2-4 | 1e-3 | 2 | 0 | 标准配置,稳定性最好 |
| 低维强非线性 | 2-4 | 0.1-0.5 | 2 | 0 | 加大散布以捕捉弯曲 |
| 中维一般场景 | 5-10 | 1e-3 | 2 | 3-n | 用kappa控制采样范围 |
| 高维保守配置 | >10 | 0.5-1 | 2 | 3-n | 防止采样点过度集中 |
| 非高斯明显 | 任意 | 1e-3 | 0-1 | 0 | 降低beta权重 |
3.3 把它嵌进卡尔曼滤波:预测和更新的分工
UT单独用只能算“给定输入分布,推输出分布”。在滤波框架里,它要参与两个环节。预测步骤把上一时刻的状态分布通过状态转移函数(f)做UT变换,得到先验均值和协方差,再加上过程噪声。更新步骤把先验状态通过观测函数(h)做UT变换,得到预测观测,然后计算互协方差,最后算卡尔曼增益。
这里有个实现上的关键点:加性噪声和非加性噪声的处理方式不同。如果过程噪声是加性的,即(x_{k+1} = f(x_k) + w_k),那就在UT变换之后直接把噪声协方差加到结果上。如果噪声是非加性的,比如(x_{k+1} = f(x_k, w_k)),就需要把状态和噪声拼成增广向量一起做UT,增广后的维度是(n + q)。增广方式精度更高但计算量更大,维度也更高,对参数选择更敏感。
我做工程实现时通常优先假设加性噪声,因为大部分物理系统的噪声确实可以建模成加性的,而且计算量小一半。只有当噪声明显以乘性方式进入模型(比如传感器增益带噪声)时,才用增广方式。
4. 常见问题与排查技巧实录
4.1 协方差非正定:最频繁的翻车点
UT相关实现里最高频的错误就是协方差矩阵非正定,滤波器跑几步就发散或者直接报错。根因通常有两个:一是(W_0^{(c)})变成负数,二是浮点累积误差。
(W_0^{(c)} = \lambda/(n+\lambda) + (1 - \alpha^2 + \beta)),当(\alpha)很小而(\beta)不够大时,这个值可能为负。中心点协方差权重为负意味着在协方差恢复时,中心点的贡献是“减去的”,这会破坏正定性。解决办法是把(\alpha)调大,或者换用保证权重非负的采样策略。
浮点误差导致的非正定更隐蔽。长期运行的滤波器里,协方差矩阵的对称性和正定性会因为数值累积逐渐退化。我的标准做法是在每次协方差计算后做一次对称化处理(P = (P + P^T)/2),并且在Cholesky分解失败时加上一个很小的对角正则项。这个正则项一般取(1e-9)到(1e-6),具体值看状态量的量纲。量纲大的状态用大一点的正则项。
还有一个更主动的做法是改用平方根形式的UT,直接对协方差的平方根做传播,从原理上保证结果正定。平方根UKF的实现复杂度高一些,但在长航时、高可靠性场景下值得投入。
注意:如果Cholesky分解频繁失败,不要只是无限加大正则项,那说明参数配置本身有问题,应该回头检查alpha和kappa的取值是否让采样点分布不合理。
4.2 采样点跑到不合理区域怎么办
非线性函数往往有定义域限制。比如角度归一化函数只在([-\pi, \pi])有意义,某些物理模型在特定区域会发散。Sigma点是根据协方差生成的,如果协方差很大,采样点可能落到函数没有定义的地方,导致变换结果出现NaN或者荒谬值。
我遇到过一个典型案例:姿态估计里,四元数的某个分量协方差被高估,Sigma点生成后跑出了单位球约束之外,归一化之后分布完全扭曲,滤波器输出开始乱跳。解决思路有三条。第一条是限制协方差的最大值,在预测步骤里对协方差做上限截断,防止它无界增长。第二条是在非线性函数里加入合法域投影,把越界的点拉回合理范围,但这会引入偏差,要慎用。第三条是换用更适合流形状态的UT变体,比如在四元数流形上直接定义Sigma点的加减操作,而不是用欧氏空间的向量加减。
实际项目中,协方差上限截断是最简单有效的。我通常根据物理量的合理范围设定协方差上限,比如角度协方差不超过((\pi/2)^2),速度协方差不超过某个物理极限的平方。这个操作看似粗暴,但能挡住大部分异常发散。
4.3 滤波器收敛慢或估计有偏
如果UT滤波器收敛比预期慢,或者稳态估计存在固定偏差,通常不是UT本身的问题,而是参数或模型的问题。先查(\alpha)是不是太小,导致Sigma点过于集中在均值附近,非线性信息没被充分采样。把(\alpha)从(1e-3)提到(0.1)试试,很多时候收敛速度会明显改善。
再查过程噪声和观测噪声的比值。UT滤波器对噪声协方差的设定比EKF更敏感,因为Sigma点的散布直接由协方差决定。过程噪声设太小,Sigma点会过度自信地集中在先验附近,观测信息无法有效修正状态;过程噪声设太大,Sigma点散布过宽,可能采到非线性剧烈的区域,引入偏差。这个比值需要在调试阶段仔细调。
还有一类偏差来自非加性噪声的近似处理。如果实际系统噪声是非加性的,但代码里按加性处理了,就会产生系统性偏差。判断方法是看残差序列是否有色(自相关),如果残差不是白噪声,基本可以确定模型处理有问题。
下面这张表汇总了常见症状和对应排查方向,可以当速查表用:
| 症状 | 可能原因 | 优先排查项 | 解决方向 |
|---|---|---|---|
| 协方差非正定崩溃 | 权重为负或浮点误差 | Wc[0]符号 | 调大alpha,加正则项 |
| 输出NaN | 采样点越界 | 协方差幅值 | 协方差上限截断 |
| 收敛慢 | alpha过小 | alpha取值 | 增大alpha至0.1以上 |
| 稳态有偏 | 噪声模型不符 | 残差自相关 | 改用增广UT处理非加性噪声 |
| 估计值抖动大 | 过程噪声过大 | Q矩阵 | 减小过程噪声 |
| 观测修正无力 | 观测噪声过大 | R矩阵 | 减小观测噪声或检查观测函数 |
4.4 维度升高后的性能衰减
UT的计算量主要来自Sigma点数量(2n+1)和每个点的函数求值。状态维度到几十维时,函数求值次数线性增长,如果函数本身很贵(比如涉及数值积分或查表),总耗时就会成为瓶颈。更麻烦的是,高维下Sigma点更容易采到非物理区域,数值稳定性变差。
我的经验是,状态维度超过20时,就要认真考虑降维或者改用其他估计方法。常见的降维手段包括:把慢变状态从滤波状态里剥离出去单独估计,把强相关状态合并,或者用子空间方法只估计主要模态。如果确实需要高维UT,建议用(\kappa=3-n)配合较大的(\alpha),并且对协方差矩阵做特征值截断,把不重要的方向裁掉,减少有效维度。
另外,高维场景下我强烈建议用平方根实现。普通UT在高维时协方差矩阵的条件数容易变差,平方根形式能显著改善数值稳定性,虽然单步计算量略大,但换来的可靠性提升是值得的。
5. 典型应用场景与效果分析
5.1 组合导航中的姿态与位置估计
组合导航是UT用得非常多的场景。惯性器件和卫星导航组合时,姿态运动学方程是强非线性的,尤其是涉及四元数或方向余弦矩阵的部分。EKF在这里要做复杂的雅可比推导,而且线性化误差在剧烈机动时会明显增大。用UT处理姿态传播,Sigma点直接穿过非线性运动学方程,避免了求导,姿态估计精度在中等机动下通常比EKF好。
我在一个车载组合导航项目里对比过两种方案。直线行驶时两者差别不大,但在连续转弯和颠簸路面,EKF的姿态误差会出现周期性波动,而UT方案的误差曲线明显更平滑。原因就是UT更好地捕捉了姿态运动学在机动条件下的非线性特性。代价是UT的单步计算量比EKF大约多50%到80%,取决于状态维度,在嵌入式平台上需要权衡。
5.2 目标跟踪中的机动目标建模
机动目标跟踪是另一个UT的主场。目标的运动模型在机动发生时高度非线性,转弯率、加速度都可能突变。EKF对机动模型线性化时,容易在机动起始和结束的瞬间产生较大误差,表现为跟踪轨迹的滞后或超调。UT的Sigma点能更好地覆盖机动带来的分布变化,跟踪滞后更小。
实际做的时候,我一般把UT和交互多模型(IMM)结合起来用。IMM负责在多个运动模型之间切换,每个模型内部用UT做状态估计。这样既处理了模型切换,又处理了单模型内的非线性。这个组合在工程上比较成熟,参数调好之后对各类机动都有不错的适应性。
5.3 传感器融合中的非高斯处理
多传感器融合时,各传感器的噪声特性往往不是严格高斯的。比如某些测距传感器在近距离时噪声小且接近高斯,远距离时噪声变大且出现重尾。UT本身假设高斯,但通过调整(\beta)和采样策略,可以在一定程度上适应轻度非高斯。更彻底的做法是用粒子滤波,但计算量太大,实时性难保证。
我的折中做法是分层处理:主状态用UT做高斯近似,对明显非高斯的传感器观测做单独的偏置补偿或野值剔除,再把修正后的观测送进UT更新。这样既保留了UT的计算优势,又缓解了非高斯带来的偏差。实测下来,在传感器噪声轻度非高斯的场景,这个方案的估计精度和纯高斯假设相比能提升20%到30%,而计算量增加有限。
5.4 和EKF、粒子滤波的选型对比
选型上,我的判断逻辑是这样的。如果系统弱非线性、状态维度低、对实时性要求极高,EKF足够用,没必要上UT。如果系统中等或强非线性、模型求导困难或者是黑箱、状态维度在合理范围(一般小于20),UT是性价比最高的选择。如果分布严重非高斯、多峰、状态维度低且算力充裕,才考虑粒子滤波。
UT和EKF的精度差距随非线性程度增大而增大。弱非线性时两者精度接近,UT略好但优势不明显;强非线性时UT的优势能到一个量级。计算量上,UT大约是EKF的1.5到3倍,取决于状态维度和函数求值成本。这个代价在多数现代处理器上是可以接受的,除非是极低功耗的嵌入式场景。
粒子滤波精度上限最高但计算量是UT的几十到几百倍,而且有粒子退化和样本枯竭的问题,参数调节也比UT麻烦得多。除非UT明显不够用,否则我不建议一上来就上粒子滤波。
| 方法 | 非线性适应性 | 计算量 | 求导需求 | 非高斯适应性 | 典型适用场景 |
|---|---|---|---|---|---|
| EKF | 弱 | 低 | 必须 | 差 | 实时嵌入式,弱非线性 |
| UT/UKF | 中到强 | 中 | 不需要 | 中等 | 组合导航,目标跟踪 |
| 粒子滤波 | 极强 | 极高 | 不需要 | 强 | 低维强非高斯,算力充裕 |
6. 我个人在UT实践中的几点体会
写到这里,原理、实现、排查、选型都覆盖了。最后分享几个我在实际项目里踩出来的体会,都是文档里不常写的东西。
第一个体会是:UT的调试应该从简单场景开始。我见过不少人一上来就把UT塞进完整的复杂系统,结果滤波器不收敛,根本不知道是UT参数问题还是模型问题。正确做法是先构造一个已知答案的简单非线性变换,单独测UT函数本身,确认均值和协方差恢复正确,再接入滤波框架。这样出问题时能快速定位。
第二个体会是:不要迷信默认参数。(\alpha=1e-3)虽然是教科书默认值,但它在很多实际场景里偏小,尤其是状态维度高或者非线性强的时候。我现在的习惯是先用默认值跑通,然后做一组参数扫描,看(\alpha)从(1e-3)到(1)之间哪个值让收敛速度和稳态精度综合最好。这个扫描花不了多少时间,但收益很明显。
第三个体会是:协方差矩阵的维护比想象中重要。UT对协方差的数值状态很敏感,一旦它失去正定性或对称性,滤波器就会慢慢跑偏。我现在养成的习惯是每次迭代后都做对称化,并且监控协方差矩阵的最小特征值,一旦接近零或者变负就触发正则化。这个监控逻辑看起来多余,但在长航时运行中救过我好几次。
第四个体会是:UT和EKF不是非此即彼。在同一个系统里,不同状态量可以用不同的处理方式。变化平缓、近似线性的状态用EKF处理,非线性强的状态用UT处理,混合架构有时比纯UT更高效。我做过一个混合方案,把线性状态和姿态状态分开,线性部分用标准卡尔曼,姿态部分用UT,整体计算量比纯UT降了三成,精度基本没损失。这个思路值得在复杂系统里试试。