我有一个使用for循环的函数,我想用numpy来提高速度。但这似乎没有做到这一点,因为颠簸版似乎慢了2倍。这是代码:
import numpy as np
import itertools
import timeit
def func():
sample = np.random.random_sample((100, 2))
disc1 = 0
disc2 = 0
n_sample = len(sample)
dim = sample.shape[1]
for i in range(n_sample):
prod = 1
for k in range(dim):
sub = np.abs(sample[i, k] - 0.5)
prod *= 1 + 0.5 * sub - 0.5 * sub ** 2
disc1 += prod
for i, j in itertools.product(range(n_sample), range(n_sample)):
prod = 1
for k in range(dim):
a = 0.5 * np.abs(sample[i, k] - 0.5)
b = 0.5 * np.abs(sample[j, k] - 0.5)
c = 0.5 * np.abs(sample[i, k] - sample[j, k])
prod *= 1 + a + b - c
disc2 += prod
c2 = (13 / 12) ** dim - 2 / n_sample * disc1 + 1 / (n_sample ** 2) * disc2
def func_numpy():
sample = np.random.random_sample((100, 2))
disc1 = 0
disc2 = 0
n_sample = len(sample)
dim = sample.shape[1]
disc1 = np.sum(np.prod(1 + 0.5 * np.abs(sample - 0.5) - 0.5 * np.abs(sample - 0.5) ** 2, axis=1))
for i, j in itertools.product(range(n_sample), range(n_sample)):
disc2 += np.prod(1 + 0.5 * np.abs(sample[i] - 0.5) + 0.5 * np.abs(sample[j] - 0.5) - 0.5 * np.abs(sample[i] - sample[j]))
c2 = (13 / 12) ** dim - 2 / n_sample * disc1 + 1 / (n_sample ** 2) * disc2
print('Normal function time: ' , timeit.repeat('func()', number=20, repeat=5, setup="from __main__ import func"))
print('numpy function time: ', timeit.repeat('func_numpy()', number=20, repeat=5, setup="from __main__ import func_numpy"))
时序输出为:
Normal function time: [2.831496894999873, 2.832342429959681, 2.8009242500411347, 2.8075121529982425, 2.824807019031141]
numpy function time: [5.154757721000351, 5.2011515340418555, 5.148996959964279, 5.095560318033677, 5.125199959962629]
我在这里缺少什么?我知道瓶颈是itertools部分因为我之前有一个100x100x2循环而不是100x2循环。 你看到另一种方法吗?
答案 0 :(得分:3)
使用NumPy,人们必须想要对事物进行矢量化,我们当然可以这样做。
仔细观察循环部分,我们在输入数据samples
的第一个轴上迭代两次循环启动:
for i, j in itertools.product(range(n_sample), range(n_sample)):
一旦我们让broadcasting
处理这些迭代,我们就可以将这些迭代转换为矢量化操作。
现在,要拥有一个完全向量化的解决方案,我们需要更多的内存空间,特别是(N,N,M)
,其中(N,M)
是输入数据的形状。
这里另一个值得注意的方面是,在每次迭代中,我们都没有做很多工作,因为我们在每一行上执行操作,每行只包含给定样本的2
元素。因此,出现的想法是我们可以沿着M
运行一个循环,这样在每次迭代时,我们都会计算prod
并积累。因此,对于给定的样本,它只是两次循环迭代。
离开循环时,我们会得到累积的prod
,只需将disc2
的求和作为最终输出。
这是实现上述想法的实现 -
prod_arr = 1
for i in range(sample.shape[1]):
si = sample[:,i]
prod_arr *= 1 + 0.5 * np.abs(si[:,None] - 0.5) + 0.5 * np.abs(si - 0.5) - \
0.5 * np.abs(si[:,None] - si)
disc2 = prod_arr.sum()
运行时测试
下面列出了原始方法的循环部分和作为方法的修改版本的精简版本:
def org_app(sample):
disc2 = 0
n_sample = len(sample)
for i, j in itertools.product(range(n_sample), range(n_sample)):
disc2 += np.prod(1 + 0.5 * np.abs(sample[i] - 0.5) + 0.5 * \
np.abs(sample[j] - 0.5) - 0.5 * np.abs(sample[i] - sample[j]))
return disc2
def mod_app(sample):
prod_arr = 1
for i in range(sample.shape[1]):
si = sample[:,i]
prod_arr *= 1 + 0.5 * np.abs(si[:,None] - 0.5) + 0.5 * np.abs(si - 0.5) - \
0.5 * np.abs(si[:,None] - si)
disc2 = prod_arr.sum()
return disc2
计时和验证 -
In [10]: sample = np.random.random_sample((100, 2))
In [11]: org_app(sample)
Out[11]: 11934.878683659041
In [12]: mod_app(sample)
Out[12]: 11934.878683659068
In [14]: %timeit org_app(sample)
10 loops, best of 3: 84.4 ms per loop
In [15]: %timeit mod_app(sample)
10000 loops, best of 3: 94.6 µs per loop
关于 900x
加速!嗯,这应该是足够的激励,希望尽可能地寻找矢量化的东西。
答案 1 :(得分:2)
正如我在评论中提到的,您的解决方案并不是真正的最佳解决方案,并且比较非理想的方法并没有多大意义。
一方面,迭代或索引NumPy数组的单个元素非常慢。我最近回答了一个问题,包括很多细节(如果你有兴趣,可以看一下:"convert np array to a set takes too long")。因此,只需将array
转换为list
:
def func():
sample = np.random.random_sample((100, 2))
disc1 = 0
n_sample = len(sample)
dim = sample.shape[1]
sample = sample.tolist() # converted to list
for i in range(n_sample):
prod = 1
for item in sample[i]:
sub = abs(item - 0.5)
prod *= 1 + 0.5 * sub - 0.5 * sub ** 2
disc1 += prod
disc2 = 0
for i, j in itertools.product(range(n_sample), range(n_sample)):
prod = 1
for k in range(dim):
a = 0.5 * abs(sample[i][k] - 0.5)
b = 0.5 * abs(sample[j][k] - 0.5)
c = 0.5 * abs(sample[i][k] - sample[j][k])
prod *= 1 + a + b - c
disc2 += prod
c2 = (13 / 12) ** dim - 2 / n_sample * disc1 + 1 / (n_sample ** 2) * disc2
我还用正常np.abs
替换了abs
次来电。正常abs
的开销较低!并且还改变了一些其他部分。最后,这比原来的“正常”方法快10-20倍。
我没有时间检查NumPy方法,@ Divarkar已经包含了一个非常好的优化方法。比较两种方法:
def func_numpy():
sample = np.random.random_sample((100, 2))
disc1 = 0
disc2 = 0
n_sample = len(sample)
dim = sample.shape[1]
disc1 = np.sum(np.prod(1 +
0.5 * np.abs(sample - 0.5) -
0.5 * np.abs(sample - 0.5) ** 2,
axis=1))
prod_arr = 1
for i in range(sample.shape[1]):
s0 = sample[:,i]
prod_arr *= (1 +
0.5 * np.abs(s0[:,None] - 0.5) +
0.5 * np.abs(s0 - 0.5) -
0.5 * np.abs(s0[:,None] - s0))
disc2 = prod_arr.sum()
c2 = (13 / 12) ** dim - 2 / n_sample * disc1 + 1 / (n_sample ** 2) * disc2
print('Normal function time: ' ,
timeit.repeat('func()', number=20, repeat=3, setup="from __main__ import func"))
# Normal function time: [1.4846746248249474, 1.5018398493266432, 1.5476674017127152]
print('numpy function time: ',
timeit.repeat('func_numpy()', number=20, repeat=3, setup="from __main__ import func_numpy"))
# numpy function time: [0.020140038561976326, 0.016502230831292763, 0.016452520269695015]
因此,优化的NumPy方法可以完美地击败“优化”的Python方法。它快了近100倍。如果你想要它更快,你可以在纯python代码的略微修改版本上使用numba:
import numba as nb
@nb.njit
def func_numba():
sample = np.random.random_sample((100, 2))
disc1 = 0
n_sample = len(sample)
dim = sample.shape[1]
for i in range(n_sample):
prod = 1
for item in sample[i]:
sub = abs(item - 0.5)
prod *= 1 + 0.5 * sub - 0.5 * sub ** 2
disc1 += prod
disc2 = 0
for i in range(n_sample):
for j in range(n_sample):
prod = 1
for k in range(dim):
a = 0.5 * abs(sample[i,k] - 0.5)
b = 0.5 * abs(sample[j,k] - 0.5)
c = 0.5 * abs(sample[i,k] - sample[j,k])
prod *= 1 + a + b - c
disc2 += prod
return (13 / 12) ** dim - 2 / n_sample * disc1 + 1 / (n_sample ** 2) * disc2
func_numba()
print('numba function time: ' ,
timeit.repeat('func_numba()', number=20, repeat=3, setup="from __main__ import func_numba"))
# numba function time: [0.003022848984983284, 0.0030429566279508435, 0.004060626777572907]
这几乎比NumPy方法快8-10倍。