時系列の信号デノイズ手法まとめ ― 移動平均・ウェーブレット・分解・カルマン・深層を比較して組み合わせる

センサーで測った時系列、ログから集めた指標、実験データ——現実の時系列はほぼ例外なくノイズまみれです。そのまま予測モデルに入れれば精度が落ち、そのまま異常検知にかければ正常なゆらぎを異常と誤検知します。だから多くの分析は、まずノイズを除いて「本当に意味のある信号」を取り出すところから始まります。これがデノイズ(信号のノイズ除去)です。

デノイズの手法は、移動平均のような素朴なものから、ウェーブレット、状態空間、深層学習まで驚くほど多様です。しかも面白いことに、それぞれ「何をノイズとみなし、どう剥がすか」の考え方が違います。本記事では、代表的なデノイズ手法を同じ合成信号に適用して対真値MSEで実測比較し、最後に複数の手法を組み合わせることで、異常検知の前処理として強力になる様子まで見ていきます。

デノイズを理解しておくと、次のような場面で効いてきます。

  • 異常検知の前処理 — トレンドや季節やノイズを剥がし、残差で「文脈からの逸脱」を測る
  • 予測の前処理 — 滑らかな信号にしてから予測すると、ノイズへの過剰適合を防げる

本記事の内容

  • デノイズの共通の考え方(観測=信号+ノイズ)
  • 線形平滑化/ロバスト/周波数領域/ウェーブレット/分解/状態空間/深層の各手法
  • 同じ信号での対真値MSE実測比較と使い分け
  • STL+ウェーブレットを組み合わせて異常を際立たせるパイプライン

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

デノイズとは何か ― 共通の考え方

ほとんどのデノイズ手法は、観測 $x_t$ を「真の信号 $f_t$ + ノイズ $\varepsilon_t$」と捉えます。

$$ \begin{equation} x_t = f_t + \varepsilon_t \end{equation} $$

目標は、観測 $x_t$ から真の信号 $f_t$ をできるだけ正確に復元することです。手法ごとに違うのは、「信号とノイズをどう区別するか」という仮定です。「信号は滑らか(ノイズは細かく振動)」「信号は低周波(ノイズは高周波)」「信号は少数の基底で表せる(ノイズは表せない)」——この仮定の違いが、手法の個性になります。

時系列デノイズの目的:ノイズを除いて真の信号を取り出す

この図が出発点です。薄い線が観測(真の信号+ガウスノイズ)、濃い線が復元したい真の信号です。本記事ではこの信号(トレンド+周期80の季節+途中にステップ状の段差)に各手法を適用し、復元結果を真の信号と比べたMSE(平均二乗誤差)で性能を測ります。MSEが小さいほど、ノイズをうまく除けたことになります。

ここで、デノイズには避けて通れない根本的なジレンマがあります。消しすぎると信号まで削れ、控えめにするとノイズが残る——これはバイアスと分散のトレードオフです。窓を広く取って強く平滑化すれば、ランダムなノイズ(分散)はよく消えますが、山や段差といった本物の信号までならされて系統的なズレ(バイアス)が増えます。逆に弱い平滑化はノイズを残します。MSEはこのバイアスの二乗と分散の和なので、両者の釣り合う『ちょうどよい平滑度』を探すことがデノイズの本質です。どの手法も、この平滑度を決めるツマミ(窓幅・カットオフ・閾値・$q/r$ 比など)を持っています。手法ごとの違いは、結局「信号とノイズをどんな軸で区別し、どこで線を引くか」に帰着します。

まずは最も素朴な「近傍をならす」発想から見ていきましょう。

線形平滑化フィルタ ― 近傍を平均する

最も直感的なデノイズは、「近くの値を平均すれば、ランダムなノイズは打ち消し合う」という発想です。代表が移動平均、指数平滑(EMA)、そしてSavitzky-Golayフィルタです。

  • 移動平均: 幅 $w$ の窓内の単純平均。実装も理解も簡単
  • 指数平滑(EMA): $y_t = \alpha x_t + (1-\alpha)y_{t-1}$。過去を指数的に重み付け。オンライン更新できる
  • Savitzky-Golay: 窓内に低次多項式を最小二乗で当て、その値を採用。山や谷の形を保ちやすい
