web-dev-qa-db-ja.com

派手な配列に関数をマップするための最も効率的な方法

派手な配列に関数をマップするための最も効率的な方法は何ですか?私の現在のプロジェクトで私がやっている方法は以下の通りです:

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])

しかし、これはおそらく非常に非効率的であるように思えます。なぜなら、私はリストの内包表記を使って新しい配列をPythonのリストとして作成し、それを変換して元の配列に戻すからです。

もっとうまくできますか?

199
Ryan

perfplot (私の小さなプロジェクト)で、提案されたすべてのメソッドとnp.array(map(f, x))をテストしました。

メッセージ#1:numpyのネイティブ関数を使用できる場合は、それを行います。

あなたが既にベクトル化しようとしている関数がisベクトル化された場合(元の投稿のx**2の例のように)、それを使用することはmuch他の何よりも高速です(ログスケールに注意してください):

enter image description here

実際にベクトル化が必要な場合、使用するバリアントはそれほど重要ではありません。

enter image description here


プロットを再現するコード:

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)',
    )
197
Nico Schlömer

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)
array([ 1,  4,  9, 16, 25])

https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

97
satomacoto

TL、DR

@ user2357112 で説明したように、関数を適用する "直接"方法は、関数をNumpy配列にマッピングするための最速かつ最も簡単な方法です。

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

np.vectorizeは、うまく動作しないし、 issues をいくつも持っている(あるいは持っていた)ので、一般的に避けてください。他の種類のデータを処理している場合は、以下に示す他の方法を調べることをお勧めします。

方法の比較

これは、関数をマップするための3つの方法を比較する簡単なテストです。この例では、Python 3.6とNumPy 1.15.4を使用しています。まず、テスト用のセットアップ機能:

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))

5つの要素を使ったテスト(最速から最短へソート):

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

何百もの要素がある場合:

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

そして、1000個以上の配列要素があります。

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

Python/NumPyのバージョンとコンパイラの最適化が異なると結果が異なるため、ご使用の環境でも同様のテストを行ってください。

48
Mike T
squares = squarer(x)

配列に対する算術演算は、Pythonレベルのループまたは内包に適用されるすべてのインタプリタのオーバーヘッドを回避する効率的なCレベルのループで、要素単位で自動的に適用されます。

NumPy配列に要素単位で適用したいと思う関数のほとんどはうまくいくでしょうが、変更が必要なものもあります。たとえば、ifは要素単位では機能しません。これらを numpy.where のような構成要素を使うように変換したいでしょう。

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

になる

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

この質問に対する回答が多かったので、 numexprnumba 、および cython があります。この答えの目的はこれらの可能性を考慮に入れることです。

しかし、最初に明白なことを述べましょう。どのようにPython関数をnumpy配列にマップしても、それはPython関数のままです。

  • numpy-array要素はPythonオブジェクト(例えばFloat)に変換されなければなりません。
  • すべての計算はPythonオブジェクトで行われます。つまり、インタプリタ、動的ディスパッチ、および不変オブジェクトのオーバーヘッドがあります。

したがって、実際に配列をループ処理するためにどの機構が使用されるかは、上記のオーバーヘッドのために大きな役割を果たすことはありません。numpyのベクトル化を使用するよりもはるかに遅くなります。

次の例を見てみましょう。

# 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は、python-python関数クラスの代表として選ばれました。 perfplot(この回答の付録にあるコードを参照)を使用すると、次の実行時間が得られます。

enter image description here

おかしなアプローチは、純粋なpythonバージョンよりも10倍から100倍高速です。配列サイズが大きくなるとパフォーマンスが低下するのは、おそらくデータがキャッシュに収まらなくなったためです。

それはフードの下でそれが純粋なCであるので、人は派手なパフォーマンスがそれが得るのと同じくらい良いことをしばしば聞く。まだ改善の余地があります。

ベクトル化されたnumpy-versionは多くの追加のメモリとメモリアクセスを使用します。 Numexp-libraryは、numpy-arrayをタイル化しようとします。そのため、キャッシュ利用率が向上します。

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

次のような比較ができます。

enter image description here

上のプロットですべてを説明することはできません。初めはnumexpr-libraryのオーバーヘッドが大きくなることがありますが、キャッシュの利用率が高いため、大きい配列のほうが約10倍高速です。


もう1つの方法は、関数をjitコンパイルして、実際の純粋なC言語のUFuncを取得することです。これがヌンバのアプローチです。

# 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

これは、元々の派手なアプローチよりも10倍高速です。

enter image description here


しかし、このタスクは厄介なほど並列化可能であるため、ループを並列に計算するためにprangeを使用することもできます。

@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

予想通り、並列関数は入力が小さいほど遅くなりますが、サイズが大きいほど速くなります(ほぼ2倍)。

enter image description here


Numbaはnumpy配列を使った操作の最適化を専門としていますが、Cythonはより一般的なツールです。 numbaと同じパフォーマンスを引き出すのはより複雑です。多くの場合、ローカルコンパイラ(gcc/MSVC)に対してllvm(numba)になります。

%%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はやや遅い機能をもたらします。

enter image description here


結論

明らかに、1つの機能だけをテストしても何も証明されません。また、選択した関数の例では、メモリの帯域幅が10 ^ 5要素を超えるサイズのボトルネックとなっていたことにも注意する必要があります。

それでも、この調査とこれまでの私の経験から、numbaが最もパフォーマンスの良い最も簡単なツールのように思われると私は述べます。


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)'
    )
16
ead

私はあなたが単にあなたがスカラ型のために書いた関数にnumpy配列を渡すことによって関数を呼ぶことができるnumpyの新しいバージョン(私は1.13を使う)を信じる、それは自動的に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])
10
Peiti Li

この記事 で述べたように、単に次のようなジェネレータ式を使ってください。

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