首先,在Numpy中,维数(dimensions)通过轴(axes)来扩展,轴的个数被称作rank。这里的rank不是线性代数中的rank(秩),它指代的依旧是维数(number of dimensions):
The number of axes (dimensions) of the array. In the Python world, the number of dimensions is referred to as rank.
在数学或者物理的概念中,dimensions被认为是在空间中表示一个点所需要的最少坐标个数,但是在Numpy中,dimensions指代的是axes。也就是说,axes、dimensions、rank这几个概念是相通的。
下面举例子,比如我们在3-D空间中定义三个点,用array的形式表达:
>>> import numpy as np
>>> a = np.array([[1,2,3],[2,3,4],[3,4,9]])
>>> a
array([[1, 2, 3],
[2, 3, 4],
[3, 4, 9]])
按照以往的理解,我们可能会认为这个array是三维的,但实际上在Numpy中,这个array的dimensions,也就是rank的值是等于2的:
>>> np.ndim(a)
2
因为它只有两个axis,只不过这两个axis的长度(length)均为3:
>>> np.shape(a)
(3, 3)
同时,这个矩阵在线性代数中的rank(秩)依旧是3(这里要加以区分):
>>> np.linalg.matrix_rank(a)
3
也就是说,在Numpy中,任何vector都被看作具有不同长度(length)的一维array。
下面继续聊聊axis/axes,我们依旧先定义一个3-D的array:
>>> b = np.array([[[1,2,3,4],[1,3,4,5]],[[2,4,7,5],[8,4,3,5]],[[2,5,7,3],[1,5,3,7]]])
>>> b
array([[[1, 2, 3, 4],
[1, 3, 4, 5]],
[[2, 4, 7, 5],
[8, 4, 3, 5]],
[[2, 5, 7, 3],
[1, 5, 3, 7]]])
>>> b.shape
(3, 2, 4)
我们定义了一个shape为(3, 2, 4)的array,这个shape(用tuple表示)可以理解为在每个轴(axis)上的size,也即占有的长度(length),三个值的含义:axis = 0对应轴上的元素length = 3,axis = 1对应轴上的元素length = 2,axis = 2对应轴上的元素length = 4,也就是说,当我们在array中选定某个元素时,可能用到的最大索引(index)范围,举例:
>>> b[1]
array([[2, 4, 7, 5],
[8, 4, 3, 5]])
>>> b[1,:,:] # Equivalent value
array([[2, 4, 7, 5],
[8, 4, 3, 5]])
这里的b[1]和b[1, :, :]表示在axis = 0的轴上选取第一个元素,同时选取axis = 1和axis = 2上的全部元素。我们可以暂时把多个axes想象成多层layers,当然也可以想象成树结构,虽然这么说并不准确,但是为了方便想象可以暂时这么理解:
axis = 0表示第一层layer:在代码中看到的效果就是b从外向里数第一层 [ ],对应的元素(length = 3)就是第一层 [ ] 中用逗号分隔的全部元素(注意:不要关注第二层及以上 [ ] 里的逗号),很明显这一层的元素数目为3;同理,axis = 1表示第二层layer:第一层layer中用逗号分隔开的任意一个 [ ] 中,再由逗号分隔开的全部元素(length = 2);axis = 2同理,表示最后一层layer中的元素(length = 4)。这么说还是有些乱,我把刚才的array重新布置一下:
像这样用猿们写c/c++代码时候的风格排布下,黑色代表第一层layer,可以看到有三个元素,红色代表第二层layer,在第一层layer相同的前提下有两个元素,蓝色代表第三层layer,在第一层和第二层layer都相同的前提下有4个元素。这样也就对应上了b.shape = (3, 2, 4)的结果。
这时候如果我们想定位到元素8应该怎么索引?可以看到8在第一层layer的第二个元素内,同时在第二层layer的第二个元素内、第三层layer的第一个元素,也就是说:8是axis = 0轴上的第二个位置、axis = 1轴上的第二个位置,axis = 2的第一个位置共同retrieve到的元素。写成代码:
>>> b[1,1,0]
8
噔噔噔~就酱。同时也可以发现,array有几维,我们就可以用几个integer去索引到(index)它的元素。
进一步看下,对于b这个array来说,最后一个元素对应的位置就是可以用到的最大index(从0开始):
>>> b[2,1,3]
7
任一axis上再大一点就会出现IndexError了:
>>> b[2,1,4]
Traceback (most recent call last):
File "<pyshell#38>", line 1, in <module>
b[2,1,4]
IndexError: index 4 is out of bounds for axis 2 with size 4
之所以要设置不同的axis,是因为对于数据我们可以进行不同维度的处理。例如现在我们收集了四个同学对苹果、榴莲、西瓜这三种水果的喜爱程度进行打分的数据(总分为10),每个同学都有三个特征:
>>> item = np.array([[1,4,8],[2,3,5],[2,5,1],[1,10,7]])
>>> item
array([[1, 4, 8],
[2, 3, 5],
[2, 5, 1],
[1, 10, 7]])
每一行包含了同一个人的三个特征,如果我们想看看哪个同学最喜欢吃水果,那就可以用:
>>> item.sum(axis = 1)
array([13, 10, 8, 18])
可以大概看出来同学4最喜欢吃水果。
如果我们想看看哪种水果最受欢迎,那就可以用:
>>> item.sum(axis = 0)
array([ 6, 22, 21])
可以看出基本是榴莲最受欢迎~~~耶~~~咳、这就是axis存在的意义- -、为了方便地从不同角度对数据进行处理。
接下来我们将对更高维度的array进行计算,会用到提供axis这个parameter的一些函数对轴这个概念进一步说明:
依旧是求和:ndarray.sum(axis=None, dtype=None, out=None, keepdims=False),等价于numpy.sum(a, axis=None, dtype=None, out=None, keepdims=False):
>>> b.sum()
94
>>> b.sum(axis = 0)
array([[ 5, 11, 17, 12],
[10, 12, 10, 17]])
>>> b.sum(axis = 1)
array([[ 2, 5, 7, 9],
[10, 8, 10, 10],
[ 3, 10, 10, 10]])
>>> b.sum(axis = 2)
array([[10, 13],
[18, 20],
[17, 16]])
不设置axis的时候,sum返回所有元素的加和;当指定axis时,意味着对某一轴内的所有元素对应求和,同时这一层layer就collapsed掉了,因为这个过程是一个reduction operation。例如求b.sum(axis = 1)时,我们还是回到这个图:
axis = 1,直观上看,意味着我们需要关注红色layer之间的元素,顺次对axis = 0、axis = 2这两个轴上相同位置的不同红框之间的元素求和(好拗口),也就是下图中连线的元素对应求和:
可以得到(非规范写法):
array([
[ 2, 5, 7, 9],
[10, 8, 10, 10],
[ 3, 10, 10, 10]
])
也就是说,假设求和后的array为b_sum,就有:
b_sum[0, 0] = b[0, 0, 0]+b[0, 1, 0]; b_sum[0, 1] = b[0, 0, 1]+b[0, 1, 1]; b_sum[0, 2] = b[0, 0, 2]+b[0, 1, 2]; b_sum[0, 3] = b[0, 0, 3]+b[0, 1, 3];
b_sum[1, 0] = b[1, 0, 0]+b[1, 1, 0]; b_sum[1, 1] = b[1, 0, 1]+b[1, 1, 1]; b_sum[1, 2] = b[1, 0, 2]+b[1, 1, 2]; b_sum[1, 3] = b[1, 0, 3]+b[1, 1, 3];
b_sum[2, 0] = b[2, 0, 0]+b[2, 1, 0]; b_sum[2, 1] = b[2, 0, 1]+b[2, 1, 1]; b_sum[2, 2] = b[2, 0, 2]+b[2, 1, 2]; b_sum[2, 3] = b[2, 0, 3]+b[2, 1, 3];
即在axis = 0、axis = 2相同的情况下,对axis = 1上的全部元素进行求和所得的结果。
同时,在对这个轴上的元素求和之后,这个axis就消失了,我们可以再检查一下求和后的shape:
>>> b.sum(axis = 1).shape
(3, 4)
可以看到,对axis = 1上的元素进行求和后的shape由原来的(3, 2, 4)坍塌到(3, 4),其他轴可以自行验证。
接下来再看排序函数:ndarray.sort(axis=-1, kind='quicksort', order=None),也可以用numpy.sort(a, axis=-1, kind='quicksort', order=None),但是前者会改变b后者不会改变b:
>>> np.sort(b)
array([[[1, 2, 3, 4],
[1, 3, 4, 5]],
[[2, 4, 5, 7],
[3, 4, 5, 8]],
[[2, 3, 5, 7],
[1, 3, 5, 7]]])
>>> np.sort(b, axis = 0)
array([[[1, 2, 3, 3],
[1, 3, 3, 5]],
[[2, 4, 7, 4],
[1, 4, 3, 5]],
[[2, 5, 7, 5],
[8, 5, 4, 7]]])
>>> np.sort(b, axis = 1)
array([[[1, 2, 3, 4],
[1, 3, 4, 5]],
[[2, 4, 3, 5],
[8, 4, 7, 5]],
[[1, 5, 3, 3],
[2, 5, 7, 7]]])
第一种情况,sort默认的axis值为-1,对于我们这个case来说,相当于对axis = 2轴上的元素进行排序(也就是倒数第一个轴)。
还是拿其中一种情况举例子,对于axis = 0,先直观来看,我们需要关注黑色框之间对应的元素:
我只标注出来了在轴axis = 0上进行排序的过程中,第一组元素之间和最后一组元素之间的对应关系,其他组元素同理。
从索引的角度也可以表示:
对b[0, 0, 0]、b[1, 0, 0]、b[2, 0, 0]进行排序,并将结果由小到大重新排列;
对b[0, 0, 1]、b[1, 0, 1]、b[2, 0, 1]进行排序,并将结果由小到大重新排列;
对b[0, 0, 2]、b[1, 0, 2]、b[2, 0, 2]进行排序,并将结果由小到大重新排列;
对b[0, 0, 3]、b[1, 0, 3]、b[2, 0, 3]进行排序,并将结果由小到大重新排列;
对b[0, 1, 0]、b[1, 1, 0]、b[2, 1, 0]进行排序,并将结果由小到大重新排列;
其余同理……
嗯就先举这两个例子吧,其他带有参数axis的函数也可以从这两个角度去分析。