Commit 16ddb891 authored by Steve Tjoa's avatar Steve Tjoa

dtw

parent fcafee84
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"%matplotlib inline\n",
"import seaborn\n",
"import numpy, scipy, scipy.spatial, matplotlib.pyplot as plt\n",
"plt.rcParams['figure.figsize'] = (14, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[← Back to Index](index.html)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Dynamic Time Warping "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"(Credit for some of the ideas in this notebook come from [FastDTW](https://github.com/slaypni/fastdtw/blob/master/fastdtw.py).)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Dynamic time warping (DTW) is an algorithm used to align two sequences of similar content but possibly different lengths. For example, you might want to align two different performances of the same musical work. These two signals, $x$ and $y$, may have similar sequences of chord progressions and instrumentations, but there may be timing deviations between the two.\n",
"\n",
"Given two sequences, $x[n], n \\in \\{0, ..., N_x - 1\\}$, and $y[n], n \\in \\{0, ..., N_y - 1\\}$, DTW produces a set of index pairs $\\{ (i, j) ... \\}$ such that $x[i]$ and $y[j]$ are similar."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create two arrays, $x$ and $y$, of lengths $N_x$ and $N_y$, respectively."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"8 9\n"
]
}
],
"source": [
"x = scipy.array([3, 3, 1, 4, 6, 1, 5, 5])\n",
"y = scipy.array([4, 2, 1, 3, 3, 4, 1, 4, 5])\n",
"Nx = len(x)\n",
"Ny = len(y)\n",
"print Nx, Ny"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[<matplotlib.lines.Line2D at 0x110817ad0>,\n",
" <matplotlib.lines.Line2D at 0x1140bced0>]"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
"<matplotlib.figure.Figure at 0x110817790>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"plt.plot(range(Nx), x, 'b', range(Ny), y, 'r')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In this simple example, there is only one value or \"feature\" at each time index. However, in practice, you can use sequences of *vectors*, e.g. spectrograms, chromagrams, or MFCC-grams."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But for now, we will add an empty dimension to the array because DTW expects a vector at each time index -- in this simple example, a \"one-dimensional vector\", i.e. a scalar."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"(8, 1)\n",
"(9, 1)\n"
]
}
],
"source": [
"x = scipy.expand_dims(x, axis=1)\n",
"y = scipy.expand_dims(y, axis=1)\n",
"print x.shape\n",
"print y.shape"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[3]\n",
" [3]\n",
" [1]\n",
" [4]\n",
" [6]\n",
" [1]\n",
" [5]\n",
" [5]]\n"
]
}
],
"source": [
"print x"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 1: Compute pairwise distances"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Compute a matrix of pairwise distances between elements of $x$ and $y$."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"(8, 9)\n"
]
}
],
"source": [
"S = scipy.spatial.distance_matrix(x, y)\n",
"print S.shape"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[ 1. 1. 2. 0. 0. 1. 2. 1. 2.]\n",
" [ 1. 1. 2. 0. 0. 1. 2. 1. 2.]\n",
" [ 3. 1. 0. 2. 2. 3. 0. 3. 4.]\n",
" [ 0. 2. 3. 1. 1. 0. 3. 0. 1.]\n",
" [ 2. 4. 5. 3. 3. 2. 5. 2. 1.]\n",
" [ 3. 1. 0. 2. 2. 3. 0. 3. 4.]\n",
" [ 1. 3. 4. 2. 2. 1. 4. 1. 0.]\n",
" [ 1. 3. 4. 2. 2. 1. 4. 1. 0.]]\n"
]
}
],
"source": [
"print S"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Element $S[i, j]$ indicates the distance between $x[i]$ and $y[j]$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The time complexity of this operation is $O(N_x N_y)$. The space complexity is $O(N_x N_y)$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Compute cumulative distances"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The basic idea of DTW is to find a path from the top left to the bottom right of matrix $S$ such that the sum of distances along the path is minimized. After all, you want to find pairs of time indices $(i, j)$ such that $x[i]$ and $y[j]$ is always low."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We will compute a matrix of cumulative path distances, $D$, such that for any pair of time indices $(i, j)$, $D[i, j]$ is the sum of the best path from $(0, 0)$ to $(i, j)$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"First, let's compute the sum of the best path from $(0, 0)$ to $(N_x-1, 0)$."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[ 1. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 2. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 5. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 5. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 7. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 10. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 11. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 12. 0. 0. 0. 0. 0. 0. 0. 0.]]\n"
]
}
],
"source": [
"D = scipy.zeros_like(S)\n",
"D[0, 0] = S[0, 0]\n",
"for i in range(1, Nx):\n",
" D[i, 0] = D[i-1, 0] + S[i, 0]\n",
"print D"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Next, compute the sum of the best path from $(0, 0)$ to $(0, N_y)$."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[ 1. 2. 4. 4. 4. 5. 7. 8. 10.]\n",
" [ 2. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 5. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 5. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 7. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 10. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 11. 0. 0. 0. 0. 0. 0. 0. 0.]\n",
" [ 12. 0. 0. 0. 0. 0. 0. 0. 0.]]\n"
]
}
],
"source": [
"for j in range(1, len(y)):\n",
" D[0, j] = D[0, j-1] + S[0, j]\n",
"print D"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, compute the sums of the best paths to any other pair of coordinates, $(i, j)$.\n",
"\n",
"The path constraint is that, at $(i, j)$, the valid steps are $(i+1, j)$, $(i, j+1)$, and $(i+1, j+1)$. In other words, the alignment always moves forward in time for at least one of the signals. It never goes backward in time. (You can adapt this to your application.)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[ 1. 2. 4. 4. 4. 5. 7. 8. 10.]\n",
" [ 2. 2. 4. 4. 4. 5. 7. 8. 10.]\n",
" [ 5. 3. 2. 4. 6. 7. 5. 8. 12.]\n",
" [ 5. 5. 5. 3. 4. 4. 7. 5. 6.]\n",
" [ 7. 9. 10. 6. 6. 6. 9. 7. 6.]\n",
" [ 10. 8. 8. 8. 8. 9. 6. 9. 10.]\n",
" [ 11. 11. 12. 10. 10. 9. 10. 7. 7.]\n",
" [ 12. 14. 15. 12. 12. 10. 13. 8. 7.]]\n"
]
}
],
"source": [
"for i in range(1, Nx):\n",
" for j in range(1, Ny):\n",
" D[i, j] = min(D[i-1, j-1], D[i-1, j], D[i, j-1]) + S[i, j]\n",
"print D"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The time complexity of this operation is $O(N_x N_y)$. The space complexity is $O(N_x N_y)$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Path Finding"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We will start at the end, $(N_x - 1, N_y - 1)$, and backtrace to the beginning, $(0, 0)$.\n",
"\n",
"For each pair of time indices, $(i, j)$, we will store the index pair in the path that preceded it."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"backsteps = [[None for j in range(Ny)] for i in range(Nx)]\n",
"for i in range(1, Nx):\n",
" backsteps[i][0] = (i-1, 0)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"for j in range(1, Ny):\n",
" backsteps[0][j] = (0, j-1)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"for i in range(1, Nx):\n",
" for j in range(1, Ny):\n",
" candidate_steps = ((i-1, j-1), (i-1, j), (i, j-1),)\n",
" candidate_distances = [D[m, n] for (m, n) in candidate_steps]\n",
" backsteps[i][j] = candidate_steps[numpy.argmin(candidate_distances)]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The time complexity of this operation is $O(N_x N_y)$. The space complexity is $O(N_x N_y)$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, just read off the sequences of time index pairs starting at the end."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[(0, 0), (1, 1), (2, 2), (3, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)]"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"xi, yi = Nx-1, Ny-1\n",
"path = [(xi, yi)]\n",
"while xi > 0 or yi > 0:\n",
" xi, yi = backsteps[xi][yi]\n",
" path.insert(0, (xi, yi))\n",
"path"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"## Complete DTW algorithm"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here is a function that combines all the steps above. (Note that this is a naive version of the DTW algorithm. There are many optimizations that can reduce the time and space complexity below $O(N_x N_y)$.)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def dtw(x, y):\n",
" Nx = len(x)\n",
" Ny = len(y)\n",
"\n",
" # Step 1: compute pairwise distances.\n",
" S = scipy.spatial.distance_matrix(x, y) \n",
" \n",
" # Step 2: compute cumulative distances.\n",
" D = scipy.zeros_like(S)\n",
" D[0, 0] = S[0, 0]\n",
" for i in range(1, Nx):\n",
" D[i, 0] = D[i-1, 0] + S[i, 0]\n",
" for j in range(1, len(y)):\n",
" D[0, j] = D[0, j-1] + S[0, j]\n",
" for i in range(1, Nx):\n",
" for j in range(1, Ny):\n",
" D[i, j] = min(D[i-1, j-1], D[i-1, j], D[i, j-1]) + S[i, j]\n",
"\n",
" # Step 3: find optimal path.\n",
" backsteps = [[None for j in range(Ny)] for i in range(Nx)]\n",
" for i in range(1, Nx):\n",
" backsteps[i][0] = (i-1, 0)\n",
" for j in range(1, Ny):\n",
" backsteps[0][j] = (0, j-1)\n",
" for i in range(1, Nx):\n",
" for j in range(1, Ny):\n",
" candidate_steps = ((i-1, j-1), (i-1, j), (i, j-1),)\n",
" candidate_distances = [D[m, n] for (m, n) in candidate_steps]\n",
" backsteps[i][j] = candidate_steps[numpy.argmin(candidate_distances)]\n",
" \n",
" xi, yi = Nx-1, Ny-1\n",
" path = [(xi, yi)]\n",
" while xi > 0 or yi > 0:\n",
" xi, yi = backsteps[xi][yi]\n",
" path.insert(0, (xi, yi))\n",
" return path"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's try it out:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[(0, 0), (1, 1), (2, 2), (3, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)]"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"path = dtw(x, y)\n",
"path"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Take a look at the two aligned sequences:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[3, 3, 1, 4, 4, 6, 1, 5, 5]\n",
"[4, 2, 1, 3, 3, 4, 1, 4, 5]\n"
]
}
],
"source": [
"print [x[i][0] for (i, j) in path]\n",
"print [y[j][0] for (i, j) in path]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Sanity check: compute the total distance of this alignment:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"7"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sum(abs(x[i][0] - y[j][0]) for (i, j) in path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Indeed, that is the same as the cumulative distance computed earlier, $D[N_x - 1, N_y - 1]$:"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"7.0"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"D[-1, -1]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[&larr; Back to Index](index.html)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.10"
}
},
"nbformat": 4,
"nbformat_minor": 1
}
...@@ -11876,6 +11876,18 @@ div#notebook { ...@@ -11876,6 +11876,18 @@ div#notebook {
<div class="inner_cell"> <div class="inner_cell">
<div class="text_cell_render border-box-sizing rendered_html"> <div class="text_cell_render border-box-sizing rendered_html">
<h2 id="Chapter-3:-Music-Synchronization">Chapter 3: Music Synchronization<a class="anchor-link" href="#Chapter-3:-Music-Synchronization">&#182;</a></h2> <h2 id="Chapter-3:-Music-Synchronization">Chapter 3: Music Synchronization<a class="anchor-link" href="#Chapter-3:-Music-Synchronization">&#182;</a></h2>
</div>
</div>
</div>
<div class="cell border-box-sizing text_cell rendered">
<div class="prompt input_prompt">
</div>
<div class="inner_cell">
<div class="text_cell_render border-box-sizing rendered_html">
<ol>
<li><a href="dtw.html">Dynamic Time Warping</a> (<a href="dtw.ipynb">ipynb</a>)</li>
</ol>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -98,6 +98,13 @@ ...@@ -98,6 +98,13 @@
"## Chapter 3: Music Synchronization" "## Chapter 3: Music Synchronization"
] ]
}, },
{
"cell_type": "markdown",
"metadata": {},
"source": [
"1. [Dynamic Time Warping](dtw.html) ([ipynb](dtw.ipynb))"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment