在numpy数组上映射函数的最有效方法是什么?

656
什么是在numpy数组上映射函数的最有效方法?我目前正在执行以下操作:
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列表之前,再将其转换回numpy数组。我们能做得更好吗?


19
为什么不使用"squares = x**2"呢?你是否需要计算更加复杂的函数? - 22degrees
5
只使用 squarer(x) 怎么样? - Life
5
也许这并不是直接回答问题的方法,但我听说过numba可以将现有的Python代码编译成并行机器指令。当我真正有机会使用它时,我会重新审查和修改这篇文章。 - 把友情留在无盐
@Life squarer(x) 将在数组元素上应用 squarer 函数,并返回一个包含单个 squarer(element) 调用结果的数组。我写这篇文章是因为一开始“只有 squarer(x)”并不够清晰。 - JustAnEuropean
11个回答

548

我已经测试了所有建议的方法以及使用 np.array(list(map(f, x)))perfplot(我的一个小项目)。

消息 #1:如果可以使用 numpy 的原生函数,请使用它们。

如果您要向量化的函数已经被向量化了(例如原始帖子中的 x**2 示例),使用它比其他任何方法都要快得多(请注意对数刻度):

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)


b = perfplot.bench(
    setup=np.random.rand,
    n_range=[2 ** k for k in range(20)],
    kernels=[
        f,
        array_for,
        array_map,
        fromiter,
        vectorize,
        vectorize_without_init,
    ],
    xlabel="len(x)",
)
b.save("out1.svg")
b.show()

15
你的图中好像漏掉了 f(x)。虽然它可能不适用于所有的函数 f,但在这里是适用的,并且是最快的解决方案。请确保将其包括在内。 - user2357112
3
此外,你的情节不支持你的主张,即“ vf = np.vectorize(f); y = vf(x) ”在处理短输入时更胜一筹。 - user2357112
3
那使用一个普通的for循环呢? - Catiger3331
1
@Vlad,只需按照注释使用math.sqrt即可。 - Nico Schlömer
2
这些函数的内存使用有显著差异吗?我有一段代码,使用直接函数方法运行速度很快,但对于大数组,由于numpy.sqrt的临时float64表示而导致内存不足。 - Pieter-Jan Busschaert
显示剩余12条评论

244
使用 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])

60
这并不更有效率。 - user2357112
142
这段内容的意思是:vectorize 函数主要用于方便使用,而非提高性能。其实现本质上是一个 for 循环。在其他问题中,有人发现 vectorize 可能会使用户的迭代速度加倍,但真正的加速取决于使用实际的 numpy 数组操作。 - hpaulj
7
请注意,向量化至少可以使非一维数组正常工作。 - Eric
1
但是 squarer(x) 可以处理非一维数组。vectorize 只比列表推导式 (问题中的那种) 有优势,而不是比 squarer(x) 更好。 - user2357112
2
以前,np.vectorize 比等价的列表推导式慢。现在它可以更好地扩展,因此对于大参数,它更快。但是,它仍然不如使用编译的 numpy 方法和运算符而没有任何 Python 级别循环那么快。 - hpaulj

135

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,因为它的性能不佳,并且存在(或曾经存在)许多问题。如果您正在处理其他数据类型,则可能需要研究下面显示的其他方法。

方法比较

以下是一些简单的测试,用于比较三种映射函数的方法,此示例使用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))

测试五个元素(从快到慢排序):

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

而对于成千上万个数组元素或更多的情况:

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和编译器优化会产生不同的结果,因此请在您的环境中进行类似的测试。请注意保留HTML标签。

3
如果使用count参数和生成器表达式,那么np.fromiter的速度会显著更快。 - juanpa.arrivillaga
4
你没有测试 f(x) 的直接解法,这个解法比其他任何解法都快一个数量级以上 - user2357112
5
如果f有2个变量且数组是二维的,该怎么办? - Sigur
7
当OP询问如何在数组上“映射”函数时,我对“f(x)”版本(“直接”版本)为什么被认为是可比较的感到困惑。对于 f(x) = x ** 2,** 是由numpy对整个数组执行的,而不是按元素进行。例如,如果 f(x) 是“lambda x: x + x”,那么答案将非常不同,因为numpy将连接数组,而不是进行按元素加法。这真的是预期的比较吗?请解释。 - Andrew Mellinger
3
“直接”版本不是逐元素的,而是逐行的。举个例子,可以看看 lambda x: str(x) - Andreas K.
显示剩余6条评论

86

这里有numexprnumbacython等技术可供选择,本回答的目标是考虑这些可能性。

但首先让我们明确一点:无论如何将Python函数映射到numpy数组上,它仍然是一个Python函数,这意味着每次评估都必须:

  • 将numpy数组元素转换为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函数类方法的代表。使用perfplot(请参见本答案附录中的代码),我们得到以下运行时间:

enter image description here

我们可以看到,numpy方法比纯Python版本快10倍至100倍。对于更大的数组大小,性能下降可能是因为数据不再适合缓存。
值得一提的是,vectorize也使用了大量内存,因此通常内存使用是瓶颈(请参阅相关SO-question)。还要注意,numpy关于np.vectorize的文档指出,它主要是为方便而提供的,而不是为了性能。
当需要性能时,应使用其他工具,除了从头开始编写C扩展之外,还有以下可能性:

常听说,numpy的性能非常好,因为它在底层是纯C。但实际上还有很大的改进空间!

向量化的numpy版本使用了大量额外的内存和内存访问。Numexp库尝试将numpy数组切分成小块,从而获得更好的缓存利用率:

# 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库在开始时有更大的开销,但由于它更好地利用了缓存,因此对于更大的数组,速度快了约10倍!


另一种方法是即时编译函数,从而获得真正的纯C UFunc。这是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

它比原始的numpy方法快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 相同的性能水平更加复杂 - 通常是 llvm(numba)与本地编译器(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导致函数速度略慢:

enter image description here


结论

显然,仅对一个函数进行测试并不能证明什么。此外,应该记住,在所选择的函数示例中,对于大于10^5个元素的大小,内存带宽是瓶颈 - 因此在这个区域中,numba、numexpr和cython具有相同的性能。

最终的答案取决于函数类型、硬件、Python发行版和其他因素。例如,Anaconda发行版使用Intel的VML来处理numpy的函数,因此在超越函数(如expsincos等)方面轻松胜过numba(除非它使用SVML,请参见此SO-post)- 请参见以下SO-post

然而,从这项调查和我的经验来看,我认为,只要不涉及超越函数,numba似乎是最易于使用且性能最佳的工具。


使用perfplot包绘制运行时间:

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

1
Numba通常可以利用Intel SVML,与Intel VML相比,时间差别不大,但在版本(0.43-0.47)中实现有点错误。我已经添加了一个性能图 https://dev59.com/4LTna4cB1Zd3GeqPCfHf#56939240,以便与您的cy_expsum进行比较。 - max9111
如果你想要最佳性能,这里提供最佳答案。 - Abhishek Divekar
根据我的经验,Cython 会导致函数速度略慢。但这取决于编译器标志。在启用编译器中的 AVX2 优化后,我得到了更快的代码,这比 Numba 在我的特定应用程序中表现更好。 - Zaero Divide

39
squares = squarer(x)

对数组进行算术运算时,会自动应用于每个元素,并使用高效的C级循环,避免了Python级别循环或推导所需的所有解释器开销。

大多数你想要逐个应用于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)

30

似乎没有人提到过 numpy 包中生产 ufunc 的内置工厂方法:np.frompyfunc。我已经对比了 np.vectorize,它的性能超过了后者约 20~30%。当然,它不会像预设的 C 代码甚至 numba(我还没有测试)一样表现良好,但它可能是一个比 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 vf(arr, arr) # 450ms

我也测试了更大的样本,改进是成比例的。请参阅文档此处


1
我重复了上述的时间测试,并且发现相比于 np.vectorize,性能提升了约30%。 - Julian - BrainAnnex.org
一个警告:这个方法似乎构建了dtype=object的数组。话虽如此,即使我添加了转换为dtype=float,它对我来说仍然比向量化略快。 - Duncan MacIntyre

19

编辑:原回答有误,np.sqrt被直接应用于数组,只是带了一点开销。

在多维情况下,如果想要应用一个作用于1D数组的内置函数,numpy.apply_along_axis 是一个不错的选择,也适用于更复杂的从numpy和scipy组成的函数组合。

以前的误导性陈述如下:

添加该方法:

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

对perfplot代码的改动会得到与np.sqrt相近的性能结果。


我对于这个简单、可扩展且内置的无脑解决方案这么多年来大多数人似乎并不知晓感到非常震惊。 - Bill Huang
8
这是误导性的。你并没有以这种方式对f进行向量化。例如,尝试在Nico的perf代码中将np.sqrt替换为math.sqrt,你会收到一个错误提示。实际发生的是,f被用一个数组参数调用,因为x是单维的,你要让它沿着第一轴应用,该轴包含所有元素。为了使这个答案有效,apply_along_axis的参数应该替换为x[None,:]。然后你会发现:在所有函数中,apply_along_axis是最慢的。 - syockit
你说得对 - 我在寻找将1维函数应用于高维数组的方法时遇到了这个问题,并尝试着是否也可以在这里工作 - 却没有意识到它直接应用了np.sqrt - LyteFM

9

我相信在更新版本(我使用1.13)的numpy中,你可以通过将numpy数组传递给你编写的标量类型函数来简单地调用该函数,它会自动将函数调用应用于numpy数组中的每个元素,并返回另一个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])

5
这并不是新鲜事物 - 它一直存在 - 这是numpy的核心特性之一。 - Eric
10
** 运算符将计算应用于t中的每个元素t。这是普通的NumPy操作。在lambda中包装它不会额外做任何事情。 - hpaulj
目前所示的方式无法与if语句一起使用。 - TriHard8

4

这篇文章所述,只需像下面这样使用生成器表达式:

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

4

以上所有答案都很好,但如果您需要使用自定义函数进行映射,并且您有 numpy.ndarray,并且需要保留数组的形状。

我只比较了两个方法,但它将保留 ndarray 的形状。我使用了包含100万条目的数组进行比较。这里我使用了平方函数,它也是numpy内置的,并具有很大的性能提升,因为如果需要其他功能,您可以选择使用自己的函数。

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)

输出

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

在这里,您可以清楚地看到numpy.fromiter相对于简单方法表现出色,如果有内置函数可用,请使用该函数。


fromiter 快了 8%,但这可能并不是决定性因素(也就是说,也许不值得额外的认知负担)。 - WestCoastProjects

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接