深度学习之卷积神经网络(11)卷积层变种

  • 1. 空洞卷积
  • 2. 转置卷积
  • 矩阵角度
  • 转置卷积实现
  • 3. 分离卷积



卷积神经网络的研究产生了各种各样优秀的网络模型,还提出了各种卷积层的变种,本节将重点介绍书中典型的卷积层变种。



1. 空洞卷积

 普通的卷积层为了减少为了的参数量,卷积核的设计通常选择较小的神经网络改变标签的输出 神经网络变种_pytorch神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02感受野大小。小卷积核使得网络提取特征时的感受野区域有限,但是增大感受野区域又会增加网络的参数量和计算代价,因此需要权衡设计。


空洞卷积Dilated/Atrous Convolution)的提出较好地解决了这个问题,空洞卷积在普通卷积的感受野上增加一个Dilation Rate参数,用于控制感受野区域的采样步长,如下图所示:


神经网络改变标签的输出 神经网络变种_卷积_03


感受野采样步长示意图

当感受野的采样步长Dilation Rate为1时,每个感受野采样点之间的距离为1,此时的空洞卷积退化为普通的卷积; 当Dilation Rate为2时,感受野每2个单元采样一个点,如上图中间绿色方框中绿色格子所示,每个采样格子之间的距离为2; 同样的方法,最右边图的Dilation Rate为3,采样步长为3,尽管Dilation Rate的增大会使得感受野区域增大,但是实际参与运算的点数仍然保持不变。


 以输入为单通道的神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_04张量,单个神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02卷积核为例,如下图所示。在初始位置,感受野从最上、最右位置开始采样,每隔一个点采样一次,共采集9个数据点,如下图中绿色方框所示。这9个数据点与卷积核相乘运算,写入输出张量的对应位置。


神经网络改变标签的输出 神经网络变种_神经网络_06


空洞卷积计算示意图-1

 卷积核窗口按着步长为神经网络改变标签的输出 神经网络变种_卷积_07向右移动一个单位,如下图所示,同样进行个点采样,共采样9个数据点,与卷积核完成相乘累加运算,写入输出张量对应为,直至卷积核移动至最下方、最右边位置。需要注意区分的是,卷积核窗口的移动步长s和感受野区域的采样步长Dilation Rate是不同的概念。


神经网络改变标签的输出 神经网络变种_神经网络_08


空洞卷积计算示意图-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来实现输出高宽大于输入高宽的效果,从而实现向上采样的目的,如下图所示。我们先介绍转置卷积的计算过程,再介绍转置卷积与普通卷积的联系。


 为了简化讨论,我们此处只讨论输入神经网络改变标签的输出 神经网络变种_卷积_09,即输入高宽相等的情况。


神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_10


转置卷积实现向上采样

神经网络改变标签的输出 神经网络变种_卷积_11神经网络改变标签的输出 神经网络变种_卷积_12倍数


 考虑输入为神经网络改变标签的输出 神经网络变种_神经网络_13的单通道特征图,转置卷积核为神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02大小,步长为神经网络改变标签的输出 神经网络变种_pytorch_15,填充神经网络改变标签的输出 神经网络变种_pytorch_16的例子。首先再输入数据点之间均匀插入神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_17个空白数据点,得到神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02的矩阵,如下图第2个矩阵所示,根据填充量神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02矩阵周围填充相应神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_20行/列,此时输入张量的高宽为神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_04,如下图中第3个矩阵所示。


神经网络改变标签的输出 神经网络变种_pytorch_22


输入填充步骤

 在神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_04的输入张量上,进行神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02卷积核,步长神经网络改变标签的输出 神经网络变种_深度学习_25,填充神经网络改变标签的输出 神经网络变种_pytorch_16的普通卷积运算(注意,此阶段的普通卷积的步长神经网络改变标签的输出 神经网络变种_深度学习_27始终为1,与转置卷积的步长神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_28不同),根据普通卷积的输出计算公式,得到输出大小为:

