K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

DDTW(Derivative Dynamic Time Warping)

04.14.2

DDTW(Derivative Dynamic Time Warping)

最終更新 2020-02-12 読了時間 7 分
まとめ
Derivative Dynamic Time Warping(DDTW)は、DTW の局所距離を「値の差 \(|x_i - y_j|\)」から「一次微分(傾き)の差 \(|D_X(i) - D_Y(j)|\)」に置き換えた手法で、波形が上がるか下がるかのパターンだけを比べます。傾きは各時点の中央差分で近似し、累積コストとワーピングパスの計算は通常の DTW とまったく同じです。このページでは derivative_series と ddtw_distance を NumPy で実装し、sin 波(A)、それを 0.6 持ち上げた B、周期と位相を変えた C の 3 本で距離を比べます。DTW では A–B が 72.2 と A–C の 51.2 より大きくなるのに対し、DDTW では A–B が 0.002、A–C が 13.6 と「形の近さ」の順に並ぶことを確かめます。

Keogh, Eamonn J., and Michael J. Pazzani. “Derivative Dynamic Time Warping.” Proceedings of the 2001 SIAM International Conference on Data Mining. (PDF)


直感 #

心電図を 2 人分比べるとき、片方の機器のゼロ点が 0.6 mV ずれていても、医師は「P 波、QRS、T 波」という形の順序で同じ波形だと判断します。DTW は値そのものを比べるので、ゼロ点のずれ 0.6 が 120 点すべてに積み上がって距離 72 になり、形が違う別の波形(距離 51)より「遠い」と答えてしまいます。DDTW は各点を「今、上がっているか下がっているか、どれくらいの速さで」という傾きに置き換えてから DTW をかけるので、ゼロ点のずれは消え、形の違いだけが距離 13.6 として残ります。

DDTW とは #

Dynamic Time Warping(DTW)は 2 本の時系列を整列させ、対応付けた点同士の距離を最小化します。ただし値の差をそのまま距離にすると、縦方向のオフセットが大きいケースで「形は近いのに距離が離れてしまう」ことがあります。Derivative Dynamic Time Warping(DDTW)は距離の定義を「傾きの差」に置き換えることで、形状の近さに注目します。ベースラインが 0.6 違っても上り坂と下り坂のパターンが同じなら距離はほぼ 0 です。ノイズが少なければ、ピークや谷といった形状の特徴が強調されます。絶対値の差そのものが重要な場面(温度の閾値を超えたかを見る異常検知など)では DTW が適しているので、目的に合わせて使い分けます。


何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:DTW は時間を伸び縮みさせて形を合わせる。タイミングがずれた似た波形を「近い」と判定できる。

アルゴリズムのポイント #

DDTW が DTW と違うのは局所距離の定義だけで、「傾きの差」を使います。

1. 傾き(一次微分)の近似 #

系列 \(X\) の \(i\) 番目の傾きは、1 つ前との差と、前後 2 点の差の半分を平均した中央差分で近似します。

$$ D_X(i) = \frac{(x_i - x_{i-1}) + (x_{i+1} - x_{i-1})/2}{2} $$

単純な差分 \(x_i - x_{i-1}\) だけだとノイズ 1 点で傾きが跳ねるので、前後 2 点にまたがる差分と平均して少し平滑化しています。端点 \(i = 1\) と \(i = n\) は前後 3 点の値を利用して同様に計算します。局所距離 \(D_{i,j}\) はこの傾きの差の絶対値 \(|D_X(i) - D_Y(j)|\)(または二乗誤差)とします。

2. 累積コストの計算 #

傾きから作成した距離行列 \(D\) を用い、通常の DTW と同じ遷移式で累積コスト \(C\) を計算します。

$$ C_{i,j} = D_{i,j} + \min\{C_{i-1,j},\ C_{i,j-1},\ C_{i-1,j-1}\} $$

3. ワーピングパス #

累積コスト行列の右下から逆向きにたどり、最小コスト経路(ワーピングパス)を復元します。距離評価が傾きを基準にしているため、値のオフセットより形状の違いが強調されます。計算量は DTW と同じ \(O(nm)\) で、長い系列では Sakoe–Chiba バンドなどの制約を加えると高速化できます。


Python で DDTW を実装する #

derivative_series が式 \(D_X(i)\) を全点に適用して傾き列を返し、ddtw_distance がその傾き列に対して DTW のページ と同じ動的計画法を実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import numpy as np

