はじめに:フィルタリングとスムージングの違い
状態空間モデルにおける状態推定には、使える観測の範囲によって 2 つの問題設定があります。
- フィルタリング:時刻 \(k\) までの観測 \(y_{1:k}\) を用いた推定 → \(p(x_k \mid y_{1:k})\) 。因果的(causal) な処理で、観測が届くたびに推定を更新できるため オンライン・リアルタイム処理 に適します。
- スムージング:時刻 \(k\) より未来の観測も用いた推定 → \(p(x_k \mid y_{1:T})\) (\(T > k\) )。非因果的(non-causal) な処理で、未来の情報を使うぶんフィルタより常に同等以上の精度が得られますが、その分の 遅延 が発生します。
「未来の観測をどこまで待つか」によって、スムーザは大きく 3 種類に分類されます。
- 固定区間スムーザ(fixed-interval smoother):全観測 \(y_{1:T}\) が揃ってから、全時刻 \(k = 0, \ldots, T\) の \(\hat{x}_{k|T}\) を推定する。代表が Rauch-Tung-Striebel(RTS)スムーザ 。完全オフライン。
- 固定ラグスムーザ(fixed-lag smoother):常に \(N\) ステップ前の状態 \(\hat{x}_{k-N|k}\) を推定する。遅延 \(N\) を許容する準リアルタイム処理。
- 固定点スムーザ(fixed-point smoother):特定の時刻 \(j\) の状態 \(\hat{x}_{j|k}\) を、観測が増えるたびに更新し続ける。初期状態やイベント発生時刻の推定に使う。
本記事では 3 種のスムーザの漸化式を導出し、計算量・遅延・用途を比較したうえで、numpy フルスクラッチの Python 実装でカルマンフィルタ・固定ラグ・RTS の RMSE を実測比較します。
基盤となる理論は カルマンフィルタの理論とPython実装 を参照してください。
3種スムーザの比較表
はじめに結論の比較表を示します。\(n\) は状態次元、\(T\) はデータ長、\(N\) はラグ長です。
| 項目 | 固定区間(RTS) | 固定ラグ | 固定点 |
|---|---|---|---|
| 推定対象 | 全時刻の \(\hat{x}_{k\|T}\) | \(\hat{x}_{k-N\|k}\) (常に \(N\) 步前) | 特定時刻の \(\hat{x}_{j\|k}\) |
| 遅延 | 全データ到着まで(オフライン) | \(N\) ステップ(準リアルタイム) | なし(逐次更新) |
| 計算量 / ステップ | \(O(n^3)\) × 前向き+後向き 2 パス | \(O(N n^3)\) (窓RTS)/ \(O((Nn)^3)\) (拡大状態) | \(O(n^3)\) |
| メモリ | \(O(T n^2)\) (全中間結果を保存) | \(O(N n^2)\) | \(O(n^2)\) |
| 精度 | 最良(全観測を利用) | \(N \to \infty\) で RTS に漸近 | 対象時刻のみ RTS と同等に漸近 |
| 主な用途 | バッチ解析・軌道再構成・EMパラメータ推定 | 通信・音声強調・目標追尾などの遅延許容処理 | 初期状態推定・イベント時刻精密化 |
カルマンフィルタの復習(前向きパス)
3 種のスムーザはいずれもカルマンフィルタの前向きパスの結果を入力とします。線形ガウス状態空間モデルを定義します。
\[ x_k = F x_{k-1} + w_{k-1}, \quad w_{k-1} \sim \mathcal{N}(0, Q) \tag{1} \] \[ y_k = H x_k + v_k, \quad v_k \sim \mathcal{N}(0, R) \tag{2} \]予測ステップ
\[ \hat{x}_{k|k-1} = F \hat{x}_{k-1|k-1} \tag{3} \] \[ P_{k|k-1} = F P_{k-1|k-1} F^T + Q \tag{4} \]更新ステップ
\[ K_k = P_{k|k-1} H^T (H P_{k|k-1} H^T + R)^{-1} \tag{5} \] \[ \hat{x}_{k|k} = \hat{x}_{k|k-1} + K_k (y_k - H \hat{x}_{k|k-1}) \tag{6} \] \[ P_{k|k} = (I - K_k H) P_{k|k-1} \tag{7} \]スムージングでは各時刻の \(\hat{x}_{k|k}, P_{k|k}, \hat{x}_{k|k-1}, P_{k|k-1}\) を保存しておく必要があります。非線形系では式 \((3)\) 〜\((7)\) を EKF や UKF の対応するステップに置き換えることで、同じ枠組みのままスムーザを構成できます。
固定区間スムーザ(RTS)
固定区間スムージングは、全観測 \(y_{1:T}\) を使って全時刻の事後分布 \(p(x_k \mid y_{1:T})\) を求める問題です。RTS スムーザは時刻 \(T\) から \(0\) へ向かう後向き再帰で解きます。
スムーザゲイン:
\[ G_k = P_{k|k} F^T P_{k+1|k}^{-1} \tag{8} \]平滑化状態・共分散:
\[ \hat{x}_{k|T} = \hat{x}_{k|k} + G_k (\hat{x}_{k+1|T} - \hat{x}_{k+1|k}) \tag{9} \] \[ P_{k|T} = P_{k|k} + G_k (P_{k+1|T} - P_{k+1|k}) G_k^T \tag{10} \]初期条件は \(\hat{x}_{T|T}, P_{T|T}\) (前向きパスの最終結果)で、\(k = T-1, \ldots, 0\) の順に適用します。\(P_{k+1|T} \preceq P_{k+1|k}\) より \(P_{k|T} \preceq P_{k|k}\) が常に成り立ち、スムーザの不確実性はフィルタ以下になります。導出の詳細は RTSスムーザの記事 を参照してください。
固定ラグスムーザ
固定ラグスムーザは、時刻 \(k\) の観測を受け取るたびに \(N\) ステップ前の平滑化推定 \(\hat{x}_{k-N|k}\) を出力します。ストリーミング処理のまま「\(N\) ステップ分だけ未来を見た」推定が得られるのが特徴です。
実現方法1:拡大状態カルマンフィルタ
過去 \(N+1\) 時刻分の状態を縦に積んだ拡大状態ベクトル
\[ z_k = \begin{bmatrix} x_k^T & x_{k-1}^T & \cdots & x_{k-N}^T \end{bmatrix}^T \tag{11} \]を定義し、対応する拡大遷移行列(1 ブロックずつシフトする構造)に対して通常のカルマンフィルタをそのまま回す方法です(Biswas–Mahalanabis 固定ラグスムーザ)。実装は単純ですが、状態次元が \((N+1)n\) になるため計算量は \(O((Nn)^3)\) とラグ長に対して急速に増えます。
実現方法2:窓付きRTS(本記事の実装)
各時刻 \(k\) で、直近の窓 \([k-N, k]\) に限定して RTS の後向き再帰(式 \((8)\) 〜\((9)\) )を回し、窓の先頭 \(\hat{x}_{k-N|k}\) だけを出力する方法です。1 ステップあたりの計算量は \(O(N n^3)\) で済み、前向きパスの中間結果を直近 \(N\) 個だけ保持すればよいためメモリも \(O(N n^2)\) です。窓の終端 \(k\) を「あたかもそこでデータが尽きた固定区間」とみなして RTS の後ろ向き再帰を \(N\) 回だけ回している、というのがこの実装の数学的な正体です。
実現方法1(拡大状態)と実現方法2(窓付きRTS)は定式化がまったく異なりますが、同じ量 \(\hat{x}_{k-N|k}\) を計算しているはずです。この一致は、後述の「Python実装」節で両方を実装して数値的に検証します。
ラグ長 \(N\) と精度のトレードオフ:分散ギャップの減衰則の導出
「\(N\) を増やすほど精度が上がるが改善は飽和する」という挙動を、RTSの共分散漸化式 \((10)\) から定量的に導出します。
時刻 \(j\) を固定し、窓の終端を \(k\) とした固定ラグ推定の共分散 \(P_{j|k}\) (窓付きRTSの実体は「\(k\) で打ち切った固定区間スムーザ」なので、これはまさに式 \((10)\) の \(T=k\) の場合そのものです)と、全データ \(T\) を使った RTS の共分散 \(P_{j|T}\) との差
\[ \delta_j^{(k)} := P_{j|k} - P_{j|T} \]を考えます。式 \((10)\) を \(T=k\) の場合と \(T\) の場合それぞれについて時刻 \(j\) で適用すると
\[ P_{j|k} = P_{j|j} + G_j (P_{j+1|k} - P_{j+1|j}), \qquad P_{j|T} = P_{j|j} + G_j (P_{j+1|T} - P_{j+1|j}) \]となり(\(G_j\) は未来のホライズンによらず前向きパスの量だけで決まるため、両者で共通です)、辺々差し引くと \(P_{j|j}\) と \(G_j P_{j+1|j}\) の項が消えて
\[ \delta_j^{(k)} = G_j \left(P_{j+1|k} - P_{j+1|T}\right) G_j^T = G_j\, \delta_{j+1}^{(k)}\, G_j^T \tag{12} \]という簡潔な後ろ向き漸化式が得られます。これを \(i=k-1,\ldots,j\) について繰り返し適用すると
\[ \delta_j^{(k)} = \left(\prod_{i=j}^{k-1} G_i\right) \delta_k^{(k)} \left(\prod_{i=j}^{k-1} G_i\right)^{T} \]となり、\(\delta_k^{(k)} = P_{k|k} - P_{k|T}\) は「通常の(ラグなし)フィルタ対RTSの分散改善量」そのものです。定常状態では \(G_i \to \bar{G}\) 、\(\delta_k^{(k)} \to \bar\delta := \bar{P}_{k|k} - \bar{P}_{k|T}\) とみなせるので、\(N = k-j\) (ラグ長)とおくと(スカラー系では転置が単なる2乗になることに注意して)
\[ \delta_j^{(j+N)} \approx \bar{G}^{2N}\, \bar\delta \tag{13} \]を得ます。式 \((9)\) の平均レベルの補正は \(\bar{G}^N\) で減衰する(ゲイン \(G\) が1回だけ掛かる線形量)のに対し、共分散(=MSE)レベルのギャップは \(G\) が2回(\(G_j(\cdot)G_j^T\) の合同変換)掛かるぶん \(\bar{G}^{2N}\) という指数が2倍の速さで減衰することが分かります。両者は推定量が線形か2次形式かの違いに対応する、独立に導出される事実です。
バイアスを無視すれば MSE は分散に一致するため、\(\delta_k^{(k)} = \mathrm{MSE}_{\mathrm{KF}} - \mathrm{MSE}_{\mathrm{RTS}}\) と対応付けられ、固定ラグ推定のMSEについて次の予測式が得られます。
\[ \mathrm{MSE}_{\mathrm{lag}}(N) \approx \mathrm{MSE}_{\mathrm{RTS}} + \left(\mathrm{MSE}_{\mathrm{KF}} - \mathrm{MSE}_{\mathrm{RTS}}\right) \bar{G}^{2N} \tag{14} \]- \(N\) を増やすほど RTS(\(N = \infty\) 相当)の精度に漸近するが、改善は \(\bar{G}^{2N}\) で指数的に飽和する
- 遅延とメモリは \(N\) に線形に増加する
実務では「\(\bar{G}^{2N}\) が十分小さくなる \(N\) 」(システムの時定数の数倍程度)を選べば、RTS とほぼ同等の精度が遅延 \(N\) で得られます。この式 \((14)\) の予測精度は、後述のモンテカルロ実験で実測値と直接照合します。
固定点スムーザ
固定点スムーザは、関心のある特定の時刻 \(j\) (例:初期時刻、イベント発生時刻)の推定 \(\hat{x}_{j|k}\) を、新しい観測 \(y_k\) (\(k > j\) )が届くたびに更新します。更新式を天下りに提示するのではなく、RTSの後ろ向き再帰から導出します。
状態推定の導出:RTS再帰のテレスコーピング
\(j \le i \le k\) に対して、時刻 \(i\) の「フィルタ推定からの平滑化補正量」を \(\Delta_i^{(k)} := \hat{x}_{i|k} - \hat{x}_{i|i}\) と定義します(\(\Delta_k^{(k)} = 0\) )。式 \((9)\) を \(T=k\) として時刻 \(i\) に適用すると
\[ \Delta_i^{(k)} = \hat{x}_{i|k} - \hat{x}_{i|i} = G_i (\hat{x}_{i+1|k} - \hat{x}_{i+1|i}) \]ここで \(\hat{x}_{i+1|i+1}\) を足して引くと
\[ \Delta_i^{(k)} = G_i \left[ \big(\hat{x}_{i+1|k} - \hat{x}_{i+1|i+1}\big) + \big(\hat{x}_{i+1|i+1} - \hat{x}_{i+1|i}\big) \right] = G_i \left(\Delta_{i+1}^{(k)} + D_{i+1}\right) \tag{15} \]となります。ここで \(D_{i+1} := \hat{x}_{i+1|i+1} - \hat{x}_{i+1|i} = K_{i+1} \nu_{i+1}\) (\(\nu_{i+1} = y_{i+1} - H\hat{x}_{i+1|i}\) )は、平滑化を一切使わないフィルタだけで決まる量です。式 \((15)\) を \(i = k-1, k-2, \ldots, j\) について展開すると(\(\Delta_k^{(k)}=0\) を初期値として)
\[ \Delta_j^{(k)} = \sum_{i=j+1}^{k} B_i D_i, \qquad B_i := \prod_{l=j}^{i-1} G_l \ \ (B_j = I) \tag{16} \]が得られます。ここで \(B_i\) 自身も \(B_i = B_{i-1} G_{i-1}\) (\(B_j = I\) )という単純な積の再帰を満たします。つまり \(\hat{x}_{j|k} = \hat{x}_{j|j} + \sum_{i=j+1}^{k} B_i K_i \nu_i\) であり、ホライズンを \(k-1\) から \(k\) へ1つ進めたときの差分を取れば
\[ \hat{x}_{j|k} = \hat{x}_{j|k-1} + B_k K_k \nu_k \tag{17} \]という、観測が届くたびに \(O(n^3)\) で更新できる漸化式が導出されました。これが冒頭に示した式である固定点スムーザの更新式です。
共分散の導出:カルマンフィルタのイノベーション直交性
誤差を \(e_j^{(k)} := x_j - \hat{x}_{j|k}\) とおくと、式 \((16)\) より \(e_j^{(k)} = e_j^{(j)} - \sum_{i=j+1}^{k} B_i K_i \nu_i\) です。カルマンフィルタのイノベーション \(\nu_i\) は互いに無相関(ホワイトノイズ)であり、かつ \(i > j\) のとき \(\nu_i\) は \(x_j\) の推定誤差 \(e_j^{(j)}\) とも無相関である(イノベーションの直交性、カルマンフィルタの標準的な性質)ため、共分散は各項の和として分解できます。
\[ P_{j|k} = P_{j|j} - \sum_{i=j+1}^{k} B_i K_i S_i K_i^T B_i^T, \qquad S_i = H P_{i|i-1} H^T + R \]この漸化式の1ステップ版は \(P_{j|k} = P_{j|k-1} - B_k K_k S_k K_k^T B_k^T\) です。ここで更新式 \((7)\) と \(K_k = P_{k|k-1}H^T S_k^{-1}\) から従う恒等式 \(P_{k|k} - P_{k|k-1} = -K_k H P_{k|k-1} = -K_k S_k K_k^T\) を代入すると
\[ P_{j|k} = P_{j|k-1} + B_k (P_{k|k} - P_{k|k-1}) B_k^T \tag{18} \]となり、これも冒頭の式と一致します。式 \((17)\) 〜\((18)\) の1ステップ更新は \(O(n^3)\) 、時刻 \(j\) の情報だけを追跡するためメモリは \(O(n^2)\) で固定です。\(B_k\) は \(k\) が進むにつれ縮小していく(式 \((13)\) の減衰則と同じ機構)ため、更新量は次第に小さくなり、\(\hat{x}_{j|k}\) は固定区間スムーザの \(\hat{x}_{j|T}\) に漸近します。
固定点スムーザは、初期状態 \(x_0\) の高精度推定(パラメータ推定の前処理)やイベント発生時刻の精密化に使われますが、拡大状態への埋め込みを避けて直接この漸化式を解く実装は数値的にデリケートで、2024年にも Cholesky分解ベースの新しい安定化手法が提案されています(後述の「近年の研究動向」を参照)。
Python実装:ランダムウォークでのRMSE比較
1 次元ランダムウォーク+観測ノイズという最もシンプルな系で、カルマンフィルタ・固定ラグスムーザ・RTS スムーザの精度を比較します。
モデル定義とデータ生成
\[ x_k = x_{k-1} + w_{k-1}, \quad y_k = x_k + v_k, \quad w \sim \mathcal{N}(0, 0.05), \; v \sim \mathcal{N}(0, 1) \tag{19} \]import numpy as np
# ---- 状態空間モデル:ランダムウォーク + 観測ノイズ ----
T = 300 # タイムステップ数
F = np.array([[1.0]]) # 状態遷移(ランダムウォーク)
H = np.array([[1.0]]) # 観測行列
Q = np.array([[0.05]]) # プロセスノイズ分散
R = np.array([[1.0]]) # 観測ノイズ分散
n, m = 1, 1
x0 = np.array([0.0])
P0 = np.diag([1.0])
カルマンフィルタ(前向きパス)
def kalman_filter(y_obs, F, H, Q, R, x0, P0):
"""カルマンフィルタの前向きパス(スムーザ用に中間結果をすべて保存)"""
T = len(y_obs)
n = len(x0)
x_filt = np.zeros((T + 1, n)) # x_{k|k}
P_filt = np.zeros((T + 1, n, n)) # P_{k|k}
x_pred = np.zeros((T, n)) # x_{k|k-1}
P_pred = np.zeros((T, n, n)) # P_{k|k-1}
x_filt[0], P_filt[0] = x0, P0
for k in range(T):
# 予測(式3, 4)
x_pred[k] = F @ x_filt[k]
P_pred[k] = F @ P_filt[k] @ F.T + Q
# 更新(式5, 6, 7)
S = H @ P_pred[k] @ H.T + R
K = P_pred[k] @ H.T @ np.linalg.inv(S)
x_filt[k + 1] = x_pred[k] + K @ (y_obs[k] - H @ x_pred[k])
P_filt[k + 1] = (np.eye(n) - K @ H) @ P_pred[k]
return x_filt, P_filt, x_pred, P_pred
RTSスムーザ(後向きパス)
def rts_smoother(x_filt, P_filt, x_pred, P_pred, F):
"""RTS(固定区間)スムーザの後向きパス"""
T = len(x_pred)
n = x_filt.shape[1]
x_smooth = np.zeros((T + 1, n))
P_smooth = np.zeros((T + 1, n, n))
x_smooth[T], P_smooth[T] = x_filt[T], P_filt[T]
for k in range(T - 1, -1, -1):
G = P_filt[k] @ F.T @ np.linalg.inv(P_pred[k]) # 式8
x_smooth[k] = x_filt[k] + G @ (x_smooth[k + 1] - x_pred[k]) # 式9
P_smooth[k] = P_filt[k] + G @ (P_smooth[k + 1] - P_pred[k]) @ G.T # 式10
return x_smooth, P_smooth
固定ラグスムーザ(窓付きRTS)
def fixed_lag_smoother(y_obs, F, H, Q, R, x0, P0, N):
"""固定ラグスムーザ:各時刻 k で直近 N ステップ分だけ後向き再帰を回し
x_{k-N|k} を出力する(窓付きRTSによる実装)"""
T = len(y_obs)
n = len(x0)
x_filt, P_filt, x_pred, P_pred = kalman_filter(y_obs, F, H, Q, R, x0, P0)
x_lag = np.zeros((T + 1, n))
x_lag[0] = x0
for k in range(1, T + 1):
j0 = max(0, k - N) # 出力する時刻 k-N
xs = x_filt[k].copy()
for j in range(k - 1, j0 - 1, -1): # 窓 [j0, k] で後向き再帰
G = P_filt[j] @ F.T @ np.linalg.inv(P_pred[j])
xs = x_filt[j] + G @ (xs - x_pred[j])
x_lag[j0] = xs
# 末尾 N 点はラグ分の未来観測が無いので、最終窓の後向き再帰で埋める
xs = x_filt[T].copy()
x_lag[T] = xs
for j in range(T - 1, max(0, T - N) - 1, -1):
G = P_filt[j] @ F.T @ np.linalg.inv(P_pred[j])
xs = x_filt[j] + G @ (xs - x_pred[j])
x_lag[j] = xs
return x_lag
固定ラグの2つの実装法の一致(数値検証)
理論編で述べた「実現方法1(拡大状態)と実現方法2(窓付きRTS)は同じ量を計算しているはず」という主張を、実際に両方を実装して確かめます。
def fixed_lag_augmented(y_obs, F, H, Q, R, x0, P0, N):
"""Biswas-Mahalanabis 型:拡大状態 z_k = [x_k; x_{k-1}; ...; x_{k-N}] に
対して通常のカルマンフィルタを回し、末尾ブロック(= x_{k-N|k})を読み出す"""
n = len(x0)
T = len(y_obs)
B = N + 1
A = np.zeros((B * n, B * n))
A[0:n, 0:n] = F # 先頭ブロックだけ状態遷移
for i in range(1, B):
A[i * n:(i + 1) * n, (i - 1) * n:i * n] = np.eye(n) # 残りはシフト(1つ前のブロックをコピー)
Q_aug = np.zeros((B * n, B * n))
Q_aug[0:n, 0:n] = Q # プロセスノイズは先頭ブロックにのみ入る
H_aug = np.zeros((H.shape[0], B * n))
H_aug[:, 0:n] = H # 観測は先頭ブロック(x_k)のみ
z0 = np.tile(x0, B)
P0_aug = np.tile(P0, (B, B)) # 初期時刻は全ブロックが x0 に完全相関
z_filt = np.zeros((T + 1, B * n))
Pz_filt = np.zeros((T + 1, B * n, B * n))
z_filt[0], Pz_filt[0] = z0, P0_aug
for k in range(T):
z_pred = A @ z_filt[k]
P_pred_aug = A @ Pz_filt[k] @ A.T + Q_aug
S = H_aug @ P_pred_aug @ H_aug.T + R
K = P_pred_aug @ H_aug.T @ np.linalg.inv(S)
z_filt[k + 1] = z_pred + K @ (y_obs[k] - H_aug @ z_pred)
Pz_filt[k + 1] = (np.eye(B * n) - K @ H_aug) @ P_pred_aug
x_lag = np.zeros((T + 1, n))
for k in range(T + 1):
j0 = max(0, k - N)
depth = k - j0 # 時刻 k における x_{j0} の格納ブロック
x_lag[j0] = z_filt[k, depth * n:(depth + 1) * n]
for i in range(B): # 末尾 N 点は最終フィルタ結果から直接読み出す
x_lag[T - i] = z_filt[T, i * n:(i + 1) * n]
return x_lag
rng_eq = np.random.default_rng(99)
x_true_eq = np.zeros((T + 1, n))
y_obs_eq = np.zeros((T, m))
for k in range(T):
x_true_eq[k + 1] = F @ x_true_eq[k] + rng_eq.multivariate_normal(np.zeros(n), Q)
y_obs_eq[k] = H @ x_true_eq[k + 1] + rng_eq.multivariate_normal(np.zeros(m), R)
for N in [1, 2, 3, 5]:
x_lag_win = fixed_lag_smoother(y_obs_eq, F, H, Q, R, x0, P0, N)
x_lag_aug = fixed_lag_augmented(y_obs_eq, F, H, Q, R, x0, P0, N)
# 起動直後は拡大状態側の初期パディング(全ブロックをx0で埋める近似)が
# 尾を引くため、k >= N+5 で比較する
diff = np.abs(x_lag_win[N + 5:, 0] - x_lag_aug[N + 5:, 0])
print(f"N={N}: 窓付きRTSと拡大状態の最大差 = {diff.max():.3e}, 平均差 = {diff.mean():.3e}")
実行結果:
N=1: 窓付きRTSと拡大状態の最大差 = 4.441e-16, 平均差 = 3.536e-17
N=2: 窓付きRTSと拡大状態の最大差 = 4.441e-16, 平均差 = 5.581e-17
N=3: 窓付きRTSと拡大状態の最大差 = 4.441e-16, 平均差 = 8.235e-17
N=5: 窓付きRTSと拡大状態の最大差 = 8.882e-16, 平均差 = 9.623e-17
最大差は \(10^{-15}\) 台で、倍精度浮動小数点の丸め誤差の範囲内で完全に一致しました。定式化がまったく異なる2つの導出(状態を拡大して標準カルマンフィルタに帰着させる方法と、RTS後ろ向き再帰を窓に限定する方法)が同一の推定量を与えることが数値的に裏付けられます。
モンテカルロ評価(50試行)
単一系列だとサンプリングの揺らぎで順位が入れ替わることがあるため、numpy.random.default_rng で 50 系列を生成して平均 RMSE を比較します。
def rmse(a, b):
return np.sqrt(np.mean((a - b) ** 2))
n_trials = 50
lags = [1, 2, 3, 5, 10, 20]
rmse_kf_list, rmse_rts_list = [], []
rmse_lag_lists = {N: [] for N in lags}
rng = np.random.default_rng(42)
for trial in range(n_trials):
# 真の状態と観測の生成
x_true = np.zeros((T + 1, n))
y_obs = np.zeros((T, m))
for k in range(T):
x_true[k + 1] = F @ x_true[k] + rng.multivariate_normal(np.zeros(n), Q)
y_obs[k] = H @ x_true[k + 1] + rng.multivariate_normal(np.zeros(m), R)
x_filt, P_filt, x_pred, P_pred = kalman_filter(y_obs, F, H, Q, R, x0, P0)
x_rts, P_rts = rts_smoother(x_filt, P_filt, x_pred, P_pred, F)
rmse_kf_list.append(rmse(x_true[1:, 0], x_filt[1:, 0]))
rmse_rts_list.append(rmse(x_true[1:, 0], x_rts[1:, 0]))
for N in lags:
x_lag = fixed_lag_smoother(y_obs, F, H, Q, R, x0, P0, N)
rmse_lag_lists[N].append(rmse(x_true[1:, 0], x_lag[1:, 0]))
rmse_kf = np.mean(rmse_kf_list)
rmse_rts = np.mean(rmse_rts_list)
print(f"KF filter RMSE : {rmse_kf:.4f}")
for N in lags:
r = np.mean(rmse_lag_lists[N])
print(f"Fixed-lag N={N:2d} : {r:.4f} ({(1 - r / rmse_kf) * 100:.1f}% improvement)")
print(f"RTS smoother : {rmse_rts:.4f} ({(1 - rmse_rts / rmse_kf) * 100:.1f}% improvement)")
実行結果
KF filter RMSE : 0.4519
Fixed-lag N= 1 : 0.4145 (8.3% improvement)
Fixed-lag N= 2 : 0.3890 (13.9% improvement)
Fixed-lag N= 3 : 0.3717 (17.7% improvement)
Fixed-lag N= 5 : 0.3521 (22.1% improvement)
Fixed-lag N=10 : 0.3397 (24.8% improvement)
Fixed-lag N=20 : 0.3387 (25.0% improvement)
RTS smoother : 0.3388 (25.0% improvement)
| 手法 | 平均 RMSE | フィルタ比改善率 | 遅延 |
|---|---|---|---|
| カルマンフィルタ | 0.4519 | — | 0 |
| 固定ラグ \(N=1\) | 0.4145 | 8.3% | 1 ステップ |
| 固定ラグ \(N=2\) | 0.3890 | 13.9% | 2 ステップ |
| 固定ラグ \(N=3\) | 0.3717 | 17.7% | 3 ステップ |
| 固定ラグ \(N=5\) | 0.3521 | 22.1% | 5 ステップ |
| 固定ラグ \(N=10\) | 0.3397 | 24.8% | 10 ステップ |
| 固定ラグ \(N=20\) | 0.3387 | 25.0% | 20 ステップ |
| RTS(固定区間) | 0.3388 | 25.0% | 全データ |
期待どおり フィルタ < 固定ラグ < RTS の順で精度が改善し、固定ラグは \(N\) の増加とともに RTS に漸近します(\(N=20\) で差はモンテカルロ誤差の範囲内)。
この系の定常予測分散は \(\bar{P}_{k|k-1} = 0.25\) で、定常スムーザゲインは \(\bar{G} = \bar{P}_{k|k} / \bar{P}_{k+1|k} = 0.8\) です。式 \((14)\) で導出した減衰則 \(\mathrm{MSE}_{\mathrm{lag}}(N) \approx \mathrm{MSE}_{\mathrm{RTS}} + (\mathrm{MSE}_{\mathrm{KF}} - \mathrm{MSE}_{\mathrm{RTS}})\bar{G}^{2N}\) が実測とどれだけ一致するかを直接検証します。
G_bar = 0.8 # 前段で導出した定常スムーザゲイン P_filt_ss/P_pred_ss = 0.2/0.25
mse_kf, mse_rts = rmse_kf**2, rmse_rts**2
print(f"{'N':>3s} | {'measured RMSE':>14s} | {'predicted RMSE':>15s} | {'abs diff':>9s}")
for N in lags:
measured = np.mean(rmse_lag_lists[N])
predicted = np.sqrt(mse_rts + (mse_kf - mse_rts) * G_bar ** (2 * N))
print(f"{N:3d} | {measured:14.4f} | {predicted:15.4f} | {abs(measured-predicted):9.4f}")
実行結果(\(\bar{G}=0.8\) を式 \((14)\) に代入):
N | measured RMSE | predicted RMSE | abs diff
1 | 0.4145 | 0.4147 | 0.0002
2 | 0.3890 | 0.3891 | 0.0001
3 | 0.3717 | 0.3718 | 0.0000
5 | 0.3521 | 0.3526 | 0.0005
10 | 0.3397 | 0.3403 | 0.0006
20 | 0.3387 | 0.3388 | 0.0000
50試行モンテカルロの実測RMSEと、式 \((14)\) から計算した理論予測RMSEの差はすべて \(0.0006\) 以下で、\(\bar{G}^{2N}\) という減衰則がほぼ完全に実測を説明できています。\(N=1\) の予測 \(0.4147\) が実測 \(0.4145\) よりわずかに高いのは、\(\bar\delta\) (定常値)を実際には非定常な区間端点付近のデータも含めた平均で近似しているためで、理論の適用範囲内の誤差です。遅延をどこまで許容できるかでラグ長を決め、\(\bar{G}^{2N}\) が数%を切る \(N\) を上限の目安にするのが実務的な設計指針です。
可視化:フィルタ・固定ラグ・RTSの比較
3手法の推定値そのものの違いと、上で検証した減衰則を1枚の図にまとめます。可視化用に、モンテカルロとは別の1系列(seed=2024)を新たに生成します。
import matplotlib.pyplot as plt
rng_viz = np.random.default_rng(2024)
x_true = np.zeros((T + 1, n))
y_obs = np.zeros((T, m))
for k in range(T):
x_true[k + 1] = F @ x_true[k] + rng_viz.multivariate_normal(np.zeros(n), Q)
y_obs[k] = H @ x_true[k + 1] + rng_viz.multivariate_normal(np.zeros(m), R)
x_filt, P_filt, x_pred, P_pred = kalman_filter(y_obs, F, H, Q, R, x0, P0)
x_rts, P_rts = rts_smoother(x_filt, P_filt, x_pred, P_pred, F)
x_lag5 = fixed_lag_smoother(y_obs, F, H, Q, R, x0, P0, N=5)
fig, axes = plt.subplots(1, 2, figsize=(13, 4.6))
# 左:時系列でのフィルタ・固定ラグ(N=5)・RTSの比較(区間 k=150..220)
ax = axes[0]
window = slice(150, 221)
tt = np.arange(T + 1)
ax.scatter(tt[window], y_obs[149:220, 0], s=12, color="#898781", alpha=0.55, label="Observations")
ax.plot(tt[window], x_true[window, 0], color="#0b0b0b", lw=2.2, label="True state")
ax.plot(tt[window], x_filt[window, 0], color="#2a78d6", lw=1.6, ls="--", label="KF filter")
ax.plot(tt[window], x_lag5[window, 0], color="#eb6834", lw=1.6, ls="-.", label="Fixed-lag (N=5)")
ax.plot(tt[window], x_rts[window, 0], color="#008300", lw=1.8, label="RTS smoother")
ax.set_xlabel("Time step $k$"); ax.set_ylabel("State $x_k$")
ax.set_title("Filtering vs. fixed-lag vs. fixed-interval (RTS) smoothing")
ax.legend(loc="upper left", fontsize=8)
# 右:ラグ長Nに対するRMSEの飽和と式(14)の理論曲線
ax = axes[1]
N_curve = np.linspace(0, 22, 200)
predicted_curve = np.sqrt(mse_rts + (mse_kf - mse_rts) * G_bar ** (2 * N_curve))
ax.axhline(rmse_kf, color="#2a78d6", lw=1.4, ls="--", label="KF filter (N=0)")
ax.axhline(rmse_rts, color="#008300", lw=1.4, label="RTS (N=inf)")
ax.plot(N_curve, predicted_curve, color="#52514e", lw=1.4, ls=":", label="eq.(14) model")
ax.scatter(lags, [np.mean(rmse_lag_lists[N]) for N in lags], color="#eb6834", s=36, label="Fixed-lag (measured)")
ax.set_xlabel("Lag length $N$"); ax.set_ylabel("RMSE (position)")
ax.set_title("Lag-length saturation: measured vs. decay-law model")
ax.legend(loc="upper right", fontsize=7.5)
fig.tight_layout()
fig.savefig("kalman_smoother_comparison.png", dpi=150)

