【问题标题】:Eliminate for-loop in Python and Numpy construct消除 Python 和 Numpy 构造中的 for 循环
【发布时间】:2012-01-31 20:06:00
【问题描述】:

我正在寻找一种 Python 和/或 Numpy 矢量化方法来消除对以下内容的 for 循环的使用:

for i in list_range_values:
    v[list_list_values[i]] += list_comp_values[i]

地点:

  • list_range_values 是一个 Python 整数值列表,例如。 [1, 3, 5],取自范围(0, R-1, 1)

  • list_comp_values 是一个 Python 数值列表,例如。 [0.7, 9.8, 1.2, 5, 10, 11.7, 6, 0.2] 使得 len(list_comp_values) = R

  • v 是一个长度为 V 的 numpy 向量,使得 R 可以是 而不是 V

  • list_list_values 是一个 Python 列表列表(每个列表包含不同数量的整数值,例如 [[3, 6, 7], [5, 7, 11, 25, 99], [8, 45 ], [4, 7, 8], [0, 1], [21, 31, 41], [9, 11, 22, 33, 44], [17, 19]]) 从范围 (0, V-1, 1) 和 len(list_list_values) = R

例如。

for i in list_range_values (= [1, 3, 5]):
    i=1: v[[5, 7, 11, 25, 99]] += list_comp_values[1] (= 9.8)
    i=3: v[[4, 7, 8]] += list_comp_values[3] (= 5)
    i=5: v[[21, 31, 41]] += list_comp_values[5] (= 11.7)

有没有一种方法可以消除for循环?

Cython、Scipy/Weave/Blitz 和 C 模块是替代解决方案,但要先确定是否有 Numpy 矢量化答案。

【问题讨论】:

  • 这是对stackoverflow.com/questions/8695806/… 上一个问题的重新表述,今天晚些时候将被删除。
  • 为什么要摆脱循环?这对我来说似乎很合适。
  • 我已经为您之前的问题找到了一种方法(使用numpy.bincount),但是对于这个问题,如果list_list_values,您需要为每组设置任意权重,我认为没有方式
  • @Niklas Baumstark。当列表的数据大小和列表的列表长度为数百万项时,for 循环是合适的,但不适用于提供性能。
  • @dbv:我明白了。如果 numpy 不提供所需的功能(我不知道),也许用 C 编写该循环是最简单的。

标签: python for-loop numpy


【解决方案1】:

虽然消除 for 循环并利用 numpy 内置插件/矢量化通常会大大加快速度。我只想指出,情况并非总是如此。将简单的 for 循环与更多涉及的矢量化进行计时,不会给您带来很大的加速并且更加冗长。只是需要考虑的事情:

from timeit import Timer

setstr="""import numpy as np
import itertools
import random

Nlists = 1000
total_lists = 5000
outsz = 100
maxsublistsz = 100


# create random list of lists
list_range_values = random.sample(xrange(total_lists),Nlists)
list_list_values = [random.sample(xrange(outsz),np.random.randint(1,maxsublistsz)) for k in xrange(total_lists)]

list_comp_values = 10*np.random.uniform(size=(total_lists,))

v = np.zeros((outsz,))

def indices(start, end):
    lens = end - start
    np.cumsum(lens, out=lens)
    i = np.ones(lens[-1], dtype=int)
    i[0] = start[0]
    i[lens[:-1]] += start[1:]
    i[lens[:-1]] -= end[:-1]
    np.cumsum(i, out=i)
    return i

def sum_by_group(values, groups):
    order = np.argsort(groups)
    groups = groups[order]
    values = values[order]
    values.cumsum(out=values)
    index = np.ones(len(groups), 'bool')
    index[:-1] = groups[1:] != groups[:-1]
    values = values[index]
    groups = groups[index]
    values[1:] = np.diff(values)
    return values, groups


"""

method1="""
list_list_lens = np.array(map(len, list_list_values))
comp_vals_expanded = np.repeat(list_comp_values, list_list_lens)

list_vals_flat = np.fromiter(itertools.chain.from_iterable(list_list_values),dtype=int)
list_list_starts = np.concatenate(([0], np.cumsum(list_list_lens)[:-1]))

toadd = indices(list_list_starts[list_range_values],(list_list_starts + list_list_lens)[list_range_values])

v[list_vals_flat[toadd]] += comp_vals_expanded[toadd]
"""

method2="""
for k in list_range_values:
    v[list_list_values[k]] += list_comp_values[k]

"""

method3="""
llv = [list_list_values[i] for i in list_range_values]
lcv = [list_comp_values[i] for i in list_range_values]
counts = map(len, llv)
indices = np.concatenate(llv)
values = np.repeat(lcv, counts)

totals, indices_unique = sum_by_group(values, indices)
v[indices_unique] += totals
"""


t1 = Timer(method1,setup=setstr).timeit(100)
print t1

t2 = Timer(method2,setup=setstr).timeit(100)
print t2

t3 = Timer(method3,setup=setstr).timeit(100)
print t3

对于列表中的大量元素:

