毎週月曜の朝、オフィスビルの電力使用量は跳ね上がります。これは異常でしょうか? もちろん正常です。では、日曜の深夜に同じ大きさの跳ね上がりがあったら? こちらは何かおかしい——空調の誤作動か、機器の故障かもしれません。
ここで効いているのは「いつ・どんな状況か」という文脈です。「電力使用量が高い」という事実だけでは、正常か異常かを決められません。曜日・時間帯・祝日・イベントといった文脈、すなわちコンテキストを知って初めて、その値が「ありふれた変動」なのか「説明のつかない逸脱」なのかを判断できます。
ところが、よく使われる時系列分解(STLなど)はこのコンテキストを扱えません。固定的な周期パターンしか学習しないため、年末年始のトラフィック急増や祝日の売上増を「異常」と誤検知してしまいます。誤報が積み重なると運用者はアラートを無視するようになり、本物の異常を見逃す——いわゆる「オオカミ少年」問題が起きます。
本記事の主役 Time-CAD は、この問題に正面から取り組んだ手法です。Nam らが ECML PKDD 2023 で発表しました(Nam et al., 2023)。アイデアは明快で、コンテキストで説明できる変動を分解の段階で取り除き、説明できない残差だけで異常を判定するというものです。ビジネスの実データ(通信サービスKPI・IT運用・エネルギー・産業IoT)で検証され、コンテキストを使わない構成に対して大きな性能改善を示しました。
Time-CADの考え方を理解すると、次の場面で武器になります。
- AIOps / ビジネスKPI監視: 曜日・祝日・キャンペーンで変動するKPIから、本当の障害だけを検出
- エネルギー・設備監視: 気温や稼働スケジュールで動く消費量から、機器異常を切り分け
- 産業IoT・制御系: 運転モードが切り替わるセンサ系列で、モード変化に伴う正常変動を誤検知しない
本記事の内容
- なぜ従来の時系列分解では不十分なのか — コンテキスト無視という弱点
- Time-CADの2フェーズ構成(文脈認識ディープ分解 → 残差ベース検知)
- 文脈ベクトル $\eta_t$ の定義と、残差式 $r_t=\Psi(x_t-f_\theta(\tau_t+s_t)+\lambda_t\eta_t)$
- 学習の目的関数と、再構成誤差による異常スコア $A_t$
- サーベイ: 4ベンチマークでの性能比較(TaF1)と、コンテキスト統合のアブレーション
- PyTorchによる簡易Time-CADの実装と、合成データでの再現実験
前提知識
この記事を読む前に、以下を読んでおくと理解が深まります。



Time-CADとは — 「説明できる変動」を引いてから異常を見る

まず核心の直感をつかみましょう。上の2つのグラフは、まったく同じ形の急増を描いています。違うのは状況だけです。左は祝日(祝日フラグ=1)に起きた急増で、「祝日なら人出が増えて当然」と文脈で説明できるため正常。右は普通の平日(祝日フラグ=0)に起きた同じ急増で、説明がつかないため異常です。
Time-CADの一言でいうアイデアはこうです。
時系列を「コンテキストで説明できる部分」と「説明できない部分」に分け、説明できない部分だけで異常を判断する。
音楽のミックスダウンに例えると分かりやすいでしょう。曲全体を「ボーカル(トレンド)」「伴奏(季節性)」「ノイズ(残差)」に分離し、ノイズが異様に大きければ「何かおかしい」と判断する——これが分解ベース異常検知の基本です。Time-CADはここに一歩踏み込み、「会場(コンテキスト)に応じて分離の仕方を変える」。野外フェスかスタジオ録音かで風の音や反響の扱いを変えるように、祝日か平日かで「許容される変動」を変えるのです。
なぜこの工夫が必要なのか。従来の分解がどこでつまずくのかを先に見ておきましょう。
なぜ従来の時系列分解では不十分なのか
時系列異常検知の定番アプローチに「時系列分解」があります。観測 $Y_t$ を3つの成分に分けるものです。
$$ \begin{equation} Y_t = T_t + S_t + R_t \end{equation} $$
- $T_t$: トレンド(ゆるやかな長期傾向。例: 年々増えるユーザ数)
- $S_t$: 季節性(繰り返すパターン。例: 毎週同じ曜日の山)
- $R_t$: 残差(トレンドにも季節性にも当てはまらない残り)
STL分解(Seasonal-Trend decomposition using Loess)が代表格で、残差 $R_t$ が大きい時点を異常とみなします。考え方はシンプルで強力です。
問題は、STLが学習する季節性 $S_t$ が固定的な周期パターンだという点です。「毎週月曜は高い」のような規則的な周期は捉えられますが、祝日・イベント・気温といった不規則なコンテキストには対応できません。

上図がその様子です。元系列(上段)では、祝日(橙の帯)に値が正常に跳ね上がっています。ところがSTLの季節成分は「いつもの曜日パターン」しか知らないので、この祝日の急増を説明できません。結果、下段の残差にくっきりと山が残ります。残差ベースの検知器はこの山を「異常」と誤検知してしまうのです。
つまり問題の根は「分解がコンテキストを知らない」ことにあります。ならば、分解そのものにコンテキストを教え込めばよい——これがTime-CADの出発点です。
Time-CADの全体像 — 2つのフェーズ
Time-CADは2つのフェーズで動きます。

Phase 1(文脈認識ディープ分解)は、入力系列をまず古典的なSTLで $\tau_t+s_t+r_t$ に初期分解し、トレンドと季節性の和(正常パターン)を深層ネットワーク $f_\theta$ で浄化したうえで、コンテキスト項 $\lambda_t\eta_t$ を残差計算に注入します。これにより「文脈で説明できる正常変動」を残差から取り除きます。
Phase 2(残差ベース検知)は、こうして得たクリーンな残差 $r_t$ を別のオートエンコーダ $g_\phi$ に通し、その再構成誤差を異常スコアとします。
ここで押さえたいのは役割分担です。トレンドと季節性そのものは古典STL(統計手法)が生成し、ニューラルネット $f_\theta$ はその和を再構成・浄化する係です。論文が “deep” decomposition と呼ぶのはこの非線形浄化を指します。よくある誤解として「ニューラルネットがトレンド/季節を直接出力する」と思いがちですが、原論文の構成はそうではありません。
では、心臓部であるPhase 1の分解を式で見ていきましょう。
Phase 1:文脈認識ディープ分解
残差の定義式
Time-CADの残差は、次の1本の式に集約されます(論文 Eq.(1))。
$$ \begin{equation} r_t = \Psi\!\big(x_t – f_\theta(\tau_t + s_t) + \lambda_t \cdot \eta_t\big) \end{equation} $$
これが論文の定義式です。公式実装(data_loader.py)ではこの加算式を乗算形式で実現しており、等価な別表現として次の式が使われています。
$$ \begin{equation} r_t = \Psi\!\Big(\bigl(x_t – f_\theta(\tau_t + s_t)\bigr) \times \bigl(1 + \mathrm{weight} \times \lambda_t\bigr)\Big) \end{equation} $$
ここで $\lambda_t = \mathbf{1}(t \in H) + \mathbf{1}(t \in \mathrm{Weekend}) \in \{0,1,2\}$(スカラー)、$\mathrm{weight}$ はハイパーパラメータ(既定 $-0.7$)です。論文式の「加算 $+\lambda_t \eta_t$」は、乗算式では「スケーリング係数 $(1 + \mathrm{weight}\times\lambda_t)$」に相当します。どちらの表現も「文脈が強い時点ほど残差の大きさを変化させる」という同じ狙いを持ちます。
各項の意味を順に押さえます。
- $x_t \in \mathbb{R}^M$: 観測値ベクトル($M$ は変量数)
- $\tau_t + s_t$: STLで得たトレンド+季節性(=素朴な正常パターン $x_t^n$)
- $f_\theta(\tau_t+s_t)$: それを GRUオートエンコーダ $f_\theta$ で浄化・再構成した正常パターン。STLが取りこぼしたノイズや、訓練データに紛れた軽微な異常をならす役割です
- $\lambda_t \cdot \eta_t$: コンテキスト項。$\eta_t$ が文脈ベクトル、$\lambda_t$ がその寄与度スカラー
- $\Psi$: ウェーブレット変換による微小ノイズ除去
記号の次元まとめ
| 記号 | 意味 | 次元・型 |
|---|---|---|
| $X \in \mathbb{R}^{N \times M}$ | 入力時系列行列 | $N$: タイムステップ数、$M$: 変量数 |
| $W_t$ | スライディング窓(長さ $W$ の部分系列) | $\mathbb{R}^{W \times M}$ |
| $x_t^n = \tau_t + s_t$ | STL正常パターン | $\mathbb{R}^M$ |
| $f_\theta$ | Phase 1 GRU-AE(浄化ネットワーク) | $\mathbb{R}^{W \times M} \to \mathbb{R}^{W \times M}$ |
| $g_\phi$ | Phase 2 GRU-AE(検知ネットワーク) | $\mathbb{R}^{W \times M} \to \mathbb{R}^{W \times M}$ |
| $\eta_t$ | 文脈ベクトル | $\mathbb{R}^5$(hour_sin, hour_cos, weekend, holiday, prev_holiday) |
| $\lambda_t$ | 文脈寄与度スカラー | 実数(ハイパーパラメータ weight、既定値 $-0.7$) |
| $r_t$ | 文脈補正後残差 | $\mathbb{R}^M$ |
| $A_t$ | 異常スコア | $\mathbb{R}_{\geq 0}$ |
| $\delta$ | 二値化閾値 | $\mathbb{R}$ |
$\lambda_t$(コード上の weight)の符号について
式(2)の $\lambda_t$ は公式実装では引数名 weight、既定値 $-0.7$ です。負値であることが重要で、”文脈で増幅される残差を縮小する方向”に働きます。$\eta_t$(実装上は holiday + is_weekend、値域は $\{0,1,2\}$)が1または2のとき、係数 $(1 + \mathrm{weight} \times \eta_t) = 1 + (-0.7) \times \eta_t$ が0.3または−0.4になり、文脈由来の変動が圧縮・符号反転されます。$\eta_t=0$(平日・非祝日)では係数が1のまま、残差は素のままです。
ポイントは、コンテキスト $\eta_t$ を分解の外から後付けで判定に足すのではなく、分解プロセスそのものに織り込む点です。論文の言葉では “we directly inject the contextual information into the decomposition process”。式(2)は「$x_t$ から正常パターンを引いた後に文脈で説明できる分を足し引きし、最後にウェーブレットでノイズを落とす」という意味を端的に表していますが、公式実装では $\eta_t$ を埋め込み枝に通してから時系列に連結(concat)する形で注入します(後述「実装が明かす真のアーキテクチャ」)。いずれにせよ、この一連で「文脈で説明できる変動」を残差から消し去る、という狙いは同じです。
文脈ベクトル $\eta_t$
文脈ベクトル $\eta_t$ は、その時点が「どんな状況か」を表すメタデータの束です。原論文では次のように定義されます。
$$ \begin{equation} \eta_t = \big[\, Z(t)\,;\, \mathbf{1}(t \in H) \,\big] \end{equation} $$
- $Z(t)$: 時刻まわりの季節情報
- $\mathbf{1}(t \in H)$: その時点 $t$ が祝日リスト $H$ に含まれるかを表す指示関数(0/1)
公式実装(utils/data_loader.py の add_temporal_info)を読むと、$\eta_t$ の中身がより具体的に分かります。実際に組み立てられる成分は次の通りです。
- 時刻の周期エンコード: $\mathrm{hour\_sin}=\sin(2\pi\,h/24)$、$\mathrm{hour\_cos}=\cos(2\pi\,h/24)$。生の時刻 $h$ を円周上に置き、23時と0時が隣り合うようにする定石です(月・年・曜日のsin/cosはコード上ではコメントアウトされており、既定では使われません)。
- 週末フラグ: 曜日が土日(weekday ∈ {5, 6})なら1、それ以外は0
- 祝日フラグ:
holidaysライブラリで生成した祝日リストに日付が含まれるか(韓国/米国の暦をデータセットごとに使い分け) - 祝日前日フラグ(
previous_holiday): 祝日の前日かどうか。連休前の需要増のような「祝日そのものではないが祝日に引きずられる変動」を捉える成分です
したがって $\eta_t$ の次元は 5(hour_sin, hour_cos, is_weekend, holiday, previous_holiday)です。論文の $Z(t)$ は実装上では hour_sin / hour_cos が担い、$\mathbf{1}(t \in H)$ は holiday フラグが担います。月・年・曜日 sin/cos はコード中にコメントアウト済みで有効ではありません。
コンテキスト注入の実装形式
「残差式(式2)の加算」という論文の表現は概念的な説明で、実装における文脈注入の経路は2通り存在します。
- 分解フェーズ(
decomposition=Trueのパス): 文脈は $\lambda_t \cdot \eta_t$ として乗算的に残差にかかります。具体的にはlambda_tを実装内ではholiday + is_weekend(0/1/2 のスカラー)に置き換え、weight(ハイパーパラメータ、既定 $-0.7$)と掛け合わせて使います。
$$ r_t = \Psi\!\left(\bigl(x_t – f_\theta(\tau_t + s_t)\bigr) \times (1 + \mathrm{weight} \times \lambda_t)\right) $$
ここで $\lambda_t = \mathbf{1}(t \in H) + \mathbf{1}(t \in \mathrm{Weekend})$ で、値域は $\{0, 1, 2\}$ です。
- 検知フェーズ(
temporal=Trueのパス): 文脈 $\eta_t$(5次元)は埋め込み枝 $\mathrm{Dense}(32) \to \mathrm{Dense}(4) \to \mathrm{Flatten} \to \mathrm{Dense}(W \times M) \to \mathrm{Reshape}(W, M)$ を通って時系列入力と concat されてからオートエンコーダに入ります。
論文の記述と実装の対応関係を整理すると下表の通りです。
論文式と実装の対応表
| 論文表記 | 実装上の変数 | 値域・型 |
|---|---|---|
| $Z(t)$ | hour_sin, hour_cos |
$[-1, 1]$、連続値 |
| $\mathbf{1}(t \in H)$ | holiday, previous_holiday |
$\{0, 1\}$、バイナリ |
| $\eta_t \in \mathbb{R}^5$ | temporal_input(5次元) |
temporal モード時 |
| $\lambda_t$(乗算スカラー) | holiday + is_weekend(値域 $\{0,1,2\}$) |
decomposition モード時 |
weight($\lambda$のスケーラ) |
lamda_t引数 |
既定値 $-0.7$ |

