python arrays numpy min

python - ¿Hay una manera de hacer numpy.argmin() tan rápido como min()?



arrays (3)

Estoy tratando de encontrar los índices de matriz mínima a lo largo de una dimensión de una matriz numpy 2D muy grande. Estoy descubriendo que esto es muy lento (ya intenté acelerarlo con un cuello de botella, lo cual fue solo una mejora mínima). Sin embargo, tomar el mínimo recto parece ser un orden de magnitud más rápido:

import numpy as np import time randvals = np.random.rand(3000,160000) start = time.time() minval = randvals.min(axis=0) print "Took {0:.2f} seconds to compute min".format(time.time()-start) start = time.time() minindex = np.argmin(randvals,axis=0) print "Took {0:.2f} seconds to compute argmin".format(time.time()-start)

En mi máquina esto produce:

Took 0.83 seconds to compute min Took 9.58 seconds to compute argmin

¿Hay alguna razón por la que argmin es mucho más lento? ¿Hay alguna manera de acelerar hasta comparable a min?


Estaba teniendo el mismo problema y encontré la gran diferencia en el rendimiento cuando se selecciona el eje 0 para encontrar el mínimo. Como se sugirió, el problema parece estar relacionado con la copia interna de la matriz.

Diseñé una solución alternativa bastante simple utilizando Cython que establece simultáneamente los valores mínimos y su posición en una matriz de números 2D a lo largo de un eje dado. Tenga en cuenta que para axis = 0 , el algoritmo funciona en una matriz de columnas (número máximo especificado por el blocksize constante; aquí se establece el equivalente a 8 kByte de datos) simultáneamente para hacer un buen uso de la caché:

