はじめに
カルマンフィルタは線形ガウスシステムで最適ですが、非線形システムでは直接適用できません。**拡張カルマンフィルタ(EKF)**はヤコビアンによる線形化で対処しますが、高次の非線形性では精度が低下します。
Unscented Kalman Filter(UKF)は、確率分布を代表する少数のシグマポイントを非線形関数に通すことで、ヤコビアンの計算なしに高精度な推定を実現します。
Unscented 変換
シグマポイントの生成
\(n\) 次元の状態 \(\mathbf{x}\)(平均 \(\bar{\mathbf{x}}\), 共分散 \(\mathbf{P}\))に対し、\(2n+1\) 個のシグマポイントを生成します。
\[\boldsymbol{\chi}_0 = \bar{\mathbf{x}} \tag{1}\]\[\boldsymbol{\chi}_i = \bar{\mathbf{x}} + \left(\sqrt{(n + \lambda) \mathbf{P}}\right)_i, \quad i = 1, \ldots, n \tag{2}\]\[\boldsymbol{\chi}_{i+n} = \bar{\mathbf{x}} - \left(\sqrt{(n + \lambda) \mathbf{P}}\right)_i, \quad i = 1, \ldots, n \tag{3}\]ここで \(\lambda = \alpha^2(n + \kappa) - n\) はスケーリングパラメータです。
重み
\[W_0^{(m)} = \frac{\lambda}{n + \lambda}, \quad W_0^{(c)} = \frac{\lambda}{n + \lambda} + (1 - \alpha^2 + \beta) \tag{4}\]\[W_i^{(m)} = W_i^{(c)} = \frac{1}{2(n + \lambda)}, \quad i = 1, \ldots, 2n \tag{5}\]典型的なパラメータ: \(\alpha = 10^{-3}\), \(\beta = 2\)(ガウス分布で最適), \(\kappa = 0\)
変換後の統計量
シグマポイントを非線形関数 \(f\) に通した後:
\[\boldsymbol{\mathcal{Y}}_i = f(\boldsymbol{\chi}_i) \tag{6}\]\[\bar{\mathbf{y}} = \sum_{i=0}^{2n} W_i^{(m)} \boldsymbol{\mathcal{Y}}_i \tag{7}\]\[\mathbf{P}_y = \sum_{i=0}^{2n} W_i^{(c)} (\boldsymbol{\mathcal{Y}}_i - \bar{\mathbf{y}})(\boldsymbol{\mathcal{Y}}_i - \bar{\mathbf{y}})^T \tag{8}\]UKF アルゴリズム
予測ステップ
- 現在の状態推定 \((\hat{\mathbf{x}}_{k-1}, \mathbf{P}_{k-1})\) からシグマポイントを生成
- 状態遷移関数 \(f\) で変換: \(\boldsymbol{\chi}_{k|k-1} = f(\boldsymbol{\chi}_{k-1})\)
- 予測平均と共分散を計算:
更新ステップ
- 予測シグマポイントを観測関数 \(h\) で変換
- カルマンゲインを計算:
- 状態と共分散を更新:
EKFとの比較
| 特徴 | EKF | UKF |
|---|---|---|
| 線形化方法 | ヤコビアン(1次近似) | シグマポイント(2次精度) |
| ヤコビアン計算 | 必要 | 不要 |
| 非線形精度 | 中程度 | 高い |
| 計算コスト | \(O(n^2)\) | \(O(n^3)\)(コレスキー分解) |
Python実装
2次元の等速円運動を追跡する例です。
import numpy as np
import matplotlib.pyplot as plt
class UKF:
def __init__(self, n, m, f, h, Q, R, alpha=1e-3, beta=2, kappa=0):
self.n = n
self.m = m
self.f = f
self.h = h
self.Q = Q
self.R = R
self.lam = alpha**2 * (n + kappa) - n
self.gamma = np.sqrt(n + self.lam)
# 重み
self.Wm = np.full(2 * n + 1, 1 / (2 * (n + self.lam)))
self.Wc = np.full(2 * n + 1, 1 / (2 * (n + self.lam)))
self.Wm[0] = self.lam / (n + self.lam)
self.Wc[0] = self.lam / (n + self.lam) + (1 - alpha**2 + beta)
def sigma_points(self, x, P):
L = np.linalg.cholesky(P)
sigmas = np.zeros((2 * self.n + 1, self.n))
sigmas[0] = x
for i in range(self.n):
sigmas[i + 1] = x + self.gamma * L[:, i]
sigmas[i + 1 + self.n] = x - self.gamma * L[:, i]
return sigmas
def predict(self, x, P, dt):
sigmas = self.sigma_points(x, P)
sigmas_pred = np.array([self.f(s, dt) for s in sigmas])
x_pred = np.dot(self.Wm, sigmas_pred)
P_pred = self.Q.copy()
for i in range(2 * self.n + 1):
d = sigmas_pred[i] - x_pred
P_pred += self.Wc[i] * np.outer(d, d)
return x_pred, P_pred, sigmas_pred
def update(self, x_pred, P_pred, sigmas_pred, z):
sigmas_z = np.array([self.h(s) for s in sigmas_pred])
z_pred = np.dot(self.Wm, sigmas_z)
Pzz = self.R.copy()
Pxz = np.zeros((self.n, self.m))
for i in range(2 * self.n + 1):
dz = sigmas_z[i] - z_pred
dx = sigmas_pred[i] - x_pred
Pzz += self.Wc[i] * np.outer(dz, dz)
Pxz += self.Wc[i] * np.outer(dx, dz)
K = Pxz @ np.linalg.inv(Pzz)
x_new = x_pred + K @ (z - z_pred)
P_new = P_pred - K @ Pzz @ K.T
return x_new, P_new
# --- 等速円運動モデル ---
def state_transition(x, dt):
px, py, vx, vy = x
return np.array([px + vx * dt, py + vy * dt, vx, vy])
def observation(x):
return np.array([x[0], x[1]])
# --- シミュレーション ---
np.random.seed(42)
dt = 0.1
T = 10
steps = int(T / dt)
omega = 0.5 # 角速度
true_states = np.zeros((steps, 4))
true_states[0] = [5, 0, 0, 5 * omega]
for k in range(1, steps):
t = k * dt
true_states[k] = [5 * np.cos(omega * t), 5 * np.sin(omega * t),
-5 * omega * np.sin(omega * t),
5 * omega * np.cos(omega * t)]
R = np.diag([0.5**2, 0.5**2])
measurements = true_states[:, :2] + np.random.multivariate_normal(
[0, 0], R, steps)
Q = np.diag([0.01, 0.01, 0.1, 0.1])
ukf = UKF(n=4, m=2, f=state_transition, h=observation, Q=Q, R=R)
x_est = np.array([measurements[0, 0], measurements[0, 1], 0, 2.5])
P_est = np.eye(4)
estimates = [x_est.copy()]
for k in range(1, steps):
x_pred, P_pred, sigmas_pred = ukf.predict(x_est, P_est, dt)
x_est, P_est = ukf.update(x_pred, P_pred, sigmas_pred, measurements[k])
estimates.append(x_est.copy())
estimates = np.array(estimates)
# --- 可視化 ---
plt.figure(figsize=(8, 8))
plt.plot(true_states[:, 0], true_states[:, 1], 'b-', linewidth=2,
label='True')
plt.scatter(measurements[:, 0], measurements[:, 1], c='gray', s=10,
alpha=0.3, label='Measurements')
plt.plot(estimates[:, 0], estimates[:, 1], 'r--', linewidth=1.5,
label='UKF estimate')
plt.xlabel('x')
plt.ylabel('y')
plt.title('UKF: Circular Motion Tracking')
plt.legend()
plt.axis('equal')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
関連記事
- パーティクルフィルタのPython実装 - UKFより高い非線形性にはパーティクルフィルタが有効です。
- Unscented変換の基礎 - UKFの核となるUnscented変換の詳細な解説です。
- ウィーナーフィルタ - 定常過程における最適線形フィルタ。UKFとの位置づけの違いを理解できます。
- 指数移動平均(EMA)フィルタの周波数特性 - 最も基本的なフィルタとの比較で、UKFの高度さが分かります。
参考文献
- Julier, S. J., & Uhlmann, J. K. (2004). “Unscented Filtering and Nonlinear Estimation”. Proceedings of the IEEE, 92(3), 401-422.
- Wan, E. A., & van der Merwe, R. (2000). “The Unscented Kalman Filter for Nonlinear Estimation”. Proceedings of the IEEE Adaptive Systems for Signal Processing, Communications, and Control Symposium.
- Simon, D. (2006). Optimal State Estimation. Wiley. Chapter 14.