$ \newcommand\bm[1]{\boldsymbol{#1}} \newcommand\ve{\varepsilon} \newcommand\vecseq[3]{{#1}_{#2}, \ldots, {#1}_{#3}} \newcommand\alg[1]{\color{blue}{\mathrm{#1}}} $
 

MATLABで『カルマンフィルタ』2章:ローカルレベルモデル

理論の説明ではなく, 実装に焦点をあてた記事.


非専門家が書いています. 十分に批判的に読んで頂くようお願いいたします. 間違い・疑問点などあれば, ぜひコンタクトフォームへ連絡いただけると幸いです.

モデル

シンプルな単変量時系列を扱うモデル. 観測量を $y_1, \ldots, y_n$ とする. そして, 観測量は状態量 $\alpha_t$ にホワイトノイズ $\ve_t$ がのったもの, 状態量の更新も同様にホワイトノイズ $\eta_t$ が乗るとする. するとモデルは,

\begin{align} \large y_t = \alpha_t + \ve_t, \quad \ve_t \sim \mathcal{N}(0, \sigma^2_\ve), \tag{Observation} \\[8pt] \large \alpha_{t+1} = \alpha_t + \eta_t, \quad \eta_t \sim \mathcal{N}(0, \sigma^2_\eta) \tag{System} \end{align}

となる. ここで, 初期状態 $\alpha_t$ についての情報はないが, とりあえず平均 $a_1$, 分散 $P_1$ に従うとする.

\begin{align} \large \alpha_1 \sim \mathcal{N}(a_1, P_1) \end{align}

フィルタリング

アルゴリズムを載せつつ, MATLABでコーディングしてみる. 対象データは有名なナイル川流量データ. Rに入ってるし, Kaggleとかからも取得可能. 便宜のため, 正規化した.

name
MATLABコード MATLAB
y = nile;
y = normalize(y);
plot(y)
xlabel('year'); ylabel('Normalized discharge')

アルゴリズム

アルゴリズムは以下のようになる. なお, $X_{\color{#cf201f}{t+1}|\color{blue}{t}}$ のような記述は, $\color{blue}{t}$ 期までの観測に基づく, $\color{#cf201f}{t+1}$ 期の $X$ の値の推定値という意味である.

\begin{align} &\large v_t = y_t - a_{t|t-1}, &\large\quad&\large F_{t } = P_{t|t-1} + \sigma^2_\ve \tag{Innovation} \\[8pt] &\large a_{t|t} = a_{t|t-1} + K_t v_t, &\large\quad&\large P_{t|t} = P_{t|t-1} L_t \notag \tag{current state} \\[8pt] &\large a_{t+1|t} = a_{t|t}, &\large\quad&\large P_{t+1|t} = P_{t|t} + \sigma^2_\eta \tag{next state} \\[8pt] \end{align}

ここで,

\begin{align} \large K_t = \frac{P_{t|t-1}}{F_{t }}, \tag{Kalman gain} \\[8pt] \large \quad L_t = 1 - K_t \notag \end{align}

である. また,

\begin{align} \large a_{1|0} = a_1,\quad P_{1|0} = P_1 \end{align}

と与えているので, 上のアルゴリズムは計算できる.

実装

実装するには, パラメータ $\sigma^2_\ve$, $\sigma^2_\eta$, $a_1$, $P_1$ を与えなければならないけれど, 今回はまず適当に与える. 以下のコードで, アルゴリズムとできるだけ文字と統一させた. 特に, $X_{t|t}$ のようなパラメータは, X_ttのように, $X_{t|t-1}$ のようなパラメータは, X_tt1のように記した.

MATLAB
y = nile;
y = normalize(y);
L = length(y);

% 適当に与える
varEps = 1;
varEta = 1;
a1 = 10;
P1 = 10;

% Preallocation
a_tt1 = zeros(size(y)); a_tt1(1) = a1;
P_tt1 = zeros(size(y)); P_tt1(1) = P1;
v_t   = zeros(size(y)); 
F_t   = zeros(size(y)); 
a_tt  = zeros(size(y)); 
P_tt  = zeros(size(y));
K_t   = zeros(size(y));

for t = 1:L
    
    % Innovation
    v_t(t) = y(t) - a_tt1(t);
    F_t(t) = P_tt1(t) + varEps;
    
    % Kalman gain
    K_t(t) = P_tt1(t)/F_t(t);

    % Current state
    a_tt(t) = a_tt1(t) + K_t(t)*v_t(t);
    P_tt(t) = P_tt1(t) * (1 - K_t(t));

    % Next state
    a_tt1(t+1) = a_tt(t);
    P_tt1(t+1) = P_tt(t) + varEta;
    
end

これで, フィルタリングが終わったので, 推定された状態($a_{t|t}$)と, その95%区間($a_{t|t} \pm 1.96 \sqrt{P_{t|t}}$)をみてみよう.

name
MATLABコード MATLAB
plot(y(2:end), ':ko', MarkerSize=2); hold on
plot(a_tt(2:end), 'r')
plot(a_tt(2:end)+1.96*sqrt(P_tt(2:end)), 'b')
plot(a_tt(2:end)-1.96*sqrt(P_tt(2:end)), 'b')
legend('観測値', 'フィルタ値', '95%区間', box='off', Numcolumns=2)
xlabel('year'); ylabel('Normalized discharge')
ylim([-4, 6])

いい感じ. ただし, 注意としては, いい感じなのはパラメータをそれっぽく選んだからである. しかもよく見ると, データ点は100あるのに, 一つも95%区間から逸脱していないなど, よくない点もある. 次の節では, しっかりとパラメータを最尤推定する方法について述べる.

初期化とパラメータ推定

前節では, カルマンフィルタが無事計算できたものの, パラメータについては先験的にそれっぽい値を与えていた. 本節では, これらを推定する方法を実装する. 推定すべきパラメータは, $\sigma^2_\ve$, $\sigma^2_\eta$, $a_1$, $P_1$ である.

散漫なカルマンフィルタ

初期状態 $a_1$, $P_1$ を決めるに当たり, 何も情報がないと仮定し, $P_1 \rightarrow \infty$ と仮定するのは自然である. その場合, 結果的に

\begin{align} \large a_{1|1} = y_1, \tag{diffuse init.} \\[8pt] \large P_{1|1} = \sigma^2_\ve \notag \end{align}

となり, $a_1$, $P_1$ を決める必要がなくなる.

証明(click)

上の, Innovation式より,

\begin{align} v_1 = y_1 - a_1, \quad F_{1|0} = P_1 + \sigma^2_\ve. \end{align}

よって, $t = 1c$ におけるcurrent state式にこれらを代入することで,

\begin{align} a_{1|1} = a_1 + \frac{P_1}{P_1 + \sigma^2_\ve}(y_1 - a_1) \rightarrow y_1 \quad (P_1 \rightarrow \infty), \\[5pt] P_{1|1} = P_1 \left( 1 - \frac{P_1}{P_1 + \sigma^2_\ve} \right) \rightarrow \sigma^2_\ve \quad (P_1 \rightarrow \infty). \notag \end{align}

$\square$

モデルの尤度

最尤推定を行ってパラメータを決めるために, モデルの尤度を求めよう.

初期状態 $a_1$, $P_1$ を与える場合

モデルの対数尤度 $\ell$ は,

\begin{align} \large \ell = -\frac{n}{2}\log{(2\pi)} - \frac{1}{2} \sum_{t=1}^n \left( \log{F_{t }} + \frac{v_t^2}{F_{t }} \right) \end{align}

で与えられる.

散漫な初期化を行う場合

発散を防ぐことを考慮した以下のような散漫対数尤度 $\ell_d$ が定義される.

\begin{align} \large \ell_d &\large = \lim_{P_1 \rightarrow \infty} \left( \ell + \frac{1}{2} \log{P_1}\right) \\[8pt] &\large = -\frac{n}{2}\log{(2\pi)} - \frac{1}{2} \sum_{t=2}^n \left( \log{F_{t }} + \frac{v_t^2}{F_{t }} \right). \end{align}

実装

散漫なカルマンフィルタの最尤推定を行う. MATLABのOptimaizing Toolboxの, 準ニュートン法(BFGS公式)を用いて, $\sigma^2_\eta$, $\sigma^2_\ve$ に関する散漫な対数尤度を最大化する. ただ, 教科書に書かれている通り, 分散は正数しか取れないので, 以下の変換を行い探索範囲を実数値とできるようにした.

\begin{align} \large \psi_\eta = \log{\sigma_\eta},\quad \psi_\ve = \log{\sigma_\ve}. \end{align}

まず, メインのコード.

MATLAB
y = nile;
y = normalize(y);

varEta = 1; % σ^2_η の初期値(適当にきめた)
varEps = 1; % σ^2_ε の初期値(適当にきめた)

psiEta = log(sqrt(varEta)); % ψ_η に変換
psiEps = log(sqrt(varEps)); % ψ_ε に変換

% minimize loglilelihood
options = optimoptions(@fminunc,'Display','iter','Algorithm','quasi-newton'); % 準ニュートン法の設定
x0 = [psiEta, psiEps];              % 探索するパラメータの初期値
f = @(x) -calcLogDiffuseLlhd(y, x); % 最小化したい函数(散漫な対数尤度の最大化なので負号をつける)
xopt = fminunc(f, x0, options);     % 実行

varEtaOpt = exp(2*xopt(1))          % 推定されたψ_ηをσ^2_ηに戻す
varEpsOpt = exp(2*xopt(2))          % 推定されたψ_εをσ^2_εに戻す

以下, メインコードの中で使われた函数.

長いので閉じます(click)

ローカルトレンドモデルの散漫な対数尤度を求める函数.

MATLAB
function logLd = calcLogDiffuseLlhd(y, vars)
% 
% ローカルトレンドモデルの散漫な対数尤度を求める函数.
% y   : データ
% vars: 尤度に関わるパラメータ(ψ_ε, ψ_η)

psiEta = vars(1); varEta = exp(2*psiEta); % σ^2_η に戻す
psiEps = vars(2); varEps = exp(2*psiEps); % σ^2_ε に戻す
L = length(y);

% a_1, P_1の初期値 式(dissufe init.)参照のこと
a1 = y(1); 
P1 = varEps;

% カルマンフィルタリング
[~, ~, F_t, v_t] = localTrendKF(y, a1, P1, varEta, varEps); 

% 散漫対数尤度を計算
tmp = sum(log(F_t(2:end)) + v_t(2:end).^2 ./ F_t(2:end));
logLd = -0.5*L*log(2*pi) - 0.5 * tmp;

end

ローカルトレンドモデルのカルマンフィルタを行う函数.

MATLAB
function [a_tt, P_tt, F_t, v_t] = localTrendKF(y, a1, P1, varEta, varEps)
%
% ローカルトレンドモデルのカルマンフィルタリングを行う函数

L = length(y);

% Preallocation
a_tt1 = zeros(size(y)); a_tt1(1) = a1;
P_tt1 = zeros(size(y)); P_tt1(1) = P1;
v_t   = zeros(size(y)); 
F_t   = zeros(size(y)); 
a_tt  = zeros(size(y)); 
P_tt  = zeros(size(y));
K_t   = zeros(size(y));

% Filtering
for t = 1:L
    
    % Innovation
    v_t(t) = y(t) - a_tt1(t);
    F_t(t) = P_tt1(t) + varEps;
    
    % Kalman gain
    K_t(t) = P_tt1(t)/F_t(t);

    % Current state
    a_tt(t) = a_tt1(t) + K_t(t)*v_t(t);
    P_tt(t) = P_tt1(t) * (1 - K_t(t));

    % Next state
    a_tt1(t+1) = a_tt(t);
    P_tt1(t+1) = P_tt(t) + varEta;
    
end
end

計算結果, $\sigma^2_\eta = 0.0494$, $\sigma^2_\ve = 0.5307$ となった. これらを, パラメータとしたカルマンフィルタを行い, 結果を見てみる.

name
MATLABコード ( click ) MATLAB
varEps = varEtaOpt;
varEta = varEpsOpt;

a1 = varEps;
P1 = varEta;

[a_tt, P_tt, ~, ~] = localTrendKF(y, a1, P1, varEta, varEps);

plot(y(2:end), ':ko', MarkerSize=2); hold on
plot(a_tt(2:end), 'r')
plot(a_tt(2:end)+1.96*sqrt(P_tt(2:end)), 'b')
plot(a_tt(2:end)-1.96*sqrt(P_tt(2:end)), 'b')
legend('観測値', 'フィルタ値', '95%区間', box='off', Numcolumns=2)
xlabel('year'); ylabel('Normalized discharge')
ylim([-4, 6])

どうもきれいすぎる(フィルタリングがデータを追いすぎている)気がする. じきに, 再確認します.

主な参考文献


野村 カルマンフィルタ ―Rを使った時系列予測と状態空間モデル― (統計学One Point 2)

北川 Rによる 時系列モデリング入門

萩原ら 基礎からわかる時系列分析―Rで実践するカルマンフィルタ・MCMC・粒子フィルタ― Data Science Library

この記事のTOP    BACK    TOP