方法1:(无for循环-jterrace)1.43秒

方法2:(for循环)4.62秒

方法3:(无for循环-bago)2.99秒

对于少量列表(将Nlists改为10),for循环明显快于jterrace的解决方案:

方法1:(无for循环-jterrace)1.05秒

方法2:(for循环)0.045秒

方法3:(无for循环-bago)0.041秒

这不是要敲@jterrace 或@bago 的解决方案,它们相当优雅。而是要指出,简单的 for 循环通常不会表现得那么差。

【讨论】:

  • 非常感谢您的时间安排,因为这正是我想知道的。我提出问题的动机是我们的列表和列表列表非常大(以百万计)并且会随着时间的推移而变得更大。我们广泛使用 Numpy 向量化,这是系统中唯一剩下的 for 循环。
  • 很好,但import itertools 应该放在设置中,而不是方法中。 Nlists=10 的 2.89 秒似乎很可疑。
  • 另外,如果您将map 更改为itertools.imap,可能会有所不同。
  • @JoshAdel,很好。我很想知道我的numpy.histogram 解决方案如何与上述内容相结合......
  • @dbv 如果这个数字以百万计,如果您没有遇到内存问题,矢量化可能会提供显着的加速。我也认为看看 cython 是个好主意。我个人在过去取得了很大的成功,因为它可以为那些不能很好地矢量化或者需要一个不适合内存的大型中间数组的东西获得很大的加速。
【解决方案2】:

使用您的示例输入:

>>> list_list_values = [[3, 6, 7], [5, 7, 11, 25, 99], [8, 45], [4, 7, 8], 
                        [0, 1], [21, 31, 41], [9, 11, 22, 33, 44], [17, 19]]
>>> list_comp_values = [0.7, 9.8, 1.2, 5, 10, 11.7, 6, 0.2]
>>> list_range_values = [1, 3, 5]

首先,一些发电机恶作剧:

>>> indices_weights = ((list_list_values[i], list_comp_values[i]) 
                       for i in list_range_values)
>>> flat_indices_weights = ((i, weight) for indices, weight in indices_weights 
                             for i in indices)

现在我们将数据传递给numpy。我不知道如何从迭代器生成rec.array,所以我不得不将上面的生成器转换为列表。也许有办法避免这种情况......

>>> i_w = numpy.rec.array(list(flat_indices_weights),       
                          dtype=[('i', int), ('weight', float)])
>>> numpy.histogram(i_w['i'], bins=range(0, 100), weights=i_w['weight'])
(array([  0. ,   0. ,   0. ,   0. ,   5. ,   9.8,   0. ,  14.8,   5. ,
         0. ,   0. ,   9.8,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,  11.7,   0. ,   0. ,   0. ,   9.8,   0. ,
         0. ,   0. ,   0. ,   0. ,  11.7,   0. ,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,   0. ,   0. ,  11.7,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,
         0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   0. ,   9.8]), 
 array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
       34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
       51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
       68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
       85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]))

我有时间用我自己的几个来跟进 JoshAdel 的测试。迄今为止最快的解决方案使用 Bago 的设置,但用内置的 histogram 函数替换了 sum_by_group 函数。这是我得到的数字(更新):

方法1(jterrace):2.65

方法2(for循环):2.25

方法 3(Bago):1.14

方法4(直方图):2.82

方法 5(3/4 组合):1.07

请注意,正如这里实现的那样,根据我的测试,第一种方法给出的结果不正确。我没有时间弄清楚问题出在哪里。我的测试代码如下;它只是轻轻地调整了 JoshAdel 的原始代码,但为了方便起见,我将其完整发布在这里。 (已更新以包括 Bago 的 cmets 并且有些混乱。)

from timeit import Timer

setstr="""import numpy as np
import itertools
import random

Nlists = 1000
total_lists = 5000
outsz = 100
maxsublistsz = 100

# create random list of lists
list_range_values = random.sample(xrange(total_lists),Nlists)
list_list_values = [random.sample(xrange(outsz),np.random.randint(1,maxsublistsz)) for k in xrange(total_lists)]

list_comp_values = list(10*np.random.uniform(size=(total_lists,)))

v = np.zeros((outsz,))

def indices(start, end):
    lens = end - start
    np.cumsum(lens, out=lens)
    i = np.ones(lens[-1], dtype=int)
    i[0] = start[0]
    i[lens[:-1]] += start[1:]
    i[lens[:-1]] -= end[:-1]
    np.cumsum(i, out=i)
    return i

def sum_by_group(values, groups):
    order = np.argsort(groups)
    groups = groups[order]
    values = values[order]
    values.cumsum(out=values)
    index = np.ones(len(groups), 'bool')
    index[:-1] = groups[1:] != groups[:-1]
    values = values[index]
    groups = groups[index]
    values[1:] = np.diff(values)
    return values, groups


"""

setstr_test = setstr + "\nprint_v = True\n"