%%cython -c=-O2 -c=-march=native import numpy as np cimport numpy as np cimport cython from libc.stdint cimport uint32_t @cython.boundscheck(False) @cython.wraparound(False) cdef void _minargmin_2d_64_axis_0(uint32_t[:] minloc, double[:] minimum, double[:, :] arr) nogil: """ Find the minimum and argmin for a 2D array of 64-bit floats along axis 0 Parameters: ----------- arr: numpy_array, dtype=np.float64, shape=(m, n) The array for which the minima should be computed. minloc: numpy_array, dtype=np.uint32, shape=(n) Stores the rows where the minima occur for each column. minimum: numpy_array, dtype=np.float64, shape=(n) The minima along each column. """ # Columns of the matrix are accessed in blocks to increase cache performance. # Specify the number of columns here: DEF blocksize = 1024 cdef int i, j, k cdef double minim[blocksize] cdef uint32_t minimloc[blocksize] # Read blocks of data to make good use of the cache cdef int blocks blocks = arr.shape[1] / blocksize remains = arr.shape[1] % blocksize for i in xrange(0, blocksize * blocks, blocksize): for k in xrange(blocksize): minim[k] = arr[0, i + k] minimloc[k] = 0 for j in xrange(1, arr.shape[0]): for k in xrange(blocksize): if arr[j, i + k] < minim[k]: minim[k] = arr[j, i + k] minimloc[k] = j for k in xrange(blocksize): minimum[i + k] = minim[k] minloc[i + k] = minimloc[k] # Work on the final ''incomplete'' block i = blocksize * blocks for k in xrange(remains): minim[k] = arr[0, i + k] minimloc[k] = 0 for j in xrange(1, arr.shape[0]): for k in xrange(remains): if arr[j, i + k] < minim[k]: minim[k] = arr[j, i + k] minimloc[k] = j for k in xrange(remains): minimum[i + k] = minim[k] minloc[i + k] = minimloc[k] # Done! @cython.boundscheck(False) @cython.wraparound(False) cdef void _minargmin_2d_64_axis_1(uint32_t[:] minloc, double[:] minimum, double[:, :] arr) nogil: """ Find the minimum and argmin for a 2D array of 64-bit floats along axis 1 Parameters: ----------- arr: numpy_array, dtype=np.float64, shape=(m, n) The array for which the minima should be computed. minloc: numpy_array, dtype=np.uint32, shape=(m) Stores the rows where the minima occur for each row. minimum: numpy_array, dtype=np.float64, shape=(m) The minima along each row. """ cdef int i cdef int j cdef double minim cdef uint32_t minimloc for i in xrange(arr.shape[0]): minim = arr[i, 0] minimloc = 0 for j in xrange(1, arr.shape[1]): if arr[i, j] < minim: minim = arr[i, j] minimloc = j minimum[i] = minim minloc[i] = minimloc @cython.boundscheck(False) @cython.wraparound(False) cdef void _minargmin_2d_32_axis_0(uint32_t[:] minloc, float[:] minimum, float[:, :] arr) nogil: """ Find the minimum and argmin for a 2D array of 32-bit floats along axis 0 Parameters: ----------- arr: numpy_array, dtype=np.float32, shape=(m, n) The array for which the minima should be computed. minloc: numpy_array, dtype=np.uint32, shape=(n) Stores the rows where the minima occur for each column. minimum: numpy_array, dtype=np.float32, shape=(n) The minima along each column. """ # Columns of the matrix are accessed in blocks to increase cache performance. # Specify the number of columns here: DEF blocksize = 2048 cdef int i, j, k cdef float minim[blocksize] cdef uint32_t minimloc[blocksize] # Read blocks of data to make good use of the cache cdef int blocks blocks = arr.shape[1] / blocksize remains = arr.shape[1] % blocksize for i in xrange(0, blocksize * blocks, blocksize): for k in xrange(blocksize): minim[k] = arr[0, i + k] minimloc[k] = 0 for j in xrange(1, arr.shape[0]): for k in xrange(blocksize): if arr[j, i + k] < minim[k]: minim[k] = arr[j, i + k] minimloc[k] = j for k in xrange(blocksize): minimum[i + k] = minim[k] minloc[i + k] = minimloc[k] # Work on the final ''incomplete'' block i = blocksize * blocks for k in xrange(remains): minim[k] = arr[0, i + k] minimloc[k] = 0 for j in xrange(1, arr.shape[0]): for k in xrange(remains): if arr[j, i + k] < minim[k]: minim[k] = arr[j, i + k] minimloc[k] = j for k in xrange(remains): minimum[i + k] = minim[k] minloc[i + k] = minimloc[k] # Done! @cython.boundscheck(False) @cython.wraparound(False) cdef void _minargmin_2d_32_axis_1(uint32_t[:] minloc, float[:] minimum, float[:, :] arr) nogil: """ Find the minimum and argmin for a 2D array of 32-bit floats along axis 1 Parameters: ----------- arr: numpy_array, dtype=np.float32, shape=(m, n) The array for which the minima should be computed. minloc: numpy_array, dtype=np.uint32, shape=(m) Stores the rows where the minima occur for each row. minimum: numpy_array, dtype=np.float32, shape=(m) The minima along each row. """ cdef int i cdef int j cdef float minim cdef uint32_t minimloc for i in xrange(arr.shape[0]): minim = arr[i, 0] minimloc = 0 for j in xrange(1, arr.shape[1]): if arr[i, j] < minim: minim = arr[i, j] minimloc = j minimum[i] = minim minloc[i] = minimloc def Min_Argmin(array_2d, axis = 1): """ Find the minima and corresponding locations (argmin) of a two-dimensional numpy array of floats along a given axis simultaneously, and returns them as a tuple of arrays (min_2d, argmin_2d). (Note: float16 arrays will be internally transformed to float32 arrays.) ---------- array_2d: numpy_array, dtype=np.float32 or np.float64, shape=(m, n) The array for which the minima should be computed. axis : int, 0 or 1 (default) : The axis along which minima are computed. min_2d: numpy_array, dtype=np.uint8, shape=(m) or shape=(n): The array where the minima along the given axis are stored. argmin_2d: The array storing the row/column where the minimum occurs. """ # Sanity checks: if len(array_2d.shape) != 2: raise IndexError(''Min_Argmin: Number of dimensions of array must be 2'') if not (axis == 0 or axis == 1): raise ValueError(''Min_Argmin: Invalid axis specified'') arr_type = array_2d.dtype if not arr_type in (''float16'', ''float32'', ''float64''): raise ValueError(''Min_Argmin: Not a float array'') # Transform float16 arrays if arr_type == ''float16'': array_2d = array_2d.astype(np.float32) # Run analysis if arr_type == ''float64'': # Double accuracy min_array = np.zeros(array_2d.shape[1 - axis], dtype = np.float64) argmin_array = np.zeros(array_2d.shape[1 - axis], dtype = np.uint32) if (axis == 0): _minargmin_2d_64_axis_0(argmin_array, min_array, array_2d) else: _minargmin_2d_64_axis_1(argmin_array, min_array, array_2d) else: # Single accuracy min_array = np.zeros(array_2d.shape[1 - axis], dtype = np.float32) argmin_array = np.zeros(array_2d.shape[1 - axis], dtype = np.uint32) if (axis == 0): _minargmin_2d_32_axis_0(argmin_array, min_array, array_2d) else: _minargmin_2d_32_axis_1(argmin_array, min_array, array_2d) # Transform back to float16 type as necessary if arr_type == ''float16'': min_array = min_array.astype(np.float16) # Return results return min_array, argmin_array

