はじめに
状態空間モデルにおいて状態を逐次的に推定する有名な手法の1つにカルマンフィルタというものがあります。
カルマンフィルタなどを用いて出来る事・概要・手順・numpyを用いた行列実装をまとめてみました。
この記事の対象としている人
- 確率分布の計算はなんとなくわかる
- 状態空間の概念はなんとなくわかる
- カルマンフィルタはよく知らない
- numpyの行列実装の仕方を知りたい
カルマンフィルタで出来る事
カルマンフィルタを使うと、ノイズ混じりの観測データから、観測ノイズを取り除いた状態を逐次推定できます。
こんな感じです。
経済やマーケティングの文脈においては、時系列データの平滑化やトレンドの把握、少し先の予測などに使われます。
ある事象に対して、時点1から時点tまで時系列に沿って観測したデータ
例えば、DAU(Daily Active User, 1日あたりにログインしたユーザー数)を想定します。
DAUが以下のように分解出来るとします。
DAUの監視を通じて知りたいのは、時点tにおけるサービスの魅力度を表す状態
そこで、フィルタリングや予測、平滑化を行います。
フィルタリング、予測、平滑化の違い
時点1から時点tまでの観測データ
より正確には、観測データが手元にあるときの時点tの状態
ここで、
- t' < t, つまり時点tまでの観測値を元に過去の時点の状態
を推定する場合、平滑化 - t' = t, つまり時点tまでの観測値を元に現在の時点の状態
を推定する場合、フィルタリング - t' > t, つまり時点tまでの観測値を元に未来の時点の状態
を推定する場合、予測
という違いがあります。
例えば、過去のトレンドを知りたいときは平滑化を行ってもよいですし、現在のトレンドをリアルタイムに知りたければ、最新時点のデータが入ってくる度にフィルタリングを行えばよいでしょう。
カルマンフィルタについて
カルマンフィルタ、カルマン予測、カルマン平滑化の順に説明します。
前提
まず、以下のような状態空間モデルを想定します
状態
状態が遷移する度に、正規分布に従うノイズ
時点tの観測値
観測値
カルマンフィルタの概要
カルマンフィルタは、時点tまでの観測値
観測値から観測ノイズを取り除いた状態を推定するので、フィルタリングと呼ばれます。
時点tまでの観測値
具体的には、時点毎のフィルタリング分布を仮定し、そのパラメータを逐次的に求めたいです。
そのために、以下の3つの分布を設定します。
フィルタリング分布は時点tまでの観測値
一期先予測分布は時点t-1までの観測値
一期先予測尤度は時点t-1までの観測値
一期先予測分布
フィルタリング分布と一期先予測分布はどちらも
最後の関数が尤度と名付いているのは、恐らくこの関数が観測された
確率"分布"(確率密度関数)は固定したパラメータからある値が発生する確率を求めるのに対し、"尤度"(尤度関数)は観測されたデータ(固定された値)を発生させたパラメータがある値だとどれくらい尤もらしいかを返す関数です。
時点tにおけるパラメータ
カルマンフィルタの手順
過去の観測値
1期先予測分布
↓
データ
↓
この計算を観測したい時点まで繰り返します。
初期状態のフィルタリング分布のパラメータ
以下の計算をT回繰り返します。
1 時点tの一期先予測分布
2 時点tの一期先予測尤度
3 時点tのカルマンゲイン
4 時点tの状態(
# コードにするとこんな感じ
def kalman_filter(m, C, y, G=G, F=F, W=W, V=V):
"""
Kalman Filter
m: 時点t-1のフィルタリング分布の平均
C: 時点t-1のフィルタリング分布の分散共分散行列
y: 時点tの観測値
"""
a = G @ m
R = G @ C @ G.T + W
f = F @ a
Q = F @ R @ F.T + V
# 逆行列と何かの積を取る場合は、invよりsolveを使った方がいいらしい
K = (np.linalg.solve(Q.T, F @ R.T)).T
# K = R @ F.T @ np.linalg.inv(Q)
m = a + K @ (y - f)
C = R - K @ F @ R
return m, C
カルマン予測の概要・手順
カルマン予測は、t+k時点予測分布
基本的な概念はカルマンフィルタと変わりません。
カルマンフィルタは
- 一期先予測分布
を計算 - データ
を元にフィルタリング分布 へ修正
という流れを辿っていましたが、未来のデータは観測出来ません。そこで
- 最新時点tのフィルタリング分布
を準備、時点tにおける0期先予測分布とする を用いてt+1時点の一期先予測分布 を計算 を用いてt+2時点の一期先予測分布 を計算
...
というように、観測データによる修正をせずひたすら予測分布の計算を繰り返します。カルマンフィルタと同じ計算式です。
時点tでの0期先予測分布
# 一期先予測分布を連続して求めるだけ
def kalman_prediction(a, R, G=G, W=W):
"""
Kalman prediction
"""
a = G @ a
R = G @ R @ G.T + W
return a, R
カルマン平滑化の概要・手順
カルマン平滑化は、時点tの平滑化分布
を求める手法です。(t < T)
カルマンフィルタを時点Tまで計算したとします。
カルマン平滑化は、時点tのフィルタリング分布
これにより、トレンドはより滑らかになります。
手順(RTSアルゴリズム)
0 時点t+1での平滑化分布:
1 時点tの平均化利得を計算
2 時点tの平滑化分布のパラメータを計算
# カルマン平滑化
# 固定区間平滑化を行う
# 時点Tまでのカルマンフィルタリングが一旦完了しているものとする
# aとRの計算は、カルマンフィルタリングの計算時に格納したものを使った方が、計算効率は良さそう
def kalman_smoothing(s, S, m, C, G=G, W=W):
"""
Kalman smoothing
"""
# 1時点先予測分布のパラメータ計算
a = G @ m
R = G @ C @ G.T + W
# 平滑化利得の計算
# solveを使った方が約30%速くなる
A = np.linalg.solve(R.T, G @ C.T).T
# A = C @ G.T @ np.linalg.inv(R)
# 状態の更新
s = m + A @ (s - a)
S = C + A @ (S - R) @ A.T
return s, S
実装
自作関数の定義だけ上に書いたものを使います。
ライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm_notebook as tqdm
np.random.seed(1234)
pd.set_option('display.max_columns', None)
sns.set_style('darkgrid')
%matplotlib inline
仮想データ
一番簡単な例として、ランダムウォークを想定します。
これは、ローカルモデルの一種です。
観測時点Tは100, 予測時点は5, 初期状態は100とします
G = np.array([[1]])
F = np.array([[1]])
W = np.array([[1]]) # 恣意的に与える必要がある
V = np.array([[10]]) # 上に同じ
T = 100
K = 5
x0 = 100
w = np.random.multivariate_normal(np.zeros(1), W, T+K)
v = np.random.multivariate_normal(np.zeros(1), V, T+K)
x = np.zeros(T+K)
y = np.zeros(T+K)
x[0] = x0 + w[0]
y[0] = x[0] + v[0]
for t in range(1, T+K):
x[t] = x[t-1] + w[t]
y[t] = x[t] + v[t]
fig, ax = plt.subplots(figsize=(16, 4))
sns.lineplot(np.arange(T+K), x, ax=ax, label=" true state")
sns.lineplot(np.arange(T+K), y, color="gray", ax=ax, label="observation")
ax.set_title("simulation data")
ax.legend()
plt.show()
パラメータ設定
# 初期状態のフィルタリング分布のパラメータ
m0 = np.array([[0]])
C0 = np.array([[1e7]])
# 結果を格納するarray
m = np.zeros((T, 1))
C = np.zeros((T, 1, 1))
a_pred = np.zeros((K, 1))
R_pred = np.zeros((K, 1, 1))
s = np.zeros((T, 1))
S = np.zeros((T, 1, 1))
実行
# カルマンフィルター
for t in range(T):
if t == 0:
m[t], C[t] = kalman_filter(m0, C0, y[t:t+1])
else:
m[t], C[t] = kalman_filter(m[t-1:t], C[t-1:t], y[t:t+1])
# カルマン予測
for t in range(K):
if t == 0:
a = G @ m[T-1:T]
R = G @ C[T-1:T] @ G.T + W
a_pred[t] = a
R_pred[t] = R
else:
a_pred[t], R_pred[t] = kalman_prediction(a_pred[t-1], R_pred[t-1])
# カルマン平滑化
for t in range(T):
t = T - t - 1
if t == T - 1:
s[t] = m[t]
S[t] = C[t]
else:
s[t], S[t] = kalman_smoothing(s[t+1], S[t+1], m[t], C[t])
結果
カルマンフィルタリング(t<=100)とカルマン予測(t>100)の結果をプロットします。
upper = 115
lower = 85
legend_loc = "lower left"
fig, axes = plt.subplots(nrows=3, figsize=(16, 12))
sns.lineplot(np.arange(T+K), x, ax=axes[0], label="true state")
sns.lineplot(np.arange(T+K), y, color="gray", ax=axes[0], label="observation")
sns.lineplot(np.arange(T), m.flatten(), color="red", ax=axes[0], label="kalman filter + prediction")
axes[0].plot(np.arange(T), (m - 1.96 * C[:,:,0]**(1/2)).flatten(), alpha=0.3, color='gray', label=".95 interval")
axes[0].plot(np.arange(T), (m + 1.96 * C[:,:,0]**(1/2)).flatten(), alpha=0.3, color='gray')
axes[0].plot(np.arange(T, T+K), a_pred.flatten(), color='red')
axes[0].plot(np.arange(T, T+K), (a_pred - 1.96 * R_pred[:,:,0]**(1/2)).flatten(), alpha=0.3, color='gray')
axes[0].plot(np.arange(T, T+K), (a_pred + 1.96 * R_pred[:,:,0]**(1/2)).flatten(), alpha=0.3, color='gray')
axes[0].axvline(100, color="black", linestyle="--", alpha=0.5, label="left: filtering, right: prediction")
axes[0].set_ylim(lower, upper)
axes[0].legend(loc=legend_loc)
axes[0].set_title("Kalman Filter + Prediction")
sns.lineplot(np.arange(T+K), x, ax=axes[1], label="true state")
sns.lineplot(np.arange(T+K), y, color="gray", ax=axes[1], label="observation")
sns.lineplot(np.arange(T), s.flatten(), color="green", ax=axes[1], label="kalman smoothing")
axes[1].plot(np.arange(T), (s - 1.96 * S[:,:,0]**(1/2)).flatten(), alpha=0.3, color='gray', label=".95 interval")
axes[1].plot(np.arange(T), (s + 1.96 * S[:,:,0]**(1/2)).flatten(), alpha=0.3, color='gray')
axes[1].axvline(100, color="black", linestyle="--", alpha=0.5, )
axes[1].set_ylim(lower, upper)
axes[1].legend(loc=legend_loc)
axes[1].set_title("Kalman Smoothing")
# sns.lineplot(np.arange(T+K), x, ax=axes[2], label="true state")
sns.lineplot(np.arange(T+K), y, color="gray", ax=axes[2], label="observation")
sns.lineplot(np.arange(T), m.flatten(), color="red", ax=axes[2], label="kalman filter")
sns.lineplot(np.arange(T), s.flatten(), color="green", ax=axes[2], label="kalman smoothing")
axes[2].axvline(100, color="black", linestyle="--", alpha=0.5)
axes[2].set_ylim(lower, upper)
axes[2].legend(loc=legend_loc)
axes[2].set_title("Kalman filter vs kalman smoothing")
plt.show()
print("カルマンフィルタリングの分散の平均: {:.3f}".format(C.mean()))
print("カルマン平滑化の分散の平均: {:.3f}".format(S.mean()))
青が真の状態、グレーが観測値、赤がカルマンフィルタ、緑がカルマン平滑化です。 カルマンフィルタ(赤)やカルマン平滑化(緑)と真の状態(青)がそれなりに近しい動き方をしていることがわかります。
観測誤差をある程度取り除いて状態を推定できました。
カルマンフィルタと比べ、カルマン平滑化の方が分散が小さくなっています。
カルマン予測は時点を追うごとに分散が大きくなっています。
過去と現在の情報から状態を推測するカルマンフィルタに比べ、カルマンフィルタの結果を1期先の平滑化分布で補正するカルマン平滑化の方が、よりなだらかです。
いかがでしたでしょうか
まとめ
- カルマンフィルタは観測値
から状態 を求める手法 - 観測誤差のノイズを取り除いて、状態を推定することができる
- pythonで実装してみた
TO DO
もっと複雑なモデルに対する適用



Comments
Let's comment your feelings that are more than good