4. UKF算法推导(下):观测更新与协方差修正

好,咱们接着往下聊。上一节我们把UKF的时间更新(预测步)讲完了,说白了就是怎么用上一时刻的状态,猜出当前时刻的状态长什么样。这一节,我们要解决另一个核心问题:有了观测值(比如电压、电流),怎么修正这个猜测?

我个人习惯把UKF的观测更新看作一个「纠偏」过程。你想想看,模型算出来的状态肯定有误差,传感器测回来的数据也有噪声,怎么把这两者巧妙地融合在一起?这就是卡尔曼滤波的精髓所在。

4.1 观测更新的核心思想

观测更新,本质上是在做一件事:用实际测量值,去修正模型预测值

我在做BMS项目时,遇到过这样一个情况:模型预测SOC还有60%,但实际电压已经掉到3.3V了。这时候如果不做修正,后面估计会越来越偏。UKF的观测更新,就是用来处理这种「模型与实测不一致」的问题。

具体来说,观测更新包含三个关键步骤:

  1. 预测观测值:用Sigma点通过观测函数,算出理论上应该测到什么值
  2. 计算卡尔曼增益:决定「相信模型多一点」还是「相信传感器多一点」
  3. 更新状态与协方差:用实测值修正状态估计

核心公式记忆口诀

预测观测算残差,增益权重来平衡。
状态更新靠修正,协方差缩显自信。

4.2 预测观测值:从状态空间到测量空间

首先,我们要把上一节得到的Sigma点(经过时间更新后的那些点),通过观测函数映射到测量空间。

观测函数一般长这样:

z = h(x, u) + v

其中v是观测噪声,服从N(0, R)分布。在电池模型中,观测函数通常就是电压方程:

V_t = OCV(SOC) - I * R_0 - V_1 - V_2

嗯,这里要注意:观测函数h(·)可能是非线性的,比如OCV-SOC曲线就是典型的非线性关系。这正是UKF派上用场的地方——它不需要线性化,直接用Sigma点去传播。

具体计算步骤:

  1. 对每个Sigma点χ_i,计算对应的观测值:
    Z_i = h(χ_i, u_k)
  2. 计算预测观测值的均值:
    z_pred = Σ W_i * Z_i
  3. 计算观测协方差矩阵:
    S = Σ W_i * (Z_i - z_pred) * (Z_i - z_pred)^T + R

避坑指南:我曾经在计算观测协方差时,忘记加观测噪声协方差矩阵R。结果卡尔曼增益算出来特别大,导致状态估计对测量噪声特别敏感,波动非常剧烈。后来排查了半天才发现是这里漏了。R矩阵的取值很关键,太小了滤波器会「过拟合」测量值,太大了又跟不上变化。

4.3 卡尔曼增益:信任的权衡

卡尔曼增益K,是UKF里最核心的「智慧」所在。它决定了:

  • 当测量噪声小(R小)时,K变大,更相信传感器
  • 当模型噪声大(Q大)时,K变大,更相信测量值
  • 当模型很准(P小)时,K变小,更相信模型

计算卡尔曼增益需要先算状态-观测互协方差

C = Σ W_i * (χ_i - x_pred) * (Z_i - z_pred)^T

然后卡尔曼增益就是:

K = C * S^(-1)

这个公式看着简单,但物理意义很深刻。说白了,C衡量的是「状态变化会引起观测怎么变化」,S衡量的是「观测的不确定性有多大」。两者的比值,就是最优的融合权重。

我个人理解:卡尔曼增益就像是一个「自适应调节器」。当模型预测很准、传感器噪声很大时,它自动降低对测量的依赖;反过来,当模型不准、传感器可靠时,它又大胆地使用测量值去修正。这种自适应能力,是卡尔曼滤波最迷人的地方。

4.4 状态更新与协方差修正

有了卡尔曼增益,更新就变得很直接了:

x_updated = x_pred + K * (z_actual - z_pred)

这里(z_actual - z_pred)叫做残差新息。它代表了「实际测量值」与「模型预测值」之间的差异。卡尔曼增益乘以这个残差,就是对状态估计的修正量。

