【问题标题】:Converting NumPy array to a set takes too long将 NumPy 数组转换为集合需要太长时间
【发布时间】:2017-10-28 17:05:57
【问题描述】:

我正在尝试执行以下操作

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)

【问题讨论】:

  • 转换为set 涉及一些散列和计算“在哪里”插入新元素到集合中。转换为list 可能只是一个简单的副本。
  • 你的x是什么形状的?类型?我看到 3 和 4 个元素的子列表。但是是的,set 是一个类似于字典的 Python 对象(键但没有值)。 tolist 是一个编译 numpy 方法。
  • 设 n 为 x 中项目的长度。在值得的情况下,每个子列表中可能有 n 个项目。有没有更快的方法?
  • a中的每个子列表做set并扔掉结果的目的是什么?
  • 在主应用程序中我有一个二维数据集。接下来,我使用 kd 树执行范围查询,它返回一个如上所述的 np 数组。每个子列表都有一个点的邻居的索引(因此每个子列表中它可能是 N)。但是我需要邻居的索引作为我应用程序下一步的集合。

标签: python arrays performance numpy set


【解决方案1】:

TL;DR:set() 函数使用 Python 迭代协议创建一个集合。但是在 NumPy 数组上迭代(在 Python 级别上)非常慢,以至于在进行迭代之前使用 tolist() 将数组转换为 Python 列表会(快得多)。

要了解迭代 NumPy 数组为何如此缓慢,了解 Python 对象、Python 列表和 NumPy 数组是如何存储在内存中的非常重要。

Python 对象需要一些簿记属性(如引用计数、指向其类的链接……)和它所代表的。例如整数ten = 10 可能如下所示:

蓝色圆圈是您在 Python 解释器中为变量 ten 使用的“名称”,而较低的对象(实例)是实际代表整数的对象(因为这里的簿记属性并不重要,我在图片)。

Python list 只是 Python 对象的集合,例如 mylist = [1, 2, 3] 会这样保存:

这次列表引用了 Python 整数 123,而名称 mylist 仅引用了 list 实例。

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

123 直接存储在 NumPy array 实例中。


有了这些信息,我可以解释为什么迭代 array 比迭代 list 慢得多:

每次访问list 中的下一个元素时,list 只会返回一个存储的对象。这非常快,因为该元素已经作为 Python 对象存在(它只需要将引用计数加一)。

另一方面,当您想要 array 的元素时,它需要在返回之前为包含所有簿记内容的值创建一个新的 Python“框”。当您遍历数组时,它需要为数组中的每个元素创建一个 Python 框:

创建这些盒子很慢,这是迭代 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)

所以如果你想要sets,你最好使用set(arr.tolist())。对于更大的数组,可能使用np.unique 是有意义的,但因为您的行只包含 3 个可能会更慢的项目(对于数千个元素,它可能要快得多! )。


在您询问有关 numba 的 cmets 中,是的,numba 确实可以加快这一速度。 Numba supports typed sets (only numeric types),但这并不意味着它会总是更快。

我不确定 numba(重新)如何实现 sets,但由于它们是键入的,因此它们很可能也避免了“Python 框”并将值直接存储在 set 中:

集合比lists 更复杂,因为它涉及hashes 和空槽(Python 对集合使用开放寻址,所以我认为 numba 也会如此)。

与 NumPy array 一样,numba set 直接保存值。因此,当您将 NumPy array 转换为 numba set(或反之亦然)时,根本不需要使用“Python 盒子”,因此当您在 numba 中创建 sets 时nopython 函数它会比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()) 方法快大约五倍。但重要的是要强调我没有从函数中返回sets。当您从 nopython numba 函数返回 set 到 Python Numba 时,会创建一个 python 集 - 包括为集中的所有值“创建框”(这是 numba 隐藏的东西)。

仅供参考:如果您将 lists 传递给 Numba nopython 函数或从这些函数返回列表,则会发生相同的装箱/拆箱。那么 Python 中的 O(1) 操作是 Numba 中的 O(n) 操作!这就是为什么通常最好将 NumPy 数组传递给 numba nopython 函数(即O(1))。

我假设如果您从函数中返回这些集合(现在实际上不可能,因为 numba 目前不支持集合列表)它会更慢(因为它创建了一个 numba 集合 并且 稍后将其转换为 python 集)或仅稍微快一点(如果转换 numbaset -> pythonset 真的非常快)。

仅当我不需要从函数中返回它们并且在函数内对集合执行所有操作并且时,我个人才会将 numba 用于集合在 nopython 模式下支持。在任何其他情况下,我都不会在这里使用 numba。


请注意:from numpy import * 应该避免使用,这样做时会隐藏几个 python 内置函数(summinmax,...),它会放很多东西进入你的全局变量。最好使用import numpy as np。函数调用前面的np. 使代码更清晰,无需输入太多内容。

【讨论】:

  • 如果我使用 numba 会更快地遍历循环吗?
  • @FelixHa 是的,numba 可能更快 - 如果您不需要函数之外的集合。我更新了答案:)
  • 感谢它确实加快了结果!!! ,你知道如何在 cython 中实现它吗?
  • @FelixHa 是的,使用 Cython 您可以将 python 解决方案加速 2 倍(它减少了循环开销和函数调用开销)。但是如果没有专门的数据结构(可能有一些 c++ 数据结构,但我没有检查过),这个问题就没有多大意义。如果您使用 IPython,您可以简单地使用 %load_ext cython,然后使用 %%cython 编译一个块。我检查了答案中的 4 个函数,与纯 python 相比,所有函数都快 1.5-2 倍。但这比 numba 慢很多。
  • 与您的许多答案一样,这是一个非常彻底的答案。我不时会浏览您的答案并从中学到很多东西。我知道你不需要这个,但认为这是一个小姿态。 :)
【解决方案2】:

这是一种加快速度的方法:避免循环并使用 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()

【讨论】:

  • 有趣,你有任何数据(时间)来支持它实际上“加快速度”的说法吗?
  • chriskiehl.com/article/parallelism-in-one-line 试试看,太棒了!它改变了我的整个编码方法
  • 由于我认为您的任务不会因为计算原因花费很长时间,您可以在 ThreadPool() 中尝试更高的 int 值(例如,对于与远程服务器的握手,我使用 30)
  • 但是如果我尝试您的代码,它会比问题中的原始代码慢 20-500 倍(取决于输入的类型),所以我想知道您使用了什么数据或者您如何安排时间来证明“加快速度”声明的合理性。
猜你喜欢
  • 1970-01-01
  • 2019-10-05
  • 2016-10-20
  • 2015-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-04
相关资源
最近更新 更多