【问题标题】:Efficient way to set values of numpy array from iterable从可迭代设置numpy数组值的有效方法
【发布时间】:2016-11-30 23:01:47
【问题描述】:

我已经分配了一个给定大小的大型numpy 数组。例如

my_array = numpy.empty(10000, numpy.float)

数组的值可以通过(模拟示例)生成

k * val ** 2 for val in range(0, 10000)

这个设置数组值的步骤做了很多次。例如,for k in range(0,1000)。除了一开始numpy.empty() 所做的分配之外,我不想做任何其他分配。

我考虑过,

my_array = numpy.array([k*val**2 for val in range(0,10000)])

但这看起来至少要分配列表[k * val ** 2 for val in range(0, 10000)]。对吗?

我也看到了numpy.fromiter,但这似乎是为了构造数组。

my_array = numpy.fromiter((k*val**2 for val in range(0,10000)), numpy.float, 10000)

这里真的还有一个分配吗?


要查看numpy.fromiter 是否分配了一个数组,我尝试了以下操作

import numpy as np

iterable1 = (x*x for x in range(5))
iterable2 = (x*x + 1.0 for x in range(5))
my_array = np.fromiter(iterable1, np.float)
print(my_array)
print(hex(id(my_array)))

my_array = np.fromiter(iterable2, np.float)
print(my_array)
print(hex(id(my_array)))

在输出 I 中,打印的两个地址不同。这是否意味着np.fromiter 分配了一个新数组,然后分配给my_array

【问题讨论】:

  • np.fromiter 不做任何进一步的分配。这就是该功能的全部本质。此外,如果您想一次更改所有项目,则无需使用np.empty
  • @Kasramvd 你确定吗?我只是不知道。 fromiter 的文档说它创建了一个数组。我假设它创建了一个 numpy 数组,然后该数组由运算符 = 移动到 my_array。但是,如果您知道没有进行新分配的事实,我会相信您。
  • 如果你必须支持任意迭代器并且不想要任何临时分配,那将很难避免最简单的for ind, elem in enumerate(iterable): my_array[ind] = elem
  • @onekeystrokeatatime 是的,这正是这个函数的作用。它从一个可迭代对象创建一个数组并将其分配给目标变量。如果您正在寻找摆脱这种情况的方法,请查看我的答案。
  • "除了一开始的 numpy.empty() 分配之外,我不想做任何其他分配。" - 你是从 C++ 的角度来看的,数组分配很昂贵,必须避免。这是 Python。与无 JIT 字节码解释、动态调度和单独分配的 24 字节 int 对象的开销相比,数组分配微不足道。

标签: python arrays numpy


【解决方案1】:

鉴于cmets中的解释,问题似乎如下:

  • 大型数组需要经常更新,并且尽可能高效;
  • 更新的来源不仅是其他 numpy 数组,还包括任意 Python 对象(可以即时生成)。

第二个问题是:只要你的值来自 Python,将它们放入一个 numpy 数组永远不会真正有效。这是因为您必须循环解释代码中的每个值。

我期待找到表达式for ind, elem in enumerate(iterable): my_array[ind] = elem 已经打包在一个内置函数中。你知道 Python 解释器是否将该表达式作为一个整体进行编译?

CPython 的虚拟机与 C++ 模型有很大不同;具体来说,编译器无法内联表达式或将其作为一个整体进行解释,以显着提高其效率。即使它支持在 C 中执行这一特定操作的字节码指令,它仍然需要调用生成器的 next 方法,该方法在执行 Python 字节码后将每个值生成为堆分配的 Python 对象。在任何一种情况下,每次迭代都涉及解释代码,您确实希望避免这种情况。

解决问题的有效方法是从头开始设计它,永远不要离开 numpy.正如评论中的其他人所解释的那样,与在 Python 中逐个处理数据的实际成本相比,分配成本(如果有效地由 numpy 完成)是微不足道的。我会设计如下:

  • 从头开始,将尽可能多的代码转换为原生使用 numpy 数组;让返回一个 numpy 数组成为您界面的一部分,不用担心分配成本。在 numpy 本身中执行尽可能多的循环,因此它们是在本机代码中完成的。切勿在 Python 中遍历大型数组的所有值。
  • 在无法使用numpy的地方,尽早使用numpy.fromiter将迭代器转换为numpy数组。
  • 使用my_array[:] = new_array[:]my_array = new_array 将新值引入数组。 (前者在微观上会花费更多时间,但当my_array 在数据模型的许多地方共享时更有意义。)
  • Benchmark 您感兴趣的操作。不要假设“复制很慢” - 可能会证明 C++ 中“慢”的操作比 Python 版本的速度快几个数量级在 C++ 中有效的操作。

如果在执行上述某些操作后 numpy 不支持,并且测量表明它的效率极低,您可以使用Python/C API 创建一个扩展模块,该模块可以有效地执行计算并将结果作为 numpy 返回在 C 中创建的数组。