method1="""
list_list_lens = np.array(map(len, list_list_values))
comp_vals_expanded = np.repeat(list_comp_values, list_list_lens)

list_vals_flat = np.fromiter(itertools.chain.from_iterable(list_list_values),dtype=int)
list_list_starts = np.concatenate(([0], np.cumsum(list_list_lens)[:-1]))

toadd = indices(list_list_starts[list_range_values],(list_list_starts + list_list_lens)[list_range_values])

v[list_vals_flat[toadd]] += comp_vals_expanded[toadd]
"""

method2="""
for k in list_range_values:
    v[list_list_values[k]] += list_comp_values[k]
"""

method3="""
llv = [np.fromiter(list_list_values[i], 'int') for i in list_range_values]
lcv = [list_comp_values[i] for i in list_range_values]
counts = map(len, llv)
indices = np.concatenate(llv)
values = np.repeat(lcv, counts)

totals, indices_unique = sum_by_group(values, indices)
v[indices_unique] += totals
"""

method4="""
indices_weights = ((list_list_values[i], list_comp_values[i]) for i in list_range_values)
flat_indices_weights = ((i, weight) for indices, weight in indices_weights for i in indices)
i_w = np.rec.array(list(flat_indices_weights), dtype=[('i', 'i'), ('weight', 'd')])
v += np.histogram(i_w['i'], bins=range(0, outsz + 1), weights=i_w['weight'], new=True)[0]
"""

method5="""
llv = [np.fromiter(list_list_values[i], 'int') for i in list_range_values]
lcv = [list_comp_values[i] for i in list_range_values]
counts = map(len, llv)
indices = np.concatenate(llv)
values = np.repeat(lcv, counts)

v += np.histogram(indices, bins=range(0, outsz + 1), weights=values, new=True)[0]
"""


t1 = Timer(method1,setup=setstr).timeit(100)
print t1

t2 = Timer(method2,setup=setstr).timeit(100)
print t2

t3 = Timer(method3,setup=setstr).timeit(100)
print t3

t4 = Timer(method4,setup=setstr).timeit(100)
print t4

t5 = Timer(method5,setup=setstr).timeit(100)
print t5

exec(setstr_test + method1 + "\nprint v\n")
exec("\nv = np.zeros((outsz,))\n" + method2 + "\nprint v\n")
exec("\nv = np.zeros((outsz,))\n" + method3 + "\nprint v\n")
exec("\nv = np.zeros((outsz,))\n" + method4 + "\nprint v\n")
exec("\nv = np.zeros((outsz,))\n" + method5 + "\nprint v\n")

【讨论】:

  • 请注意,为避免将 98 和 99 放在同一个 bin 中,您必须改用 bins=range(0, 101)。
  • 我稍微改变了设置,llv = [np.fromiter(list_list_values[i], 'int') for i in list_range_values]。传递一个 ndarrays 列表来连接似乎比传递一个列表列表要快得多。直方图方法是个好主意。除非索引非常稀疏,否则最好避免在赋值左侧有索引。
  • @Bago,感谢您指出这一点——它有很大的不同!我已经更新了时间和代码。
【解决方案3】:

首先,设置你给的变量:

import numpy as np
list_range_values = [1, 3, 5]
list_list_values = [[3, 6, 7], [5, 7, 11, 25, 99], [8, 45],
                    [4, 7, 8], [0, 1], [21, 31, 41]]
list_comp_values = [0.7, 9.8, 1.2, 5, 10, 11.7]
v = np.arange(100, dtype=float)

接下来,list_list_values 和 list_comp_values 需要被展平,以便它们是连续的:

list_list_lens = np.array(map(len, list_list_values))
comp_vals_expanded = np.repeat(list_comp_values, list_list_lens)
import itertools
list_vals_flat = np.fromiter(itertools.chain.from_iterable(list_list_values),
                             dtype=int)

然后,需要每个子数组的起始索引:

list_list_starts = np.concatenate(([0], np.cumsum(list_list_lens)[:-1]))

现在我们有了起始值和结束值,我们可以使用indices function from this question 来获取选择器索引数组:

def indices(start, end):
    lens = end - start
    np.cumsum(lens, out=lens)
    i = np.ones(lens[-1], dtype=int)
    i[0] = start[0]
    i[lens[:-1]] += start[1:]
    i[lens[:-1]] -= end[:-1]
    np.cumsum(i, out=i)
    return i

toadd = indices(list_list_starts[list_range_values],
                (list_list_starts + list_list_lens)[list_range_values])

现在我们已经完成了所有这些魔术,可以像这样添加数组:

v[list_vals_flat[toadd]] += comp_vals_expanded[toadd]

【讨论】:

  • 谢谢。结果与for循环相同。您的解决方案类似于 Bago 的解决方案,它可以满足我们的需求。
  • @dbv 和@jterrace。当list_vals_flat[toadd] 中有重复时,你们检查过这是否有效吗?除非我遗漏了什么,否则此方法可能存在错误。
猜你喜欢
  • 2018-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-24
  • 2017-11-01
  • 2021-12-20
  • 2014-01-29
  • 1970-01-01
相关资源
最近更新 更多