【问题标题】:Why NumPy creates a view for x[[slice(None), 1, 2]]为什么 NumPy 为 x[[slice(None), 1, 2]] 创建一个视图
【发布时间】:2018-11-12 16:43:21
【问题描述】:

advanced indexing 的 NumPy 文档中提到

还要认识到x[[1, 2, 3]] 将触发高级索引,而x[[1, 2, slice(None)]] 将触发基本切片。

一个矩阵被顺序存储到内存中。我知道查看x[[1, 2, slice(None)]] 是有意义的,因为元素按顺序存储到内存中。但是为什么 Numpy 会返回 x[[1, slice(None), 2]]x[[slice(None), 1, 2]] 的视图。例如,假设

x = [[[ 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]]]

x[[1, slice(None), 2]] 返回 [11, 14, 17] 的视图,该视图未按顺序存储在内存中,x[[slice(None), 1, 2]] 返回 [5, 14, 23]

我想知道

  1. 为什么 NumPy 在这两种情况下都返回一个视图

  2. NumPy 如何处理内存寻址以创建这些视图

【问题讨论】:

  • 这是一个不好的例子 - x[[slice(None), 1, 2]] 从 1.15 起已弃用,应拼写为 x[slice(None), 1, 2]

标签: python numpy indexing numpy-ndarray numpy-slicing


【解决方案1】:

来自SciPy cookbook

创建切片视图的经验法则是,可以使用原始数组中的偏移量、步幅和计数来处理查看的元素。

当你有一个像x[[1, slice(None), 2]] 这样的索引时,你会得到一个视图,因为切片整个轴允许一定的偏移量、步幅和计数来表示原始数组的切片。

例如,x = np.arange(27).reshape(3, 3, 3).copy(),我们有:

In [79]: x_view = x[1, :, 2]  # or equivalently x[[1, slice(None), 2]]

In [80]: x_view
Out[80]: array([11, 14, 17])

In [81]: x_view.base
Out[81]: 
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]]])

然后我们可以使用numpy.byte_bounds(不是公共 API 的一部分,YMMV)来说明从原始数组中获取切片的偏移量。

In [82]: np.byte_bounds(x_view)[0] - np.byte_bounds(x_view.base)[0]
Out[82]: 88

这是有道理的,因为在切片中的第一个值 11 之前有 11 个 8 字节整数。NumPy 使用您可以see here 使用原始数组的步幅的公式计算此偏移量。

In [93]: (x.strides * np.array([1, 0, 2])).sum()
Out[93]: 88

我们切片中的步幅简单地变成x 沿我们正在切片的轴(或多个轴)的步幅。即x.strides[1] == x_view.strides[0]。现在,偏移量、新步幅和计数一起足以让 NumPy 从原始数组中查看我们的切片。

In [94]: x_view.strides
Out[94]: (24,)

In [95]: x_view.size
Out[95]: 3

最后,您使用 x[[0, 1, 2]] 触发花哨索引的原因是因为在没有全轴切片的情况下,通常不可能制定一些新的偏移量、字节顺序、步幅和计数,以便我们可以查看具有相同基础数据的切片。

【讨论】:

  • 感谢您的详尽解释。我的观点是,一旦第一个元素的内存引用地址已知(偏移量),其余部分将增量检索(一个接一个而不是跨步)。例如x_view = [11, 14, 17],我的视图概念不再有效,因为 11、14 和 17 没有增量存储在内存中。但是您的回答清楚地解释了 Numpy 创建视图的机制。
【解决方案2】:

我喜欢使用__array_interface__ 来检查数组的属性:

与您的x

In [51]: x.__array_interface__
Out[51]: 
{'data': (43241792, False),
 'strides': None,
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3, 3, 3),
 'version': 3}
In [52]: x.strides
Out[52]: (72, 24, 8)

这是一个 (3,3,3) 数组。最后一个轴可以一次步进8个字节扫描,大小为x.itemsize。 3*8 步行,3*3*8 步通过平面(第一个暗淡)。

In [53]: y = x[:,1,2]
In [54]: y.shape
Out[54]: (3,)
In [55]: y.strides
Out[55]: (72,)
In [56]: y.__array_interface__['data']
Out[56]: (43241832, False)

y 元素可以通过平面步进来寻址,3*3*8。 43241832为起点,40字节入数据缓冲区,5*8

In [59]: y
Out[59]: array([ 5, 14, 23])

所以它从第 5 个元素开始,一次向前走一个平面(9 个元素),总共 3 个元素。

y.__array_interface__['data']x '数据' 范围内的事实告诉我y 是一个视图。这是一个视图,因为这个缓冲区起点、步幅和形状的组合让我们可以访问y 的所有值。

使用高级索引,(通常)无法使用这些简单参数访问元素,因此numpy 必须制作数据的副本。


只需更改步幅和“数据”起点即可反转视图:

In [60]: z = y[::-1]
In [61]: z.__array_interface__
Out[61]: 
{'data': (43241976, False),
 'strides': (-72,),
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3,),
 'version': 3}

Transpose 也改变了步幅:

In [62]: x.T.strides
Out[62]: (8, 24, 72)

【讨论】:

  • 感谢您的提示。实际上,我自己早些时候检查了__array_interface__baseview。我只是不明白为什么 Numpy 甚至会在我的问题的情况下创建一个视图。但是你在这里解释的我觉得非常有用。也请看看我在答案中留下的评论
猜你喜欢
  • 2019-09-11
  • 2021-04-01
  • 1970-01-01
  • 2020-11-19
  • 2020-03-19
  • 1970-01-01
  • 2016-03-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多