深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)
前言
原先是将这篇笔记和上一篇笔记合起来写的,但是由于内容很多,于是将卷积与转置卷积分作两篇。转置卷积(transposed convolution)是一种上采样技术,操作过程是卷积的反过程,也被称作反卷积(deconvolution),但它的操作结果不是卷积的逆。它也可以通过卷积操作来实现,只是需要将卷积核旋转180度。它主要应用在图像分割和超分辨率等任务中。笔记主要包括转置卷积操作和Pytorch转置卷积层。本笔记的知识框架主要来源于深度之眼,并依此作了内容的丰富拓展,拓展内容主要源自对torch文档的翻译,对孙玉林等著的PyTorch深度学习入门与实战的参考和自己的粗浅理解,所用数据来源于网络。发现有人在其他平台照搬笔者笔记,不仅不注明出处,有甚者更将其作为收费文章,因此笔者将在文中任意位置插入识别标志。
笔记是笔者根据自己理解一字一字打上去的,还要到处找合适的图片,有时为了便于理解还要修图,原创不易,转载请注明出处,文中笔者哪怕是引图也注明了出处的
结果可视化见:深度之眼Pytorch打卡(十):Pytorch数据预处理——数据统一与数据增强(上)
卷积操作见:深度之眼Pytorch打卡(十四):Pytorch卷积神经网络部件——卷积操作与卷积层、转置卷积操作与转置卷积层(反卷积)(对卷积转置卷积细致动图分析)
转置卷积操作(transposed convolution)
转置卷积有时被称作反卷积,结果上它并不是卷积的逆,但操作上的确是卷积的反过程。它是一种上采样技术,可以理解成把输入尺寸放大的技术,被广泛的应用在图像分割,超分辨率等应用中。与传统的上采样技术,如线性插值,双线性插值等方法相比,转置卷积是一种需要训练,可学习的方法。图1所示的是一种著名的图像分割网络架构,即编码器-解码器网络,其编码部分用CNN架构,解码部分便使用的转置卷积1。图源
现假设有一输入尺寸为2x2,转置卷积核大小为2x2,需要上采样到3x3,如图2所示,图改自图源。将输入左上角的值0
与内核相乘,得到的2x2的张量放输出的左上角(红框区域),将输入右上角的值1
与内核相乘,得到的2x2的张量放输出的右上角(蓝框区域),将输入左下角的值2
与内核相乘,得到的2x2的张量放输出的左下角(黄框区域),将输入右下角的值3
与内核相乘,得到的2x2的张量放输出的右下角(灰区域),重叠部分的值相加就得到了转置卷积的结果。
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
上述转置卷积过程可以通过图3清晰表达出来,图源。观其过程,输入的每一个神经元都与输出的每4个神经元相连,并且共享一个卷积核,操作上的确是卷积反过来。从图1中解码部分示意图中也可以瞥见一斑。
所以,转置卷积的输出尺寸公式,就应是卷积输出公式的反函数,如式(1)。输出尺寸w1
、输入尺寸w
,卷积核大小f
,缩减大小p
和步长s
。padding
变成了减,故有padding
时应该将输入向中心缩小。s(w-1)
,可以知道当有stride
时,是将输入做膨胀,与dilation
类似。两者都是卷积处操作的反过程。
上述的运算过程可以通过卷积运算来完成,将内核旋转180度,然后与输入做卷积,保证输入与内核至少有一个元素相交,不相交部分输入补零。过程如图4所示,图改自图源。
Pytorch转置卷积层
Pytorch的转置卷积层有三个,分别是nn.ConvTranspose1d()、nn.ConvTranspose2d()和nn.ConvTranspose3d()。三者的操作过程与参数都是相同的,所以以下也只单独学习最最常用的nn.ConvTranspose2d()
。
CLASS torch.nn.ConvTranspose2d(in_channels: int,
out_channels: int,
kernel_size: Union[int, Tuple[int, int]],
stride: Union[int, Tuple[int, int]] = 1,
padding: Union[int, Tuple[int, int]] = 0,
output_padding: Union[int, Tuple[int, int]] = 0,
groups: int = 1,
bias: bool = True,
dilation: int = 1,
padding_mode: str = 'zeros')
容易发现转置卷积层的参数与卷积层的参数很多是一致的,没有差异的参数将不赘述,详见此文。 以下只列出有差异的参数。用1x1
的kernel
时,可以由输入输出之间的关系,来间接反应这些参数的影响,以此来验证式(1)处的分析。
代码:
import torch
import torch.nn as nn
in_tensor = torch.tensor([[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5]], dtype=torch.float)
in_tensor = torch.reshape(in_tensor, [1, 1, 5, 5]) # 转到四维
print(in_tensor)
deconv1 = nn.ConvTranspose2d(1, 1, (1, 1), bias=False, stride=1)
deconv1.weight.data = torch.tensor([[[[1]]]], dtype=torch.float)
out_tensor = deconv1(in_tensor)
print(out_tensor)
# CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112
stride: 当stride=1
、padding=0
、kernel=1*1
并且权值为1
时,由图5方法可得输出应该和输入一模一样。代码验证后发现的确如此。维持其他参数不变,让stride=2
时,可以发现输出是在输入的每两个元素间都插入了一个0
的结果,此时两个相邻非零元素的间距是2
。维持其他参数不变,让stride=3
时,可以发现输出是在输入的每两个元素间都插入了两个0
的结果,此时两个相邻非零元素的间距是3
…。可见转置卷积层的stride
,就等价于在input
每两个元素之间插入(stride-1)
个0
,即将输入膨胀。
结果:
# input
tensor([[[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]]]])
# stride = 1 padding = 0
tensor([[[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]]]], grad_fn=<SlowConvTranspose2DBackward>)
# stride = 2 padding = 0
tensor([[[[1., 0., 2., 0., 3., 0., 4., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5.]]]],
grad_fn=<SlowConvTranspose2DBackward>)
# stride = 3 padding = 0
tensor([[[[1., 0., 0., 2., 0., 0., 3., 0., 0., 4., 0., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 0., 2., 0., 0., 3., 0., 0., 4., 0., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 0., 2., 0., 0., 3., 0., 0., 4., 0., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 0., 2., 0., 0., 3., 0., 0., 4., 0., 0., 5.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 0., 2., 0., 0., 3., 0., 0., 4., 0., 0., 5.]]]],
grad_fn=<SlowConvTranspose2DBackward>)
# CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112
padding: 与上面同样的输入和卷积核,让stride = 1
,修改padding
,padding=1
时,输出是输入上下少一行,左右少一列的结果。padding=2
时,输出是输入上下少两行行,左右少两行的结果。可见转置卷积层的padding
,就等价于在input
上,边缘处上下各去掉padding
行,左右各去掉padding
列的结果。即将输入向中心缩小。
# input
tensor([[[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]]]])
# padding = 1 stride = 1
tensor([[[[2., 3., 4.],
[2., 3., 4.],
[2., 3., 4.]]]], grad_fn=<SlowConvTranspose2DBackward>)
# padding = 2 stride = 1
tensor([[[[3.]]]], grad_fn=<SlowConvTranspose2DBackward>)
# CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112
output_padding: 默认在输出的最右边拓展output_padding
列,在输出的最下边拓展output_padding
行。output_padding
必须比stride
或者dilation
小,否则会报错:RuntimeError: output padding must be smaller than either stride or dilation.
deconv1 = nn.ConvTranspose2d(1, 1, (1, 1), bias=False, stride=2, padding=0, output_padding=1)
结果:
# input
tensor([[[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]]]])
# stride=2, padding=0, output_padding=1
tensor([[[[1., 0., 2., 0., 3., 0., 4., 0., 5., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 2., 0., 3., 0., 4., 0., 5., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]],
grad_fn=<SlowConvTranspose2DBackward>)
# CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112
加上dilation,output_padding
后的转置卷积输出尺寸公式如下所示。其中,W
指输出的宽,H
指输出的高。
H out =(H in−1)×stride[0]−2×padding[0]+dilation[0]×(kernel_size[0]−1)+output_padding[0]+1
W out =(W in−1)×stride[1]−2×padding[1]+dilation[1]×(kernel_size[1]−1)+output_padding[1]+1
转置卷积同样需要转矩阵乘法来实现,如图5所示,其改自国外一篇文章:Up-sampling with Transposed Convolution。与卷积一样,转置卷积实现的关键也在于产生稀疏矩阵(sparse matrix),将稀疏矩阵与输入内积的结果resize
就可以得到转置卷积的输出。
稀疏矩阵D(sparse matrix D) 也是由被flatten
的转置卷积核叠放而成的,与卷积层那里有相似之处。剔除卷积核中的某些元素,让其和输入尺寸一致,然后再fatten
成一维张量。由图4的方法,我们可以知道应该保留
转置卷积核的哪些值,该剔除
哪些值,如图6所示,其改自国外一篇文章:Up-sampling with Transposed Convolution。由于输出是4X4
,故应该叠16
层。
回忆卷积层处的稀疏矩阵C,当转置卷积与卷积的输入输出尺寸是互换的,并且相同卷积核时,转置卷积形成的稀疏矩阵D与卷积形成的稀疏矩阵C是转置的关系,不同卷积核时,如果只看元素位置,不看元素大小,也是转置的关系,这也是转置卷积名称的来源,如图7所示,其改自国外一篇文章:Up-sampling with Transposed Convolution。所以我们可以用产生稀疏矩阵C的方法,来间接产生稀疏矩阵D,以简化产生过程。
在设计时,一般我们是知晓输入和输出尺寸的,而我们需要推算的应该是stride
、padding
和卷积核的大小。以上一篇笔记中卷积得到的438x438的特征图作为输入。输出440x440的图像,由于只放大了一点点,故stride=1
,在不用空洞,不设dilation,output_padding
的情况下,则440=437-2*padding+kernel_size
,如果kernel_size=3
,则padding=0
,若kernel_size=4
,则padding=1
。
取stride=1
,padding=0
,kernel_size=4
这正好是先前卷积过程的反过程。
代码:transform_inverse()函数定义见此文
import torch
import torch.nn as nn
from PIL import Image
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
from tools.transform_inverse import transform_inverse
pil_img = Image.open('data/lenna.jpg').convert('L')
img = transforms.ToTensor()(pil_img)
c = img.size()[0]
h = img.size()[1]
w = img.size()[2]
input_img = torch.reshape(img, [1, c, h, w]) # 转换成4维,[batch_size, c, h, w]
print(input_img.size())
# 卷积层
conv1 = nn.Conv2d(1, 2, (3, 3), bias=False) # 实例化
conv1.weight.data[0] = torch.tensor([[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]]) # 水平边缘的sobel算子
conv1.weight.data[1] = torch.tensor([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]) # 竖直边缘的sobel算子
# print(conv1.weight.data)
out_img = conv1(input_img) # 两张特征图
print(out_img.size())
out_img = torch.squeeze(out_img, dim=0)
out_img_ = out_img
out_img = out_img[0]+out_img[1] # 水平边缘加竖直边缘等于全部边缘
out_pil_img = transform_inverse(torch.reshape(out_img, [1, out_img.size()[0], out_img.size()[1]]), None)
plt.figure(0)
ax = plt.subplot(2, 2, 1)
ax.set_title('conv input picture')
ax.imshow(pil_img, cmap='gray')
ax = plt.subplot(2, 2, 2)
ax.set_title('conv output picture')
ax.imshow(out_pil_img, cmap='gray')
# CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112
# 转置卷积层
deconv1 = nn.ConvTranspose2d(2, 1, (3, 3), bias=False)
deconv1.weight.data[0] = torch.tensor([[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]]) # 水平边缘的sobel算子
deconv1.weight.data[1] = torch.tensor([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]) # 竖直边缘的sobel算子
de_out = deconv1(torch.reshape(out_img_, [1, out_img_.size()[0],
out_img_.size()[1], out_img_.size()[2]]))
de_out = torch.squeeze(de_out, dim=0)
print(de_out.size())
de_out_pil_img = transform_inverse(de_out, None)
ax = plt.subplot(2, 2, 3)
ax.set_title('deconv input picture')
ax.imshow(out_pil_img, cmap='gray')
ax = plt.subplot(2, 2, 4)
ax.set_title('deconv output picture')
ax.imshow(de_out_pil_img, cmap='gray')
plt.show()
# CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112
结果:经过上采样,细节更丰富了,但并没有恢复原图,如图8所示。可见转置卷积只可以被看做是卷积的反过程,而不是卷积的逆。
-
https://towardsdatascience.com/transposed-convolution-demystified-84ca81b4baba ↩︎