在神经网络运算过程中,维度变换是最核心的张量操作,通过维度变换可以将数据任意地切换形式,满足不同场合的运算需求。
那么为什么需要维度变换?通过线性层的批量形式入手
Y = X@W + b
其中,假设X包含了2个样本,每个样本的特征长度为4,X的shape为[2,4]。
线性层的输出为3个节点,即W的 shape 定义为[4,3],偏置b的shape 定义为[3]。那么X@W 的运算结果张量 shape 为[2,3],需要叠加上 shape 为[3]的偏置b。不同shape的2个张量怎么直接相加?
因此,对于2个样本的输人X,需要将shape为[3]的偏置
b
=
[
b
1
b
2
b
3
]
b=\left[ \begin{matrix} {{b}_{1}} \\ {{b}_{2}} \\ {{b}_{3}} \\ \end{matrix} \right]
b=
b1b2b3
按按样本数量复制1份,变成如下矩阵形式B’:
B
′
=
[
b
1
b
2
b
3
b
1
b
2
b
3
]
{{B}^{'}}=\left[ \begin{matrix} {{b}_{1}} & {{b}_{2}} & {{b}_{3}} \\ {{b}_{1}} & {{b}_{2}} & {{b}_{3}} \\ \end{matrix} \right]
B′=[b1b1b2b2b3b3]
即可满足矩阵相加的数学条件
通过这种方式,既满足了数学上矩阵相加需要shape 一致的条件,又达到了给每个输入样本的输出节点共享偏置向量的逻辑。为了实现这种运算方式,将偏置向量b插入一个新的维度,并把它定义为 Batch维度,然后在Batch维度将数据复制1份,得到变换后的 B’,新的shape为[2,3]。这一系列的操作就是维度变换操作。
维度变换的功能:算法的每个模块对于数据张量的格式有不同的逻辑要求,当现有的数据格式不满足算法要求时,需要通过维度变换将数据调整为正确的格式。
基本的维度变换操作函数包含改变视图reshape
、插入新维度expand_dims
、删除维度squeeze
、交换维度 transpose
、复制数据 tile
等函数。
1.1 改变视图
在理解改变视图reshape操作之前,先来认识张量的存储(Storage)
和视图(View)
的概念。
张量的视图就是理解张量的方式,例如shape为[2,4,4,3]的张量A,从逻辑上可以理解为2张图片,每张图片4行4列,每个位置有RGB3个通道的数据。张量的存储体现在张量在内存上保存为一段连续的内存区域(平坦结构)
,对于同样的存储,可以有不同的理解方式,例如上述张量A,可以在不改变张量的存储下,将张量A理解为2个样本,每个样本的特征为长度48(
4
∗
4
∗
3
4*4*3
4∗4∗3)的向量。同一个存储,从不同的角度观察数据,可以产生不同的视图,这就是存储与视图的关系。
通过tf.range()
模拟生成一个向量数据,并通过tf.reshape
视图改变函数产生不同的视图,例如:
x = tf.range(96)
print(x)
x = tf.reshape(x,[2,4,4,3])
print(x)
输出结果为:
tf.Tensor(
[ 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95], shape=(96,), dtype=int32)
tf.Tensor(
[[[[ 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]
[27 28 29]
[30 31 32]
[33 34 35]]
[[36 37 38]
[39 40 41]
[42 43 44]
[45 46 47]]]
[[[48 49 50]
[51 52 53]
[54 55 56]
[57 58 59]]
[[60 61 62]
[63 64 65]
[66 67 68]
[69 70 71]]
[[72 73 74]
[75 76 77]
[78 79 80]
[81 82 83]]
[[84 85 86]
[87 88 89]
[90 91 92]
[93 94 95]]]], shape=(2, 4, 4, 3), dtype=int32)
改变x的视图,获得四维(4D)张量,存储并未改变。可以观察到数据仍然是0~95的顺序,可见数据并未改变,改变的是数据的结构。
存储数据时,内存并不支持这个维度层级概念,只能以平铺方式按序写人内存,为了方便表达,把张量shape 列表中相对靠左侧的维度称为大维度,shape列表中相对靠右侧的维度称为小维度,例如[2,4,4,3]的张量中,图片数量维度与通道数量相比,图片数量称为大维度,通道数称为小维度。
改变视图操作的默认前提是存储不需要改变,否则改变视图操作就是非法的
张量A按着初始视图[b,h,w,c]写人的内存布局,改变A的理解方式,它可以有如下多种合法的理解方式,如下:
(1)[b,
h
∗
w
h*w
h∗w,c]张量理解为b张图片,
h
∗
w
h*w
h∗w个像素点,c个通道;
(2)[b,h,
w
∗
c
w*c
w∗c]张量理解为b张图片h行,每行的特征长度为
w
∗
c
w*c
w∗c;
(3)[b,
h
∗
w
∗
c
h*w*c
h∗w∗c]张量理解为b张图片,每张图片的特征长度为
h
∗
w
∗
c
h*w*c
h∗w∗c;
不合法的情况举例如下:
[b,c,
h
∗
w
h*w
h∗w]
一种正确使用视图变换操作的技巧就是跟踪存储的维度顺序。例如根据“图片数量-行-列-通道”初始视图保存的张量,存储也是按照“图片数量-行-列-通道”的顺序写人的。如果按着“图片数量-像素-通道”的方式恢复视图,并没有与“图片数量-行-列-通道”相悖,因此能得到合法的数据。但是如果按着“图片数量-通道-像素”的方式恢复数据,由于内存布局是按着“图片数量-行-列-通道”的顺序,视图维度顺序与存储维度顺序相悖,提取的数据将是错乱的。
1.2 增加、删除维度
1.2.1 增加维度
增加维度:增加一个长度为1的维度相当于给原有的数据添加一个新维度的概念,维度长度为1,故数据并不需要改变,仅仅是改变数据的理解方式,因此它其实可以理解为改变视图的一种特殊方式。
假如一张
28
∗
28
28*28
28∗28大小的灰度图片的数据保存为shape为[28,28]的张量,在末尾给张量增加一新维度,定义为通道数维度,此时张量的shape 变为[28,28,1],实现如下:
# 生成形状为 [3, 3] 的 tf.int32 类型随机数,值在 0 到 10 之间
x = tf.random.uniform([3, 3], minval=0, maxval=10, dtype=tf.int32)
print(x)
x = tf.expand_dims(x,axis=2)
print(x)
输出结果如下:
tf.Tensor(
[[0 4 2]
[8 2 7]
[0 9 7]], shape=(3, 3), dtype=int32)
tf.Tensor(
[[[0]
[4]
[2]]
[[8]
[2]
[7]]
[[0]
[9]
[7]]], shape=(3, 3, 1), dtype=int32)
可以看到,插入一个新维度后,数据的存储顺序并没有改变,依然按顺序保存,仅仅是在插人一个新的维度后,改变了数据的视图。
tf.expand_dims
的axis
为正时,表示在当前维度之前插人一个新维度,为负时,表示当前维度之后插入一个新的维度。
1.2.1 删除维度
删除维度是增加维度的逆操作,与增加维度一样,删除维度只能删除长度为1的维度,也不会改变张量的存储。继续考虑增加维度后shape为[1,3,3,1]的例子,如果希望将图片数量维度删除,可以通过tf.squeeze(x,axis)
函数实现,axis参数为待删除的维度的索引号,例如,图片数量的维度轴axis=0:
# 生成形状为 [3, 3] 的 tf.int32 类型随机数
x = tf.random.uniform([3, 3], minval=0, maxval=10, dtype=tf.int32)
print("x.shape:", x.shape)
print(x)
# 假设你有一个额外的维度(例如,通过 tf.expand_dims 添加的),然后你可以使用 tf.squeeze
x_expanded = tf.expand_dims(x, axis=0) # 现在形状是 [1, 3, 3]
print("x_expanded.shape:", x_expanded.shape)
# 移除第一个维度(现在它是 1)
x_squeezed = tf.squeeze(x_expanded, axis=0)
print("x_squeezed.shape:", x_squeezed.shape)
print(x_squeezed)
输出结果如下:
x.shape: (3, 3)
tf.Tensor(
[[4 1 3]
[4 4 2]
[9 0 3]], shape=(3, 3), dtype=int32)
x_expanded.shape: (1, 3, 3)
x_squeezed.shape: (3, 3)
tf.Tensor(
[[4 1 3]
[4 4 2]
[9 0 3]], shape=(3, 3), dtype=int32)
如果不指定维度参数 axis,即tf.squeeze(x),那么它会默认删除所有长度为1的维度
1.3 交换维度
改变视图、增删维度都不会影响张量的存储。在实现算法逻辑时,保持维度顺序不变的条件下,仅仅改变张量的理解方式是不够的,有时需要直接调整存储顺序,即交换维度(Transpose)。
通过交换维度操作,改变了张量的存储顺序,同时也改变了张量的视图。交换维度操作是非常常见的,例如在TensorFlow中,图片张量的默认存储格式是通道后行格式为[b,h,w,c],但是部分库的图片格式是通道先行格式为[b,c,h,w],因此需要完成[b,h,w,c]到[b,c,h,w]维度交换运算,此时若简单地使用改变视图函数reshape,则新视图的存储方式需要改变,因此使用改变视图函数是不合法的。以[b,h,w,c]转换到[b,c,h,w]为例,介绍如何使用tf.transpose(x,perm)
函数完成维度交换操作,其中参数perm表示新维度的顺序List。考虑图片张量shape为[2,2,2,3],“图片数量、行、列、通道数”的维度索引分别为0、1、2、3,如果需要交换为[b,c,h,w]格式,则新维度的排序为“图片数量、通道数、行、列”,对应的索引号为[0,3,1,2],因此参数perm需设置为[0,3,1,2]。
x = tf.random.uniform([2,2,2,3], minval=0, maxval=10, dtype=tf.int32)
print(x)
x = tf.transpose(x, perm=[0,2,1,3])
print(x)
输出结果如下:
tf.Tensor(
[[[[8 2 4]
[6 0 1]]
[[6 6 7]
[4 3 9]]]
[[[2 3 7]
[7 8 4]]
[[9 8 0]
[1 4 0]]]], shape=(2, 2, 2, 3), dtype=int32)
tf.Tensor(
[[[[8 2 4]
[6 6 7]]
[[6 0 1]
[4 3 9]]]
[[[2 3 7]
[9 8 0]]
[[7 8 4]
[1 4 0]]]], shape=(2, 2, 2, 3), dtype=int32)
由输出结果可知成功将[b,h,w,c]交换为[b,w,h,c],即将高、宽维度互换,新维度索引为[0,2,1,3]。
通过tf.transpose
完成维度交换后,张量的存储顺序已经改变,视图也随之改变,后续的所有操作必须基于新的存续顺序和视图进行。相对于改变视图操作,维度交换操作的计算代价更高。
1.4 复制数据
当通过增加维度操作插入新维度后,可能希望在新的维度上面复制若干份数据,满足后续算法的格式要求。考虑Y=X@W+b的例子,偏置b插入样本数的新维度后,需要在新维度上复制 Batch Size份数据,将shape变为与X@W 一致后,才能完成张量相加运算。
可以通过 tf.tile(x,multiples)
函数完成数据在指定维度上的复制操作,multiples分别指定了每个维度上面的复制倍数,对应位置为1表明不复制,为2表明新长度为原来长度的2倍,即数据复制一份,以此类推。
以输人为[2,4],输出为3个节点线性变换层为例,偏置b定义为:
b
=
[
b
1
b
2
b
3
]
b=\left[ \begin{matrix} {{b}_{1}} \\ {{b}_{2}} \\ {{b}_{3}} \\ \end{matrix} \right]
b=
b1b2b3
通过 tf.expand_dims(b,axis=0)
插人新维度,变成矩阵:
B
=
[
b
1
b
2
b
3
]
B=\left[ \begin{matrix} \ {{b}_{1}} & {{b}_{2}} & {{b}_{3}} \\ \end{matrix} \right]
B=[ b1b2b3]
此时B的shape 变为[1,3],需要在axis=0图片数量维度上根据输入样本的数量复制若干次,这里的BatchSize为2,即复制一份,变成:
B
′
=
[
b
1
b
2
b
3
b
1
b
2
b
3
]
{{B}^{'}}=\left[ \begin{matrix} {{b}_{1}} & {{b}_{2}} & {{b}_{3}} \\ {{b}_{1}} & {{b}_{2}} & {{b}_{3}} \\ \end{matrix} \right]
B′=[b1b1b2b2b3b3]