import numpy as np
from scipy.signal import savgol_filter

def moving_avg(x, w=11):
    return np.convolve(x, np.ones(w)/w, mode="same")

def ema(x, alpha=0.2):
    y = np.empty_like(x); y[0] = x[0]
    for i in range(1, len(x)):
        y[i] = alpha*x[i] + (1-alpha)*y[i-1]
    return y

savgol = lambda x: savgol_filter(x, 21, 3)   # 窓21・3次多項式

線形平滑化フィルタ(移動平均/指数平滑/Savitzky-Golay)

ステップ段差の周辺を拡大したものです。3手法ともノイズはならせていますが、移動平均はステップ端をぼかしてしまっているのが分かります。これは先ほどのバイアスの典型で、窓を広げるほどノイズは消えますが段差はなまります。Savitzky-Golayは窓内に多項式を当てるため、山や段差の形の保ちがよく、対真値MSEも最小クラス(0.048)でした。単純平均が「窓内を定数で近似」するのに対し、Savitzky-Golayは「多項式で近似」するぶん、曲がった信号へのバイアスが小さいのです。

一方、指数平滑(EMA)はMSEが0.440と、なんとノイズあり観測(0.366)より悪化しています。EMAは過去だけを使う因果的(オンライン)なフィルタなので、振動する信号では位相が遅れるため、遅れによる誤差がノイズ除去の効果を上回ってしまうのです。「オンラインで使える代わりに遅れる」——これがEMAの性格で、リアルタイム監視のように未来を使えない場面で価値を発揮します。事後にまとめて処理できるなら、両側を使える移動平均やSavitzky-Golayのほうが遅れません。

線形平滑には、もう一つ弱点があります。スパイク状の外れ値です。

ロバストフィルタ ― スパイクに強い

1点だけ大きく飛んだスパイク外れ値があると、平均ベースの線形平滑はその値を周囲に塗り広げてしまいます。平均は外れ値に弱いからです。ここで効くのが、平均の代わりに中央値を使うロバストフィルタです。

  • メディアンフィルタ: 窓内の中央値を採用。スパイクは中央値に影響しないので消える
  • Hampelフィルタ: 窓内の中央値とMAD(中央絶対偏差)で外れ値を判定し、外れ値だけ中央値に置換。正常点は触らない
def hampel(x, w=7, nsig=3):
    y = x.copy(); k = 1.4826
    for i in range(len(x)):
        lo, hi = max(0, i-w), min(len(x), i+w+1)
        win = x[lo:hi]; med = np.median(win)
        s = k * np.median(np.abs(win - med))     # ロバストな標準偏差推定
        if s > 0 and abs(x[i]-med) > nsig*s:
            y[i] = med                            # 外れ値だけ中央値に置換
    return y

ロバストフィルタ(Hampel)はスパイク外れ値に強い

スパイクを4つ仕込んだ信号です。移動平均(オレンジ)はスパイクの位置で山を作り、周囲を持ち上げてしまっています。1個の外れ値が窓内の平均を引っ張るからです。一方Hampelフィルタ(緑)は、スパイクだけをピンポイントで除去し、それ以外の波形には手を付けていません。中央値は、データの半分が外れ値になるまで壊れない(高いブレークダウン点を持つ)ため、少数のスパイクには微動だにしないのです。「外れ値は消したいが、正常な変動は保ちたい」ときにロバストフィルタが効くわけです。ただし、連続的に乗るガウスノイズのような「外れ値でないノイズ」を消す力は弱いので、線形平滑やウェーブレットと役割分担するのが普通です。

ここまでは時間領域の操作でした。視点を周波数に移すと、別のノイズの捉え方が見えてきます。

周波数領域 ― 高周波を切るローパスフィルタ

「信号はゆっくり変化する低周波、ノイズは細かく振動する高周波」と考えると、フーリエ変換で周波数成分に分け、高周波を捨ててから戻すことでデノイズできます。これがローパスフィルタです。

def fourier_lowpass(x, keep=0.06):
    X = np.fft.rfft(x)          # 周波数成分へ
    cut = int(len(X) * keep)
    X[cut:] = 0                 # 高周波を捨てる
    return np.fft.irfft(X, n=len(x))