def derivative_series(x: np.ndarray) -> np.ndarray:
    """中央差分で一次微分を近似する。"""
    x = np.asarray(x, dtype=float)
    if x.size < 3:
        raise ValueError("系列の長さは 3 以上にしてください。")

    d = np.empty_like(x)
    d[0] = ((x[1] - x[0]) + (x[2] - x[0]) / 2.0) / 2.0
    d[-1] = ((x[-1] - x[-2]) + (x[-1] - x[-3]) / 2.0) / 2.0
    for i in range(1, len(x) - 1):
        d[i] = ((x[i] - x[i - 1]) + (x[i + 1] - x[i - 1]) / 2.0) / 2.0
    return d

def ddtw_distance(
    x: np.ndarray,
    y: np.ndarray,
    window: int | None = None,
) -> tuple[float, list[tuple[int, int]]]:
    dx, dy = derivative_series(x), derivative_series(y)
    n, m = len(dx), len(dy)

    if window is None:
        window = max(n, m)
    window = max(window, abs(n - m))

    cost = np.full((n + 1, m + 1), np.inf)
    cost[0, 0] = 0.0

    for i in range(1, n + 1):
        for j in range(max(1, i - window), min(m + 1, i + window + 1)):
            dist = abs(dx[i - 1] - dy[j - 1])
            cost[i, j] = dist + min(cost[i - 1, j], cost[i, j - 1], cost[i - 1, j - 1])

    i, j = n, m
    path: list[tuple[int, int]] = []
    while i > 0 and j > 0:
        path.append((i - 1, j - 1))
        step = np.argmin((cost[i - 1, j], cost[i, j - 1], cost[i - 1, j - 1]))
        if step == 0:
            i -= 1
        elif step == 1:
            j -= 1
        else:
            i -= 1
            j -= 1
    path.reverse()
    return cost[n, m], path

DTW と DDTW を比較する #

sin 波(A)、それを 0.6 持ち上げただけの B、周期を 1.2 倍にして位相を 0.3 ずらした C を 120 点ずつ作ります。B は「形は同じで高さが違う」、C は「高さは同じで形が違う」例です。

1
2
python --version  # 例: Python 3.13.0
pip install matplotlib
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt

np.random.seed(0)
t = np.linspace(0, 4 * np.pi, 120)

series_a = np.sin(t)
series_b = np.sin(t) + 0.6
series_c = np.sin(1.2 * t + 0.3)

plt.figure(figsize=(10, 3))
plt.plot(t, series_a, label="Series A", color="black")
plt.plot(t, series_b, label="Series B (offset)", color="tab:green")
plt.plot(t, series_c, label="Series C (shape change)", color="tab:orange")
plt.legend()
plt.title("比較する 3 つの時系列")
plt.tight_layout()
plt.show()

DTW と DDTW を比較する figure

比較用に制約なしの dtw_distance を 9 行で定義し、A–B と A–C の距離を DTW と DDTW の両方で出します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def dtw_distance(x: np.ndarray, y: np.ndarray) -> float:
    n, m = len(x), len(y)
    cost = np.full((n + 1, m + 1), np.inf)
    cost[0, 0] = 0.0
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            dist = abs(x[i - 1] - y[j - 1])
            cost[i, j] = dist + min(cost[i - 1, j], cost[i, j - 1], cost[i - 1, j - 1])
    return cost[n, m]

dtw_ab = dtw_distance(series_a, series_b)
dtw_ac = dtw_distance(series_a, series_c)
ddtw_ab, _ = ddtw_distance(series_a, series_b)
ddtw_ac, _ = ddtw_distance(series_a, series_c)

print(f"DTW (A, B):  {dtw_ab:.3f}")
print(f"DTW (A, C):  {dtw_ac:.3f}")
print(f"DDTW(A, B):  {ddtw_ab:.3f}")
print(f"DDTW(A, C):  {ddtw_ac:.3f}")
1
2
3
4
DTW (A, B):  72.248
DTW (A, C):  51.158
DDTW(A, B):   0.002
DDTW(A, C):  13.632

DTW では 0.6 持ち上げただけの Series B の距離が 72.2 と大きく、形状が異なる Series C の 51.2 の方が小さくなっています。DDTW では傾きが同じ Series B との距離が 0.002 とほぼゼロになり、形状の異なる Series C が 13.6 と大きくなる点が決定的な違いです。