神经网络改变标签的输出 神经网络变种_神经网络_29

神经网络改变标签的输出 神经网络变种_深度学习_30大小的输出。我们直接按照此计算流程给出最终转置卷积输出与输入关系。在神经网络改变标签的输出 神经网络变种_深度学习_31神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_28倍数时,满足关系:

神经网络改变标签的输出 神经网络变种_神经网络_33

 转置卷积并不是普通的逆过程,但是二者之间有一定的联系,同时转置卷积也是基于普通卷积实现的。在相同的设定下,输入神经网络改变标签的输出 神经网络变种_卷积_34经过普通卷积运算得到神经网络改变标签的输出 神经网络变种_深度学习_35,我们将o送入转置卷积运算后,得到神经网络改变标签的输出 神经网络变种_pytorch_36,其中神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_37,但是神经网络改变标签的输出 神经网络变种_深度学习_38神经网络改变标签的输出 神经网络变种_卷积_34形状相同。我们可以用输入为神经网络改变标签的输出 神经网络变种_深度学习_30,步长神经网络改变标签的输出 神经网络变种_pytorch_15,填充神经网络改变标签的输出 神经网络变种_pytorch_16神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02卷积核的普通卷积运算进行验证演示,如下图所示:


神经网络改变标签的输出 神经网络变种_pytorch_44


利用普通卷积恢复等大小输入

 可以看到,将转置卷积的输出神经网络改变标签的输出 神经网络变种_深度学习_30在同设定条件下送入普通卷积,可以得到神经网络改变标签的输出 神经网络变种_神经网络_13的输出,此大小恰好就是转置卷积的输入大小,同时我们也观察到,输出神经网络改变标签的输出 神经网络变种_神经网络_13矩阵并不是转置卷积输入的神经网络改变标签的输出 神经网络变种_神经网络_13矩阵。转置卷积与普通卷积并不是互为逆过程,不能恢复出对方的输入内容,仅能恢复出等大小的张量。因此称之为反卷积并不切贴。

&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)

运行结果如下图所示:


神经网络改变标签的输出 神经网络变种_神经网络_49

 现在我们将普通卷积的输出作为转置卷积的输入,验证转置卷积的输出是否为神经网络改变标签的输出 神经网络变种_深度学习_30,代码如下:

# 普通卷积的输出作为转置卷积的输入,进行转置卷积运算
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)

神经网络改变标签的输出 神经网络变种_神经网络_51不为神经网络改变标签的输出 神经网络变种_卷积_12倍数

 让我们更加深入地分析卷积运算中输入与输出大小关系的一个细节。考虑卷积运算的输出表达式:
神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_53
当步长神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_54时,神经网络改变标签的输出 神经网络变种_pytorch_55向下取整运算使得出现多种不同输入尺寸神经网络改变标签的输出 神经网络变种_卷积_56对应到相同的输出尺寸神经网络改变标签的输出 神经网络变种_深度学习_57上。举个例子,考虑输入大小为神经网络改变标签的输出 神经网络变种_pytorch_58,卷积核大小为神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02,步长为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)

运行结果如下图所示:


神经网络改变标签的输出 神经网络变种_神经网络_60

此种情况也能获得神经网络改变标签的输出 神经网络变种_神经网络_13大小的卷积输出,与利用普通卷积可以获得相同大小的输出。因此,不同输入大小的卷积运算可能获得相同大小的输出。考虑到卷积与专制卷积输入输出大小关系互换,从转置卷积的角度来说,输入尺寸神经网络改变标签的输出 神经网络变种_卷积_56经过转置卷积运算后,可能获得不同的输出神经网络改变标签的输出 神经网络变种_深度学习_57大小。因此通过填充神经网络改变标签的输出 神经网络变种_神经网络_64行、神经网络改变标签的输出 神经网络变种_神经网络_64列来实现不同大小的输出神经网络改变标签的输出 神经网络变种_深度学习_57,从而恢复普通卷积不同大小的输入的情况,其中神经网络改变标签的输出 神经网络变种_神经网络_64关系为:
神经网络改变标签的输出 神经网络变种_卷积_68
此时转置卷积的输出变为:
神经网络改变标签的输出 神经网络变种_卷积_69
 在TensorFlow中间,不需要手动指定神经网络改变标签的输出 神经网络变种_神经网络_64参数,只需要指定输出尺寸即可,TensorFlow会自动推导需要填充的行列数神经网络改变标签的输出 神经网络变种_神经网络_64,前提是输出尺寸合法。例如:

# 恢复出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的张量。

矩阵角度

神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_72产生的稀疏矩阵神经网络改变标签的输出 神经网络变种_神经网络_73在计算过程中需要先转置神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_74,再进行矩阵相乘运算,而普通卷积并没有转置神经网络改变标签的输出 神经网络变种_神经网络_73的步骤。这也是它被称为转置卷积的名字的由来。


 考虑普通Conv2d运算: 神经网络改变标签的输出 神经网络变种_卷积_76神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_72,需要根据strides将卷积核在行、列方向循环移动获取参与运算的感受野的数据,串行计算每个窗口的“相乘累加”值,计算效率极地。为了加速运算,在数学上可以将卷积核神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_72根据strides重排成稀疏矩阵神经网络改变标签的输出 神经网络变种_神经网络_73,再通过神经网络改变标签的输出 神经网络变种_神经网络_80一次完成运算(实际上,神经网络改变标签的输出 神经网络变种_神经网络_73矩阵过于稀疏,导致很多无用的0乘运算,很多深度学习框架也不是通过这种方式实现的)。


 以4行4列的输入神经网络改变标签的输出 神经网络变种_卷积_76,高宽为3×3,步长为1,无padding的卷积核神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_72的卷积运算为例,首先将神经网络改变标签的输出 神经网络变种_卷积_76打平成神经网络改变标签的输出 神经网络变种_深度学习_85,如下图所示:


神经网络改变标签的输出 神经网络变种_神经网络_86


X'

然后将卷积核神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_72转换成稀疏矩阵神经网络改变标签的输出 神经网络变种_神经网络_73,如下图所示:


神经网络改变标签的输出 神经网络变种_pytorch_89


W'

 此时通过一次矩阵相乘即可实现普通卷积运算:

神经网络改变标签的输出 神经网络变种_深度学习_90

如果给定神经网络改变标签的输出 神经网络变种_深度学习_91,希望能够生成与神经网络改变标签的输出 神经网络变种_卷积_76相同形状大小的张量,怎么实现呢?将神经网络改变标签的输出 神经网络变种_神经网络_73转置后与上图方法重排后的神经网络改变标签的输出 神经网络变种_神经网络_94完成矩阵相乘即可:

神经网络改变标签的输出 神经网络变种_深度学习_95

得到的神经网络改变标签的输出 神经网络变种_深度学习_85通过reshape操作变为与原来的输入神经网络改变标签的输出 神经网络变种_卷积_76尺寸一致,但是内容不同。例如神经网络改变标签的输出 神经网络变种_神经网络_94的shape为神经网络改变标签的输出 神经网络变种_神经网络_99神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_74的shape为神经网络改变标签的输出 神经网络变种_神经网络_101,Reshape后即可产生神经网络改变标签的输出 神经网络变种_深度学习_102形状的张量。由于转置卷积在矩阵运算时,需要将神经网络改变标签的输出 神经网络变种_神经网络_73转置后才能与转置卷积的输入神经网络改变标签的输出 神经网络变种_神经网络_94矩阵相乘,故称为转置卷积。


 转置卷积具有“放大特征图”的功能,在生成对抗网络、语义分割等中得到了广泛应用,如DCGAN[1]中的生成器通过堆叠转置卷积层实现逐层“放大”特征图,最后获得十分逼真的生成图片。


