做时序分析的人,迟早会撞上卡尔曼滤波。这个算法在状态空间模型里几乎就是“下篇”的代名词:上一篇文章我们把状态空间模型的框架搭好,状态方程、观测方程都摆出来了,接下来最核心的问题就是——怎么从一堆带噪声的观测里,把真正想要的那个隐含状态实时估计出来。卡尔曼滤波干的就是这件事。
很多入门教程喜欢一上来把五个公式排开,然后告诉你“看,这就是卡尔曼滤波”。但公式背下来之后,一遇到实际数据还是会懵:Q 设多大?P0 怎么给?为什么滤波结果明显滞后?为什么干脆发散了?这篇的内容不是推公式,而是把卡尔曼滤波当作一个“能用的工具”来拆解,结合可运行的一维和多维示例,聊清楚每个参数到底是什么、在实际项目里怎么调试。无论你是做传感器融合、金融时序建模,还是设备趋势预测,这篇文章都能帮你绕过那些我自己踩过的坑。
1. 先把状态空间模型这盘棋盘明白
1.1 状态方程和观测方程到底在描述什么
状态空间模型的核心假设是:你关心的东西,它真实存在,但你观测不到。这句话听起来有点哲学,其实特别实际。比如你想追踪一辆车的位置,真实位置就是“状态”;车载定位模块给你的坐标是“观测”,这个观测有偏差、有噪声,你不能完全相信它。
数学上用两个方程表达:
状态方程: x_t = F x_{t-1} + B u_t + w_t
观测方程: y_t = H x_t + v_t
- x_t 是 t 时刻的隐含状态向量;
- F 是状态转移矩阵,描述系统状态从上一步怎么演变到这一步;
- u_t 是外部控制量,比如油门、刹车力道,B 是对应的控制矩阵;
- w_t 是过程噪声,代表模型自身没有考虑到的随机扰动;
- y_t 是传感器观测值;
- H 是观测矩阵,把状态空间映射到观测空间;
- v_t 是观测噪声。
w_t 和 v_t 一般是零均值的正态分布,协方差分别记为 Q 和 R。整个模型的本质是“状态动态地走,观测隔着噪声看到状态”,你需要根据一串观测 y_1, y_2, ..., y_t 把 x_t 估计出来。
最常见的类比是赌场骰子:状态是荷官手里的实际点数,观测是隔着磨砂玻璃看过去的模糊数字。单次观测可能看错,但如果你持续观察,又知道骰子大概率不会突然变成别的数字,那就能通过时间序列不断校正自己的判断。卡尔曼滤波只不过是把这种直觉用协方差矩阵量化了。
1.2 为什么卡尔曼滤波是一种“最优”估计
在状态空间模型里,估计隐含状态的方法不止卡尔曼滤波一种,为什么唯独它出场频率最高?
因为在线性系统和高斯噪声的假设下,卡尔曼滤波给出了最小均方误差意义上的最优解。所谓最优,指的是它求的其实就是条件期望 E[x_t | y_1, y_2, ..., y_t]——基于所有已知观测,理论上最合理的状态估计。而且高斯分布的特点是“均值和协方差就能完整描述分布”,所以卡尔曼滤波每步输出的状态估计 x_hat 和误差协方差 P,其实就是后验概率分布的完整参数。
这意味着你不用保留整段历史数据,只需要记住当前这一步的均值与协方差,就能递归地处理所有后续观测。换个说法:它是个在线贝叶斯递推器,每一步只需要上一时刻的估计结果和当前最新观测,内存占用固定,实时性非常好。
但代价也很明确:这套“最优性”只在线性高斯世界成立。如果系统是非线性的,或者噪声不是高斯,卡尔曼滤波就退化成一种“近似”估计,这时需要考虑扩展卡尔曼滤波、无迹卡尔曼滤波,甚至粒子滤波。所以这篇先把线性高斯场景吃透,后面遇到非线性才有比较对象。
2. 两条主线:从预测到校正
卡尔曼滤波的核心循环只有两步:预测和更新。预测是“根据模型猜”,更新是“根据观测改”。理解了这两步之间的博弈,就理解了整个算法。
2.1 预测步:用模型外推,先赌一把
已知上一时刻的状态估计 x_{t-1},误差协方差 P_{t-1},预测步做的事情很简单:
x_pred = F x_{t-1}
P_pred = F P_{t-1} F^T + Q
x_pred 就是在没有新观测之前,模型认为当前状态应该在哪里。P_pred 则有两部分贡献:一部分来自 F P_{t-1} F^T,这是把上一时刻的不确定性顺着系统动力学“传导”过来;另一部分来自 Q,这是模型本身对过程噪声的容忍度。
为什么必须加 Q?因为实际系统永远不会像 F 矩阵描述得那么完美。比如你用匀速模型 F = [[1, dt], [0, 1]] 去预测运动目标,但目标实际上在加速,那么误差会随时间平方地增长。如果 P_pred 里没有 Q 帮忙扩宽不确定区间,滤波器就会对自己的预测过于自信,后面观测想“纠偏”都纠不动。
我常用一个比喻:预测步相当于天气预报里的“数值模式往前推”,Q 告诉你这个模式到底靠谱到什么程度。如果模式很不准,就应该把 Q 调大,给后面的观测留出一定的发言权。
2.2 更新步:把观测拉回来
拿到新的观测 y_t 后,滤波器先算一个叫“新息”的东西:
y_res = y_t - H x_pred
这个残差是“实际观测”与“预测观测”的差异。如果模型完美、观测无噪声,y_res 永远为零;现实中它总是有一些值,而卡尔曼滤波需要决定:这个差异到底有多少是真实状态变化导致的,又有多少只是观测噪声在捣乱?
更新步的关键是卡尔曼增益 K:
K = P_pred H^T (H P_pred H^T + R)^{-1}
然后用它去修正状态和协方差:
x_new = x_pred + K y_res
P_new = (I - K H) P_pred
x_new 的物理含义特别直白:预测值加上“新息”乘以一个比例。K 越大,说明观测越值得相信;K 越小,说明观测噪声太大,不如继续相信模型的预测。P_new 则是修正后的不确定性,通常比 P_pred 小,因为新增观测提供了信息,让估计变得更确定。
2.3 卡尔曼增益为什么这么关键
看 K 的表达式,它的核心是 P_pred 和 R 的相对大小。
| 场景 | P_pred 对比 R | 增益趋势 | 滤波行为 |
|---|---|---|---|
| 预测很不确定(Q 大)或观测很准(R 小) | P_pred >> R | K 接近 1 | 高度信任观测,状态估计几乎跟着观测走 |
| 预测很准(Q 小)或观测噪声很大(R 大) | P_pred << R | K 接近 0 | 高度信任模型,观测对结果影响很小 |
| 两者可比 | 量级相当 | K 在 0~1 之间 | 根据各自不确定性做最优折中 |
这个表我建议你贴在电脑旁边,调参时特别有用。我见过很多新手把卡尔曼滤波当低通滤波器用,结果 R 设得不合理,输出要么完全跟着噪声跳,要么对真实变化视而不见。实际调试时先看 K 的平均值落在哪个区间,通常能快速定位问题。
举个极端例子:假设你的传感器是实验室级的高精度仪器,R 设得极小,那么 K 会很大,滤波器几乎就是“观测值 + 微调”。反过来,如果传感器是一个精度很差的廉价模块,R 设大一点,滤波器就会更平滑,把单点跳变当作噪声而不是真实信号。理解这个“信任天平”,比背任何公式都有用。
3. 手写一个卡尔曼滤波器:从一维到多维
纸上谈兵到这里差不多了,下面动手写代码。我的习惯永远是先写一维,跑通、画图、确认行为合理,再扩展到多维。直接上手多维矩阵,公式出了一点维度错,报错信息很容易让人崩溃。
3.1 一维温度估计示例:先学会走路
假设你在恒温箱里测温度。真实温度基本稳定在 25.0 度,但传感器每次读数有噪声。这是一个最简状态空间模型:
x_t = x_{t-1} + w_t
y_t = x_t + v_t
F = 1, H = 1,全部退化成一维标量。用 NumPy 写卡尔曼滤波的核心函数:
import numpy as np def kf_predict(x, P, F, Q): x_pred = F @ x P_pred = F @ P @ F.T + Q return x_pred, P_pred def kf_update(x_pred, P_pred, H, R, y): y_res = y - H @ x_pred S = H @ P_pred @ H.T + R K = P_pred @ H.T @ np.linalg.inv(S) x_new = x_pred + K @ y_res P_new = (np.eye(len(x_pred)) - K @ H) @ P_pred return x_new, P_new, K然后生成模拟数据跑一遍:
rng = np.random.default_rng(42) true_temp = 25.0 R = 0.1 # 观测噪声方差 Q = 0.001 # 过程噪声方差,温度很稳定所以设很小 obs = true_temp + np.sqrt(R) * rng.normal(size=50) x = np.array([20.0]) # 初始估计,故意给偏一点 P = np.array([4.0]) # 初始协方差,代表“不太确定” F = np.array([1.0]) H = np.array([1.0]) for y in obs: x_pred, P_pred = kf_predict(x, P, F, Q) x, P, K = kf_update(x_pred, P_pred, H, R, y) print(f"观测={y:.3f} 滤波={x[0]:.3f} K={K[0]:.3f} P={P[0]:.4f}")你会看到两个现象。第一,滤波值从 20.0 起步,经过几步快速接近 25.0,这就是收敛过程。第二,稳态之后 P 会停在一个小值不再变化,K 也趋于一个稳定值,这是线性时不变系统的正常表现。这里 Q 和 R 的比例决定了最终的滤波平滑程度。
3.2 二维匀速运动模型:进入矩阵世界
真实项目里很少是一维标量,多数是位置、速度甚至加速度的组合。这里以一个典型的二维匀速运动模型为例。
状态向量定义为:
x = [px, vx, py, vy]^T
分别是水平位置、水平速度、垂直位置、垂直速度。dt 是时间步长,状态转移矩阵:
F = np.array([ [1, dt, 0, 0], [0, 1, 0, 0], [0, 0, 1, dt], [0, 0, 0, 1] ])如果只能观测到位置,H 矩阵是:
H = np.array([ [1, 0, 0, 0], [0, 0, 1, 0] ])过程噪声 Q 的选择比一维麻烦。如果是“加速度是白噪声”的连续模型,常用公式是:
q_1d = q_c * [[dt^3 / 3, dt^2 / 2], [dt^2 / 2, dt]]
然后扩展到二维:
q_c = 0.1 Q = q_c * np.array([ [dt**3 / 3, dt**2 / 2, 0, 0], [dt**2 / 2, dt, 0, 0], [0, 0, dt**3 / 3, dt**2 / 2], [0, 0, dt**2 / 2, dt] ])如果你觉得这个公式不好记,也可以直接调 Q 的对角线数值,凭经验试。但我建议还是用上面的形式,因为它在物理上对应“随机加速度扰动”,调 q_c 一个标量就能控制整体强度,比直接填矩阵容易理解得多。
dt 的取值对 Q 影响很大。dt 太小,单位时间内的随机加速度扰动不大;dt 大,位置的不确定度增长非常快。我处理采样频率不均匀的数据时,常常把 dt 作为变量传进 F 和 Q 矩阵,这样不同采样间隔的数据也能统一处理。
3.3 Python 代码演示与运行结果
下面是一个完整示例:生成一条带了随机速度和观测噪声的二维轨迹,然后用卡尔曼滤波实时估计位置。
import numpy as np def kf_predict(x, P, F, Q): x_pred = F @ x P_pred = F @ P @ F.T + Q return x_pred, P_pred def kf_update(x_pred, P_pred, H, R, y): y_res = y - H @ x_pred S = H @ P_pred @ H.T + R K = P_pred @ H.T @ np.linalg.inv(S) x_new = x_pred + K @ y_res P_new = (np.eye(len(x_pred)) - K @ H) @ P_pred return x_new, P_new, K rng = np.random.default_rng(7) dt = 0.1 steps = 200 q_c = 0.1 sigma_obs = 0.5 true_state = np.zeros((4, steps)) true_state[:, 0] = [0, 1.0, 0, 2.0] for i in range(1, steps): true_state[1, i] = true_state[1, i - 1] + np.sqrt(q_c * dt) * rng.normal() true_state[3, i] = true_state[3, i - 1] + np.sqrt(q_c * dt) * rng.normal() true_state[0, i] = true_state[0, i - 1] + true_state[1, i - 1] * dt true_state[2, i] = true_state[2, i - 1] + true_state[3, i - 1] * dt F = np.array([ [1, dt, 0, 0], [0, 1, 0, 0], [0, 0, 1, dt], [0, 0, 0, 1] ]) H = np.array([ [1, 0, 0, 0], [0, 0, 1, 0] ]) R = np.array([[sigma_obs**2, 0], [0, sigma_obs**2]]) Q = q_c * np.array([ [dt**3 / 3, dt**2 / 2, 0, 0], [dt**2 / 2, dt, 0, 0], [0, 0, dt**3 / 3, dt**2 / 2], [0, 0, dt**2 / 2, dt] ]) obs = H @ true_state + rng.normal(0, sigma_obs, size=(2, steps)) x = np.array([0, 1.0, 0, 2.0]) P = np.eye(4) * 10.0 pos_pred = [] for i in range(steps): x_pred, P_pred = kf_predict(x, P, F, Q) x, P, _ = kf_update(x_pred, P_pred, H, R, obs[:, i]) pos_pred.append(x[[0, 2]].copy()) pos_pred = np.array(pos_pred).T跑完后可以计算位置估计的 RMSE 和纯观测的 RMSE:
obs_rmse = np.sqrt(np.mean((obs[:2, :] - true_state[[0, 2], :])**2)) kf_rmse = np.sqrt(np.mean((pos_pred - true_state[[0, 2], :])**2)) print(f"观测RMSE: {obs_rmse:.3f}, 滤波RMSE: {kf_rmse:.3f}")由于用了随机数种子,结果可复现。一般跑下来滤波后的位置 RMSE 会明显小于观测 RMSE,这就是卡尔曼滤波的价值:它不只是把单点位置拿出来,而是结合了速度信息,用时间上的连贯性把噪声磨平了。
我建议你把这个例子下载下来,改改 q_c 和 sigma_obs 再跑几遍。改参数不花钱,但能帮你迅速建立直觉:q_c 大,估计就躁动;sigma_obs 大,估计就平滑但滞后。这种直觉比任何教科书都记得牢。
4. 核心参数的调优心得:Q、R、P0 的实操经验
卡尔曼滤波的公式是固定的,真正让效果天差地别的是 Q、R、P0 这三个协方差矩阵。公式只保证“给定这些噪声参数后估计最优”,可这些参数本身该怎么定,公式没有回答。这部分我非常想展开聊聊,因为现实项目里 80% 的调参时间都花在这里。
4.1 过程噪声 Q 怎么设才不飘
Q 代表你对模型的信任程度。Q 设得越小,代表你认为状态转移方程越准确,滤波器会越发相信预测,观测带来的修正作用就弱。
如果 Q 设得太小,会发生什么?我用某车载定位项目的例子说明。当时用匀速模型估计车辆运动,初始 Q 设得很保守,结果在车辆拐弯时滤波轨迹总是滞后于真实轨迹——因为模型一直在“匀速直线外推”,拐弯被模型认为是不可能事件,观测数据也拉不回状态。这个滞后让人误以为算法有问题,其实只是 Q 太小,给了模型过高的信任度。
如果 Q 设得太大,滤波结果又几乎退化成逐点观测平滑,输出会很毛糙,失去了状态空间模型融合历史信息的优势。
一个务实的做法是从物理过程出发估计扰动强度。比如无人机在悬停模式下,位置受风扰动,你可以观察位置波动的方差,反推加速度噪声的量级,再代入连续白噪声公式得到 Q。另一个做法是先用仿真数据做网格搜索:固定其他参数,让 Q 在几个数量级之间扫描,选一个让测试集 RMSE 最小的值。刚开始可以粗一点,对数间隔 10 倍搜索,找到量级后再细调。
注意:Q 矩阵不是越大越好,“信任度”和“响应速度”是两个互相矛盾的目标,Q 越大响应越快,但噪声也越大;Q 越小越平滑,但真实变化会被磨平。你需要根据业务场景选折中点,比如报警系统宁可灵敏一点,因为漏报比误报代价更大。
4.2 观测噪声 R 怎么设才不盲信
R 由传感器的测量精度决定,这个值相对 Q 更容易定,因为传感器厂商通常会给精度指标,或者你自己做几次静态测量统计方差。
假如某定位模块标称精度是 0.5 米,一般可以近似认为观测噪声标准差是 0.5,于是 R = 0.5^2 = 0.25。注意如果你的观测向量是二维,R 就是一个 2x2 的协方差矩阵;如果 x、y 方向的精度不同,甚至相关,就需要完整的协方差矩阵。
在温度监控场景,我曾经直接用传感器的出厂精度指标设 R,效果还行。但温度传感器在快速变化环境里其实会有滞后,这时 R 设得太小,滤波结果反而会被这种系统性偏差带偏。所以不要只眼盯精度指标,还要看实际采集数据里的残差统计。
我经常做一件事:先采集一段静止状态的数据,把观测值的方差算出来当作 R 的初始值;再采集一段运动状态的数据,看滤波残差是否还有明显的非噪声成分。如果残差里还有系统性趋势,说明模型或者状态向量设计有问题,光调 R 是无济于事的。
4.3 初始协方差 P0 别小气
P0 是初始时刻对状态估计不确定性的描述。很多新手直接设成零矩阵,理由是我对初始状态很自信。但一旦设 P0=0,滤波器会认为初始状态绝对正确,即使后面观测和预测差异再大,增益也很难提上去,收敛会异常缓慢,甚至出现看起来像“不发散”的僵住状态。
我建议 P0 给大一点,这是我从某导师那里学来的习惯,后来自己实践发现非常有效:初始化时对状态不确定,干脆把它设成 10 ~ 1000 倍的合理方差。比如真实位置可能在 0~10 之间,P0 就设 100;初始速度不确定,也设一个较大的值。这意味着滤波器前期非常愿意相信观测,能在几个周期内快速拉到数据集附近,然后再慢慢收敛到稳态性能。
大 P0 带来的唯一副作用是前几步协方差很大,状态估计波动略大。如果你需要在开头几个点就交付结果,可以把初始状态尽量设准,并给一个适中的 P0,例如估计位置误差的平方,而不是零。
另一个相关经验:P0 的选择会影响早期 K 的峰值,有些实现里因为 P0 太大,K 在第一步甚至超过 1,这没关系,只要 P 能快速收敛即可。
5. 明明能跑却不对?卡尔曼滤波的常见坑与排查技巧
代码能跑通,不代表结果合理。我在不同项目里见过太多次“卡尔曼滤波跑起来了,但结果很诡异”的情况,下面把最常踩的坑和排查思路按优先级列出来。
5.1 五个最容易踩的坑
坑一:F、H 矩阵维度不匹配。状态向量是 4 维,观测向量是 2 维,H 就应该是 2x4,写成 4x2 运行时虽然某些情况下不报错,但语义完全错乱。我习惯在初始化后立刻打印所有矩阵形状并在注释里标出,例如# F: 4x4, H: 2x4, Q: 4x4, R: 2x2。
坑二:把非线性过程硬套卡尔曼滤波。卡尔曼滤波是线性系统假设下的最优解。如果你处理的是角度、带饱和特性或强非线性的系统,直接套标准卡尔曼滤波会系统性偏差。至少要用扩展卡尔曼滤波或无迹卡尔曼滤波。判断方法很简单:把滤波残差画出来,如果残差均值明显非零,且只靠调 Q 调不掉,多半是模型非线性问题。
坑三:Q 设成 0 导致协方差衰减到停滞。当 Q=0,预测步中 P_pred 不会增长,滤波器会越来越信任模型,最终 K 变得极小,后续观测再也影响不了状态。这种情况在静止场景可能看起来没问题,一旦目标开始运动,结果会“卡住”。排查时打印 P 的对角线,如果它快速跌到 1e-6 以下,这就是症状。
坑四:P0 设成 0 或太小。前面说过,P0 太小等于告诉滤波器“初始状态绝对可信”,它会失去学习能力。如果滤波结果在前 20 个点都变化很慢,优先查 P0 是否给了足够的不确定性。
坑五:用全体数据的误差来评判定阶滤波效果。滤波有一个收敛期,尤其是初始状态不准时,前几个点的误差天然很大。评估模型性能和调参时,我会把前 5% ~ 10% 的时间点排除在外,或者分两段统计:收敛段和稳态段。否则很可能把一个好模型误判成坏模型,然后把一个过拟合噪声的模型选成“最优”。
5.2 一个排障实例:滤波结果总是滞后于观测
有段时间我在处理一个温控系统的实验数据,卡尔曼滤波输出比传感器观测晚了很多拍,但肉眼能看到传感器本身是有滞后特性的。一开始我以为是时间同步问题,后来仔细看才发现是参数比例失衡。
排查步骤是这样的:
首先画“观测值、滤波值、新息”三张图。新息是观测减去预测观测。如果新息呈明显的正弦或锯齿形状,说明模型和真实系统之间存在周期性差异;如果滤波值总是跟随在观测点之后,大概率是 R 相对 Q 太大,导致 K 太小,滤波器把真实变化也当成了噪声。
我的做法是先把 Q 固定,只调 R,从 0.5 倍传感器标称方差开始测试,每档对比新息的均值、方差和滤波结果的滞后步数,然后反向调整。调了几轮之后,我把 R 降到原来的 1/4,滞后基本消失,新息序列也接近零均值白噪声。
这个经验给我一个很大教训:卡尔曼滤波调参永远不要只盯输出曲线,一定要把“新息序列”作为重要诊断工具。理想的新息序列应该均值接近零,自相关很小,方差和 R 的量级一致。如果新息均值长期非零,模型假设出错;如果新息方差远大于 R,说明观测噪声被低估或模型噪声被低估;如果新息序列高度相关,通常意味着状态转移模型不够丰富。
5.3 快速自查清单
| 症状表现 | 优先排查项 | 常用解决方法 |
|---|---|---|
| 滤波结果滞后明显 | R 太大 / Q 太小 | 增大 Q 或减小 R,观察新息序列 |
| 结果噪声大、抖动剧烈 | Q 太大 / R 太小 | 减小 Q 或增大 R,提高平滑度 |
| 协方差 P 快速趋近零 | Q=0 或 Q 过小 | 给 Q 一个非零下界 |
| 初始段收敛极慢 | P0 太小 | 初始协方差设大,例如状态不确定度的平方 |
| 滤完仍有明显趋势残差 | 模型非线性或状态不完整 | 检查残差、考虑扩展卡尔曼滤波,或增加状态维度 |
| 运行时报矩阵维度错误 | F/H/Q/R 形状不一致 | 逐矩阵打印形状,核对手写注释 |
写在最后
我最初学习卡尔曼滤波时一直试图“证明”每一步公式,后来发现实战里最重要的是建立直觉:预测步像经验判断,更新步像新信息修正,Q 和 R 则是判断两边的可信度。只要这个直觉在,公式只是把它变成可计算的形式而已。
如果你也想练手,建议先找一个带轨迹的位置数据,把它当作观测,用这篇文章里的多维模型跑一遍,然后故意把 Q、R 调到“明显不合理”,看看滤波曲线各长什么样。踩过几次坑之后,你对卡尔曼滤波的理解会比看十篇文章都深。接下来如果碰到非线性系统,扩展卡尔曼和无迹卡尔曼其实也是在这套框架上打补丁,到时候再聊也不迟。