为什么NumPy为x[[slice(None), 1, 2]]创建了视图

3
在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] - Eric
2个回答

3

来自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,可能会有所不同)来说明从原始数组中获取切片的偏移量。

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

这是有道理的,因为在切片中第一个值之前有11个8字节的整数。NumPy使用原始数组的步幅计算此偏移量,您可以在此处看到公式。
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创建视图的机制。 - Yousof

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*3*8个平面,每个平面需要经过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 的 'data' 范围内,这说明 y 是一个视图。它是一个视图,因为通过此缓冲区起始点、步幅和形状的组合,我们可以访问所有 y 的值。

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


通过更改步幅和 'data' 起始点即可创建反向视图:

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__ 检查了 base 和 _view_。但我并不理解为什么在我的问题中Numpy会创建一个视图。但是你在这里解释的对我非常有用。请还看一下我在答案中留下的评论。 - Yousof

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