時系列異常検知におけるGRUの役割 — 予測・再構成・確率的RNNの3つの使われ方

時系列の異常検知を学んでいると、ある事実に気づきます。代表的な深層学習ベースの手法——Telemanom、OmniAnomaly、USAD、Time-CAD——の心臓部に、判で押したように GRU(Gated Recurrent Unit) が座っているのです。なぜこんなに多くの手法がGRUを選ぶのでしょうか。そして、GRUは異常検知の中で具体的に「何をしている」のでしょうか。

この記事は、GRUそのものの仕組み(ゲートの数式や勾配の流れ)を解説する記事ではありません。それはGRUの理論記事に譲ります。本記事のテーマは一段上、「異常検知という応用の中で、GRUがどんな役割を担い、なぜ選ばれるのか」を位置づけることです。

結論を先に言えば、GRUの使われ方は大きく3つに整理できます。

  1. 予測ベース: GRUで「次の値」を予測し、予測が外れた度合いを異常とする
  2. 再構成ベース: GRUオートエンコーダで時系列を再構成し、再構成の失敗度を異常とする
  3. 確率的RNN: GRUとVAEを組み合わせ、観測の「ありそうになさ」を確率で測る

この3分類を頭に入れると、新しい手法に出会っても「ああ、これは再構成ベースのGRUを残差に効かせているのね」と素早く位置づけられるようになります。

本記事の内容

  • なぜ異常検知でLSTMやTransformerではなくGRUが選ばれるのか
  • GRUの3つの役割(予測ベース・再構成ベース・確率的RNN)と代表手法
  • 各役割の異常スコアの数式
  • 予測ベースと再構成ベースをPyTorchで実装し、同一データで比較
  • どの役割をいつ使うべきかの指針

前提知識

この記事を読む前に、以下を読んでおくと理解が深まります。

画像なし
GRUの理論と実装 — LSTMとの違いとゲート統合の仕組み
本記事の土台。リセット/更新ゲートの数式とLSTMとの違い。
オートエンコーダによる異常検知
再構成誤差で異常を測る考え方。役割②③の基礎です。
時系列の異常検知手法を体系的に解説
手法全体の地図。GRU系の位置づけが俯瞰できます。

なぜ異常検知でGRUが選ばれるのか

時系列の異常検知では、「いまの値が正常か」を判断するために過去の文脈が要ります。深夜にサーバ負荷が上がるのが異常かどうかは、その直前までの推移を踏まえないと決められません。だから、時間依存をモデル化できる仕組みが必要です。候補は素朴なRNN、LSTM、GRU、Transformerと複数あります。その中でGRUが好まれるのには理由があります。

なぜ異常検知でGRUが選ばれるか

上図は4つの選択肢を3つの観点で大まかに比べたものです(概念的な相対比較)。ポイントを整理します。

  • 素朴なRNNは軽いものの、勾配消失で長期依存をほとんど扱えません。異常検知では「少し前の周期パターン」を覚える必要があるため力不足です。
  • LSTMは長期依存に強いですが、ゲートが3つ・状態が2つあり重め。多変量・多エンティティ(例: サーバ28台)を個別に学習する異常検知では、この重さがコストに直結します。
  • GRUはLSTMの長期依存性能をほぼ保ったまま、パラメータを約25%削減した中庸の選択肢です。学習が速く、正常データだけで多数のモデルを回す異常検知の運用と相性が良いのです。
  • Transformerは長期依存と並列計算に優れますが、ストリーミング(逐次到着するテレメトリ)では一窓ごとに全系列を再計算するコストがかかり、データが少ない正常系列では過学習しやすい面もあります。

つまりGRUは「長期依存をそこそこ扱えて、軽くて速く、逐次処理に向く」というバランスの良さで選ばれています。異常検知は「正常データだけで、多数の系列に対して、何度も学習し直す」性質があるため、この三拍子が効くのです。