周波数領域ローパスフィルタ(フーリエ)

左は振幅スペクトルです。低周波側に信号のエネルギーが集中し、高周波側にノイズが広く分布しています。カットオフ(赤破線)より高い周波数を捨てて時間領域に戻すと、右のように滑らかな信号が得られます(MSE 0.117)。カットオフ周波数が、ここでの平滑度のツマミです。低く取りすぎると信号の速い成分まで削れ、高く取りすぎるとノイズが残ります。

ただしローパスには固有の注意点があります。ステップ状の段差は多数の高周波成分の重ね合わせでできているため、高周波を切るとギブス現象(段差の前後でリンギング状に波打つ)が出やすいのです。フーリエ基底は「どこでも一様に振動するサイン波」なので、ある一点だけで起こる急変を表すのが本質的に苦手——この弱点が、次のウェーブレットを生む動機になりました。

この「段差や急変が苦手」という弱点を、見事に克服するのがウェーブレットです。

ウェーブレット縮小 ― 多重解像度で「大きい係数だけ残す」

フーリエが「波の周波数」だけで信号を見るのに対し、ウェーブレットは「どの場所に、どのスケールの変化があるか」を同時に捉えます(多重解像度解析)。デノイズの考え方はシンプルで、信号をウェーブレット係数に変換すると、信号は少数の大きな係数に、ノイズは多数の小さな係数に散らばります。だから「小さい係数を捨てて大きい係数だけ残す」とノイズが落ちます。これがウェーブレット縮小です。

小さい係数を0にし、大きい係数を少し縮めるソフト閾値処理がよく使われます。

$$ \eta_\lambda(c) = \operatorname{sign}(c)\,\max(|c| – \lambda,\ 0) $$

閾値 $\lambda$ には、ノイズ標準偏差 $\sigma$ から定める普遍閾値 $\lambda = \sigma\sqrt{2\ln N}$ が定番です($\sigma$ は最細スケール係数のMADから頑健に推定)。

import pywt

def wavelet_denoise(x, wav="db4", level=4):
    coeffs = pywt.wavedec(x, wav, level=level)
    sigma = np.median(np.abs(coeffs[-1])) / 0.6745          # MADでノイズσ推定
    uthr = sigma * np.sqrt(2*np.log(len(x)))                # 普遍閾値
    out = [coeffs[0]] + [pywt.threshold(c, uthr, "soft") for c in coeffs[1:]]
    return pywt.waverec(out, wav)[:len(x)]

ウェーブレット縮小とソフト閾値処理の概念

左は最も細かいスケールの詳細係数で、大半が閾値(赤破線)より小さい——つまりノイズです。これらを0にします。右がソフト閾値処理の関数で、閾値内(灰色帯)を0につぶし、閾値外を原点側へ縮めます。重要なのは、ウェーブレットが段差やスパイクのような局所的な変化を少数の大きな係数で表せる点です。

ウェーブレットはステップ端を保ってノイズを落とす

ステップ段差の周辺です。移動平均(オレンジ)が段差をなまらせるのに対し、ウェーブレット(緑)は段差の鋭さを保ったままノイズだけを落としています(MSE 0.074)。なぜこれができるかというと、ウェーブレットは「位置」と「スケール」の両方を持つ基底なので、段差のような局所的な急変をその場所の少数の大きな係数で表せるからです。フーリエが段差を表すのに無数のサイン波を要したのと対照的です。普遍閾値 $\sigma\sqrt{2\ln N}$ は、「$N$ 個の独立な標準正規ノイズの最大値がおよそ $\sqrt{2\ln N}$ に収まる」という事実に基づいており、ノイズ由来の係数をほぼ確実に消せるぎりぎりの大きさになっています。エッジや急変・スパイク的な構造を残したいデータで、ウェーブレットが選ばれる理由です。

次は、時系列ならではの構造(トレンド・季節)を積極的に使う方法です。

分解ベース ― STLでトレンドと季節を剥がす

