una tutorial transpuesta matriz matrices funcion español elementos ejemplos array arange agregar python performance numpy

python - tutorial - La forma más eficiente de asignar funciones sobre una matriz numpy



python matrices tutorial (9)

¿Cuál es la forma más eficiente de asignar una función sobre una matriz numpy? La forma en que lo he estado haciendo en mi proyecto actual es la siguiente:

import numpy as np x = np.array([1, 2, 3, 4, 5]) # Obtain array of square of each element in x squarer = lambda t: t ** 2 squares = np.array([squarer(xi) for xi in x])

Sin embargo, esto parece ser probablemente muy ineficiente, ya que estoy usando una comprensión de lista para construir la nueva matriz como una lista de Python antes de convertirla nuevamente en una matriz numpy.

¿Podemos hacerlo mejor?


TL; DR

Como señaló @user2357112 , un método "directo" para aplicar la función es siempre la forma más rápida y sencilla de mapear una función sobre matrices Numpy:

import numpy as np x = np.array([1, 2, 3, 4, 5]) f = lambda x: x ** 2 squares = f(x)

En general, evite np.vectorize , ya que no funciona bien y tiene (o ha tenido) varios issues . Si está manejando otros tipos de datos, es posible que desee investigar los otros métodos que se muestran a continuación.

Comparación de métodos

Aquí hay algunas pruebas simples para comparar tres métodos para mapear una función, este ejemplo con Python 3.6 y NumPy 1.15.4. Primero, las funciones de configuración para probar:

import timeit import numpy as np f = lambda x: x ** 2 vf = np.vectorize(f) def test_array(x, n): t = timeit.timeit( ''np.array([f(xi) for xi in x])'', ''from __main__ import np, x, f'', number=n) print(''array: {0:.3f}''.format(t)) def test_fromiter(x, n): t = timeit.timeit( ''np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))'', ''from __main__ import np, x, f'', number=n) print(''fromiter: {0:.3f}''.format(t)) def test_direct(x, n): t = timeit.timeit( ''f(x)'', ''from __main__ import x, f'', number=n) print(''direct: {0:.3f}''.format(t)) def test_vectorized(x, n): t = timeit.timeit( ''vf(x)'', ''from __main__ import x, vf'', number=n) print(''vectorized: {0:.3f}''.format(t))

Prueba con cinco elementos (ordenados del más rápido al más lento):

x = np.array([1, 2, 3, 4, 5]) n = 100000 test_direct(x, n) # 0.265 test_fromiter(x, n) # 0.479 test_array(x, n) # 0.865 test_vectorized(x, n) # 2.906

Con cientos de elementos:

x = np.arange(100) n = 10000 test_direct(x, n) # 0.030 test_array(x, n) # 0.501 test_vectorized(x, n) # 0.670 test_fromiter(x, n) # 0.883

Y con miles de elementos de matriz o más:

x = np.arange(1000) n = 1000 test_direct(x, n) # 0.007 test_fromiter(x, n) # 0.479 test_array(x, n) # 0.516 test_vectorized(x, n) # 0.945

Las diferentes versiones de Python / NumPy y la optimización del compilador tendrán resultados diferentes, por lo tanto, realice una prueba similar para su entorno.



Como esta pregunta fue respondida, sucedieron muchas cosas: hay numexpr , numba y cython . El objetivo de esta respuesta es tener en cuenta estas posibilidades.

Pero primero expongamos lo obvio: no importa cómo mapees una función de Python en una matriz numpy, sigue siendo una función de Python, eso significa para cada evaluación:

  • El elemento numpy-array debe convertirse en un objeto Python (por ejemplo, un Float ).
  • todos los cálculos se realizan con objetos Python, lo que significa tener la sobrecarga de intérprete, despacho dinámico y objetos inmutables.

Entonces, qué maquinaria se utiliza para recorrer la matriz no juega un papel importante debido a la sobrecarga mencionada anteriormente: se mantiene mucho más lenta que el uso de la vectorización de Numpy.

Echemos un vistazo al siguiente ejemplo:

# numpy-functionality def f(x): return x+2*x*x+4*x*x*x # python-function as ufunc import numpy as np vf=np.vectorize(f) vf.__name__="vf"

np.vectorize se np.vectorize como un representante de la clase de enfoques de la función de python puro. Usando perfplot (vea el código en el apéndice de esta respuesta) obtenemos los siguientes tiempos de ejecución:

Podemos ver que el enfoque numpy es 10x-100x más rápido que la versión pura de Python. La disminución del rendimiento para tamaños de matriz más grandes probablemente se deba a que los datos ya no se ajustan al caché.

A menudo se escucha que el rendimiento de numpy es tan bueno como es posible, porque es puro C debajo del capó. ¡Sin embargo, hay mucho margen de mejora!

La versión numpy vectorizada utiliza mucha memoria adicional y accesos a la memoria. Numexp-library intenta enlosar las matrices numpy y así obtener una mejor utilización de la caché:

# less cache misses than numpy-functionality import numexpr as ne def ne_f(x): return ne.evaluate("x+2*x*x+4*x*x*x")

Lleva a la siguiente comparación:

No puedo explicar todo en el gráfico anterior: podemos ver una sobrecarga mayor para numexpr-library al principio, pero debido a que utiliza mejor el caché, ¡es aproximadamente 10 veces más rápido para matrices más grandes!

Otro enfoque es compilar jit la función y así obtener un UFunc puro en C real. Este es el enfoque de numba:

# runtime generated C-function as ufunc import numba as nb @nb.vectorize(target="cpu") def nb_vf(x): return x+2*x*x+4*x*x*x

