【问题标题】:How to optimize a nested for loop in Python如何在 Python 中优化嵌套的 for 循环
【发布时间】:2017-12-18 21:41:45
【问题描述】:

所以我正在尝试编写一个 python 函数来返回一个称为 Mielke-Berry R 值的指标。该指标的计算方式如下:

我写的当前代码可以工作,但是由于等式中的总和,我唯一能想到的解决方法就是在 Python 中使用嵌套的 for 循环,这非常慢...

下面是我的代码:

def mb_r(forecasted_array, observed_array):
    """Returns the Mielke-Berry R value."""
    assert len(observed_array) == len(forecasted_array)
    y = forecasted_array.tolist()
    x = observed_array.tolist()
    total = 0
    for i in range(len(y)):
        for j in range(len(y)):
            total = total + abs(y[j] - x[i])
    total = np.array([total])
    return 1 - (mae(forecasted_array, observed_array) * forecasted_array.size ** 2 / total[0])

我将输入数组转换为列表的原因是因为我听说(尚未测试)使用 python for 循环索引 numpy 数组非常慢。

我觉得可能有某种 numpy 函数可以更快地解决这个问题,有人知道吗?

【问题讨论】:

  • 除非已经有某种 numpy 的东西,我想你可以尝试“Cythonize”它。看起来你会得到很大的收益。
  • 看起来这可以在没有 Cython 的 NumPy 中完成
  • 我觉得我忽略了一些东西,但这不等于sum(abs(n*y - sum(x)))吗?在这种情况下,您可以使用np.sum(np.absolute(n*forecasted_array - np.sum(observed_array)))

标签: python arrays numpy


【解决方案1】:

这是利用broadcasting 获取total 的一种矢量化方式-

np.abs(forecasted_array[:,None] - observed_array).sum()

要同时接受列表和数组,我们可以使用 NumPy 内置的外部减法,就像这样 -

np.abs(np.subtract.outer(forecasted_array, observed_array)).sum()

我们还可以利用numexpr module 进行更快的absolute 计算,并在一次numexpr evaluate 调用中执行summation-reductions,这样可以提高内存效率,就像这样 -

import numexpr as ne

forecasted_array2D = forecasted_array[:,None]
total = ne.evaluate('sum(abs(forecasted_array2D - observed_array))')

【讨论】:

  • 我要补充一点,广播方法涉及分配大小为 (N, N) 的数组,这里 numexpr 的主要优点是无需分配额外的内存。
  • 我刚想说,我用来测试的数组是一个包含 12,000 个值的数组,所以我只是使用 numpy. numexpr 模块方法有效,并且速度可能快 100-500 倍。谢谢!
【解决方案2】:

在 numpy 中广播

如果您不受内存限制,优化numpy 中的嵌套循环的第一步是使用广播并以矢量化方式执行操作:

import numpy as np    

def mb_r(forecasted_array, observed_array):
        """Returns the Mielke-Berry R value."""
        assert len(observed_array) == len(forecasted_array)
        total = np.abs(forecasted_array[:, np.newaxis] - observed_array).sum() # Broadcasting
        return 1 - (mae(forecasted_array, observed_array) * forecasted_array.size ** 2 / total[0])

但在这种情况下,循环发生在 C 而不是 Python 中,它涉及到分配一个大小为 (N, N) 的数组。

广播不是万能的,尝试展开内循环

如上所述,广播意味着巨大的内存开销。所以它应该小心使用,它并不总是正确的方法。虽然您可能会有在任何地方使用它的第一印象 - 不要。不久前,我还对这个事实感到困惑,请参阅我的问题Numpy ufuncs speed vs for loop speed。不要太冗长,我会在你的例子中展示这个:

import numpy as np

# Broadcast version
def mb_r_bcast(forecasted_array, observed_array):
    return np.abs(forecasted_array[:, np.newaxis] - observed_array).sum()

# Inner loop unrolled version
def mb_r_unroll(forecasted_array, observed_array):
    size = len(observed_array)
    total = 0.
    for i in range(size):  # There is only one loop
        total += np.abs(forecasted_array - observed_array[i]).sum()
    return total

小数组(广播速度更快)

forecasted = np.random.rand(100)
observed = np.random.rand(100)

%timeit mb_r_bcast(forecasted, observed)
57.5 µs ± 359 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit mb_r_unroll(forecasted, observed)
1.17 ms ± 2.53 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

中等大小的数组(相等)

forecasted = np.random.rand(1000)
observed = np.random.rand(1000)

%timeit mb_r_bcast(forecasted, observed)
15.6 ms ± 208 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit mb_r_unroll(forecasted, observed)
16.4 ms ± 13.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

大型数组(广播速度较慢)

forecasted = np.random.rand(10000)
observed = np.random.rand(10000)

