☰
线性卡尔曼滤波从零实现:Python多传感器融合与调参实战
2026/10/16 2:09:39 网站建设 项目流程

1. 从一个状态估计的刚需说起

做机器人、做自动驾驶、做传感器融合,甚至做金融时序预测,绕不开的一个基础问题就是:我手上有一堆带噪声的观测数据,怎么估计出系统真实的状态?这个问题听起来很学术,但落到工程里就是——你的IMU在抖,你的GPS在跳,你的雷达点云有杂波,你怎么把这些乱七八糟的数据揉成一个可信的位置、速度、姿态估计。线性卡尔曼滤波就是解决这个问题最经典、最基础、也最经得起时间考验的工具。

我第一次真正把卡尔曼滤波用进项目里,是在一个多传感器融合的小车上。当时用编码器算里程计,用IMU算航向角,两个传感器各有各的毛病:编码器打滑就飘,IMU积分久了就偏。单独用哪个都不靠谱,但把两者用卡尔曼滤波一融合,轨迹立刻就稳了。那种“原来这东西真的有用”的感觉,比看十篇推导都来得直接。

这篇内容面向的是已经懂一点概率、懂一点线性代数,但还没真正把卡尔曼滤波跑起来的人。我会用Python把线性卡尔曼滤波从零实现一遍,不调库,不黑箱,把每一步的矩阵运算和背后的直觉都讲清楚。你看完之后应该能做到三件事:第一,理解预测和更新这两步到底在干什么;第二,能自己写出一个可运行的线性卡尔曼滤波器;第三,知道调参的时候该动哪个旋钮,以及为什么。

2. 线性卡尔曼滤波到底在解决什么问题

2.1 从“加权平均”的直觉切入

先别急着看公式。卡尔曼滤波的本质,其实就是一个动态的加权平均。你有一个预测值,来自你对系统运动规律的理解;你有一个观测值,来自传感器。两个值都不准,但你希望把它们合起来,得到一个比两者都准的估计。怎么合?按“谁更可信就给谁更大权重”的原则来合。

这个“可信度”用什么衡量?用协方差。预测的协方差大,说明你对预测没底,那就多信观测;观测的协方差大,说明传感器噪声大,那就多信预测。卡尔曼滤波的精妙之处在于,它不是拍脑袋定权重,而是通过概率推导,给出了最优的权重——在系统是线性的、噪声是高斯的这两个前提下,它给出的估计在最小均方误差意义下是最优的。

这就是为什么它叫“最优估计器”。但注意这个“最优”是有条件的,条件不满足的时候它就不是最优,甚至可能发散。这一点后面会展开讲。

2.2 五个核心公式的物理含义

线性卡尔曼滤波的标准形式是五个方程,分成预测和更新两组。我不打算一上来就甩公式,而是先把每个方程在“说什么”讲明白。

预测步有两个方程。第一个是状态预测:用上一时刻的估计,加上控制输入,推算出这一时刻的状态先验估计。说白了就是“根据运动模型,我猜现在在哪”。第二个是协方差预测:把上一时刻的估计不确定性,加上过程噪声,推算出这一时刻先验估计的不确定性。说白了就是“我对我这个猜测有多没底”。

更新步有三个方程。第一个算卡尔曼增益:根据先验不确定性和观测不确定性,算出一个权重矩阵。第二个用增益把观测残差加权,修正先验估计,得到后验估计。第三个更新后验协方差,反映修正之后不确定性降低了多少。

把这五步串起来看,就是一个“猜—看—改”的循环。猜是用模型猜,看是用传感器看,改是按可信度改。理解了这三个字,公式只是把这三个字写成了矩阵。

2.3 为什么工程上偏爱它

有人会问,现在深度学习这么火,为什么还用卡尔曼滤波?原因很实际。第一,它计算量小,矩阵维度通常就几维到几十维,嵌入式平台跑起来毫无压力。第二,它可解释,每一步的协方差都有明确的物理意义,出问题能查。第三,它不需要训练数据,只要有模型和噪声统计就能跑。第四,它递归,不需要存历史数据,来一帧算一帧,内存占用恒定。

在传感器融合这个场景里,这四点几乎每一点都戳中痛点。深度学习方法精度可能更高,但你要标数据、要训练、要部署算力,而且出了错你很难说清楚为什么错。卡尔曼滤波虽然假设强,但在假设基本成立的场景下,它是性价比最高的选择。

3. 用Python从零实现一个线性卡尔曼滤波器

3.1 状态空间建模:先把问题写清楚

写代码之前,必须先把状态空间模型定下来。线性卡尔曼滤波要求系统能写成这样两个方程:

状态方程:x_k = F * x_{k-1} + B * u_k + w_k

观测方程:z_k = H * x_k + v_k

