NumPy 배열의 모든 셀에서 효율적인 함수 평가


124

NumPy 배열 A가 주어지면 동일한 함수 f모든 셀 에 적용하는 가장 빠르고 효율적인 방법은 무엇입니까?

  1. A (i, j)f (A (i, j)) 를 할당한다고 가정합니다 .

  2. 함수 f 에는 바이너리 출력이 없으므로 mask (ing) 연산이 도움이되지 않습니다.

"명백한"이중 루프 반복 (모든 셀)이 최적의 솔루션입니까?


답변:


165

벡터화 할 수 있습니다. 기능을 다음 NumPy와 배열에 직접 당신이 그것을 필요로 할 때마다 적용 :

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

벡터화 할 때 명시 적 출력 유형을 직접 지정하는 것이 좋습니다.

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

19
벡터화 된 함수는 모든 배열 요소를 통한 "수동"이중 루프 반복 및 할당보다 빠를 수 없습니다. 특히 초기 입력이 아닌 새로 생성 된 변수에 결과를 저장하기 때문 입니다. 답장을 보내 주셔서 감사합니다 :)
Peter

1
@Peter : 아, 이제 원래 질문에서 결과를 이전 배열에 다시 할당하는 것에 대해 언급했음을 알았습니다. 처음 읽을 때 놓쳐서 미안합니다. 예,이 경우 이중 루프가 더 빨라야합니다. 그러나 어레이의 평면화 된 뷰에서 단일 루프를 시도해 보셨습니까? 약간의 루프 오버 헤드를 절약하고 Numpy는 각 반복에서 곱셈과 더하기 (데이터 오프셋 계산을 위해)를 한 번 덜 수행해야하기 때문에 약간 더 빠를 수 있습니다 . 또한 임의로 차원이 지정된 배열에서 작동합니다. 아주 작은 배열에서는 느릴 수 있습니다.
blubberdiblub

45
vectorize함수 설명에 제공된 경고에 유의 하십시오 . 벡터화 함수는 주로 성능이 아닌 편의를 위해 제공됩니다. 구현은 본질적으로 for 루프입니다. 따라서 이것은 프로세스 속도를 전혀 높이 지 못할 가능성이 큽니다.
Gabriel

vectorize반환 유형을 결정하는 방법에주의 하십시오. 그것은 버그를 낳았습니다. frompyfunc조금 더 빠르지 만 dtype 객체 배열을 반환합니다. 둘 다 행이나 열이 아닌 스칼라를 공급합니다.
hpaulj

1
@Gabriel RK45 np.vectorize를 활용하는 내 기능을 사용하는 것만으로도 약 20 배의 속도를 얻을 수 있습니다.
Suuuehgi



0

더 나은 해결책을 찾았다 고 생각합니다. 함수를 파이썬 범용 함수로 변경하는 아이디어 ( 문서 참조 ). 내부에서 병렬 계산을 실행할 수 있습니다.

ufuncC로 커스터마이징 된 자신 만의 코드를 작성할 수 있는데, 이는 확실히 더 효율적이거나 np.frompyfunc내장 된 팩토리 메소드 인 을 호출하여 작성할 수 있습니다 . 테스트 후 다음보다 더 효율적입니다 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

나는 또한 더 큰 샘플을 테스트했으며 개선은 비례합니다. 다른 방법의 성능 비교는 이 게시물을 참조하십시오.


0

2d-array (또는 nd-array)가 C- 또는 F- 연속적 일 때, 함수를 2d-array에 매핑하는이 작업은 1d-array에 함수를 매핑하는 작업과 실질적으로 동일합니다. 예를 들어 np.ravel(A,'K') .

1d-array에 대한 가능한 솔루션은 여기 에서 예를 들어 논의되었습니다 .

그러나 2d 배열의 메모리가 연속적이지 않으면 상황이 조금 더 복잡해집니다. 축이 잘못된 순서로 처리 될 경우 가능한 캐시 미스를 피하고 싶기 때문입니다.