では、そのGRUが具体的にどんな役割で使われるのかを見ていきましょう。

GRUの3つの役割

時系列異常検知におけるGRUの3つの役割

上図がこの記事の地図です。GRUは「時間依存のモデル化」という共通の心臓部を担いながら、その出力をどう異常スコアに変えるかで3つに枝分かれします。それぞれを代表手法とともに見ていきます。

役割①:予測ベース — 「次」を当てて、外れたら異常

最も直感的な使い方です。GRUに過去 $x_{t-w:t}$ を入れて次の値 $\hat{x}_{t+1}$ を予測させ、実際の観測との誤差を異常スコアにします。

$$ \begin{equation} A_{t+1} = \big\lVert x_{t+1} – \hat{x}_{t+1} \big\rVert^2, \qquad \hat{x}_{t+1} = g_\theta(x_{t-w:t}) \end{equation} $$

予測ベース異常検知の仕組み

正常な区間ではGRUの予測がよく当たるのでスコアは小さく、異常が起きると予測が大きく外れてスコアが跳ねます。NASAが宇宙機テレメトリに使った Telemanom / LSTM-NDT(Hundman et al., 2018)がこの代表で、原典はLSTMですが、同じ発想でGRUに置き換えた実装も広く使われます。「正常を予測するモデル」を学べばよいので設計がシンプルなのが利点です。

弱点もあります。本質的に予測しにくい(ゆらぎの大きい)系列では、正常でも予測が外れて誤検知が増える点です。次の再構成ベースは、この弱点に別角度から答えます。

役割②:再構成ベース — 正常を「写し取れない」ものを異常とみなす

GRUをオートエンコーダ(GRU-AE)として使う方法です。窓 $x_{t-w:t}$ をエンコーダGRUで文脈ベクトル $z$ に圧縮し、デコーダGRUで元の窓を復元します。正常パターンだけで学習するので、異常窓はうまく復元できず再構成誤差が大きくなります。

$$ \begin{equation} A_t = \big\lVert x_{t-w:t} – \hat{x}_{t-w:t} \big\rVert^2, \qquad \hat{x}_{t-w:t} = \mathrm{Dec}_\theta\big(\mathrm{Enc}_\theta(x_{t-w:t})\big) \end{equation} $$

GRUオートエンコーダによる再構成ベース検知

予測ベースが「1点先を当てる」のに対し、再構成ベースは「窓全体を写し取る」点が違います。未来を当てる必要がないので、予測困難なゆらぎに振り回されにくく、論文でも「再構成ベースは予測ベースより頑健」としばしば指摘されます(実データはしばしば予測不能だから、というのが理由です)。

この再構成ベースGRUは応用範囲が広く、USAD(敵対的に鍛えたオートエンコーダ)や Time-CAD の検知器(残差を再構成する双方向GRU-AE)がこの系統です。とくにTime-CADでは、後述の双方向GRUが使われます。

Time-CAD:コンテキストを考慮した時系列分解による異常検知
残差を双方向GRU-AEで再構成する、再構成ベースGRUの応用例。

役割③:確率的RNN — 「ありそうになさ」を確率で測る

3つめは、GRUをVAE(変分オートエンコーダ)と融合させる使い方です。潜在表現を1点ではなく確率分布として持ち、観測の再構成確率(その観測がどれだけありそうか)を異常スコアにします。スコアが低い(ありそうにない)ほど異常です。

$$ \begin{equation} S_t = \log p_\theta(x_t \mid z_{t-w:t}) \end{equation} $$

これを実現した代表が OmniAnomaly(Su et al., 2019)で、GRUとVAEを組み合わせた「確率的RNN」に、潜在変数どうしを時間でつなぐ確率変数接続を加えています。正常なゆらぎを分布の広がりとして吸収できるため、ノイズの大きい系列に強いのが特長です。

OmniAnomaly:確率的RNNによる多変量時系列の異常検知
GRU+VAEの確率的RNN。役割③の代表例です。