上図は、本記事の実装で使う文脈ベクトルの例です。時刻を周期的に表す成分(sin/cos)と、週末・祝日のフラグを並べています。これらが「いまはこういう状況だ」という情報を分解に与え、$\lambda_t\eta_t$ の項を通じて「その状況なら起きて当然の変動」を残差から差し引くわけです。
初期分解の中身
実際にSTLで分解すると、系列は次のように3成分へ分かれます。

上から元系列・トレンド・季節性・残差です。トレンドは大局的な水準、季節性は日内の繰り返しを捉えています。問題は最下段の残差で、ここに祝日の正常変動がまだ残っています。これを文脈項で消すのがTime-CADの仕事です。
文脈項の効果
文脈項を入れる前後で、残差がどう変わるかを見ましょう。

上段(w/o CAD)は文脈項なしの残差で、祝日帯(橙)に大きな山が残っています。下段(w/ CAD)は文脈項を入れた残差で、祝日帯の山が抑えられ、ベースラインに近い平坦さに戻っています。これにより「祝日だから増えた」という正常変動は残差から消え、後段の検知器が祝日を異常と取り違えなくなります。
ここまでで、文脈を織り込んだクリーンな残差が手に入りました。次は、これを使ってどう学習し、どう異常を判定するかです。
学習:2つの再構成損失
Time-CADは2つのオートエンコーダを、いずれも平均二乗誤差(MSE)で学習します。
まず Phase 1 の浄化ネットワーク $f_\theta$ は、正常パターン $x_t^n = \tau_t + s_t$ を入力し、それ自身を再構成するよう学びます。
$$ \begin{equation} \mathcal{L}_\theta = \frac{1}{W \cdot M}\sum_{i=1}^{W}\sum_{j=1}^{M}\bigl(x_{t,j}^n – f_\theta(x_t^n)_{i,j}\bigr)^2 \end{equation} $$
これはノイズ除去オートエンコーダとして働き、STLの分解残りを吸収して頑健な正常パターンを作ります。$W$ は窓長、$M$ は変量数です(公式実装では Keras の loss="mse" に直接対応)。
次に Phase 2 の検知ネットワーク $g_\phi$ は、文脈を除いた残差 $r_t$ を再構成するよう学びます。
$$ \begin{equation} \mathcal{L}_\phi = \frac{1}{W \cdot M}\sum_{i=1}^{W}\sum_{j=1}^{M}\bigl(r_{t,j} – g_\phi(r_t)_{i,j}\bigr)^2 \end{equation} $$
注目したいのは、トレンドの平滑性や季節性の周期性に対する明示的な正則化項が無いことです。分解の制約は古典STLが受け持ち、ニューラルネット側は純粋なMSE再構成だけ。コンテキストの効果は損失項ではなく、先の残差式(2)の乗算項とウェーブレット $\Psi$ で実現されています。役割をきれいに分けた、見通しのよい設計です。
$f_\theta$(浄化GRU-AE)のアーキテクチャ詳細
公式実装(model.py の GRU_AE)によれば、$f_\theta$ のエンコーダ・デコーダ構成は次の通りです。
| 層 | ユニット数 | 補足 |
|---|---|---|
| GRU(エンコーダ1層目) | 64 | return_sequences=True |
| Dropout | 0.4 | |
| GRU(エンコーダ2層目) | 32 | ボトルネック(コンテキストベクトル) |
| RepeatVector($W$) | — | デコーダへのブリッジ |
| GRU(デコーダ1層目) | 32 | return_sequences=True |
| Dropout | 0.4 | |
| GRU(デコーダ2層目) | 64 | |
| Dense($W \times M$) + Reshape | — | 出力を入力形状に復元 |
最適化器は Adam(学習率 0.001)、最大エポック数 50、バッチサイズ 128、早期停止(patience=5、val_loss 監視)。
ここまでは式(2)を「文脈の加算項」として直感的に扱ってきました。しかし公式実装を読むと、文脈の入り方はもう少し作り込まれています。次節で、リポジトリのコードが明かす真のデータフローまで踏み込みましょう。
実装が明かす真のアーキテクチャ
式(2)の $\lambda_t\eta_t$ は「文脈で説明できる分を残差から足し引きする」という意味を端的に表した式です。一方、公式実装(kaist-dmlab/Time-CAD、Apache-2.0)では、文脈はスカラーの加算ではなく専用の埋め込み枝を通って時系列に合流します。ここを押さえると、Time-CADが単なる「残差から定数を引く」手法ではないことが見えてきます。
2つの入力、2本の枝
検知器(*_Temporal モデル)は2入力を取ります。一方は再構成したい時系列窓 ts_input(分解を使う設定では残差 $r_t$ の窓)、もう一方は文脈ベクトルの窓 temporal_input($\eta_t$ を窓化したもの)です。文脈側はまず小さな埋め込みネットワークを通ります(GRU版の例)。
# 文脈(temporal)情報の埋め込み枝
x = Dense(32, activation='relu')(temporal_input) # 文脈ベクトルを32次元へ
x = Dense(4, activation='relu')(x) # 4次元にボトルネック圧縮
x = Flatten()(x)
x = Dense(W * D)(x) # 窓長W×次元D に整形
temporal_output = Reshape((W, D))(x) # 時系列と同じ形
# 時系列(残差)と文脈埋め込みをチャネル方向に連結してからAEへ
concat = concatenate([ts_input, temporal_output], axis=-1)
h = GRU(64, return_sequences=True)(concat)
h = GRU(32)(h)
h = RepeatVector(W)(h)
h = GRU(32, return_sequences=True)(h)
h = GRU(64)(h)
rec = Reshape((W, D))(Dense(W * D)(h)) # 残差の再構成
ポイントは concatenate([ts_input, temporal_output], axis=-1) の一行です。文脈は入力チャネルとしてオートエンコーダに与えられ、エンコーダ・デコーダが「いまどんな状況か」を見ながら残差を再構成します。式(2)が示す加算的な直感は正しいのですが、実体は「文脈を条件として与えた条件付き再構成」——つまり文脈はネットワークの入力に連結(concat)される条件信号なのです。
連結後の入力次元は $W \times (M + M) = W \times 2M$ です。埋め込み枝が Dense(4) という極端なボトルネックを挟んだあと Dense(W × M) で時系列と同じ形 $(W, M)$ に展開し、元の残差 $(W, M)$ と concat しているため、GRUの実際の入力特徴量次元は $2M$ になります。
本記事の合成実験では、この条件付けを「文脈で説明できる残差成分を最小二乗で引く」線形版に簡約していますが、原実装は埋め込み枝で非線形に文脈を取り込む点が違います。埋め込み枝の Dense(4) ボトルネックは、文脈情報を少数の潜在因子に圧縮してから時系列形へ展開するためです。生の祝日フラグや時刻sin/cosを丸ごと足し込むのではなく、「効く文脈の組み合わせ」を学習で抽出してから再構成に効かせる、という設計意図が読み取れます。
2つのパスの位置づけ整理
Time-CAD は decomposition=True(分解パス)と temporal=True(文脈AEパス)を組み合わせて使います。
- 分解パス単独(
decomposition=True, temporal=False): $r_t$ は乗算係数で文脈補正し、普通のAE $g_\phi$(文脈なし)で再構成。 - 分解+文脈AEパス(
decomposition=True, temporal=True): $r_t$ の乗算補正に加えて、$g_\phi$ が $\eta_t$ も concat 入力として受け取る。論文のTime-CADはこのフルバージョンに相当。 - 文脈AE単独(
decomposition=False, temporal=True): 分解なし、生系列を文脈条件付きAEで再構成するベースライン相当。
λ(寄与度)とウェーブレット Ψ の正体
式(2)の $\lambda_t$ は、実装では lamda_t(temporal auxiliary information の重み)というハイパーパラメータに対応し、既定値は $-0.7$ です。$\lambda$ が負値も取りうるのは、$\lambda_t\eta_t$ が「説明できる変動を打ち消す」向きに働くことを思えば自然です。文脈で説明できる分だけ残差を引き戻すので、符号と大きさで「どれだけ文脈に従わせるか」を調整します。
ウェーブレット $\Psi$ の中身も明快で、Haar基底による離散ウェーブレット変換(DWT)+ソフト閾値処理です。
def _wavelet(signal):
cA, cD = pywt.dwt(signal, "haar") # 近似係数cA・詳細係数cD に分解
cat = pywt.threshold(cA, np.std(cA), "soft") # 標準偏差を閾値にソフト縮小
cdt = pywt.threshold(cD, np.std(cD), "soft")
return pywt.idwt(cat, cdt, "haar") # 逆変換で再合成
詳細係数 $cD$(細かい揺らぎ)と近似係数 $cA$ をともに各係数の標準偏差でソフト閾値にかけ、逆変換で戻します。これにより残差に乗った微小ノイズが落ち、本物の異常スパイクだけが際立つ——式(2)の $\Psi(\cdot)$ が担うのはこの「残差のノイズ取り」です。
ウェーブレット適用回数の実装詳細
実装では _wavelet を 2回固定呼び出したあと、さらに wavelet_num(デフォルト 3)回繰り返します(合計 $2 + \text{wavelet\_num}$ 回)。デフォルトでは合計5回適用されます。これは公式コードで次のように書かれています(Samsung データの例):
train_resid_wav = _wavelet(train_resid) # 1回目(固定)
train_resid_wavelet = _wavelet(train_resid_wav) # 2回目(固定)
for _ in range(wavelet_num): # さらに wavelet_num 回
train_resid_wavelet = _wavelet(train_resid_wavelet)
この多段適用が「周期性ノイズをほぼ除去して、鋭い異常スパイクだけを際立たせる」ための設計です。
STL初期分解の設定
Phase 1 の初期分解は、自前実装ではなく statsmodels の STL をそのまま使います。
stl = STL(series, seasonal=7, period=6*24*7) # 10分粒度 → 週周期(6*24*7)
res = stl.fit()
trend, seasonal, resid = res.trend, res.seasonal, res.resid
period=6*24*7 は「10分粒度で1週間」、すなわち週単位の季節性を抽出する設定です(Samsung/RCSデータセット: 10分間隔, 窓長 $W=36$=6時間)。他データセットでは窓長 $W=60$(Energy: 1分粒度=1時間, KPI: 1分粒度=1時間, IoT系: 1秒粒度=1分)です。曜日パターンのような規則的周期はここでSTLが吸収し、残った $r_t$ に対して文脈枝が「祝日・週末・祝日前日」という不規則な暦の効果を引き受ける——という二段構えになっています。
データセット別の窓長まとめ
| データセット | 粒度 | 窓長 $W$ | 窓の時間幅 |
|---|---|---|---|
| Samsung (RCS) | 10分 | 36 | 6時間 |
| Energy | 1分 | 60 | 1時間 |
| KPI | 60秒 | 60 | 1時間 |
| IoT-Fridge | 1秒 | 60 | 1分 |
| IoT-Modbus | 1秒 | 60 | 1分 |
いずれも stride=1(1ステップずつスライド)で学習・評価データを窓化します。入力 $X$ は正規化後に窓化され、形状は $(N_\mathrm{windows}, W, M)$ です。MinMaxScaler(値域 $[0,1]$)で正規化してから窓化します。
論文が示す定性的証拠
この「STLの残差にはまだ説明できる変動が残り、deep分解+文脈がそれを除く」という主張を、論文はRCSデータの実残差で可視化しています。まず生の時系列です。

出典: Nam et al., “Context-Aware Deep Time-Series Decomposition for Anomaly Detection in Businesses,” ECML PKDD 2023, 公式リポジトリ images/ より(Apache-2.0)
8次元のRCS KPIが規則的な日内周期を描き、下端の赤帯が真の異常区間です。異常は系列全体から見ればごく一部で、しかも振幅だけでは正常な日内変動に紛れています。次に、STLだけで分解した残差を見ます。

出典: Nam et al., ECML PKDD 2023, 公式リポジトリ images/ より(Apache-2.0)
STL残差は0付近に均されてはいるものの、9月中旬(09-20前後)に赤帯の無い領域でも大きなうねりが残っています。これは祝日・イベント由来の正常変動で、ここを検知器に渡すと誤検知の温床になります。最後に、deepNN分解の有無で残差を比べたのが次の図です。

出典: Nam et al., ECML PKDD 2023, 公式リポジトリ images/ より(上=deepNNなし残差、下=Time-CADの残差。Apache-2.0)
上段(deepNNなし)では赤帯の無い区間にも残差のうねりが点在しますが、下段(Time-CAD)では同じ区間がほぼ平坦に潰れ、残るのは赤帯付近の鋭いスパイクだけになっています。文脈認識ディープ分解が「説明できる変動」を残差から消し、検知器に渡る信号を本物の異常に絞り込んでいる様子が、実データではっきり確認できます。本記事の合成実験(後述)が再現するのも、まさにこの構図です。
全期間での3段変化:STL → deepNNなし → Time-CAD
上の3図はズームイン版(異常区間周辺の拡大)でした。次に、テストデータ全期間を並べた3段比較を見ておきましょう。