時系列に明確なトレンドや季節性があるなら、それらを構造として推定して取り除くのが自然です。STL分解は、時系列をトレンド $T_t$・季節 $S_t$・残差 $R_t$ に分け、$T_t + S_t$ を「滑らかな信号」、$R_t$ を「ノイズ+構造で説明できない逸脱」とみなします。

from statsmodels.tsa.seasonal import STL

res = STL(obs, period=80, robust=True).fit()
denoised = res.trend + res.seasonal      # デノイズ済み信号
residual = res.resid                     # ノイズ+逸脱

分解ベース(STL)でトレンド+季節を取り出し残差に落とす

上段でトレンド+季節(緑)が観測の骨格を捉え、下段の残差にノイズと逸脱が落ちています。分解ベースが他の手法と決定的に違うのは、残差そのものが異常検知に直結する点です。移動平均やウェーブレットが「滑らかな信号」を出力に求めるのに対し、分解は「予測できる正常パターン(トレンド・季節)」と「説明できない残り」を明示的に切り分けます。トレンドや季節を剥がした後に残る大きな逸脱は、まさに異常の候補です。さらに、季節変動の大きいデータでは「夏だから値が高い」を正常と扱えるので、文脈に依存した正常範囲を自然に表現できます。STLの内部(LOESSによる局所回帰やロバスト推定、内側/外側ループ)は別記事で詳しく扱っています。

トレンドや季節がはっきりしない、あるいはオンラインで逐次処理したい場合は、状態空間モデルが有力です。

状態空間/カルマン平滑 ― 逐次的に推定する

カルマンフィルタは、「真の状態は滑らかに変化し、観測にはノイズが乗る」という状態空間モデルを置き、観測ノイズと状態変動のバランスを取りながら逐次的に状態を推定します。前向き(フィルタ)に加え、後ろ向きにならす平滑化(RTSスムーザ)を使うと、各時点で前後の情報を活かしたデノイズになります。

def kalman_smooth(x, q=0.01, r=0.36):    # q:状態変動, r:観測ノイズ
    n = len(x); xf = np.zeros(n); Pf = np.zeros(n)
    xp, Pp = x[0], 1.0
    for i in range(n):                    # 前向きフィルタ
        if i > 0: xp, Pp = xf[i-1], Pf[i-1] + q
        K = Pp / (Pp + r)
        xf[i] = xp + K*(x[i]-xp); Pf[i] = (1-K)*Pp
    xs = xf.copy()
    for i in range(n-2, -1, -1):          # 後ろ向き平滑化(RTS)
        C = Pf[i] / (Pf[i] + q)
        xs[i] = xf[i] + C*(xs[i+1] - xf[i])
    return xs

状態空間/カルマン平滑による逐次デノイズ

カルマン平滑は観測によく追従しつつノイズをならしています(MSE 0.128)。ここでの平滑度のツマミは $q$ と $r$ の比です。$r$ に対して $q$ を小さくすると「状態はあまり動かない」と信じて強く平滑化し、大きくすると観測に素早く追従します。カルマンの強みは、この比を通じて確率モデルとして不確かさを扱える点と、欠測やオンライン更新に強い点です。観測が飛んだ区間も、状態方程式で補間しながら推定を続けられます。一方、強い季節性を1本の局所レベルモデルで表すのは苦手で、トレンドや季節を状態に組み込むなど、信号の構造に合わせたモデル設計が要ります(状態空間モデルの設計自体が一つの技術です)。

最後に、データから「正常な構造」を学習させる深層的な方法です。

深層 ― オートエンコーダで再構成する

オートエンコーダ(AE)は、入力を低次元のボトルネックに圧縮してから復元するニューラルネットです。ボトルネックには主要な構造しか通せないため、ランダムなノイズは再構成されず、出力は自然と滑らかになります。これがデノイジングオートエンコーダの発想です。窓に切った信号で学習させます。

import torch, torch.nn as nn
# 窓Wに切った系列で学習。中央のボトルネック(次元4)が主要構造だけを通す
ae = nn.Sequential(nn.Linear(W,16), nn.Tanh(), nn.Linear(16,4), nn.Tanh(),
                   nn.Linear(4,16), nn.Tanh(), nn.Linear(16,W))