【讨论】:

    【解决方案2】:

    首先确保您了解变量赋值的作用:

     my_array = numpy.empty(10000, numpy.float)
     my_array = numpy.fromiter(...)
    

    第二个赋值替换第一个。 my_array 最初引用的对象是空闲的并被垃圾回收。这只是基本的 Python 变量处理。要保留原始数组(可变对象),您必须更改其值,

    my_array[:] = <new values>
    

    但是生成&lt;new values&gt; 的进程很可能会创建一个临时缓冲区(或两个或三个)。然后将这些值复制到目标。甚至x += 1 也会进行缓冲计算。很少有就地 numpy 操作。

    通常尝试再次猜测 numpy 的内存分配是行不通的。效率只能通过时间测试来衡量,而不是通过猜测幕后发生的事情来衡量。

    除非您需要迭代地填充它,否则不要打扰“预分配”:

    In [284]: my_array = np.empty(10, int)
    In [285]: for i in range(my_array.shape[0]):
         ...:     my_array[i] = 2*i+3
    In [286]: my_array
    Out[286]: array([ 3,  5,  7,  9, 11, 13, 15, 17, 19, 21])
    

    与以下相比,这是一种可怕的创建数组的方式:

    In [288]: np.arange(10)*2+3
    Out[288]: array([ 3,  5,  7,  9, 11, 13, 15, 17, 19, 21])
    

    fromiter 方法更漂亮但不是更快。

    In [290]: np.fromiter((i*2+3 for i in range(10)),int)
    Out[290]: array([ 3,  5,  7,  9, 11, 13, 15, 17, 19, 21])
    

    一些时间安排:

    In [292]: timeit np.fromiter((i*2+3 for i in range(10000)),int)
    100 loops, best of 3: 4.76 ms per loop
    # giving a count drops the time to 4.28 ms
    
    In [293]: timeit np.arange(10000)*2+3
    The slowest run took 8.73 times longer than the fastest. This could mean that an intermediate result is being cached.
    10000 loops, best of 3: 47.4 µs per loop
    
    In [294]: %%timeit 
         ...: my_array=np.empty(10000,int)
         ...: for i in range(my_array.shape[0]):
         ...:     my_array[i] = 2*i+3
         ...:     
    100 loops, best of 3: 4.72 ms per loop
    
    In [303]: timeit np.array([i*2+3 for i in range(10000)],int)
    100 loops, best of 3: 4.48 ms per loop
    

    fromiter 与显式循环一样长,而纯 numpy 解决方案的速度要快几个数量级。在时间上,np.array 与列表理解和 fromiter 与生成器之间几乎没有区别。

    从预先存在的列表创建数组大约需要 1/3 的时间。

    In [311]: %%timeit alist=[i*2+3 for i in range(10000)]
         ...: x=np.array(alist, int)
         ...: 
    1000 loops, best of 3: 1.63 ms per loop
    

    将列表分配给现有的 empty 数组并不快。

    In [315]: %%timeit alist=[i*2+3 for i in range(10000)]
         ...: arr = np.empty(10000,int)
         ...: arr[:] = alist
    1000 loops, best of 3: 1.65 ms per loop
    In [316]: %%timeit alist=[i*2+3 for i in range(10000)]; arr=np.empty(10000,int)
         ...: arr[:] = alist
    1000 loops, best of 3: 1.63 ms per loop
    

    有些numpy 函数采用out 参数。您可以通过这种方式重用数组来节省一些时间。 np.cross 是一个利用这一点的函数(代码是 Python 并且可读)。

    另一种从标量函数创建值的“矢量化”方式:

    In [310]: %%timeit f=np.frompyfunc(lambda i: i*2+3,1,1)
         ...: f(range(10000))
         ...: 
    100 loops, best of 3: 8.31 ms per loop
    

    【讨论】:

      【解决方案3】:

      np.fromiter 不做任何进一步的分配。它只是从可迭代对象中创建一个数组。这就是该功能的全部本质。它还接受 count 参数,该参数允许 fromiter 预先分配输出数组,而不是按需调整其大小。

      此外,如果您想一次更改所有项目,则无需使用np.empty

      毕竟,如果您通过对一系列数字进行一些数学运算来构建新数组,您也可以简单地对 Numpy 数组进行运算。

      这是一个例子:

      In [4]: a = np.arange(10)
      
      In [6]: a**2 + 10
      Out[6]: array([10, 11, 14, 19, 26, 35, 46, 59, 74, 91])
      

      【讨论】:

      • np.fromiter 进行新分配,它分配它返回的数组。 OP 已经明确表示他想多次设置同一个数组的元素(在开始时分配一次),每次内容都来自迭代器。
      • @user4815162342 通过分配,我的意思是它只是从迭代器创建新数组,而不是将项目缓存在内存中并将它们转换为 numpy 数组。它得到一个可迭代对象并将其转换为一个 numpy 数组。
      • 我不确定您的建议是否准确。我试过这段代码import numpy as np iterable1 = (x*x for x in range(5)) iterable2 = (x*x + 1.0 for x in range(5)) my_array = np.fromiter(iterable1, np.float) print(my_array) print(hex(id(my_array))) my_array = np.fromiter(iterable2, np.float) print(my_array) print(hex(id(my_array))),看起来my_array的地址变了。
      • 奥普斯。注释中的 Python 会丢失对齐方式。
      • @onekeystrokeatatime 正如我所说,formiter() 直接从一个可迭代对象创建一个 numpy 数组,如果您不希望这样做,您应该直接在您的数组上执行操作。
      猜你喜欢
      • 2021-03-07
      • 2015-06-12
      • 1970-01-01
      • 2021-02-18
      • 2015-04-29
      • 2017-03-28
      • 2014-10-04
      • 2014-01-02
      • 1970-01-01
      相关资源
      最近更新 更多