其中x是状态向量,F是状态转移矩阵,B是控制矩阵,u是控制输入,w是过程噪声,服从均值0、协方差Q的高斯分布。z是观测向量,H是观测矩阵,v是观测噪声,服从均值0、协方差R的高斯分布。

我拿一个最经典的例子来贯穿全文:一维匀速直线运动的位置估计。状态向量取位置和速度,x = [p, v]^T。状态转移矩阵F就是:

F = [[1, dt], [0, 1]]

这个矩阵的含义是:新位置 = 旧位置 + 速度 * dt,新速度 = 旧速度。dt是采样间隔。观测只测位置,所以H = [1, 0],观测矩阵把二维状态映射到一维观测。

这个例子简单到可以手算,但麻雀虽小五脏俱全,预测、更新、增益、协方差全都在。把它跑通,换成更复杂的模型只是改矩阵的事。

3.2 预测步的代码实现与参数解读

先看预测步。代码不长,但每一行都有讲究。

import numpy as np def predict(x, P, F, Q, B=None, u=None): # 状态预测 x_pred = F @ x if B is not None and u is not None: x_pred = x_pred + B @ u # 协方差预测 P_pred = F @ P @ F.T + Q return x_pred, P_pred

x_pred = F @ x这一行就是状态外推。P_pred = F @ P @ F.T + Q这一行是协方差外推。注意F @ P @ F.T这个操作,它做的是把不确定性沿状态转移传播。如果F里有dt,那么位置的不确定性会因为速度的不确定性而增长,这个增长就体现在F @ P @ F.T里。

Q是过程噪声协方差,它代表“模型有多不准”。Q给太小,滤波器会过度相信模型,观测来了也不怎么改,响应迟钝;Q给太大,滤波器会过度相信观测,轨迹跟着噪声跳。Q的调法后面专门讲。

3.3 更新步的代码实现与增益含义

更新步是卡尔曼滤波的灵魂,卡尔曼增益K是灵魂中的灵魂。

def update(x_pred, P_pred, z, H, R): # 观测残差 y = z - H @ x_pred # 残差协方差 S = H @ P_pred @ H.T + R # 卡尔曼增益 K = P_pred @ H.T @ np.linalg.inv(S) # 后验状态估计 x_post = x_pred + K @ y # 后验协方差 P_post = (np.eye(len(x_pred)) - K @ H) @ P_pred return x_post, P_post

y = z - H @ x_pred是观测残差,也就是“观测值减去预测的观测值”。这个残差是滤波器用来修正的信息来源。S = H @ P_pred @ H.T + R是残差的协方差,它综合了预测不确定性和观测不确定性。K = P_pred @ H.T @ inv(S)就是增益。

增益的直觉是这样的:如果P_pred很大(预测没底),K就大,修正幅度大;如果R很大(观测很脏),S就大,K就小,修正幅度小。K本质上是一个信噪比驱动的权重。

P_post = (I - K @ H) @ P_pred这一行更新后验协方差。注意这个形式在数值上不是最稳定的,工程上更推荐用Joseph形式:

P_post = (np.eye(n) - K @ H) @ P_pred @ (np.eye(n) - K @ H).T + K @ R @ K.T

Joseph形式能保证协方差矩阵始终对称正定,长期运行时不容易因为浮点误差累积而失去正定性。这个坑我在一个长时间运行的跟踪系统里踩过,跑几个小时之后协方差矩阵出现负特征值,滤波器直接崩掉,换成Joseph形式就好了。

3.4 完整可运行的最小示例

把预测和更新串起来,加上一个模拟的数据源,就是一个完整的最小系统。

np.random.seed(42) dt = 0.1 F = np.array([[1, dt], [0, 1]]) H = np.array([[1, 0]]) Q = np.array([[0.01, 0], [0, 0.01]]) R = np.array([[0.5]]) n_steps = 200 true_p = 0.0 true_v = 1.0 x = np.array([[0.0], [0.0]]) P = np.eye(2) * 1.0 results = [] for i in range(n_steps): true_p += true_v * dt z = np.array([[true_p + np.random.randn() * np.sqrt(R[0,0])]]) x, P = predict(x, P, F, Q) x, P = update(x, P, z, H, R) results.append((true_p, z[0,0], x[0,0]))

这段代码跑下来,你会看到估计值比原始观测平滑得多,而且滞后很小。这就是卡尔曼滤波最直观的效果。你可以把R调大,看估计变得更平滑但更滞后;把Q调大,看估计变得更跟手但更抖。这种手动调参的体验,比看任何理论都更能建立直觉。

4. 调参与工程落地的那些坑

4.1 Q和R的调参逻辑

Q和R是卡尔曼滤波里最需要动手调的两个参数,它们直接决定了滤波器的性格。很多人调参是瞎试,其实有章法。

