3、多维卡尔曼滤波:矩阵形式推导、状态向量与协方差矩阵、Python实现多维KF
好,咱们进入第三讲。说实话,很多同学学卡尔曼滤波,卡就卡在多维这一关。一维的还能看懂,一扩展到矩阵就懵了。我当年刚接触时也差不多,看着满屏的矩阵符号,心里直打鼓。但后来我发现,只要把矩阵当成一个“打包工具”,事情就简单多了。
3.1 为什么需要多维?
你想想看,现实中的系统哪有那么单纯?一个无人机,既要估计位置(x,y,z),又要估计速度(vx,vy,vz),还得考虑姿态角。如果每个变量单独用一个一维滤波器,那变量之间的耦合关系就全丢了。比如位置和速度本来就是导数关系,分开滤波反而会引入误差。
所以,多维卡尔曼滤波的核心思想就是:把所有待估计的状态量打包成一个向量,用一个统一的框架去处理它们之间的相关性。
3.2 状态向量与协方差矩阵
咱们先看状态向量。假设我们要估计一个物体的二维位置和速度,那状态向量可以写成:
x = [px, py, vx, vy]^T
嗯,这里用上标T表示转置,实际上是个列向量。4个状态量,这就是一个4维的状态空间。
那协方差矩阵呢?它长这样:
P = | σ²_px σ_px_py σ_px_vx σ_px_vy |
| σ_py_px σ²_py σ_py_vx σ_py_vy |
| σ_vx_px σ_vx_py σ²_vx σ_vx_vy |
| σ_vy_px σ_vy_py σ_vy_vx σ²_vy |
对角线是每个状态的方差,非对角线是状态之间的协方差。比如σ_px_vx表示位置px和速度vx的相关程度。如果这个值很大,说明位置和速度强相关——这很合理,因为位置变化快慢本来就由速度决定。
关键点:协方差矩阵必须是对称正定的。我在项目中遇到过有人手写代码时忘了维护对称性,结果滤波器直接发散。所以,每次更新完P矩阵,最好加一句:P = (P + P.T) / 2,强制对称。
3.3 矩阵形式的卡尔曼滤波公式
好了,重头戏来了。多维卡尔曼滤波的五个核心公式,用矩阵写出来其实非常简洁:
预测阶段:
x_pred = F @ x_prev + B @ u
P_pred = F @ P_prev @ F.T + Q
更新阶段:
K = P_pred @ H.T @ inv(H @ P_pred @ H.T + R)
x_updated = x_pred + K @ (z - H @ x_pred)
P_updated = (I - K @ H) @ P_pred
你看,和一维的公式长得一模一样,只是把标量换成了矩阵。F是状态转移矩阵,H是观测矩阵,Q是过程噪声协方差,R是观测噪声协方差。
我个人习惯把F矩阵画成这样的结构:
| 状态 | px | py | vx | vy |
|---|---|---|---|---|
| px' | 1 | 0 | dt | 0 |
| py' | 0 | 1 | 0 | dt |
| vx' | 0 | 0 | 1 | 0 |
| vy' | 0 | 0 | 0 | 1 |
说白了,这就是一个“恒定速度模型”。位置更新时加上速度×时间,速度保持不变。如果你要加加速度(加速度),那就得扩展状态向量了。
3.4 Python实现多维KF
纸上谈兵没意思,咱们直接上代码。我写了一个通用的多维卡尔曼滤波器类,你可以直接拿去用:
import numpy as np
class MultiDimKalmanFilter:
def __init__(self, F, H, Q, R, x_init, P_init):
self.F = F # 状态转移矩阵
self.H = H # 观测矩阵
self.Q = Q # 过程噪声协方差
self.R = R # 观测噪声协方差
self.x = x_init # 初始状态
self.P = P_init # 初始协方差
def predict(self, u=None, B=None):
# 预测步骤
if u is not None and B is not None:
self.x = self.F @ self.x + B @ u
else:
self.x = self.F @ self.x
self.P = self.F @ self.P @ self.F.T + self.Q
return self.x, self.P
def update(self, z):
# 更新步骤
y = z - self.H @ self.x # 残差
S = self.H @ self.P @ self.H.T + self.R # 残差协方差
K = self.P @ self.H.T @ np.linalg.inv(S) # 卡尔曼增益
self.x = self.x + K @ y
self.P = (np.eye(self.P.shape[0]) - K @ self.H) @ self.P
return self.x, self.P
避坑指南:我曾经在计算卡尔曼增益时直接用了np.linalg.inv(S),结果遇到奇异矩阵直接报错。后来我改用np.linalg.solve(S.T, H.T).T,或者用scipy.linalg.inv加rcond参数。更稳妥的做法是:K = np.linalg.solve(S, H @ self.P).T,这样数值稳定性好很多。
3.5 一个完整的例子:二维目标跟踪
咱们来跑一个实际例子。假设用雷达跟踪一个匀速直线运动的物体,雷达只能测距离和角度:
# 参数设置
dt = 0.1 # 时间步长
F = np.array([[1, 0, dt, 0],
[0, 1, 0, dt],
[0, 0, 1, 0],
[0, 0, 0, 1]])
# 观测矩阵:只观测位置
H = np.array([[1, 0, 0, 0],
[0, 1, 0, 0]])
# 噪声协方差
Q = np.eye(4) * 0.01 # 过程噪声
R = np.eye(2) * 0.1 # 观测噪声
# 初始状态
x_init = np.array([0, 0, 1, 0.5])
P_init = np.eye(4) * 10
# 创建滤波器
kf = MultiDimKalmanFilter(F, H, Q, R, x_init, P_init)
# 模拟数据
true_states = []
measurements = []
for t in range(100):
true_x = np.array([t*dt*1, t*dt*0.5, 1, 0.5])
true_states.append(true_x)
z = true_x[:2] + np.random.normal(0, 0.1, 2)
measurements.append(z)
# 运行滤波
estimates = []
for z in measurements:
kf.predict()
est, _ = kf.update(z)
estimates.append(est.copy())
print("滤波完成!估计误差:", np.mean(np.abs(np.array(estimates) - np.array(true_states)), axis=0))
跑完你会发现,位置估计误差大概在0.1左右,速度估计误差在0.05左右。嗯,效果还不错。
3.6 调参心得
最后聊点实战经验。Q和R的调参,说白了就是平衡“相信模型”还是“相信测量”。
- Q太大:滤波器太相信测量,结果噪声大,抖动厉害
- Q太小:滤波器太相信模型,结果响应慢,跟不上真实变化
- R太大:不相信测量,滤波结果平滑但滞后
- R太小:太相信测量,噪声直接传进估计值
我一般先用Q = np.eye(n) * 0.01和R = np.eye(m) * 0.1作为起点,然后看残差序列。如果残差均值不为零,说明模型有偏差;如果残差方差太大,说明R设小了。
注意:千万别把Q设成零矩阵!我曾经犯过这个错,结果滤波器“死”了——因为模型完全不相信任何新信息,估计值再也不更新了。Q再小也得有点值,哪怕1e-6都行。
好了,这一讲就到这里。矩阵形式的卡尔曼滤波,说白了就是把一维的标量运算“打包”成矩阵运算。你只要把F、H、Q、R这四个矩阵搞明白,剩下的就是矩阵乘法的事了。下一讲咱们聊聊非线性情况下的扩展卡尔曼滤波(EKF),那才是真正考验矩阵功底的时候。