对于使用Tensorflow的朋友们,想必都熟悉张量(tensor)这一概念。此外,numpy库中还存在一个类似的概念ndarray。本文记录和整理了ndarray和tensor之间,关于轴(axis)和形状(shape)之间的一些异同比较。考虑到大多数读者对数组的图形解释比较熟悉和习惯,(例如C/C++和python的数组),本文结合《图解深度学习与神经网络:从张量到TensorFlow实现》一书中的tensor图解,对数组与tensor进行较为详细的比较和总结。
ndarray的存储/读取逻辑
首先我们来讲一下ndarray对于数组的存储/读取的逻辑,这一点对实际使用并无影响。计算机怎么存就会怎么读,编程人员获取的结果都是一样的,但有助于理解后续的维度和轴。
ndarray对于数组的存储方式有两类,分别是C/C++语言模式(即行优先)和Fortran语言模式(即列优先),分别用order属性的’C’ 和’F’表示,默认以C语言模式存储。
下面以C模式为例:
如果我们想要创建一个多维数组,一般可以采用嵌套的数组表示,如[[[1, 2],[3,5],[2,4]],[[2,4],[4,6],[2,9]],[[3,5],[1,3],[34,12]]],这是一个三维数组。根据行优先的存储模式,可以知道,计算机内存存储时的实际顺序便是上方数组的书写顺序。三维数组包含列、行、页三个维度。计算机存储三维数组的过程便是先存列,再存行,最后存页。等等,不是说好的行优先吗,为什么说先存列。其实行优先只是便于理解的一种描述,即存完一行再存下一行,而不是存完一列再存下一列。实际上从数组角度来看,存放时先放的是第0行第0列,然后是第0行第1列,第0行第2列,第0行存放完才轮到第1行第0列,以此类推。可以发现先发生改变的或者说先出现维度概念的是列,之后才是行。
当你熟悉了ndarray对象的使用后,你就可以完全摒弃到行列这种容易混淆理解的说法,而是以轴(axis)来进行理解和表达,因为对于高维数组是不存在真正的行和列的,只有轴(axis)是始终有意义的表达方式。当然为了便于初学者理解,下文仍使用行、列的描述,以展现它们和轴的关系。
ndarray和tensor
1. ndarray
对于numpy中的多维数组,即ndarray,遵循上文的默认存储/读取顺序,随着维度的增高,数组将逐一产生列、行、页等维度。需要指出,列、行和页只是方便理解的通俗表述,严谨来说应该表述为第几维度。
为了方便理解和比较,对于不同维度,选择了各不相同的长度。
下面先给出不同维数数组的shape、axis和维度情况:
一维数组,例如5列的一维数组:
[15 6 19 1 18]
该数组的shape=(5, ),分别对应列维度,也对应于axis=0。
注意向量陷阱,严格来说一维数组并非向量,向量必须为二维数组。因为一维数组缺少行维度,在部分操作时会产生错误的结果,例如:
a = np.random.randn(5) #shape=(5,)
np.dot(a.T,a) # 得到常数
b = np.random.randn(1,5) #shape=(1,5)
np.dot(b.T,b) # 得到5行5列的二维数组
二维数组,例如4行5列的二维数组:
[[15 6 19 1 18]
[18 0 16 6 7]
[ 2 17 5 15 19]
[15 17 4 18 3]]
shape=(4,5),分别对应行和列维度,也对应于axis=0,1。
三维数组
三维数组可以看作由多个二维数组构成,例如3页4行5列的三维数组:
[[[15 6 19 1 18]
[18 0 16 6 7]
[ 2 17 5 15 19]
[15 17 4 18 3]]
[[12 19 9 10 5]
[14 16 1 12 17]
[ 3 0 18 15 18]
[15 3 4 8 4]]
[[14 12 11 10 7]
[11 11 1 5 12]
[10 13 19 1 17]
[ 7 10 11 0 8]]]
shape=(3,4,5),分别对应页、行和列三个维度,也对应于axis=0,1,2。
高维数组
从四维数组开始,新产生的维度没有统一的简洁称呼,一般称为第几维度。或者直接采用轴(axis)进行描述。同理,四维数组可看作由多个三维数组组成。例如,由2个3页4行5列三维数组构成的四维数组:
[[[[15 6 19 1 18]
[18 0 16 6 7]
[ 2 17 5 15 19]
[15 17 4 18 3]]
[[12 19 9 10 5]
[14 16 1 12 17]
[ 3 0 18 15 18]
[15 3 4 8 4]]
[[14 12 11 10 7]
[11 11 1 5 12]
[10 13 19 1 17]
[ 7 10 11 0 8]]]
[[[ 6 2 10 7 17]
[19 8 13 7 14]
[12 18 17 6 9]
[10 14 13 3 5]]
[[10 18 6 5 13]
[19 7 12 8 6]
[13 0 12 5 3]
[ 0 17 6 6 13]]
[[ 7 19 1 14 9]
[17 10 12 8 13]
[ 0 8 12 13 16]
[ 0 1 18 15 10]]]]
shape=(2,3,4,5),分别对应第四维度、页、行和列,也对应于axis=0,1,2,3。
★需要指出,官方关于axis和维度的对应关系与上文关于维度的表述是相反的,对于一个n维数组,其定义为:
axis = 0,表示第一个维度;axis = 1,表示第二个维度,以此类推。
而本文的表述是按维度产生的前后顺序排列的,即行, 列, 页, 第四维度, 第五维度, …, 第n维度,对应着axis=n, n-1, n-2, …, 1。
官方的定义没有纳入行、列、页的概念,为了包含行、列、页的概念,方便大家理解,本文的表述没有遵循官方的标准。
总结,在ndarray中,
(注意此处没有采用官方的维度与轴对应关系)
①新的维度总是出现在shape的最左侧
②轴(axis)与维度之间没有固定的对应关系,而是与shape相对应
(在二维数组中axis=0对应行,而三维数组中axis=0则对应页)
注:纳入行列页概念后的弊端,如采用官方的定义则不存在这一问题,轴(axis)、维度和shape始终保持对应关系。
③ndarray的图解与数组的打印格式完全一致,在对数组进行处理时这将提供很大的便利
④需要指出,即便采用Fortran语言模式(即列优先)存储,也不会改变上述轴和形状的顺序和定义
1.1轴(axis)
在实际应用中,numpy内置的许多函数能够对数组进行复杂而高效的操作,这些函数中都包含参数axis。因此,理解axis这一概念是必不可少的。
关于轴(axis)这一概念,可以类比数学中的坐标轴,下图给出了二维数组的轴、轴坐标和各元素的索引。
一般来说,我们认为对于二维数组,行(的)方向为横向,列(的)方向为纵向。在图中,0号轴(axis=0)和1号轴(axis=1)分别沿着列方向和行方向。这似乎与此前二维数组中维度与轴的对应关系相反(axis=0对应行维度、axis=1对应列维度)。
这是由于,前面说的axis=0对应行维度,实际上是对应着行维度的长度,即m行n列数组中的m,因此其必须沿行号变化的方向(即所谓的列方向)。换言之,0号轴上的坐标反映了不同的行号,反过来也对应了行维度,如下图所示。
因此,建议按照 0号轴对应行维度⇄0号轴沿行号变化的方向来理解和记忆,这样不会引起混淆。1号轴(axis=1)同理。
如果你理解了上述表述,那么高维数组都是一样的。例如三维数组,如下图所示。
按照之前的表述,在三维数组中,axis=0对应页维度,axis=1对应行维度,axis=2对应列维度,因此0号轴(axis=0)沿页号变化的方向,1号轴(axis=1)沿行号变化的方向,2号轴(axis=2)沿列号变化的方向。与下图完全对应,非常简单易懂。
总结:
n号轴对应第n维度⇄n号轴沿第n维度编号变化的方向
1.2 轴(axis)的应用
知道轴的含义和方向后,现在实战一些numpy的内置函数
求和:sum(axis)
用法:np.sum(A,axis) 或 A.sum(axis)
以下面的3页4行5列三维数组为例,axis的取值有None、0、1和2:
当参数为None时,把数组中的所有元素相加,得到一个常数;
当参数为0时,沿页号变化的方向(0号轴方向)累加,得到一个4行5列的二维数组(页维度被压缩,其他维度不变);
当参数为1时,沿行号变化的方向(1号轴方向)累加,得到一个3行5列的二维数组(行维度被压缩,页维度变为行维度)
当参数为2时,沿列号变化的方向(2号轴方向)累加,得到一个3行4列的二维数组(列维度被压缩,页维度变为行维度,行维度变为列维度)
#三维数组A
[[[15 6 19 1 18]
[18 0 16 6 7]
[ 2 17 5 15 19]
[15 17 4 18 3]]
[[12 19 9 10 5]
[14 16 1 12 17]
[ 3 0 18 15 18]
[15 3 4 8 4]]
[[14 12 11 10 7]
[11 11 1 5 12]
[10 13 19 1 17]
[ 7 10 11 0 8]]]
A.sum() = 614
A.sum(axis=0) =
[[41 37 39 21 30]
[43 27 18 23 36]
[15 30 42 31 54]
[37 30 19 26 15]]
A.sum(axis=1) =
[[50 40 44 40 47]
[44 38 32 45 44]
[42 46 42 16 44]]
A.sum(axis=2) =
[[59 47 58 57]
[55 60 54 34]
[54 40 60 36]]
求积:prod(axis)
该函数用来计算数组元素的乘积,对于多维数组可以指定轴
同样以上面的3页4行5列三维数组为例,
当参数为None时,求数组的所有元素乘积,得到一个常数;
当参数为0时,沿页号变化的方向(0号轴方向)累乘,得到一个4行5列的二维数组(页维度被压缩,其他维度不变);
当参数为1时,沿行号变化的方向(1号轴方向)累乘,得到一个3行5列的二维数组(行维度被压缩,页维度变为行维度)
当参数为2时,沿列号变化的方向(2号轴方向)累乘,得到一个3行4列的二维数组(列维度被压缩,页维度变为行维度,行维度变为列维度);
#三维数组A
A.prod() = 0
A.prod(axis=0) =
[[2520 1368 1881 100 630]
[2772 0 16 360 1428]
[ 60 0 1710 225 5814]
[1575 510 176 0 96]]
A.prod(axis=1) =
[[ 8100 0 6080 1620 7182]
[ 7560 0 648 14400 6120]
[10780 17160 2299 0 11424]]
A.prod(axis=2) =
[[ 30780 0 48450 55080]
[102600 45696 0 5760]
[129360 7260 41990 0]]
统计量
mean(axis):计算元素的均值
var(axis):计算元素的方差
std(axis) :计算元素标准差
max(axis):计算元素的最大值
min(axis):计算元素的最小值
ptp(axis):计算元素的取值范围,即最大值和最小值的差值
median(axis):计算元素的中位数
以max(axis)函数为例,结果如下
#三维数组A
A.max() = 19
A.max(axis=0) =
[[15 19 19 10 18]
[18 16 16 12 17]
[10 17 19 15 19]
[15 17 11 18 8]]
A.max(axis=1) =
[[18 17 19 18 19]
[15 19 18 15 18]
[14 13 19 10 17]]
A.max(axis=2) =
[[19 18 19 18]
[19 17 18 15]
[14 12 19 11]]
总结:
从效果上来说,axis参数可理解为“将要被消除或折叠的维度或轴”。
从计算上来说,axis参数可理解为“数组沿轴的方向进行运算”。
1.3 形状(shape)
shape的概念实际上非常简单,这里简单说一下高维数组的shape
[[[[15 6 19 1 18]
[18 0 16 6 7]
[ 2 17 5 15 19]
[15 17 4 18 3]]
[[12 19 9 10 5]
[14 16 1 12 17]
[ 3 0 18 15 18]
[15 3 4 8 4]]
[[14 12 11 10 7]
[11 11 1 5 12]
[10 13 19 1 17]
[ 7 10 11 0 8]]]
[[[ 6 2 10 7 17]
[19 8 13 7 14]
[12 18 17 6 9]
[10 14 13 3 5]]
[[10 18 6 5 13]
[19 7 12 8 6]
[13 0 12 5 3]
[ 0 17 6 6 13]]
[[ 7 19 1 14 9]
[17 10 12 8 13]
[ 0 8 12 13 16]
[ 0 1 18 15 10]]]]
对于上面这个四维数组,其shape=(2,3,4,5)。
有两种理解方法:
①按照行列页的维度去理解,即由2个 3页4行5列的三维数组构成;
②从外向内依次去括号,即最外侧(第1个)括号内包含2个“元素”:
#元素1
[[[15 6 19 1 18]
[18 0 16 6 7]
[ 2 17 5 15 19]
[15 17 4 18 3]]
[[12 19 9 10 5]
[14 16 1 12 17]
[ 3 0 18 15 18]
[15 3 4 8 4]]
[[14 12 11 10 7]
[11 11 1 5 12]
[10 13 19 1 17]
[ 7 10 11 0 8]]]
#元素2
[[[ 6 2 10 7 17]
[19 8 13 7 14]
[12 18 17 6 9]
[10 14 13 3 5]]
[[10 18 6 5 13]
[19 7 12 8 6]
[13 0 12 5 3]
[ 0 17 6 6 13]]
[[ 7 19 1 14 9]
[17 10 12 8 13]
[ 0 8 12 13 16]
[ 0 1 18 15 10]]]
次外侧(第2个)括号内包含3个“元素”:
#元素1
[[15 6 19 1 18]
[18 0 16 6 7]
[ 2 17 5 15 19]
[15 17 4 18 3]]
#元素2
[[12 19 9 10 5]
[14 16 1 12 17]
[ 3 0 18 15 18]
[15 3 4 8 4]]
#元素3
[[14 12 11 10 7]
[11 11 1 5 12]
[10 13 19 1 17]
[ 7 10 11 0 8]]]
同理,第3个括号内包含4个“元素”,第4个括号内包含4个“元素”。
因此,shape=(2,3,4,5)
2. tensor
说了这么多,下面讲一讲Tensorflow中的张量(tensor),其是一种具有统一类型的多维数组。因此,tensor与ndarray具有很大的相似性。
但如果你看过或者了解过《图解深度学习与神经网络:从张量到TensorFlow实现》一书(简称《图解张量》),或看过笔者的Tensorflow中CNN的基础概念解析,则会发现张量的图解和维度与此前的ndarray存在很大的区别。
因此,先简单介绍一下tensor的图解和维度,重点解读一下二者的异同和二种表述的优劣。
2.1 tensor的图解与维度
后续以《图解张量》中图解的张量为例进行解读
一维张量
即向量,注意其没有行/列向量的区别。
实际上其仍然遵循ndarray中一维数组的shape规则,即
tensor = tf.constant([1,-2,2,1], dtype=tf.double) #shape=(4,)
tf.transpose(tensor) #转置,shape不变—— shape=(4,)
ndarray=tensor.numpy() #tensor转换为ndarray,shape=(4,)
#ndarray=[1,-2,2,1]
如下图所示
尽管一维张量没有行列的概念,但为了方便理解,姑且认为对应的维度为行维度
二维张量
即矩阵,例如
tensor = tf.constant([[-1,1,8,5]
[ 2,3,1,9]
[ 7,2,6,4]], dtype=tf.double) #shape=(3,4)
ndarray=tensor.numpy() #shape=(3,4)
其图解如下图所示,即3行4列的二维张量(矩阵),与ndarray中的二维数组基本一致。
三维张量
可以类比ndarray中的三维数组,例如
tensor = tf.constant([[[1,-11],[1,11],[8,18],[5,15]],
[[2, 12],[3,13],[1,11],[9,19]],
[[7, 17],[2,12],[6,16],[4,14]]],
dtype=tf.double) #shape=(3,4,2)
ndarray=tensor.numpy() #shape=(3,4,2)
如下图所示,即3行4列2深度的三维张量。此时,其shape与ndarray的shape一致,但维度描述和图解有很大的区别。在ndarray中,其是3页4行2列的三维数组。
换言之,在tensor中,对于三维张量,按照shape的顺序,维度依次为行、列和深度,而不是ndarray中的页、行和列。这一顺序也是维度的产生先后顺序。
四维张量
可以类比ndarray中的三维数组,例如
tensor = tf.constant([[[[1,-11],[1,11],[8,18],[5,15]],
[[2, 12],[3,13],[1,11],[9,19]],
[[7, 17],[2,12],[6,16],[4,14]]],
[[[5,-21],[1,21],[6,18],[2,25]],
[[7, 22],[5,23],[7,21],[9,19]],
[[6, 27],[3,22],[4,26],[2,14]]]],
dtype=tf.double) #shape=(2,3,4,2)
ndarray=tensor.numpy() #shape=(2,3,4,2)
如下图所示,即由2个3行4列深度为2的三维张量构成的四维张量(描述顺序与shape的分量顺序一致)。其shape与ndarray的shape一致,但维度描述和图解同样存在很大区别。在ndarray中,其是由2个3页4行2列的三维数组构成的四维数组。
从第四维度开始,新维度出现在shape的最左侧,而不是最右侧。这也是tensor图解和维度描述最容易搞错的地方。
总结:
在tensor中,
①对于三维张量,按照shape的顺序,维度依次为行、列和深度,而不是ndarray中的页、行和列
②随着张量维度的增加,依次出现的维度为行、列、深度…
③从第四维度开始,新维度出现在shape的最左侧,而不是最右侧,与ndarray一致
④shape与ndarray的shape是一致的,shape与axis的对应关系也与ndarray一致,从左至右依次为axis=0,1,2,3…
2.2 tensor的轴(axis)
正如前面所述,tensor的shape和axis都与ndarray中保持一致,因此关于轴参数的含义、运算结果都是一致的。只是由于二者图解和维度存在的区别,轴的方向也会产生差别。
以上面的三维张量为例,其各个轴的方向如下方第一张图所示
上方第二张图是上述三维张量转化为ndarray中三维数组后,其轴方向的示意图。尽管从图解上看,轴的方向完全不同,但张量的轴方向仍然可以按照ndarray中的轴进行分析,即n号轴对应第n维度⇄n号轴沿第n维度编号变化的方向。
需要注意,二者的轴方向虽然看似不一致,但实际都沿着同一个方向。例如,tensor中axis=0沿行号变化方向,ndarray中沿页号变化方向,实际上都是沿着-1→ 2→ 7的方向。
因此,tensor的运算中axis参数的作用和效果与ndarray是完全一致的,不用担心二者图解和维度描述的差异(只是人为描述的差别),axis始终与shape保持一致。
2.3 tensor图解和维度描述的用途
相信有读者疑惑,既然二者关于axis的操作完全一致,而且计算机运算时不涉及到维度的概念,为何tensor要特立独行,采用行、列和深度这种顺序的维度描述方法。
这一疑问实际上代入tensor的应用场景中,便可以得到解答。tensorflow的一个主要的应用场景就是图片和视频数据。
对于一张彩色图片,其张量的shape=(H,W,C),其中W和H是图片的宽和高,C是颜色的通道数(如RGB图像是3通道,即C=3)。
而对于视频,其张量的shape=(F,H,W,C),其中F是frames(帧数),视频的每一帧都是一个彩色图片。如果是多个视频组成的5D张量,其shape=(S,F,H,W,C),其中S是samples(视频数量)。
从视频张量每个维度的含义可以发现,其与tensor的图解、维度的概念和顺序是一致的,分别对应着第5维度、第4维度、行、列和深度。
此外,在CNN中,还存在一个多通道卷积的概念,这里通道可以指图片的颜色通道。在多通道卷积时,卷积核也是多通道的,即输入图的各个颜色通道具有不同的卷积核参数,如下图。如果你不熟悉tensorflow中CNN的概念,可以去简单了解一下笔者的Tensorflow中CNN的基础概念解析。
可见,这种维度排列顺序可以极大便利神经网络中的张量运算(部分张量运算对张量首尾维度的长度有要求)。
换言之,在ndarray中,一个二维数组就是一个完整的数据了,三维数组和更高维的数组看作为多个/组的二维数组。而在tensorflow中,一个三维张量才能构成一个完整的数据(宽度、高度、通道),因此从四维张量开始才看作是多个/组的三维张量(即,从第四维度开始,新维度出现在shape的最左侧)。
至此,本文的内容就算全部讲完了,内容比较多,笔者已经尽可能地进行整理和归纳。如果有不理解和不正确的地方,欢迎大家提问和指出。