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

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

言語 JA

Soft-DTW(Differentiable Dynamic Time Warping)

上級

04.14.5

Soft-DTW(Differentiable Dynamic Time Warping)

最終更新 2020-02-26 読了時間 6 分
まとめ
DTW の累積コスト更新に出てくる \(\min\) は、3 つの候補のうち 1 つだけを選ぶ「角のある」演算なので微分できず、ニューラルネットの損失関数には使えません。Soft-DTW はこの \(\min\) を、スムージング係数 \(\gamma\) つきの log-sum-exp で定義した soft-min に置き換え、どこでも微分できる距離にしたものです。soft-min は常に \(\min\) 以下の値を返すので、\(\gamma\) を 0.01 のように小さくすると DTW に一致し、大きくすると全経路をまとめて評価した値へ離れていきます。このページでは NumPy の soft_dtw を 20 行で実装し、\(\gamma\) を 0.01 から 5.0 まで 4 段階に変えて DTW と比べたあと、tslearn.metrics.soft_dtw_gradient で入力系列に対する勾配を取り出します。

動的時間伸縮(DTW) で累積コスト行列と \(\min\) による遷移式を先に見ておくと、このページで置き換える箇所が 1 行だけだと分かります。

Cuturi, Marco, and Mathieu Blondel. “Soft-DTW: a Differentiable Loss Function for Time-Series.” ICML 2017. (PDF)


直感 #

迷路で最短経路を 1 本だけ選ぶのが DTW です。壁を 1 mm 動かしても最短経路は同じか、別の経路に突然切り替わるかのどちらかで、「経路の長さが壁の位置に対してどう変わるか」という傾きが定義できません。Soft-DTW は「すべての経路を、短いものほど大きな重みで足し合わせる」と考え直します。\(\gamma\) が小さいと最短経路だけが残って DTW と同じ値になり、\(\gamma\) が大きいと遠回りの経路の寄与も混ざって、1 本の経路の長さとは別の値になります。この足し合わせは壁の位置をわずかに動かすと滑らかに変わるので、勾配が定義でき、「予測した波形を正解の波形に近づける」学習に使えます。

Soft-DTW とは #

Dynamic Time Warping(DTW)は最小コスト経路を厳格に選ぶため、距離関数は非凸であり、ワーピングパスも 1 通りに決まります。一方で、ニューラルネットの損失関数として DTW をそのまま使おうとすると、\(\min\) が微分できないため勾配を求められません。

Soft-DTW はこの「最小」を soft-min(滑らかな最小演算)に置き換えることで、なめらかな距離関数を定義します。スムージング係数 \(\gamma > 0\) を導入し、累積コスト更新式を

$$ R_{i,j} = D_{i,j} + \text{softmin}\bigl(R_{i-1,j},\ R_{i,j-1},\ R_{i-1,j-1}\bigr) $$

とします。ここで

$$ \text{softmin}(a,b,c) = -\gamma \log\bigl(\exp(-a/\gamma) + \exp(-b/\gamma) + \exp(-c/\gamma)\bigr) $$

と定義すると、\(\gamma\) を小さくすると DTW に漸近し、\(\gamma\) が大きいほど平均化された距離になります。\(a\) が他より 10 以上小さければ \(\exp(-a/\gamma)\) だけが残って \(\min\) と同じ値を返し、3 つが近い値のときだけ \(\min\) より少し小さい値になるのが soft-min の性質です。Soft-DTW は勾配を解析的に求められるため、ニューラルネットワークなどで時系列の形状を意識した損失関数として用いられます。


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

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

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

アルゴリズムの流れ #

まず通常の DTW と同様に、\(D_{i,j} = |x_i - y_j|^2\) などで局所距離の行列を作ります。次に累積コスト行列 \(R\) を初期化し、上の soft-min を使って動的計画法で左上から右下へ埋めていきます。\(\exp(-a/\gamma)\) は \(a\) が大きいと 0 にアンダーフローするため、実装では最小値を先に引いてから指数を取る log-sum-exp の技巧を使います。最後に Soft-DTW の値を \(R_{n,m}\)(必要に応じて正規化)として取り出します。\(\gamma\) が 0 に近づくほど経路が鋭くなり、DTW に収束します。計算量は DTW と同じ \(O(nm)\) ですが、各マスで exp と log を呼ぶ分だけ定数倍だけ重くなります。


Python で Soft-DTW を実装する #

以下は NumPy だけで Soft-DTW を計算する簡易実装です。DTW の実装と比べると、min(...) が softmin_three(...) に替わり、局所距離が絶対値から二乗になった 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
import numpy as np

def pairwise_sq_euclidean(x: np.ndarray, y: np.ndarray) -> np.ndarray:
    return (x[:, None] - y[None, :]) ** 2

def softmin_three(a: float, b: float, c: float, gamma: float) -> float:
    values = np.array([a, b, c], dtype=float)
    m = values.min()
    return m - gamma * np.log(np.exp(-(values - m) / gamma).sum())

def soft_dtw(x: np.ndarray, y: np.ndarray, gamma: float = 1.0) -> float:
    if gamma <= 0:
        raise ValueError("gamma は 0 より大きい値にしてください。")

    x = np.asarray(x, dtype=float)
    y = np.asarray(y, dtype=float)
    n, m = len(x), len(y)
    D = pairwise_sq_euclidean(x, y)

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

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            r0, r1, r2 = R[i - 1, j], R[i, j - 1], R[i - 1, j - 1]
            R[i, j] = D[i - 1, j - 1] + softmin_three(r0, r1, r2, gamma)
    return R[n, m]

Soft-DTW を正確に微分したい場合は、上記論文に示された後方パスを実装するか、tslearn.metrics.soft_dtw(tslearn 0.5 以降)など既存ライブラリを利用します。


DTW と Soft-DTW を比較する #

Soft-DTW が \(\gamma\) によって DTW とユークリッド距離の間を補間する様子を、sin 波(A)、位相を 0.4 ずらして振幅を 5% 増やした B、cos 波の C の 3 本で確認します。

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(42)
t = np.linspace(0, 2 * np.pi, 80)

series_a = np.sin(t)
series_b = np.sin(t + 0.4) * 1.05  # 形は近いが少し位相ずれ
series_c = np.cos(t)               # 形状が大きく異なる

plt.figure(figsize=(8, 3))
plt.plot(t, series_a, label="Series A", color="black")
plt.plot(t, series_b, label="Series B", color="tab:blue")
plt.plot(t, series_c, label="Series C", color="tab:red")
plt.legend()
plt.title("比較する 3 系列")
plt.tight_layout()
plt.show()

Soft-DTW が \(\gamma\) によって DTW とユークリッド距離の間を補間する様子を、簡単な例で確認…の図

比較用に通常の DTW を定義し、gamma を 0.01 から 5.0 まで 4 段階に変えて Soft-DTW の値を並べます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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]

for gamma in [0.01, 0.1, 1.0, 5.0]:
    val_ab = soft_dtw(series_a, series_b, gamma=gamma)
    val_ac = soft_dtw(series_a, series_c, gamma=gamma)
    print(f"gamma={gamma:>4}: Soft-DTW(A, B)={val_ab:8.3f}, Soft-DTW(A, C)={val_ac:8.3f}")

print(f"DTW(A, B) = {dtw_distance(series_a, series_b):.3f}")
print(f"DTW(A, C) = {dtw_distance(series_a, series_c):.3f}")
1
2
3
4
5
6
gamma=0.01: Soft-DTW(A, B)=   6.478, Soft-DTW(A, C)=  18.742
gamma=0.1 : Soft-DTW(A, B)=   6.514, Soft-DTW(A, C)=  18.904
gamma=1.0 : Soft-DTW(A, B)=   7.721, Soft-DTW(A, C)=  21.053
gamma=5.0 : Soft-DTW(A, B)=  17.089, Soft-DTW(A, C)=  28.162
DTW(A, B) = 6.469
DTW(A, C) = 18.707

上の出力は元のノートブック環境で得たもので、\(\gamma\) が小さいうちは Soft-DTW が DTW に近い値を取り、\(\gamma\) を大きくするほど DTW から離れていく傾向を示しています。Soft-DTW の値は局所距離の定義(絶対値か二乗か)と gamma に強く依存し、上の soft_dtw 実装では \(-\gamma \log 3\) の項が積み重なるため gamma が大きいと負の値も取ります。数値そのものより、「\(\gamma \to 0\) で DTW に一致し、\(\gamma\) が大きいほど 1 本の経路の長さから離れる」という向きを押さえてください。


勾配が欲しい場合 #

Soft-DTW の利点は微分可能性にあります。自分で後方パスを実装しない場合は、tslearn の soft_dtw_gradient で入力系列に対する勾配をそのまま取り出せます。

1
pip install tslearn
1
2
3
4
5
6
7
8
9
from tslearn.metrics import soft_dtw as ts_soft_dtw
from tslearn.metrics import soft_dtw_gradient

gamma = 0.5
loss = ts_soft_dtw(series_a.reshape(-1, 1), series_b.reshape(-1, 1), gamma=gamma)
grad = soft_dtw_gradient(series_a.reshape(-1, 1), series_b.reshape(-1, 1), gamma=gamma)

print("Soft-DTW (tslearn):", loss)
print("Gradient shape:", grad.shape)

図の再生成は python scripts/generate_timeseries_assets.py --pattern shape/005_soft_dtw を利用してください。

soft_dtw_gradient は入力系列に対する損失の勾配を返すので、ニューラルネットワークの学習ループに組み込むことができます。


まとめ #

Soft-DTW は DTW の遷移式にある \(\min\) を、\(\gamma\) つきの log-sum-exp(soft-min)に置き換えただけの距離で、実装上の違いも min の 1 行です。soft-min は \(\min\) を超えないので、\(\gamma = 0.01\) では DTW とほぼ同じ値になり、\(\gamma\) を 5.0 まで上げると \(-\gamma \log 3\) の項が積み重なって 1 本の経路の長さとは別の値(負にもなり得る)に変わります。損失関数に使う \(\gamma\) は、形の近い組の方が小さいという順序が保たれる範囲から選びます。微分できるので tslearn.metrics.soft_dtw_gradient や SoftDTWLossPyTorch を通してニューラルネットの損失関数にでき、「タイミングが少しずれても形が合っていれば損失が小さい」学習が可能になります。