协方差更新也很关键:

P_updated = P_pred - K * S * K^T

这个公式告诉我们:每次观测更新后,状态的不确定性都会降低。因为新的测量信息帮助我们缩小了可能的范围。

重要提醒:协方差矩阵P必须保持对称正定。在实际计算中,由于浮点误差,P可能会变得不对称。我建议每次更新后做一次对称化处理:

P = (P + P^T) / 2

这个小技巧能避免很多数值稳定性问题。

4.5 完整的观测更新流程

把上面所有步骤串起来,完整的观测更新流程如下:

步骤 操作 公式
1 Sigma点通过观测函数 Z_i = h(χ_i, u_k)
2 计算预测观测均值 z_pred = Σ W_i * Z_i
3 计算观测协方差 S = Σ W_i * (Z_i - z_pred)(Z_i - z_pred)^T + R
4 计算互协方差 C = Σ W_i * (χ_i - x_pred)(Z_i - z_pred)^T
5 计算卡尔曼增益 K = C * S^(-1)
6 更新状态 x_updated = x_pred + K * (z_actual - z_pred)
7 更新协方差 P_updated = P_pred - K * S * K^T

4.6 观测更新流程图

下面这张图,把观测更新的整个流程画出来了。我建议你对照着代码看,会清晰很多。

UKF观测更新流程图 输入:x_pred, P_pred 步骤1:Sigma点通过观测函数 Z_i = h(χ_i, u_k) 步骤2:计算预测观测均值 z_pred = Σ W_i * Z_i 步骤3:计算观测协方差 S 和互协方差 C S = Σ W_i*(Z_i-z_pred)(Z_i-z_pred)^T + R 步骤4:计算卡尔曼增益 K = C * S^(-1) 步骤5:更新状态与协方差 x_updated = x_pred + K*(z_actual - z_pred) 输出:x_updated, P_updated

4.7 代码实现要点

最后,我给出一个观测更新的Python代码片段。这是我在实际项目中用过的模板,你可以直接参考:

def ukf_observation_update(sigma_points, weights, x_pred, P_pred, z_actual, R):
    """
    UKF观测更新
    sigma_points: 时间更新后的Sigma点 (n_sigma, n_states)
    weights: 权重向量
    x_pred: 预测状态均值
    P_pred: 预测协方差
    z_actual: 实际测量值
    R: 观测噪声协方差
    """
    n_sigma = sigma_points.shape[0]
    n_obs = len(z_actual)
    
    # 步骤1: Sigma点通过观测函数
    Z = np.array([h_func(sp) for sp in sigma_points])
    
    # 步骤2: 预测观测均值
    z_pred = np.dot(weights, Z)
    
    # 步骤3: 观测协方差
    S = np.zeros((n_obs, n_obs))
    for i in range(n_sigma):
        diff = Z[i] - z_pred
        S += weights[i] * np.outer(diff, diff)
    S += R  # 别忘了加观测噪声
    
    # 步骤4: 互协方差
    C = np.zeros((x_pred.shape[0], n_obs))
    for i in range(n_sigma):
        diff_x = sigma_points[i] - x_pred
        diff_z = Z[i] - z_pred
        C += weights[i] * np.outer(diff_x, diff_z)
    
    # 步骤5: 卡尔曼增益
    K = C @ np.linalg.inv(S)
    
    # 步骤6: 状态更新
    x_updated = x_pred + K @ (z_actual - z_pred)
    
    # 步骤7: 协方差更新
    P_updated = P_pred - K @ S @ K.T
    
    return x_updated, P_updated

避坑指南:我曾经在写这段代码时,把np.outer和np.dot搞混了。np.outer是计算外积(向量变矩阵),np.dot是内积(矩阵乘法)。在计算协方差时一定要用np.outer,否则维度会完全不对。

好了,观测更新这部分就讲到这里。核心就是:用卡尔曼增益做加权融合,用残差做修正,用协方差更新来量化不确定性。把这个流程理解透了,UKF的整个框架你就掌握了80%。剩下的就是调参和工程化的问题了。


公众号:蓝海资料掘金营,微信deep3321