所以我试着编写一个python函数来返回一个名为Mielke-Berry R值的度量。度量标准计算如下:
我编写的当前代码有效,但由于等式中的和的总和,我能想到解决它的唯一问题是在Python中使用嵌套的for循环,这非常慢...... / p>
以下是我的代码:
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功能可以更快地解决这个问题,任何人都知道什么?
答案 0 :(得分:9)
这是一种利用broadcasting
获取total
-
np.abs(forecasted_array[:,None] - observed_array).sum()
要同时接受列表和数组,我们可以使用NumPy内置的外部减法,如下所示 -
np.abs(np.subtract.outer(forecasted_array, observed_array)).sum()
我们还可以使用numexpr
module进行更快的absolute
计算,并在一次summation-reductions
次调用中执行numexpr evaluate
,因此会更有效,如此 -
import numexpr as ne
forecasted_array2D = forecasted_array[:,None]
total = ne.evaluate('sum(abs(forecasted_array2D - observed_array))')
答案 1 :(得分:2)
如果您不受内存限制,优化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)
正如您所看到的,对于小型阵列,广播版本比展开的版本 20x 更快,对于中型阵列,它们相当相等,但对于大型阵列强4倍,因为内存开销正在付出昂贵的代价。
另一种方法是使用numba
及其 magic 强大的@jit
函数装饰器。在这种情况下,只需稍微修改您的初始代码即可。另外,为了使循环平行,您应该将range
更改为prange
并提供parallel=True
关键字参数。在下面的代码段中,我使用@njit
装饰器,它与@jit(nopython=True)
相同:
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科学生态系统非常庞大,我只提到了一些其他等效选项来加快速度:Cython
,Nuitka
,Pythran
,bottleneck
等等。也许你对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 快,并且 24.5x 比numexpr
解决方案更快(也许你需要英特尔的矢量数学库来加速它)。与初始版本相比,使用内循环展开的简单方法可使您加速 60x 。我的规格是:
英特尔(R)酷睿(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-5x 更快。当然,这些结果必须带有一定的反讽:)
答案 2 :(得分:1)
作为参考,以下代码:
#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扩展的单核上,大概是x10加速。