システムログは、内部で実行された操作・コマンドの記録そのものだ。「セッション開始 → 認証 → ファイル割り当て → 書き込み → 解放」のように、正常な処理にはきまった操作の型(順序)がある。だとすれば、この型から外れた順序 ―― 認証の前に書き込みが来る、解放されたはずのリソースに再アクセスする ―― は異常のサインになる。これを深層学習で捉えたのが DeepLog(Du et al., ACM CCS 2017) だ。ログを自然言語の文のように扱い、LSTM で「次に来る操作」を予測し、予測から外れた操作を異常とする。
本記事は、CPS/操作シーケンス側の異常検知として、DeepLog を原論文の図を全て引用しながら技術的に深掘りし、さらにPyTorch で実装して実測する。前回のCPS多変量時系列の深掘りが「連続値のセンサ応答」を扱ったのに対し、こちらは「離散の操作系列」を扱う ―― 異常検知の両輪のもう一方だ。

上の図が DeepLog の中核アイデアだ。直前の $h$ 個の操作キー列から、LSTM が「次に来るキーの確率分布」を予測する。実際の次キーが予測の上位 $g$ 個(top-g)に入っていれば正常、入っていなければ異常と判定する。なぜこの素朴な仕組みが効くのか、原論文の図とともに見ていこう。
前提知識
DeepLogの全体像

出典: Du et al., “DeepLog: Anomaly Detection and Diagnosis from System Logs through Deep Learning”, ACM CCS 2017, Fig.1。
Fig.1 が DeepLog の全体像だ。生のログはまずパーサで構造化され、ログキー(log key) の系列とパラメータ値ベクトルに分けられる。ログキーとは「どの種類のメッセージか」を表す識別子で、可変部分(IDや数値)を除いたメッセージのテンプレートにあたる。例えば Received block blk_123 of size 456 は「Received block * of size *」というテンプレート=1つのログキーになる。DeepLog はこのログキー系列とパラメータ値の両方を、それぞれ別のモデルで異常検知する。さらに正常時のログからワークフロー(実行パス)を構築し、検出した異常の診断にも使う。
ログのパース(テンプレート抽出)には、DeepLog の論文では Spell(Streaming Parser for Event Logs using LCS)が使われている。Spell は最長共通部分列(LCS)に基づくオンラインパーサで、流れてくるログ行をほぼ線形時間でテンプレートに分類する。これにより、生のテキストログが「ログキーの系列」という離散シンボル列に変換され、自然言語の文と同じように系列モデルで扱えるようになる。重要なのは、この変換が異常検知の前処理であり、ここでの取りこぼし(テンプレート誤分類)は後段の精度に直結する点だ。まずは中心となるログキーの異常検知モデルを見よう。
ログキー異常検知モデル ― 次のキーを予測する

出典: Du et al., CCS 2017, Fig.2。
Fig.2 がモデルの考え方だ。DeepLog はログキーの系列を多クラス分類問題として扱う。直前の $h$ 個のキー列を入力に、LSTM が「次に来るキー」の確率分布を出力する。正常なログで訓練すると、モデルは正常な実行パスにおける遷移の確率を学ぶ。

出典: Du et al., CCS 2017, Fig.3。
Fig.3 はモデルの内部を展開したものだ。各時刻のログキーを入力に LSTM セルが状態を更新し、最終時刻の隠れ状態から全ログキーに対する確率分布を出す。
数式で書くと、$m$ 種類のログキー $\{k_1,\dots,k_m\}$ に対し、直前の窓 $w=(m_{t-h},\dots,m_{t-1})$ が与えられたときの次キーの条件付き確率
$$ \begin{equation} \Pr(m_t = k_i \mid w), \qquad i = 1,\dots,m \end{equation} $$
を LSTM が出力する(出力層は $m$ クラスの softmax)。学習は正常ログ上で、実際の次キーを正解とする多クラス交差エントロピーを最小化するだけだ。検出時には、この確率の高い順に並べた上位 $g$ 個の候補集合
$$ \begin{equation} \mathrm{TopG}(w) = \{\,k_i : \Pr(m_t=k_i\mid w)\ \text{が上位}\ g\,\} \end{equation} $$
に、実際の次キー $m_t$ が含まれるかを見る。含まれれば正常、外れていれば異常だ。$g$ は検出の感度を決める重要なパラメータで、小さいほど厳しく(異常を多く拾うが誤報も増える)、大きいほど緩い。後ほど実装でこのトレードオフを実測する。ログキーの「順序」だけでなく、ログに付随する「数値」の異常も見られる。
パラメータ値の異常

出典: Du et al., CCS 2017, Fig.9。
Fig.9 はパラメータ値の異常検知だ。各ログキーには、処理時間やバイト数などの数値(パラメータ値ベクトル)が付随する。DeepLog はこれらの時系列も別の LSTM で予測し、予測値と実測の誤差(MSE)が大きい点を異常とする。「順序は正常だが、処理時間が異常に長い」といったパフォーマンス異常を捉えられる。順序モデルとパラメータモデルの二段構えが DeepLog の守備範囲を広げている。検出した異常を「診断」するのがワークフローだ。
ワークフロー構築と異常診断

