为什么 NumPy 为 x[[slice(None), 1, 2]] 创建视图
Why NumPy creates a view for x[[slice(None), 1, 2]]
在 advanced indexing 的 NumPy 文档中提到
Also recognize that x[[1, 2, 3]]
will trigger advanced indexing, whereas x[[1, 2, slice(None)]]
will trigger basic slicing.
一个矩阵按顺序存入内存。我知道查看 x[[1, 2, slice(None)]]
是有意义的,因为元素是按顺序存储在内存中的。但是为什么 Numpy returns 是 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]]
returns [11, 14, 17]
的视图未按顺序存储在内存中以及 x[[slice(None), 1, 2]]
returns [5, 14, 23]
.
我想知道
为什么 NumPy 在这两种情况下甚至 returns 一个视图
NumPy 如何处理内存寻址以创建这些视图
The rule of thumb for creating a slice view is that the viewed elements can be addressed with offsets, strides, and counts in the original array.
当你有像 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
(不是public 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]]
触发奇特索引的原因是因为在没有全轴切片的情况下,通常不可能制定一些新的偏移量、字节顺序、步幅和count 这样我们就可以查看具有相同基础数据的切片。
我喜欢用__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
'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)
在 advanced indexing 的 NumPy 文档中提到
Also recognize that
x[[1, 2, 3]]
will trigger advanced indexing, whereasx[[1, 2, slice(None)]]
will trigger basic slicing.
一个矩阵按顺序存入内存。我知道查看 x[[1, 2, slice(None)]]
是有意义的,因为元素是按顺序存储在内存中的。但是为什么 Numpy returns 是 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]]
returns [11, 14, 17]
的视图未按顺序存储在内存中以及 x[[slice(None), 1, 2]]
returns [5, 14, 23]
.
我想知道
为什么 NumPy 在这两种情况下甚至 returns 一个视图
NumPy 如何处理内存寻址以创建这些视图
The rule of thumb for creating a slice view is that the viewed elements can be addressed with offsets, strides, and counts in the original array.
当你有像 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
(不是public 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]]
触发奇特索引的原因是因为在没有全轴切片的情况下,通常不可能制定一些新的偏移量、字节顺序、步幅和count 这样我们就可以查看具有相同基础数据的切片。
我喜欢用__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
'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)