不同卷积操作详解
References:
- A guide to convolution arithmetic for deeplearning, Vincent Dumoulin and Francesco Visin;
- https://github.com/vdumoulin/conv_arithmetic/blob/master/README.md.
引言
我们知道CNN在深度学习中占有举足轻重的地位,而CNN的核心就在于卷积操作。本文提供了对不同卷积操作的感性理解,这其中包含有卷积层(Convolutional),池化层(Pooling)和转置卷积层(Transposed convolutional)里面的输入形状(Input shape),核形状(Kernel shape),零填充(Zero padding),滑动步长(Stride)和输出形状(Output shape)之间的关系。 另外,本文还解释了卷积层和转置卷积层之间的关系。
离散卷积
诸如图像和声音片段类型的数据,他们都有一些本质上相同的结构,这类型的数据都含有一些共同点:
- 一个样本以多维数组的形式存储;
- 这个多维数组里面的一些维度存在***序***的关系,比如图像的pixel位置数据,声音片段的时序,这些维度都不能随便更改,否则会破坏相邻元素之间的关系;
- 存在一个所谓***通道***的维度,这个维度可以被看成是数据在不同视角下的体现,比如Image RGB Channels, Stereo Audio LR Channels。
离散卷积是一个线性变换,并且保留了输入数据的序的关系。它是稀疏的(即只有少部分的输入单元对输出有贡献),以及它重用了参数(相同的权重参数被应用到了输入的多个位置)。
下图展示了离散卷积的一个例子:
其中卷积核为
顺带提一句,数学上的卷积和这里CNN里的卷积操作是不一样的。有时间我再写一篇文章专门探讨数学上的卷积和CNN卷积的区别和联系。上面只是举了一个单通道输入Feature map和单个核做卷积,显然,其输出也是一个单通道的Feature map。
下图展示了一个多通道的输入Feature maps到多通道输出的Feature maps的例子。
这里输入由两个通道的Feature maps组成。输出为三通道的Feature maps。 假设输入通道数为 C i n C_{in} Cin,输出通道数为 C o u t C_{out} Cout,卷积核宽度和高度分别为 W W W和 H H H,这里卷积核的形状为:
w = [ C i n , C o u t , W , H ] \mathbf{w}=[C_{in}, C_{out}, W, H] w=[Cin,Cout,W,H]
卷积核个数为 C i n ∗ C o u t C_{in}*C_{out} Cin∗Cout,权重个数为 C i n ∗ C o u t ∗ W ∗ H C_{in}*C_{out}*W*H Cin∗Cout∗W∗H,假设输入为 I n = [ C i n , W i n , H i n ] In=[C_{in}, W_{in}, H_{in}] In=[Cin,Win,Hin], 输出为 O u t = [ C o u t , W o u t , H o u t ] Out=[C_{out}, W_{out}, H_{out}] Out=[Cout,Wout,Hout],那么第 k k k个输出通道的Feature map由如下计算得到:
O u t k = ∑ i = 1 C i n C o n v ( I n i , w i , k ) Out_k=\sum_{i=1}^{C_{in}}Conv(In_i, w_{i,k}) Outk=∑i=1CinConv(Ini,wi,k)
其中 C o n v ( ⋅ , ⋅ ) Conv(\cdot,\cdot) Conv(⋅,⋅)表示单通道Feature map与单核的卷积操作。
之前讲述的卷积操作都是no zero padding的。Zero padding,按操作讲就是在一个输入的Feature map外围包裹一层或者多层的0。在某些情况下,卷积操作无法完全处理一个输入Feature map的所有特征点,这种情况下就需要在外围填0。在卷积操作中还涉及到一个超参数叫做滑动步长(Stride),Stride决定了卷积核在输入Feature map上的滑动步长。下图展示了一个二维的带zero padding和stride=2时的卷积操作。
Stride可以被看成是一种下采样(subsampling),用一种形象的解释。如下图所示。
左图表示的是stride为2,右图表示的是stride为1。可以看到,输入Feature map在stride为1的时候会产生一个3x3的output,然后对这个3x3的output进行降采样产生一个2x2的输出,这就相当于stride为2直接进行卷积操作得到的结果。影响输出Feature map尺寸的变量有这样一些:输入Feature map的尺寸,核尺寸,Stride和Padding大小。
Pooling
池化也是CNN里面另外一个非常重要的操作。池化操作使用一些函数对Feature map的子区域进行汇总,从而降低Feature map的尺寸。比如典型的,对一个Feature map的子区域取最大或者取平均。从某种角度来讲,池化的工作形式与卷积非常相似。只是把卷积的线性组合换成其他的函数。影响池化结果尺寸的变量有这样一些:输入Feature map的尺寸,Pooling窗口大小,以及Stride。下图展示了一个平均池化的例子(Stride=1)。
##卷积算法
本章分析输入Feature map,Kernel size,Stride,Pooling尺寸与输出Feature map尺寸之间的关系。为了简化问题,我们只考虑2维卷积,且输入Feature map,Kernel都是Squared。Stride和Zero padding在每一维上分别是一样的。padding的作用:padding出来的像素与图像边缘像素组合,构成了一大类独特的“边缘特征”。我们可以认为卷积神经网络同时处理了两种任务:位于图像中部的图像特征和位于图像边缘的图像特征,甚至你可以把这当成两个分类任务,只不过在一个网络中实现,就好比我一个网络就要实现对mnist的分类和对cifar10的分类。(https://www.zhihu.com/question/263904898/answer/379907224)
假设:
-
输入Feature map的长宽都为 i i i。
-
Kernel size长宽都为 k k k。
-
Stride长宽都为 s s s。
-
Padding长宽都为 p p p。
那么输出Feature map的长宽是相等的,我们令其为 o o o。
当p和s分别取不同的值的时候,输出Featuer map的尺寸 o o o被分为下面的Case进行计算。
1)No padding, s=1
这种情况最简单,No zero padding,Stride=1。
o = ( i − k ) + 1 o=(i-k) + 1 o=(i−k)+1
示例:
###2)Padding,s=1
o = ( i − k ) + 2 p + 1 o=(i-k)+2p+1 o=(i−k)+2p+1
此时相当于输入Feature map的尺寸由i变成了i + 2p,然后进行No padding, s=1的卷积操作。
示例:
###3)Half (same) padding, s=1
Same padding表示的意思是我们希望输出Feature map的尺寸等于输入Feature map的尺寸。即 o = i o=i o=i,此时的 p p p应该是多少?按照2)逆推,如果k为奇数,此时:
p = ⌊ k 2 ⌋ p=\lfloor \frac{k}{2}\rfloor p=⌊2k⌋
当k为偶数的时候,在这种情况下无法使得 o = i o=i o=i,除非只padding每一维的其中一边。
示例:
4)Full padding, s=1
Full padding可能会导致 o ≥ i o\geq i o≥i,Full padding下设置 p = k − 1 p =k -1 p=k−1。注意到,Padding的大小不能大于等于Kernel的大小。如果设置 p ≥ k p \geq k p≥k,将导致输出Feature map里也会包含Zero padding,这显示是没用的。
o = [ i + 2 ( k − 1 ) ] − k + 1 = i + k − 1 o=[i + 2(k-1)] - k + 1=i + k-1 o=[i+2(k−1)]−k+1=i+k−1
之前介绍的卷积操作都是基于unit stride的基础上做的。下面我们介绍一下non-unit stride的情况。
5)No padding, s > 1
这种情况下,
o = ⌊ i − k s ⌋ + 1 o=\lfloor \frac{i-k}{s} \rfloor + 1 o=⌊si−k⌋+1
向下取整函数是说在有些情况下Kernel并不能达到输入Feature map的边界。
6) padding, s >1
这种情况下,
o = ⌊ i + 2 p − k s ⌋ + 1 o=\lfloor\frac{i + 2p-k}{s} \rfloor +1 o=⌊si+2p−k⌋+1
一些示例:
[外链图片转存失败(img-EuwccEqt-1568639624639)(./images/s>1.png)]
Pooling操作
理论上讲,在卷积操作之后,就可以利用提取到的Feature map训练分类器了。但这样做面临着计算量的挑战,例如:对于一个 96X96 像素的图像,假设我们已经学习得到了400个定义在8X8输入上的特征,每一个特征和图像卷积都会得到一个 (96 − 8 + 1) * (96 − 8 + 1) = 7921 维的卷积特征,由于有 400 个特征,所以每个样例 (example) 都会得到一个 892 * 400 = 3,168,400 维的卷积特征向量。学习一个拥有超过 3 百万特征输入的分类器十分不便,并且容易出现过拟合 (over-fitting)。
我们之所以决定使用卷积后的特征是因为图像具有一种‘静态性‘的属性。这也就意味着在一个图像区域有用的特征极有可能在另一个区域同样适用。因此,为了描述大的图像,一个很自然的想法就是对不同位置的特征进行聚合统计,例如,人们可以计算图像一个区域上的某个特定特征的平均值 (或最大值)。这些概要统计特征不仅具有低得多的维度 (相比使用所有提取得到的特征),同时还会改善结果(不容易过拟合)。这种聚合的操作就叫做池化 (pooling),有时也称为平均池化或者最大池化 (取决于计算池化的方法)。
一般而言,池化不会涉及到padding,因此,池化后,
o = ⌊ i − k s ⌋ + 1 o=\lfloor \frac{i - k}{s}\rfloor + 1 o=⌊si−k⌋+1
转置卷积操作(Transposed/Fractional Strided Convolution)
一般情况下,卷积操作可以说,抽取出了输入Feature map的关键特征,但是也损失了不少的信息。在从原始输入到最终输出的过程中,经过层层卷积,最终的特征精炼,关键。但是在某些任务中,我们可能需要原始输入中的某些微小的区域。通过层层的卷积操作,这些微小区域很可能被filter掉了。比如在Object detection任务中,如果想要检测到小的object,通过这样的操作,是无法检测小object的。
回顾一般的卷积操作,实际上,一般卷积可以被看成是对输入的一个线性变换,即
Y = C X Y = CX Y=CX,其中C为一个矩阵,其元素由卷积核的元素构成,比如一个简单的2x2卷积, stride=1, padding=0,输入为4x4的情况。C就为
这里的X为4x4的输入展成16维向量,那么输出就为4x1,经过一定的排列组合形成2x2的形状,即为卷积的输出。
这里 Y = C X Y=CX Y=CX为卷积的前向操作,即通过输入得到输出的过程。那么在反向传播的时候呢?
假设我们从更深层的网络中得到了 ∂ l o s s ∂ Y \frac{\partial loss}{\partial Y} ∂Y∂loss,现在我们要求 ∂ l o s s ∂ X \frac{\partial loss}{\partial X} ∂X∂loss。
∂ l o s s ∂ X j = ∑ i ∂ l o s s ∂ Y i ∂ Y i ∂ X j \frac{\partial loss}{\partial X_j} = \sum_i \frac{\partial loss}{\partial Y_i} \frac{\partial Y_i}{\partial X_j} ∂Xj∂loss=∑i∂Yi∂loss∂Xj∂Yi
由于 ∂ Y i ∂ X j = C i , j \frac{\partial Y_i}{\partial X_j} = C_{i, j} ∂Xj∂Yi=Ci,j,所以 ∑ i ∂ l o s s ∂ Y i ∂ Y i ∂ X j = ∂ l o s s ∂ Y ⋅ C ∗ , j = C ∗ , j T ∂ l o s s ∂ Y \sum_i \frac{\partial loss}{\partial Y_i} \frac{\partial Y_i}{\partial X_j}=\frac{\partial loss}{\partial Y} \cdot C_{*, j} = C_{*, j}^T \frac{\partial loss}{\partial Y} ∑i∂Yi∂loss∂Xj∂Yi=∂Y∂loss⋅C∗,j=C∗,jT∂Y∂loss。
我们看到,前向操作是输入左乘一个 C C C,反向传播的时候是左乘 C T C^T CT。
那么所谓转置卷积操作实际上就是在前向操作中左乘一个 C T C^T CT,反向传播的时候左乘 ( C T ) T (C^T)^T (CT)T。那么我们可以看到,一般卷积降低了输入Feature map的特征个数,而转置卷积则增加了特征个数。
转置卷积是一种上采样操作。下面具体分为几种情况来分别理解。
-
Stride = 1, no padding的一般卷积
上图展示了一个stride=1, no padding的一般卷积。那么其对应的转置卷积就是
可以看到,no padding操作变成了Full padding操作。公式关系为正向卷积s=1, p=0,而转置卷积k’=k, s’=s, p’=k-1。这可以从直观上理解转置卷积。
对于一般卷积s=1, p=0,k,与之关联的转置卷积为s’=s, p’=k-1, k’=k, i’。那么转置卷积的输出为
o ′ = i ′ + ( k − 1 ) o'=i'+(k-1) o′=i′+(k−1)
-
Stride > 1, no padding的一般卷积
上图展示了一个Stride = 2卷积的转置卷积过程,实际上,这里转置卷积的Stride相对于原始输入来讲是一个小于1的数。s’ = 1 / s,因此转置卷积也被称为Fractional strided convolution(微步卷积)。因此,这里需要将输入变成带’洞’的形式。这时,直观上理解,转置卷积的stride相对于原始输入确实是小于1的。一般卷积输入i = 5, s = 2, p = 0,得到的卷积大小为2x2。那么对应的转置卷积过程则是i’ = 2,要恢复5x5的大小,i’的输入之间需要补0,s’=1, p‘ = k - 1。
对于一般卷积s > 1,p=0, k,与之关联的转置卷积为s’=1, k’=k, p’=k-1,i’, 输入的每个特征之间加s-1个空洞(0),转置卷积的输出为:
o ′ = s ( i ′ − 1 ) + k o'=s(i'-1)+k o′=s(i′−1)+k
-
Stride = 1, padding > 0
上图展示的是一个原卷积为i=5, k=4, s=1, p=2的转置卷积操作。可以注意到,原卷积操作后,Feature map的规模变大了。因此,转置卷积操作缩小了Feature map。
对于一般卷积s = 1, k and p > 0,转置卷积的k’=k, s’=s, p’ = k - p - 1。转置卷积的输出为:
o ′ = i ′ + ( k − 1 ) − 2 p o'=i'+(k-1)-2p o′=i′+(k−1)−2p
-
Stride = 1, padding = Same
对于一般卷积k为奇数,s=1, p = (k - 1) / 2,其对应的转置卷积为k’=k, s’=s, p’=p。
-
Stride = 1, padding = Full
对于一般卷积s = 1, k, p = k - 1。其转置卷积为k’=k, s’=s, p’=0。转置卷积的输出为:
o ′ = i ′ + ( k − 1 ) − 2 p = i ′ + ( k − 1 ) o'=i'+(k-1)-2p=i'+(k - 1) o′=i′+(k−1)−2p=i′+(k−1)
-
Stride > 1, padding > 0
对于一般卷积k, s, p, i,其转置卷积为k’=k, s’=1, p’=k - p - 1, 输入间需要加入s-1个空洞(0),a = (i + 2p - k) mod s表示额外需要在输入的底部和右部补0的圈数。那么转置卷积的输出为:
o ′ = s ( i ′ − 1 ) + a + k − 2 p o'=s(i'-1)+a+k-2p o′=s(i′−1)+a+k−2p
需要注意的是,转置卷积实际上也是在进行一般的卷积操作!只不过输入,输出的形式,stride,padding这些参数做了一些转化而已。
空洞卷积操作(Dilated Convolution)
转置卷积在Stride<1的情况下,相当于对输入Feature map做了一个inflate。而空洞卷积则是对kernel进行inflate,但是不改变kernel的大小。
如图展示的是一个i=7, s=1, p=0,d=2的空洞卷积。其中d为带洞的大小。