ここまでのシリーズで、「操作で動いた分を異常としない」を、残差(予測モデル)・状態遷移(FSM)・適合性(参照との距離)という3つの実装で見てきました。最後に扱うのは、条件付き分布 $p(x\mid c)$ を深層生成モデルで厳密にモデル化するもっとも原理的なアプローチ——条件付き正規化フローです。

親サーベイ記事で定式化したとおり、私たちが学習したいのは無条件の $p(x)$ ではなく、運用モードや制御入力という条件 $c$ のもとでの $p(x\mid c)$ でした。異常スコアはその条件付き尤度の低さ、$A = -\log p(x\mid c)$ です。

オートエンコーダの再構成誤差やVAEのELBO(尤度の下界)と違い、正規化フローは対数尤度そのものを厳密に計算できます。だから $-\log p(x\mid c)$ をそのまま、理論的に意味のある異常スコアとして使えます。本記事は、多変量時系列の異常検知に条件付き正規化フローを用いる手法(Guan ら “Conditional normalizing flow for multivariate time series anomaly detection,” ISA Transactions, vol. 143, 2023 の提案手法 AFNF を念頭に置きます)を、理論・原論文のアーキテクチャ・PyTorch実装の3面から掘り下げます。
この手法が効く場面は明確です。
- 運用モードが切り替わる多変量プロセス: 各モードの正常密度を1つのモデルで条件付きに表現
- 外生入力で動くシステム: 制御入力・気温・負荷を条件にして「説明できる変動」を吸収
- 厳密な尤度が欲しい監視: スコアの確率的な解釈・閾値設定が要る系
この記事の内容
- 正規化フローの復習 — 可逆変換と変数変換公式による厳密な尤度
- 条件付きアフィンカップリング層 — 条件 $c$ をどう注入するか
- なぜフローか — オートエンコーダ・VAEとの違い(厳密尤度)
- 多変量時系列への適用 — 過去窓・外生入力を条件に
- PyTorch実装 — 条件付きRealNVPを実装し、無条件フローが別モードの高密度に騙される一方、条件付きが逸脱を捉える様子を密度ヒートマップで可視化・実測
前提知識


1. 正規化フローの復習 — 厳密な尤度
正規化フローの中心アイデアは、「複雑なデータ分布 $x$ を、可逆な変換 $f$ で単純な分布(標準正規分布)$z$ へ写す」ことです。

$z = f(x)$ が可逆なら、変数変換公式によってデータの対数尤度を厳密に書けます。
$$ \begin{equation} \log p(x) = \log p_Z\!\big(f(x)\big) + \log\left|\det \frac{\partial f}{\partial x}\right| \end{equation} $$
右辺第1項は「変換後の点 $z=f(x)$ が標準正規分布のもとでどれだけありふれているか」、第2項は変換のヤコビアン行列式(局所的な体積の伸縮)です。$f$ を可逆かつヤコビアンが計算しやすいニューラルネットで構成すれば、対数尤度を直接最大化して学習できます。
第2項の幾何的な意味を、データ空間の微小領域がどう写るかで見ておきます。

確率は保存されます。データ空間の微小領域 $dx$ にある確率質量 $p_X(x)\,|dx|$ は、変換で写った先の領域 $dz$ にある質量 $p_Z(z)\,|dz|$ と等しくなければなりません。両者の体積比 $|dz|/|dx|$ がまさにヤコビアン行列式の絶対値 $|\det\,\partial f/\partial x|$ です。$f$ が局所的に空間を引き伸ばす(体積を増やす)場所では、同じ確率質量が薄く広がるので密度は下がり、その分を対数尤度の第2項が補正します。つまり変数変換公式は「変換後の点のありふれ具合」と「変換が空間をどれだけ伸ばしたか」の2項で、データの密度を過不足なく書き切る式なのです。
異常検知への応用は素直で、学習した密度のもとで尤度が低い点が異常です。問題は「運用文脈ごとに密度が違う」こと。そこで $f$ を条件 $c$ に依存させます。
2. 条件付きアフィンカップリング層
正規化フローの代表的な構成要素が、RealNVP のアフィンカップリング層です。入力を2つに分け、片方はそのまま通し($z_1 = x_1$)、もう片方を片方の関数でアフィン変換します。
$$ \begin{equation} z_1 = x_1, \qquad z_2 = x_2 \odot \exp\!\big(s(x_1)\big) + t(x_1) \end{equation} $$
この形なら、ヤコビアンが三角行列になり、行列式が $\exp(\sum s)$ と簡単に計算できます。