Es 10 veces más rápido que el enfoque original de numpy:

Sin embargo, la tarea es vergonzosamente paralelizable, por lo que también podríamos usar prange para calcular el ciclo en paralelo:

@nb.njit(parallel=True) def nb_par_jitf(x): y=np.empty(x.shape) for i in nb.prange(len(x)): y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i] return y

Como se esperaba, la función paralela es más lenta para entradas más pequeñas, pero más rápida (casi factor 2) para tamaños más grandes:

Mientras que numba se especializa en optimizar operaciones con matrices numpy, Cython es una herramienta más general. Es más complicado extraer el mismo rendimiento que con numba: a menudo se reduce a llvm (numba) frente al compilador local (gcc / MSVC):

%%cython -c=/openmp -a import numpy as np import cython #single core: @cython.boundscheck(False) @cython.wraparound(False) def cy_f(double[::1] x): y_out=np.empty(len(x)) cdef Py_ssize_t i cdef double[::1] y=y_out for i in range(len(x)): y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i] return y_out #parallel: from cython.parallel import prange @cython.boundscheck(False) @cython.wraparound(False) def cy_par_f(double[::1] x): y_out=np.empty(len(x)) cdef double[::1] y=y_out cdef Py_ssize_t i cdef Py_ssize_t n = len(x) for i in prange(n, nogil=True): y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i] return y_out

Cython resulta en funciones algo más lentas:

Conclusión

Obviamente, probar solo una función no prueba nada. También se debe tener en cuenta que, para el ejemplo de función elegido, el ancho de banda de la memoria era el cuello de botella para tamaños mayores de 10 ^ 5 elementos, por lo que tuvimos el mismo rendimiento para numba, numexpr y cython en esta región.

Sin embargo, a partir de esta investigación y de mi experiencia hasta ahora, diría que la numba parece ser la herramienta más fácil con el mejor rendimiento.

Trazado de tiempos de ejecución con perfplot :

import perfplot perfplot.show( setup=lambda n: np.random.rand(n), n_range=[2**k for k in range(0,24)], kernels=[ f, vf, ne_f, nb_vf, nb_par_jitf, cy_f, cy_par_f, ], logx=True, logy=True, xlabel=''len(x)'' )


Como se menciona en esta publicación , solo use expresiones generadoras como esta:

numpy.fromiter((<some_func>(x) for x in <something>),<dtype>,<size of something>)


Creo que en la versión más nueva (uso 1.13) de numpy, simplemente puede llamar a la función pasando la matriz numpy a la función que escribió para el tipo escalar, aplicará automáticamente la llamada de función a cada elemento sobre la matriz numpy y le devolverá otra matriz numpy

>>> import numpy as np >>> squarer = lambda t: t ** 2 >>> x = np.array([1, 2, 3, 4, 5]) >>> squarer(x) array([ 1, 4, 9, 16, 25])


He probado todos los métodos sugeridos más np.array(map(f, x)) con perfplot (un pequeño proyecto mío).

Mensaje # 1: Si puedes usar las funciones nativas de numpy, hazlo.

Si la función que está tratando de vectorizar ya está vectorizada (como el ejemplo x**2 en la publicación original), usarla es mucho más rápido que cualquier otra cosa (tenga en cuenta la escala logarítmica):

Si realmente necesita vectorización, realmente no importa mucho qué variante use.

Código para reproducir las tramas:

import numpy as np import perfplot import math def f(x): # return math.sqrt(x) return np.sqrt(x) vf = np.vectorize(f) def array_for(x): return np.array([f(xi) for xi in x]) def array_map(x): return np.array(list(map(f, x))) def fromiter(x): return np.fromiter((f(xi) for xi in x), x.dtype) def vectorize(x): return np.vectorize(f)(x) def vectorize_without_init(x): return vf(x) perfplot.show( setup=lambda n: np.random.rand(n), n_range=[2**k for k in range(20)], kernels=[ f, array_for, array_map, fromiter, vectorize, vectorize_without_init ], logx=True, logy=True, xlabel=''len(x)'', )


Parece que nadie ha mencionado un método de fábrica ufunc producir ufunc en un paquete np.frompyfunc : np.frompyfunc que he probado nuevamente np.vectorize y lo hemos superado en un 20 ~ 30%. Por supuesto, funcionará bien según el código C prescrito o incluso numba (que no he probado), pero puede ser una mejor alternativa que np.vectorize

f = lambda x, y: x * y f_arr = np.frompyfunc(f, 2, 1) vf = np.vectorize(f) arr = np.linspace(0, 1, 10000) %timeit f_arr(arr, arr) # 307ms %timeit vf(arr, arr) # 450ms

También he probado muestras más grandes, y la mejora es proporcional. Vea la documentación también here


Quizás usar vectorize sea mejor

def square(x): return x**2 vfunc=vectorize(square) vfunc([1,2,3,4,5]) output:array([ 1, 4, 9, 16, 25])


squares = squarer(x)

Las operaciones aritméticas en matrices se aplican automáticamente por elementos, con bucles eficientes de nivel C que evitan toda la sobrecarga del intérprete que se aplicaría a un bucle o comprensión de nivel Python.

La mayoría de las funciones que desearía aplicar a una matriz NumPy por elementos simplemente funcionarán, aunque algunas pueden necesitar cambios. Por ejemplo, if no funciona por elementos. numpy.where convertirlos para usar construcciones como numpy.where :

def using_if(x): if x < 5: return x else: return x**2

se convierte

def using_where(x): return numpy.where(x < 5, x, x**2)