4、扩展卡尔曼滤波:非线性系统线性化、雅可比矩阵计算、风速风向融合案例

各位同学,欢迎来到第四讲。

前面我们聊了标准卡尔曼滤波,它很强大,但有个硬伤——只能处理线性系统。可咱们的气象站,偏偏就是个非线性世界。风速和风向的关系,可不是简单的加减乘除。今天,我们就来啃这块硬骨头:扩展卡尔曼滤波(EKF)

4.1 为什么需要扩展卡尔曼滤波?

标准卡尔曼滤波假设系统是线性的,噪声是高斯分布的。但现实很骨感。你想想看,风速传感器测的是标量风速,风向标测的是角度。这两个东西怎么融合?直接相加?那风向 350° 和 10° 的平均值,按线性算出来是 180°,但实际应该是 0° 或 360°。这明显不对。

我早年做第一个气象站项目时,就踩过这个坑。当时天真地用了标准卡尔曼,结果风向融合结果一塌糊涂,尤其是在风速很低、风向乱飘的时候。后来才明白,非线性系统必须用非线性滤波器

EKF 的核心思想很简单:把非线性函数在当前估计点附近做一阶泰勒展开,近似成线性系统。说白了,就是用切线去逼近曲线。

核心公式(线性化后的状态预测):
k|k-1 = f(x̂k-1|k-1, uk-1)
Pk|k-1 = Fk-1 · Pk-1|k-1 · Fk-1T + Qk-1

这里的 F 就是雅可比矩阵,它描述了状态转移函数 f 对每个状态变量的偏导数。

4.2 雅可比矩阵:EKF 的“翻译官”

雅可比矩阵,听起来很高大上,其实它就是多变量函数的导数矩阵。对于 EKF,我们需要计算两个雅可比矩阵:

  • 状态转移雅可比矩阵 F:描述状态如何随时间变化。
  • 观测雅可比矩阵 H:描述状态如何映射到观测值。

我个人的习惯是,先写出系统的非线性方程,然后逐项求偏导。别怕,大部分情况下,这些偏导都有解析解,不需要数值微分。

4.2.1 状态转移雅可比矩阵 F

假设我们的状态向量是 [风速, 风向]ᵀ。风速变化可以用一个随机游走模型,风向变化则复杂一些。一个简化的模型是:

// 状态方程(非线性)
风速(k) = 风速(k-1) + w_speed
风向(k) = 风向(k-1) + w_dir

// 雅可比矩阵 F(对状态求偏导)
F = [ ∂f1/∂风速  ∂f1/∂风向 ]
    [ ∂f2/∂风速  ∂f2/∂风向 ]

// 对于这个简单模型,F 就是单位矩阵
F = [ 1  0 ]
    [ 0  1 ]

嗯,这里要注意,如果风向模型里包含了风速对风向的影响(比如低风速时风向更不稳定),那 F 就不是单位矩阵了。我在实际项目中,通常会加入一个风速相关的噪声项,让低风速时的过程噪声 Q 更大。

4.2.2 观测雅可比矩阵 H

这是 EKF 的关键。我们的观测值是什么?是风速传感器测的标量风速 v,和风向标测的角度 θ。但这两个观测值,与状态向量 [风速, 风向]ᵀ 的关系是线性的吗?

是的,在这个例子里,观测方程是线性的:

// 观测方程(线性)
z_v = 风速 + v_speed_noise
z_θ = 风向 + v_dir_noise

// 观测雅可比矩阵 H
H = [ ∂h1/∂风速  ∂h1/∂风向 ]
    [ ∂h2/∂风速  ∂h2/∂风向 ]

// 对于线性观测,H 就是
H = [ 1  0 ]
    [ 0  1 ]

等等,你可能会问:“既然观测是线性的,那为什么还要用 EKF?” 问得好!因为状态转移可能是非线性的。比如,当风速很低时,风向的随机游走模型会变得非常不稳定,甚至出现“跳变”。这时候,我们需要在状态转移中引入非线性函数,比如一个风速相关的阻尼项。

避坑指南: 我曾经在计算风向的雅可比时,忽略了角度环绕问题(350° 和 10° 的差值应该是 20°,而不是 -340°)。结果滤波器在风向过 0° 时直接发散。后来我强制将角度差值归一化到 [-π, π] 区间,问题才解决。

