Evaluación eficiente de una función en cada celda de una matriz NumPy

124

Dada una matriz NumPy A , ¿cuál es la forma más rápida / eficiente de aplicar la misma función, f , a cada celda?

  1. Supongamos que asignaremos a A (i, j) la f (A (i, j)) .

  2. La función, f , no tiene una salida binaria, por lo que las operaciones de máscara (ing) no ayudarán.

¿Es la iteración doble "obvia" (a través de cada celda) la solución óptima?

Peter
fuente

Respuestas:

165

Puede simplemente vectorizar la función y luego aplicarla directamente a una matriz de Numpy cada vez que la necesite:

import numpy as np

def f(x):
    return x * x + 3 * x - 2 if x > 0 else x * 5 + 8

f = np.vectorize(f)  # or use a different name if you want to keep the original f

result_array = f(A)  # if A is your Numpy array

Probablemente sea mejor especificar un tipo de salida explícito directamente al vectorizar:

f = np.vectorize(f, otypes=[np.float])
blubberdiblub
fuente
19
Me temo que la función vectorizada no puede ser más rápida que la iteración de doble ciclo "manual" y la asignación a través de todos los elementos de la matriz. Especialmente, porque almacena el resultado en una variable recién creada (y no directamente en la entrada inicial). Muchas gracias por su respuesta :) :)
Peter
1
@ Peter: Ah, ahora veo que has mencionado la asignación del resultado a la matriz anterior en tu pregunta original. Lo siento, me perdí eso cuando lo leí por primera vez. Sí, en ese caso el doble circuito debe ser más rápido. Pero, ¿también ha probado un solo bucle en la vista aplanada de la matriz? Eso podría ser un poco más rápido, ya que ahorra una pequeña sobrecarga de bucle y Numpy necesita hacer una multiplicación y adición menos (para calcular el desplazamiento de datos) en cada iteración. Además, funciona para arreglos dimensionados arbitrariamente. Puede ser más lento en matrices muy pequeñas, aunque.
blubberdiblub
45
Observe la advertencia dada en la vectorizedescripción de la función: la función vectorizar se proporciona principalmente por conveniencia, no por rendimiento. La implementación es esencialmente un bucle for. Por lo tanto, es muy probable que esto no acelere el proceso en absoluto.
Gabriel
Presta atención a cómo vectorizedetermina el tipo de retorno. Eso ha producido errores. frompyfunces un poco más rápido, pero devuelve una matriz de objetos dtype. Ambos alimentan escalares, no filas o columnas.
hpaulj
1
@Gabriel Simplemente activando np.vectorizemi función (que utiliza RK45) me da una aceleración de un factor de ~ 20.
Suuuehgi
0

Creo que he encontrado una mejor solución. La idea de cambiar la función a la función universal de Python (ver documentación ), que puede ejercer un cálculo paralelo bajo el capó.

Uno puede escribir el suyo personalizado ufuncen C, que seguramente es más eficiente, o invocando np.frompyfunc, que es un método de fábrica incorporado. Después de la prueba, esto es más eficiente 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 f_arr(arr, arr) # 450ms

También he probado muestras más grandes, y la mejora es proporcional. Para comparar el rendimiento de otros métodos, vea esta publicación

Wunderbar
fuente
0

Cuando el 2d-array (o nd-array) es contiguo en C o F, entonces esta tarea de mapear una función en un 2d-array es prácticamente la misma que la tarea de mapear una función en un 1d-array - simplemente tiene que verlo de esa manera, por ejemplo, a través de np.ravel(A,'K').

La posible solución para 1d-array se ha discutido, por ejemplo, aquí .

Sin embargo, cuando la memoria de la matriz 2d no es contigua, entonces la situación es un poco más complicada, porque a uno le gustaría evitar posibles errores de caché si el eje se maneja en el orden incorrecto.

Numpy ya cuenta con una maquinaria para procesar ejes en el mejor orden posible. Una posibilidad para usar esta maquinaria es np.vectorize. Sin embargo, la documentación de numpy np.vectorizedice que "se proporciona principalmente por conveniencia, no por rendimiento": ¡una función lenta de Python sigue siendo una función lenta de Python con toda la sobrecarga asociada! Otro problema es su gran consumo de memoria; consulte, por ejemplo, esta publicación SO .

