【问题标题】:Most efficient way to map function over numpy array在 numpy 数组上映射函数的最有效方法
【发布时间】:2018-07-13 07:35:39
【问题描述】:

将函数映射到 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 数组。

我们可以做得更好吗?

【问题讨论】:

  • 为什么不使用“squares = x**2”?您是否需要评估更复杂的函数?
  • 只有squarer(x)怎么样?
  • 也许这不是直接回答问题,但我听说numba 可以将现有的python 代码编译成并行机器指令。当我真正有机会使用它时,我会重新访问和修改这篇文章。

标签: python performance numpy


【解决方案1】:

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

消息 #1:如果您可以使用 numpy 的本机函数,请执行此操作。

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

如果您确实需要矢量化,那么使用哪种变体并不重要。


重现情节的代码:

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

【讨论】:

  • 您似乎已将f(x) 排除在您的阴谋之外。它可能不适用于每个f,但它适用于这里,并且在适用时它很容易成为最快的解决方案。
  • 另外,您的情节不支持您声称vf = np.vectorize(f); y = vf(x) 赢得短输入的说法。
  • 香草 for 循环怎么样?
  • @Vlad 只需按照评论使用 math.sqrt。
  • 这些函数的内存使用有什么显着差异吗?我有使用直接函数方法快速运行的代码,但对于大型数组,它会耗尽内存(由于 numpy.sqrt 的临时 float64 表示)。
【解决方案2】:

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

【讨论】:

  • 这没有任何效率。
  • 来自该文档:The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop. 在其他问题中,我发现vectorize 可能会使用户迭代速度加倍。但真正的加速是使用真正的numpy 数组操作。
  • 请注意,vectorize 至少可以让非一维数组工作
  • 但是squarer(x) 已经适用于非一维数组。 vectorize 仅比列表理解(如问题中的那个)具有任何优势,而不是 squarer(x)
【解决方案3】:

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。如果您正在处理其他数据类型,您可能需要研究下面显示的其他方法。

方法比较

这里有一些简单的测试来比较映射函数的三种方法,这个例子使用 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

拥有 100 多个元素:

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 和编译器优化会有不同的结果,所以对你的环境做类似的测试。

【讨论】:

  • 如果您使用count 参数和生成器表达式,那么np.fromiter 会明显更快。
  • 所以,例如,使用'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))'
  • 你没有测试f(x)which beats everything else by over an order of magnitude的直接解法。
  • 如果f有2个变量并且数组是二维的呢?
  • 当 OP 询问如何在数组中“映射”函数时,我很困惑 'f(x)' 版本(“direct”)实际上是如何被认为具有可比性的?在 f(x) = x ** 2 的情况下, ** 由 numpy 对整个数组执行,而不是基于每个元素。例如,如果 f(x) 是 'lambda x: x + x" 那么答案就大不相同了,因为 numpy 连接数组而不是每个元素相加。这真的是预期的比较吗?请解释一下。
【解决方案4】:

周围有numexprnumbacython,这个答案的目标是考虑到这些可能性。

但首先让我们明确一点:无论您如何将 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 函数类方法的代表。使用perfplot(参见本答案附录中的代码),我们得到以下运行时间:

我们可以看到,numpy-approach 比纯 python 版本快 10 到 100 倍。更大数组大小的性能下降可能是因为数据不再适合缓存。

还值得一提的是,vectorize 也使用了大量内存,因此内存使用通常是瓶颈(参见相关的SO-question)。另请注意,numpy 在np.vectorize 上的文档指出它“主要是为了方便,而不是为了性能”。

应该使用其他工具,当需要性能时,除了从头开始编写 C 扩展外,还有以下可能性:


人们经常听到,numpy 的性能是最好的,因为它在引擎盖下是纯 C。但是还有很大的改进空间!

矢量化的 numpy 版本使用大量额外的内存和内存访问。 Numexp-library 尝试平铺 numpy-arrays 从而获得更好的缓存利用率:

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

导致以下比较:

我无法解释上图中的所有内容:我们一开始可以看到 numexpr-library 的开销更大,但因为它更好地利用缓存,所以对于更大的数组来说,它的速度大约快了 10 倍!


另一种方法是对函数进行 jit 编译,从而获得真正的纯 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-approach 快 10 倍:


