将NumPy数组转换为集合需要太长时间

16

我正在尝试执行以下操作

from numpy import *
x = array([[3,2,3],[711,4,104],.........,[4,4,782,7845]])  # large nparray
for item in x:
    set(item)

而且与之相比,需要很长时间:

x = array([[3,2,3],[711,4,104],.........,[4,4,782,7845]])  # large nparray
for item in x:
    item.tolist()
为什么将NumPy数组转换为set比转换为list需要更长的时间? 我的意思是两者复杂度基本相同,都为O(n)

1
转换为set需要进行一些哈希和计算,以确定在集合中插入新元素的位置。转换为list可能只是一个简单的复制。 - Jean-François Fabre
2
你的 x 是什么形状?数据类型是什么?我看到了3个和4个元素的子列表。但是,是的,set 是一个类似于字典(只有键没有值)的Python对象。tolist 是一个编译numpy方法。 - hpaulj
让n为x中项目的长度。在最坏的情况下,每个子列表中可能有n个项目。有没有更快的选项? - Felix Ha
在主应用程序中,我有一个二维数据集。接下来,我使用kd树执行一个范围查询,该查询返回一个类似上述的np数组。每个子列表都包含从一个点开始的邻居的索引(因此每个子列表中可能会有N个)。但是,在我的应用程序的下一步中,我需要将点的邻居的索引作为一个集合来使用。 - Felix Ha
下一步是将每个点与最佳点的交集。最佳点是邻居最多的点。如果我使用集合,对于每个点只需要(最坏情况)O(n) 的时间。我已经尝试过numpy.intersect1d和列表,但是集合的性能比其他两种方法要好得多(除了第一步)。 - Felix Ha
显示剩余2条评论
2个回答

35
TL;DR: set()函数使用Python迭代协议创建一个集合。但是,在Python级别上遍历NumPy数组非常慢,因此在进行迭代之前,使用tolist()将数组转换为Python列表会更快。
要理解为什么遍历NumPy数组很慢,重要的是要知道Python对象、Python列表和NumPy数组在内存中的存储方式。
Python对象需要一些簿记属性(如引用计数、链接到其类的指针等)以及它所表示的值。例如,整数ten = 10可能看起来像这样:

enter image description here

蓝色圆圈代表在Python解释器中用于变量ten的“名称”,而下方的对象(实例)实际上表示整数(由于此处不重要,因此我在图像中忽略了它们的记录属性)。
Python的list只是Python对象的集合,例如mylist = [1, 2, 3]将被保存如下:

enter image description here

这次列表引用了Python整数123以及名称mylist只是引用list实例。

但是一个数组myarray = np.array([1, 2, 3])不会将Python对象存储为元素:

enter image description here

NumPy的array实例直接存储值123
使用这些信息,我可以解释为什么迭代数组比迭代列表要慢得多:
每次访问列表中的下一个元素时,列表只返回一个存储的对象。这非常快,因为元素已经存在作为Python对象(它只需要将引用计数增加一)。
另一方面,当您想要一个数组元素时,它需要创建一个新的Python“盒子”来存储值以及所有簿记信息,然后才能返回。当您遍历数组时,它需要为数组中的每个元素创建一个Python盒子。

enter image description here

创建这些盒子很慢,这也是为什么迭代NumPy数组比迭代Python集合(列表/元组/集合/字典)慢得多的主要原因,因为它们存储值及其盒子
import numpy as np
arr = np.arange(100000)
lst = list(range(100000))

def iterateover(obj):
    for item in obj:
        pass

%timeit iterateover(arr)
# 20.2 ms ± 155 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit iterateover(lst)
# 3.96 ms ± 26.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

set 的“构造函数”只是对对象进行迭代。

有一件事情我无法确定,那就是为什么 tolist 方法更快。最终,结果 Python 列表中的每个值都需要在“Python 盒子”中,因此 tolist 无法避免太多工作。但其中一件确定的事情是,list(array)array.tolist() 更慢:

arr = np.arange(100000)

%timeit list(arr)
# 20 ms ± 114 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit arr.tolist()
# 10.3 ms ± 253 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

每个操作的时间复杂度都是O(n),但常数因子不同。
在您的情况下,您将set()与tolist()进行了比较,这并不是一个特别好的比较。将set(arr)与list(arr)或set(arr.tolist())与arr.tolist()进行比较会更有意义。
arr = np.random.randint(0, 1000, (10000, 3))

def tosets(arr):
    for line in arr:
        set(line)

def tolists(arr):
    for line in arr:
        list(line)

def tolists_method(arr):
    for line in arr:
        line.tolist()

def tosets_intermediatelist(arr):
    for line in arr:
        set(line.tolist())

%timeit tosets(arr)
# 72.2 ms ± 2.68 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit tolists(arr)
# 80.5 ms ± 2.18 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit tolists_method(arr)
# 16.3 ms ± 140 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit tosets_intermediatelist(arr)
# 38.5 ms ± 200 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