Cuando se quiere tener el rendimiento de una función C pero usar la maquinaria de numpy, una buena solución es usar numba para la creación de ufuncs, por ejemplo:

# 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

Supera fácilmente, np.vectorizepero también cuando la misma función se realizaría como multiplicación / suma de matriz numpy, es decir

# 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"

Vea el apéndice de esta respuesta para el código de medición de tiempo:

ingrese la descripción de la imagen aquí

La versión de Numba (verde) es aproximadamente 100 veces más rápida que la función python (es decir np.vectorize), lo cual no es sorprendente. Pero también es aproximadamente 10 veces más rápido que la funcionalidad numpy, porque la versión numbas no necesita matrices intermedias y, por lo tanto, usa la caché de manera más eficiente.


Si bien el enfoque ufunc de numba es una buena compensación entre usabilidad y rendimiento, todavía no es lo mejor que podemos hacer. Sin embargo, no hay una bala de plata o un enfoque mejor para cualquier tarea: uno tiene que entender cuáles son las limitaciones y cómo se pueden mitigar.

Por ejemplo, para las funciones trascendentales (p exp. Ej . sin, cos) Numba no ofrece ninguna ventaja sobre numpy's np.exp(no se crean matrices temporales, la fuente principal de la aceleración). Sin embargo, mi instalación de Anaconda utiliza el VML de Intel para vectores mayores que 8192 ; simplemente no puede hacerlo si la memoria no es contigua. Por lo tanto, podría ser mejor copiar los elementos en una memoria contigua para poder usar el VML de Intel:

import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp(x):
    return np.exp(x)

def np_copy_exp(x):
    copy = np.ravel(x, 'K')
    return np.exp(copy).reshape(x.shape) 

Para ser justos en la comparación, he desactivado la paralelización de VML (ver código en el apéndice):

ingrese la descripción de la imagen aquí

Como se puede ver, una vez que se inicia VML, la sobrecarga de la copia está más que compensada. Sin embargo, una vez que los datos se vuelven demasiado grandes para el caché L3, la ventaja es mínima, ya que la tarea vuelve a estar vinculada al ancho de banda de memoria.

Por otro lado, numba también podría usar SVML de Intel, como se explica en esta publicación :

from llvmlite import binding
# set before import
binding.set_option('SVML', '-vector-library=SVML')

import numba as nb

@nb.vectorize(target="cpu")
def nb_vexp_svml(x):
    return np.exp(x)

y usando VML con rendimientos de paralelización:

ingrese la descripción de la imagen aquí

La versión de numba tiene menos sobrecarga, pero para algunos tamaños VML supera a SVML incluso a pesar de la sobrecarga de copia adicional, lo que no es una sorpresa ya que los ufuncs de numba no están paralelos.


Listados:

A. comparación de la función polinómica:

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

B. comparación de exp:

import perfplot
import numexpr as ne # using ne is the easiest way to set vml_num_threads
ne.set_vml_num_threads(1)
perfplot.show(
    setup=lambda n: np.random.rand(n,n)[::2,::2],
    n_range=[2**k for k in range(0,12)],
    kernels=[
        nb_vexp, 
        np.exp,
        np_copy_exp,
        ],
    logx=True,
    logy=True,
    xlabel='len(x)',
    )
Ead
fuente
0

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

He comparado solo dos, pero conservará la forma de ndarray. He usado la matriz con 1 millón de entradas para comparar. Aquí uso la función cuadrada. Estoy presentando el caso general de la matriz n dimensional. Para dos dimensiones solo crea iter2D.

import numpy, time

def A(e):
    return e * e

def timeit():
    y = numpy.arange(1000000)
    now = time.time()
    numpy.array([A(x) for x in y.reshape(-1)]).reshape(y.shape)        
    print(time.time() - now)
    now = time.time()
    numpy.fromiter((A(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 numpy.fromiterla función cuadrada del usuario, use cualquiera de su elección. Si su función depende de los i, j índices de la matriz, repita el tamaño de la matriz for ind in range(arr.size), use numpy.unravel_indexpara obtener en i, j, ..función de su índice 1D y la forma de la matriz numpy.unravel_index

Esta respuesta está inspirada en mi respuesta a otra pregunta aquí

Rushikesh
fuente