├── setup.py
├── Makefile.dpcore_py
├── LICENSE
├── dp.py
├── README.md
├── dpcore_py.c
└── dpcore.py
/setup.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup, Extension
2 |
3 | module1 = Extension('_dpcore_py',
4 | sources = ['dpcore_py.c'])
5 |
6 | setup (name = '_dpcore_py',
7 | version = '0.0',
8 | description = 'Dynamic programming core routine',
9 | ext_modules = [module1])
10 |
--------------------------------------------------------------------------------
/Makefile.dpcore_py:
--------------------------------------------------------------------------------
1 | # Makefile for dpcore_py.c
2 | #
3 | # 2014-05-30 Dan Ellis dpwe@ee.columbia.edu
4 |
5 | # Compile rules for C-extension version of dpcore
6 |
7 | # darwinports
8 | #PYDIR=/opt/local/Library
9 | # homebrew
10 | PYDIR=/usr/local/Cellar/python/2.7.6
11 |
12 | CFLAGS=-I${PYDIR}/Frameworks/Python.framework/Versions/2.7/include/python2.7 -I${PYDIR}/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/numpy/core/include
13 |
14 |
15 | # ---- Link ---------------------------
16 | _dpcore_py.so: dpcore_py.o
17 | gcc -bundle -flat_namespace -undefined suppress -o _dpcore_py.so dpcore_py.o
18 |
19 | # ---- gcc C compile ------------------
20 | dpcore_py.o: dpcore_py.c
21 | gcc ${CFLAGS} -O3 -c dpcore_py.c
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Daniel P. W. Ellis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/dp.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # 3.0
3 |
4 | #
5 |
6 | %matplotlib inline
7 |
8 | #
9 |
10 | cd /Users/dpwe/projects/millionsong/python/midi-dataset
11 |
12 | #
13 |
14 | import numpy as np
15 | import matplotlib.pyplot as plt
16 | import dpcore
17 |
18 | #
19 |
20 | reload(dpcore)
21 |
22 | #
23 |
24 | M = np.random.rand(50,50)
25 | plt.imshow(M, interpolation='none', cmap='binary')
26 |
27 | #
28 |
29 | %timeit DC, phiC = dpcore.dpcore(M, 0.2, True)
30 | %timeit DP, phiP = dpcore.dpcore(M, 0.2, False)
31 |
32 | #
33 |
34 | DC, phiC = dpcore.dpcore(M, 0.2, True)
35 | DP, phiP = dpcore.dpcore(M, 0.2, False)
36 |
37 | #
38 |
39 | plt.imshow(DC,interpolation='none')
40 |
41 | #
42 |
43 | plt.imshow(DC-DP, interpolation='none')
44 | print np.max(np.abs(DC-DP))
45 |
46 | #
47 |
48 | plt.imshow(phiC-phiP, interpolation='none')
49 |
50 | #
51 |
52 | MM = np.random.rand(5, 5)
53 | pen = 0.2
54 | gut = 0.3
55 | p,q,C,phi = dpcore.dp(MM, pen, gut)
56 | print p, q
57 | print MM
58 | print C
59 | print "best cost =", C[p[-1],q[-1]], "=", np.sum(MM[p, q])+pen*(np.sum(phi[p, q]>0))
60 | plt.imshow(MM, interpolation='none', cmap='binary')
61 | plt.hold(True)
62 | plt.plot(q,p,'-r')
63 | plt.hold(False)
64 | plt.show()
65 |
66 | #
67 |
68 | M2 = np.copy(M)
69 | M2[20:30,20:30] += np.random.rand(10,10)
70 | M2[10:40,10:40] += np.random.rand(30,30)
71 | plt.imshow(M2, interpolation='none', cmap='binary')
72 | p,q,C,phi = dpcore.dp(M2,0.1,0.1)
73 | plt.hold(True)
74 | plt.plot(q,p,'-r')
75 | plt.hold(False)
76 | plt.show()
77 |
78 | #
79 |
80 | import librosa
81 |
82 | #
83 |
84 | # Mirror matlab example from http://www.ee.columbia.edu/ln/rosa/matlab/dtw/
85 | d1, sr = librosa.load('/Users/dpwe/projects/dtw/sm1_cln.wav', sr=16000)
86 | d2, sr = librosa.load('/Users/dpwe/projects/dtw/sm2_cln.wav', sr=16000)
87 | D1 = librosa.stft(d1, n_fft=512, hop_length=128)
88 | D2 = librosa.stft(d2, n_fft=512, hop_length=128)
89 | librosa.display.specshow(20*np.log10(np.abs(D1)), sr=sr, hop_length=128)
90 |
91 | #
92 |
93 | # Cosine similarity matrix (slow one-liner)
94 | SM = np.array([[np.sum(a*b)/np.sqrt(np.sum(a**2)*np.sum(b**2)) for b in np.abs(D2.T)] for a in np.abs(D1.T)])
95 |
96 | #
97 |
98 | plt.imshow(SM)
99 |
100 | #
101 |
102 | p, q, C, phi = dpcore.dp(1-SM)
103 |
104 | #
105 |
106 | plt.imshow(SM, interpolation='none', cmap='binary')
107 | plt.hold(True)
108 | plt.plot(q,p,'-r')
109 | plt.hold(False)
110 | plt.show()
111 |
112 | #
113 |
114 | C[-1,-1]
115 |
116 | #
117 |
118 |
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | dp_python
2 | =========
3 |
4 | Optimized Dynamic Programming (DP) / Dynamic Time Warp (DTW) as a Python external.
5 |
6 | Implments the classic dynamic programming best-path calculation. Because the inner loop is implemented as a C routine, it is 500-1000x faster than the equivalent pure Python.
7 |
8 | The external library needs to be compiled; this should be possible with `python setup.py build`. This creates the `_dpcore_py.so` file that needs to go in the same directory as `dpcore.py`. (If you're using HomeBrew on a Mac, you may be able to simply `make -f Makefile.dpcore_py` to create the compiled object.)
9 |
10 | See http://nbviewer.ipython.org/github/dpwe/dp_python/blob/master/dp.ipynb for an ipython notebook demonstrating the DTW alignment of two spoken utterances.
11 |
12 | Based on the Matlab DP external: http://labrosa.ee.columbia.edu/matlab/dtw/
13 |
14 |
15 |
16 | Functions in `dpcore.py`
17 | ------------------------
18 |
19 | #####`dp(local_costs, penalty=0.0, gutter=0.0)`
20 |
21 | Use dynamic programming to find a min-cost path through a matrix
22 | of local costs.
23 |
24 | **params**
25 |
26 | - local_costs : np.array of float
27 | - matrix of local costs at each cell
28 | - penalty : float
29 | - additional cost incurred by (0,1) and (1,0) steps [default: 0.0]
30 | - gutter : float
31 | - proportion of edge length away from [-1,-1] that best path will
32 | be accepted at. [default: 0.0 i.e. must reach top-right]
33 |
34 |
35 | **returns**
36 |
37 | - p, q : np.array of int
38 | - row and column indices of best path
39 | - total_costs : np.array of float
40 | - matrix of minimum costs to each point
41 | - phi : np.array of int
42 | - traceback matrix indicating preceding best-path step for each cell:
43 |
44 | - 0 -- diagonal predecessor
45 | - 1 -- previous column, same row
46 | - 2 -- previous row, same column
47 |
48 |
49 |
50 | **note**
51 | Port of Matlab routine `dp.m` (with some modifications). See
52 | http://labrosa.ee.columbia.edu/matlab/dtw/
53 |
54 |
55 |
56 | #####`dpcore(M, pen, use_extension=True)`
57 |
58 | Core dynamic programming calculation of best path.
59 |
60 | M[r,c] is the array of local costs.
61 | Create D[r,c] as the array of costs-of-best-paths to r,c,
62 | and phi[r,c] as the indicator of the point preceding [r,c] to
63 | allow traceback; 0 = (r-1,c-1), 1 = (r,c-1), 2 = (r-1, c)
64 |
65 | **params**
66 |
67 | - M : np.array of float
68 | - Matrix of local costs
69 | - pen : float
70 | - Penalty to apply for non-diagonal steps
71 | - use_extension : boolean
72 | - If False, use the pure-python parallel implementation [default: True]
73 |
74 |
75 | **returns**
76 |
77 | - D : np.array of float
78 | - Array of best costs to each point, starting from (0,0)
79 | - phi : np.array of int
80 | - Traceback indices indicating the last step taken by
81 | the lowest-cost path reaching this point. Values:
82 |
83 | - 0 -- previous point was r-1, c-1
84 | - 1 -- previous point was r, c-1
85 | - 2 -- previous point was r-1, c
86 |
87 |
88 |
--------------------------------------------------------------------------------
/dpcore_py.c:
--------------------------------------------------------------------------------
1 | /*
2 | * dpcore_py.c - calculate dynamic programming inner loop
3 | * Python extension version
4 | * 2014-05-30 Dan Ellis dpwe@ee.columbia.edu
5 | */
6 |
7 | /* see http://wiki.scipy.org/Cookbook/C_Extensions/NumPy_arrays */
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | static void calc_dpcore(
14 | double *pM, /* input scores */
15 | int rows, /* size of arrays */
16 | int cols,
17 | double pen, /* nondiagonal penalty score */
18 | double *pD, /* output best-cost matrix */
19 | int *pP /* output traceback matrix */
20 | )
21 | {
22 | /* Data is passed in as pointers to contiguous, row-first memory
23 | blocks for each array, that we interpret appropriately here */
24 | int i, j, k, tb;
25 | double d1, d2, d3, v;
26 | double *costs;
27 | int *steps;
28 | int ncosts;
29 |
30 | /* setup cost matrix */
31 | int ii;
32 |
33 | ncosts = 3;
34 | costs = (double *)malloc(ncosts*sizeof(double));
35 | steps = (int *)malloc(ncosts*2*sizeof(int));
36 | steps[0] = 1; steps[1] = 1; costs[0] = 0.0;
37 | steps[2] = 0; steps[3] = 1; costs[1] = pen;
38 | steps[4] = 1; steps[5] = 0; costs[2] = pen;
39 |
40 | /* do dp */
41 | v = pM[0];
42 | tb = 0; /* value to use for 0,0 */
43 | for (j = 0; j < cols; ++j) {
44 | for (i = 0; i < rows; ++i) {
45 | d1 = pM[i*cols + j];
46 | for (k = 0; k < ncosts; ++k) {
47 | if ( i >= steps[2*k] && j >= steps[2*k+1] ) {
48 | d2 = d1 + costs[k] + pD[(i-steps[2*k])*cols + (j-steps[2*k+1])];
49 | if (d2 < v) {
50 | v = d2;
51 | tb = k;
52 | }
53 | }
54 | }
55 |
56 | pD[i*cols + j] = v;
57 | pP[i*cols + j] = tb;
58 | v = NPY_INFINITY;
59 | }
60 | }
61 | free((void *)costs);
62 | free((void *)steps);
63 | }
64 |
65 | //////////////////////////// python extension wrapper /////////////////////
66 |
67 | /* ==== Check that PyArrayObject is a double (Float) type and a matrix ==========
68 | return 1 if an error and raise exception */
69 | int not_doublematrix(PyArrayObject *mat) {
70 | if (mat->descr->type_num != NPY_DOUBLE || mat->nd != 2) {
71 | PyErr_SetString(PyExc_ValueError,
72 | "In not_doublematrix: array must be of type Float and 2 dimensional (n x m).");
73 | return 1; }
74 | return 0;
75 | }
76 |
77 | static PyObject *
78 | dpcore_py_dpcore(PyObject *self, PyObject *args)
79 | {
80 | PyArrayObject *Sin, *Dout, *Pout;
81 | double pen;
82 | double *Sptr, *Dptr;
83 | int *Pptr;
84 | int nrows, ncols;
85 | npy_intp dims[2];
86 |
87 | /* parse input args */
88 | if (!PyArg_ParseTuple(args, "O!d",
89 | &PyArray_Type, &Sin, &pen))
90 | return NULL;
91 | if (Sin == NULL) return NULL;
92 |
93 | /* Check that object input is 'double' type and a matrix
94 | Not needed if python wrapper function checks before call to this routine */
95 | if (not_doublematrix(Sin)) return NULL;
96 |
97 | /* Get the dimension of the input */
98 | nrows = Sin->dimensions[0];
99 | ncols = Sin->dimensions[1];
100 |
101 | /* Set up output matrices */
102 | dims[0] = nrows;
103 | dims[1] = ncols;
104 | Dout = (PyArrayObject *)PyArray_SimpleNew(2, dims, NPY_DOUBLE);
105 | Pout = (PyArrayObject *)PyArray_SimpleNew(2, dims, NPY_INT);
106 |
107 | /* Change contiguous arrays into C *arrays */
108 | Sptr = (double *)Sin->data;
109 | Dptr = (double *)Dout->data;
110 | Pptr = (int *)Pout->data;
111 |
112 | /* run calculation */
113 | calc_dpcore(Sptr, nrows, ncols, pen, Dptr, Pptr);
114 |
115 | /* return the result */
116 | PyObject *tupleresult = PyTuple_New(2);
117 | PyTuple_SetItem(tupleresult, 0, PyArray_Return(Dout));
118 | PyTuple_SetItem(tupleresult, 1, PyArray_Return(Pout));
119 | return tupleresult;
120 | }
121 |
122 | /* standard hooks to Python, per http://docs.python.org/2/extending/extending.html */
123 |
124 | static PyMethodDef DpcoreMethods[] = {
125 | {"dpcore", dpcore_py_dpcore, METH_VARARGS},
126 | {NULL, NULL} /* Sentinel */
127 | };
128 |
129 |
130 | /* ==== Initialize the C_test functions ====================== */
131 | // Module name must be _dpcoremodule in compile and linked
132 | void init_dpcore_py() {
133 | (void) Py_InitModule("_dpcore_py", DpcoreMethods);
134 | import_array(); // Must be present for NumPy. Called first after above line.
135 | }
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/dpcore.py:
--------------------------------------------------------------------------------
1 | #
2 | # dpcore.py
3 | #
4 | # Dynamic Programming routine in Python
5 | # with optional C extension.
6 | #
7 | # 2014-05-30 Dan Ellis dpwe@ee.columbia.edu
8 |
9 | import numpy as np
10 |
11 | USE_EXTENSION = True
12 |
13 | if USE_EXTENSION:
14 | import _dpcore_py
15 |
16 | def dpcore(M, pen, use_extension=USE_EXTENSION):
17 | """Core dynamic programming calculation of best path.
18 | M[r,c] is the array of local costs.
19 | Create D[r,c] as the array of costs-of-best-paths to r,c,
20 | and phi[r,c] as the indicator of the point preceding [r,c] to
21 | allow traceback; 0 = (r-1,c-1), 1 = (r,c-1), 2 = (r-1, c)
22 |
23 | :params:
24 | M : np.array of float
25 | Array of local costs
26 | pen : float
27 | Penalty to apply for non-diagonal steps
28 |
29 | :returns:
30 | D : np.array of float
31 | Array of best costs to each point, starting from (0,0)
32 | phi : np.array of int
33 | Traceback indices indicating the last step taken by
34 | the lowest-cost path reaching this point. Values:
35 | 0 : previous point was r-1, c-1
36 | 1 : previous point was r, c-1
37 | 2 : previous point was r-1, c
38 | """
39 | if use_extension:
40 | D, phi = _dpcore_py.dpcore(M, pen)
41 | else:
42 | # Pure python equivalent
43 | D = np.zeros(M.shape, dtype=np.float)
44 | phi = np.zeros(M.shape, dtype=np.int)
45 | # bottom edge can only come from preceding column
46 | D[0,1:] = M[0,0]+np.cumsum(M[0,1:]+pen)
47 | phi[0,1:] = 1
48 | # left edge can only come from preceding row
49 | D[1:,0] = M[0,0]+np.cumsum(M[1:,0]+pen)
50 | phi[1:,0] = 2
51 | # initialize bottom left
52 | D[0,0] = M[0,0]
53 | phi[0,0] = 0
54 | # Calculate the rest recursively
55 | for c in range(1, np.shape(M)[1]):
56 | for r in range(1, np.shape(M)[0]):
57 | best_preceding_costs = [D[r-1,c-1], pen+D[r,c-1], pen+D[r-1, c]]
58 | tb = np.argmin(best_preceding_costs)
59 | D[r,c] = best_preceding_costs[tb] + M[r,c]
60 | phi[r,c] = tb
61 |
62 | return D, phi
63 |
64 | def dp(local_costs, penalty=0.0, gutter=0.0):
65 | """
66 | Use dynamic programming to find a min-cost path through a matrix
67 | of local costs.
68 |
69 | :params:
70 | local_costs : np.array of float
71 | matrix of local costs at each cell
72 | penalty : float
73 | additional cost incurred by (0,1) and (1,0) steps [default: 0.0]
74 | gutter : float
75 | proportion of edge length away from [-1,-1] that best path will
76 | be accepted at. [default: 0.0 i.e. must reach top-right]
77 |
78 | :returns:
79 | p, q : np.array of int
80 | row and column indices of best path
81 | total_costs : np.array of float
82 | matrix of minimum costs to each point
83 | phi : np.array of int
84 | traceback matrix indicating preceding best-path step for each cell:
85 | 0 diagonal predecessor
86 | 1 previous column, same row
87 | 2 previous row, same column
88 |
89 | :note:
90 | port of Matlab routine dp.m,
91 | http://labrosa.ee.columbia.edu/matlab/dtw/
92 | """
93 | rows, cols = np.shape(local_costs)
94 | total_costs = np.zeros( (rows+1, cols+1), np.float)
95 | total_costs[0,:] = np.inf
96 | total_costs[:,0] = np.inf
97 | total_costs[0,0] = 0
98 | # add gutters at start too
99 | colgutter = int(np.maximum(1, np.round(gutter*cols)))
100 | total_costs[0, :colgutter] = -penalty * np.arange(colgutter)
101 | rowgutter = int(np.maximum(1, np.round(gutter*rows)))
102 | total_costs[:rowgutter, 0] = -penalty * np.arange(rowgutter)
103 | # copy in local costs
104 | total_costs[1:(rows+1), 1:(cols+1)] = local_costs
105 |
106 | # Core routine to calculate matrix of min costs
107 | total_costs, phi = dpcore(total_costs, penalty)
108 |
109 | # Strip off the edges of the matrices used to create gutters
110 | total_costs = total_costs[1:, 1:]
111 | phi = phi[1:,1:]
112 |
113 | if gutter == 0:
114 | # Traceback from top left
115 | i = rows-1
116 | j = cols-1
117 | else:
118 | # Traceback from lowest cost "to edge" (gutters)
119 | best_top_pt = (cols - colgutter
120 | + np.argmin(total_costs[-1, -colgutter:]))
121 | best_right_pt = (rows - rowgutter
122 | + np.argmin(total_costs[-rowgutter:, -1]))
123 | if total_costs[-1, best_top_pt] < total_costs[best_right_pt, -1]:
124 | i = rows - 1
125 | j = best_top_pt
126 | else:
127 | i = best_right_pt
128 | j = cols - 1
129 |
130 | # Do traceback from best end point to find best path
131 | # Start from lowest-total-cost point
132 | p = [i]
133 | q = [j]
134 | # Work backwards until we get to starting point (0, 0)
135 | while i >= 0 and j >= 0:
136 | tb = phi[i,j];
137 | if (tb == 0):
138 | i = i-1
139 | j = j-1
140 | elif (tb == 1):
141 | j = j-1
142 | elif (tb == 2):
143 | i = i-1
144 | p.insert(0, i)
145 | q.insert(0, j)
146 |
147 | return p[1:], q[1:], total_costs, phi
148 |
149 |
150 |
--------------------------------------------------------------------------------