神经网络改变标签的输出 神经网络变种_卷积_105


DCGAN生成器网络结构

[1] A. Radford, L. Metz 和 S. Chintala, Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks, 2015.

转置卷积实现

 在TensorFlow中,可以通过nn.conv2d_transpose实现转置卷积运算。我们先通过nn.conv2d完成普通卷积运算。注意转置卷积的卷积核的定义格式为神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_106。例如:

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)

运行结果如下图所示:


神经网络改变标签的输出 神经网络变种_深度学习_107

 保持strides=1,padding=‘VALID’,卷积核不变的情况下,我们通过卷积核神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_108与输出神经网络改变标签的输出 神经网络变种_神经网络_109的转置卷积运算尝试恢复与输入神经网络改变标签的输出 神经网络变种_卷积_110相同大小的高宽张量,代码如下:

# 恢复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)

可以看到,转置卷积生成了神经网络改变标签的输出 神经网络变种_深度学习_111的特征图,单特征图的数据与输入神经网络改变标签的输出 神经网络变种_卷积_110并不相同。

 在使用tf.nn.conv2d_transpose进行转置卷积运算时,需要额外手动设置输出的高宽。tf.nn.conv2d_transpose并不支持自定义padding设置,只能设置为VALID或者SAME。

 当设置padding=‘VALID’时,输出大小表达为:
神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_113

 当设置padding=‘SAME’时,输出大小表达为:
神经网络改变标签的输出 神经网络变种_深度学习_114
 如果我们还是对转置卷积原理细节暂时无法理解,可以先牢记上述两个表达式即可。例如:

神经网络改变标签的输出 神经网络变种_神经网络_13的转置卷积输入与神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02的卷积核运算,strides=1,padding=‘VALID’时,输出大小为:
神经网络改变标签的输出 神经网络变种_卷积_117

神经网络改变标签的输出 神经网络变种_神经网络_13的转置卷积输入与神经网络改变标签的输出 神经网络变种_神经网络改变标签的输出_02的卷积核运算,strides=1,padding=‘VALID’时,输出大小为:
神经网络改变标签的输出 神经网络变种_卷积_120
 转置卷积也可以和其他层一样,通过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)为例。普通卷积在对多通道输入进行运算时,卷积核的每个通道与输入的每个通道分别进行卷积运算,得到多通道的特征图,再对应元素相加产生单个卷积核的最终输出,如下图所示:


神经网络改变标签的输出 神经网络变种_深度学习_121


普通卷积计算示意图

 分离卷积的计算流程则不同,卷积核的每个通道与输入的每个通道进行卷积预算,得到多个通道的中间特征,如下图所示。这个多通道的中间特征张量接下来进行多个神经网络改变标签的输出 神经网络变种_pytorch卷积核的普通卷积运算,得到多个高宽不变的输出,这些输出在通道轴上面进行拼接,从而产生最终的分离卷积层的输出。可以看到,分离卷积层包含了两步卷积运算,第一步卷积运算是单个卷积核,第二个卷积运算包含了多个卷积核。


神经网络改变标签的输出 神经网络变种_pytorch_123


深度可分离卷积计算示意图

 那么采用分离卷积有什么优势呢?一个很明显的优势在于,同样的输入和输出,采用Separable Convolution的参数量约是普通卷积的神经网络改变标签的输出 神经网络变种_卷积_124。考虑上图中的普通卷积和分离卷积的例子。普通卷积的参数量是
神经网络改变标签的输出 神经网络变种_pytorch_125
分离卷积的第一部分参数量是
神经网络改变标签的输出 神经网络变种_pytorch_126
第二部分参数量是
神经网络改变标签的输出 神经网络变种_pytorch_127
分离卷积的总参数量只有39,但是却能实现普通卷积同样的输入输出尺寸变换。分离卷积在Xception和MobileNets等对计算代价敏感的领域中得到了大量应用。