一、参考资料
【keras/Tensorflow/pytorch】Conv2D和Conv2DTranspose详解
怎样通俗易懂地解释反卷积?
转置卷积(Transposed Convolution)
抽丝剥茧,带你理解转置卷积(反卷积)
二、标准卷积(Conv2D)
1. Conv2D
中的步长stride
1.1 当步长stride=1
输入特征图(蓝色):
(
H
i
n
,
W
i
n
)
=
(
4
,
4
)
(H_{in},W_{in})=(4,4)
(Hin,Win)=(4,4)。
卷积核:
K
=
3
,
s
t
r
i
d
e
(
S
)
=
1
,
p
a
d
d
i
n
g
=
0
K=3,stride(S)=1,padding=0
K=3,stride(S)=1,padding=0。
输出特征图(绿色):
(
H
o
u
t
,
W
o
u
t
)
=
(
2
,
2
)
(H_{out},W_{out})=(2,2)
(Hout,Wout)=(2,2)。
代入
公式
(
1
)
公式(1)
公式(1)中,可得:
H
o
u
t
=
H
i
n
+
2
p
−
K
S
+
1
H
o
u
t
=
4
+
2
∗
0
−
3
1
+
1
=
2
H_{out}=\frac{H_{in}+2p-K}S+1\\ H_{out}=\frac{4+2*0-3}1+1=2
Hout=SHin+2p−K+1Hout=14+2∗0−3+1=2
1.2 当步长stride=2
输入特征图(蓝色):
(
H
i
n
,
W
i
n
)
=
(
5
,
5
)
(H_{in},W_{in})=(5,5)
(Hin,Win)=(5,5)。
卷积核:
K
=
3
,
s
t
r
i
d
e
(
S
)
=
2
,
p
a
d
d
i
n
g
=
1
K=3,stride(S)=2,padding=1
K=3,stride(S)=2,padding=1。
输出特征图(绿色):
(
H
o
u
t
,
W
o
u
t
)
=
(
3
,
3
)
(H_{out},W_{out})=(3,3)
(Hout,Wout)=(3,3)。
代入
公式
(
1
)
公式(1)
公式(1)中,可得:
H
o
u
t
=
H
i
n
+
2
p
−
K
S
+
1
H
o
u
t
=
5
+
2
∗
1
−
3
2
+
1
=
3
H_{out}=\frac{H_{in}+2p-K}S+1\\ H_{out}=\frac{5+2*1-3}2+1=3
Hout=SHin+2p−K+1Hout=25+2∗1−3+1=3
2. Conv2D
计算公式
标准卷积计算公式有:
o
=
i
+
2
p
−
K
S
+
1
i
=
size
of
input
o
=
size
of
output
p
=
p
a
d
d
i
n
g
K
=
size
of
kernel
S
=
s
t
r
i
d
e
s
o=\frac{i+2p-K}S+1 \quad \begin{array}{l} \\i=\textit{size of input}\\o=\textit{size of output}\\p=padding\\K=\textit{size of kernel}\\S=strides\end{array}
o=Si+2p−K+1i=size of inputo=size of outputp=paddingK=size of kernelS=strides
以特征图的高度Height为例,经过卷积操作之后,输出特征图计算公式为:
H
o
u
t
=
H
i
n
+
2
p
−
K
S
+
1
(
1
)
H_{out}=\frac{H_{in}+2p-K}S+1\quad(1)
Hout=SHin+2p−K+1(1)
三、转置卷积(Conv2DTranspose)
1. 引言
对于很多生成模型(如语义分割、自动编码器(Autoencoder)、GAN中的生成器等模型),我们通常希望进行与标准卷积相反的转换,即执行上采样。对于语义分割,首先用编码器提取特征图,然后用解码器恢复原始图像大小,这样来分类原始图像的每个像素。
实现上采样的传统方法是应用插值方案或人工创建规则。而神经网络等现代架构则倾向于让网络自动学习合适的变换,无需人类干预。为了做到这一点,我们可以使用转置卷积。
2. 对转置卷积名称的误解
转置卷积又叫反卷积、逆卷积。然而,转置卷积是目前最为正规和主流的名称,因为这个名称更加贴切的描述了Conv2DTranspose
的计算过程,而其他的名字容易造成误导。在主流的深度学习框架中,如TensorFlow,Pytorch,Keras中的函数名都是 conv_transpose
。所以,学习转置卷积之前,我们一定要弄清楚标准名称,遇到他人说反卷积、逆卷积也要帮其纠正,让不正确的命名尽早的淹没在历史的长河中。
我们先说一下为什么人们很喜欢将转置卷积称为反卷积或逆卷积。首先举一个例子,将一个4x4的输入通过3x3的卷积核在进行普通卷积(无padding, stride=1),将得到一个2x2的输出。而转置卷积将一个2x2的输入通过同样3x3大小的卷积核将得到一个4x4的输出,看起来似乎是普通卷积的逆过程。就好像是加法的逆过程是减法,乘法的逆过程是除法一样,人们自然而然的认为这两个操作似乎是一个可逆的过程。转置卷积不是卷积的逆运算(一般卷积操作是不可逆的),转置卷积也是卷积。转置卷积并不是正向卷积的完全逆过程(逆运算),它不能完全恢复输入矩阵的数据,只能恢复输入矩阵的大小(shape)。所以,转置卷积的名字就由此而来,而并不是“反卷积”或者是“逆卷积”,不好的名称容易给人以误解。
有些地方,转置卷积又被称作 fractionally-strided convolution
或者deconvolution
,但 deconvolution
具有误导性,不建议使用。因此,本文将会使用两个名字,分别对应代码版和学术论文版,分别是 Conv2DTranspose
和 fractionally-strided convolutions
。
3. Conv2DTranspose
的概念
转置卷积(Transposed Convolution) 在语义分割或者==对抗神经网络(GAN)==中比较常见,其主要作用是做上采样(UpSampling)。
4. Conv2D
与Conv2DTranspose
对比
转置卷积和标准卷积有很大的区别,直接卷积是用一个“小窗户”去看一个“大世界”,而转置卷积是用一个“大窗户”的一部分去看“小世界”。
标准卷积(大图变小图)中,输入(5,5),步长(2,2),输出(3,3)。
转置卷积操作中(小图变大图),输入(3,3)输出(5,5)。
5. Conv2DTranspose
计算过程
转置卷积核大小为k,步长s,填充p,则转置卷积的计算步骤可以总结为三步:
- 第一步:对输入特征图变换(插值);
- 第二步:对卷积核变换,并padding新的输入特征图;
- 第三步:执行标准卷积操作。
5.1 第一步:对输入特征图变换(插值)
对输入特征图 m m m 进行插值(interpolation)零元素,得到新的输入特征图 m ′ m^{\prime} m′ 。
以特征图的高度Height为例,输入特征图的Height高为
H
i
n
H_{in}
Hin ,中间有
(
H
i
n
−
1
)
(H_{in}-1)
(Hin−1) 个空隙。
两个相邻位置中间的插0个数:
s
−
1
s-1
s−1,
s
s
s 表示步长。
高度方向上总共插0个数:
(
H
i
n
−
1
)
∗
(
s
−
1
)
(H_{in}-1) * (s-1)
(Hin−1)∗(s−1)。
新的输入特征图的大小:
H
i
n
′
=
H
i
n
+
(
H
i
n
−
1
)
∗
(
s
−
1
)
H_{in}^{\prime} = H_{in} + (H_{in}-1)*(s-1)
Hin′=Hin+(Hin−1)∗(s−1)。
5.2 第二步:对卷积核变换,并padding新的输入特征图
对卷积核 k k k 进行上下、左右翻转,得到新的卷积核 k ′ k^{\prime} k′。再对新的输入特征图四周填充padding零元素。
- 新卷积核 k ′ k^{\prime} k′ 大小不变: k ′ = k k^{\prime}=k k′=k。
- 新卷积核的步长stride永远不变: s ′ = 1 s^{\prime}=1 s′=1。
- 填充padding零元素个数:
p
′
=
k
−
p
−
1
p^{\prime} = k-p-1
p′=k−p−1。
5.3 第三步:执行标准卷积操作
用变换后的卷积核对变换后的输入特征图进行标准卷积操作,得到的结果就是转置卷积的结果。
根据标准卷积的计算公式可知:
H
o
u
t
=
(
H
i
n
′
+
2
p
′
−
k
′
)
s
′
+
1
(
2
)
\mathrm{H_{out}}=\frac{(\mathrm{H_{in}^{\prime}}+2\mathrm{p^{\prime}}-\mathrm{k^{\prime}})}{\mathrm{s^{\prime}}}+1\quad(2)
Hout=s′(Hin′+2p′−k′)+1(2)
H ′ = H i n + ( H i n − 1 ) ∗ ( s − 1 ) H^{\prime} = H_{in} + (H_{in}-1)*(s-1) H′=Hin+(Hin−1)∗(s−1),
p ′ = k − p − 1 p^{\prime} = k-p-1 p′=k−p−1,
k ′ = k k^{\prime}=k k′=k。
将第一、二步中变换的结果代入上式,可得:
H
o
u
t
=
(
H
i
n
+
H
i
n
∗
s
−
H
−
s
+
1
)
+
2
∗
(
k
−
p
−
1
)
−
k
s
′
+
1
(
3
)
\text{H}_{out}=\frac{(\text{H}_{in}+\text{H}_{in}*s-\text{H}-\text{s}+1)+2*(\text{k}-\text{p}-1)-\text{k}}{\text{s}'}+1\quad(3)
Hout=s′(Hin+Hin∗s−H−s+1)+2∗(k−p−1)−k+1(3)
化简,可得:
H
o
u
t
=
(
H
i
n
−
1
)*s
+
k
−
2
p
−
1
s
′
+
1
(
4
)
\text{H}_{out}=\frac{(\text{H}_{in}-1\text{)*s}+\text{k}-2\text{p}-1}{\text{s}'}+1\quad(4)
Hout=s′(Hin−1)*s+k−2p−1+1(4)
上式中,分母步长
s
′
=
1
s^{\prime}=1
s′=1,则最终结果为:
H
o
u
t
=
(
H
i
n
−
1
)
∗
s
−
2
p
+
k
(
5
)
\mathrm{H_out}=(\mathrm{H}_{in}-1)*\text{s}-2\text{p}+\mathrm{k}\quad(5)
Hout=(Hin−1)∗s−2p+k(5)
综上所述,可以求得特征图Height和Width两个方向上进行转置卷积计算的结果:
H
o
u
t
=
(
H
i
n
−
1
)
×
s
t
r
i
d
e
[
0
]
−
2
×
p
a
d
d
i
n
g
[
0
]
+
k
e
r
n
e
l
_
s
i
z
e
[
0
]
W
o
u
t
=
(
W
i
n
−
1
)
×
s
t
r
i
d
e
[
1
]
−
2
×
p
a
d
d
i
n
g
[
1
]
+
k
e
r
n
e
l
_
s
i
z
e
[
1
]
H_{out}=(H_{in}−1)×stride[0]−2×padding[0]+kernel\_size[0]\\ W_{out}=(W_{in}−1)×stride[1]−2×padding[1]+kernel\_size[1]
Hout=(Hin−1)×stride[0]−2×padding[0]+kernel_size[0]Wout=(Win−1)×stride[1]−2×padding[1]+kernel_size[1]
由
公式
(
1
)
公式(1)
公式(1)和
公式
(
5
)
公式(5)
公式(5)可以看出, Conv2D
和 Conv2DTranspose
在输入和输出形状方面互为倒数。
5.4 举例说明
输入特征图
m
m
m :
H
i
n
=
3
H_{in}=3
Hin=3。
输入卷积核kernel:
K
=
(
3
,
3
)
,
S
=
2
,
p
=
1
K=(3,3),S=2, p=1
K=(3,3),S=2,p=1。
新的输入特征图
m
′
m^{\prime}
m′ :
H
i
n
′
=
3
+
(
3
−
1
)
∗
(
2
−
1
)
=
3
+
2
=
5
H_{in}^{\prime}=3+(3−1)∗(2−1)=3+2=5
Hin′=3+(3−1)∗(2−1)=3+2=5。注意加上padding之后才是7。
新的卷积核
K
′
K^{\prime}
K′:
K
′
=
K
,
S
′
=
1
,
p
′
=
3
−
1
−
1
=
1
K^{\prime}=K,S^{\prime}=1,p^{\prime}=3−1−1=1
K′=K,S′=1,p′=3−1−1=1。
转置卷积计算的最终结果:
H
o
u
t
=
(
3
−
1
)
∗
2
−
2
∗
1
+
3
=
5
\mathrm{H_{out}}=(3-1)*2-2*1+3=5
Hout=(3−1)∗2−2∗1+3=5。
5.5 Conv2DTranspose
示例
下图展示了转置卷积中不同s和p的情况:
|
|
|
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| s=1, p=0, k=3 | s=2, p=0, k=3 | s=2, p=1, k=3 |
6. Conv2DTranspose
中的步长stride
6.1 当步长stride=1
输入特征图(蓝色):
(
2
,
2
)
(2,2)
(2,2)。
卷积核:
K
=
3
,
s
t
r
i
d
e
(
S
)
=
1
,
p
a
d
d
i
n
g
=
0
K=3,stride(S)=1, padding=0
K=3,stride(S)=1,padding=0。
输出特征图(绿色):
(
4
,
4
)
(4,4)
(4,4)。
代入
公式
(
5
)
公式(5)
公式(5)中,可得:
H
o
u
t
=
(
H
i
n
−
1
)
∗
S
+
K
−
2
p
H
o
u
t
=
(
2
−
1
)
∗
1
+
3
−
2
∗
0
=
4
\mathrm{H_out}=(\mathrm{H}_{in}-1)*\text{S}+\mathrm{K}-2\text{p}\\ \mathrm{H_out}=(2-1)*1+3-2*0=4
Hout=(Hin−1)∗S+K−2pHout=(2−1)∗1+3−2∗0=4
6.2 当步长stride=2
输入特征图(蓝色):
(
3
,
3
)
(3,3)
(3,3)。
卷积核:
K
=
3
,
s
t
r
i
d
e
(
S
)
=
2
,
p
a
d
d
i
n
g
=
1
K=3,stride(S)=2, padding=1
K=3,stride(S)=2,padding=1。
输出特征图(绿色):
(
5
,
5
)
(5,5)
(5,5)。
代入
公式
(
5
)
公式(5)
公式(5)中,可得:
H
o
u
t
=
(
H
i
n
−
1
)
∗
S
+
K
−
2
p
H
o
u
t
=
(
3
−
1
)
∗
2
+
3
−
2
∗
1
=
4
\mathrm{H_out}=(\mathrm{H}_{in}-1)*\text{S}+\mathrm{K}-2\text{p}\\ \mathrm{H_out}=(3-1)*2+3-2*1=4
Hout=(Hin−1)∗S+K−2pHout=(3−1)∗2+3−2∗1=4
7. 棋盘效应(Checkerboard Artifacts)
棋盘效应(Checkerboard Artifacts)
卷积操作总结(三)—— 转置卷积棋盘效应产生原因及解决
Deconvolution and Checkerboard Artifacts
棋盘效应是由于转置卷积的“不均匀重叠”(Uneven overlap)的结果,使图像中某个部位的颜色比其他部位更深。
8. 总结
Conv2D
,特征图变换:
H o u t = H i n + 2 p − K S + 1 H_{out}=\frac{H_{in}+2p-K}S+1 Hout=SHin+2p−K+1
Conv2DTranspose
,特征图变换:
H o u t = ( H i n − 1 ) ∗ S − 2 p + K \mathrm{H_out}=(\mathrm{H}_{in}-1)*\text{S}-2\text{p}+\mathrm{K} Hout=(Hin−1)∗S−2p+K
Conv2D
和Conv2DTranspose
在输入和输出形状方面互为倒数。- 标准卷积(大图变小图,(5,5)到(3,3)),转置卷积(小图变大图,(3,3)到(5,5))。
- 第二步新卷积核的步长stride永远为1。
Conv2DTranspose()
函数参数中的步长stride是指第三步进行标准卷积操作时的stride。
四、相关经验
tf.layers.Conv2DTranspose
TensorFlow函数:tf.layers.Conv2DTranspose
以 tensorflow2 框架的Conv2DTranspose
为例,介绍转置卷积函数。
layers.Conv2DTranspose(
filters,
kernel_size,
strides=(1, 1),
padding='valid',
output_padding=None,
data_format=None,
dilation_rate=(1, 1),
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None,
**kwargs,
)
Docstring:
Transposed convolution layer (sometimes called Deconvolution).
参数解释
filters
:整数,输出空间的维数(即卷积中的滤波器数)。kernel_size
:一个元组或2个正整数的列表,指定过滤器的空间维度;可以是单个整数,以指定所有空间维度的相同值。strides
:一个元组或2个正整数的列表,指定卷积的步长;可以是单个整数,以指定所有空间维度的相同值。padding
:可以是一个"valid"或"same"(不区分大小写)。data_format
:一个字符串,可以是一个channels_last
(默认)、channels_first
,表示输入中维度的顺序。channels_last
对应于具有形状(batch, height, width, channels)的输入,而channels_first
对应于具有形状(batch, channels, height, width)的输入。dilation_rate
:。activation
:激活功能,将其设置为“None”以保持线性激活。use_bias
:Boolean,表示该层是否使用偏置。kernel_initializer
:卷积内核的初始化程序。bias_initializer
:偏置向量的初始化器,如果为None,将使用默认初始值设定项。kernel_regularizer
:卷积内核的可选正则化器。bias_regularizer
:偏置矢量的可选正则化器。activity_regularizer
:输出的可选正则化函数。kernel_constraint
:由Optimizer更新后应用于内核的可选投影函数(例如,用于实现层权重的范数约束或值约束);该函数必须将未投影的变量作为输入,并且必须返回投影变量(必须具有相同的形状);在进行异步分布式培训时,使用约束是不安全的。bias_constraint
:由Optimizer更新后应用于偏置的可选投影函数。trainable
:Boolean,如果为True,还将变量添加到图集合GraphKeys。TRAINABLE_VARIABLES中(请参阅参考资料tf.Variable
)。name
:字符串,图层的名称。
torch.nn.ConvTranspose2d
torch.nn.ConvTranspose2d
代码示例(TensorFlow)
#创建生成器
def make_generator_model():
model = tf.keras.Sequential()#创建模型实例
#第一层须指定维度 #batch无限制
model.add(layers.Dense(7*7*BATCH_SIZE, use_bias=False, input_shape=(100,)))#Desne第一层可以理解为全连接层输入,它的秩必须小于2
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Reshape((7,7,256)))
assert model.output_shape == (None,7,7,256)
#转化为7*7*128
model.add(layers.Conv2DTranspose(128,(5,5),strides=(1,1),padding='same',use_bias=False))
assert model.output_shape == (None,7,7,128)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
#转化为14*14*64
model.add(layers.Conv2DTranspose(64,(5,5),strides=(2,2),padding='same',use_bias=False))
assert model.output_shape == (None,14,14,64)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
#转化为28*28*1
model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False,activation='tanh'))
assert model.output_shape == (None, 28, 28, 1)
return model
代码示例(PyTorch)
import torch
import torch.nn as nn
def transposed_conv_official():
feature_map = torch.as_tensor([[1, 0],
[2, 1]], dtype=torch.float32).reshape([1, 1, 2, 2])
print(feature_map)
trans_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1,
kernel_size=3, stride=1, bias=False)
trans_conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 1],
[0, 1, 1],
[1, 0, 0]], dtype=torch.float32).reshape([1, 1, 3, 3])})
print(trans_conv.weight)
output = trans_conv(feature_map)
print(output)
def transposed_conv_self():
feature_map = torch.as_tensor([[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0],
[0, 0, 2, 1, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0]], dtype=torch.float32).reshape([1, 1, 6, 6])
print(feature_map)
conv = nn.Conv2d(in_channels=1, out_channels=1,
kernel_size=3, stride=1, bias=False)
conv.load_state_dict({"weight": torch.as_tensor([[0, 0, 1],
[1, 1, 0],
[1, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])})
print(conv.weight)
output = conv(feature_map)
print(output)
def main():
transposed_conv_official()
print("---------------")
transposed_conv_self()
if __name__ == '__main__':
main()
输出结果
tensor([[[[1., 0.],
[2., 1.]]]])
Parameter containing:
tensor([[[[1., 0., 1.],
[0., 1., 1.],
[1., 0., 0.]]]], requires_grad=True)
tensor([[[[1., 0., 1., 0.],
[2., 2., 3., 1.],
[1., 2., 3., 1.],
[2., 1., 0., 0.]]]], grad_fn=<SlowConvTranspose2DBackward>)
---------------
tensor([[[[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 0.],
[0., 0., 2., 1., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]]]])
Parameter containing:
tensor([[[[0., 0., 1.],
[1., 1., 0.],
[1., 0., 1.]]]], requires_grad=True)
tensor([[[[1., 0., 1., 0.],
[2., 2., 3., 1.],
[1., 2., 3., 1.],
[2., 1., 0., 0.]]]], grad_fn=<ThnnConv2DBackward>)
Process finished with exit code 0
DCGAN
论文:Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks