カルマンフィルタの理論とPython実装

カルマンフィルタ(Kalman Filter)のアルゴリズムをPythonで実装します。状態空間モデルの定義から予測・更新ステップの導出、1次元等速運動モデルでの実装例を解説します。

カルマンフィルタとは

カルマンフィルタ(Kalman Filter)は、線形ガウスシステムにおける最適な再帰的状態推定アルゴリズムです。ノイズを含む観測データから、システムの内部状態を逐次的に推定します。1960年にRudolf E. Kalmanによって提案されて以来、航法、制御、信号処理、経済学など幅広い分野で利用されています。

カルマンフィルタは、以下の拡張手法の基礎となります:

各手法の概要はフィルタリング手法の基礎で紹介しています。本記事では、最も基本的なカルマンフィルタの理論と実装を解説します。

状態空間モデル

カルマンフィルタは、以下の線形状態空間モデルを前提とします。

状態遷移モデル:

\[ \mathbf{x}_k = A \mathbf{x}_{k-1} + B \mathbf{u}_{k-1} + \mathbf{w}_{k-1}, \quad \mathbf{w}_{k-1} \sim \mathcal{N}(\mathbf{0}, Q) \tag{1} \]

観測モデル:

\[ \mathbf{z}_k = H \mathbf{x}_k + \mathbf{v}_k, \quad \mathbf{v}_k \sim \mathcal{N}(\mathbf{0}, R) \tag{2} \]

各変数の意味は以下の通りです:

記号意味
\(\mathbf{x}_k\)時刻 \(k\) の状態ベクトル
\(A\)状態遷移行列
\(B\)制御入力行列
\(\mathbf{u}_{k-1}\)制御入力
\(\mathbf{w}_{k-1}\)プロセスノイズ(共分散 \(Q\))
\(\mathbf{z}_k\)観測ベクトル
\(H\)観測行列
\(\mathbf{v}_k\)観測ノイズ(共分散 \(R\))

アルゴリズム

カルマンフィルタは、予測ステップ更新ステップの2段階を交互に繰り返します。

予測ステップ

前時刻の推定値から、現時刻の状態を予測します。

予測状態:

\[ \hat{\mathbf{x}}_{k|k-1} = A \hat{\mathbf{x}}_{k-1|k-1} + B \mathbf{u}_{k-1} \tag{3} \]

予測共分散:

\[ P_{k|k-1} = A P_{k-1|k-1} A^T + Q \tag{4} \]

更新ステップ

新しい観測を取り込み、予測を修正します。

イノベーション(観測残差):

\[ \mathbf{y}_k = \mathbf{z}_k - H \hat{\mathbf{x}}_{k|k-1} \tag{5} \]

イノベーション共分散:

\[ S_k = H P_{k|k-1} H^T + R \tag{6} \]

カルマンゲイン:

\[ K_k = P_{k|k-1} H^T S_k^{-1} \tag{7} \]

更新状態:

\[ \hat{\mathbf{x}}_{k|k} = \hat{\mathbf{x}}_{k|k-1} + K_k \mathbf{y}_k \tag{8} \]

更新共分散:

\[ P_{k|k} = (I - K_k H) P_{k|k-1} \tag{9} \]

カルマンゲイン \(K_k\) は、観測ノイズとプロセスノイズのバランスに応じて予測と観測の信頼度を自動的に調整します。観測ノイズが小さい(\(R\) が小さい)場合は観測を重視し、プロセスノイズが小さい(\(Q\) が小さい)場合はモデル予測を重視します。

Python実装

1次元の等速運動モデルを使ってカルマンフィルタを実装します。

状態空間モデルの定義

状態ベクトル \(\mathbf{x} = [p, v]^T\)(位置、速度)として、以下のモデルを考えます:

\[ A = \begin{bmatrix} 1 & \Delta t \\\ 0 & 1 \end{bmatrix}, \quad H = \begin{bmatrix} 1 & 0 \end{bmatrix} \]

位置のみが観測可能で、速度はフィルタによって推定されます。

import numpy as np
import matplotlib.pyplot as plt

# ---- パラメータの設定 ----
dt = 1.0  # タイムステップ [s]

# 状態遷移行列(等速運動モデル)
A = np.array([[1, dt],
              [0,  1]])

# 観測行列(位置のみ観測可能)
H = np.array([[1, 0]])

# プロセスノイズ共分散(加速度ノイズから導出)
q = 0.1  # 加速度ノイズの分散
Q = q * np.array([[dt**3 / 3, dt**2 / 2],
                  [dt**2 / 2, dt]])

# 観測ノイズ共分散
R = np.array([[1.0]])

カルマンフィルタの実装

