Commit 19bb5d91 authored by Steve Tjoa's avatar Steve Tjoa

lcs; dtw

parent f8568285
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -2,16 +2,14 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"execution_count": 74,
"metadata": {},
"outputs": [],
"source": [
"%matplotlib inline\n",
"import seaborn\n",
"import numpy, scipy, scipy.spatial, matplotlib.pyplot as plt\n",
"plt.rcParams['figure.figsize'] = (14, 2)"
"plt.rcParams['figure.figsize'] = (14, 3)"
]
},
{
......@@ -32,16 +30,20 @@
"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).)"
"In MIR, we often want to compare two sequences of different lengths. For example, we may want to compute a similarity measure between two versions of the same song. These two signals, $x$ and $y$, may have similar sequences of chord progressions and instrumentations, but there may be timing deviations between the two. Even if we were to express the two audio signals using the same feature space (e.g. chroma or MFCCs), we cannot simply sum their pairwise distances because the signals have different lengths.\n",
"\n",
"As another example, you might want to align two different performances of the same musical work, e.g. so you can hop from one performance to another at any moment in the work. This problem is known as **music synchronization** (FMP, p. 115)."
]
},
{
"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",
"**Dynamic time warping (DTW)** ([Wikipedia](https://en.wikipedia.org/wiki/Dynamic_time_warping); FMP, p. 131) is an algorithm used to align two sequences of similar content but possibly different lengths. \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."
"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 coordinate pairs $\\{ (i, j) ... \\}$ such that $x[i]$ and $y[j]$ are similar.\n",
"\n",
"We will use the same approach described in the notebooks [Dynamic Programming](dp.html) and [Longest Common Subsequence](lcs.html)."
]
},
{
......@@ -60,50 +62,36 @@
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"8 9\n"
]
}
],
"execution_count": 123,
"metadata": {},
"outputs": [],
"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"
"x = [0, 4, 4, 0, -4, -4, 0]\n",
"y = [1, 3, 4, 3, 1, -1, -2, -1, 0]\n",
"nx = len(x)\n",
"ny = len(y)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"execution_count": 127,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[<matplotlib.lines.Line2D at 0x110817ad0>,\n",
" <matplotlib.lines.Line2D at 0x1140bced0>]"
"<matplotlib.legend.Legend at 0x11ce5d910>"
]
},
"execution_count": 3,
"execution_count": 127,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"image/png": "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\nREREtN4/u6cPBCJ5QIcAAAAASUVORK5CYII=\n",
"text/plain": [
"<matplotlib.figure.Figure at 0x110817790>"
"<matplotlib.figure.Figure at 0x11cd40410>"
]
},
"metadata": {},
......@@ -111,7 +99,9 @@
}
],
"source": [
"plt.plot(range(Nx), x, 'b', range(Ny), y, 'r')"
"plt.plot(x)\n",
"plt.plot(y, c='r')\n",
"plt.legend(('x', 'y'))"
]
},
{
......@@ -125,261 +115,247 @@
"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."
"## Distance Metric"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"DTW requires the use of a distance metric between corresponding observations of `x` and `y`. One common choice is the **Euclidean distance** ([Wikipedia](https://en.wikipedia.org/wiki/Euclidean_distance); FMP, p. 454):"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"execution_count": 128,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"(8, 1)\n",
"(9, 1)\n"
]
"data": {
"text/plain": [
"5.0"
]
},
"execution_count": 128,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"x = scipy.expand_dims(x, axis=1)\n",
"y = scipy.expand_dims(y, axis=1)\n",
"print x.shape\n",
"print y.shape"
"scipy.spatial.distance.euclidean(0, [3, 4])"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"execution_count": 129,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[3]\n",
" [3]\n",
" [1]\n",
" [4]\n",
" [6]\n",
" [1]\n",
" [5]\n",
" [5]]\n"
]
"data": {
"text/plain": [
"13.0"
]
},
"execution_count": 129,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"print x"
"scipy.spatial.distance.euclidean([0, 0], [5, 12])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 1: Compute pairwise distances"
"Another choice might be the **cosine distance** ([Wikipedia](https://en.wikipedia.org/wiki/Cosine_similarity); FMP, p. 376) which can be interpreted as the (normalized) angle between two vectors:"
]
},
{
"cell_type": "markdown",
"cell_type": "code",
"execution_count": 134,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0.0"
]
},
"execution_count": 134,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Compute a matrix of pairwise distances between elements of $x$ and $y$."
"scipy.spatial.distance.cosine([1, 0], [100, 0])"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"execution_count": 132,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"(8, 9)\n"
]
"data": {
"text/plain": [
"1.0"
]
},
"execution_count": 132,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"S = scipy.spatial.distance_matrix(x, y)\n",
"print S.shape"
"scipy.spatial.distance.cosine([1, 0, 0], [0, 0, -1])"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"execution_count": 133,
"metadata": {},
"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"
]
"data": {
"text/plain": [
"2.0"
]
},
"execution_count": 133,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"print S"
"scipy.spatial.distance.cosine([1, 0], [-1, 0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Element $S[i, j]$ indicates the distance between $x[i]$ and $y[j]$."
"## Step 1: Cost Table Construction"
]
},
{
"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)$."
"As described in the notebooks [Dynamic Programming](dp.html) and [Longest Common Subsequence](lcs.html), we will use dynamic programming to solve this problem. First, we create a table which stores the solutions to all subproblems. Then, we will use this table to solve each larger subproblem until the problem is solved for the full original inputs."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Compute cumulative distances"
"The basic idea of DTW is to find a path of index coordinate pairs the sum of distances along the path $P$ is minimized:\n",
"\n",
"$$ \\min \\sum_{(i, j) \\in P} d(x[i], y[j]) $$"
]
},
{
"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."
"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 forward in time for one signal and backward in time for the other signal."
]
},
{
"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)$."
"Here is the optimal substructure. Suppose that the best alignment contains index pair `(i, j)`, i.e., `x[i]` and `y[j]` are part of the optimal DTW path. Then, we prepend to the optimal path \n",
"\n",
"$$ \\mathrm{argmin} \\ \\{ d(x[i-1], y[j]), d(x[i], y[j-1]), d(x[i-1], j-1]) \\} $$"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"First, let's compute the sum of the best path from $(0, 0)$ to $(N_x-1, 0)$."
"We create a table where cell `(i, j)` stores the optimum cost of `dtw(x[:i], y[:j])`, i.e. the optimum cost from `(0, 0)` to `(i, j)`. First, we solve for the boundary cases, i.e. when either one of the two sequences is empty. Then we populate the table from the top left to the bottom right."
]
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 135,
"metadata": {
"collapsed": false
"collapsed": true
},
"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": {},
"outputs": [],
"source": [
"Next, compute the sum of the best path from $(0, 0)$ to $(0, N_y)$."
"def dtw_table(x, y):\n",
" nx = len(x)\n",
" ny = len(y)\n",
" table = [[0 for _ in range(ny+1)] for _ in range(nx+1)]\n",
" \n",
" # Compute left column separately, i.e. j=0.\n",
" for i in range(1, nx+1):\n",
" d = scipy.spatial.distance.euclidean(x[i-1], 0)\n",
" table[i][0] = d + table[i-1][0]\n",
" \n",
" # Compute top row separately, i.e. i=0.\n",
" for j in range(1, ny+1):\n",
" d = scipy.spatial.distance.euclidean(0, y[j-1])\n",
" table[0][j] = d + table[0][j-1]\n",
" \n",
" # Fill in the rest.\n",
" for i in range(1, nx+1):\n",
" for j in range(1, ny+1):\n",
" d = scipy.spatial.distance.euclidean(x[i-1], y[j-1])\n",
" table[i][j] = d + min(table[i-1][j], table[i][j-1], table[i-1][j-1])\n",
" return table"
]
},
{
"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"
]
}
],
"execution_count": 136,
"metadata": {},
"outputs": [],
"source": [
"for j in range(1, len(y)):\n",
" D[0, j] = D[0, j-1] + S[0, j]\n",
"print D"
"table = dtw_table(x, y)"
]
},
{
"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.)"
"Let's visualize this table:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"execution_count": 142,
"metadata": {},
"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"
" 1 3 4 3 1 -1 -2 -1 0\n",
" +----------------------------------------\n",
" | 0 1 4 8 11 12 13 15 16 16\n",
" 0 | 0 1 4 8 11 12 13 15 16 16\n",
" 4 | 4 3 2 2 3 6 11 17 20 20\n",
" 4 | 8 6 3 2 3 6 11 17 22 24\n",
" 0 | 8 7 6 6 5 4 5 7 8 8\n",
" -4 | 12 12 13 14 12 9 7 7 10 12\n",
" -4 | 16 17 19 21 19 14 10 9 10 14\n",
" 0 | 16 17 20 23 22 15 11 11 10 10\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"
"print ' ', ''.join('%4d' % n for n in y)\n",
"print ' +' + '----' * (ny+1)\n",
"for i, row in enumerate(table):\n",
" if i == 0:\n",
" z0 = ''\n",
" else:\n",
" z0 = x[i-1]\n",
" print ('%4s |' % z0) + ''.join('%4d' % z for z in row)"
]
},
{
......@@ -393,63 +369,14 @@
"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)]"
"## Step 2: Backtracking"
]
},
{
"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)$."
"To assemble the best path, we use **backtracking** (FMP, p. 139). We will start at the end, $(N_x - 1, N_y - 1)$, and backtrack to the beginning, $(0, 0)$."
]
},
{
......@@ -461,120 +388,62 @@
},
{
"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",
"execution_count": 138,
"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",
"def dtw(x, y, table):\n",
" i = len(x)\n",
" j = len(y)\n",
" path = [(i, j)]\n",
" while i > 0 or j > 0:\n",
" minval = numpy.inf\n",
" if table[i-1][j] < minval:\n",
" minval = table[i-1][j]\n",
" step = (i-1, j)\n",
" if table[i][j-1] < minval:\n",
" minval = table[i][j-1]\n",
" step = (i, j-1)\n",
" if table[i-1][j-1] < minval:\n",
" minval = table[i-1][j-1]\n",
" step = (i-1, j-1)\n",
" path.insert(0, step)\n",
" i, j = step\n",
" return path"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's try it out:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false
},
"execution_count": 139,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[(0, 0), (1, 1), (2, 2), (3, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)]"
"[(0, 0),\n",
" (1, 0),\n",
" (1, 1),\n",
" (2, 2),\n",
" (2, 3),\n",
" (3, 3),\n",
" (3, 4),\n",
" (4, 5),\n",
" (4, 6),\n",
" (5, 7),\n",
" (6, 7),\n",
" (7, 8),\n",
" (7, 9)]"
]
},
"execution_count": 16,
"execution_count": 139,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"path = dtw(x, y)\n",
"path = dtw(x, y, table)\n",
"path"
]
},
......@@ -582,86 +451,61 @@
"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]"
"The time complexity of this operation is $O(N_x + N_y)$."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Sanity check: compute the total distance of this alignment:"
"As a sanity check, compute the total distance of this alignment:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"collapsed": false
},
"execution_count": 144,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"7"
"10"
]
},
"execution_count": 18,
"execution_count": 144,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sum(abs(x[i][0] - y[j][0]) for (i, j) in path)"
"sum(abs(x[i-1] - y[j-1]) for (i, j) in path if i >= 0 and j >= 0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Indeed, that is the same as the cumulative distance computed earlier, $D[N_x - 1, N_y - 1]$:"
"Indeed, that is the same as the cumulative distance of the optimal path computed earlier:"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"collapsed": false
},
"execution_count": 146,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"7.0"
"10.0"
]
},
"execution_count": 19,
"execution_count": 146,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"D[-1, -1]"
"table[-1][-1]"
]
},
{
......@@ -688,7 +532,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.10"
"version": "2.7.13"
}
},
"nbformat": 4,
......
......@@ -11940,6 +11940,7 @@ div#notebook {
<div class="text_cell_render border-box-sizing rendered_html">
<ol>
<li><a href="dp.html">Dynamic Programming</a> (<a href="dp.ipynb">ipynb</a>)</li>
<li><a href="lcs.html">Longest Common Subsequence</a> (<a href="lcs.ipynb">ipynb</a>)</li>
<li><a href="dtw.html">Dynamic Time Warping</a> (<a href="dtw.ipynb">ipynb</a>)</li>
</ol>
......
......@@ -137,6 +137,7 @@
"metadata": {},
"source": [
"1. [Dynamic Programming](dp.html) ([ipynb](dp.ipynb))\n",
"1. [Longest Common Subsequence](lcs.html) ([ipynb](lcs.ipynb))\n",
"1. [Dynamic Time Warping](dtw.html) ([ipynb](dtw.ipynb))"
]
},
......
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[&larr; Back to Index](index.html)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Longest Common Subsequence"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To motivate dynamic time warping, let's look at a classic dynamic programming problem: find the **longest common subsequence (LCS)** of two strings ([Wikipedia](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem)). A subsequence is not required to maintain consecutive positions in the original strings, but they must retain their order. Examples:\n",
"\n",
" lcs('cake', 'baker') -> 'ake'\n",
" lcs('cake', 'cape') -> 'cae'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can solve this using recursion. We must find the optimal substructure, i.e. decompose the problem into simpler subproblems. \n",
"\n",
"Let `x` and `y` be two strings. Let `z` be the true LCS of `x` and `y`. \n",
"\n",
"If the first characters of `x` and `y` were the same, then that character must also be the first character of the LCS, `z`. In other words, if `x[0] == y[0]`, then `z[0]` must equal `x[0]` (which equals `y[0]`). Therefore, append `x[0]` to `lcs(x[1:], y[1:])`.\n",
"\n",
"If the first characters of `x` and `y` differ, then solve for both `lcs(x, y[1:])` and `lcs(x[1:], y)`, and keep the result which is longer."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here is the recursive solution:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"def lcs(x, y):\n",
" if x == \"\" or y == \"\":\n",
" return \"\"\n",
" if x[0] == y[0]:\n",
" return x[0] + lcs(x[1:], y[1:])\n",
" else:\n",
" z1 = lcs(x[1:], y)\n",
" z2 = lcs(x, y[1:])\n",
" return z1 if len(z1) > len(z2) else z2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Test:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"pairs = [\n",
" ('cake', 'baker'),\n",
" ('cake', 'cape'),\n",
" ('catcga', 'gtaccgtca'),\n",
" ('zxzxzxmnxzmnxmznmzxnzm', 'nmnzxmxzmnzmx'),\n",
" ('dfkjdjkfdjkjfdkfdkfjd', 'dkfjdjkfjdkjfkdjfkjdkfjdkfj'),\n",
"]"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ake\n",
"cae\n",
"ctca\n",
"zxmxzmnzmx\n",
"dfjdjkfdjkjfdkfdkfj\n"
]
}
],
"source": [
"for x, y in pairs:\n",
" print lcs(x, y)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The time complexity of the above recursive method is $O(2^{n_x+n_y})$. That is slow because we might compute the solution to the same subproblem multiple times. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Memoization"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can do better through memoization, i.e. storing solutions to previous subproblems in a table."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here, we create a table where cell `(i, j)` stores the length `lcs(x[:i], y[:j])`. When either `i` or `j` is equal to zero, i.e. an empty string, we already know that the LCS is the empty string. Therefore, we can initialize the table to be equal to zero in all cells. Then we populate the table from the top left to the bottom right."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"def lcs_table(x, y):\n",
" nx = len(x)\n",
" ny = len(y)\n",
" \n",
" # Initialize a table.\n",
" table = [[0 for _ in range(ny+1)] for _ in range(nx+1)]\n",
" \n",
" # Fill the table.\n",
" for i in range(1, nx+1):\n",
" for j in range(1, ny+1):\n",
" if x[i-1] == y[j-1]:\n",
" table[i][j] = 1 + table[i-1][j-1]\n",
" else:\n",
" table[i][j] = max(table[i-1][j], table[i][j-1])\n",
" return table "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's visualize this table:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[[0, 0, 0, 0, 0, 0],\n",
" [0, 0, 0, 0, 0, 0],\n",
" [0, 0, 1, 1, 1, 1],\n",
" [0, 0, 1, 2, 2, 2],\n",
" [0, 0, 1, 2, 3, 3]]"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"x = 'cake'\n",
"y = 'baker'\n",
"table = lcs_table(x, y)\n",
"table"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" b a k e r\n",
" 0 0 0 0 0 0\n",
"c 0 0 0 0 0 0\n",
"a 0 0 1 1 1 1\n",
"k 0 0 1 2 2 2\n",
"e 0 0 1 2 3 3\n"
]
}
],
"source": [
"xa = ' ' + x\n",
"ya = ' ' + y\n",
"print ' '.join(ya)\n",
"for i, row in enumerate(table):\n",
" print xa[i], ' '.join(str(z) for z in row)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, we backtrack, i.e. read the table from the bottom right to the top left:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"def lcs(x, y, table, i=None, j=None):\n",
" if i is None:\n",
" i = len(x)\n",
" if j is None:\n",
" j = len(y)\n",
" if table[i][j] == 0:\n",
" return \"\"\n",
" elif x[i-1] == y[j-1]:\n",
" return lcs(x, y, table, i-1, j-1) + x[i-1]\n",
" elif table[i][j-1] > table[i-1][j]:\n",
" return lcs(x, y, table, i, j-1)\n",
" else:\n",
" return lcs(x, y, table, i-1, j)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ake\n",
"cae\n",
"ctca\n",
"zxmxzmnzmx\n",
"dfjdjkfdjkjfdkfdkfj\n"
]
}
],
"source": [
"for x, y in pairs:\n",
" table = lcs_table(x, y)\n",
" print lcs(x, y, table)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Table construction has time complexity $O(mn)$, and backtracking is $O(m+n)$. Therefore, the overall running time is $O(mn)$."
]
},
{
"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.13"
}
},
"nbformat": 4,
"nbformat_minor": 1
}
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