# 再構成誤差 ||x - ae(x)||^2 を最小化するよう学習(省略)

デノイジングオートエンコーダ(深層)

学習したAEの出力は、ノイズを落としつつ信号構造をよく復元しています(MSE 0.047で本実験では最良クラス)。ここでの平滑度のツマミはボトルネックの次元です。狭くするほど少数の主要成分しか通せず強く平滑化され、広げるほどノイズまで通します。深層の強みは、移動平均やフーリエのように信号の形(滑らか・周期的)を決め打ちせず、データから「正常な構造」そのものを学習できることで、非線形で複雑なパターンにも適応します。代償としてデータ量と計算コストが要り、学習データに異常が混じると「異常まで上手に再構成」して見逃す危険があります。実は、この「正常を再構成し、復元できなかった残差で異常を見る」発想は、USADやOmniAnomalyなど再構成ベース異常検知の核そのものであり、デノイズと異常検知が地続きであることを示しています。

ここまでの手法を、同じ土俵で比べてみましょう。

手法の比較と使い分け

同じ信号に全手法を適用し、対真値MSEを並べたものが次の図です。

デノイザ比較(対真値MSE)

この実験では、デノイジングAE(0.047)とSavitzky-Golay(0.048)が最良、続いてウェーブレット(0.074)、フーリエ(0.117)、カルマン(0.128)、移動平均(0.167)。指数平滑(0.440)だけは位相遅れでノイズあり観測(0.366)より悪化しました。ただしこの順位はこの信号に固有であり、信号の性質しだいで得手不得手は入れ替わります。要点を整理すると次のとおりです。

手法 信号とノイズの仮定 得意 苦手・注意
移動平均/EMA 信号は滑らか 単純・高速・オンライン(EMA) 段差をぼかす/EMAは遅れる
Savitzky-Golay 局所的に多項式 山谷の形を保つ 窓・次数の調整
メディアン/Hampel ノイズはスパイク 外れ値除去 連続的なノイズには非力
フーリエローパス ノイズは高周波 周期信号 段差でギブス現象
ウェーブレット 信号は少数の大係数 段差・急変を保つ 基底・閾値の選択
STL分解 トレンド+季節+残差 構造の分離・異常検知 周期を与える必要
カルマン平滑 状態は滑らかに変化 逐次・欠測対応 モデル設計が要る
デノイジングAE 正常構造を学習 複雑パターンに適応 データ量・計算・異常混入

大切なのは、「万能なデノイザは無い」「信号の性質(滑らか/段差/周期/外れ値)に合わせて選ぶ」ということです。実務的な選び方を、おおまかな指針にまとめておきます。

  • 滑らかでなだらかな信号なら、まず移動平均やSavitzky-Golayで十分。手軽で速い
  • スパイク状の外れ値が主問題なら、メディアン/Hampelで先に叩く
  • 明確な周期があるなら、フーリエローパスか、トレンド・季節を分けたいならSTL分解
  • 段差・急変・スパイクを保ちたいなら、ウェーブレット縮小
  • オンライン処理・欠測対応が要るなら、カルマン平滑
  • 構造が複雑で学習データが豊富なら、デノイジングオートエンコーダ
  • 異常検知の前処理なら、分解で正常パターンを剥がし残差を見る(後述の組み合わせ)

迷ったら、まず信号をプロットして「ノイズは連続的か、スパイク的か」「段差や急変はあるか」「周期はあるか」を目で確認するのが近道です。そして実務では、1つの手法で抱え込まず、しばしば複数を組み合わせます

組み合わせる ― STL+ウェーブレットで異常を残差に立てる

ここまで見た手法は、それぞれ別の種類のノイズや構造を相手にしているので、互いに補い合えます。1つの手法で全部を抱え込むより、段階的に剥がすほうが効くのです。たとえば分解で予測できる構造(トレンド・季節)を剥がし、ウェーブレットで残ったランダムノイズを落とすと、残差には「説明できない逸脱=異常」だけがくっきり残ります。STLは「構造」を、ウェーブレットは「ノイズ」を相手にしており、守備範囲が重ならないからこそ組み合わせが効きます。

