Cubature Kalman Filter(CKF)の理論とPython実装

Cubature Kalman Filter(キュバチャカルマンフィルタ)のアルゴリズムをPythonで実装します。UKFとの違いやシグマポイントの選び方を数式とともに解説します。

Cubature Kalman Filterとは

Unscented変換(UT)では、非線形変換後の確率変数の平均と共分散を、少数のシグマポイントを用いて効率的に推定する方法を紹介しました。Unscented Kalman Filter(UKF)はこのUTをカルマンフィルタの予測・更新ステップに適用したものです。

しかし、UKFには以下の問題があります:

  • パラメータ \(\alpha, \beta, \kappa\) の調整が必要
  • 高次元になると重みが負になり、共分散行列の正定値性が保証されない

Cubature Kalman Filter(CKF) は、球面-放射キュバチャ則(spherical-radial cubature rule)に基づいてシグマポイント(キュバチャポイント)を選択することで、これらの問題を解決します。CKFはパラメータのチューニングが不要で、すべての重みが正であるため、数値的に安定です。

参考文献:Arasaratnam, I., & Haykin, S. (2009). “Cubature Kalman Filters.” IEEE Transactions on Automatic Control, 54(6), 1254-1269.

理論

ガウス加重積分

非線形フィルタリングでは、以下のガウス加重積分を計算する必要があります:

\[ I = \int_{\mathbb{R}^n} f(\mathbf{x}) \cdot \mathcal{N}(\mathbf{x}; \boldsymbol{\mu}, \mathbf{P}) \, d\mathbf{x} \]

変数変換 \(\mathbf{x} = \sqrt{\mathbf{P}} \mathbf{z} + \boldsymbol{\mu}\) により、標準正規分布上の積分に変換できます:

\[ I = \int_{\mathbb{R}^n} f(\sqrt{\mathbf{P}} \mathbf{z} + \boldsymbol{\mu}) \cdot \mathcal{N}(\mathbf{z}; \mathbf{0}, \mathbf{I}) \, d\mathbf{z} \]

球面-放射キュバチャ則

標準正規分布上の積分を、球面座標と放射座標に分離します。\(\mathbf{z} = r \mathbf{s}\) と変換すると( \(r = \|\mathbf{z}\|\), \(\mathbf{s} = \mathbf{z}/\|\mathbf{z}\|\) ):

\[ I = \int_0^{\infty} \int_{U_n} f(r \mathbf{s}) \cdot r^{n-1} e^{-r^2/2} \, d\sigma(\mathbf{s}) \, dr \cdot \frac{1}{(2\pi)^{n/2}} \]

ここで \(U_n\) は \(n\) 次元単位球面、\(d\sigma(\mathbf{s})\) は球面上の面積要素です。

3次キュバチャ則では、球面上の積分点として \(2n\) 個の点を使います:

\[ \boldsymbol{\xi}_i = \sqrt{n} \, \mathbf{e}_i, \quad i = 1, \ldots, 2n \]

ここで \(\mathbf{e}_i\) は以下のように定義されます:

\[ \mathbf{e}_i = \begin{cases} \text{第 } i \text{ 単位ベクトル} & i = 1, \ldots, n \\\ -\text{第 } (i-n) \text{ 単位ベクトル} & i = n+1, \ldots, 2n \end{cases} \]

各キュバチャポイントの重みはすべて等しく:

\[ w_i = \frac{1}{2n}, \quad i = 1, \ldots, 2n \tag{1} \]

キュバチャポイントの生成

状態 \(\mathbf{x}\) の平均 \(\boldsymbol{\mu}\) と共分散 \(\mathbf{P}\) が与えられたとき、キュバチャポイントは以下で計算されます:

\[ \mathbf{X}_i = \sqrt{\mathbf{P}} \, \boldsymbol{\xi}_i + \boldsymbol{\mu}, \quad i = 1, \ldots, 2n \tag{2} \]

ここで \(\sqrt{\mathbf{P}}\) はコレスキー分解による行列平方根です。

UKFとの比較

UKFCKF
ポイント数\(2n + 1\)\(2n\)
パラメータ\(\alpha, \beta, \kappa\)なし
重み負になりうるすべて正(\(1/2n\))
理論的根拠ヒューリスティックキュバチャ則

CKFはUKFの特殊ケース(\(\alpha=1, \beta=0, \kappa=0\))とも解釈できます。

CKFアルゴリズム

状態空間モデル:

\[ \mathbf{x}_k = f(\mathbf{x}_{k-1}) + \mathbf{w}_{k-1}, \quad \mathbf{w}_{k-1} \sim \mathcal{N}(\mathbf{0}, \mathbf{Q}) \]\[ \mathbf{y}_k = h(\mathbf{x}_k) + \mathbf{v}_k, \quad \mathbf{v}_k \sim \mathcal{N}(\mathbf{0}, \mathbf{R}) \]

予測ステップ

1. キュバチャポイントの生成:

\[ \mathbf{X}_{i,k-1} = \sqrt{\mathbf{P}_{k-1}} \, \boldsymbol{\xi}_i + \hat{\mathbf{x}}_{k-1}, \quad i = 1, \ldots, 2n \tag{3} \]

2. キュバチャポイントの伝播:

\[ \mathbf{X}_{i,k|k-1}^{*} = f(\mathbf{X}_{i,k-1}) \tag{4} \]

3. 予測平均:

\[ \hat{\mathbf{x}}_{k|k-1} = \frac{1}{2n} \sum_{i=1}^{2n} \mathbf{X}_{i,k|k-1}^{*} \tag{5} \]

4. 予測共分散:

\[ \mathbf{P}_{k|k-1} = \frac{1}{2n} \sum_{i=1}^{2n} \mathbf{X}_{i,k|k-1}^{*} (\mathbf{X}_{i,k|k-1}^{*})^T - \hat{\mathbf{x}}_{k|k-1} \hat{\mathbf{x}}_{k|k-1}^T + \mathbf{Q} \tag{6} \]

更新ステップ

5. キュバチャポイントの再生成:

\[ \mathbf{X}_{i,k|k-1} = \sqrt{\mathbf{P}_{k|k-1}} \, \boldsymbol{\xi}_i + \hat{\mathbf{x}}_{k|k-1}, \quad i = 1, \ldots, 2n \tag{7} \]

6. 観測のキュバチャポイント:

\[ \mathbf{Y}_{i,k|k-1} = h(\mathbf{X}_{i,k|k-1}) \tag{8} \]

7. 予測観測平均:

\[ \hat{\mathbf{y}}_{k|k-1} = \frac{1}{2n} \sum_{i=1}^{2n} \mathbf{Y}_{i,k|k-1} \tag{9} \]

8. イノベーション共分散と相互共分散:

\[ \mathbf{S}_k = \frac{1}{2n} \sum_{i=1}^{2n} \mathbf{Y}_{i,k|k-1} \mathbf{Y}_{i,k|k-1}^T - \hat{\mathbf{y}}_{k|k-1} \hat{\mathbf{y}}_{k|k-1}^T + \mathbf{R} \tag{10} \]\[ \mathbf{C}_k = \frac{1}{2n} \sum_{i=1}^{2n} \mathbf{X}_{i,k|k-1} \mathbf{Y}_{i,k|k-1}^T - \hat{\mathbf{x}}_{k|k-1} \hat{\mathbf{y}}_{k|k-1}^T \tag{11} \]

9. カルマンゲインと状態更新:

\[ \mathbf{K}_k = \mathbf{C}_k \mathbf{S}_k^{-1} \tag{12} \]\[ \hat{\mathbf{x}}_k = \hat{\mathbf{x}}_{k|k-1} + \mathbf{K}_k (\mathbf{y}_k - \hat{\mathbf{y}}_{k|k-1}) \tag{13} \]\[ \mathbf{P}_k = \mathbf{P}_{k|k-1} - \mathbf{K}_k \mathbf{S}_k \mathbf{K}_k^T \tag{14} \]

Python実装

2次元の等速旋回運動モデルを使ってCKFを実装します。

状態空間モデルの定義

状態ベクトル \(\mathbf{x} = [p_x, p_y, v, \theta]^T\)(位置、速度、方位角)として、以下の非線形遷移モデルを考えます:

\[ f(\mathbf{x}) = \begin{bmatrix} p_x + \frac{v}{\omega}\sin(\theta + \omega \Delta t) - \frac{v}{\omega}\sin\theta \\\ p_y - \frac{v}{\omega}\cos(\theta + \omega \Delta t) + \frac{v}{\omega}\cos\theta \\\ v \\\ \theta + \omega \Delta t \end{bmatrix} \]

観測モデルは位置のみ観測可能とします:\(h(\mathbf{x}) = [p_x, p_y]^T\)。

import numpy as np
import scipy.linalg
import matplotlib.pyplot as plt

# ---- 状態空間モデルの定義 ----
dt = 1.0
omega = 0.05  # 旋回角速度 [rad/s]

def f(x):
    """非線形状態遷移関数(等速旋回運動モデル)"""
    px, py, v, theta = x
    if abs(omega) < 1e-6:
        return np.array([px + v * np.cos(theta) * dt,
                         py + v * np.sin(theta) * dt,
                         v, theta])
    return np.array([
        px + v / omega * (np.sin(theta + omega * dt) - np.sin(theta)),
        py - v / omega * (np.cos(theta + omega * dt) - np.cos(theta)),
        v,
        theta + omega * dt
    ])

def h(x):
    """観測関数(位置のみ観測可能)"""
    return np.array([x[0], x[1]])

n = 4   # 状態次元
m = 2   # 観測次元

# プロセスノイズと観測ノイズ
Q = np.diag([0.1, 0.1, 0.01, 0.001])
R = np.diag([1.0, 1.0])

CKFの実装

def generate_cubature_points(mu, P, n):
    """キュバチャポイントの生成(式2)"""
    num_points = 2 * n
    sqrt_P = scipy.linalg.cholesky(n * P, lower=True)

    points = np.zeros((n, num_points))
    for i in range(n):
        points[:, i] = mu + sqrt_P[:, i]       # +sqrt(nP) の列
        points[:, n + i] = mu - sqrt_P[:, i]   # -sqrt(nP) の列

    return points

def ckf_predict(x_est, P_est):
    """CKF予測ステップ"""
    # 式3: キュバチャポイント生成
    X = generate_cubature_points(x_est, P_est, n)
    num_points = 2 * n
    w = 1.0 / num_points  # 式1: 等しい重み

    # 式4: 非線形変換
    X_pred = np.zeros_like(X)
    for i in range(num_points):
        X_pred[:, i] = f(X[:, i])

    # 式5: 予測平均
    x_pred = w * np.sum(X_pred, axis=1)

    # 式6: 予測共分散
    P_pred = np.zeros((n, n))
    for i in range(num_points):
        diff = X_pred[:, i] - x_pred
        P_pred += w * np.outer(diff, diff)
    P_pred += Q

    return x_pred, P_pred

def ckf_update(x_pred, P_pred, y):
    """CKF更新ステップ"""
    # 式7: キュバチャポイント再生成
    X = generate_cubature_points(x_pred, P_pred, n)
    num_points = 2 * n
    w = 1.0 / num_points

    # 式8: 観測のキュバチャポイント
    Y = np.zeros((m, num_points))
    for i in range(num_points):
        Y[:, i] = h(X[:, i])

    # 式9: 予測観測平均
    y_pred = w * np.sum(Y, axis=1)

    # 式10: イノベーション共分散
    S = np.zeros((m, m))
    for i in range(num_points):
        dy = Y[:, i] - y_pred
        S += w * np.outer(dy, dy)
    S += R

    # 式11: 相互共分散
    C = np.zeros((n, m))
    for i in range(num_points):
        dx = X[:, i] - x_pred
        dy = Y[:, i] - y_pred
        C += w * np.outer(dx, dy)

    # 式12-14: カルマンゲインと状態更新
    K = C @ np.linalg.inv(S)
    x_est = x_pred + K @ (y - y_pred)
    P_est = P_pred - K @ S @ K.T

    return x_est, P_est

シミュレーション

np.random.seed(42)
T = 100  # タイムステップ数

# 真の初期状態
x_true = np.array([0.0, 0.0, 1.0, np.pi / 2])

# CKFの初期推定
x_est = np.array([0.5, -0.5, 0.8, np.pi / 2 + 0.1])
P_est = np.diag([1.0, 1.0, 0.5, 0.1])

# 記録用配列
true_states = [x_true.copy()]
measurements = []
estimates = [x_est.copy()]

for k in range(T):
    # 真の状態の更新
    x_true = f(x_true) + np.random.multivariate_normal(np.zeros(n), Q)
    true_states.append(x_true.copy())

    # 観測の生成
    y = h(x_true) + np.random.multivariate_normal(np.zeros(m), R)
    measurements.append(y.copy())

    # CKF
    x_est, P_est = ckf_predict(x_est, P_est)
    x_est, P_est = ckf_update(x_est, P_est, y)
    estimates.append(x_est.copy())

true_states = np.array(true_states)
measurements = np.array(measurements)
estimates = np.array(estimates)

# ---- 結果のプロット ----
plt.figure(figsize=(10, 8))
plt.plot(true_states[:, 0], true_states[:, 1], "b-", label="True trajectory")
plt.scatter(measurements[:, 0], measurements[:, 1],
            c="gray", s=10, alpha=0.5, label="Measurements")
plt.plot(estimates[:, 0], estimates[:, 1], "r--", label="CKF estimate")
plt.xlabel("x")
plt.ylabel("y")
plt.title("Cubature Kalman Filter - Coordinated Turn Tracking")
plt.legend()
plt.axis("equal")
plt.grid(True)
plt.tight_layout()
plt.savefig("ckf_result.png", dpi=150)
plt.show()

# RMSE計算
rmse_x = np.sqrt(np.mean((true_states[1:, 0] - estimates[1:, 0])**2))
rmse_y = np.sqrt(np.mean((true_states[1:, 1] - estimates[1:, 1])**2))
print(f"Position RMSE: x={rmse_x:.4f}, y={rmse_y:.4f}")

CKFはUKFと比較してパラメータ調整が不要であり、特に高次元の問題において数値的に安定な推定を実現できます。

参考文献

  • Arasaratnam, I., & Haykin, S. (2009). “Cubature Kalman Filters.” IEEE Transactions on Automatic Control, 54(6), 1254-1269.
  • 片山 徹 (2011). 「非線形カルマンフィルタの基礎」 計測と制御, 50(9), 638-643.