%timeit mb_r_bcast(forecasted, observed)
1.51 s ± 18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit mb_r_unroll(forecasted, observed)
377 ms ± 994 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

如您所见,对于小型阵列,广播版本比展开版本快 20 倍,对于中型阵列,它们相当相等,但对于大型阵列它慢了 4 倍,因为内存开销正在付出代价高昂的代价。

Numba jit 和并行化

另一种方法是使用numba 及其魔法强大的@jit 函数装饰器。在这种情况下,只需对初始代码稍作修改。此外,要使循环并行,您应该将 range 更改为 prange 并提供 parallel=True 关键字参数。在下面的 sn-p 中,我使用了与 @jit(nopython=True) 相同的 @njit 装饰器:

from numba import njit, prange

@njit(parallel=True)
def mb_r_njit(forecasted_array, observed_array):
    """Returns the Mielke-Berry R value."""
    assert len(observed_array) == len(forecasted_array)
    total = 0.
    size = len(forecasted_array)
    for i in prange(size):
        observed = observed_array[i]
        for j in prange(size):
            total += abs(forecasted_array[j] - observed)
    return 1 - (mae(forecasted_array, observed_array) * size ** 2 / total)

您没有提供mae 函数,但要在njit 模式下运行代码,您还必须装饰mae 函数,或者如果它是一个数字,则将其作为参数传递给jitted 函数。

其他选项

Python 科学生态系统非常庞大,我只提一些其他等效的加速选项:CythonNuitkaPythranbottleneck 等等。或许你对gpu computing感兴趣,但这其实是另外一回事了。

时间

在我的电脑上,不幸的是旧电脑,时间是:

import numpy as np
import numexpr as ne

forecasted_array = np.random.rand(10000)
observed_array   = np.random.rand(10000)

初始版本

%timeit mb_r(forecasted_array, observed_array)
23.4 s ± 430 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

numexpr

%%timeit
forecasted_array2d = forecasted_array[:, np.newaxis]
ne.evaluate('sum(abs(forecasted_array2d - observed_array))')[()]
784 ms ± 11.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

广播版

%timeit mb_r_bcast(forecasted, observed)
1.47 s ± 4.13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

内循环展开版

%timeit mb_r_unroll(forecasted, observed)
389 ms ± 11.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

numba njit(parallel=True) 版本

%timeit mb_r_njit(forecasted_array, observed_array)
32 ms ± 4.05 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

可以看出njit的方法比你的初始方案快730x,也比numexpr的方案快24.5x(也许你需要Intel的Vector数学库来加速它)。与初始版本相比,内循环展开的简单方法也可以让您的速度提高 60 倍。我的规格是:

Intel(R) Core(TM)2 四核 CPU Q9550 2.83GHz
Python 3.6.3
numpy 1.13.3
numba 0.36.1
numexpr 2.6.4

最后说明

我对您的短语感到惊讶“我听说(尚未测试)使用 python for 循环索引 numpy 数组非常慢。”所以我测试:

arr = np.arange(1000)
ls = arr.tolistist()

%timeit for i in arr: pass
69.5 µs ± 282 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

%timeit for i in ls: pass
13.3 µs ± 81.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit for i in range(len(arr)): arr[i]
167 µs ± 997 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

%timeit for i in range(len(ls)): ls[i]
90.8 µs ± 1.07 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

事实证明你是对的。迭代列表的速度要快 2-5 倍。当然,这些结果必须带有一定的讽刺意味:)

【讨论】:

  • 所以我只是在我的计算机上运行了测试,带有@njit 装饰器的 numba 模块的运行速度确实快了大约 17 倍!谢谢!
  • @Wade 快五倍然后什么?
  • 它的运行速度比 numerexp 快了大约 17 倍,这非常令人惊讶!
【解决方案3】:

作为参考,以下代码:

#pythran export mb_r(float64[], float64[])
import numpy as np

def mb_r(forecasted_array, observed_array):
    return np.abs(forecasted_array[:,None] - observed_array).sum()

在纯 CPython 上以以下速度运行:

% python -m perf timeit -s 'import numpy as np; x = np.random.rand(400); y = np.random.rand(400); from mbr import mb_r' 'mb_r(x, y)' 
.....................
Mean +- std dev: 730 us +- 35 us

当使用Pythran 编译时,我得到了

% pythran -march=native -DUSE_BOOST_SIMD mbr.py
% python -m perf timeit -s 'import numpy as np; x = np.random.rand(400); y = np.random.rand(400); from mbr import mb_r' 'mb_r(x, y)'
.....................
Mean +- std dev: 65.8 us +- 1.7 us

在带有 AVX 扩展的单核上,速度大约提高了 10 倍。

【讨论】:

    猜你喜欢
    • 2017-05-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-27
    • 2020-05-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多