深度之眼Pytorch打卡(十四):Pytorch卷积神经网络部件——卷积操作与卷积层(对卷积操作超细致动图分析,卷积转矩阵乘法分析)

前言


  人每天处理的信号中,有超过70%的是视觉信号,所以视觉问题,包括分类,检测,分割、风格转换等等占了深度学习任务中的很大部分。而卷积神经网络是计算机视觉领域当之无愧的霸主。卷积神经网络是稀疏连接,并且权值共享的,参数比全连接要少非常多,所以完完全全可以用图像全像素作为输入,并且它比全连接网络更容易训练,且能做得更深。另外,卷积神经网络,浅层卷积提取简单特征,深层卷积提取复杂特征,还有感受野等设计都在一定程度上受大脑视觉皮层结构的启发1 ,所以它比较适合视觉任务。这篇笔记主要学习卷积神经网络的核心——卷积操作和Pytorch的卷积层。本笔记的知识框架主要来源于深度之眼,并依此作了内容的丰富拓展,拓展内容主要源自对torch文档的翻译,对孙玉林等著的PyTorch深度学习入门与实战的参考和自己的粗浅理解,所用数据来源于网络。发现有人在其他平台照搬笔者笔记,不仅不注明出处,有甚者更将其作为收费文章,因此笔者将在文中任意位置插入识别标志。

  张量转图片可视化见:深度之眼Pytorch打卡(十):Pytorch数据预处理——数据统一与数据增强(上)
  全连接网络部件见:深度之眼Pytorch打卡(十三):Pytorch全连接神经网络部件——线性层、非线性激活层与Dropout层(即全连接层、常用激活函数与失活 )


卷进操作(convolution)


  第一次接触到卷积是在信号与系统中,把一个信号送入一个系统,在时域内输出信号就是输入信号与系统函数的卷积。它其实那就是一个相乘和相加操作,即线性运算,那里的卷积有四个步骤:翻转,移位,相乘和相加。每一次的移位都会有一个相乘求和结果,它的大小可以衡量此位置翻转后的系统函数与输入信号的相似性,数值越大,在该位置信号与系统函数的相似性就越高。深度学习或者是图像处理里的卷积,类似于信号处理里相关,没有翻转的操作,只有移位和相乘相加,它的输出结果是卷积核或者滤波器与其覆盖区域的相似性,激活值越大,代表覆盖区域与卷积核或者滤波器有越相近的特征。Pytorch的卷积操作,有一维卷积,二维卷积和三维卷积和空洞卷积等。其中二维卷积,尤其是二维卷积中的多通道卷积最为常用,因为图像本身就是二维多通道的

  • 一维卷积

  一维卷积示意图如图1所示,其摘自国外的一篇文章:Reading Minds with Deep Learning,卷积核在一个维度上,即一条线移动。图中非常明确的展现了一维卷积的过程——移位,相乘,相加。每次移位的元素个数称为步长stride,图中stride=1,由于没有拓展两端即Padding=0,所以卷积后的输出长度变短了。

在这里插入图片描述
  输出尺寸w1与输入尺寸w,卷积核大小f,拓展大小p和步长s之间的关系如式(1),图中输入尺度w=6,卷积核尺度f=3,步长s=1,拓展宽度p=0,带入即可的w1=4
一维卷积

图1.一维卷积示意图
  • 二维单通道卷积

  二维单通道的示意图如图2所示,其摘自国外的一篇文章:Convolutional Neural Networks - Basics,卷积核在两个维度上,即平面上移动,图中用的3*3的卷积核,熟悉图像处理的朋友应该能发现它是检测水平线的Sobel算子。图中展现的也是二维卷积核在二维输入上移位、相乘相加的过程。步长、拓展和输出计算同一维卷积,由图可见,其stride=1Padding=0
二维单通道卷积

图2.二维单通道卷积
  • 二维多通道卷积

  二维多通道卷积示意图如图3所示,其摘自这里,虽然它的输入和卷积核都是多维的,但它只是二维单通道卷积的拓展,本质上还是二维卷积。从图中可以看出输入通道数为3,所以对应的卷积核通道数也为3,即两者应该同深度。图中只有1个卷积核,所以输出的通道数是1,或者输出特征图数是1,又或者输出深度是1。对于这样的多通道卷积,它的操作过程与单通道一样,只是输出是卷积核各通道分别与输入的对应通道做卷积,然后将各通道卷积输出相加,再加上偏置的结果。 图中stride=1Padding=1

二维三(多)通道卷积

图3.二维三(多)通道卷积
  • 三维单通道卷积

  三维单通道卷积示意图如图4所示,其摘自国外的一篇博客:3D Convolutional Networks for Traffic Forecasting。三维卷积平常见的很少,卷积核在一个三维空间上移动,每移动一次都进行一次相乘相加操作。它与二维多通道卷积很像,尤其是它的输入和卷积核深度都是3时连运算过程本质上都是一样,但二维卷积不论有多少个通道,卷积核都只是在一个平面上移动,而不是三维卷积中的空间上移动。
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)

