1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Transformerの仕組みを理解して異常検知

1
Last updated at Posted at 2025-12-14

この記事は Qiita Advent Calendar 2025 - 時系列データ の17日目の記事です。

Day 10(LSTM予測型)、Day 12(LSTM-Autoencoder)に続き、今回はTransformerの仕組みを基礎から解説し、異常検知への応用としてAnomaly Transformerを実装します。

なぜTransformerなのか?

Day 10, 12 で使ったLSTMは、Cell Stateにより長期記憶を保持できます。

LSTM: t1 → t2 → t3 → ... → t100
      └──────────────────────┘
      位置1→100の情報伝達に99ステップ必要(逐次処理)

TransformerはSelf-Attentionにより、任意の2点間を 直接参照 します。

Transformer: t1 ←────────────────→ t100
             ↑    1ステップで直接参照    ↑
             └─→ t2 ←→ t3 ←→ ... ←─┘
観点 LSTM Transformer
処理方式 逐次(t1→t2→...) 並列(全時点同時)
位置i→jのパス長 O(|i-j|) O(1)
長系列の学習 勾配消失リスク 安定

Self-Attentionの仕組み

Self-Attentionは「各時点が他の全時点にどれだけ注目するか」を学習します。

Query, Key, Value の直感的理解

  • Query (Q): 「私は何を探している?」
  • Key (K): 「私はこういう情報を持っている」
  • Value (V): 「実際の情報はこれ」
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) \times V

計算の流れ

ステップ 計算 出力
1. 類似度計算 $QK^T$ 関連度(任意の実数)
2. スケーリング $\div \sqrt{d_k}$ Attention Score(正規化済み)
3. 確率分布化 $\text{softmax}(\cdot)$ Attention Weights(合計1)
4. 加重和 $\times V$ 出力ベクトル

Attention Weights が「どの時点にどれだけ注目するか」を表す確率分布で、これがSelf-Attentionの核心です。

なぜ内積で「類似度」が測れるのか?

2つのベクトル $\mathbf{q}$ と $\mathbf{k}$ の内積は、幾何学的には次のように分解できます:

\mathbf{q} \cdot \mathbf{k} = \|\mathbf{q}\| \|\mathbf{k}\| \cos\theta
  • $|\mathbf{q}|, |\mathbf{k}|$: 各ベクトルの大きさ(ノルム)
  • $\theta$: 2つのベクトルのなす角度
  • $\cos\theta$: 方向の一致度(-1〜1)

つまり内積は「ベクトルの大きさ × 方向の一致度」を表します。

状態 $\cos\theta$ 内積 意味
同じ方向 1 最大(正) 非常に似ている
直交 0 0 無関係
逆方向 -1 最小(負) 正反対

QueryとKeyの内積が大きいほど「Queryが探しているもの」と「Keyが持っている情報」の方向が一致している、つまり 関連性が高い と判断できます。

Burnのソースコードで理解するQ, K, V

Day 10でLSTMの内部構造を見たように、今回もBurnのソースコードを追ってみましょう。

MultiHeadAttentionの構造体

BurnのMultiHeadAttentionは、Q, K, Vそれぞれに対応する独立した線形変換層を持っています。

// burn-nn-0.19.1/src/modules/attention/mha.rs より抜粋
pub struct MultiHeadAttention<B: Backend> {
    /// Linear layer to transform the input features into the query space.
    pub query: Linear<B>,
    /// Linear layer to transform the input features into the key space.
    pub key: Linear<B>,
    /// Linear layer to transform the input features into the value space.
    pub value: Linear<B>,
    /// Linear layer to transform the output features back to the original space.
    pub output: Linear<B>,
    // ...
    pub d_k: usize,   // Size of the key and query vectors
}

Q, K, Vは同じ入力から別々の線形変換で生成されています

入力 X ─┬─→ [Linear_Q] ─→ Q(何を探しているか)
        ├─→ [Linear_K] ─→ K(どんな情報を持っているか)
        └─→ [Linear_V] ─→ V(実際の情報)

初期化時の処理