出典: Du et al., CCS 2017, Fig.4。
Fig.4 は、正常ログからワークフロー(実行パスのグラフ)を構築する様子だ。LSTM の予測確率を使って、並行・分岐・ループといった構造を持つタスクを分離し、正常な処理フローを復元する。

出典: Du et al., CCS 2017, Fig.5。
Fig.5 は診断の例だ。実際の実行パスと、ワークフローが許す正常なパスを突き合わせ、どこで分岐が想定外になったかを特定する。これにより「異常を検出した」だけでなく「どの操作で逸脱したか」まで示せる。

出典: Du et al., CCS 2017, Fig.11。
Fig.11 は OpenStack の仮想マシン作成ワークフローを DeepLog が復元した例だ。VM 生成という一連の操作の正常な型がグラフとして表現されている。ここまでが理論だ。次に、ログキーモデルを実際に PyTorch で書いて動かす。
PyTorchで実装する
ログキー異常検知モデルを実装しよう。まず、正常な実行パスを模した合成ログを作る。各ログキーから次に来うるキーを2つに限定した確率的オートマトン(=正常な処理の型)から、正常系列を生成する。
import numpy as np
import torch
import torch.nn as nn
torch.manual_seed(0); np.random.seed(0)
NUM_KEYS = 18
rng = np.random.default_rng(0)
# 各キーから次に来うるキー集合(正常な遷移ルール)
transitions = {k: list(rng.choice(NUM_KEYS, size=2, replace=False)) for k in range(NUM_KEYS)}
def gen_normal(n):
seq = [0]
for _ in range(n-1):
seq.append(int(rng.choice(transitions[seq[-1]])))
return seq
H = 10 # 直前h個の窓
def make_windows(seq):
X, y = [], []
for i in range(len(seq)-H):
X.append(seq[i:i+H]); y.append(seq[i+H])
return np.array(X), np.array(y)
train_seq = gen_normal(6000)
Xtr, ytr = make_windows(train_seq)
Xtr = torch.tensor(Xtr, dtype=torch.long); ytr = torch.tensor(ytr, dtype=torch.long)
次に、DeepLog のログキーモデルを定義する。埋め込み層+LSTM(2層)+全結合で、最終時刻の隠れ状態から次キーの logits を出すだけのシンプルな構成だ。
class DeepLogKey(nn.Module):
def __init__(self, num_keys, emb=16, hidden=64, layers=2):
super().__init__()
self.emb = nn.Embedding(num_keys, emb)
self.lstm = nn.LSTM(emb, hidden, layers, batch_first=True)
self.fc = nn.Linear(hidden, num_keys)
def forward(self, x):
h, _ = self.lstm(self.emb(x))
return self.fc(h[:, -1, :]) # 最終時刻から次キーのlogits
model = DeepLogKey(NUM_KEYS)
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
lossf = nn.CrossEntropyLoss()
for ep in range(30):
perm = torch.randperm(len(Xtr))
for i in range(0, len(Xtr), 256):
idx = perm[i:i+256]
opt.zero_grad()
loss = lossf(model(Xtr[idx]), ytr[idx]); loss.backward(); opt.step()
学習が終わったら、異常列を作る。正常な遷移ルールに違反するキー(来てはいけないキー)を確率 8% で混ぜる。そして top-g 判定を実装する ―― 実際の次キーが予測の上位 $g$ 個に入らなければ異常だ。
def gen_abnormal(n, p=0.08):
seq = [0]
for _ in range(n-1):
if rng.random() < p:
illegal = [k for k in range(NUM_KEYS) if k not in transitions[seq[-1]]]
seq.append(int(rng.choice(illegal))) # 遷移違反=異常
else:
seq.append(int(rng.choice(transitions[seq[-1]])))
return seq
@torch.no_grad()
def detect(seq, g):
X, y = make_windows(seq)
X = torch.tensor(X, dtype=torch.long); y = torch.tensor(y, dtype=torch.long)
topg = model(X).topk(g, dim=1).indices
flagged = (topg != y[:, None]).all(dim=1) # 実キーがtop-gに無い→異常
return flagged.numpy(), y.numpy(), X.numpy()
test_norm = gen_normal(3000); test_abn = gen_abnormal(3000, p=0.08)
for g in [1, 2, 3, 5, 9]:
fa, ya, Xa = detect(test_abn, g)
truth = np.array([ya[i] not in transitions[Xa[i,-1]] for i in range(len(ya))])
tp = (fa & truth).sum(); fp = (fa & ~truth).sum(); fn = (~fa & truth).sum()
prec = tp/(tp+fp+1e-9); rec = tp/(tp+fn+1e-9); f1 = 2*prec*rec/(prec+rec+1e-9)
print(f"g={g}: Precision={prec:.3f} Recall={rec:.3f} F1={f1:.3f}")
実行すると、次の出力が得られる。
g=1: Precision=0.145 Recall=0.992 F1=0.253
g=2: Precision=0.443 Recall=0.988 F1=0.612
g=3: Precision=0.470 Recall=0.934 F1=0.625
g=5: Precision=0.542 Recall=0.838 F1=0.659
g=9: Precision=0.604 Recall=0.548 F1=0.575