三维卷积

图4.三维卷积
  • 空洞卷积

  空洞卷积示意图如图5所示,其摘自国外的一篇博客:An Introduction to different Types of Convolutions in Deep Learning。空洞卷积的卷积核中某些特定位置值被置成了0,即空洞。空洞卷积可以提升感受野,毕竟图中用3*3卷积核的参数就有了5*5大小卷积核的感受野,虽然可能会丢失一些细节信息,但是每一个输出的确关联到了输入中更大的范围。空洞卷积多应用与分割任务。

空洞卷积

图5.空洞卷积
  • 稀疏连接与权值共享

  卷积神经网络稀疏连接示意图如图6所示,由卷积过程,我们可以知道,一个输出神经元只与卷积核覆盖区域的若干输入神经元相连(有运算关系),而不是像全连接网络那样与全部的输入神经元相连,所以卷积神经网络神经元与神经元之间的连接是稀疏的。另外,由于特征的位置不变性,所以检测同一个特征时无论它出现在图像或者特征图的哪一个位置,用来检测提取它的卷积核都是一样的,所以输出同一幅特征图时是共用一组权值的,即同一个卷积核。这两个特点使得卷积神经网络可以做得比较深,而且更容易训练。
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)

卷积神经网络的稀疏连接或者局部连接

图6.稀疏连接或者局部连接

Pytorch卷积层(conv layer)


  Pytorch的卷积层主要有nn.Conv1d()、nn.Conv2d(),nn.Conv3d()等,它们的操作与参数都是相同的,所以下面只单独学习最最常用的二维卷积nn.Conv2d()

CLASS torch.nn.Conv2d(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, 
                      dilation: Union[int, Tuple[int, int]] = 1, 
                      groups: int = 1, 
                      bias: bool = True,
                      padding_mode: str = 'zeros')

# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)

  in_channels: 输入通道数,即输入图像或者特征图的深度,如RGB图像是3,灰度图像是1,和图3中输入深度3。它决定了卷积核的通道数或者深度

  out_channels: 输出通道数,即输出特征图的个数或者深度,如图3中输出通道数为1它决定了卷积核的个数,由权值共享可知一个特征图对应一个卷积核。

  kernel_size:卷积核尺寸,卷积核宽高

  padding: 拓展边缘的宽度,二元组分别代表拓展的宽度和高度值。主要用于维持卷积前后的尺寸不变或者加强边缘信息的利用。如图7,通过padding来维持卷积前后尺寸不变,其摘自国外的一篇博客:An Introduction to different Types of Convolutions in Deep Learning
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)

输入输出尺度不变

图7.卷积输入输出尺度不变

  dilation:膨胀,用于控制卷积核两个元素之间的间隔,默认为1,代表正常的卷积。为2或者大于2时便是如图5所示的那种空洞卷积。由于在卷积核中间填充了空洞,等效于卷积核变大了,所以输出尺寸表达式(1)中应该在kernal_size处做修改,得到式(2)和(3)。
卷积输出尺寸公式
  输出输入,一般都会让宽高一样,而且两个维度上的padding,stride,kernal_size等一般也是相同的,即式(2)与(3)的输出值相同。当前面所述有一个不同时,式(2)与式(3)的值都会有不同。

  groups: 分组卷积,默认为1,即不分组。经典的卷积神经网络AlexNet的groups就是2,如图8所示,图源。当时Hinton他们是为了充分利用他们手上的两块GPU才做的分组,现在分组卷积变成了一种方法。分组卷积常用在轻量型高效网络中,因为它用少量的参数量和运算量就能生成大量的特征图。
AlexNet

图8.AlexNet

   bias: 是否要偏置。
  padding_mode: 填充模式,为字符串,包括填充0,镜像,复制等。'zeros', 'reflect', 'replicate' or 'circular'

  • nn.Conv2d()实现

   nn.Conv1d()、nn.Conv2d(),nn.Conv3d()都继承于_ConvNd,而_ConvNd又继承于Module,所以它们像上一篇笔记中所述的线性层那样,是Pytorch神经网络模型的模块,且是不可再分的最小模块。类Conv2d()中,也是主要由__init__forward方法构成,源码如下。