4.3 风速风向融合案例:从理论到代码

好了,理论讲完了,我们来看一个完整的案例。假设我们有一个超声波风速计,它能同时输出风速和风向。但这两个信号都有噪声,而且噪声特性不同。我们的目标是融合它们,得到更平滑、更可靠的风速风向估计。

4.3.1 系统模型

参数 符号 说明
状态向量 x = [v, θ]ᵀ v: 风速 (m/s), θ: 风向 (rad)
观测向量 z = [v_meas, θ_meas]ᵀ 传感器直接测量值
过程噪声 Q = diag(q_v, q_θ) q_v 和 q_θ 根据风速大小调整
观测噪声 R = diag(r_v, r_θ) 由传感器手册或标定得到

4.3.2 EKF 迭代步骤(C 语言伪代码)

// 1. 预测步骤
void ekf_predict(EKF_t *ekf) {
    // 状态预测:简单模型,直接复制
    ekf->x[0] = ekf->x[0];  // 风速
    ekf->x[1] = ekf->x[1];  // 风向

    // 雅可比矩阵 F(这里假设是单位阵)
    float F[2][2] = {{1, 0}, {0, 1}};

    // 协方差预测:P = F * P * F' + Q
    // 由于 F 是单位阵,简化为 P = P + Q
    ekf->P[0][0] += ekf->Q[0][0];
    ekf->P[1][1] += ekf->Q[1][1];
}

// 2. 更新步骤
void ekf_update(EKF_t *ekf, float z_v, float z_theta) {
    // 观测雅可比矩阵 H(线性观测,单位阵)
    float H[2][2] = {{1, 0}, {0, 1}};

    // 计算卡尔曼增益 K = P * H' * inv(H * P * H' + R)
    // 这里 H 是单位阵,简化计算
    float S[2][2];
    S[0][0] = ekf->P[0][0] + ekf->R[0][0];
    S[1][1] = ekf->P[1][1] + ekf->R[1][1];

    float K[2][2];
    K[0][0] = ekf->P[0][0] / S[0][0];
    K[1][1] = ekf->P[1][1] / S[1][1];

    // 计算残差(注意风向角度归一化!)
    float y_v = z_v - ekf->x[0];
    float y_theta = z_theta - ekf->x[1];
    // 关键:将风向残差归一化到 [-PI, PI]
    y_theta = atan2f(sinf(y_theta), cosf(y_theta));

    // 状态更新
    ekf->x[0] += K[0][0] * y_v;
    ekf->x[1] += K[1][1] * y_theta;

    // 协方差更新
    ekf->P[0][0] = (1 - K[0][0]) * ekf->P[0][0];
    ekf->P[1][1] = (1 - K[1][1]) * ekf->P[1][1];
}
重要提醒: 上面的代码为了教学做了大量简化。实际项目中,你需要:
  • 处理风向的周期性(0° 和 360° 是同一个点)
  • 根据风速大小动态调整 Q 矩阵(低风速时风向噪声更大)
  • 考虑传感器失效时的容错机制

4.4 实战中的坑与经验

最后,分享几个我亲身踩过的坑:

  • 初始化很重要:EKF 对初始状态和协方差很敏感。我建议用前 10 个观测值的均值作为初始状态,协方差设得大一些(比如传感器噪声方差的 10 倍),让滤波器自己收敛。
  • Q 矩阵不是常数:风速稳定时,Q 可以小一点;风速剧烈变化时,Q 要相应增大。我习惯用一个风速的方差来动态调整 Q。
  • 风向的“跳变”问题:当风向在 0° 附近时,测量值可能在 359° 和 1° 之间跳变。如果不做处理,EKF 会认为这是巨大误差,导致发散。解决方案就是我在代码里写的:残差归一化

好了,这一讲的内容就到这里。EKF 是处理非线性系统的利器,但也不是万能的。如果非线性太强,或者系统维度太高,你可能需要考虑无迹卡尔曼滤波(UKF)或粒子滤波。那是后话了。

下一讲,我们会讨论无迹卡尔曼滤波,看看它是如何用“采样点”来避免雅可比矩阵计算的。敬请期待。