转置卷积
上采样方式有三种:双线性或最近邻插值、反池化和转置卷积。现在见的比较多的是双线性和转置卷积。在经典的FCN8中用的就是转置卷积(文中称其为反卷积deconvolution)。
特点
转置卷积顾名思义是一种卷积方式,只是卷积前多了个参数转置的过程。
优点:可学习,理论上模型可以通过学习获取最适合当前数据集的上采样方式。
缺点:存在棋盘效应, 分割边界为锯齿状。前者可以参考国外大佬的博客,消除办法是将kernel设置为stride的整数倍。后者为个人经验,目前没找到好的解决办法。
实现
目前在网上看到的对转置卷积实现方式的解释主要有两种。
- https://blog.csdn.net/LoseInVain/article/details/81098502
- https://blog.csdn.net/w55100/article/details/106467776
以前我偏向第一种解释,把图像和参数展成两个数组相乘,看着比较简单。今天打算仔细研究一下,结果想了半天没想明白偶数如4*4的卷积核该怎么展开。
那还能怎么办呢,叛变呗,第二种解释真香。这里只讨论最基础的情形,即只考虑kernel、stride和padding,不考虑dilation(文献看的少,好像没遇到过上采样时用dilation的)。
先上两个公式,从理论上看一下:
下采样(普通卷积)
L
=
(
H
+
2
∗
p
a
d
d
i
n
g
−
k
e
r
n
e
l
)
/
s
t
r
i
d
e
+
1
L = (H + 2 * padding - kernel) / stride + 1
L=(H+2∗padding−kernel)/stride+1
上采样(转置卷积)
H
=
(
L
−
1
)
∗
s
t
r
i
d
e
+
k
e
r
n
e
l
−
2
∗
p
a
d
d
i
n
g
H = (L - 1) * stride + kernel - 2 * padding
H=(L−1)∗stride+kernel−2∗padding
再举个实际的简单例子。
首先是下采样,为了方便,我们设定图片img为44,kernel为44,stride为2,padding为1。对于下采样,为了简单img和kernel如下:
对img补零,因为padding为1,在img上下左右补一圈0,然后以stride为2滑动,很容易就能得到22的卷积结果:
上采样,我们设定img为22,kernel为4*4,stride为2,padding为1,为了方便,img和kernel如下:
对img补零,重点来了,补零的依据不是padding,而是stride,并且补零是在img里边进行,而不是外围。
卷积前需要先对kernel进行转置,逆时针旋转180°。
在补零后的图像上滑动,滑动间隔固定为1,与stride无关,得到一个6*6的上采样结果。
因为padding为1,我们去掉最外围的一圈,得到最终的上采样结果。
通过拆解转置卷积计算输出尺寸的公式可以清晰看到以上操作的依据
H
=
(
L
−
1
)
∗
s
t
r
i
d
e
+
k
e
r
n
e
l
−
2
∗
p
a
d
d
i
n
g
H = (L - 1) * stride + kernel - 2 * padding
H=(L−1)∗stride+kernel−2∗padding
补零:(L - 1) * stride个
消融:2 * padding行和列,即padding圈
pytorch验证
import torch
import torch.nn.functional as F
import numpy as np
#创建img
img = np.array([1,2,3,4], np.float32)
img = np.reshape(img, [1,1,2,2])
img = torch.from_numpy(img)
#创建kernel
w = np.array(range(1,17),np.float32)
w = np.reshape(w, [1,1,4,4])
w = torch.from_numpy(w)
#计算
output = F.conv_transpose2d(img, w, None, 2, 1)
print(output)
总结
转置卷积与普通卷积在具体实现上都是卷积运算,不同的是对于转置卷积:
- 普通卷积在图像外部补零,转置卷积在图像内部补零
- 普通卷积由padding决定补零个数,转置卷积由stride决定补零个数
- 转置卷积由padding决定最后消去的圈数
PS:由于在实际使用中kernel为随机初始化,所以不需要特意去关心转置的问题。