欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

程序员文章站 2022-07-15 22:27:37
...

前言


  原先是将这篇笔记和上一篇笔记合起来写的,但是由于内容很多,于是将卷积与转置卷积分作两篇。转置卷积(transposed convolution)是一种上采样技术,操作过程是卷积的反过程,也被称作反卷积(deconvolution),但它的操作结果不是卷积的逆。它也可以通过卷积操作来实现,只是需要将卷积核旋转180度。它主要应用在图像分割和超分辨率等任务中。笔记主要包括转置卷积操作和Pytorch转置卷积层。本笔记的知识框架主要来源于深度之眼,并依此作了内容的丰富拓展,拓展内容主要源自对torch文档的翻译,对孙玉林等著的PyTorch深度学习入门与实战的参考和自己的粗浅理解,所用数据来源于网络。发现有人在其他平台照搬笔者笔记,不仅不注明出处,有甚者更将其作为收费文章,因此笔者将在文中任意位置插入识别标志。

   笔记是笔者根据自己理解一字一字打上去的,还要到处找合适的图片,有时为了便于理解还要修图,原创不易,转载请注明出处,文中笔者哪怕是引图也注明了出处的

  结果可视化见:深度之眼Pytorch打卡(十):Pytorch数据预处理——数据统一与数据增强(上)
  卷积操作见:深度之眼Pytorch打卡(十四):Pytorch卷积神经网络部件——卷积操作与卷积层、转置卷积操作与转置卷积层(反卷积)(对卷积转置卷积细致动图分析)


转置卷积操作(transposed convolution)


  • 转置卷积

  转置卷积有时被称作反卷积,结果上它并不是卷积的逆,但操作上的确是卷积的反过程。它是一种上采样技术,可以理解成把输入尺寸放大的技术,被广泛的应用在图像分割,超分辨率等应用中。与传统的上采样技术,如线性插值,双线性插值等方法相比,转置卷积是一种需要训练,可学习的方法。图1所示的是一种著名的图像分割网络架构,即编码器-解码器网络,其编码部分用CNN架构,解码部分便使用的转置卷积1图源

深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

图1.图像分割的下采样和上采样网络
  • 转置卷积操作

  现假设有一输入尺寸为2x2,转置卷积核大小为2x2,需要上采样到3x3,如图2所示,图改自图源。将输入左上角的值0与内核相乘,得到的2x2的张量放输出的左上角(红框区域),将输入右上角的值1与内核相乘,得到的2x2的张量放输出的右上角(蓝框区域),将输入左下角的值2与内核相乘,得到的2x2的张量放输出的左下角(黄框区域),将输入右下角的值3与内核相乘,得到的2x2的张量放输出的右下角(灰区域),重叠部分的值相加就得到了转置卷积的结果。
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

图2.输入、内核与输出尺寸

  上述转置卷积过程可以通过图3清晰表达出来,图源。观其过程,输入的每一个神经元都与输出的每4个神经元相连,并且共享一个卷积核,操作上的确是卷积反过来。从图1中解码部分示意图中也可以瞥见一斑。

深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

图3.转置卷积过程

  所以,转置卷积的输出尺寸公式,就应是卷积输出公式反函数,如式(1)。输出尺寸w1、输入尺寸w,卷积核大小f,缩减大小p和步长spadding变成了,故有padding时应该将输入向中心缩小。s(w-1),可以知道当有stride时,是将输入做膨胀,与dilation类似。两者都是卷积处操作的反过程。

深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

  上述的运算过程可以通过卷积运算来完成,将内核旋转180度,然后与输入做卷积,保证输入与内核至少有一个元素相交,不相交部分输入补零。过程如图4所示,图改自图源

深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

图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')

  容易发现转置卷积层的参数与卷积层的参数很多是一致的,没有差异的参数将不赘述,详见此文。 以下只列出有差异的参数。用1x1kernel时,可以由输入输出之间的关系,来间接反应这些参数的影响,以此来验证式(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=1padding=0kernel=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,修改paddingpadding=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

  • nn.ConvTranspose2d()实现

  转置卷积同样需要转矩阵乘法来实现,如图5所示,其改自国外一篇文章:Up-sampling with Transposed Convolution。与卷积一样,转置卷积实现的关键也在于产生稀疏矩阵(sparse matrix),将稀疏矩阵与输入内积的结果resize就可以得到转置卷积的输出。

深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

图5.转置卷积转矩阵乘法
  

  稀疏矩阵D(sparse matrix D) 也是由被flatten的转置卷积核叠放而成的,与卷积层那里有相似之处。剔除卷积核中的某些元素,让其和输入尺寸一致,然后再fatten成一维张量。由图4的方法,我们可以知道应该保留转置卷积核的哪些值,该剔除哪些值,如图6所示,其改自国外一篇文章:Up-sampling with Transposed Convolution。由于输出是4X4,故应该叠16层。
深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

图6.转置卷积稀疏矩阵

  回忆卷积层处的稀疏矩阵C,当转置卷积与卷积的输入输出尺寸是互换的,并且相同卷积核时,转置卷积形成的稀疏矩阵D与卷积形成的稀疏矩阵C是转置的关系,不同卷积核时,如果只看元素位置,不看元素大小,也是转置的关系,这也是转置卷积名称的来源,如图7所示,其改自国外一篇文章:Up-sampling with Transposed Convolution。所以我们可以用产生稀疏矩阵C的方法,来间接产生稀疏矩阵D,以简化产生过程。

深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)

图7.转置卷积稀疏矩阵与卷积稀疏矩阵的关系
  • nn.ConvTranspose2d()应用

  在设计时,一般我们是知晓输入和输出尺寸的,而我们需要推算的应该是stridepadding和卷积核的大小。以上一篇笔记中卷积得到的438x438的特征图作为输入。输出440x440的图像,由于只放大了一点点,故stride=1,在不用空洞,不设dilation,output_padding的情况下,则440=437-2*padding+kernel_size,如果kernel_size=3,则padding=0,若kernel_size=4,则padding=1

  取stride=1padding=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所示。可见转置卷积只可以被看做是卷积的反过程,而不是卷积的逆。
深度之眼Pytorch打卡(十五):Pytorch卷积神经网络部件——转置卷积操作与转置卷积层(对转置卷积操作全网最细致分析,转置卷积的stride与padding,转置与反卷积名称论证)


  1. https://towardsdatascience.com/transposed-convolution-demystified-84ca81b4baba ↩︎