組み合わせ方の典型は、おおよそ次の順序です。

  1. 構造を剥がす — STLや分解、あるいは予測モデルで、トレンド・季節など「予測できる正常パターン」を除く
  2. ノイズを落とす — ウェーブレットや平滑化で、残差に残った細かなノイズを抑える
  3. 逸脱を測る — きれいになった残差にMAD閾値などをかけ、異常を検出する

この「分解 → デノイズ → 残差で判定」というパターンは、時系列異常検知の前処理として広く使われています。各段に何を使うかは設計しだいで、分解を深層モデルに置き換えたり、ノイズ除去を周波数フィルタにしたり、文脈情報(休日・イベント等)を加味して差し引いたりと、バリエーションがあります。近年の分解ベース異常検知(たとえばSTLと深層・ウェーブレットを組み合わせるTime-CADなど)も、この枠組みの一例にあたります。いずれにせよ共通するのは、前処理(デノイズ)の質が、後段の検出性能をそのまま左右するという点です。

この「分解 → デノイズ → 残差で判定」を、STL+ウェーブレットの簡易パイプラインで実際に試してみます。

obs_a = clean_base + noise                 # 観測
obs_a[110:114] += 4.0; obs_a[300:304] += 4.0   # 2つの短い異常を仕込む

res = STL(obs_a, period=80, robust=True).fit()
resid = res.resid                          # ①トレンド+季節を除去
resid_clean = wavelet_denoise(resid)       # ②残差をウェーブレットでデノイズ

# 閾値はウェーブレット前の残差スケールから決める(デノイズ後の縮小に引きずられない)
mad = np.median(np.abs(resid - np.median(resid))) / 0.6745
detected = np.where(np.abs(resid_clean) > 6*mad)[0]
print(detected)        # -> 110 111 112 113 300 301 302 303

組み合わせパイプライン(STL+ウェーブレット)で異常を残差に立てる

3段で効果が分かります。上段の観測では、2つの短い異常(赤帯)が季節変動に紛れて目立ちません。中段でSTLがトレンドと季節を剥がすと残差に異常が現れますが、まだノイズに埋もれ気味です。下段でその残差をウェーブレットでデノイズすると、ノイズ床が抑えられて異常だけが閾値を突き抜け、仕込んだ2区間(110–113, 300–303)を過不足なく検出できました(偽陽性ゼロ)。

実装上の小さなコツとして、異常判定の閾値はウェーブレットでデノイズする前の残差のスケール(MAD)から決めています。デノイズ後の残差はノイズ床が極端に小さくなるため、その縮んだスケールで閾値を引くと、わずかな揺らぎまで異常と誤判定してしまうからです。前段のスケールで閾値を固定し、後段のきれいになった残差に当てることで、感度を適正に保てます。

単独の手法では埋もれていた異常が、分解+ウェーブレットの組み合わせで初めてくっきり浮かび上がる——これが組み合わせの力です。「滑らかな信号を出す」だけがデノイズではなく、「邪魔な成分を順番に剥がして、見たいもの(ここでは異常)を際立たせる」という視点を持つと、手法の組み合わせ方が見えてきます。

まとめ

本記事では、時系列のデノイズ手法を横断的にまとめ、実測で比較しました。

  • デノイズは「観測=信号+ノイズ」から信号を復元する操作で、手法ごとに信号とノイズの仮定が違う
  • 線形平滑(移動平均/EMA/Savitzky-Golay)は単純だが段差をぼかし、EMAは遅れる
  • ロバストフィルタ(メディアン/Hampel)はスパイク外れ値に強い
  • フーリエローパスは周期信号に強いが段差が苦手、ウェーブレットは段差・急変を保ってノイズを落とす
  • STL分解は構造を分離して残差を異常検知に使え、カルマンは逐次・欠測に強く、深層AEは複雑パターンに適応する
  • 万能な手法は無く、信号の性質で選び、しばしば組み合わせる。「分解 → デノイズ → 残差で判定」は、異常を残差に際立たせる定番の前処理パターン

「どの手法が一番か」ではなく、「この信号にはどの仮定が合うか」で選ぶこと——これがデノイズを使いこなす鍵です。

次のステップとして、以下の記事も参考にしてください。