// 初期化部分
pub fn init<B: Backend>(&self, device: &B::Device) -> MultiHeadAttention<B> {
    let linear = |config: &Self| {
        LinearConfig::new(config.d_model, config.d_model)
            .with_initializer(self.initializer.clone())
            .init(device)
    };

    MultiHeadAttention {
        query: linear(self),   // Q用の線形変換
        key: linear(self),     // K用の線形変換
        value: linear(self),   // V用の線形変換
        output: linear(self),  // 出力用の線形変換
        d_k: self.d_model / self.n_heads,  // 各ヘッドの次元
        // ...
    }
}

d_k = d_model / n_heads という計算でヘッドを分割しています。例えば d_model=64, n_heads=4 なら d_k=16 となり、各ヘッドは16次元のQ, K, Vを扱います。

forward処理

pub fn forward(&self, input: MhaInput<B>) -> MhaOutput<B> {
    let [batch_size, seq_length_1, d_model] = input.query.dims();

    // Step 1: Q, K, V を生成(線形変換 + Multi-Head用のreshape)
    let query = self.attention_linear(input.query, &self.query);
    let key = self.attention_linear(input.key, &self.key);
    let value = self.attention_linear(input.value, &self.value);

    // Step 2: Attention スコア計算(QK^T / √d_k)
    let attn_scores = self.attn_scores(query, key);

    // Step 3: Softmax で重み計算
    let weights = self.attn_weights(attn_scores, input.mask_pad, input.mask_attn);

    // Step 4: Value の加重和
    let context = weights.clone().matmul(value);
    // ...
}

Multi-Head Attention

単一のAttentionでは、1つの関係性しか捉えられません。しかしデータには複数の関係性が同時に存在します(構文構造、意味的関係、長距離依存など)。Multi-Head Attention は、各ヘッドが異なる種類の関係を並列に学習 することで、多様なパターンを同時に捉える仕組みです。

attention_linear関数は、入力を線形変換した後、複数ヘッドに分割 します。

fn attention_linear(&self, x: Tensor<B, 3>, linear: &Linear<B>) -> Tensor<B, 4> {
    let [batch_size, seq_length, _d_model] = x.dims();
    linear
        .forward(x)  // [batch, seq, d_model]
        .reshape([batch_size, seq_length, self.n_heads, self.d_k])
        .swap_dims(1, 2)  // → [batch, n_heads, seq, d_k]
}

次元の変化を追うと:

[batch, seq, d_model]
    ↓ linear.forward()
[batch, seq, d_model]
    ↓ reshape()
[batch, seq, n_heads, d_k]
    ↓ swap_dims(1, 2)
[batch, n_heads, seq, d_k]

この形状により、各ヘッドが独立してAttentionを計算 します。

Attentionスコアの計算

数式 $\frac{QK^T}{\sqrt{d_k}}$ に対応するコードです。

fn attn_scores(&self, query: Tensor<B, 4>, key: Tensor<B, 4>) -> Tensor<B, 4> {
    let attn_scores = query
        .matmul(key.transpose())       // QK^T: [batch, heads, seq, seq]
        .div_scalar((self.d_k as f32).sqrt());  // ÷ √d_k

    self.dropout.forward(attn_scores)
}

key.transpose() でKeyの最後の2次元を転置し、matmul で行列積を計算。結果の形状は [batch, n_heads, seq, seq] となり、各時点が他の全時点にどれだけ注目するか を表す行列になります。

なぜ√d_kで割るのか?

QとKの内積は次元数dが大きいほど値が大きくなります。Softmaxは入力値が大きいと勾配が小さくなる(飽和する)ため、√d_kで正規化してスケールを調整しています。

Self-Attentionとは

「Self」Attentionの「Self」は、Q, K, Vの元となる入力が同じ という意味です。

Self-Attention の場合:
                    ┌─→ [Linear_Q] ─→ Q
  同じ入力 X ───────┼─→ [Linear_K] ─→ K   ← 3つとも同じXから生成
                    └─→ [Linear_V] ─→ V

Cross-Attention の場合(参考):
  入力 X ──────────────→ [Linear_Q] ─→ Q   ← Queryは自分
  別の入力 Y ─────┬─→ [Linear_K] ─→ K   ← Key/Valueは別ソース
                  └─→ [Linear_V] ─→ V

Burnでは MhaInput::self_attn(input) がこれを担います:

// Burnの実装(mha.rs より)
pub fn self_attn(tensor: Tensor<B, 3>) -> Self {
    Self {
        query: tensor.clone(),  // 同じtensorを
        key: tensor.clone(),    // 3箇所に
        value: tensor,          // セット
        mask_pad: None,
        mask_attn: None,
    }
}