左図から、フィルタ推定(青破線)が観測ノイズに敏感に反応するのに対し、固定ラグ(橙一点鎖線)と RTS(緑実線)は滑らかで真値(黒実線)に近いことが視覚的に確認できます。右図は、実測の RMSE(橙点)が式 \((14)\) の理論曲線(灰色点線)にほぼ乗っており、\(N \gtrsim 10\) でフィルタとRTSの中間から RTS 側へほぼ完全に収束することを示しています。
エッジケース:退化条件でのスムーザの挙動
理論を導出しただけでは見落としがちな2つの退化条件を、実際にコードで確認します。
エッジケース1:ラグ長 \(N \geq T\) は固定区間スムーザ(RTS)に厳密退化する
窓付きRTS実装(実現方法2)は「窓の終端で打ち切った固定区間スムーザ」そのものなので、窓が全データを覆うほど大きければ、固定ラグ推定は RTS の推定と数値的に完全一致するはずです。
x_lag_full = fixed_lag_smoother(y_obs, F, H, Q, R, x0, P0, N=T)
x_lag_over = fixed_lag_smoother(y_obs, F, H, Q, R, x0, P0, N=T + 100)
print(f"max |x_lag(N=T) - x_rts| = {np.max(np.abs(x_lag_full[:, 0] - x_rts[:, 0])):.3e}")
print(f"max |x_lag(N=T+100) - x_rts| = {np.max(np.abs(x_lag_over[:, 0] - x_rts[:, 0])):.3e}")
実行結果:
max |x_lag(N=T) - x_rts| = 0.000e+00
max |x_lag(N=T+100) - x_rts| = 0.000e+00
差は完全にゼロです(j0 = max(0, k - N) により \(N \geq T\)
では常に窓の左端が \(0\)
に張り付き、窓付きRTSの再帰がRTSの後ろ向き再帰と1ステップも省略せず一致するため)。実務上は「ラグ長をデータ長以上に設定しても安全に RTS と同じ結果が返る」ことを意味し、\(N\)
の上限を気にせず大きめに設定しても破綻しないことが保証されます。
エッジケース2:プロセスノイズ \(Q=0\) の極限とスムーザゲインの罠
スムーザゲイン \(G_k = P_{k|k} F^T P_{k+1|k}^{-1}\) に \(Q=0\) (この1ステップだけプロセスノイズがない)を代入すると、\(P_{k+1|k} = F P_{k|k} F^T\) となるため
\[ G_k = P_{k|k} F^T (F P_{k|k} F^T)^{-1} = F^{-1} \]と、\(P_{k|k}\) の値によらず厳密に \(G_k = F^{-1}\) になります(\(F\) が可逆な場合)。\(F=1\) のランダムウォークでは \(G_k=1\) で自明ですが、平均回帰系(例えば \(F=0.9\) の AR(1) モデル)で確認すると非自明な値になります。
F_ar, H_ar, R_ar = 0.9, 1.0, 1.0
P = 1.0 # 恣意的な初期値(定常状態である必要はない)
for k in range(8):
P_pred_val = F_ar**2 * P + 0.0 # Q = 0 を厳密に代入
G_val = P * F_ar / P_pred_val
print(f"k={k}: P_filt={P:.6e}, G_k={G_val:.6f} (1/F = {1/F_ar:.6f})")
S = H_ar**2 * P_pred_val + R_ar
K = P_pred_val * H_ar / S
P = (1 - K * H_ar) * P_pred_val
実行結果:
k=0: P_filt=1.000000e+00, G_k=1.111111 (1/F = 1.111111)
k=1: P_filt=4.475138e-01, G_k=1.111111 (1/F = 1.111111)
k=2: P_filt=2.660476e-01, G_k=1.111111 (1/F = 1.111111)
k=3: P_filt=1.772923e-01, G_k=1.111111 (1/F = 1.111111)
k=4: P_filt=1.255736e-01, G_k=1.111111 (1/F = 1.111111)
k=5: P_filt=9.232390e-02, G_k=1.111111 (1/F = 1.111111)
k=6: P_filt=6.957907e-02, G_k=1.111111 (1/F = 1.111111)
k=7: P_filt=5.335217e-02, G_k=1.111111 (1/F = 1.111111)
\(P_{k|k}\) が単調に縮小していく(観測を重ねるほど不確実性が減る)にもかかわらず、\(G_k\) は \(1/F=1.1111\ldots\) に完全に固定されたままです。これは式の分子・分母に同じ \(P_{k|k}\) が現れて厳密に約分されるためで、近似ではなく恒等式です。
ここで重要な注意点があります。「\(Q \to 0\) の極限を取る」という操作を、単一ステップで \(Q=0\) を代入するのではなく、**定常状態(すべての \(k\) で \(Q\) が同じ小さな値を取り続けた場合の固定点)**で行うと、結果はまったく異なります。\(|F|<1\) の安定系では、\(Q \to 0\) につれて定常共分散 \(\bar{P}_{k|k}\) 自身も \(Q\) に比例して \(0\) へ縮小するため(\(\bar{P}_{k|k} \approx Q/(1-F^2)\) 、AR(1) 過程の定常分散の標準的な近似式)、比 \(G_k = \bar{P}_{k|k} F/(F^2\bar{P}_{k|k}+Q)\) は \(0/0\) の不定形になり、\(1/F\) ではなく \(F\) 自身に収束します。
for Qval in [1e-1, 1e-3, 1e-6, 1e-9, 1e-12]:
P = 1.0
for _ in range(2000): # 定常状態まで反復
P_pred_val = F_ar**2 * P + Qval
S = H_ar**2 * P_pred_val + R_ar
K = P_pred_val * H_ar / S
P = (1 - K * H_ar) * P_pred_val
P_pred_val = F_ar**2 * P + Qval
G_val = P * F_ar / P_pred_val
print(f"Q={Qval:.0e}: steady-state P_filt={P:.3e}, G_bar={G_val:.8f}")
実行結果:
Q=1e-01: steady-state P_filt=2.153e-01, G_bar=0.70620719
Q=1e-03: steady-state P_filt=5.124e-03, G_bar=0.89538818
Q=1e-06: steady-state P_filt=5.263e-06, G_bar=0.89999526
Q=1e-09: steady-state P_filt=5.263e-09, G_bar=0.90000000
Q=1e-12: steady-state P_filt=5.263e-12, G_bar=0.90000000
\(Q\) を小さくするほど \(G_{\mathrm{bar}}\) は \(F=0.9\) に収束し、単一ステップで \(Q=0\) を代入したときの \(1/F=1.111\ldots\) とは異なる値に落ち着きます。「\(Q=0\) を代入する」ことと「\(Q\to0\) の定常極限を取る」ことは可換ではないという、極限操作の順序に関する典型的な落とし穴です。実務でプロセスノイズを小さく見積もりすぎた設定でスムーザゲインの直感的な挙動を予測する際は、この非可換性に注意する必要があります。
近年の研究動向:固定点スムーザの数値的安定化
固定区間スムーザ(RTS)の研究が2020年代も活発なのに対し、固定点スムーザは長らく実装上の注意点(拡大状態への埋め込みによる計算量増大、あるいは本記事で導出した漸化式 \((17)\) 〜\((18)\) を素朴に実装した際の数値不安定性)があまり整理されないまま使われてきました。この隙間を埋める研究として、Krämer (2024/2025) の “Numerically Robust Fixed-Point Smoothing Without State Augmentation” は、状態拡大を一切使わずに固定点スムーザをコレスキー分解ベースで実装する定式化を提案し、ダウンデート(コレスキー因子からの減算的な更新で数値誤差を増幅しやすい操作)を避けながら、実行速度と数値的頑健性を両立できることを示しました。従来手法は速度か頑健性のどちらかを犠牲にせざるを得なかった、と著者は指摘しています。初期状態が未知なシステムの推定という応用上重要な場面で、固定点スムーザは固定区間スムーザに比べて研究の蓄積が薄いままだったため、この研究は実務上のギャップを埋めるものです。
発展:非線形・非ガウスへの拡張
本記事は線形ガウス系に限定しましたが、同じ 3 分類は非線形・非ガウス系にもそのまま持ち上がります。EKF/UKF ベースのスムーザに加え、 粒子フィルタ の重み付きサンプルを後向きに再重み付けする粒子スムーザ(forward-filtering backward-smoothing、\(O(T M^2)\) 、\(M\) は粒子数)を使えば、多峰性のある事後分布の平滑化も可能です。さらに近年は、状態遷移・観測モデル自体をニューラルネットで学習する Deep State Space Model(Deep Kalman Filter、S4/Mamba 系列など)が発展しており、変分推論で近似した事後分布に対して同様のフィルタリング/スムージングの区別が現れます。古典スムーザの遅延・計算量の設計感覚は、これらの現代的手法を使う際にもそのまま役立ちます。
まとめ
- スムージングは未来の観測を使う非因果的な推定で、フィルタリングより常に同等以上の精度が得られる代わりに遅延が生じる
- RTS(固定区間) は全時刻を最高精度で推定するオフラインの標準解、固定ラグ は遅延 \(N\) と引き換えに準リアルタイムでスムージング精度を得る折衷案、固定点 は特定時刻だけを逐次精密化する軽量な方式
- 固定ラグの2実装(拡大状態カルマンフィルタ/窓付きRTS)が数値的に完全一致すること(差 \(10^{-15}\) 台)、固定点スムーザの更新式がRTS再帰のテレスコーピングから厳密に導出できることを示した
- ランダムウォーク系の 50 試行モンテカルロで、フィルタ RMSE 0.4519 → 固定ラグ \(N=5\) で 0.3521 → RTS で 0.3388 と単調に改善することを確認した
- ラグ長 \(N\) の効果は定常スムーザゲイン \(\bar{G}\) の2乗 \(\bar{G}^{2N}\) で指数的に飽和することを導出・実測の両面で確認した(実測と理論予測RMSEの差は最大 \(0.0006\) )
- \(Q=0\) を1ステップに代入すると \(G_k=F^{-1}\) が厳密に成り立つ一方、\(Q\to0\) の定常極限では \(G_{\mathrm{bar}}\to F\) に収束するという非可換性に注意が必要(実測: \(F=0.9\) の系で \(1/F=1.111\) 対 \(F=0.9\) )
おすすめ書籍
カルマンフィルタの導出から実装例までを日本語で丁寧に解説した定番書です。状態空間モデルの理解が深まります。
※ 上記は Amazon アソシエイトのリンクです。
関連記事
- カルマンスムーザ(RTS Smoother)の理論・前向き後向き再帰・Python実装 - 固定区間スムーザ(RTS)の導出と実装を詳説しています。本記事の前提となる記事です。
- カルマンフィルタの理論とPython実装 - 全スムーザの前向きパスとなるカルマンフィルタの基礎です。
- 拡張カルマンフィルタ(EKF)の理論とPython実装 - 非線形系への第一歩。EKF ベースの非線形スムーザの基盤です。
- Unscented Kalman Filter(UKF)の理論とPython実装 - シグマポイントによる非線形推定。UKF スムーザへ拡張できます。
- 粒子フィルタのPython実装:リサンプリング手法の比較 - 非ガウス系の粒子スムーザの基盤となる粒子フィルタを解説しています。
- 非線形カルマンスムーザ(EKS/URTS) - 本記事末尾で触れたEKF/UKFベースのスムーザを実装し、強非線形ベンチマークで精度を比較した発展編です。
参考文献
- Rauch, H. E., Tung, F., & Striebel, C. T. (1965). “Maximum likelihood estimates of linear dynamic systems.” AIAA Journal, 3(8), 1445-1450.
- Bryson, A. E., & Frazier, M. (1963). “Smoothing for linear and nonlinear dynamic systems.” Proceedings of the Optimum System Synthesis Conference, 353-364.
- Biswas, K. K., & Mahalanabis, A. K. (1972). “An approach to fixed-lag smoothing problems.” IEEE Transactions on Aerospace and Electronic Systems, AES-8(5), 676-682.
- Meditch, J. S. (1969). Stochastic Optimal Linear Estimation and Control. McGraw-Hill.
- Anderson, B. D. O., & Moore, J. B. (1979). Optimal Filtering. Prentice-Hall.
- Särkkä, S. (2013). Bayesian Filtering and Smoothing. Cambridge University Press.
- Krämer, N. (2024/2025). “Numerically Robust Fixed-Point Smoothing Without State Augmentation.” arXiv:2409.20004.