出典: Nam et al., “Context-Aware Deep Time-Series Decomposition for Anomaly Detection in Businesses,” ECML PKDD 2023, 公式リポジトリ images/ (Apache-2.0). 上: STL残差(文脈・深層なし), 中: deepNN分解後残差(文脈なし), 下: Time-CAD残差(文脈あり深層分解後)
3段を左から右へ通して読むと、2つの浄化ステップがそれぞれ何を取り除いているかが見えます。
まず上段(STL残差)は、移動平均・周期平均で作られた粗い分解なので、KPIの日内変動がおおむね除かれたあとも、祝日・週末・季節的なイベント由来の大きなうねりが全体に残っています。
中段(deepNN分解後・文脈なし)になると、GRUオートエンコーダ $f_\theta$ の非線形浄化によって長周期のうねりが整理され、全体的な振幅は落ちます。しかし文脈を入れていないため、祝日帯の山は依然として残留しています。
下段(Time-CAD)では、文脈項 $\lambda_t\eta_t$ が祝日・週末由来の成分を吸収し、残差がほぼフラットに収まります。赤帯(真の異常区間)付近だけに鋭い突出が残っているのが確認でき、検知器が「本物の異常だけ」を見ている状態になっています。
全期間スパンで見ると、deepNNが担う「STLの粗さを補う広帯域浄化」と文脈項が担う「カレンダー依存変動の除去」という2役が分担して機能していることが直感的に分かります。それぞれを単独で使うより、組み合わせることで残差が安定して低く保たれる——これが TaF1 0.955 という高スコアの下地です。
役割分担が腑に落ちたところで、いよいよ異常判定に進みます。
推論:残差の再構成誤差で異常を測る
検出時は、未知系列の残差 $\hat{r}_t$ を検知器 $g_\phi$ に通し、再構成誤差を異常スコアとします。
$$ \begin{equation} A_t = \big\lVert \hat{r}_t – g_\phi(\hat{r}_t) \big\rVert_2 \end{equation} $$
スコアが大きいほど「正常な残差のパターンから外れている」=異常です。あとは閾値 $\delta$ で二値化します。
$$ \begin{equation} y_t = \begin{cases} 1 & (A_t > \delta) \\ 0 & (\text{otherwise}) \end{cases} \end{equation} $$

上図は、後述する合成データでのスコア時系列です。赤帯が真の異常窓で、そこでスコアがしっかり跳ね上がり、閾値 $\delta$(赤破線)がそれらを拾える高さに引かれています。
閾値の決め方について、原論文は評価上の工夫をしています。テストデータのスコアの最小〜最大を1,001候補(最小値 + ステップサイズ $\times$ 1,000 の等差数列)に分割して全て試し、TaF1を最大化する閾値での性能(best score)を報告します。
実装上の厳密な手順は以下の通りです(utils/evaluator.py の _enumerate_thresholds)。
$$ \delta_k = \min(A) + k \times \frac{\max(A) – \min(A)}{1000}, \quad k = 0, 1, \ldots, 1000 $$
ここで $A$ はテストデータ全窓のスコア列です。各 $\delta_k$ に対して TaF1 を計算し、最大を与える $k^* = \mathrm{arg\,max}_k \, \mathrm{TaF1}(\delta_k)$ を採用します。これは「特定の最良閾値での性能」であり、いわばオラクル的な評価です(閾値を自動決定する実運用とは別物)。ベンチマーク比較を読むときはこの点に留意してください。
また、異常スコアの計算式は各窓ごとのMSEです。上の $A_t = \lVert \hat{r}_t – g_\phi(\hat{r}_t)\rVert_2$ は概念を $L_2$ ノルム形式で示した論文式ですが、実装では窓内の各ステップ・各変量で平均した MSE が使われます(scoring='square_mean')。
$$ A_t = \frac{1}{W \cdot M}\sum_{i=1}^{W}\sum_{j=1}^{M}\bigl(\hat{r}_{t,i,j} – g_\phi(\hat{r}_t)_{i,j}\bigr)^2 $$
ここで $i$ は窓内の時刻インデックス($1 \leq i \leq W$)、$j$ は変量インデックス($1 \leq j \leq M$)です。
理論が揃ったので、論文の実験をサーベイとしてまとめます。
サーベイ①:評価データセットと指標
Time-CADは4つのビジネス系ベンチマーク(計7エンティティ)で評価されました。
| データセット | エンティティ×次元 | 訓練長 | テスト長 | 異常数(率) | 内容 |
|---|---|---|---|---|---|
| RCS(GitHub名 Samsung) | 3×8 | 21,600 | 13,302 | 160 (0.46%) | 通信サービス(RCS)のKPI、本論文の私有データ |
| KPI | 1×1 | 66,822 | 44,548 | 1,102 (0.99%) | IT運用の単変量KPI(NetMan AIOps) |
| Energy | 1×1 | 41,654 | 5,349 | 2,772 (5.90%) | エネルギー消費(AI-Hub) |
| IoT-Modbus | 1×4 | 15,332 | 35,774 | 16,106 (31.51%) | 産業制御系(Modbus)の通信(ToN-IoT) |
数値は公式リポジトリ(kaist-dmlab/Time-CAD)の Public Data Sets 表に一致させています。RCSとEnergyは韓国の祝日、KPIとIoT-Modbusは米国の祝日を文脈に使う実装になっており、各ドメインの暦が文脈として効くことが分かります。
評価指標は単純なpoint-wise F1ではなく、時系列を意識した TaPR / TaF1(time-series aware precision-recall, Hwang et al., CIKM 2019)です。連続した異常区間を1イベントとして扱うため、時系列異常検知の実態に即しています。各モデルは3回実行の平均で比較されます。
データセットを見て分かるのは、対象がビジネス・IT・エネルギー・産業IoTだという点です。原論文に衛星テレメトリは含まれません(コンテキスト分解の考え方は軌道位相や運用モードを文脈とすれば衛星にも応用可能ですが、それは原論文のスコープ外の発展です)。
サーベイ②:検出性能(TaF1)の比較
代表的な手法との比較が次表です(平均 best TaF1、7エンティティ平均)。Time-CADは分解系に分類され、非分解系(LOF/OCSVM/AE/OmniAnomaly/TranAD など)とも比較されています。
| 手法 | 種別 | 平均 best TaF1 | 順位 |
|---|---|---|---|
| LOF | 非分解 | 0.555 | 12 |
| OCSVM | 非分解 | 0.656 | 10 |
| AE | 非分解 | 0.739 | 8 |
| OmniAnomaly | 非分解 | 0.774 | 4 |
| TranAD | 非分解 | 0.773 | 5 |
| MS-RNN | 非分解 | 0.773 | 6 |
| TFAD | 分解 | 0.798 | 3 |
| AE-STL | 分解 | 0.915 | 2 |
| Time-CAD | 分解 | 0.955 | 1 |