3つの役割を概観したところで、再構成ベースでよく出てくる「双方向GRU」を補足しておきます。

補足:双方向GRU

検知ではしばしば双方向GRU(Bi-GRU) が使われます。順方向(過去→未来)と逆方向(未来→過去)の2つのGRUを走らせ、両方向の文脈を合わせて各時点を表現します。

双方向GRUの仕組み

予測タスク(役割①)では「未来を見てはいけない」ので順方向しか使えませんが、再構成タスク(役割②)では窓全体が既知なので、未来側の文脈も使えます。前後両方から見ることで「その点が周囲と整合しているか」をより正確に測れるため、Time-CADの検知器など再構成ベースで好んで使われます。

理屈が揃ったので、役割①と②を実際に実装して比べてみましょう。

Pythonでの実装:予測ベース vs 再構成ベース

ここでは同じGRU・同じ合成データで、予測ベース再構成ベースを実装して比較します。確かめたいのは「同じGRUでも、出力の使い方(予測か再構成か)で異常検知の振る舞いがどう変わるか」です。

合成データ

時間依存を持つ正常系列(複数の正弦波の重ね合わせ)に、性質の異なる4種類の異常——点スパイク・レベルシフト・分散増大・周波数異常——を埋め込みます。

import numpy as np
import torch
import torch.nn as nn
from sklearn.metrics import roc_auc_score, average_precision_score, precision_recall_curve, roc_curve

def gen_series(n, anomalies, seed):
    rng = np.random.default_rng(seed)
    t = np.arange(n)
    x = np.sin(0.10*t) + 0.5*np.sin(0.031*t + 0.7) + 0.3*np.sin(0.007*t)
    x = x + 0.06 * rng.standard_normal(n)
    lab = np.zeros(n, int)
    if anomalies:
        for s in [120, 480]:                       # 点スパイク
            x[s] += 2.2; lab[s] = 1
        x[220:240] += 1.3; lab[220:240] = 1        # レベルシフト
        x[330:350] += 0.9 * rng.standard_normal(20); lab[330:350] = 1  # 分散増大
        x[560:585] += 0.9 * np.sin(0.9*t[560:585]); lab[560:585] = 1   # 周波数異常
    return x.astype(np.float32), lab

W = 30
def windows(x, w=W):
    return np.stack([x[i:i+w] for i in range(len(x)-w+1)]).astype(np.float32)

合成データに埋め込んだ4種類の異常

色帯がそれぞれ異常の種類です。点スパイクのような瞬間的な異常と、レベルシフト・分散増大・周波数異常のような持続的な異常が混在しています。性質が違うので、予測ベースと再構成ベースで得意・不得意が分かれることが期待できます。

役割①:予測ベースのGRU

過去 $w$ 点から次の1点を予測し、誤差をスコアにします。

class GRUPredictor(nn.Module):
    def __init__(self, H=48):
        super().__init__()
        self.gru = nn.GRU(1, H, batch_first=True)
        self.head = nn.Linear(H, 1)
    def forward(self, x):
        h, _ = self.gru(x)
        return self.head(h[:, -1])          # 最終隠れ状態から次の1点

def make_pred_set(x, w=W):
    return windows(x[:-1], w)[..., None], x[w:][:, None]

def train_predictor(x_tr, seed=0, epochs=60):
    torch.manual_seed(seed)
    X, y = make_pred_set(x_tr); X, y = torch.tensor(X), torch.tensor(y)
    m = GRUPredictor(); opt = torch.optim.Adam(m.parameters(), lr=5e-3)
    for _ in range(epochs):
        perm = torch.randperm(len(X))
        for i in range(0, len(X), 128):
            b = perm[i:i+128]
            loss = ((m(X[b]) - y[b])**2).mean()
            opt.zero_grad(); loss.backward(); opt.step()
    return m