class KalmanFilter:
    """線形カルマンフィルタ"""

    def __init__(self, A, H, Q, R, x0, P0):
        self.A = A
        self.H = H
        self.Q = Q
        self.R = R
        self.x = x0.copy()
        self.P = P0.copy()

    def predict(self, u=None, B=None):
        """予測ステップ(式3, 4)"""
        self.x = self.A @ self.x
        if u is not None and B is not None:
            self.x += B @ u
        self.P = self.A @ self.P @ self.A.T + self.Q
        return self.x.copy(), self.P.copy()

    def update(self, z):
        """更新ステップ(式5-9)"""
        # イノベーション
        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
        I = np.eye(len(self.x))
        self.P = (I - K @ self.H) @ self.P
        return self.x.copy(), self.P.copy()

シミュレーション

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

# 真の初期状態
x_true = np.array([0.0, 1.0])  # 位置0, 速度1

# カルマンフィルタの初期化
x0 = np.array([0.0, 0.0])     # 初期推定(速度を知らないと仮定)
P0 = np.diag([1.0, 1.0])      # 初期共分散
kf = KalmanFilter(A, H, Q, R, x0, P0)

# 記録用配列
true_positions = [x_true[0]]
true_velocities = [x_true[1]]
measurements = []
est_positions = [x0[0]]
est_velocities = [x0[1]]
P_history = [P0.copy()]

for k in range(T):
    # 真の状態の更新
    x_true = A @ x_true + np.random.multivariate_normal([0, 0], Q)
    true_positions.append(x_true[0])
    true_velocities.append(x_true[1])

    # 観測の生成
    z = H @ x_true + np.random.multivariate_normal([0], R)
    measurements.append(z[0])

    # カルマンフィルタ
    kf.predict()
    x_est, P_est = kf.update(z)
    est_positions.append(x_est[0])
    est_velocities.append(x_est[1])
    P_history.append(P_est.copy())

true_positions = np.array(true_positions)
true_velocities = np.array(true_velocities)
measurements = np.array(measurements)
est_positions = np.array(est_positions)
est_velocities = np.array(est_velocities)
P_history = np.array(P_history)

# ---- 結果のプロット ----
time = np.arange(T + 1)
sigma_pos = np.sqrt(P_history[:, 0, 0])

fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

# 位置の推定
axes[0].plot(time, true_positions, "b-", linewidth=1.5, label="True position")
axes[0].scatter(time[1:], measurements, c="gray", s=15, alpha=0.5,
                label="Measurements", zorder=3)
axes[0].plot(time, est_positions, "r--", linewidth=1.2, label="KF estimate")
axes[0].fill_between(time,
                      est_positions - 2 * sigma_pos,
                      est_positions + 2 * sigma_pos,
                      color="red", alpha=0.15, label="$\pm 2\sigma$")
axes[0].set_ylabel("Position")
axes[0].set_title("Kalman Filter - 1D Constant Velocity Tracking")
axes[0].legend(loc="upper left", fontsize=9)
axes[0].grid(True, alpha=0.3)

# 速度の推定
sigma_vel = np.sqrt(P_history[:, 1, 1])
axes[1].plot(time, true_velocities, "b-", linewidth=1.5, label="True velocity")
axes[1].plot(time, est_velocities, "r--", linewidth=1.2, label="KF estimate")
axes[1].fill_between(time,
                      est_velocities - 2 * sigma_vel,
                      est_velocities + 2 * sigma_vel,
                      color="red", alpha=0.15, label="$\pm 2\sigma$")
axes[1].set_xlabel("Time step $k$")
axes[1].set_ylabel("Velocity")
axes[1].legend(loc="upper left", fontsize=9)
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("kalman_filter_result.png", dpi=150)
plt.show()

# RMSE計算
rmse_pos = np.sqrt(np.mean((true_positions[1:] - est_positions[1:]) ** 2))
rmse_vel = np.sqrt(np.mean((true_velocities[1:] - est_velocities[1:]) ** 2))
print(f"Position RMSE: {rmse_pos:.4f}")
print(f"Velocity RMSE: {rmse_vel:.4f}")

結果の考察

カルマンフィルタは数ステップで収束し、ノイズの多い観測から真の状態を精度良く推定できています。\(\pm 2\sigma\) の信頼区間が真の状態をよく覆っていることから、フィルタが推定の不確かさを適切に評価していることがわかります。

速度は直接観測されていないにもかかわらず、位置の観測から間接的に推定されている点がカルマンフィルタの特徴です。

パラメータの調整について:

  • \(Q\)(プロセスノイズ)を大きくする:モデルへの信頼が低下し、観測をより重視するため応答が速くなりますが、推定がノイズの影響を受けやすくなります
  • \(R\)(観測ノイズ)を大きくする:観測への信頼が低下し、モデル予測をより重視するため推定が滑らかになりますが、真の変化への追従が遅れます

カルマンフィルタは線形ガウスシステムに限定されます。非線形システムに対しては、ヤコビ行列による線形化を用いるEKF、シグマポイントを用いるUKFやCKF、モンテカルロサンプリングに基づく粒子フィルタが用いられます。

関連記事

参考文献

  • Welch, G., & Bishop, G. (2006). “An Introduction to the Kalman Filter.” UNC Chapel Hill TR 95-041.
  • Thrun, S., Burgard, W., & Fox, D. (2005). “Probabilistic Robotics.” MIT Press.