Numpy는 이미 최상의 순서로 축을 처리하는 기계를 갖추고 있습니다. 이 기계를 사용할 수있는 한 가지 가능성은 np.vectorize. 그러나 numpy의 문서에 np.vectorize따르면 "성능이 아닌 편의를 위해 주로 제공"됩니다. 느린 파이썬 함수는 전체 관련 오버 헤드와 함께 느린 파이썬 함수로 유지됩니다! 또 다른 문제는 엄청난 메모리 소비입니다. 예를 들어이 SO-post를 참조하십시오 .

C 함수의 성능을 원하지만 numpy의 기계를 사용하려는 경우 좋은 해결책은 ufuncs 생성에 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

쉽게 이길 수 np.vectorize있지만 동일한 기능이 numpy-array 곱셈 / 덧셈과 같이 수행 될 때도 마찬가지입니다.

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

시간 측정 코드에 대해서는이 답변의 부록을 참조하십시오.

여기에 이미지 설명 입력

Numba의 버전 (녹색)은 python-function (예 :)보다 약 100 배 빠릅니다 np.vectorize. 이는 놀라운 일이 아닙니다. 그러나 numbas 버전은 중간 배열이 필요하지 않으므로 캐시를 더 효율적으로 사용하기 때문에 numpy- 기능보다 약 10 배 더 빠릅니다.


numba의 ufunc 접근 방식은 유용성과 성능 사이의 좋은 절충안이지만 여전히 우리가 할 수있는 최선은 아닙니다. 그러나 모든 작업에 가장 적합한 은색 총알이나 접근 방식은 없습니다. 한계가 무엇이며 어떻게 완화 될 수 있는지 이해해야합니다.

예를 들어, 초월 함수 (예를 들어 exp, sin, cos) numba을 통해 어떤 이점을 제공하지 않습니다 NumPy와의 np.exp(생성 된 임시 배열이없는 - 속도 업의 주요 소스). 그러나 내 Anaconda 설치는 8192보다 큰 벡터에 대해 Intel의 VML을 사용 합니다. 메모리가 인접하지 않으면 사용할 수 없습니다. 따라서 Intel의 VML을 사용할 수 있으려면 요소를 인접한 메모리에 복사하는 것이 좋습니다.

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) 

비교의 공정성을 위해 VML의 병렬화를 해제했습니다 (부록의 코드 참조).

여기에 이미지 설명 입력

보시다시피 VML이 시작되면 복사 오버 헤드가 보상되는 것 이상입니다. 그러나 데이터가 L3 캐시에 비해 너무 커지면 작업이 다시 한 번 메모리 대역폭에 제한되므로 이점이 최소화됩니다.

반면에 numba는 이 게시물에 설명 된대로 Intel의 SVML도 사용할 수 있습니다 .

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)

병렬화와 함께 VML을 사용하면 다음이 생성됩니다.

여기에 이미지 설명 입력

numba의 버전은 오버 헤드가 적지 만, 일부 크기의 경우 추가 복사 오버 헤드에도 불구하고 VML이 SVML을 능가합니다. 이는 numba의 ufunc가 병렬화되지 않기 때문에 그리 놀라운 일이 아닙니다.


목록 :

A. 다항 함수 비교 :

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. 비교 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

위의 모든 답변은 잘 비교되지만 매핑을 위해 사용자 정의 함수를 사용해야하고을 가지고 numpy.ndarray있고 배열의 모양을 유지해야하는 경우.

두 개만 비교했지만 ndarray. 비교를 위해 1 백만 개의 항목이있는 배열을 사용했습니다. 여기에서는 제곱 함수를 사용합니다. n 차원 배열에 대한 일반적인 사례를 제시하고 있습니다. 2 차원의 iter경우 2D로 만듭니다.

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)

산출

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

여기에서 numpy.fromiter사용자 평방 함수를 명확하게 볼 수 있으며 원하는 것을 사용하십시오. 당신에 의존하는 기능을 경우 i, j 그 반복 처리 같은 배열의 크기, 배열의 인덱스이며 for ind in range(arr.size), 사용 numpy.unravel_index얻을 i, j, ..배열의 당신의 1D 지수와 모양에 따라 numpy.unravel_index

이 답변은 여기 다른 질문에 대한 제 답변에서 영감을 얻었습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.