@torch.no_grad()
def predictor_score(m, x, w=W):
    X, y = make_pred_set(x)
    err = (m(torch.tensor(X)).numpy() - y)**2
    s = np.zeros(len(x)); s[w:] = err[:, 0]   # 予測対象点に誤差を割当
    return s

役割②:再構成ベースのGRU-AE

エンコーダGRUで窓を文脈ベクトルに圧縮し、それを展開してデコーダGRUで窓全体を復元します。

class GRUAE(nn.Module):
    def __init__(self, H=48):
        super().__init__()
        self.enc = nn.GRU(1, H, batch_first=True)
        self.dec = nn.GRU(H, H, batch_first=True)
        self.out = nn.Linear(H, 1)
    def forward(self, x):
        _, hn = self.enc(x)
        z = hn[-1:].transpose(0, 1).repeat(1, x.size(1), 1)  # 文脈ベクトルを窓長に展開
        d, _ = self.dec(z)
        return self.out(d)

def train_ae(x_tr, seed=0, epochs=60):
    torch.manual_seed(seed)
    Wt = torch.tensor(windows(x_tr))[..., None]
    m = GRUAE(); opt = torch.optim.Adam(m.parameters(), lr=5e-3)
    for _ in range(epochs):
        perm = torch.randperm(len(Wt))
        for i in range(0, len(Wt), 128):
            b = Wt[perm[i:i+128]]
            loss = ((m(b) - b)**2).mean()
            opt.zero_grad(); loss.backward(); opt.step()
    return m

@torch.no_grad()
def ae_point_score(m, x, w=W):
    Wt = torch.tensor(windows(x))[..., None]
    err = ((m(Wt) - Wt)**2).numpy()[..., 0]
    s = np.zeros(len(x)); cnt = np.zeros(len(x))
    for i in range(err.shape[0]):            # 各点を覆う窓の誤差を平均
        s[i:i+w] += err[i]; cnt[i:i+w] += 1
    return s / np.maximum(cnt, 1)

エンコーダ最終隠れ状態 $z$ を窓長ぶん複製してデコーダに渡すのがポイントです。GRUは「正常な窓の形」を $z$ に圧縮するよう学ぶので、異常窓は $z$ にうまく収まらず、復元時に誤差が出ます。

学習して比較する

両者を同じ正常データで学習し、テスト系列で異常スコアを出して比べます。

np.random.seed(0); torch.manual_seed(0)
x_tr, _ = gen_series(2000, anomalies=False, seed=1)
x_te, lab_te = gen_series(700, anomalies=True, seed=7)
mu, sd = x_tr.mean(), x_tr.std()
x_trn, x_ten = (x_tr-mu)/sd, (x_te-mu)/sd

mp = train_predictor(x_trn); ma = train_ae(x_trn)
s_pred = predictor_score(mp, x_ten)
s_rec = ae_point_score(ma, x_ten)

def best_f1(y, s):
    p, r, _ = precision_recall_curve(y, s); f = 2*p*r/(p+r+1e-12); return float(np.nanmax(f))

for name, s in [("predict", s_pred), ("reconstruct", s_rec)]:
    valid = s > 0 if name == "predict" else np.ones(len(s), bool)
    yy, ss = lab_te[valid], s[valid]
    print("%-11s AUC=%.3f AP=%.3f F1=%.3f" % (
        name, roc_auc_score(yy, ss), average_precision_score(yy, ss), best_f1(yy, ss)))

実行結果は次のとおりです。

役割 ROC-AUC PR-AUC(AP) 最良F1
① 予測ベース 0.908 0.728 0.686
② 再構成ベース(GRU-AE) 0.966 0.816 0.767

この設定では再構成ベースが予測ベースを上回りました。スコアの時系列を見ると理由が見えてきます。

予測ベースの異常スコア

予測ベース(上)は、点スパイクや急なレベルシフトの立ち上がりで鋭く反応します。一方、レベルシフトが定着した後や、分散増大のようなゆらぎでは、GRUがその場の値をある程度追従できてしまい、スコアが伸び悩む箇所があります。