入力は同じでも、Linear_Q, Linear_K, Linear_Vは別々に学習されたパラメータ を持つため、出力されるQ, K, Vは異なる値になります。

use burn::nn::attention::{MultiHeadAttention, MultiHeadAttentionConfig, MhaInput};

let config = MultiHeadAttentionConfig::new(d_model, n_heads);
let mha: MultiHeadAttention<Backend> = config.init(device);

// Self-Attention: 同じinputからQ, K, Vを生成
let mha_input = MhaInput::self_attn(input);
let output = mha.forward(mha_input);

// Attention weights: [batch, n_heads, seq, seq]
println!("Attention weights: {:?}", output.weights.dims());

Attention Weights の学習

Attention Weights は [seq, seq] の行列で、各行が「その位置から他の全位置への注目度」 を表します。

長さ8の系列で、学習前後の変化を見るとします。

学習前(初期状態):

Q, K, V の重みがランダムなので、注目先も不定です。

         t0    t1    t2    t3    t4    t5    t6    t7
    t0 [0.12, 0.13, 0.11, 0.14, 0.12, 0.13, 0.12, 0.13]  ← ほぼ均等
    t1 [0.11, 0.14, 0.12, 0.13, 0.13, 0.12, 0.12, 0.13]
    ...

学習後:

タスクに応じて関係性を捉えるパターンを獲得します。

           参照先 →
         t0    t1    t2    t3    t4    t5    t6    t7
    t0 [0.47, 0.35, 0.14, 0.03, 0.00, 0.00, 0.00, 0.00]
参  t1 [0.30, 0.42, 0.20, 0.05, 0.02, 0.01, 0.00, 0.00]
照  t2 [0.15, 0.25, 0.40, 0.12, 0.05, 0.02, 0.01, 0.00]
元  ...
↓   t7 [0.00, 0.00, 0.01, 0.03, 0.08, 0.18, 0.30, 0.40]
  • 各行の合計は 1(Softmaxで確率分布化)
  • t0 の行:t0 自身(0.47)と隣接 t1(0.35)に集中
  • t7 の行:t7 自身(0.40)と隣接 t6(0.30)に集中

この例では自身と隣接位置に注目が集中していますが、学習されるパターンはタスク依存 です。

原論文 "Attention Is All You Need" では、機械翻訳タスクで異なるパターンが示されています:

  • 長距離依存: "making" が離れた "more difficult" に注目(フレーズ補完)
  • 照応解決: "its" が "The Law" に注目(代名詞の参照先)
  • Multi-Head: 各ヘッドが構文・意味など異なる関係を学習

どのパターンが重要かを データから自動的に学習 できる点がAttentionの強みです。

Positional Encoding

Self-Attentionは全位置を同時に見るため、順序の概念がありません。位置0と位置7を区別できないのです。

そこで、位置情報を入力に加算します。

PE(pos, 2i) = \sin(pos / 10000^{2i/d_{model}})
PE(pos, 2i+1) = \cos(pos / 10000^{2i/d_{model}})

Burnのソースコードで理解する

// burn-nn-0.19.1/src/modules/pos_encoding.rs より抜粋
pub fn generate_sinusoids<B: Backend>(
    length: usize,
    d_model: usize,
    max_timescale: usize,
    device: &B::Device,
) -> Tensor<B, 2> {
    // 対数スケールの増分を計算
    let log_timescale_increment = -(max_timescale as f32).ln() / d_model as f32;

    for i in 0..length {           // 各位置
        for k in (0..d_model).step_by(2) {  // 各次元(偶数)
            let div_term = (k as f32 * log_timescale_increment).exp();
            row.push((div_term * i as f32).sin());  // 偶数次元: sin
            row.push((div_term * i as f32).cos());  // 奇数次元: cos
        }
    }
    // ...
}

生成される sinusoids(正弦波・余弦波)[max_sequence_size, d_model] の行列で、各行が各位置のエンコーディングです。

          dim0  dim1  dim2  dim3  dim4  dim5
          sin   cos   sin   cos   sin   cos   ← 交互に配置