Time-CADは平均TaF1 0.955で全11手法中の首位です。論文はこの結果を「平均で全ベースラインを最大(up to)46%上回る」と表現しています。ここは誤読されやすいので注意してください。「46%」は改善幅の平均ではなく、特定のベースライン・データセットに対する最大の上回り幅です。実際、最下位のLOF(0.555)に対しては約1.7倍ですが、非分解系最強のOmniAnomaly(0.774)に対しては約+23%で、46%が指す対象は論文中で明示されていません。
表からもう一つ読み取れるのは、上位2つ(Time-CAD, AE-STL)がともに分解系だという事実です。分解して残差で見るアプローチが、生の系列を直接モデル化するより有利——という分解ベースの優位を裏づけています。なお、USAD や Anomaly Transformer はこの比較表には含まれず、引用のみである点も補足しておきます。
サーベイ③:コンテキスト統合の効果(アブレーション)
Time-CADの主張は「コンテキスト統合が効く」です。これを直接検証したのが、文脈認識分解(CAD)を入れた場合(w/ CAD)と外した場合(w/o CAD)の比較です。

差は劇的です。とくにRCSデータセットで顕著で、RCS-1ではCADありの0.944に対し、CADなしは0.633まで落ちます(RCS-3も0.944→0.622)。RCSは祝日やイベントでKPIが大きく動く通信サービスのデータなので、コンテキスト無しだとそれらの正常変動を片端から誤検知してしまうわけです。一方、コンテキストの影響が小さいKPIやEnergyでは差は小さく(KPIは0.937→0.905)、「文脈が効くデータほどCADが効く」という納得のいく傾向が出ています。
さらに論文は、このCADを他の検知器にも付けられることを示しています。MS-RNNやOmniAnomalyの前段にCADを噛ませると、いずれも性能が向上しました(例: OmniAnomaly on RCS-1 は w/o CAD 0.503 → w/ CAD 0.939)。CADは特定モデルに縛られないモジュールであり、ここがTime-CADの実用上の強みです。
数式とサーベイを踏まえて、実際に手を動かしましょう。
Pythonでのスクラッチ実装
ここからは簡易版Time-CADをPyTorchで実装し、合成データで再現します(原論文の公式実装はTensorFlowですが、本記事はPyTorchで統一します)。確かめたいのは1点、コンテキスト統合(CAD)が誤検知を減らし、検出性能を上げるかです。
コンテキスト依存の正常変動を持つ合成データ
Time-CADの強みが出るのは「コンテキストで正常パターンが変わる」データです。そこで、平日昼にピークを持つ日内季節性に、週末はレベル増・祝日はさらに急増という文脈依存の変動を持たせます。異常は「平日・非祝日なのに急変」、つまり文脈で説明できない逸脱として埋め込みます。
import numpy as np
import torch
import torch.nn as nn
from sklearn.metrics import roc_auc_score, average_precision_score, precision_recall_curve
HOURS_PER_DAY = 24
def make_calendar(n_days, seed):
rng = np.random.default_rng(seed)
dow = np.repeat(np.arange(n_days) % 7, HOURS_PER_DAY) # 0=月 ... 6=日
hour = np.tile(np.arange(HOURS_PER_DAY), n_days)
weekend = ((dow == 5) | (dow == 6)).astype(float)
hol_day = (rng.random(n_days) < 0.10).astype(float) # 各日10%で祝日
holiday = np.repeat(hol_day, HOURS_PER_DAY)
return dow, hour, weekend, holiday
def context_features(hour, weekend, holiday):
# 文脈ベクトル eta_t = [hour(sin,cos), weekend, holiday]
hs = np.sin(2 * np.pi * hour / HOURS_PER_DAY)
hc = np.cos(2 * np.pi * hour / HOURS_PER_DAY)
return np.stack([hs, hc, weekend, holiday], axis=1)
def normal_pattern(hour, weekend, holiday):
n = len(hour); t = np.arange(n)
trend = 1.0 + 0.0008 * t # 緩やかな増加
daily = 1.0 + 0.6 * np.sin(2 * np.pi * (hour - 8) / HOURS_PER_DAY)
level = 1.0 + 0.4 * weekend + 1.3 * holiday # 週末/祝日でレベル増(正常)
return trend * daily * level
def gen_series(n_days, anomalies, seed):
dow, hour, weekend, holiday = make_calendar(n_days, seed)
rng = np.random.default_rng(seed + 100)
x = normal_pattern(hour, weekend, holiday) + 0.08 * rng.standard_normal(len(hour))
lab = np.zeros(len(x), int)
if anomalies: # 平日・非祝日に異常を仕込む
wd = np.where((weekend == 0) & (holiday == 0))[0]
wd = wd[(wd > HOURS_PER_DAY * 3) & (wd < len(x) - 8)]
starts = rng.choice(wd, size=6, replace=False)
for s in starts[:4]: # レベルシフト異常
x[s:s+6] += 1.4; lab[s:s+6] = 1
for s in starts[4:]: # 点的スパイク異常
x[s] += 2.2; lab[s] = 1
return x, lab, context_features(hour, weekend, holiday), (dow, hour, weekend, holiday)