R的物理意义是传感器噪声协方差,这个通常可以从数据手册或者实测数据里估出来。比如你静止采集一段IMU数据,算方差,那就是R的一个合理起点。R不该乱调,它是有物理依据的。

Q的物理意义是过程噪声,它代表模型没建模到的部分。这个往往没法直接测,只能调。Q的调法有个经验:先给一个较小的值,然后逐渐加大,直到滤波器对突变的响应速度满足你的要求。Q太小,滤波器对机动反应慢;Q太大,滤波器把模型当空气,退化成对观测的平滑。

有一个实用的技巧:把Q和R的比值当作一个整体来调。因为卡尔曼增益本质上取决于P和R的相对大小,而P又由Q驱动。所以真正影响行为的是Q/R的量级关系,而不是绝对值。你可以固定R,只调Q,这样调参维度就降了一维。

4.2 数值稳定性:别让协方差矩阵烂掉

前面提到Joseph形式,这里展开说一下数值稳定性。卡尔曼滤波在理论上协方差矩阵永远对称正定,但在浮点运算里,反复的矩阵乘法和求逆会累积误差,跑久了可能出现不对称甚至负特征值。一旦出现负特征值,增益计算就会出问题,滤波器可能发散。

除了Joseph形式,还有几个手段。第一,对称化:每次更新后强制P = (P + P.T) / 2。第二,用平方根滤波:直接对协方差的Cholesky因子做递推,从根本上保证正定性,代价是计算复杂一些。第三,避免显式求逆:用np.linalg.solve代替inv,数值上更稳。

我在一个跑了连续几天的跟踪系统里,最初没注意这个问题,结果每隔十几个小时滤波器就飘一次。后来加了对称化和Joseph形式,连续跑一周都没事。这种问题在短时间测试里根本发现不了,只有长期运行才暴露。

4.3 观测丢失与异常值处理

实际系统里,观测不是每帧都有的。传感器可能丢帧,可能被遮挡,可能返回NaN。这时候不能硬把无效观测塞进更新步,否则滤波器会被带偏。

标准做法是跳过更新步,只做预测。协方差会因为只预测不更新而持续增长,这是对的,因为你对状态的把握确实在变差。等观测恢复,增益会因为P变大而变大,滤波器会快速把估计拉回来。

异常值处理更麻烦。卡尔曼滤波假设噪声是高斯的,但实际数据里常有离群点。一个离群点如果被当成正常观测,会把估计拽偏很远。常用的手段是卡方检验:算观测残差的马氏距离,如果超过阈值就判定为异常,跳过这次更新。阈值一般取卡方分布的95%或99%分位数,自由度等于观测维度。

def is_outlier(y, S, threshold=9.49): # 自由度=2时,99%分位数约9.21 d = y.T @ np.linalg.inv(S) @ y return d > threshold

这个简单的判断能挡掉大部分离群点,代价是可能误杀一些正常但偏大的残差。阈值的选择要在漏检和误杀之间权衡。

5. 从线性到非线性:什么时候该换工具

5.1 线性假设失效的信号

线性卡尔曼滤波的适用前提是状态方程和观测方程都是线性的。一旦出现非线性,比如观测是距离而状态是位置(涉及开方),或者状态更新涉及角度(涉及三角函数),线性假设就破了。

判断标准很直接:你能不能把状态转移和观测写成矩阵乘法?能,就用线性卡尔曼;不能,就得考虑扩展卡尔曼或者无迹卡尔曼。扩展卡尔曼的做法是在当前估计点对非线性函数做一阶泰勒展开,用雅可比矩阵代替F和H。无迹卡尔曼则是用一组sigma点去传播分布,不线性化,精度更高但计算量更大。

5.2 线性版本仍然值得先跑通

虽然非线性场景更常见,但我强烈建议先把线性版本跑通再上非线性。原因有三。第一,线性版本的五个方程是非线性的基础,扩展卡尔曼只是把F和H换成雅可比,骨架完全一样。第二,线性版本没有雅可比推导的干扰,能让你专注于理解预测和更新的本质。第三,很多看起来非线性的问题,在局部线性化之后用线性版本就能跑得不错,没必要一上来就上复杂工具。

我见过不少人直接上无迹卡尔曼,结果调参调不明白,因为他不理解协方差是怎么传播的。如果先用线性版本建立直觉,再上非线性,理解成本会低很多。

5.3 一个角度估计的扩展思路

举个具体的例子。假设你要估计一个机器人的航向角,状态里包含角度theta。观测可能是某个传感器测到的方向向量。这时候观测方程涉及sin和cos,是非线性的。扩展卡尔曼的做法是对sin和cos在当前theta处求导,得到雅可比,然后用雅可比代替H。

