理解Numpy多维数组索引

3
请问,有人能解释一下这三种索引操作之间的区别吗:
- 索引(indexing) - 切片(slicing) - 迭代(iteration)
y = np.arange(35).reshape(5,7)

# Operation 1
y[np.array([0,2,4]),1:3]
# Operation 2
y[np.array([0,2,4]), np.array([[1,2]])]
# Operation 3
y[np.array([0,2,4]), np.array([[1],[2]])]

我不明白的是:
- 为什么操作2无法正常工作,而操作1可以正常工作? - 为什么操作3可以正常工作,但返回了我期望的转置结果(即操作1的结果)?
根据numpy参考文献:
如果索引数组的形状不相同,则会尝试将它们广播到相同的形状。如果它们不能广播到相同的形状,则会引发异常。
所以这意味着我不能做:
y[np.array([0,2,4]), np.array([1,2])]

但是NumPy参考文档还提到了操作1:

实际上,切片会被转换为一个索引数组 np.array([[1,2]])(形状为(1,2)),然后与索引数组进行广播,以产生形状为(3,2)的结果数组。

那么为什么我不能这样做:

y[np.array([0,2,4]), np.array([[1,2]])]

我遇到了这个错误:
IndexError: 形状不匹配:无法将形状为 (3,) 和 (1,2) 的数组广播在一起进行索引。
1个回答

3
In [1]: import numpy as np; y = np.arange(35).reshape(5,7)

操作 1

In [2]: y[np.array([0,2,4]), 1:3]
Out[2]: 
array([[ 1,  2],
       [15, 16],
       [29, 30]])

这里使用了高级索引(使用数组)和基本索引(使用切片)混合的方式,并且只使用了一次高级索引。根据参考文献,

单个高级索引可以替代一个切片,而结果数组将是相同的 [...]

这是正确的,如下代码所示:

In [3]: y[::2, 1:3]
Out[3]: 
array([[ 1,  2],
       [15, 16],
       [29, 30]])

唯一的区别在于Out[2]Out[3]之间,前者是y数据的副本(高级索引总是生成副本),而后者是与y共享内存的视图(基本索引只生成视图)。
因此,通过操作1,我们选择了通过np.array([0,2,4])选择了行,通过1:3选择了列。

操作2

In [4]: y[np.array([0,2,4]), np.array([[1,2]])]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-4-bf9ee1361144> in <module>()
----> 1 y[np.array([0,2,4]), np.array([[1,2]])]

IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (1,2) 

这是一个失败的例子,要理解为什么,我们首先必须意识到此示例中索引的本质与操作1根本不同。现在我们只有高级索引(而且不止一个高级索引)。这意味着索引数组必须具有相同的形状,或至少是广播兼容的形状。让我们看一下这些形状。
In [5]: np.array([0,2,4]).shape
Out[5]: (3,)
In [6]: np.array([[1,2]]).shape
Out[6]: (1, 2)

这意味着广播机制将尝试合并这两个数组:
np.array([0,2,4])  (1d array):     3
np.array([[1,2]])  (2d array): 1 x 2
Result             (2d array): 1 x F

最后一行的 F 表示这些形状不兼容。这就是操作2中出现 IndexError 的原因。

操作3

In [7]: y[np.array([0,2,4]), np.array([[1],[2]])]
Out[7]: 
array([[ 1, 15, 29],
       [ 2, 16, 30]])

再次提醒,我们只有高级索引功能。现在让我们看看形状是否兼容:

In [8]: np.array([0,2,4]).shape
Out[8]: (3,)
In [9]: np.array([[1],[2]]).shape
Out[9]: (2, 1)

这意味着广播将按照以下方式工作:
np.array([0,2,4])     (1d array):     3
np.array([[1],[2]])   (2d array): 2 x 1
Result                (2d array): 2 x 3

现在广播起作用了!由于我们的索引数组被广播到一个2x3的数组中,因此结果的形状也将是这样。这也解释了结果的形状与操作1不同的原因。
为了得到像操作1中形状为3x2的结果,我们可以这样做:
In [10]: y[np.array([[0],[2],[4]]), np.array([1, 2])]
Out[10]: 
array([[ 1,  2],
       [15, 16],
       [29, 30]])

现在广播机制的工作方式如下:
np.array([[0],[2],[4]])  (2d array): 3 x 1
np.array([1, 2])         (1d array):     2
Result                   (2d array): 3 x 2

给出一个3x2的数组。除了np.array([1, 2])以外。
In [11]: y[np.array([[0],[2],[4]]), np.array([[1, 2]])]
Out[11]: 
array([[ 1,  2],
       [15, 16],
       [29, 30]])

由于某种原因而能够工作

np.array([[0],[2],[4]])  (2d array): 3 x 1
np.array([[1, 2]])       (2d array): 1 x 2
Result                   (2d array): 3 x 2

谢谢,讲得非常清楚!所以,广播机制只允许在每个维度除了一个维度的值不为1时,才能组合nd-arrays,对吗? - Evann Courdier
谢谢您接受我的答案 :) 我不确定我是否正确理解了您的问题。 如果您想知道两个ndarrays是否兼容广播,请像我在答案中所做的那样写下它们的形状(就像在示例中一样)。 然后开始比较数字。如果两个数字相同或其中一个为1,则一切正常。如果数字不同且都不等于1,则广播将失败。 - yogabonito
我的问题确实不太清楚,但你回答了它!再次感谢!由于声望不足,我无法为您的答案点赞,但还是非常感谢 :) - Evann Courdier

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