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

338

¿Cuál es la forma más eficiente de asignar una función a 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?

Ryan
fuente
10
¿Por qué no "cuadrados = x ** 2"? ¿Tiene una función mucho más complicada que necesita evaluar?
22 grados
44
¿Qué tal solo squarer(x)?
Vida
1
Tal vez esto no esté respondiendo directamente a la pregunta, pero he oído que numba puede compilar el código python existente en instrucciones de máquina paralelas. Revisaré y revisaré esta publicación cuando tenga la oportunidad de usarla.
把 友情 留 在 无 盐
x = np.array([1, 2, 3, 4, 5]); x**2funciona
Shark Deng

Respuestas:

283

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

Mensaje n. ° 1: Si puede usar las funciones nativas de numpy, hágalo.

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

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí


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],
    xlabel="len(x)",
)
Nico Schlömer
fuente
77
Parece que te has quedado f(x)fuera de tu trama. Puede que no sea aplicable para todos f, pero es aplicable aquí, y es fácilmente la solución más rápida cuando corresponde.
user2357112 es compatible con Monica el
2
Además, su trama no respalda su reclamo que vf = np.vectorize(f); y = vf(x)gana por entradas cortas.
user2357112 es compatible con Monica el
Después de instalar perfplot (v0.3.2) a través de pip ( pip install -U perfplot), veo el mensaje: AttributeError: 'module' object has no attribute 'save'al pegar el código de ejemplo.
tsherwen
¿Qué tal un vainilla para loop?
Catiger3331
1
@Vlad simplemente usa math.sqrt como comentado.
Nico Schlömer
138

¿Qué hay de usar numpy.vectorize.

import numpy as np
x = np.array([1, 2, 3, 4, 5])
squarer = lambda t: t ** 2
vfunc = np.vectorize(squarer)
vfunc(x)
# Output : array([ 1,  4,  9, 16, 25])
satomacoto
fuente
36
Esto no es más eficiente.
user2357112 es compatible con Monica
78
De ese documento: The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop. en otras preguntas descubrí que vectorizepodría duplicar la velocidad de iteración del usuario. Pero la aceleración real es con numpyoperaciones de matriz real .
hpaulj
2
Tenga en cuenta que vectorize al menos hace que las cosas funcionen para arreglos no 1d
Eric
Pero squarer(x)ya funcionaría para matrices no 1d. vectorizesolo tiene alguna ventaja sobre la comprensión de una lista (como la de la pregunta), no termina squarer(x).
user2357112 es compatible con Monica el
79

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 np.vectorize, evítelo , ya que no funciona bien y tiene (o tuvo) varios problemas . 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, así que haga una prueba similar para su entorno.

Mike T
fuente
2
Si usa el countargumento y una expresión generadora, entonces np.fromiteres significativamente más rápido.
juanpa.arrivillaga
3
Entonces, por ejemplo, use'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))'
juanpa.arrivillaga el
44
No probaste la solución directa de f(x), que supera todo lo demás en un orden de magnitud .
user2357112 es compatible con Monica el
44
¿Qué pasa si ftiene 2 variables y la matriz es 2D?
Sigur
2
Estoy confundido acerca de cómo la versión 'f (x)' ("directa") se considera realmente comparable cuando el OP preguntaba cómo "asignar" una función a través de una matriz. En el caso de f (x) = x ** 2, ** se realiza mediante numpy en toda la matriz, no por elemento. Por ejemplo, si f (x) es 'lambda x: x + x ", entonces la respuesta es muy diferente porque numpy concatena las matrices en lugar de hacer la suma por elemento. ¿Es realmente la comparación prevista? Por favor explique.
Andrew Mellinger
49

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 del intérprete, el despacho dinámico y los objetos inmutables.

Entonces, qué maquinaria se usa para recorrer el conjunto no juega un papel importante debido a la sobrecarga mencionada anteriormente: se mantiene mucho más lenta que el uso de la funcionalidad incorporada 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.vectorizese selecciona como 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:

ingrese la descripción de la imagen aquí

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é.

También vale la pena mencionar que vectorizetambién usa mucha memoria, por lo que a menudo el uso de la memoria es el cuello de botella (consulte la pregunta SO relacionada ). También tenga en cuenta que la documentación de Numpy np.vectorizedice que "se proporciona principalmente por conveniencia, no por desempeño".

Deben usarse otras herramientas, cuando se desea rendimiento, además de escribir una extensión C desde cero, existen las siguientes posibilidades:


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:

