说明
本文主要参考了 Numpy快速上手指南 — 基础篇 和 Numpy快速上手指南 — 进阶篇,并且带有一些主观的思考与解释,仅用于帮助理解,因此不保证结论的准确性,也欢迎您指出错误。
Numpy 的轴与秩
Numpy 的轴可以认为是维度,结合 C 语言中的多维数组的概念,或者结合数学中坐标轴的概念,都可以帮助理解“轴”的含义。轴的个数叫做秩,或者说是维数,秩的大小可以理解为描述元素位置所需要的参数个数。例如:
array([[1, 2, 3],
[4, 5, 6]])
元素5位于第二个一维数组的第二个位置,所以我们可以使用(1, 1)来描述它,因此该数组有两个轴,秩为 2。同时我们可以依据寻找元素5的过程来确定0 轴和1 轴——我们首先确定元素位于哪个一维数组中,因此0 轴就是“垂直向下”的方向;进而确定元素在一维数组中的具体位置,因此1 轴就是“水平向右”的方向。
之所以详细介绍0 轴和1 轴,主要是为了便于理解 获取最大最小值、求和 等操作。举例来讲,我们有一个2*3的矩阵,想要得到每一列的最大值,那么就是沿着纵轴来不断比较得到最大值,存放在长度为 3 的一维数组中,所以我们可以通过设置axis=0来实现:
import numpy as np
arr = np.array([[1, 2, 3], [4, 5, 6]])
np.max(arr, axis=0) # array([4, 5, 6])
同理,我们要得到各行的最大值,可以设置axis=1。
如果不清楚数组的秩/形状,可以调用数组的ndim或shape属性来得到答案:
arr.ndim # 2
arr.shape # (2, 3)
以上仅是介绍了秩为 2 的数组,更高维的情况是类似的。譬如秩为 3 的数组arr2,获取其每一行的最大值,这里每一行其实是2 轴的方向,所以设置axis=2:
arr2 = np.arange(24).reshape(2, 3, 4)
'''
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]]])
'''
np.max(arr2, axis=2)
'''
array([[ 3, 7, 11],
[15, 19, 23]])
'''
了解各轴的含义对于后续的学习很有帮助,如广播机制。前文对于各轴的描述其实也是从打印的效果来给出的,观察上述例子,也可以得到以下打印规则(引自 Numpy快速上手指南 — 基础篇 ):
打印一个数组,NumPy以类似嵌套列表的形式显示它,但是呈以下布局:
- 最后的轴从左到右打印;
- 次后的轴从顶向下打印;
- 剩下的轴从顶向下打印,每个切片通过一个空行与下一个隔开;
- 一维数组被打印成行,二维数组成矩阵,三维数组成矩阵列表。
索引
Numpy 的索引机制与 Matlab 颇为相似,可以使用整数数组来作为索引得到多个想要的元素,非常灵活。
注意: 下面的介绍暂不考虑切片的情况,仅仅考虑索引。
单数值索引
我们从简单的开始介绍,沿用arr2,我们获取元素20,其位置可以用(1, 2, 0)来唯一对应,具体实现也是如此:
arr2[1, 2, 0] # 20
arr2[(1, 2, 0)] # 20
可以看到,我们传入元组索引或者多个整数索引都可以得到元素值,但如果我们传入多个列表,情况就有所不同:
arr2[[1], [2], [0]] # array([20])
由于没有查询源码,这里推测是使用列表(或数组)时,是可以检索多个元素的,那么返回的查询结果也是多个元素组成的数组。仅检索一个元素是特殊情况,检索两个元素时就比较明晰了:
# 检索(0, 1, 2), (1, 2, 3)两个位置的元素
arr2[[0, 1], [1, 2], [2, 3]] # array([ 6, 23])
这里传入的各列表需要同等维度或者符合传播机制的规则,数组索引后续会详细介绍,这里只是为了区分几种不同的传参方式。
数组索引
数组索引可以同时检索多个元素,假设需要在秩为 d 的数组中检索 n 个元素 {e1, e2, ···, en},其中 ei 的索引为 (ei1, ei2, ···, eid),那么调用方式即为 arr_name[arg1, arg2, ···, argd],其中 argj = [e1j, e2j, ···, enj]。简单来说,就是要提供每一维度上的索引数组,里面存放着各元素在该维度上的索引值。前文中有对该方式有举例,这里也给出一个较为简单的例子:
arr3 = np.arange(10) * 2 # array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
arr3[[0, 1, 1, 6]] # array([ 0, 2, 2, 12])
值得注意的是,这里并不一定传入一维数组,也可以传入高维数组索引,返回结果与传入数组的shape相同:
idx = np.array([[1, 6], [5, 8]])
arr3[idx]
'''
array([[ 2, 12],
[10, 16]])
'''
布尔索引
通过布尔数组可以实现对元素的筛选,True表示选中,False表示未选中。举个简单的例子,假如我们要筛选arr3中大于 5 的元素,我们可以直接把筛选条件写进去:
arr3[arr3 > 5] # array([ 6, 8, 10, 12, 14, 16, 18])
这个过程其实可以看成两个步骤,先由 arr3 > 5得到由各元素对应的 bool 值组成的 bool 数组,然后再根据数组索引得到 bool 值为True的元素:
arr3 > 5 # array([False, False, False, True, True, True, True, True, True, True])
arr3[np.array([False, False, False, True, True, True, True, True, True, True])]
# array([ 6, 8, 10, 12, 14, 16, 18]), 与 arr3[arr3 > 5] 结果相同
从这个例子可以看出 bool 数组较好的筛选作用,只需要给出筛选条件就可以得到我们想要的元素,而不是给出具体的索引值。
布尔数组的形状是需要和待检索数组相匹配的,上面的例子是一维数组,传入的 bool 数组对轴上的每一个元素都进行了筛选;对于高维数组,我们自然可以传入和其shape相同的 bool 数组来筛选每一个元素。但是,也可以针对每个维度给出一个 bool 数组,比较类似前面介绍的数组索引。简单来说,检索思路就是通过查看各个维度上第一个True的位置,来确定第一个元素的位置;进而得到所有的待检索元素。所以每一个维度中True的个数要相同。举个例子会更加清晰一点:
arr4 = np.arange(12).reshape(3, 4)
'''
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
'''
arr4[[True, False, True], [False, True, True, False]] # array([ 1, 10])
小结
前面的索引方式,对于每一维都有限制;但实际上,我们可能对某一维并没有限制条件,只要在其它维上的条件满足即可,那么一个最普适的思路就是,我们对某一维不做任何代码上的限制:
arr4[[True, False, True]]
'''
array([[ 0, 1, 2, 3],
[ 8, 9, 10, 11]])
'''
可以看到,我们只限制了第一维(第一维的下标需要是0或2),最后得到所有第一维下标是0或2的元素。换个角度来看这一过程,考虑到第二维上没有任何限制,我们可以沿着arr4的第一维切成 3 份,那么每一份上的元素就是“等价”的,因为第一维是相同的,而第二维又没有做进一步的限制。这样一来,由于限制条件是[True, False, True]
,所以把第 1 份和第 3 份拿来就好了,而这也正是最终的结果。虽然这里的描述很是拖沓,但也是我对“切片”的最为直观的理解了。
切片
未完待续