時系列の異常検知を学んでいると、ある事実に気づきます。代表的な深層学習ベースの手法——Telemanom、OmniAnomaly、USAD、Time-CAD——の心臓部に、判で押したように GRU(Gated Recurrent Unit) が座っているのです。なぜこんなに多くの手法がGRUを選ぶのでしょうか。そして、GRUは異常検知の中で具体的に「何をしている」のでしょうか。
この記事は、GRUそのものの仕組み(ゲートの数式や勾配の流れ)を解説する記事ではありません。それはGRUの理論記事に譲ります。本記事のテーマは一段上、「異常検知という応用の中で、GRUがどんな役割を担い、なぜ選ばれるのか」を位置づけることです。
結論を先に言えば、GRUの使われ方は大きく3つに整理できます。
- 予測ベース: GRUで「次の値」を予測し、予測が外れた度合いを異常とする
- 再構成ベース: GRUオートエンコーダで時系列を再構成し、再構成の失敗度を異常とする
- 確率的RNN: GRUとVAEを組み合わせ、観測の「ありそうになさ」を確率で測る
この3分類を頭に入れると、新しい手法に出会っても「ああ、これは再構成ベースのGRUを残差に効かせているのね」と素早く位置づけられるようになります。
本記事の内容
- なぜ異常検知でLSTMやTransformerではなくGRUが選ばれるのか
- GRUの3つの役割(予測ベース・再構成ベース・確率的RNN)と代表手法
- 各役割の異常スコアの数式
- 予測ベースと再構成ベースをPyTorchで実装し、同一データで比較
- どの役割をいつ使うべきかの指針
前提知識
この記事を読む前に、以下を読んでおくと理解が深まります。


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

上図は4つの選択肢を3つの観点で大まかに比べたものです(概念的な相対比較)。ポイントを整理します。
- 素朴なRNNは軽いものの、勾配消失で長期依存をほとんど扱えません。異常検知では「少し前の周期パターン」を覚える必要があるため力不足です。
- LSTMは長期依存に強いですが、ゲートが3つ・状態が2つあり重め。多変量・多エンティティ(例: サーバ28台)を個別に学習する異常検知では、この重さがコストに直結します。
- GRUはLSTMの長期依存性能をほぼ保ったまま、パラメータを約25%削減した中庸の選択肢です。学習が速く、正常データだけで多数のモデルを回す異常検知の運用と相性が良いのです。
- Transformerは長期依存と並列計算に優れますが、ストリーミング(逐次到着するテレメトリ)では一窓ごとに全系列を再計算するコストがかかり、データが少ない正常系列では過学習しやすい面もあります。
つまりGRUは「長期依存をそこそこ扱えて、軽くて速く、逐次処理に向く」というバランスの良さで選ばれています。異常検知は「正常データだけで、多数の系列に対して、何度も学習し直す」性質があるため、この三拍子が効くのです。
では、そのGRUが具体的にどんな役割で使われるのかを見ていきましょう。
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} $$

予測ベースが「1点先を当てる」のに対し、再構成ベースは「窓全体を写し取る」点が違います。未来を当てる必要がないので、予測困難なゆらぎに振り回されにくく、論文でも「再構成ベースは予測ベースより頑健」としばしば指摘されます(実データはしばしば予測不能だから、というのが理由です)。
この再構成ベースGRUは応用範囲が広く、USAD(敵対的に鍛えたオートエンコーダ)や Time-CAD の検知器(残差を再構成する双方向GRU-AE)がこの系統です。とくにTime-CADでは、後述の双方向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」に、潜在変数どうしを時間でつなぐ確率変数接続を加えています。正常なゆらぎを分布の広がりとして吸収できるため、ノイズの大きい系列に強いのが特長です。

3つの役割を概観したところで、再構成ベースでよく出てくる「双方向GRU」を補足しておきます。
補足:双方向GRU
検知ではしばしば双方向GRU(Bi-GRU) が使われます。順方向(過去→未来)と逆方向(未来→過去)の2つの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)

色帯がそれぞれ異常の種類です。点スパイクのような瞬間的な異常と、レベルシフト・分散増大・周波数異常のような持続的な異常が混在しています。性質が違うので、予測ベースと再構成ベースで得意・不得意が分かれることが期待できます。
役割①:予測ベースの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がその場の値をある程度追従できてしまい、スコアが伸び悩む箇所があります。

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

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
次のステップとして、以下も参考にしてください。


参考文献
- 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.