再構成ベースの異常スコア

再構成ベース(下)は窓全体を見るため、持続的な異常(レベルシフト・分散増大・周波数異常)の区間まるごとでスコアが高止まりします。異常の「点」ではなく「区間」を捉えやすいわけです。

予測ベースvs再構成ベースのROC/PR比較

ROC/PR曲線でも再構成ベースが全域で上回りました。ただしこれは「再構成が常に勝つ」という意味ではありません。点的で突発的な異常が主なら予測ベースが有利なこともあり、異常の性質とデータの予測しやすさ次第です。だからこそ、両者を役割として理解し、状況で選ぶことが大切です。

どの役割をいつ使うか

最後に、3つの役割の使い分けを整理します。

役割 スコア 得意 苦手 代表手法
① 予測ベース 予測誤差 突発的・点的な異常、立ち上がりの検知 予測困難なゆらぎで誤検知 Telemanom / LSTM-NDT
② 再構成ベース 再構成誤差 持続的な異常、区間の検知。頑健 学習データに異常が混入すると弱い USAD / Time-CAD
③ 確率的RNN 再構成確率 ノイズの大きい系列、解釈(原因次元) 計算が重い OmniAnomaly

実務的な指針としては、まず②再構成ベースのGRU-AEをベースラインにするのが堅実です。実装が素直で頑健、多くの手法の出発点になっています。予測しやすい系列で突発異常を狙うなら①を、ノイズが大きく確率的な扱いや原因特定まで欲しいなら③を検討する、という順序が分かりやすいでしょう。

いずれの役割でも、GRUは「時間依存を軽量にモデル化する」という同じ仕事をしています。だからこそ、GRUの仕組み自体を理解しておくと、これら手法の中身が一段クリアに見えてきます。

まとめ

本記事では、時系列異常検知におけるGRUの役割を位置づけました。

  • なぜGRUか: 長期依存をそこそこ扱え、軽くて速く、逐次処理に向く——正常データで多数の系列を何度も学習する異常検知の運用に合う
  • 3つの役割: ①予測ベース(予測誤差)、②再構成ベース(再構成誤差)、③確率的RNN(再構成確率)
  • 代表手法: ①Telemanom/LSTM-NDT、②USAD/Time-CAD、③OmniAnomaly。いずれもGRUが時間依存モデル化の心臓部
  • 実験: 同一GRU・同一データで予測ベースと再構成ベースを比較。この設定では再構成ベースが上回り(AUC 0.908→0.966)、持続的な異常を区間ごと捉えた
  • 使い分け: まず再構成ベースをベースラインに。突発異常なら予測ベース、ノイズ・解釈重視なら確率的RNN

次のステップとして、以下も参考にしてください。

画像なし
GRUの理論と実装 — LSTMとの違いとゲート統合の仕組み
本記事の土台。GRUの内部構造を数式から理解する。
OmniAnomaly:確率的RNNによる多変量時系列の異常検知
役割③の代表。GRU+VAEの確率的RNN。
Time-CAD:コンテキストを考慮した時系列分解による異常検知
役割②の応用。残差を双方向GRU-AEで再構成。

参考文献

  • K. Hundman, V. Constantinou, C. Laporte, I. Colwell, T. Soderstrom. “Detecting Spacecraft Anomalies Using LSTMs and Nonparametric Dynamic Thresholding.” KDD ’18, 2018.
  • Y. Su, R. Liu, Y. Zhao, W. Sun, C. Niu, D. Pei. “Robust Anomaly Detection for Multivariate Time Series through Stochastic Recurrent Neural Network.” KDD ’19, 2019.
  • J. Audibert et al. “USAD: UnSupervised Anomaly Detection on Multivariate Time Series.” KDD ’20, 2020.
  • K. Cho et al. “Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation.” EMNLP, 2014.