深度学习之卷积神经网络(11)卷积层变种
- 1. 空洞卷积
- 2. 转置卷积
- 矩阵角度
- 转置卷积实现
- 3. 分离卷积
卷积神经网络的研究产生了各种各样优秀的网络模型,还提出了各种卷积层的变种,本节将重点介绍书中典型的卷积层变种。
1. 空洞卷积
普通的卷积层为了减少为了的参数量,卷积核的设计通常选择较小的和感受野大小。小卷积核使得网络提取特征时的感受野区域有限,但是增大感受野区域又会增加网络的参数量和计算代价,因此需要权衡设计。
空洞卷积(Dilated/Atrous Convolution)的提出较好地解决了这个问题,空洞卷积在普通卷积的感受野上增加一个Dilation Rate参数,用于控制感受野区域的采样步长,如下图所示:
感受野采样步长示意图
当感受野的采样步长Dilation Rate为1时,每个感受野采样点之间的距离为1,此时的空洞卷积退化为普通的卷积; 当Dilation Rate为2时,感受野每2个单元采样一个点,如上图中间绿色方框中绿色格子所示,每个采样格子之间的距离为2; 同样的方法,最右边图的Dilation Rate为3,采样步长为3,尽管Dilation Rate的增大会使得感受野区域增大,但是实际参与运算的点数仍然保持不变。
以输入为单通道的张量,单个卷积核为例,如下图所示。在初始位置,感受野从最上、最右位置开始采样,每隔一个点采样一次,共采集9个数据点,如下图中绿色方框所示。这9个数据点与卷积核相乘运算,写入输出张量的对应位置。
空洞卷积计算示意图-1
卷积核窗口按着步长为向右移动一个单位,如下图所示,同样进行个点采样,共采样9个数据点,与卷积核完成相乘累加运算,写入输出张量对应为,直至卷积核移动至最下方、最右边位置。需要注意区分的是,卷积核窗口的移动步长s和感受野区域的采样步长Dilation Rate是不同的概念。
空洞卷积计算示意图-2
空洞卷积在不增加网络参数的条件下,提供了更大的感受野窗口。但是在使用空洞卷积设置网络模型时,需要精心设计Dilation Rate参数来避免出现网格效应,同样较大的Dilation Rate参数并不利于小物体的检测、语义分割等任务。
在TensorFlow中,可以通过设置layers.Conv2D()类的dilation_rate参数来选择使用普通卷积还是空洞卷积。例如:
import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
x = tf.random.normal([1,7,7,1]) # 模拟输入
# 空洞卷积,1个3×3的卷积核
layer = layers.Conv2D(1, kernel_size=3, strides=1, dilation_rate=2)
out = layer(x) # 向前计算
print(out.shape)
运行结果如下图所示:
![在这里插入图片描述]()
当dilation_rate参数设置为默认值1时,使用普通卷积方式进行计算; 当dilation_rate参数大于1时,采用空洞卷积方式进行计算。
2. 转置卷积
转置卷积(Transposed Convolution,或Fractionally Strided Convolution,部分资料也称之为反卷积/Deconvolution,实际上反卷积在数学上定义为卷积的逆过程,单转置卷积并不能恢复出原卷积的输入,因此称为反卷积并不妥当)通过在输入之间填充大量的padding来实现输出高宽大于输入高宽的效果,从而实现向上采样的目的,如下图所示。我们先介绍转置卷积的计算过程,再介绍转置卷积与普通卷积的联系。
为了简化讨论,我们此处只讨论输入,即输入高宽相等的情况。
转置卷积实现向上采样
为倍数
考虑输入为的单通道特征图,转置卷积核为大小,步长为,填充的例子。首先再输入数据点之间均匀插入个空白数据点,得到的矩阵,如下图第2个矩阵所示,根据填充量矩阵周围填充相应行/列,此时输入张量的高宽为,如下图中第3个矩阵所示。
输入填充步骤
在的输入张量上,进行卷积核,步长,填充的普通卷积运算(注意,此阶段的普通卷积的步长始终为1,与转置卷积的步长不同),根据普通卷积的输出计算公式,得到输出大小为:
大小的输出。我们直接按照此计算流程给出最终转置卷积输出与输入关系。在为倍数时,满足关系:
转置卷积并不是普通的逆过程,但是二者之间有一定的联系,同时转置卷积也是基于普通卷积实现的。在相同的设定下,输入经过普通卷积运算得到,我们将o送入转置卷积运算后,得到,其中,但是与形状相同。我们可以用输入为,步长,填充,卷积核的普通卷积运算进行验证演示,如下图所示:
利用普通卷积恢复等大小输入
可以看到,将转置卷积的输出在同设定条件下送入普通卷积,可以得到的输出,此大小恰好就是转置卷积的输入大小,同时我们也观察到,输出矩阵并不是转置卷积输入的矩阵。转置卷积与普通卷积并不是互为逆过程,不能恢复出对方的输入内容,仅能恢复出等大小的张量。因此称之为反卷积并不切贴。
&emspl;基于TensorFlow实现上述例子的转置卷积运算,代码如下:
import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
# 创建X矩阵,高宽为5×5
x = tf.range(25)+1
# Reshape为合法维度的张量
x = tf.reshape(x, [1, 5, 5, 1])
x = tf.cast(x, tf.float32)
# 创建固定内容的卷积核矩阵
w = tf.constant([[-1, 2, -3.], [4, -5, 6], [-7, 8, -9]])
# 调整为合法维度的张量
w = tf.expand_dims(w, axis=2)
w = tf.expand_dims(w, axis=3)
# 进行普通卷积运算
out = tf.nn.conv2d(x, w, strides=2, padding='VALID')
print(out)
运行结果如下图所示:
现在我们将普通卷积的输出作为转置卷积的输入,验证转置卷积的输出是否为,代码如下:
# 普通卷积的输出作为转置卷积的输入,进行转置卷积运算
xx = tf.nn.conv2d_transpose(out, w, strides=2,
padding='VALID',
output_shape=[1, 5, 5, 1])
# 输出的高宽为5×5
print(xx)
运行结果如下:
tf.Tensor(
[[[[ 67.]
[ -134.]
[ 278.]
[ -154.]
[ 231.]]
[[ -268.]
[ 335.]
[ -710.]
[ 385.]
[ -462.]]
[[ 586.]
[ -770.]
[ 1620.]
[ -870.]
[ 1074.]]
[[ -468.]
[ 585.]
[-1210.]
[ 635.]
[ -762.]]
[[ 819.]
[ -936.]
[ 1942.]
[-1016.]
[ 1143.]]]], shape=(1, 5, 5, 1), dtype=float32)
不为倍数
让我们更加深入地分析卷积运算中输入与输出大小关系的一个细节。考虑卷积运算的输出表达式:
当步长时,向下取整运算使得出现多种不同输入尺寸对应到相同的输出尺寸上。举个例子,考虑输入大小为,卷积核大小为,步长为1的卷积运算,代码如下:
import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
# 创建X矩阵,高宽为5×5
x = tf.random.normal([1, 6, 6, 1])
x = tf.cast(x, tf.float32)
# 创建固定内容的卷积核矩阵
w = tf.constant([[-1, 2, -3.], [4, -5, 6], [-7, 8, -9]])
# 调整为合法维度的张量
w = tf.expand_dims(w, axis=2)
w = tf.expand_dims(w, axis=3)
# 进行普通卷积运算
out = tf.nn.conv2d(x, w, strides=2, padding='VALID')
print(out)
print(out.shape)
运行结果如下图所示:
此种情况也能获得大小的卷积输出,与利用普通卷积可以获得相同大小的输出。因此,不同输入大小的卷积运算可能获得相同大小的输出。考虑到卷积与专制卷积输入输出大小关系互换,从转置卷积的角度来说,输入尺寸经过转置卷积运算后,可能获得不同的输出大小。因此通过填充行、列来实现不同大小的输出,从而恢复普通卷积不同大小的输入的情况,其中关系为:
此时转置卷积的输出变为:
在TensorFlow中间,不需要手动指定参数,只需要指定输出尺寸即可,TensorFlow会自动推导需要填充的行列数,前提是输出尺寸合法。例如:
# 恢复出6×6大小
out = tf.nn.conv2d(x, w, strides=2, padding='VALID')
print(out)
print(out.shape)
# 普通卷积的输出作为转置卷积的输入,进行转置卷积运算
xx = tf.nn.conv2d_transpose(out, w, strides=2,
padding='VALID',
output_shape=[1, 6, 6, 1])
# 输出的高宽为5×5
print(xx)
运行结果如下所示:
tf.Tensor(
[[[[ -8.0665455]
[ 16.133091 ]
[ -4.7349663]
[ -38.92934 ]
[ 58.394012 ]
[ 0. ]]
[[ 32.266182 ]
[ -40.332726 ]
[ -29.459408 ]
[ 97.32335 ]
[-116.788025 ]
[ 0. ]]
[[ -59.992893 ]
[ 71.58651 ]
[ 38.390343 ]
[-126.35293 ]
[ 131.13538 ]
[ 0. ]]
[[ 14.108292 ]
[ -17.635365 ]
[ 79.89131 ]
[ -73.41109 ]
[ 88.09331 ]
[ 0. ]]
[[ -24.68951 ]
[ 28.216583 ]
[-134.51918 ]
[ 117.45774 ]
[-132.13995 ]
[ 0. ]]
[[ 0. ]
[ 0. ]
[ 0. ]
[ 0. ]
[ 0. ]
[ 0. ]]]], shape=(1, 6, 6, 1), dtype=float32)
通过改变参数output_shape=[1, 5, 5, 1]
也可以获得高宽为5×5的张量。
矩阵角度
产生的稀疏矩阵在计算过程中需要先转置,再进行矩阵相乘运算,而普通卷积并没有转置的步骤。这也是它被称为转置卷积的名字的由来。
考虑普通Conv2d运算: 和,需要根据strides将卷积核在行、列方向循环移动获取参与运算的感受野的数据,串行计算每个窗口的“相乘累加”值,计算效率极地。为了加速运算,在数学上可以将卷积核根据strides重排成稀疏矩阵,再通过一次完成运算(实际上,矩阵过于稀疏,导致很多无用的0乘运算,很多深度学习框架也不是通过这种方式实现的)。
以4行4列的输入,高宽为3×3,步长为1,无padding的卷积核的卷积运算为例,首先将打平成,如下图所示:
X'
然后将卷积核转换成稀疏矩阵,如下图所示:
W'
此时通过一次矩阵相乘即可实现普通卷积运算:
如果给定,希望能够生成与相同形状大小的张量,怎么实现呢?将转置后与上图方法重排后的完成矩阵相乘即可:
得到的通过reshape操作变为与原来的输入尺寸一致,但是内容不同。例如的shape为,的shape为,Reshape后即可产生形状的张量。由于转置卷积在矩阵运算时,需要将转置后才能与转置卷积的输入矩阵相乘,故称为转置卷积。
转置卷积具有“放大特征图”的功能,在生成对抗网络、语义分割等中得到了广泛应用,如DCGAN[1]中的生成器通过堆叠转置卷积层实现逐层“放大”特征图,最后获得十分逼真的生成图片。
DCGAN生成器网络结构
[1] A. Radford, L. Metz 和 S. Chintala, Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks, 2015.
转置卷积实现
在TensorFlow中,可以通过nn.conv2d_transpose
实现转置卷积运算。我们先通过nn.conv2d完成普通卷积运算。注意转置卷积的卷积核的定义格式为。例如:
import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
# 创建4×4大小的输入
x = tf.range(16)+1
x = tf.reshape(x, [1, 4, 4, 1])
x = tf.cast(x, tf.float32)
# 创建3×3卷积核
w = tf.constant([[-1, 2, -3.], [4, -5, 6], [-7, 8, -9]])
w = tf.expand_dims(w, axis=2)
w = tf.expand_dims(w, axis=3)
# 普通卷积运算
out = tf.nn.conv2d(x, w, strides=1, padding='VALID')
print(out)
运行结果如下图所示:
保持strides=1,padding=‘VALID’,卷积核不变的情况下,我们通过卷积核与输出的转置卷积运算尝试恢复与输入相同大小的高宽张量,代码如下:
# 恢复4×4大小的输入
xx = tf.nn.conv2d_transpose(out, w, strides=1,
padding='VALID',
output_shape=[1, 4, 4, 1])
tf.squeeze(xx)
print(xx)
运行结果如下所示:
tf.Tensor(
[[[[ 56.]
[ -51.]
[ 46.]
[ 183.]]
[[-148.]
[ -35.]
[ 35.]
[-123.]]
[[ 88.]
[ 35.]
[ -35.]
[ 63.]]
[[ 532.]
[ -41.]
[ 36.]
[ 729.]]]], shape=(1, 4, 4, 1), dtype=float32)
可以看到,转置卷积生成了的特征图,单特征图的数据与输入并不相同。
在使用tf.nn.conv2d_transpose
进行转置卷积运算时,需要额外手动设置输出的高宽。tf.nn.conv2d_transpose
并不支持自定义padding设置,只能设置为VALID或者SAME。
当设置padding=‘VALID’
时,输出大小表达为:
当设置padding=‘SAME’
时,输出大小表达为:
如果我们还是对转置卷积原理细节暂时无法理解,可以先牢记上述两个表达式即可。例如:
的转置卷积输入与的卷积核运算,strides=1,padding=‘VALID’时,输出大小为:
的转置卷积输入与的卷积核运算,strides=1,padding=‘VALID’时,输出大小为:
转置卷积也可以和其他层一样,通过layers.Conv2DTranspose类创建一个转置卷积层,然后调用实例即可完成向前计算。代码如下:
# 创建转置卷积类
layer = layers.Conv2DTranspose(1, kernel_size=3, strides=1, padding='VALID')
xx2 = layer(out)
print(xx2)
运行结果如下:
tf.Tensor(
[[[[ 6.942313 ]
[ 33.887856 ]
[ 24.800087 ]
[ -4.222195 ]]
[[ 21.720724 ]
[ 68.23484 ]
[ 73.74913 ]
[ 28.219326 ]]
[[ 15.284215 ]
[ 10.42746 ]
[ 12.951116 ]
[ 20.34887 ]]
[[ -1.9099097]
[-26.6492 ]
[-56.841545 ]
[-32.62231 ]]]], shape=(1, 4, 4, 1), dtype=float32)
3. 分离卷积
这里以深度可分离卷积(Depth-wise Separable Convolution)为例。普通卷积在对多通道输入进行运算时,卷积核的每个通道与输入的每个通道分别进行卷积运算,得到多通道的特征图,再对应元素相加产生单个卷积核的最终输出,如下图所示:
普通卷积计算示意图
分离卷积的计算流程则不同,卷积核的每个通道与输入的每个通道进行卷积预算,得到多个通道的中间特征,如下图所示。这个多通道的中间特征张量接下来进行多个卷积核的普通卷积运算,得到多个高宽不变的输出,这些输出在通道轴上面进行拼接,从而产生最终的分离卷积层的输出。可以看到,分离卷积层包含了两步卷积运算,第一步卷积运算是单个卷积核,第二个卷积运算包含了多个卷积核。
深度可分离卷积计算示意图
那么采用分离卷积有什么优势呢?一个很明显的优势在于,同样的输入和输出,采用Separable Convolution的参数量约是普通卷积的。考虑上图中的普通卷积和分离卷积的例子。普通卷积的参数量是
分离卷积的第一部分参数量是
第二部分参数量是
分离卷积的总参数量只有39,但是却能实现普通卷积同样的输入输出尺寸变换。分离卷积在Xception和MobileNets等对计算代价敏感的领域中得到了大量应用。