位置0: [ 0.000, 1.000, 0.000, 1.000, 0.000, 1.000]
位置1: [ 0.841, 0.540, 0.046, 0.999, 0.002, 1.000]
位置2: [ 0.909,-0.416, 0.093, 0.996, 0.004, 1.000]
  • dim0-1(周期短):位置ごとに値が大きく変化 → 近い位置の区別に有効
  • dim4-5(周期長):値がほぼ一定 → 遠い位置の関係に有効

Transformerでの役割

PEは 入力埋め込みに加算 されます

入力 → Embedding → + PE → Self-Attention → ...
                    ↑           ↓
               ここで加算   Q, K, V を計算

PEが加算された入力からQ, K, Vが生成されるため、位置情報が間接的にAttentionに反映されます。

使い方

sinusoidsは 初期化時に事前計算 され、学習可能なパラメータはありません。

let pe = PositionalEncodingConfig::new(d_model)
    .with_max_sequence_size(100)
    .init(device);  // sinusoidsを事前計算

let with_position = pe.forward(embeddings);  // embeddings + sinusoids

Anomaly Transformer

ここからがいよいよ異常検知についてです。

Anomaly Transformer は、機械学習のトップ会議 ICLR 2022 で Spotlight 論文(上位約5%)として発表された、清華大学チームによる手法です。公式実装は時系列異常検知におけるTransformer系手法の代表格として広く参照されています。

Day 12 との共通点:再構成ベース

Day 12 の LSTM-Autoencoder と同様に、Anomaly Transformer も入力系列を再構成し、その誤差(Reconstruction Error)を異常検知に利用します。

入力 X → [Encoder] → 潜在表現 → [Decoder] → 再構成 X'

Reconstruction Error = ||X - X'||

ただし、Anomaly Transformer はこれに加えて Association Discrepancy という独自の指標を導入しています。

Association Discrepancy

  • 正常点: 系列全体と多様な関連を持つ(グローバルな依存関係)
  • 異常点: 隣接時点としか関連を持てない(ローカルな依存関係のみ)

この違いを Association Discrepancy(関連パターンの乖離)として定量化します。

Prior-Association vs Series-Association

Association 定義 特徴
Prior (P) ガウシアンカーネル 隣接点に集中(事前分布)
Series (S) 学習されたAttention データから学習(実際の依存関係)

ガウシアンカーネルとは?

ガウス分布(正規分布)の形をした重み付け関数です。「近いほど重く、遠いほど軽く」を数式化します。

距離:  0     1     2     3     4
重み: 1.00  0.61  0.14  0.01  0.00
      ↑自分自身   ↑隣接    ↑遠い点はほぼゼロ

σ(シグマ)は「どれくらい広く見るか」を制御し、時点ごとに学習されます。

Rust実装を見てみましょう:

// 距離行列: dist[i,j] = |i - j|
let positions: Vec<f32> = (0..seq_length).map(|i| i as f32).collect();
let pos_row = pos_tensor.clone().reshape([seq_length, 1]);
let pos_col = pos_tensor.reshape([1, seq_length]);
let dist_matrix = (pos_row - pos_col).abs();  // 位置の差の絶対値

// Gaussian: exp(-dist² / 2σ²)
let gaussian = (-dist_squared / (two * sigma_squared)).exp();

ポイントは dist_matrix = (pos_row - pos_col).abs() の部分です。これは 入力データの値を一切見ておらず、位置(インデックス)の差だけ を計算しています。つまり Prior-Association は「近い時点ほど重要」という事前知識を表現しているだけで、データの内容には依存しません。

  • Prior-Association: $P_{i,j} = G(|i-j|; \sigma)$ ← 位置の差のみ(データに依存しない)
  • Series-Association: $S_{i,j} = \text{Softmax}(QK^T/\sqrt{d})$ ← 入力データの内容から計算

Minimax戦略

単にAssociation Discrepancyを計算するだけでは、正常と異常の差は明確になりません。Anomaly Transformerでは、Minimax戦略 を用いてこの差を拡大させます。

損失関数

2つのフェーズを交互に実行します:

Minimize Phase(σ を学習):

\mathcal{L}_{min} = \mathcal{L}_{recon} + \lambda \cdot \text{KL}(P \| S_{detached})

Maximize Phase(Attention weights を学習):