但角度有个特殊问题:周期性。theta和theta+2pi是同一个角度,但数值上差很多。如果状态估计在pi附近,观测在-pi附近,残差会算出一个接近2pi的巨大值,滤波器会被带飞。解决办法是角度归一化:把残差归一化到[-pi, pi]区间。这个细节在纯线性问题里不存在,但在角度相关的非线性问题里是必踩的坑。

6. 常见问题速查与排查思路

6.1 滤波器发散的几种典型原因

滤波器发散是最常见也最让人头疼的问题。发散的表现是估计值越来越偏离真实值,协方差越来越大或者越来越小到不合理的程度。原因通常有这么几类。

第一类是模型和实际不匹配。比如你用匀速模型去跟踪一个频繁机动的目标,模型预测总是错,滤波器就会持续被观测拽着走,表现得很抖。这时候要么换模型,要么加大Q。

第二类是Q或R设置严重不当。Q给得极小,滤波器过度自信,观测来了几乎不改,一旦模型有偏差就永远纠不回来。R给得极小,滤波器把噪声当信号,估计剧烈抖动。

第三类是数值问题。前面说的协方差失去正定性就是典型。表现是跑一段时间后突然崩掉,而不是逐渐变差。

第四类是观测异常值没处理。一个巨大的离群点能把估计拽到很远,如果后续观测不足以拉回来,就发散了。

排查的时候,我习惯先把Q和R打印出来看量级,再把协方差矩阵的特征值打印出来看有没有负的,最后检查观测数据有没有异常值。这三步能覆盖大部分发散问题。

6.2 估计滞后与过平滑的权衡

很多人第一次用卡尔曼滤波会觉得“怎么估计值总是慢半拍”。这是滤波器的固有特性:它在平滑噪声的同时必然引入滞后。滞后的大小取决于增益,增益越小滞后越大。

如果你需要更快的响应,就得加大Q或者减小R,让增益变大。但代价是估计变得更抖。这个权衡没有免费午餐,只能根据应用需求来定。跟踪快速机动的目标,就得接受更抖的估计;跟踪缓慢变化的过程,就可以用更小的增益换更平滑的轨迹。

有一个折中手段是自适应卡尔曼滤波:根据残差的大小动态调整Q或R。残差持续偏大,说明模型不准,加大Q;残差正常,就维持。这样在平稳段平滑,在机动段跟手。实现不复杂,但调参又多了一层,要谨慎使用。

6.3 常见问题速查表

现象可能原因排查手段解决方向
估计持续偏离真值模型不匹配或Q过小检查残差是否持续同号换模型或加大Q
估计剧烈抖动R过小或Q过大看增益是否接近1加大R或减小Q
跑一段时间后崩溃协方差失去正定性打印特征值用Joseph形式并对称化
偶发大幅偏移观测离群点算残差马氏距离加卡方检验门限
响应明显滞后增益过小看增益量级加大Q或减小R
观测丢失后飘走只预测不更新太久看协方差增长限制纯预测步数

这张表是我自己排查问题时总结的,基本覆盖了八成以上的常见故障。实际遇到问题,先对号入座,再深入分析。

7. 我个人的一些实操体会

卡尔曼滤波这东西,看公式觉得简单,真上手调才知道细节多。我最大的体会是:不要试图一次调好,要建立快速迭代的调试环境。把真实数据录下来,离线反复跑,改一个参数看一次结果,比在线瞎调效率高十倍。离线环境里你可以随便试,不用担心把系统搞崩。

第二个体会是可视化比数值更重要。把真值、观测、估计三条曲线画在一张图上,一眼就能看出滤波器是在平滑、在滞后、还是在发散。光看协方差数值,很难建立直觉。我习惯每调一次参数就画一次图,看图调参比看数调参快得多。

第三个体会是先跑通最简单的例子,再往复杂场景迁移。一维匀速是最简单的,跑通了再上二维、再上多传感器、再上非线性。每一步只增加一个复杂度,这样出问题的时候能快速定位是哪一步引入的。一上来就搞复杂系统,出了问题根本不知道从哪查。

第四个体会是记录每次调参的结果。Q和R的组合空间不小,凭记忆很容易乱。我习惯用一个表格记录每次的参数和对应的表现,调着调着就能看出规律。这个习惯在项目交接的时候也特别有用,别人接手能快速理解参数是怎么定下来的。

最后说一个容易被忽略的点:卡尔曼滤波的输出质量,上限取决于模型质量,而不是调参技巧。如果模型本身错了,再怎么调Q和R也只能在“抖”和“滞后”之间选一个不那么差的。所以遇到效果不理想,先回头检查模型假设,别一头扎进调参里出不来。模型对了,参数往往很快就能调好;模型错了,调参就是无底洞。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询