結果を読み取ろう。左の学習曲線は、次キー予測の交差エントロピーが順調に下がって収束したことを示す(最終損失約 0.69 ―― 各キーの正解候補が2つあるので、完璧でも $\log 2\approx0.69$ が下限になる。モデルは正常な遷移をほぼ学びきった)。右は $g$ を変えたときの検出率と誤報率だ。$g$ のトレードオフがはっきり出ている。
- $g=1$:再現率 0.992(ほぼ全ての違反を検出)だが、適合率 0.145 と低い。正常でも「もう一方の正当な次キー」が来ると top-1 から外れて誤報するためで、誤報率(FPR)は 0.5 を超える。
- $g$ を大きくする:誤報は激減($g=3$ で FPR ≈ 0.001)するが、違反キーも上位に紛れて見逃しが増える($g=9$ で再現率 0.548)。
- 最良の F1 は $g=5$ の 0.659:このデータでは、感度と誤報のバランスがこの辺りで取れる。
この「$g$ をどう選ぶか」こそ DeepLog の実運用上の勘所だ。論文でもパラメータ感度として詳しく分析されている。
評価 ― HDFS・OpenStack・BlueGene/L
DeepLog は実ログのベンチマークで評価された。

出典: Du et al., CCS 2017, Fig.6。
Fig.6 は HDFS(分散ファイルシステム)ログでの精度・再現率・F1 で、PCA や Invariant Mining、N-gram といった従来手法を上回る。

出典: Du et al., CCS 2017, Fig.7。
Fig.7 は OpenStack ログでの評価で、こちらでも高い検出性能を示す。

出典: Du et al., CCS 2017, Fig.8。
Fig.8 はパラメータ(窓長 $h$、候補数 $g$、LSTM層数など)に対する性能の変化だ。私たちの実装で見た「$g$ のトレードオフ」が、実ログでも同様に現れている。適切な $g$ の選択が性能を左右することがわかる。

出典: Du et al., CCS 2017, Fig.10。
Fig.10 は BlueGene/L ログでの評価で、オンライン学習の効果を示す。DeepLog は運用中に管理者のフィードバック(誤検知の指摘など)を受けてモデルを逐次更新できる。少ない訓練データ(最初の 1%/10%)から始めても、オンライン更新で性能が改善していく。新しい正常パターンが現れる実システムで重要な性質だ。
発展 ― DeepLog以降のログ異常検知
DeepLog(2017)は、ログ異常検知を「系列予測問題」として定式化した出発点であり、その後の研究の土台になった。主要な発展を押さえておこう。
- LogAnomaly(2019):DeepLog はログキーを単なる ID(記号)として扱うため、「未知の/少し違う表現のログ」に弱い。LogAnomaly はログテンプレートを単語の意味埋め込み(semantic vector)で表し、語彙の揺れに頑健にした。テンプレートが多少変わっても意味が近ければ正常と判断できる。
- LogBERT(2021):BERT 系の Transformer を使い、ログキー列をマスク言語モデルとして学習する。系列の双方向の文脈を使えるため、LSTM の一方向予測より豊かな表現を得る。
- LogGPT などLLM系(2023〜):大規模言語モデルにログ系列を解釈させる流れ。本ブログの AAD-LLM の記事 で扱ったように、ドメイン知識を言語で渡して異常を判定・説明させる方向につながる。
いずれも「ログ=操作シーケンスを系列モデルで学び、逸脱を異常とする」という DeepLog の発想を受け継ぎ、表現(記号→意味→文脈→言語)を進化させたものだ。DeepLog はこの系譜の原点として、いまも実装・比較の基準になっている。
まとめ
本記事では、DeepLog を原論文の全図とともに深掘りし、PyTorch で実装しました。
- ログ=操作シーケンス:ログをログキーの系列とみなし、LSTM で次キーを予測。実キーが top-g に入らなければ異常とする。
- 二段構え:順序の異常(ログキーモデル)に加え、パラメータ値の異常(パフォーマンス異常)も別モデルで検出する。
- ワークフロー診断:正常ログから実行パスのグラフを構築し、「どこで逸脱したか」まで示せる。
- 実装で確認した $g$ のトレードオフ:$g=1$ は再現率 0.992/適合率 0.145、$g$ を上げると誤報は減るが見逃しが増え、F1 は $g=5$ で最良の 0.659。$g$ の選択が実運用の鍵。
- オンライン学習:運用中のフィードバックで逐次更新でき、新しい正常パターンに追従する。
連続値のセンサ応答(USAD・OmniAnomaly・TranAD など)と、離散の操作シーケンス(DeepLog)。この両輪を押さえると、異常検知の守備範囲がぐっと広がります。