\mathcal{L}_{max} = \mathcal{L}_{recon} - \lambda \cdot \text{KL}(P_{detached} \| S)
Phase 損失関数 学習対象 目的
Minimize $\mathcal{L}_{recon} + \lambda \cdot \text{AssDis}$ σ(Priorの広がり) Prior → Series に近づける
Maximize $\mathcal{L}_{recon} - \lambda \cdot \text{AssDis}$ Attention weights Series → Prior から遠ざける

ここで detached は勾配を止めることを意味します。Minimize Phase では Series を固定して σ だけを学習し、Maximize Phase では Prior を固定して Attention weights だけを学習します。λ はハイパーパラメータ(論文では 3.0)です。

なぜこれで差が拡大するのか

正常点:

  • Series は全体を見渡せる → 遠くの類似パターンを参照しても再構成可能
  • Maximize Phase で Series は Prior から離れようとする → Discrepancy 大

異常点:

  • 周囲とパターンが違うため、遠くの正常パターンを参照すると再構成誤差が増加する
  • 再構成誤差を小さく保つには、隣接点(自分自身や直近)を見ざるを得ない
  • Maximize Phase で離れようとしても、再構成誤差という制約があるためPriorから逃げられないDiscrepancy 小

結果として「正常 = Discrepancy大、異常 = Discrepancy小」という構造が 学習によって強制的に作られます 。これは画期的ですね。

Rust実装を見てみましょう:

pub fn compute_training_loss(
    &self,
    input: Tensor<B, 3>,
    output: &AnomalyTransformerOutput<B>,
    phase: TrainingPhase,
    lambda: f32,
) -> Tensor<B, 1> {
    // 再構成誤差(MSE)
    let diff = input - output.reconstruction.clone();
    let recon_loss = (diff.clone() * diff).mean();

    // Association Discrepancy(フェーズに応じて detach を制御)
    let ass_dis_loss = self.compute_association_discrepancy_for_training(
        &output.prior_associations,
        &output.series_associations,
        phase,  // Minimize時: series.detach() / Maximize時: prior.detach()
    );
    let ass_dis_mean = ass_dis_loss.mean();

    let lambda_tensor = Tensor::from_floats([lambda], &self.device);

    match phase {
        TrainingPhase::Minimize => {
            // Minimize: ReconLoss + λ * AssDis(Prior を Series に近づける)
            // series は detach されているので、σ のみが学習される
            recon_loss + lambda_tensor * ass_dis_mean
        }
        TrainingPhase::Maximize => {
            // Maximize: ReconLoss - λ * AssDis(Series を Prior から遠ざける)
            // prior は detach されているので、Attention weights のみが学習される
            recon_loss - lambda_tensor * ass_dis_mean
        }
    }
}

compute_association_discrepancy_for_training の中で、フェーズに応じて detach() を適用しています:

// Apply detach based on training phase
let (prior_for_kl, series_for_kl) = match phase {
    TrainingPhase::Minimize => {
        // Minimize: σ を学習、Series は固定
        (prior.clone(), series_avg.detach())
    }
    TrainingPhase::Maximize => {
        // Maximize: Attention weights を学習、Prior は固定
        (prior.clone().detach(), series_avg)
    }
};

detach() は勾配計算を止める操作です。Minimize Phase では Series を固定(detach)して σ だけを学習し、Maximize Phase では Prior を固定(detach)して Attention weights だけを学習します。これにより、2つのパラメータが互いに干渉せず、それぞれの役割に集中します。

異常スコアの計算

2つの指標を組み合わせる:

$\text{Association Discrepancy} = \text{KL}(P | S) + \text{KL}(S | P)$

$\text{異常スコア} = \text{Softmax}(-\text{AssDis}) \times \text{Reconstruction Error}$

指標 正常点 異常点
Association Discrepancy 大(多様な関連) 小(隣接のみ)
Softmax(-AssDis)
Reconstruction Error
最終スコア 小×小 = 大×大 = 大

Day 12 の Reconstruction Error だけでは捉えきれない「隣接依存パターン」を、Association Discrepancy が補完します。

Rust実装を見てみましょう:

pub fn anomaly_score(
    &self,
    input: Tensor<B, 3>,
    reconstruction: Tensor<B, 3>,
    association_discrepancy: Tensor<B, 2>,
) -> Tensor<B, 2> {
    let [batch_size, seq_length, _] = input.dims();

    // 再構成誤差(MSE per time point)
    let diff = input - reconstruction;
    let recon_error: Tensor<B, 2> = (diff.clone() * diff)
        .mean_dim(2)
        .reshape([batch_size, seq_length]);

    // Softmax(-AssDis): 小さいAssDisに大きな重み → 異常点を強調
    let neg_ass_dis = -association_discrepancy;
    let weights = burn::tensor::activation::softmax(neg_ass_dis, 1);

    // 最終スコア = 重み × 再構成誤差
    // 異常点: 低AssDis → 高weight、かつ高再構成誤差 → 高スコア
    weights * recon_error
}

Association Discrepancy の計算

Prior(事前分布)と Series(学習されたAttention)の乖離を、KL(Kullback-Leibler)ダイバージェンスで測定します。

KLダイバージェンスとは?

2つの確率分布 $P$ と $Q$ の「違い」を測る指標です。

\text{KL}(P \| Q) = \sum_i P_i \log \frac{P_i}{Q_i}
  • 値が0: $P$ と $Q$ が完全に一致
  • 値が大きい: $P$ と $Q$ が大きく異なる
  • 非対称: $\text{KL}(P | Q) \neq \text{KL}(Q | P)$

Anomaly Transformer では 対称化 するため、両方向を足し合わせます:

$\text{AssDis} = \text{KL}(P | S) + \text{KL}(S | P)$

重要なのは、KLダイバージェンスは 分布の形がどれだけ似ているか を測る点です。確率値の絶対的な大きさではなく、分布のパターンの違いを見ています。

状態 Prior (P) Series (S) Association Discrepancy
両方とも隣接集中 隣接集中 隣接集中 小さい(形が似ている)
パターンが異なる 隣接集中 全体分散 大きい(形が異なる)

異常点では Series も Prior と同様に隣接に集中するため Discrepancy が小さくなり、正常点では Series が全体に分散するため Discrepancy が大きくなります。

Rust実装:

pub fn association_discrepancy<B: Backend>(
    prior: Tensor<B, 3>,   // [batch, seq, seq] - ガウシアンカーネル
    series: Tensor<B, 3>,  // [batch, seq, seq] - 学習されたAttention
) -> Tensor<B, 2> {
    let [batch_size, seq_length, _] = prior.dims();
    let eps = 1e-8;  // log(0)回避
    let prior_safe = prior.clone() + eps;
    let series_safe = series.clone() + eps;

    // KL(P || S) = Σ_j P[i,j] * log(P[i,j] / S[i,j])
    let kl_ps = (prior.clone() * (prior_safe.clone().log() - series_safe.clone().log())).sum_dim(2);

    // KL(S || P) = Σ_j S[i,j] * log(S[i,j] / P[i,j])
    let kl_sp = (series * (series_safe.log() - prior_safe.log())).sum_dim(2);

    // 対称化KL: AssDis[i] = KL(P || S) + KL(S || P)
    (kl_ps + kl_sp).reshape([batch_size, seq_length])
}

GitHubコントリビューションで検証

Day 12 と同様に、学習期間とテスト期間を分離し、週単位の非オーバーラップウィンドウ で評価します。

データ分割

期間 範囲 週数
学習 2023-01-02 〜 2025-04-27 121週
テスト 2025-05-05 〜 2025-12-07 31週

重要:

  • 正規化は学習データの統計量のみを使用(データリーク防止)
  • 月曜始まりの週単位ウィンドウ(Day 12 と同じ)

学習設定

パラメータ 備考
window_size 7 週単位(Day 12 と同じ)
stride 7 非オーバーラップ
epochs 5
batch_size 32
learning_rate 1e-5
lambda (AssDis weight) 1.0

検出結果(テスト期間・シーケンス単位)

three_methods_comparison.png

閾値: mean + 2σ = 2.3680

# Week Score Anomaly?
4 2025-05-26 ~ 2025-06-01 2.3407
6 2025-06-09 ~ 2025-06-15 2.3519 ✗(閾値わずかに未満)
8 2025-06-23 ~ 2025-06-29 4.7912 YES

検出された異常週: 1件

  • 2025-06-23 ~ 2025-06-29(スコア: 4.7912)

既知イベントの検証

イベント 検出 スコア
6月増加 (2025-06-09, 101件) 2025-06-09 ~ 2025-06-15 2.3519

101件スパイクを含む週のスコアは 2.3519 で、閾値 2.3680 をわずかに下回りました。

考察

Day 12 との比較

観点 Day 12 (LSTM-AE) Day 17 (Anomaly Transformer)
検出異常週数 4週 1週
101件スパイク週 ✓ 検出 ✗ 閾値未満(僅差)
6月下旬の高活動 ✓ 検出 ✓ 検出(最高スコア)

精度は比較できない

今回のデータには「異常ラベル」がありません。何を異常とすべきか定義されていない以上、どちらの精度が高いかは判断できませんね。

ただし 検出力 の観点では、LSTMのほうが多くの異常を検出しているようですね(4週 vs 1週)。これが「感度が高い」のか「過検出」なのかは、ドメイン知識や業務要件によります。

Attention Weightsの可視化

Transformerの利点の一つは、Attention Weightsを可視化して「モデルが何に注目しているか」を確認できることです。

異常週(2025-06-23 ~ 2025-06-29, スコア: 26.49):

Mon Tue Wed Thu Fri Sat Sun 合計
コントリビューション 16 1 46 95 91 11 17 277
Attention Mon Tue Wed Thu Fri Sat Sun
Mon 0.12 0.34 0.04 0.10 0.09 0.19 0.12
Tue 0.15 0.26 0.07 0.10 0.09 0.19 0.15
Wed 0.08 0.36 0.05 0.15 0.15 0.12 0.09
Thu 0.05 0.35 0.04 0.20 0.21 0.07 0.07
Fri 0.06 0.36 0.04 0.18 0.19 0.08 0.08
Sat 0.16 0.28 0.07 0.07 0.07 0.19 0.14
Sun 0.15 0.32 0.06 0.07 0.07 0.19 0.13

正常週(2025-09-01 ~ 2025-09-07, スコア: 0.09):

Mon Tue Wed Thu Fri Sat Sun 合計
コントリビューション 0 3 0 5 9 1 0 18
Attention Mon Tue Wed Thu Fri Sat Sun
Mon 0.16 0.13 0.14 0.12 0.12 0.16 0.17
Tue 0.15 0.13 0.14 0.13 0.12 0.16 0.16
Wed 0.15 0.14 0.14 0.13 0.13 0.15 0.16
Thu 0.16 0.14 0.14 0.13 0.13 0.15 0.15
Fri 0.16 0.14 0.15 0.13 0.13 0.15 0.15
Sat 0.16 0.14 0.14 0.13 0.12 0.15 0.16
Sun 0.16 0.14 0.14 0.12 0.12 0.15 0.16

attention_heatmap_comparison.png

週タイプ Attentionパターン 解釈
異常週 火曜に集中(0.26-0.36) 特定時点への過度な固執
正常週 全曜日に均等(0.12-0.17) 幅広い時点と関連(グローバル)

異常週では全曜日から火曜(コントリビューション最少の1件)への注目が集中しています。なぜ値の小さい火曜に注目が集まったのでしょうか?

一つの解釈は、火曜の値(1件)が周囲(木曜95件、金曜91件など)と極端に異なるため、モデルがこの「異常な特徴」に過剰に反応している可能性です。異常値は目立つため、モデルがそこに引きずられてしまったのかもしれません。

正常時のグローバル(均一)な視点に対し、異常時は特定の時点に視野が固定されてしまっている状態です。

いつTransformerを選ぶべきか

今回は7日間と短いスパンでしたが、長いスパンのほうがTransformerの強みが出てきます。また、TransformerモデルはAttentionの可視化が可能です。

条件 推奨手法
長期依存性が重要 Transformer
リソース制約あり LSTM / ESN
データ量が少ない LSTM / ESN
解釈性が必要 Attention可視化可能なTransformer

まとめ

  1. Self-Attention: 全時点を同時に参照(パス長 O(1))
  2. Positional Encoding: sinusoidsによる位置情報の注入
  3. Anomaly Transformer: Prior/Series Association の Minimax学習

参考文献

  • Xu, J., Wu, H., Wang, J., & Long, M. (2022). Anomaly Transformer: Time Series Anomaly Detection with Association Discrepancy. ICLR 2022 (Spotlight). arXiv:2110.02642 / GitHub
  • Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762
  • Burn: Deep Learning Framework for Rust. burn.dev / GitHub

シリーズ記事:

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?