El código se puede colocar y compilar en una celda de notebook IPython después de cargar el soporte de Cython:

%load_ext Cython

y luego llamado en el formulario:

min_array, argmin_array = Min_Argmin(two_dim_array, axis = 0 or 1)

Ejemplo de tiempo:

random_array = np.random.rand(20000, 20000).astype(np.float32) %timeit min_array, argmin_array = Min_Argmin(random_array, axis = 0) %timeit min_array, argmin_array = Min_Argmin(random_array, axis = 1) 1 loops, best of 3: 405 ms per loop 1 loops, best of 3: 307 ms per loop

Para comparacion:

%%timeit min_array = random_array.min(axis = 0) argmin_array = random_array.argmin(axis = 0) 1 loops, best of 3: 10.3 s per loop %%timeit min_array = random_array.min(axis = 1) argmin_array = random_array.argmin(axis = 1) 1 loops, best of 3: 423 ms per loop

Por lo tanto, hay una aceleración significativa para el axis = 0 (y aún una pequeña ventaja para el axis = 1 , si uno está interesado tanto en el mínimo como en la ubicación).


Simplemente eché un vistazo al código fuente y, si bien no entiendo completamente por qué las cosas se están haciendo como están, esto es lo que sucede:

  1. np.min es básicamente una llamada a np.minimum.reduce .

  2. np.argmin primero mueve el eje sobre el que quiere operar hasta el final de la tupla de forma, luego lo convierte en una matriz contigua, lo que, por supuesto, activa una copia de la matriz completa a menos que el eje sea el último para comenzar.

Como se está haciendo una copia, puede ser creativo e intentar crear instancias más baratas:

a = np.random.rand(1000, 2000) def fast_argmin_axis_0(a): matches = np.nonzero((a == np.min(a, axis=0)).ravel())[0] rows, cols = np.unravel_index(matches, a.shape) argmin_array = np.empty(a.shape[1], dtype=np.intp) argmin_array[cols] = rows return argmin_array In [8]: np.argmin(a, axis=0) Out[8]: array([230, 532, 815, ..., 670, 702, 989], dtype=int64) In [9]: fast_argmin_axis_0(a) Out[9]: array([230, 532, 815, ..., 670, 702, 989], dtype=int64) In [10]: %timeit np.argmin(a, axis=0) 10 loops, best of 3: 27.3 ms per loop In [11]: %timeit fast_argmin_axis_0(a) 100 loops, best of 3: 15 ms per loop

No me atrevería a llamar un error a la implementación actual, ya que puede haber buenas razones para hacer lo que hace como lo hace, pero este tipo de trucos puede acelerar lo que debería ser una función altamente optimizada. sugiere firmemente que las cosas se podrían hacer mejor.


In [1]: import numpy as np In [2]: a = np.random.rand(3000, 16000) In [3]: %timeit a.min(axis=0) 1 loops, best of 3: 421 ms per loop In [4]: %timeit a.argmin(axis=0) 1 loops, best of 3: 1.95 s per loop In [5]: %timeit a.min(axis=1) 1 loops, best of 3: 302 ms per loop In [6]: %timeit a.argmin(axis=1) 1 loops, best of 3: 303 ms per loop In [7]: %timeit a.T.argmin(axis=1) 1 loops, best of 3: 1.78 s per loop In [8]: %timeit np.asfortranarray(a).argmin(axis=0) 1 loops, best of 3: 1.97 s per loop In [9]: b = np.asfortranarray(a) In [10]: %timeit b.argmin(axis=0) 1 loops, best of 3: 329 ms per loop

¿Tal vez min es lo suficientemente inteligente como para hacer su trabajo secuencialmente sobre la matriz (por lo tanto, con la localidad de caché), y argmin está saltando alrededor de la matriz (causando una gran cantidad de fallas de caché)?

De todos modos, si estás dispuesto a mantener randvals como una matriz ordenada por Fortran desde el principio, será más rápido, aunque copiar en Fortran ordenado no ayuda.