ingrese la descripción de la imagen aquí

No puedo explicar todo en el diagrama 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 numpy original:

ingrese la descripción de la imagen aquí


Sin embargo, la tarea es vergonzosamente paralelizable, por lo que también podríamos usarla prangepara 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:

ingrese la descripción de la imagen aquí


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:

ingrese la descripción de la imagen aquí


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.

Al final, la respuesta definitiva depende del tipo de función, hardware, distribución de Python y otros factores. Por ejemplo Anaconda-de distribución utiliza VML de Intel para funciones de numpy y por lo tanto supera a numba (a menos que utiliza SVML, ver este SO-post ) fácilmente para funciones trascendentales como exp, sin, cosy similares - véase, por ejemplo la siguiente SO-post .

Sin embargo, a partir de esta investigación y de mi experiencia hasta el momento, afirmaría que la numba parece ser la herramienta más fácil con el mejor rendimiento siempre que no se involucren funciones trascendentales.


Trazado de tiempos de ejecución con perfplot -package :

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)'
    )
Ead
fuente
1
Numba puede hacer uso de Intel SVML, lo que resulta en tiempos bastante comparables en comparación con Intel VML, pero la implementación es un poco defectuosa en la versión (0.43-0.47). He agregado un diagrama de rendimiento stackoverflow.com/a/56939240/4045774 para compararlo con su cy_expsum.
max9111
29
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, ifno funciona por elementos. Desea 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)
user2357112 es compatible con Monica
fuente
9

En muchos casos, numpy.apply_along_axis será la mejor opción. Aumenta el rendimiento en aproximadamente 100 veces en comparación con los otros enfoques, y no solo para funciones de prueba triviales, sino también para composiciones de funciones más complejas de numpy y scipy.

Cuando agrego el método:

def along_axis(x):
    return np.apply_along_axis(f, 0, x)

al código del diagrama de trama, obtengo los siguientes resultados: ingrese la descripción de la imagen aquí

LyteFM
fuente
Estoy extremadamente conmocionado por el hecho de que la mayoría de la gente no parece estar al tanto de esta simple, escalable e incorporada obviedad durante tantos años ...
Bill Huang
8

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])
Peiti Li
fuente
3
Esto no es remotamente nuevo, siempre ha sido así, es una de las características principales de numpy.
Eric
8
Es el **operador el que aplica el cálculo a cada elemento t de t. Eso es ordinario numpy. Envolverlo en el lambdano hace nada extra.
hpaulj
Esto no funciona con las declaraciones if como se muestra actualmente.
TriHard8
7

Parece que nadie ha mencionado un método de fábrica incorporado para producir ufuncen un paquete numpy: np.frompyfuncque he probado nuevamente np.vectorizey lo he 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 quenp.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 aquí.

Wunderbar
fuente
1
Repetí las pruebas de tiempo anteriores, y también encontré una mejora en el rendimiento (sobre np.vectorize) de aproximadamente el 30%
Julian - BrainAnnex.org
2

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

numpy.fromiter((<some_func>(x) for x in <something>),<dtype>,<size of something>)
plátano
fuente
2

Todas las respuestas anteriores se comparan bien, pero si necesita usar una función personalizada para el mapeo, y tiene numpy.ndarray , y necesita conservar la forma de la matriz.

Comparé solo dos, pero conservará la forma de ndarray. He usado la matriz con 1 millón de entradas para comparar. Aquí utilizo la función cuadrada, que también está incorporada en numpy y tiene un gran aumento de rendimiento, ya que si era necesario algo, puede usar la función que prefiera.

import numpy, time
def timeit():
    y = numpy.arange(1000000)
    now = time.time()
    numpy.array([x * x for x in y.reshape(-1)]).reshape(y.shape)        
    print(time.time() - now)
    now = time.time()
    numpy.fromiter((x * x for x in y.reshape(-1)), y.dtype).reshape(y.shape)
    print(time.time() - now)
    now = time.time()
    numpy.square(y)  
    print(time.time() - now)

Salida

>>> timeit()
1.162431240081787    # list comprehension and then building numpy array
1.0775556564331055   # from numpy.fromiter
0.002948284149169922 # using inbuilt function

aquí puede ver claramente que numpy.fromiterfunciona muy bien teniendo en cuenta un enfoque simple, y si la función incorporada está disponible, úsela.

Rushikesh
fuente