【问题标题】:Speed up nested for loop with elements exponentiation使用元素求幂加速嵌套 for 循环
【发布时间】:2014-02-11 17:42:36
【问题描述】:

我正在编写一个大型代码,我发现自己需要加快其中的特定部分。我创建了一个MWE,如下所示:

import numpy as np
import time

def random_data(N):
    # Generate some random data.
    return np.random.uniform(0., 10., N).tolist()

# Lists that contain all the data.
list1 = [random_data(10) for _ in range(1000)]
list2 = [random_data(1000), random_data(1000)]

# Start taking the time.
tik = time.time()

list4 = []
# Loop through all elements in list1.
for elem in list1:

    list3 = []
    # Loop through elements in list2.
    for elem2 in zip(*list2):

        A = np.exp(-0.5*((elem[0]-elem2[0])/elem[3])**2)
        B = np.exp(-0.5*((elem[1]-elem2[1])/elem[3])**2)
        list3.append(A*B)

    # Sum elements in list3 and append result to list4.
    sum_list3 = sum(list3) if sum(list3)>0. else 1e-06
    list4.append(sum_list3)

# Print the elapsed time.
print time.time()-tik

list1list2 的奇怪格式是因为这就是这段代码接收它们的方式。

花费大部分时间的明显部分是递归计算 AB 术语。

有没有什么方法可以加快这段代码的速度而不必并行化它(我之前尝试过它,它给了我a lot of troubles)?我愿意使用任何软件包,numpyscipy 等。


添加

这是应用 abarnert 优化的结果,也是 Jaime 建议只进行一次幂运算的结果。优化后的功能在我的系统上平均快了约 60 倍。

import numpy as np
import timeit

def random_data(N):
    return np.random.uniform(0., 10., N).tolist()

# Lists that contain all the data.
list1 = [random_data(10) for _ in range(1000)]
list2 = [random_data(1000), random_data(1000)]

array1 = np.array(list1)
array2 = np.array(zip(*list2))


# Old non-optimezed function.
def func1():
    list4 = []
    # Process all elements in list1.
    for elem in list1:
        # Process all elements in list2.
        list3 = []
        for elem2 in zip(*list2):
            A = np.exp(-0.5*((elem[0]-elem2[0])/elem[3])**2)
            B = np.exp(-0.5*((elem[1]-elem2[1])/elem[3])**2)
            list3.append(A*B)
        # Sum elements in list3 and append result to list4.
        sum_list3 = sum(list3) if sum(list3)>0. else 1e-06
        list4.append(sum_list3)

# New optimized function.
def func2():
    list4 = []
    # Process all elements in list1.
    for elem in array1:

        # Broadcast over elements in array2.
        A = -0.5*((elem[0]-array2[:,0])/elem[3])**2
        B = -0.5*((elem[1]-array2[:,1])/elem[3])**2
        array3 = np.exp(A+B)

        # Sum elements in array3 and append result to list4.
        sum_list3 = max(array3.sum(), 1e-10)
        list4.append(sum_list3)


# Get time for both functions.
func1_time = timeit.timeit(func1, number=10)
func2_time = timeit.timeit(func2, number=10)

# Print hom many times faster func2 is versus func1.
print func1_time/func2_time

【问题讨论】:

  • 当你在顶部有一个 NumPy 依赖项时,为什么这段代码都是基于列表的?
  • 没有特别的原因,这就是这段代码从另一段代码接收list1list2 的方式。关于list3list4,这是我能弄清楚如何填充它们的最佳方法。如果您认为这会有所作为,它们都可以转换为 numpy 数组。
  • @Gabriel:当然会有所作为。这就是使用numpy 的全部意义——如果您可以通过数组广播计算,则将 Python 循环替换为 C 循环,并删除每个算术计算周围的所有装箱/拆箱,这意味着您的代码通常从 4 -400 倍快。
  • +1 用于发布优秀的 MWE。一个很好的例子,说明了提出问题并因此得到了很好的答案。已添加书签,以便我以后可以链接到它。
  • 最后一点说明:您不应该尝试使用time.time 自己计时。 timeit 模块(或者,如果你使用 IPython,%timeit 魔法语句)确保选择正确的计时器,处理一系列你甚至不会想到的问题,让你重复测试并正确总结它们,并使事情更容易启动。 (当您的代码花费的时间比您预期的要长 100 倍时,这通常没什么大不了的,但值得养成始终使用timeit 的习惯。)

标签: python performance numpy scipy


【解决方案1】:

您希望逐渐将其从使用列表和循环转换为使用数组和广播,首先抓住最简单和/或最关键的部分,直到速度足够快。

第一步是不要一遍又一遍地这样做zip(*list2)(特别是如果这是Python 2.x)。当我们这样做时,我们不妨将它存储在一个数组中,并对list1 做同样的事情——你现在仍然可以遍历它们。所以:

array1 = np.array(list1)
array2 = np.array(zip(*list2))
# …
for elem in array1:
    # …
    for elem2 in array2:

这不会加快速度——在我的机器上,它需要我们从 14.1 秒到 12.9 秒——但它给了我们一个开始工作的地方。

你还应该去掉sum(list3)的双重计算:

sum_list3 = sum(list3)
sum_list3 = sum_list3 if sum_list3>0. else 1e-06

同时,您希望value <= 0 转到1e-6,但不理会0 < value < 1e-6,这有点奇怪。这真的是故意的吗?如果没有,你可以解决这个问题,同时简化代码,只需这样做:

sum_list3 = max(array3.sum(), 1e-06)

现在,让我们广播AB 的计算:

# Broadcast over elements in list2.
A = np.exp(-0.5*((elem[0]-array2[:,0])/elem[3])**2)
B = np.exp(-0.5*((elem[1]-array2[:, 1])/elem[3])**2)
array3 = A*B

# Sum elements in list3 and append result to list4.
sum_list3 = max(array3.sum(), 1e-06)

list4.append(sum_list3)

这让我们从 12.9 秒缩短到 0.12 秒。您还可以通过array1 进行广播,并用预先分配的数组替换list4 等等,更进一步,但这可能已经足够快了。

【讨论】:

  • 取幂很昂贵:不要将np.exp 取两次然后相乘,而是将两个值相加然后取np.exp
  • @Jaime:我们已经获得了 99% 的加速,我怀疑他还需要 10%。 (我估计很快——跳过exp 将时间缩短到 0.79 倍,所以做一个而不是两个可能大约是 0.9 倍。)但我敢打赌,你可以通过将其移出一个级别来获得更多的改进(一次广播exp 所有行,然后sum 所有行,而不是每行一次),所以我认为如果现有的改进还不够,广播array1array4应该是第一步,然后寻找优化其中的数学。
  • 如果你对整个事情进行矢量化,你可以获得额外的 20%,也就是说,我必须同意,在前 99% 中可以忽略不计。但这是我最讨厌的事情之一,就像不取距离的平方根,如果它的平方足以满足你正在做的事情,我就是无法抗拒,对不起噪音......
  • @Jaime:这不是噪音;值得在 cmets 中进行额外的优化——以防 OP 需要它们,也只是供 OP 学习。我只是在解释为什么我认为不需要进入答案,作为评论可能就足够了。
  • @Gabriel:人们经常不假思索地这么说,但请考虑一下:是否值得花 2 小时的工作来加速您的代码,例如每次运行 180 微秒的增益?这样做 10000 次,你最多节省了 3598.2 秒的负时间——而且你增加了它不正确的机会,以及调试它所需的时间(尤其是如果你发现新的版本更难理解)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-10
  • 1970-01-01
  • 2021-04-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多