ワーピングパスの可視化 #

A と C の累積コスト行列を熱マップにし、DDTW のワーピングパスを重ねます。形が違う 2 本をどう対応づけようとしているかが経路の曲がり方に現れます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def ddtw_cost_matrix(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, list[tuple[int, int]]]:
    dx, dy = derivative_series(x), derivative_series(y)
    n, m = len(dx), len(dy)
    cost = np.full((n + 1, m + 1), np.inf)
    cost[0, 0] = 0.0

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            dist = abs(dx[i - 1] - dy[j - 1])
            cost[i, j] = dist + min(cost[i - 1, j], cost[i, j - 1], cost[i - 1, j - 1])

    i, j = n, m
    path: list[tuple[int, int]] = []
    while i > 0 and j > 0:
        path.append((i - 1, j - 1))
        step = np.argmin((cost[i - 1, j], cost[i, j - 1], cost[i - 1, j - 1]))
        if step == 0:
            i -= 1
        elif step == 1:
            j -= 1
        else:
            i -= 1
            j -= 1
    path.reverse()
    return cost[1:, 1:], path

cost_ac, path_ac = ddtw_cost_matrix(series_a, series_c)

plt.figure(figsize=(5, 4))
plt.imshow(cost_ac, origin="lower", cmap="viridis")
path_arr = np.array(path_ac)
plt.plot(path_arr[:, 1], path_arr[:, 0], color="white", linewidth=1.5)
plt.title("DDTW cumulative cost (Series A vs Series C)")
plt.xlabel("Series index j")
plt.ylabel("Series index i")
plt.colorbar(shrink=0.8, label="Cumulative cost")
plt.tight_layout()
plt.show()

ワーピングパスの可視化 figure

Series A と Series C の形状が異なるため、パスが水平・垂直方向に大きく移動しながら対応づけていることが分かります。これは周期 1.2 倍の違いを時間の伸縮で埋め合わせようとする DDTW の挙動を示しており、それでも埋めきれなかった傾きの差が距離 13.6 として残ります。


まとめ #

DDTW は傾き(中央差分)の列に DTW をかける手法で、実装上の違いは derivative_series を通す 1 行だけです。値の差で測る DTW では 0.6 持ち上げた B が 72.2、形の違う C が 51.2 と直感に反する順序になりましたが、DDTW では B が 0.002、C が 13.6 と形の近さどおりに並びました。オフセットやベースラインの変動には強く、ノイズには弱いので、心電図や歩行波形のような形状比較では DDTW、温度や在庫のように水準そのものに意味がある比較では DTW と使い分けます。


よくある質問 #

DDTWとDTWはどう使い分ける? #

DTWは「値の絶対差」を距離にするため、振幅のスケールやベースラインの違いが距離に直接影響します。DDTWは「傾きの差」を使うため、上昇・下降パターンが似ていれば、縦方向のオフセットや振幅の差があっても小さい距離になります。異常検知など絶対値が重要な場面ではDTW、波形パターンの形状比較(歩行・心電図・ジェスチャー認識など)ではDDTWが向いています。

微分を使うとノイズに弱くなる? #

その通りです。差分演算はノイズを増幅する性質があります。中央差分で3点を使った平均(DDTW標準の近似式)はある程度平滑化されますが、センサーノイズが多いデータでは事前にローパスフィルターや移動平均を適用してからDDTWを計算することを推奨します。

Sakoe–Chibaバンドの設定は? #

DTWと同様に、系列長の10〜20%程度のウィンドウを設定することが多いです。バンド幅が小さすぎると最適パスを見逃す可能性があり、大きすぎると計算コストが増えます。実務ではグリッドサーチでバンド幅を調整し、クラスタリングや分類の精度で評価するのが標準的です。

DDTWを高速化する方法は? #

まず Sakoe–Chiba バンドを使うと計算量を \(O(nw)\)(\(w\) はバンド幅)に削減でき、ddtw_distance の window 引数がそれに当たります。次に tslearn や aeon などのライブラリは Cython や Numba で高速化した DTW 実装を提供しており、傾き列を渡せば大規模データでも DDTW として使えます。系列長が数千を超える場合は、PAA(区分集約近似)などで次元圧縮してから DDTW を計算するアプローチも有効です。