4. KF的Python实现:一维温度估计案例

好了,终于到了动手写代码的时候了。

前面我们推导了那么多公式,说实话,光看数学确实有点枯燥。我个人习惯是,先跑通一个最简单的例子,再回头理解公式,这样心里更有底。

这一章,我们就用Python从零实现一个卡尔曼滤波器。场景选什么呢?——一维温度估计。说白了,就是用一个温度计去测室温,但温度计有噪声,我们要用KF把真实温度“猜”出来。

4.1 问题定义:我们要解决什么?

想象一下这个场景:

  • 你有一个温度计,每隔1秒测一次室温
  • 温度计有测量噪声,比如±0.5°C的随机误差
  • 房间温度其实也在缓慢变化(过程噪声)
  • 你想实时知道“真实”温度是多少

嗯,这就是一个典型的一维卡尔曼滤波问题。状态量只有一个:温度。控制量没有(或者说为0)。

我当年第一次做这个案例时,犯了个低级错误——把过程噪声设得太小,结果滤波器几乎不相信测量值,温度变化半天都追不上。后来才明白,调参这事,真的得靠手感。

4.2 系统建模:状态方程和观测方程

对于一维温度估计,我们的模型非常简单:

状态方程(预测):

x(k) = x(k-1) + w(k-1)

其中w是过程噪声,服从N(0, Q)。这里Q就是过程噪声协方差。

观测方程(测量):

z(k) = x(k) + v(k)

其中v是测量噪声,服从N(0, R)。R是测量噪声协方差。

你看,状态转移矩阵A=1,观测矩阵H=1。简单到不能再简单了。但正是这个简单的例子,能让你看清KF的每一步到底在干什么。

核心参数:

  • Q:过程噪声协方差——你有多相信模型?Q越大,滤波器越“灵活”,但也越容易受噪声影响
  • R:测量噪声协方差——你有多相信传感器?R越大,滤波器越“迟钝”,平滑效果越好

4.3 从零手写KF代码

下面就是完整的Python实现。我尽量写得清晰,每一步都对应前面讲过的公式。

import numpy as np
import matplotlib.pyplot as plt

class KalmanFilter1D:
    """一维卡尔曼滤波器 - 温度估计"""
    
    def __init__(self, Q=1e-4, R=0.25):
        # 初始化参数
        self.Q = Q  # 过程噪声协方差
        self.R = R  # 测量噪声协方差
        
        # 状态估计
        self.x = 25.0  # 初始温度估计(随便猜一个)
        self.P = 1.0   # 初始误差协方差(不确定度)
        
    def predict(self):
        """预测步骤"""
        # x(k|k-1) = A * x(k-1|k-1) + B * u(k)
        # 这里A=1, B=0, 所以就是保持不变
        self.x = self.x
        
        # P(k|k-1) = A * P(k-1|k-1) * A^T + Q
        # 这里A=1, 所以就是P + Q
        self.P = self.P + self.Q
        
    def update(self, z):
        """更新步骤"""
        # 卡尔曼增益
        # K = P * H^T / (H * P * H^T + R)
        # 这里H=1, 所以就是P / (P + R)
        K = self.P / (self.P + self.R)
        
        # 状态更新
        # x(k|k) = x(k|k-1) + K * (z - H * x(k|k-1))
        self.x = self.x + K * (z - self.x)
        
        # 误差协方差更新
        # P(k|k) = (I - K * H) * P(k|k-1)
        self.P = (1 - K) * self.P
        
        return self.x

代码就这么短。你看,核心逻辑就两个函数:predict和update。每次来一个新测量值,先predict一下,再update一下,就得到了新的最优估计。

小技巧:初始值x和P怎么设?

如果你对初始状态比较确定,P可以设小一点(比如0.1)。如果不确定,就设大一点(比如1.0或更大)。滤波器会很快收敛的,别担心。

4.4 生成模拟数据并运行

有了滤波器,我们还需要模拟数据来测试。我生成一组“真实温度”加上高斯噪声,模拟温度计的读数。

# 生成模拟数据
np.random.seed(42)  # 固定随机种子,方便复现

n_steps = 100
true_temps = np.zeros(n_steps)
measurements = np.zeros(n_steps)

# 真实温度:先稳定在25°C,然后缓慢上升
for i in range(n_steps):
    if i < 30:
        true_temps[i] = 25.0
    elif i < 70:
        true_temps[i] = 25.0 + (i - 30) * 0.1  # 缓慢升温
    else:
        true_temps[i] = 29.0  # 稳定在29°C

# 添加测量噪声(标准差0.5°C)
measurements = true_temps + np.random.normal(0, 0.5, n_steps)

# 运行卡尔曼滤波
kf = KalmanFilter1D(Q=1e-4, R=0.25)
estimates = np.zeros(n_steps)