如果你想要set,最好使用set(arr.tolist())。对于更大的数组,使用np.unique可能会更有意义,但由于你的行只包含3个项目,所以这可能会更慢(对于数千个元素,它可能会快得多!)。
在评论中,您询问了有关numba的问题,是的,numba可以加速此过程。Numba支持类型化集合(仅数字类型),但这并不意味着它总是更快。
我不确定numba如何重新实现集合,但由于它们是类型化的,很可能也避免了“Python盒子”,并直接将值存储在集合内部:

enter image description here

由于涉及到哈希和空槽(Python 在集合中使用开放地址法),因此集合比列表更加复杂。

与 NumPy 的 array 类似,numba 的 set 直接保存值。因此,当您将 NumPy 的 array 转换为 numba 的 set(或反之亦然)时,它根本不需要使用“Python boxes”,因此在 numba 的 nopython 函数中创建 set 会比 set(arr.tolist()) 操作快得多:

import numba as nb
@nb.njit
def tosets_numba(arr):
    for lineno in range(arr.shape[0]):
        set(arr[lineno])

tosets_numba(arr)  # warmup
%timeit tosets_numba(arr)
# 6.55 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这比使用set(arr.tolist())方法快大约五倍。但需要强调的是,我没有从函数中返回set。当您从Numba nopython函数返回一个set到Python时,Numba会创建一个Python set——包括为集合中所有值“创建盒子”(这是Numba隐藏的内容)。

顺便说一下:如果您将list传递给Numba nopython函数或从这些函数返回列表,则会发生相同的装箱/拆箱操作。因此,在Numba中,Python中的O(1)操作变成了O(n)操作!这就是为什么通常最好将NumPy数组传递给numba nopython函数(这是O(1))。

enter image description here

我假设如果你从函数中返回这些集合(现在不太可能,因为numba目前不支持列表),这将会更慢(因为它创建了一个numba集合,并稍后将其转换为python集合),或者只是稍微快一点(如果numbaset -> pythonset的转换速度真的非常快)。
个人而言,我仅会在以下条件下使用numba来处理集合:不需要从函数中返回集合且所有操作都在函数内部进行,并且集合上的所有操作都支持nopython模式。在其他任何情况下,我都不会在此处使用numba。
注意:应避免使用from numpy import *,因为这样会隐藏一些Python内置函数(例如summinmax等),并将大量内容放入全局变量中。最好使用import numpy as np。在函数调用前加上np.可以使代码更清晰,而且打字量也不多。

@FelixHa 是的,如果你不需要函数外的集合,numba 可能会更快。我已经更新了答案 :) - MSeifert
谢谢,它真的加快了结果!你有想过如何在Cython中实现它吗? - Felix Ha
@FelixHa 是的,使用Cython,您可以将Python解决方案的速度提高2倍(它减少了循环开销和函数调用开销)。但是,如果没有专用的数据结构(也许有一些C++数据结构,但我还没有检查),那么这将没有多大意义。如果您使用IPython,可以简单地使用%load_ext cython,然后使用%%cython编译一个块。我检查了答案中的4个函数,与纯Python相比,所有函数都快1.5-2倍。但这比numba慢得多。 - MSeifert
@TadhgMcDonald-Jensen 嗯,对于 object 数组来说更为复杂。对于一维数组,它们的存储方式类似于列表。但对于多维数组,则会有所不同:使用嵌套列表时,外部列表引用内部列表,而最内层列表引用实际对象——对象数组将是一个包含对所有元素(作为不同对象)的引用的数组,但使用步幅“模拟”多维度。我认为,对于 array -> set 转换,tolist 方法会更快(至少我的电脑是这样说的)。 - MSeifert
1
这个回答像你的许多其他回答一样,非常详尽。我时不时地浏览你的回答并从中学到很多东西。我知道你不需要这个,但请把它视为一个小小的表达。 :) - ayhan
显示剩余4条评论

1

以下是一种加快速度的方法:避免使用循环,而是使用multiprocessing pool.map技巧。

from multiprocessing.dummy import Pool as ThreadPool
import multiprocessing

pool = ThreadPool(multiprocessing.cpu_count()) # get the number of CPU
y = pool.map(set,x) # apply the function to your iterable
pool.close()
pool.join()

有趣,你有任何数据(时间)支持这个说法,它实际上“加速了事情的进展”吗? - MSeifert
尝试一下这个:http://chriskiehl.com/article/parallelism-in-one-line/,真的很神奇!它改变了我编码的整个方式。 - zar3bski
由于我认为您的任务在计算方面不需要太长时间,因此您可以尝试在ThreadPool()中使用更高的int值(例如,对于与远程服务器的握手,我使用30)。 - zar3bski
但是,如果我尝试您的代码,它比问题中的原始代码要慢20-500倍(取决于输入类型),因此我想知道您使用了什么数据或如何计时来证明“加速”语句。 - MSeifert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接