class Conv2d(_ConvNd):

    def __init__(self, in_channels, out_channels, kernel_size, stride=1,
                 padding=0, dilation=1, groups=1,
                 bias=True, padding_mode='zeros'):
        kernel_size = _pair(kernel_size)
        stride = _pair(stride)
        padding = _pair(padding)
        dilation = _pair(dilation)
        super(Conv2d, self).__init__(
            in_channels, out_channels, kernel_size, stride, padding, dilation,
            False, _pair(0), groups, bias, padding_mode)

    def conv2d_forward(self, input, weight):
        if self.padding_mode == 'circular':
            expanded_padding = ((self.padding[1] + 1) // 2, self.padding[1] // 2,
                                (self.padding[0] + 1) // 2, self.padding[0] // 2)
            return F.conv2d(F.pad(input, expanded_padding, mode='circular'),
                            weight, self.bias, self.stride,
                            _pair(0), self.dilation, self.groups)
        return F.conv2d(input, weight, self.bias, self.stride,
                        self.padding, self.dilation, self.groups)

    def forward(self, input):
        return self.conv2d_forward(input, self.weight)
        
# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)

  F.conv2d不能直接进入,它是由C++写的,我们只能看到接口,源码大概在这里。笔者没有理清源码的思路,所以另查了资料来间接搞清楚实现过程。

  卷积的实现并不是像上文说的那样移位、相乘再相加,那样太费时了,而是通过矩阵乘法的方式高效完成的,如图9所示,图改自国外的一篇文章:A Comprehensive Introduction to Different Types of Convolutions in Deep Learning。把卷积核转成稀疏矩阵C,再把输入flatten成一维张量,两者内积再resize便得到了卷积结果。

二维卷积的矩阵实现

图9.二维卷积的矩阵实现

  
  稀疏矩阵C(Sparse matrix C)可以理解成是由若干被flatten成一维张量的卷积核(kernel)移位叠放而成的,叠放层数等于卷积输出的元素个数,如图9中的2X2=4。在flatten卷积核前,要先把它拓展到与input相同的尺寸,往右边和下边拓展,用零填充。卷积核填充与flatten过程,如图10所示。
卷积核转稀疏矩阵

图10.卷积核转稀疏矩阵

  
  把卷积核kernel填充并且flatten后变成1x16的张量kernel_flatten稀疏矩阵C的第一行,将kernel_flatten向右移0*stride位后放入,第二行,将kernel_flatten向右移1*stride位后放入,前端空白处补0,后端超过的去掉。以此往下…,第n行,将kernel_flatten向右移(n-1)*stride位后放入。当满足条件:移位步数(n-1)*stride加上卷积核的宽Wk等于Input的宽Wi的时候(即卷积核在滑动过程中换行点在稀疏矩阵中的反映),则下一行,将kernel_flatten移位s*k*Wi位后放入,其中s为在高维度上的步长,k满足上述条件的次数,并以该行为第一行重复上述步骤。当行数总和等于输出元素个数时,稀疏矩阵C就形成了。(以上是笔者个人理解,有不对之处,望指出)

  通过上述方法,很容易得到图11中的稀疏矩阵C,并且可以透过C观察出宽和高维度上的stride都等于1。稀疏矩阵C中的每一行与Input的内积,都一一对应卷积操作中的一个移位相乘相加。
卷积核转稀疏矩阵

图11.卷积核转稀疏矩阵
  • nn.Conv2d()使用

  图像处理中的很多操作都是通过卷积运算来完成的,以下用一个简单的边缘检测算子sobel来学习卷积层的使用,检测水平和竖直方向边缘算子见图12。将这两个算子填入卷积层的权值张量中,不设置偏置,代码如下。

sobel算子

图12.sobel算子

  main.py

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)
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(1, 2, 1)
ax.set_title('input picture')
ax.imshow(pil_img, cmap='gray')
ax = plt.subplot(1, 2, 2)
ax.set_title('output picture')
ax.imshow(out_pil_img, cmap='gray')
plt.show()

# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)

  transform_inverse.py,该函数详细解释见这篇笔记

import torch
import numpy as np
from PIL import Image
from torchvision import transforms


def transform_inverse(img, transform):
    # 将tensor转换成pil数据
    if 'Normalize' in str(transform):
        Normalize_trans = list(filter(lambda x: isinstance(x, transforms.Normalize), transform.transforms))
        m = torch.tensor(Normalize_trans[0].mean, dtype=img.dtype, device=img.device)
        s = torch.tensor(Normalize_trans[0].std, dtype=img.dtype, device=img.device)
        img.mul_(torch.reshape(s, [-1, 1, 1])).add_(torch.reshape(m, [-1, 1, 1]))       # 需要调整形状才能通道对应
    img = torch.transpose(img, dim0=0, dim1=1)                                          # C H W ->H C W
    img = torch.transpose(img, dim0=1, dim1=2)                                          # H C W ->H W C
    if img.requires_grad:
        img = img.detach().numpy()
    else:
        img = np.array(img)*255                                                             # 去归一化
    if img.shape[2] == 3:
        img = Image.fromarray(img.astype('uint8')).convert('RGB')                       # 转换成PIL RGB图像
    elif img.shape[2] == 1:
        img = Image.fromarray(img.astype('uint8').squeeze(), 'L')                     # (1, H, W)->(H, W)
    else:
        print('Invalid img format')
    return img

# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)

  结果:

torch.Size([1, 1, 440, 440])
torch.Size([1, 2, 438, 438])

卷积前后的lenna图像

图13.卷积前后的lenna图像

参考


  1. [16] Hubel DH, Wiesel TN. Receptive fields, binocular interaction and functional architecture in the cat’s visual cortex[J]. Journal of Physiology,1962,160(1): 106-154. ↩︎

猜你喜欢

转载自blog.csdn.net/sinat_35907936/article/details/107833112