for i in range(n_steps):
    kf.predict()
    estimates[i] = kf.update(measurements[i])

# 计算误差
mse_before = np.mean((measurements - true_temps) ** 2)
mse_after = np.mean((estimates - true_temps) ** 2)

print(f"原始测量均方误差: {mse_before:.4f}")
print(f"卡尔曼滤波均方误差: {mse_after:.4f}")

运行结果会告诉你,滤波后的误差明显小于原始测量误差。这就是KF的价值所在。

4.5 结果可视化

光看数字不够直观,我们画个图看看。

plt.figure(figsize=(12, 6))
plt.plot(true_temps, 'g-', label='真实温度', linewidth=2)
plt.plot(measurements, 'r.', label='测量值(含噪声)', alpha=0.5)
plt.plot(estimates, 'b-', label='卡尔曼估计', linewidth=2)
plt.legend()
plt.xlabel('时间步')
plt.ylabel('温度 (°C)')
plt.title('一维卡尔曼滤波 - 温度估计')
plt.grid(True, alpha=0.3)
plt.show()

你会看到:

  • 红色测量点上下跳动,噪声很明显
  • 蓝色估计线平滑很多,紧贴着绿色真实值
  • 在温度变化时(第30步附近),估计值会有一点延迟,但很快追上

我曾经踩过的坑:

刚开始调参时,我把Q设成了0。结果温度变化时,滤波器完全追不上,因为模型认为温度不会变。后来我把Q调到1e-3,效果就好多了。记住:Q不能为0,否则滤波器会“死掉”。

4.6 参数调优:Q和R的博弈

Q和R的比值,决定了滤波器的行为。我整理了一个表格,方便你理解:

Q/R比值 滤波器行为 适用场景
Q很小,R很大 非常平滑,但响应慢 温度稳定、传感器噪声大
Q很大,R很小 响应快,但容易受噪声影响 温度变化快、传感器精度高
Q和R适中 平衡平滑和响应 大多数通用场景

说白了,这就是一个权衡。你想想看,如果传感器很准(R小),你就多信测量值;如果模型很准(Q小),你就多信预测值。卡尔曼增益K就是自动做这个权衡的。

4.7 完整代码汇总

为了方便你直接运行,我把完整代码放在下面。你复制到Python环境里就能跑。

import numpy as np
import matplotlib.pyplot as plt

class KalmanFilter1D:
    def __init__(self, Q=1e-4, R=0.25):
        self.Q = Q
        self.R = R
        self.x = 25.0
        self.P = 1.0
        
    def predict(self):
        self.P = self.P + self.Q
        
    def update(self, z):
        K = self.P / (self.P + self.R)
        self.x = self.x + K * (z - self.x)
        self.P = (1 - K) * self.P
        return self.x

# 生成数据
np.random.seed(42)
n_steps = 100
true_temps = np.zeros(n_steps)
for i in range(n_steps):
    if i < 30:
        true_temps[i] = 25.0
    elif i < 70:
        true_temps[i] = 25.0 + (i - 30) * 0.1
    else:
        true_temps[i] = 29.0
measurements = true_temps + np.random.normal(0, 0.5, n_steps)

# 运行滤波
kf = KalmanFilter1D(Q=1e-4, R=0.25)
estimates = np.zeros(n_steps)
for i in range(n_steps):
    kf.predict()
    estimates[i] = kf.update(measurements[i])

# 绘图
plt.figure(figsize=(12, 6))
plt.plot(true_temps, 'g-', label='真实温度', linewidth=2)
plt.plot(measurements, 'r.', label='测量值', alpha=0.5)
plt.plot(estimates, 'b-', label='卡尔曼估计', linewidth=2)
plt.legend()
plt.xlabel('时间步')
plt.ylabel('温度 (°C)')
plt.title('一维卡尔曼滤波 - 温度估计')
plt.grid(True, alpha=0.3)
plt.show()

print(f"原始MSE: {np.mean((measurements - true_temps)**2):.4f}")
print(f"滤波MSE: {np.mean((estimates - true_temps)**2):.4f}")

4.8 本章知识结构图

下面这张图,帮你把这一章的核心逻辑串起来。从问题定义到代码实现,每一步都清晰可见。

一维温度估计KF实现流程 问题定义 一维温度估计 系统建模 状态方程 + 观测方程 代码实现 predict + update 核心参数 Q: 过程噪声协方差 R: 测量噪声协方差 数据生成 真实温度 + 高斯噪声 模拟温度计读数 结果评估 MSE误差对比 可视化曲线 参数调优 Q/R比值权衡 避坑指南 Q不能为0等常见错误 完整代码 可直接运行的Python脚本

嗯,这一章的内容就到这里。代码不长,但麻雀虽小五脏俱全。你把这个一维例子跑通了,后面扩展到多维就水到渠成了。


专注资料整理