生成した系列がこちらです。橙の帯が祝日(正常な急増)、赤の帯が真の異常(平日の説明できない変動)です。見た目には祝日の急増のほうが大きく、純粋な大きさだけでは異常と区別がつきません。文脈を使わなければ、祝日を異常と誤検知してしまう——この状況こそTime-CADの狙いどころです。
Phase 1:分解とコンテキスト項
古典STL相当の分解(移動平均トレンド+周期平均季節)を行い、残差から「文脈で説明できる成分」を最小二乗で推定して差し引きます。これが式(2)の $\lambda_t\eta_t$ に相当します。
def classical_decompose(x, period=HOURS_PER_DAY, tw=2*HOURS_PER_DAY+1):
n = len(x); pad = tw // 2
trend = np.convolve(np.pad(x, pad, mode="reflect"), np.ones(tw)/tw, mode="valid")[:n]
detr = x - trend
season = np.zeros(n)
for p in range(period): # 時刻ごとの平均=季節性
season[np.arange(n) % period == p] = detr[np.arange(n) % period == p].mean()
return trend, season, x - trend - season
def context_explained(resid, ctx, lam=1.0):
# 文脈で説明できる残差成分を最小二乗で推定 (lambda_t * eta_t に相当)
A = np.concatenate([ctx, np.ones((len(ctx), 1))], axis=1)
beta, *_ = np.linalg.lstsq(A, resid, rcond=None)
return lam * (A @ beta)
context_explained は、残差を文脈ベクトルで回帰し、「祝日・週末・時刻で説明できる分」を取り出します。これを残差から引けば(w/ CAD)、文脈由来の変動が消えます。引かなければ(w/o CAD)、従来分解のままです。この一行の有無が性能を分けます。
Phase 2:残差を再構成するGRUオートエンコーダ
検知器 $g_\phi$ は、残差の窓を再構成する双方向GRUオートエンコーダです。正常な残差で学習し、再構成誤差を異常スコアにします。
W = 24
class GRUAE(nn.Module):
def __init__(self, H=32, bidir=True):
super().__init__()
self.enc = nn.GRU(1, H, batch_first=True, bidirectional=bidir)
self.dec = nn.GRU((2 if bidir else 1) * H, H, batch_first=True)
self.out = nn.Linear(H, 1)
def forward(self, x):
h, _ = self.enc(x)
d, _ = self.dec(h)
return self.out(d)
def windows(r, w=W):
return np.stack([r[i:i+w] for i in range(len(r)-w+1)]).astype(np.float32)
def train_detector(r_train, seed=0, epochs=40):
torch.manual_seed(seed)
Wt = torch.tensor(windows(r_train))[..., None]
m = GRUAE(); opt = torch.optim.Adam(m.parameters(), lr=3e-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 detector_score(m, r_test):
Wt = torch.tensor(windows(r_test))[..., None]
return ((m(Wt) - Wt) ** 2).mean(dim=(1, 2)).numpy()
w/CAD と w/o CAD を比較する
仕上げに、コンテキスト統合の有無で性能を比べます。
np.random.seed(0); torch.manual_seed(0)
x_tr, lab_tr, ctx_tr, _ = gen_series(90, anomalies=False, seed=1)
x_te, lab_te, ctx_te, cal_te = gen_series(60, anomalies=True, seed=7)
mu, sd = x_tr.mean(), x_tr.std()
x_trn, x_ten = (x_tr - mu) / sd, (x_te - mu) / sd
_, _, r_tr0 = classical_decompose(x_trn)
_, _, r_te0 = classical_decompose(x_ten)
# w/o CAD: 文脈項なし / w/ CAD: 文脈で説明できる分を除去
r_tr_cad = r_tr0 - context_explained(r_tr0, ctx_tr)
r_te_cad = r_te0 - context_explained(r_te0, ctx_te)
yte = np.array([lab_te[i:i+W].max() for i in range(len(lab_te)-W+1)])
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, r_tr, r_te in [("w/o CAD", r_tr0, r_te0), ("w/ CAD", r_tr_cad, r_te_cad)]:
m = train_detector(r_tr, seed=0)
s = detector_score(m, r_te)
print("%-7s AUC=%.3f AP=%.3f F1=%.3f" % (
name, roc_auc_score(yte, s), average_precision_score(yte, s), best_f1(yte, s)))
実行結果は次のとおりです。
| 構成 | ROC-AUC | PR-AUC(AP) | 最良F1 |
|---|---|---|---|
| w/o CAD(文脈なし) | 0.859 | 0.324 | 0.512 |
| w/ CAD(文脈あり) | 0.981 | 0.902 | 0.821 |
コンテキスト統合だけで、AUCは0.859→0.981、PR-AUCは0.324→0.902、F1は0.512→0.821と大きく改善しました。とりわけPR-AUCの伸び(0.32→0.90)が劇的です。異常率の低い設定ではPR-AUCが実力を映すので、「誤検知が激減した」ことを意味します。
なぜこうなるのか。検知器に入る残差を見ると一目瞭然です。

灰色(w/o CAD)は祝日帯(橙)で大きな山が立ち、検知器はこれを異常と誤認します。青(w/ CAD)では文脈項がその山を消しているため、検知器は祝日に反応せず、平日の本当の異常だけに集中できます。ROC/PR曲線でも差は明確です。

w/CAD(青)がw/o CAD(灰)を全域で上回っています。論文がRCSデータで示したアブレーション(0.633→0.944)と同じ構図——文脈が効くデータでは、コンテキスト統合が誤検知を抑えて性能を底上げする——が、この小さな合成実験でも再現できました。
Time-CADの強みと限界
最後に、手法の立ち位置を整理しておきます。
強み
- 偽陽性の大幅削減: コンテキストで説明できる変動を分解で除くため、祝日・イベント由来の誤検知を抑えられる
- モデル非依存(model-agnostic): CADは前段モジュールとして、MS-RNNやOmniAnomalyなど他の検知器にも付けて性能を上げられる
- 解釈性: 「この変動は文脈Xで説明できる/できない」と分解結果で示せる
- 教師なし: 正常データだけで学習でき、異常ラベルが要らない
限界
- 加法的構造の仮定: 「トレンド+季節性+残差」の足し算分解が前提で、強い非線形・乗法的な相互作用には対応しきれない場合がある
- 文脈の事前用意が必要: 祝日・イベントといったコンテキストをカレンダー等から与える必要がある。良質な文脈が手に入らないドメインでは効果が限定的
- 概念ドリフトへの対応は弱い: 季節・祝日のような周期的文脈は扱えるが、機器が年単位で劣化するような長期変化には明示的に対応していない
コンテキスト統合の3流儀 — Time-CAD・TAAD・TICC
Time-CADを理解したところで、「コンテキストをどう扱うか」という観点で近縁手法と比較しておきます。コンテキスト考慮型の時系列異常検知には、大きく3つの流儀があります。
流儀①:コンテキストを「与える」(Time-CAD)
Time-CADは、カレンダーや外部データから事前に取り出せるコンテキスト(曜日・祝日・時刻)を分解プロセスに注入します。コンテキストは人が設計した特徴量 $\eta_t$ として分解式に直接現れ、モデルはその情報を使って「説明できる変動」を取り除きます。
前提として、コンテキストが何であるかを人間が知っていること、そしてそれをカレンダーや記録から取り出せることが必要です。ビジネスKPI・IT運用・エネルギー監視のように、「祝日のパターン変動」が明確なドメインで真価を発揮します。
流儀②:コンテキストを「分離する」(TAAD)
TAAD(Continuous Test-Time Domain Adaptation for Fault Detection, PHME 2024)は、コンテキストをあらかじめ与えるのでなく、データから内在的に分離する手法です。人が制御できる設定値(運転条件 = System Parameters)と、それへの応答として測定される計測値(Measurements)を分けてモデル化し、「設定値の変化で説明できる計測値変化は正常、説明できない変化は異常」と判断します。
さらにバッチ正規化(BN)の統計量のみをEMAで更新するTest-Time Adaptation(TTA)を使い、長期の概念ドリフトにもオンラインで追従します。Time-CADがカレンダーという外部知識に依存するのと対照的に、TADDはデータ内部のSP/計測値という構造的分離でコンテキストを表現します。

流儀③:コンテキストを「発見する」(TICC)
TICC(Toeplitz Inverse Covariance-based Clustering, KDD 2017)は、コンテキストそのものを外部から与えも、構造的に分離もしません。多変量時系列を「変量間の相関構造(逆共分散行列=グラフィカルモデル)」でクラスタリングし、どんな動作モード(レジーム)があるかを教師なしで発見します。
発見された各レジームが「動作モード=暗黙のコンテキスト」に相当し、レジーム別の正常モデルを使えば文脈依存の異常検知ができます。Time-CADが「コンテキストは既知」前提で動くのに対し、TICCは「コンテキスト自体を学ぶ」ところから始まります。

3流儀の使い分け
| 流儀 | 代表手法 | コンテキストの扱い | 前提 |
|---|---|---|---|
| 与える | Time-CAD | カレンダー等から設計 | 文脈の種類が既知 |
| 分離する | TAAD | データ内のSP/計測値を分離 | 入力変数が役割別に分類できる |
| 発見する | TICC | 逆共分散クラスタリング | 何も前提しない |
3流儀は排他的ではなく、組み合わせも有効です。TICCでレジームを発見して「コンテキストのラベル」にし、Time-CAD流の分解に注入する——というハイブリッドは自然な拡張方向です。
関係ベースの異常検知(変量間の相関構造の崩れを検出する)については、以下も参照してください。

まとめ
本記事では、Time-CADを理論から実装、論文サーベイまで通して解説しました。
- 核心: 時系列分解にコンテキスト(曜日・祝日・時刻など)を組み込み、「説明できる変動」と「説明できない変動」を分離。異常判定は残差(説明できない部分)だけで行う
- 2フェーズ: ①STL+GRU-AE浄化+文脈項で残差 $r_t=\Psi\!\bigl((x_t-f_\theta(\tau_t+s_t))\times(1+\mathrm{weight}\times\lambda_t)\bigr)$ を作る → ②残差をGRU-AE $g_\phi$ で再構成し、窓ごとMSEを異常スコア $A_t$ とする
- 入出力次元: 入力 $X \in \mathbb{R}^{N \times M}$、窓 $W_t \in \mathbb{R}^{W \times M}$($W$は36〜60)、文脈ベクトル $\eta_t \in \mathbb{R}^5$(hour_sin/cos + weekend + holiday + prev_holiday)、スカラー $\lambda_t \in \{0,1,2\}$、
weight(既定 $-0.7$) - 学習: 2つのMSE再構成損失(Keras
loss="mse")のみ。最大50エポック、Adam(lr=0.001)、早期停止(patience=5) - ウェーブレット: Haar DWT+両係数ソフト閾値、$2+\mathrm{wavelet\_num}$ 回適用(デフォルト計5回)
- 閾値: テストスコアを1001等分した $\delta_k = \min(A) + k \times (\max(A)-\min(A))/1000$($k=0,\ldots,1000$)のうちTaF1最大を採用するオラクル的評価
- 実験: 4ベンチマーク(RCS/KPI/Energy/IoT-Modbus)・TaF1で全ベースライン中の首位(平均0.955)。アブレーションでコンテキスト統合の寄与が大きいことを確認(RCSで0.62→0.94級)
- 再現: 文脈依存の正常変動を持つ合成データで、CADの有無がAUC 0.859→0.981、PR-AUC 0.324→0.902 を分けることを確認
「値の大きさ」ではなく「文脈で説明できるか」で異常を測る——この視点は、運用環境に応じて正常が変わるあらゆる時系列(IT・エネルギー・産業IoT)に通じる、汎用的な発想です。
次のステップとして、以下も参考にしてください。







参考文献
- Y. Nam, P. Trirat, T. Kim, Y. Lee, J.-G. Lee. “Context-Aware Deep Time-Series Decomposition for Anomaly Detection in Businesses.” Machine Learning and Knowledge Discovery in Databases: Applied Data Science and Demo Track (ECML PKDD 2023), LNCS vol. 14174, pp. 330–345, 2023. DOI: 10.1007/978-3-031-43427-3_20. 公式コード: https://github.com/kaist-dmlab/Time-CAD (Apache License 2.0)。本記事の引用図(生時系列・STL残差・deep残差比較・全期間3段比較)は同リポジトリ
images/所収のものを出典明記のうえ再配布しています。アーキテクチャ・$\eta_t$・$\lambda_t$・ウェーブレット・STL設定の記述は同リポジトリのmodel.py/train.py/decomposition.py/utils/data_loader.pyを精読して確認しました。 - R. B. Cleveland, W. S. Cleveland, J. E. McRae, I. Terpenning. “STL: A Seasonal-Trend Decomposition Procedure Based on Loess.” Journal of Official Statistics, 6(1), 1990.
- W.-S. Hwang et al. “Time-Series Aware Precision and Recall for Anomaly Detection.” CIKM, 2019.
- Y. Su et al. “Robust Anomaly Detection for Multivariate Time Series through Stochastic Recurrent Neural Network.” KDD ’19, 2019.
- D. Hallac, S. Vare, S. Boyd, J. Leskovec. “Toeplitz Inverse Covariance-Based Clustering of Multivariate Time Series Data.” KDD, 2017.
- Y. Sun et al. “Continuous Test-time Domain Adaptation for Efficient Fault Detection under Evolving Operating Conditions.” PHME, 2024.
- J. Dai et al. “Spatial Association-Aware Anomaly Detection and Diagnosis for Multivariate Time Series.” NeurIPS, 2024.