Avaliação eficiente de uma função em todas as células de um array NumPy


124

Dada uma matriz NumPy A , qual é a maneira mais rápida / eficiente de aplicar a mesma função, f , a todas as células?

  1. Suponha que vamos atribuir a A (i, j) a f (A (i, j)) .

  2. A função, f , não possui uma saída binária, portanto as operações de máscara (ing) não ajudarão.

A iteração "óbvia" de loop duplo (através de todas as células) é a solução ideal?


Respostas:


165

Você pode apenas vetorizar a função e aplicá-la diretamente a uma matriz Numpy sempre que precisar:

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

Provavelmente é melhor especificar diretamente um tipo de saída explícito ao vetorizar:

f = np.vectorize(f, otypes=[np.float])

19
Receio que a função vetorizada não possa ser mais rápida que a iteração e a atribuição de loop duplo "manual" através de todos os elementos da matriz. Especialmente porque armazena o resultado em uma variável recém- criada (e não diretamente na entrada inicial). Muito obrigado pela sua resposta, porém :) #
Peter Peter

1
@ Peter: Ah, agora vejo que você mencionou a atribuição do resultado à matriz anterior na sua pergunta original. Me desculpe, eu perdi isso quando o li pela primeira vez. Sim, nesse caso, o loop duplo deve ser mais rápido. Mas você também tentou um único loop na visualização plana da matriz? Isso pode ser um pouco mais rápido, pois você economiza um pouco de sobrecarga de loop e o Numpy precisa fazer menos uma multiplicação e adição (para calcular o deslocamento de dados) a cada iteração. Além disso, ele funciona para matrizes dimensionadas arbitrariamente. Pode ser mais lento em matrizes muito pequenas, também.
blubberdiblub

45
Observe o aviso fornecido na vectorizedescrição da função: A função vetorizar é fornecida principalmente por conveniência, não por desempenho. A implementação é essencialmente um loop for. Portanto, é muito provável que isso não acelere o processo.
Gabriel

Preste atenção em como vectorizedetermina o tipo de retorno. Isso produziu bugs. frompyfuncé um pouco mais rápido, mas retorna uma matriz de objetos dtype. Ambos alimentam escalares, não linhas ou colunas.
hpaulj

1
@Gabriel Apenas jogando np.vectorizena minha função (que utiliza RK45) me dá uma velocidade por um fator de ~ 20.
Suuuehgi



0

Acredito ter encontrado uma solução melhor. A ideia de alterar a função para a função universal python (consulte a documentação ), que pode exercer computação paralela sob o capô.

Pode-se escrever seu próprio personalizado ufuncem C, que certamente é mais eficiente, ou invocando np.frompyfunc, que é o método de fábrica embutido. Após o teste, isso é mais eficiente do 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

Também testei amostras maiores e a melhoria é proporcional. Para comparação de desempenhos de outros métodos, consulte este post


0

Quando a matriz 2d (ou matriz nd) é contígua em C ou F, essa tarefa de mapear uma função em uma matriz 2d é praticamente a mesma que a tarefa de mapear uma função em uma matriz 1d - nós apenas tem que vê-lo dessa maneira, por exemplo, via np.ravel(A,'K').

A possível solução para a matriz 1d foi discutida, por exemplo, aqui .

No entanto, quando a memória do 2d-array não é contígua, a situação é um pouco mais complicada, porque se deseja evitar possíveis falhas de cache se o eixo for tratado na ordem errada.

A Numpy já possui um mecanismo para processar os eixos na melhor ordem possível. Uma possibilidade de usar esta maquinaria é np.vectorize. No entanto, a documentação da numpy np.vectorizeafirma que ela é "fornecida principalmente por conveniência, não por desempenho" - uma função python lenta permanece uma função python lenta com toda a sobrecarga associada! Outra questão é seu enorme consumo de memória - veja, por exemplo, este SO-post .

Quando alguém deseja executar uma função C, mas usar o maquinário de numpy, uma boa solução é usar o numba para criação de ufuncs, por exemplo:

# 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

Ele bate facilmente, np.vectorizemas também quando a mesma função seria executada como multiplicação / adição de array numpy, ou seja,

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

Veja o apêndice desta resposta para o código de medição do tempo:

insira a descrição da imagem aqui

A versão do Numba (verde) é cerca de 100 vezes mais rápida que a função python (ou seja np.vectorize), o que não é surpreendente. Mas também é 10 vezes mais rápido que a funcionalidade numpy, porque a versão numbas não precisa de matrizes intermediárias e, portanto, usa o cache com mais eficiência.


Embora a abordagem não-funcional da numba seja uma boa alternativa entre usabilidade e desempenho, ela ainda não é a melhor que podemos fazer. No entanto, não existe uma bala de prata ou uma abordagem melhor para qualquer tarefa - é preciso entender quais são as limitações e como elas podem ser mitigadas.

Por exemplo, para as funções transcendentes (por exemplo exp, sin, cos) numba não fornece quaisquer vantagens em relação aos da numpy np.exp(não há matrizes temporários criados - a principal fonte do aumento de velocidade). No entanto, minha instalação do Anaconda utiliza o VML da Intel para vetores maiores que 8192 - apenas não pode ser feito se a memória não for contígua. Portanto, pode ser melhor copiar os elementos para uma memória contígua para poder usar o VML da 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 a equidade da comparação, desativei a paralelização da VML (consulte o código no apêndice):

insira a descrição da imagem aqui

Como se pode ver, uma vez que a VML entra em ação, a sobrecarga da cópia é mais do que compensada. No entanto, uma vez que os dados se tornam grandes demais para o cache L3, a vantagem é mínima, pois as tarefas se tornam novamente vinculadas à largura de banda da memória.

Por outro lado, a numba também poderia usar o SVML da Intel, conforme explicado neste post :

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)

e usando VML com paralelização produz:

insira a descrição da imagem aqui

A versão do numba tem menos sobrecarga, mas, para alguns tamanhos, o VML supera o SVML, apesar da sobrecarga adicional de cópia - o que não é uma surpresa, já que os ufuncs do numba não são paralelos.


Listagens:

A. comparação da função polinomial:

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. comparação 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)',
    )

0

Todas as respostas acima se comparam bem, mas se você precisar usar a função personalizada para mapeamento, e tiver numpy.ndarray, e precisar manter a forma da matriz.

Comparei apenas dois, mas ele manterá a forma de ndarray. Eu usei a matriz com 1 milhão de entradas para comparação. Aqui eu uso a função quadrada. Estou apresentando o caso geral de n array dimensional. Para bidimensional basta criar 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)

Resultado

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

aqui você pode ver claramente a numpy.fromiterfunção quadrada do usuário, use qualquer uma de sua escolha. Se a sua função depende dos i, j índices da matriz, itere no tamanho da matriz for ind in range(arr.size), use numpy.unravel_indexpara obter com i, j, ..base no seu índice 1D e na forma da matriz numpy.unravel_index

Esta resposta é inspirada na minha resposta em outra pergunta aqui

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.