出典: L. Dinh, J. Sohl-Dickstein, S. Bengio, “Density estimation using Real NVP,” ICLR 2017, Fig. 2.
論文の原図を見ると、カップリング層の核心がよく分かります。順伝播(左)では $x_1$ をそのまま通し($=$)、$x_1$ から計算した $s, t$ で $x_2$ をスケール($\times$)・平行移動($+$)して $y_2$ を作ります。逆伝播(右)はまったく同じ部品を引き算($-$)と除算($\div$)で辿るだけ——だから逆変換が順変換と同じ計算コストで厳密に求まり、可逆性が保証されます。
条件付きにするには、スケール $s$ と平行移動 $t$ を計算するネットワークに、条件 $c$ を追加入力するだけです。
$$ \begin{equation} z_2 = x_2 \odot \exp\!\big(s(x_1, c)\big) + t(x_1, c) \end{equation} $$

条件 $c$(運用モードのone-hot、外生入力ベクトルなど)を $s, t$ ネットに注入することで、同じフローが条件ごとに違う密度を表現できるようになります。カップリング層を複数重ね、毎回どの次元を「素通り」させるかのマスクを入れ替えれば、全次元が混ざり合い、複雑な条件付き密度を表現できます。
ヤコビアンの対数行列式 $\sum s(x_1, c)$ は尤度にそのまま加算されるので、学習も推論も計算が軽いのが利点です。
アフィンカップリングは「可逆で、かつヤコビアンが三角行列(=行列式が対角の積で済む)になる変換」を作る一つの流儀にすぎません。同じ目標を別の構造で達成する手法群があり、可逆変換の作り方によって「順方向(尤度評価)と逆方向(サンプリング)のどちらが速いか」が変わります。

NICEの加法カップリング(スケールを使わず平行移動だけ)はヤコビアン行列式が常に1、RealNVPはそれにスケール $\exp(s)$ を足してヤコビアンを $\prod \exp(s)$ にしたものです。MAF/IAFは自己回帰構造で三角ヤコビアンを作りますが、尤度評価が速いMAFとサンプリングが速いIAFは互いに「逆向き」の関係にあります。Glowは $1\times1$ 可逆畳み込みで次元の混ぜ方を学習可能にしました。異常検知では尤度評価さえ速ければよいので、本記事のように尤度評価が高速な(順方向 $x\to z$ が軽い)構成を選びます。逆変換でサンプルを生成する必要はありません。
次に、なぜVAEやオートエンコーダでなくフローを使うのかを整理します。
3. なぜフローか — 厳密な尤度
生成モデルベースの異常検知にはいくつか選択肢がありますが、「尤度」の扱いが決定的に違います。

- オートエンコーダ: 再構成誤差で測る。これは尤度ではなく、確率的な解釈が曖昧。
- VAE: 対数尤度の下界(ELBO)を最大化する。尤度そのものは近似でしか得られない。
- 正規化フロー: 変数変換公式により対数尤度を厳密に計算する。
フローなら $-\log p(x\mid c)$ を、近似なしの異常スコアとして使えます。閾値の確率的な意味づけ(前記事のコンフォーマル予測との相性も良い)や、次元ごとの寄与の分解もしやすくなります。多変量時系列に適用する場合は、条件 $c$ を時間方向の情報に拡張します。

