この記事は 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 |
検出結果(テスト期間・シーケンス単位)
閾値: 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パターン | 解釈 |
|---|---|---|
| 異常週 | 火曜に集中(0.26-0.36) | 特定時点への過度な固執 |
| 正常週 | 全曜日に均等(0.12-0.17) | 幅広い時点と関連(グローバル) |
異常週では全曜日から火曜(コントリビューション最少の1件)への注目が集中しています。なぜ値の小さい火曜に注目が集まったのでしょうか?
一つの解釈は、火曜の値(1件)が周囲(木曜95件、金曜91件など)と極端に異なるため、モデルがこの「異常な特徴」に過剰に反応している可能性です。異常値は目立つため、モデルがそこに引きずられてしまったのかもしれません。
正常時のグローバル(均一)な視点に対し、異常時は特定の時点に視野が固定されてしまっている状態です。
いつTransformerを選ぶべきか
今回は7日間と短いスパンでしたが、長いスパンのほうがTransformerの強みが出てきます。また、TransformerモデルはAttentionの可視化が可能です。
| 条件 | 推奨手法 |
|---|---|
| 長期依存性が重要 | Transformer |
| リソース制約あり | LSTM / ESN |
| データ量が少ない | LSTM / ESN |
| 解釈性が必要 | Attention可視化可能なTransformer |
まとめ
- Self-Attention: 全時点を同時に参照(パス長 O(1))
- Positional Encoding: sinusoidsによる位置情報の注入
- 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
シリーズ記事:
- Day 4: 統計的手法による異常検知入門
- Day 10: LSTM予測型で異常検知
- Day 12: LSTM-Autoencoderで異常検知
- Day 17: 本記事(Anomaly Transformer)

