├── 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 | --------------------------------------------------------------------------------