過去の窓 $x_{t-w:t-1}$ や外生入力 $u_t$ を条件 $c_t$ に符号化し、各時刻の条件付き密度 $p(x_t \mid c_t)$ を測ります。こうすれば「過去の文脈・操作で説明できる変動」を条件に吸収し、説明できない逸脱だけが低尤度として浮かび上がります。
ただし多変量時系列では「条件 $c$ をどう作るか」が自明ではありません。次元 $D$・時刻 $T$ の同時分布 $p(\mathbf{X})$ をそのまま1つのフローで表すのは高次元すぎて難しい。そこで使うのが因子分解(factorization)です。同時分布を条件付き密度の積に分解し、各要素 $x_t^d$ を「その点が依存している文脈 $c_t^d$ 」で条件づけます。

$$ \begin{equation} \log p(\mathbf{X}) = \sum_{t}\sum_{d} \log p\big(x_t^{d} \mid c_t^{d}\big) \end{equation} $$
条件 $c_t^d$ には「同じ属性の過去(時間方向)」と「同時刻の他属性(属性間)」の両方の文脈が入ります。この分解こそが、本記事が念頭に置く原論文の出発点です。理屈が揃ったので、まず単純な合成データで条件付きフローの効果を実装で確かめ、そのあとで原論文がこの因子分解をどう作り込んでいるかを見ます。
4. PyTorch実装 — 条件付きRealNVP
2次元・3モードのデータで、条件付きフローの効果を確かめます。まず条件付きRealNVPを実装します。密度評価には順方向 $x\to z$ とlog-detだけあれば十分です(逆変換は不要)。
import numpy as np, torch, torch.nn as nn
class CondRealNVP(nn.Module):
def __init__(self, dim=2, cdim=3, hidden=64, n_layers=6, conditional=True):
super().__init__()
self.dim = dim; self.conditional = conditional
masks = []; self.st = nn.ModuleList()
in_extra = cdim if conditional else 0
for i in range(n_layers):
m = torch.zeros(dim); m[i % dim] = 1.0 # 交互マスク(素通りする次元)
masks.append(m)
self.st.append(nn.Sequential(
nn.Linear(dim + in_extra, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU(),
nn.Linear(hidden, dim * 2)))
self.masks = torch.stack(masks)
def forward(self, x, c): # x -> z, log|det|
logdet = torch.zeros(x.shape[0]); z = x
for i, net in enumerate(self.st):
m = self.masks[i]; z_m = z * m
inp = torch.cat([z_m, c], 1) if self.conditional else z_m
h = net(inp); s, t = h[:, :self.dim], h[:, self.dim:]
s = torch.tanh(s) * (1 - m); t = t * (1 - m) # 変換するのはマスク外の次元
z = z_m + (1 - m) * (z * torch.exp(s) + t)
logdet = logdet + s.sum(1)
return z, logdet
def log_prob(self, x, c): # 変数変換公式で厳密な対数尤度
z, logdet = self.forward(x, c)
base = -0.5 * (z ** 2).sum(1) - 0.5 * self.dim * np.log(2 * np.pi)
return base + logdet
3モードのデータを作ります。各モードは平均と相関が異なります。
torch.manual_seed(0); np.random.seed(0)
rng = np.random.default_rng(0)
M, n = 3, 1500
means = np.array([[-2.5, -2.0], [0.3, 2.6], [2.8, -1.5]])
covs = []
for k in range(M):
A = rng.normal(0, 1, (2, 2)); covs.append(A @ A.T * 0.45 + 0.18 * np.eye(2))
X, C = [], []
for k in range(M):
X.append(rng.multivariate_normal(means[k], covs[k], n)); C.append(np.full(n, k))
X = np.vstack(X); C = np.concatenate(C)
perm = rng.permutation(len(X)); X, C = X[perm], C[perm]
mu, sd = X.mean(0), X.std(0); Xs = (X - mu) / sd
n_tr = int(0.7 * len(Xs))
Xtr, Ctr = Xs[:n_tr], C[:n_tr]; Xte_n, Cte_n = Xs[n_tr:], C[n_tr:]
異常は「自モードから別モードのクラスタ近くへずらした」点です。これは無条件で見れば「別モードの正常データ」に紛れるステルスな逸脱です。
na = len(Xte_n); Xan = []; Can = []
for i in range(na):
m = rng.integers(M); w = (m + rng.integers(1, M)) % M
tgt = means[m] + 0.8 * (means[w] - means[m]) # 別モードのクラスタ近くへ
x = rng.multivariate_normal(tgt, covs[w], 1)[0]
Xan.append((x - mu) / sd); Can.append(m) # ラベルは本来のモードm
Xan = np.vstack(Xan); Can = np.array(Can)
Xte = np.vstack([Xte_n, Xan]); Cte = np.concatenate([Cte_n, Can])
yte = np.concatenate([np.zeros(len(Xte_n)), np.ones(len(Xan))])
oh = lambda c: np.eye(M)[c]
条件付きフローと無条件フローを、それぞれ負の対数尤度を最小化して学習し、$-\log p$ をスコアにします。
Xtr_t = torch.tensor(Xtr, dtype=torch.float32); Ctr_t = torch.tensor(oh(Ctr), dtype=torch.float32)
Xte_t = torch.tensor(Xte, dtype=torch.float32); Cte_t = torch.tensor(oh(Cte), dtype=torch.float32)
def train(model):
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
for ep in range(600):
opt.zero_grad(); loss = -model.log_prob(Xtr_t, Ctr_t).mean()
loss.backward(); opt.step()
cond = CondRealNVP(conditional=True); train(cond)
uncond = CondRealNVP(conditional=False); train(uncond)
with torch.no_grad():
s_cond = (-cond.log_prob(Xte_t, Cte_t)).numpy()
s_uncond = (-uncond.log_prob(Xte_t, Cte_t)).numpy()
def auc(score, y):
order = np.argsort(score); ranks = np.empty(len(score)); ranks[order] = np.arange(1, len(score)+1)
npos = y.sum(); nneg = len(y) - npos
return (ranks[y==1].sum() - npos*(npos+1)/2) / (npos*nneg)
print(f"無条件フロー AUC={auc(s_uncond, yte):.3f}")
print(f"条件付きフロー AUC={auc(s_cond, yte):.3f}")
実行結果(seed固定):
無条件フロー AUC=0.721
条件付きフロー AUC=0.999
なぜこれほど差がつくのか。学習した密度をヒートマップで見ると一目瞭然です。

無条件フローは3モードの混合分布を学ぶので、3つすべてに密度の山ができます。すると、モード0の異常点(赤)が別モードのクラスタ位置にあると、そこは無条件フローにとって高密度の領域——「ありふれた正常データ」に見えてしまい、見逃します。

一方、条件付きフローに「モード0」という条件を与えると、密度の山はモード0のところだけに立ちます。別モード方向へずれた異常点は低密度の領域に落ち、$-\log p(x\mid c)$ が大きく跳ね上がって検出されます。

訓練曲線を見ると、条件付き・無条件いずれもNLLが順調に減少し、学習自体は成立しています。差は学習の良し悪しではなく、条件 $c$ を使えるかどうかから生まれています。

スコア分布でも、無条件フロー(左)は正常と異常の $-\log p$ が大きく重なるのに対し、条件付きフロー(右)は明確に分離しています。

数字の読み取りです。
- 無条件フロー=0.721:混合分布を学んでしまうため、別モードのクラスタに紛れた逸脱を「正常」と誤認します。
- 条件付きフロー=0.999:条件 $c$ で各モードの密度を切り分けるため、「このモードにしてはあり得ない」点を厳密な低尤度として捉えます。
これは合成データの理想的なケースですが、メカニズムは実データでも同じです。運用文脈を条件に入れることで、混合してしまうと消える「文脈固有の異常」が、厳密な条件付き尤度として浮かび上がる。
ここまでは条件 $c$ を「モードのone-hot」という最も単純な形で与えました。実データの多変量時系列では条件は手で与えられません。次節で、原論文がこの条件 $c$ をどう自動生成しているかを見ます。
5. 原論文のアーキテクチャ — 因子分解 × アテンション
本記事が念頭に置く原論文は、Guan, He, Ma, Gao “Conditional normalizing flow for multivariate time series anomaly detection,” ISA Transactions, vol. 143, pp. 231–243, 2023 です。提案手法は AFNF(Attention Factorization Normalizing Flow) と名付けられています。前節までで揃えた「因子分解で条件付き密度の積にする」「条件 $c$ を注入した可逆変換で厳密な尤度を測る」という2つの道具を、多変量時系列に対して具体的に組み上げたものと読めます。

5.1 問題意識
多変量時系列の異常検知が難しいのは、(1) 時間依存性(各属性が過去に依存する)、(2) 高次元性(属性間に複雑な相関がある)、(3) ラベル稀少性(正常データしか十分にない)という3つが同時に絡むからです。AFNF は 「異常は分布の低密度領域にある」 という素直な仮定のもと、正常データだけから条件付き密度を学び、尤度の低さで異常を測る教師なし手法です。鍵は、前節の式 (4) の条件 $c_t^d$ をどうやって時間依存と属性間相関の両方を反映した形で自動生成するか、にあります。
5.2 手法・データフロー
AFNF の条件生成は、上の図のように2方向のアテンションで構成されます。
- 時間方向のアテンション — 1つの属性について、任意長の過去の履歴を潜在状態ベクトルに要約します。これで「その属性のこれまでの振る舞い」という時間的文脈が条件 $c$ に入ります。
- 属性方向のアテンション — 同一時刻における異なる属性の隠れ表現どうしの関連を解析します。これで「同時刻に他の属性がどうなっているか」という属性間相関が条件 $c$ に入ります。
アテンションは集合に対して置換不変(順序を入れ替えても結果が変わらない)なので、そのままでは時刻の並び順という局所的な時間情報が落ちてしまいます。これを補うのが次の2つです。
- 隣接コントラスト(adjacency contrasting) — 隣り合う時刻どうしを対比させる仕組みで、置換不変性で失われる局所的な時間依存を明示的にモデル化します。
- 学習可能な大域位置符号化(global location encoding) — 系列全体の中での位置(長期的な位置文脈)を学習可能なベクトルとして与えます。
これら4つの部品が集約されて条件 $c$ ができ、各観測 $x_t^d$ を条件付き正規化フロー $z = f(x_t^d;\, c_t^d)$ で標準正規分布へ写し、変数変換公式で対数尤度を厳密に評価します。学習は正常データの負の対数尤度(NLL)最小化、異常スコアは式 (4) に従う $-\log p(x_t^d \mid c_t^d)$ です。本記事の実装で使った one-hot 条件が、AFNF では「アテンションが生成する文脈ベクトル」に置き換わっているだけで、条件付き尤度で測るという骨格は同じです。
5.3 尤度を異常スコアにするときの落とし穴
ここで一つ、深層生成モデルで尤度を異常スコアにする際の有名な注意点に触れておきます。「尤度が低い=異常」は常に成り立つわけではありません。

左図は学習分布 $\mathcal{N}(0,1)$ に対し、正常テスト(同じ $\mathcal{N}(0,1)$)と、分散だけ小さい「最頻値付近に過剰に集中した異常」$\mathcal{N}(0,0.3)$ を重ねたものです。右図のとおり、後者の方が平均対数尤度はむしろ高く(正常 $-1.417$ vs 異常 $-0.965$)、対数尤度だけを見ると異常を「正常」と誤ります(Nalisnick らが 2019 年に指摘した尤度ベース生成モデルのOOD病理)。だからこそ AFNF のように条件 $c$ で密度を切り分けることが効きます。「全体として尤度が高いか」ではなく「この文脈にしてはあり得ないか」を問えば、最頻値に紛れた逸脱も条件付き低尤度として浮かび上がるのです。前節の合成実験(無条件0.721 vs 条件付き0.999)は、まさにこの違いの最小再現でした。
5.4 実データでの位置づけ
AFNF は SWaT を含む実データセットで評価され、フローベースの条件付き密度推定が再構成誤差ベースの手法を上回ることが報告されています。同じ「条件付きフローで密度を測る」系譜の手法を SWaT の AUROC で並べると、傾向が見えます。

再構成ベース(EncDecAD)や距離ベース(DeepSAD)、敵対的再構成(USAD)に対し、グラフ構造つき条件付きフローの GANF(ICLR 2022)や、それを動的グラフに拡張した MTGFlow(AAAI 2023)が一段高い AUROC を出しています。AFNF もこのフローベース系譜に属し、条件 $c$ の作り方(アテンション因子分解)で密度推定の精度を押し上げる立場です。条件付き尤度という共通の原理のもとで、条件の作り込み方が性能を分ける——これが時系列フロー手法の現在地です。
注:AFNF(ISA Transactions 2023)の原論文本文・図はオープンアクセスではないため、本節の図はすべて手法の構造を自作で再現した概念図です。RealNVP(ICLR 2017, 公開)の図のみ §2 で原図を出典明記のうえ引用しています。
6. まとめ — シリーズの締めくくり
条件付き正規化フローによる異常検知を見てきました。
- 核心:条件 $c$(運用モード・外生入力)のもとでの密度 $p(x\mid c)$ を可逆変換で厳密にモデル化し、$-\log p(x\mid c)$ を異常スコアにする。
- 厳密な尤度:変数変換公式により、VAEのELBO(下界)やAEの再構成誤差と違い、対数尤度そのものが得られる。
- 条件の注入:アフィンカップリング層のスケール・平行移動ネットに $c$ を追加するだけ。
- 実証:無条件フロー(0.721)は別モードの高密度に騙されるが、条件付きフロー(0.999)は文脈固有の逸脱を厳密な低尤度で捉える。
このシリーズでは、「操作・運用で動いた分を異常としない」という一つの原理を、残差(連続予測)・状態遷移(離散コマンド)・適合性(参照との距離+コンフォーマル)・条件付き尤度(深層生成モデル)という4つの異なる実装で見てきました。表現は違えど、全てが「操作という条件で説明できる変動を取り除き、残りで異常を測る」という同じ思想に貫かれています。どれを選ぶかは、条件が観測できるか、離散か連続か、厳密な尤度や誤報保証が要るか、という実務的な問いで決まります。




主な参考文献
- S. Guan, Z. He, S. Ma, M. Gao, “Conditional normalizing flow for multivariate time series anomaly detection,” ISA Transactions, vol. 143, pp. 231–243, 2023. doi:10.1016/j.isatra.2023.09.002(提案手法 AFNF: Attention Factorization Normalizing Flow)
- L. Dinh, J. Sohl-Dickstein, S. Bengio, “Density estimation using Real NVP,” ICLR, 2017.
- E. Dai, J. Chen, “Graph-Augmented Normalizing Flows for Anomaly Detection of Multiple Time Series (GANF),” ICLR, 2022.
- Q. Zhou, et al., “Detecting Multivariate Time Series Anomalies with Zero Known Label (MTGFlow),” AAAI, 2023.
- E. Nalisnick, et al., “Do Deep Generative Models Know What They Don’t Know?,” ICLR, 2019.
- D. Rezende, S. Mohamed, “Variational Inference with Normalizing Flows,” ICML, 2015.
- G. Papamakarios, et al., “Normalizing Flows for Probabilistic Modeling and Inference,” JMLR, 2021.