但是,任务是并行化的,因此我们也可以使用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 倍):


虽然 numba 专注于使用 numpy-arrays 优化操作,但 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 会导致函数有些慢:


结论

显然,仅测试一个功能并不能证明任何事情。还应该记住,对于选择的函数示例,内存的带宽是大于 10^5 个元素的大小的瓶颈 - 因此我们在该区域中对 numba、numexpr 和 cython 具有相同的性能。

最后,最终答案取决于功能类型、硬件、Python 分布和其他因素。例如,Anaconda-distribution 使用 Intel 的 VML 来处理 numpy 的功能,因此对于 expsincos 和类似的超验功能,其性能优于 numba(除非它使用 SVML,请参见 SO-post)。以下SO-post

但根据这次调查和我目前的经验,我想说,只要不涉及超越函数,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)'
    )

【讨论】:

  • Numba 通常可以使用英特尔 SVML,这导致与英特尔 VML 相比具有相当可比性的时序,但在版本 (0.43-0.47) 中实现有点错误。我添加了一个性能图 stackoverflow.com/a/56939240/4045774 用于与您的 cy_expsum 进行比较。
【解决方案5】:
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)

【讨论】:

    【解决方案6】:

    编辑: 原始答案具有误导性, np.sqrt 直接应用于数组,只是开销很小。 p>

    在您想要应用对一维数组进行操作的内置函数的多维情况下,numpy.apply_along_axis 是一个不错的选择,对于来自 numpy 和 scipy 的更复杂的函数组合也是如此。

    先前的误导性陈述:

    添加方法:

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

    perfplot 代码给出的性能结果接近np.sqrt

    【讨论】:

    • 我感到非常震惊的是,大多数人多年来似乎都没有意识到这个简单、可扩展和内置的简单易用......
    • 这是误导。您实际上并没有以这种方式矢量化f。例如,尝试在 Nico 的 perf 代码中将 np.sqrt 替换为 math.sqrt,你会得到一个错误。这里实际发生的是 f 使用数组参数调用,因为 x 是一维的,并且您告诉它沿包含所有元素的第一个轴应用它。为了使这个答案有效,apply_along_axis 的参数应该替换为x[None,:]。然后你会发现long_axis是最慢的。
    • 你说得对——我在寻找一种将一维函数应用于高维数组的方法时遇到了这个问题,并尝试了它是否也可以在这里工作——没有意识到它只是适用于 @987654331直接@。
    【解决方案7】:

    似乎没有人提到在 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
    

    我还测试了更大的样本,并且改进是成比例的。另请参阅文档here

    【讨论】:

    • 我重复了上面的时序测试,也发现性能提升(超过 np.vectorize)大约 30%
    • 一个警告:看起来这个方法用 dtype=object 构造数组。话虽如此,即使我添加了对 dtype=float 的转换,它仍然比矢量化快一点。
    【解决方案8】:

    我相信在 numpy 的较新版本(我使用 1.13)中,您可以通过将 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])
    

    【讨论】:

    • 这并不是什么新鲜事——它一直都是这样——它是 numpy 的核心特性之一。
    • ** 运算符将计算应用于t 的每个元素 t。那是普通的numpy。将其包裹在 lambda 中不会做任何额外的事情。
    • 这不适用于当前显示的 if 语句。
    【解决方案9】:

    正如this post 中提到的,只需像这样使用生成器表达式:

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

    【讨论】:

      【解决方案10】:

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

      我只比较了两个,但它会保留ndarray 的形状。我使用了包含 100 万个条目的数组进行比较。这里我使用了 square 函数,它也是 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% .. 这可能不会改变游戏规则(即可能不值得额外的认知负担)。
      【解决方案11】:

      使用numpy.fromfunction(function, shape, **kwargs)

      见“https://docs.scipy.org/doc/numpy/reference/generated/numpy.fromfunction.html

      【讨论】:

      • 没有。这将创建一个网格并将整个发送到function。这不是 OP 想要的。
      猜你喜欢
      • 2022-01-24
      • 2022-10-04
      • 1970-01-01
      • 2016-08-14
      • 2011-02-15
      • 2011-10-09
      • 2021-12-27
      • 2020-08-06
      相关资源
      最近更新 更多