DeepLog:システムログの操作シーケンスをLSTMで学び異常を検知する(PyTorch実装)

システムログは、内部で実行された操作・コマンドの記録そのものだ。「セッション開始 → 認証 → ファイル割り当て → 書き込み → 解放」のように、正常な処理にはきまった操作の型(順序)がある。だとすれば、この型から外れた順序 ―― 認証の前に書き込みが来る、解放されたはずのリソースに再アクセスする ―― は異常のサインになる。これを深層学習で捉えたのが DeepLog(Du et al., ACM CCS 2017) だ。ログを自然言語の文のように扱い、LSTM で「次に来る操作」を予測し、予測から外れた操作を異常とする。

本記事は、CPS/操作シーケンス側の異常検知として、DeepLog を原論文の図を全て引用しながら技術的に深掘りし、さらにPyTorch で実装して実測する。前回のCPS多変量時系列の深掘りが「連続値のセンサ応答」を扱ったのに対し、こちらは「離散の操作系列」を扱う ―― 異常検知の両輪のもう一方だ。

DeepLogログキーモデルの概念:直前h個から次キーを予測しtop-g判定

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

前提知識

DeepLogの全体像

DeepLogの全体アーキテクチャ(出典: Du et al., CCS 2017, Fig.1)

出典: 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)

出典: Du et al., CCS 2017, Fig.2。

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

ログキーモデルの詳細(出典: Du et al., CCS 2017, Fig.3)

出典: 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)

出典: Du et al., CCS 2017, Fig.9。

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

ワークフロー構築と異常診断

タスク分離とワークフロー構築(出典: Du et al., CCS 2017, Fig.4)

出典: Du et al., CCS 2017, Fig.4。

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

ワークフローによる異常診断(出典: Du et al., CCS 2017, Fig.5)

出典: Du et al., CCS 2017, Fig.5。

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

OpenStack VM作成のワークフロー(出典: Du et al., CCS 2017, Fig.11)

出典: 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

DeepLog実装の学習曲線とgのトレードオフ

結果を読み取ろう。左の学習曲線は、次キー予測の交差エントロピーが順調に下がって収束したことを示す(最終損失約 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 は実ログのベンチマークで評価された。

HDFSログでの評価(出典: Du et al., CCS 2017, Fig.6)

出典: Du et al., CCS 2017, Fig.6。

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

OpenStackログでの評価(出典: Du et al., CCS 2017, Fig.7)

出典: Du et al., CCS 2017, Fig.7。

Fig.7 は OpenStack ログでの評価で、こちらでも高い検出性能を示す。

パラメータ依存性(出典: Du et al., CCS 2017, Fig.8)

出典: Du et al., CCS 2017, Fig.8。

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

BlueGene/Lログでの評価とオンライン学習(出典: Du et al., CCS 2017, Fig.10)

出典: 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)。この両輪を押さえると、異常検知の守備範囲がぐっと広がります。

CPS多変量時系列の異常検知を論文で深掘りする
連続値のセンサ応答側の深掘り。DeepLogと対になる記事。
コマンド・操作に対する応答を学習する異常検知
操作シーケンスを含む条件付き異常検知の俯瞰。DeepLogの位置づけがわかる。
時系列異常検知のサーベイ
パラダイム分類と評価指標の全体像。