卷积神经网络是使用卷积层的一组神经网络。在一个成熟的CNN中,往往会涉及到卷积层、池化层、线性层(全连接层)以及各类激活函数。因此,在构筑卷积网络时,需从整体全部层的需求来进行考虑。

1 二维卷积层nn.Conv2d

在PyTorch中,卷积层作为构成神经网络的层,自然是nn.Module模块下的类。

按照输入数据的维度,卷积层可以分为三类:处理时序数据(samples,channels,length)的一维卷积(Conv1d),处理图像数据的(samples,height,width,channels)的二维卷积(Conv2d),以及处理视频数据(samples,frames,height,width,channels)的三维卷积(Conv3d)。时序数据是存在时间维度、受时间影响的三维数据,常被用于循环神经网络中,但卷积也可以处理这种数据。视频数据则是由多张图像在时间轴上排列构成的,因此视频数据可以被看做是图像数据的序列。视频数据中的frames是“帧数”,即一个视频中图像的总数量。在之后的课程中,我们会就视频数据及其处理展开详细说明。

按照卷积的操作和效果,又可分为普通卷积、转置卷积、延迟初始化的lazyConv等等。最常用的是处理图像的普通卷积nn.Conv2d。其类及其包含的超参数参数内容如下(注意Conv2d是大写):

CLASS torch.nn.Conv2d (in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,groups=1, bias=True, padding_mode=‘zeros’)

可以看到,除了之前的扫描操作之外,还有许多未知的参数。我们可以通过解析Conv2d的参数来说明卷积操作中的许多细节。需要说明的是,参数groups和dilation分别代表着分组卷积(Grouped Convolution)与膨胀卷积(Dilated Convolution),属于卷积神经网络的入门级操作,但却比我们现在学习的内容更加复杂。之后,我们会详细描述这两种卷积网络的原理与流程,现在,我们还是专注在普通卷积上。

1.1 卷积核尺寸kernel_size

kernel_size是我们第一个需要讲解的参数,但同时也是最简单的参数。

卷积核的高和宽一般用KHK_{H}KH​和KWK_{W}KW​表示。在许多其他资料或教材中,如果把卷积核称为filter过滤器,也可能使用字母FHF_{H}FH​和FWF_{W}FW​来表示。卷积核的对整个卷积网络的参数量有很大的影响。在之前的例子中,我们使用3X3结构的卷积核,每个卷积核就会携带9个参数(权重),如果我们使用3X2的结构,每个卷积核就会携带6个参数。

卷积核的尺寸应该如何选择呢?如果你在使用经典架构,那经典架构的论文中所使用的尺寸就是最好的尺寸。如果你在写自己的神经网络,那3x3几乎就是最好的选择。对于这个几乎完全基于经验的问题,我可以提供以下几点提示:

1、卷积核几乎都是正方形。最初是因为在经典图像分类任务中,许多图像都被处理成正方形或者接近正方形的形状(比如Fashion-MNIST,CIFAR,ImageNet)等等。如果你的原始图像尺寸很奇妙(例如,非常长或非常宽),你可以使用与原图尺寸比例一致的卷积核尺寸。

2、卷积核的尺寸最好是奇数,例如3x3,5x5,7x7等。这是一种行业惯例,传统视觉中认为这是为了让被扫描区域能够“中心对称”,无论经历过多少次卷积变换,都能够以正常比例还原图像。

相对的,如果扫描区域不是中心对称的,在多次进行卷积操作之后,像素会“偏移”导致图像失真(由最左侧图像变换为右侧的状况)。然而,这种说法缺乏有效的理论基础,如果你发现你的神经网络的确更适合偶数卷积核(并且你能够证明、或说服你需要说服的人来接受你的决定),那你可以自由使用偶数卷积核。目前为止,还没有卷积核的奇偶会对神经网络的效果造成影响的明确理论。
3、在计算机视觉中,卷积核的尺寸往往都比较小(相对的,在NLP中,许多网络的卷积核尺寸都可以很大)。这主要是因为较小的卷积核所需要的训练参数会更少,之后我们会详细地讨论关于训练参数量的问题。

1.2 卷积的输入与输出:in_channels,out_channels,bias

除了kernel_size之外,还有两个必填参数:in_channels与out_channels。简单来说:in_channels是输入卷积层的图像的通道数或上一层传入特征图的数量,out_channels是指这一层输出的特征图的数量。这两个数量我们都可以自己来确定,但具体扫描流程中的细节还需要理清。

在之前的例子中,我们在一张图像上使用卷积核进行扫描,得到一张特征图。这里的“被扫描图像”是一个通道,而非一张彩色图片。如果卷积核每扫描一个通道,就会得到一张特征图,那多通道的图像应该被怎样扫描呢?会有怎样的输出呢?
在一次扫描中,我们输入了一张拥有三个通道的彩色图像。对于这张图,拥有同样尺寸、但不同具体数值的三个卷积核会分别在三个通道上进行扫描,得出三个相应的“新通道”。由于同一张图片中不同通道的结构一定是一致的,卷积核的尺寸也是一致的,因此卷积操作后得出的“新通道”的尺寸也是一致的。
得出三个“新通道”后,我们将对应位置的元素相加,形成一张新图,这就是卷积层输入的三彩色图像的第一个特征图。这个操作对于三通道的RGB图像、四通道的RGBA或者CYMK图像都是一致的。只不过,如果是四通道的图像,则会存在4个同样尺寸、但数值不同的卷积核分别扫描4个通道。
因此,在一次扫描中,无论图像本身有几个通道,卷积核会扫描全部通道之后,将扫描结果加和为一张feature map。所以,一次扫描对应一个feature map,无关原始图像的通道数目是多少,所以out_channels就是扫描次数,这之中卷积核的数量就等于输入的通道数in_channels x 扫描次数out_channels。那对于一个通道,我们还有可能多次扫描,得出多个feature map吗?当然有可能!卷积核的作用是捕捉特征,同一个通道上很有可能存在多个需要不同的卷积核进行捕捉的特征,例如,能够捕捉到孔雀脖子轮廓的卷积核,就不一定能够捕捉到色彩绚丽的尾巴。因此,对同一个通道提供多个不同的卷积核来进行多次扫描是很普遍的操作。不过,我们并不能对不同的通道使用不同的卷积核数量。比如,若规定扫描三次,则每次扫描时通道都会分别获得自己的卷积核,我们不能让卷积网络执行类似于“红色通道扫描2次,蓝色通道扫描3次”的操作。
需要注意的是,当feature maps被输入到下一个卷积层时,它也是被当作“通道”来处理的。不太严谨地说,feature map其实也可以是一种“通道”,虽然没有定义到具体的颜色,但它其实也是每个元素都在[0,255]之间的图。这是说,当feature map进入到下一个卷积层时,新卷积层上对所有feature map完成之后,也会将它们的扫描结果加和成一个新feature map。所以,在新卷积层上,依然是一次扫描对应生成一个feature map,无关之前的层上传入的feature map有多少。
这其实与DNN中的线性层很相似,在线性层中,下一个线性层输入的数目就等于上一个线性层的输出的数目。我们来看一下具体的卷积层的代码:

import torch
from torch import nn
#假设一组数据
#还记得吗?虽然默认图像数据的结构是(samples, height, width, channels)
#但PyTorch中的图像结构为(samples, channels, height, width),channels排在高和宽前面
#PyTorch中的类(卷积)无法读取channels所在位置不正确的图像
data = torch.ones(size=(10,3,28,28)) #10张尺寸为28*28的、拥有3个通道的图像
conv1 = nn.Conv2d(in_channels = 3,out_channels = 6 #全部通道的扫描值被合并,6个卷积核形成6个feature map,kernel_size = 3) #这里表示3x3的卷积核
conv2 = nn.Conv2d(in_channels = 6 #对下一层网络来说,输入的是上层生成的6个feature map,out_channels = 4 #全部特征图的扫描值被合并,4个卷积核形成4个新的feature map,kernel_size = 3)
#conv3 = nn.Conv2d(in? out?)
#通常在网络中,我们不会把参数都写出来,只会写成:
#conv1 = nn.Conv2d(3,6,3)
#查看一下通过卷积后的数据结构
conv1(data).shape
#torch.Size([10, 6, 26, 26])
conv2(conv1(data)).shape
#torch.Size([10, 4, 24, 24])
#尝试修改一下conv2的in_channels,看会报什么错?
conv2 = nn.Conv2d(in_channels = 10,out_channels = 4,kernel_size = 3)
conv2(conv1(data))

掌握卷积层输入输出的结构,对于构筑卷积网络十分重要。

在DNN中,每一层权重www都带有偏差 ,我们可以决定是否对神经网络加入偏差,在卷积中也是一样的。在这里我们的权重就是卷积核,因此每个卷积层中都可以加入偏差,偏差的数量与扫描的数量一致。当我们得到feature_map后,如果有偏差的存在,我们会将偏差值加到feature_map的每个元素中,与矩阵 + 常数的计算方法一致。

当参数bias=True时,最终的feature_map是包含常数项的,反之则不包含。

1.3 特征图的尺寸:stride,padding,padding_mode

stride

不知你是否注意到,在没有其他操作的前提下,经过卷积操作之后,新生成的特征图的尺寸往往是小于上一层的特征图的。在之前的例子中,我们使用3X3的卷积核在6X6大小的通道上进行卷积,得到的特征图是4X4。如果在4X4的特征图上继续使用3X3的卷积核,我们得到的新特征图将是2X2的尺寸。最极端的情况,我们使用1X1的卷积核,可以得到与原始通道相同的尺寸,但随着卷积神经网络的加深,特征图的尺寸是会越来越小的。

对于一个卷积神经网络而言,特征图的尺寸非常重要,它既不能太小,也不能太大。如果特征图太小,就可能缺乏可以提取的信息,进一步缩小的可能性就更低,网络深度就会受限制。如果特征图太大,每个卷积核需要扫描的次数就越多,所需要的卷积操作就会越多,影响整体计算量。同时,卷积神经网络往往会在卷积层之后使用全连接层,而全连接层上的参数量和输入神经网络的图像像素量有很大的关系(记得我们之前说的吗?全连接层需要将像素拉平,每一个像素需要对应一个参数,对于尺寸600X400的图片需要2.4* 个参数),因此,在全连接层登场之前,我们能够从特征图中提取出多少信息,并且将特征图的尺寸、也就是整体像素量缩小到什么水平,将会严重影响卷积神经网络整体的预测效果和计算性能。也因此,及时了解特征图的大小,对于卷积神经网络的架构来说很有必要。
Hout =Hin −KH+1Wout =Win −KW+1\begin{array}{l} H_{\text {out }}=H_{\text {in }}-K_{H}+1 \\ W_{\text {out }}=W_{\text {in }}-K_{W}+1 \end{array}Hout ​=Hin ​−KH​+1Wout ​=Win ​−KW​+1​那怎么找出卷积操作后的特征图的尺寸呢?假设特征图的高为 ,特征图的宽为 ,则对于上图所示的卷积操作,我们可以有如下式子:


其中,HinH_{i n}Hin​与WinW_{i n}Win​是输入数据的高和宽,对于第一个卷积层而言,也就是输入图像的高和宽,对于后续的卷积层而言,就是前面的层所输出的特征图的高和宽。KHK_{H}KH​和KWK_{W}KW​如同之前提到的,则代表在这一层与输入图像进行卷积操作的卷积核的高和宽。在之前的例子中,HinH_{i n}Hin​和WinW_{i n}Win​都等于6,KHK_{H}KH​和KWK_{W}KW​都等于3,因此HoutH_{out}Hout​=WoutW_{out}Wout​= 6-3+1 = 4。但在实际情况中,图像的宽高往往是不一致的。因行业的约定俗成,卷积核的形状往往是正方形,但理论上来说KHK_{H}KH​和KWK_{W}KW​也可以不一致。在PyTorch中,卷积核的大小由参数Kernel_size确定。设置kernel_size=(3,3),即表示卷积核的尺寸为(3,3)。

这是特征图尺寸计算的“最简单”的情况。在实际进行卷积操作时,还有很多问题。比如说,现在每执行一次卷积,我们就将感受野向右移动一个像素,每扫描完一行,我们就向下移动一个像素,直到整张图片都被扫描完为止。在尺寸较小的图片上(比如,28X28像素),这样做并没有什么问题,但对于很大的图片来说(例如600X800),执行一次卷积计算就需要扫描很久,并且其中有许多像素都是被扫描了很多次的,既浪费时间又浪费资源。于是,我们定义一个新的超参数:卷积操作中的“步长”,参数名称stride(也译作步幅)。

步长是每执行完一次卷积、或扫描完一整行后,向右、向下移动的像素数。水平方向的步长管理横向移动,竖直方向的步长管理纵向移动。在pytorch中,当我们对参数stride输入整数时,则默认调整水平方向扫描的步长。当输入数组时,则同时调整水平和竖直方向上的步长。默认状况下,水平和竖直方向的步长都是1,当我们把步长调整为(2,2),则每次横向和纵向移动时,都会移动2个像素。
步长可以根据自己的需求进行调整,通常都设置为1-3之间的数字,也可以根据kernel_size来进行设置。在DNN中,我们把形如(sampels, features)结构的表数据中的列,也就是特征也叫做“维度”。对于表数据来说,要输入DNN,则需要让DNN的输入层上拥有和特征数一样数量的神经元,因此“高维”就意味着神经元更多。之前我们提到过,任何神经网络中一个神经元上都只能有一个数字,对图像来说一个像素格子就是一个神经元,因此卷积网络中的“像素”就是最小特征单位,我们在计算机视觉中说“降维”,往往是减少一张图上的像素量。参数步长可以被用于“降维”,也就是可以让输入下一层的特征图像素量降低,特征图的尺寸变得更小。

以上图中的特征图为例,通道尺寸为7X7,卷积核尺寸为3X3,若没有步长,则会生成5X5的特征图(7-3+1)。但在(2,2)的步长加持下,只会生成3X3的特征图。带步长的特征图尺寸计算公式为:
Hout=Hin−KHS[0]+1Wout=Win−KWS[1]+1\begin{aligned} H_{o u t} &=\frac{H_{i n}-K_{H}}{S[0]}+1 \\ W_{o u t} &=\frac{W_{i n}-K_{W}}{S[1]}+1 \end{aligned} Hout​Wout​​=S[0]Hin​−KH​​+1=S[1]Win​−KW​​+1​其中S[0]代表横向的步长,S[1]代表纵向的步长。步长可以加速对特征图的扫描,并加速缩小特征图,令计算更快。

Padding,Padding_mode

除了步长之外,还有一个常常在神经网络中出现的问题:扫描不完全或扫描不均衡。

先来看扫描不完全,同样还是7X7的特征图和3X3的卷积核:
S[0]=1,Hout =(7−3)/1+1=5S[0]=2,Hout =(7−3)/2+1=3S[0]=3,Hout =(7−3)/3+1=2.33\begin{array}{l} S[0]=1, H_{\text {out }}=(7-3) / 1+1=5 \\ S[0]=2, H_{\text {out }}=(7-3) / 2+1=3 \\ S[0]=3, H_{\text {out }}=(7-3) / 3+1=2.33 \end{array} S[0]=1,Hout ​=(7−3)/1+1=5S[0]=2,Hout ​=(7−3)/2+1=3S[0]=3,Hout ​=(7−3)/3+1=2.33​当步长为3时,feature map的尺寸出现了小数,无法再包含完整的像素了。在图像上来看也非常明显,当步长为3的时候,向右移动一次后,就没有足够的图像来进行扫描了。此时,我们不得不舍弃掉没有扫描的最后一列像素。同时,在我们进行扫描的时候,如果我们的步长小于卷积核的宽度和长度,那部分像素就会在扫描的过程中被扫描多次,而边缘的像素则只会在每次感受野来到边缘时被扫描到,这就会导致“中间重边缘轻”,扫描不均衡。为了解决这个问题,我们要采用“填充法”对图像进行处理。
所谓填充法,就是在图像的两侧使用0或其他数字填上一些像素,扩大图像的面积,使得卷积核能将整个图像尽量扫描完整。
在PyTorch中,填充与否由参数padding控制和padding_mode控制。padding接受大于0的正整数或数组作为参数,但通常我们只使用整数。padding=1则时在原通道的上下左右方向各添上1个像素,所以通道的尺寸实际上会增加2*padding。padding_mode则可以控制填充什么内容。在图上展示的是zero_padding,也就是零填充,但我们也可以使用其他的填充方式。pytorch提供了两种填充方式,0填充与环型填充。在padding_mode中输入“zero”则使用0填充,输入“circular”则使用环型填充(原始通道的数据复制出去用)。

需要注意的是,虽然pytorch官方文档上说padding_mode可以接受四种填充模式,但实际上截至版本1.7.1,仍然只有"zeros"和"circular"两种模式有效,其他输入都会被当成零填充。如果想要使用填充镜面翻转值的reflection padding,则必须使用单独定义的层nn.ReflectionPad2d,同样的,"replicate"模式所指代的填充边缘重复值需要使用单独的类nn.ReplicationPad2d。
不难发现,如果输入通道的尺寸较小,padding数目又很大,padding就可能极大地扩充通道的尺寸,并让feature map在同样的卷积核下变得更大。我们之前说,在没有其他操作时feature map往往是小于输入通道的尺寸的,而加入padding之后feature map就有可能大于输入通道了,这在经典卷积网络的架构中也曾出现过。通常来说,我们还是会让feature map随着卷积层的深入逐渐变小,这样模型计算才会更快,因此,padding的值也不会很大,基本只在1~3之间。

实际上,Padding并不能够保证图像一定被扫描完全或一定均衡。看下面的例子:
不难发现,即便已经填充了一个像素,在现在的步长与卷积核大小下,依然无法将整张图扫描完全。此时,有两种解决方案,一种叫做"valid",一种叫做"same"。
valid模式就是放弃治疗,对于扫描不到的部分,直接丢弃。“same”模式是指,在当前卷积核与步长设置下无法对全图进行扫描时,对图像右侧和下边进行“再次填充”,直到扫描被允许进行为止。从上图看,same模式下的padding设置本来是1(即左右两侧都填上0),但在右侧出现11、12、13和填充的0列无法被扫描的情况,则神经网络自动按照kernel_size的需要,在右侧再次填充一个0列,实现再一次扫描,让全部像素都被扫描到。

这个操作看上去很智能,但遗憾的是只能够在tensorflow中实现。对于PyTorch而言,没有“same”的选项,只要无法扫描完全,一律抛弃。为什么这样做呢?主要还是因为kernel_size的值一般比较小,所以被漏掉的像素点不会很多,而且基本集中在边缘。随着计算机视觉中所使用的图片分辨率越来越高,图像尺寸越来越大,边缘像素包含关键信息的可能性会越来越小,丢弃边缘就变得越来越经济。对于一张28x28的图像而言,丢弃2、3列或许会有不少信息损失,但对于720x1080的图片而言,究竟是720x1078还是720x1080,其实并无太大区别。

那如果,你的图像尺寸确实较小,你希望尽量避免未扫描的像素被丢弃,那你可以如下设置:

  • 1、卷积核尺寸控制在5x5以下,并且kernel_size > stride
  • 2、令2*padding > stride

这样做不能100%避免风险,但可以大规模降低像素被丢弃的风险(个人经验,无理论基础)。

padding操作会影响通道的大小,因此padding也会改变feature map的尺寸,当padding中输入的值为P时,特征图的大小具体如下:Hout=Hin+2P−KHS[0]+1Wout=Win +2P−KWS[1]+1\begin{aligned} H_{o u t} &=\frac{H_{i n}+2 P-K_{H}}{S[0]}+1 \\ W_{o u t} &=\frac{W_{\text {in }}+2 P-K_{W}}{S[1]}+1 \end{aligned} Hout​Wout​​=S[0]Hin​+2P−KH​​+1=S[1]Win ​+2P−KW​​+1​我们在代码中来感受一下特征图尺寸的变化:

#记住我们的计算公式
#(H + 2p - K)/S + 1
#(W + 2p - K)/S + 1
#并且卷积网络中,默认S=1,p=0
data = torch.ones(size=(10,3,28,28))
conv1=nn.Conv2d(3,6,3)
conv2=nn.Conv2d(6,10,3)
conv3=nn.Conv2d(10,16,5,stride=2,padding=1)
conv4=nn.Conv2d(16,3,5,stride=(2,3),padding=2)
#(28 + 0 - 3)/1 + 1 = 26
#验证一下
conv1(data).shape
#torch.Size([10, 6, 26, 26])
#conv2,输入结构26*26
#(26 + 0 - 3)/1 + 1 = 24
#验证
conv2(conv1(data)).shape
#torch.Size([10, 10, 24, 24])
#conv3,输入结构24*24
#(24 + 2 - 5)/2 + 1 = 11,扫描不完全的部分会被舍弃
conv3(conv2(conv1(data))).shape
#torch.Size([10, 16, 11, 11])
#conv4,输入结构11*11
#(11 + 4 - 5)/3 + 1 = 4.33,扫描不完全的部分会被舍弃
conv4(conv3(conv2(conv1(data)))).shape
#torch.Size([10, 3, 6, 4])

padding和stride是卷积层最基础的操作,之后的课程中我们还会有各种各样的操作,他们都有可能会改变卷积层的输出结构或参数量。对于入门而言,学会padding和stride也就足够了。

2 池化层nn.MaxPool & nn.AvgPool

无论是调整步长还是加入填充,我们都希望能够自由控制特征图的尺寸。除了卷积层之外,另一种可以高效减小特征图尺寸的操作是“池化”Pooling。池化是一种非常简单(甚至有些粗暴的)的降维方式,经常跟在卷积层之后,用以处理特征图。最常见的是最大池化(Max Pooling)和平均池化(Average Pooling)两种操作,他们都很容易理解:
池化层也有核,但它的核没有值,只有尺寸。在上图之中,池化核的尺寸就是(2,2)。池化核的移动也有步长stride,但默认步长就等于它的核尺寸,这样可以保证它在扫描特征图时不出现重叠。当然,如果我们需要,我们也可以设置参数令池化核的移动步长不等于核尺寸,在行业中这个叫“Overlapping Pooling”,即重叠池化,但它不是非常常见。通常来说,对于特征图中每一个不重叠的、大小固定的矩阵,池化核都按照池化的标准对数字进行计算或筛选。在最大池化中,它选出扫描区域中最大的数字。在平均池化中,它对扫描区域中所有的数字求平均。在加和池化中,它对扫描区域中所有的数字进行加和。

在这几种简单的方法中,最大池化是应用最广泛的,也是比较有效的。考虑看看feature map中的信息是怎么得来的?feature map中每个像素上的信息都是之前的卷积层中,图像与卷积核进行互相关操作的结果。对之前的卷积层而言,卷积核都是一致的,唯一不同的就是每次被扫描的区域中的像素值。像素值越大,说明颜色信息越多,像素值越小,说明图像显示约接近黑色,因此经过卷积层之后,像素值更高的点在原始图像上更有可能带有大量信息。MaxPooling通过摘取这些带有更多信息的像素点,有效地将冗余信息消除,实现了特征的“提炼”。相对的,平均和加和的“提炼”效应就弱一些。
在PyTorch中,池化层也有多种选项,但这些多属于“普通池化”的范围。在传统计算机视觉中,我们还有空间金字塔池化(Spatial Pyramid Pooling)等操作。
以MaxPool2d为例,其类和参数的详情如下:

CLASS torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False,ceil_mode=False)

其中kernel_size就是池化核的尺寸,一般都设置为2X2或3X3。Padding参数与Stride参数一般都不填写。需要提醒的是,池化层的步长一般与核尺寸保持一致,因此stride参数的默认值就是kernel_size。池化层对特征图尺寸的影响,也符合我们之前所写的这个公式:Hout=Hin+2P−KHS[0]+1Wout=Win +2P−KWS[1]+1\begin{aligned} H_{o u t} &=\frac{H_{i n}+2 P-K_{H}}{S[0]}+1 \\ W_{o u t} &=\frac{W_{\text {in }}+2 P-K_{W}}{S[1]}+1 \end{aligned} Hout​Wout​​=S[0]Hin​+2P−KH​​+1=S[1]Win ​+2P−KW​​+1​只不过此时的padding、kernel_size以及stride都是池化层的参数。我们在代码中来看看:

data = torch.ones(size=(10,3,28,28))
conv1 = nn.Conv2d(3,6,3) #(28 + 0 - 3)/1 + 1 = 26
conv3 = nn.Conv2d(6,16,5,stride=2,padding=1) # (26 + 2 - 5)/2 +1 = 12
pool1 = nn.MaxPool2d(2) #唯一需要输入的参数,kernel_size=2,则默认使用(2,2)结构的核,默认步长stride=(2,2)
# (12 + 0 - 2)/2 + 1 =6
#验证一下
conv1(data).shape
#torch.Size([10, 6, 26, 26])
conv3(conv1(data)).shape
#torch.Size([10, 16, 12, 12])
pool1(conv3(conv1(data))).shape
#torch.Size([10, 16, 6, 6])

事实上,使用(2,2)结构的池化层总是会将featrue map的行列都减半,将整个feature map的像素数减少3/4,因此它是非常有效的降维方法。

与卷积不同,池化层的操作简单,没有任何复杂的数学原理和参数,这为我们提供了精简池化层代码的可能性。通常来说,当我们使用池化层的时候,我们需要像如上所示的方法一样来计算输出特征图的尺寸,但PyTorch提供的“Adaptive”相关类,允许我们输入我们希望得到的输出尺寸来执行池化:

CLASS torch.nn.AdaptiveMaxPool2d (output_size, return_indices=False)
CLASS torch.nn.AdaptiveAvgPool2d (output_size)

可以看到,在这两个类中,我们可以输入output_size,而池化层可以自动帮我们将特征图进行裁剪。我们来试试看:

data = torch.ones(size=(10,3,28,28))
conv1 = nn.Conv2d(3,6,3) #(28 + 0 - 3)/1 + 1 = 26
conv3 = nn.Conv2d(6,16,5,stride=2,padding=1) # (26 + 2 - 5)/2 +1 = 12
pool1 = nn.AdaptiveMaxPool2d(7) #输入单一数字表示输出结构为7x7,也可输入数组
pool1(conv3(conv1(data))).shape
#torch.Size([10, 16, 7, 7])

可惜的是,PyTorch官方没有给出这两个类具体是怎样根据给出的输出特征图的尺寸数来倒推步幅和核尺寸的,在PyTorch官方论坛也有一些关于这个问题的讨论,但都没有给出非常令人满意的结果。大家达成一致的是:如果输入一些奇怪的数字,例如(3,7),那在池化的过程中是会出现比较多的数据损失的。

除了能够有效降低模型所需的计算量、去除冗余信息之外,池化层还有特点和作用呢?

1、提供非线性变化。卷积层的操作虽然复杂,但本质还是线性变化,所以我们通常会在卷积层的后面增加激活层,提供ReLU等非线性激活函数的位置。但池化层自身就是一种非线性变化,可以为模型带来一些活力。然而,学术界一直就池化层存在的必要性争论不休,因为有众多研究表明池化层并不能提升模型效果(有争议)。
2、有一定的平移不变性(有争议)。
3、池化层所有的参数都是超参数,不涉及到任何可以学习的参数,这既是优点(增加池化层不会增加参数量),也是致命的问题(池化层不会随着算法一起进步)。
4、按照所有的规律对所有的feature map一次性进行降维,feature map不同其本质规律不然不同,使用同样的规则进行降维,必然引起大估摸信息损失。

不过,在经典神经网络架构中,池化层依然是非常关键的存在。如果感兴趣的话,可以就池化与卷积的交互相应深入研究下去,继续探索提升神经网络效果的可能性。

3 Dropout2d与BatchNorm2d

Dropout与BN是神经网络中非常经典的,用于控制过拟合、提升模型泛化能力的技巧,在卷积神经网络中我们需要应用的是二维Dropout与二维BN。对于BN我们在前面的课程中有深入的研究,它是对数据进行归一化处理的经典方法,对于图像数据,我们所需要的类如下:

CLASS torch.nn.BatchNorm2d (num_features, eps=1e-05, momentum=0.1, affine=True,track_running_stats=True)

BN2d所需要的输入数据是四维数据(第一个维度是samples),我们需要填写的参数几乎只有num_features一个。在处理表数据的BatchNorm1d里,num_features代表了输入bn层的神经元个数,然而对于卷积网络来说,由于存在参数共享机制,则必须以卷积核/特征图为单位来进行归一化,因此当出现在卷积网络前后时,BatchNorm2d所需要输入的是上一层输出的特征图的数量。例如:

data = torch.ones(size=(10,3,28,28))
conv1 = nn.Conv2d(3,32,5,padding=2)
bn1 = nn.BatchNorm2d(32)
bn1(conv1(data)).shape #不会改变feature map的形状
#torch.Size([10, 32, 28, 28])
#输入其他数字则报错
#bn1 = nn.BatchNorm2d(10)

同时,BN层带有β\betaβ和γ\gammaγ参数,这两个参数的数量也由特征图的数量决定。例如,对有32张特征图的数据进行归一化时,就需要使用32组不同的β\betaβ和γ\gammaγ参数,总参数量为特征图数 * 2 = 64。

理论上BN能完全替代Dropout的功能。Dropout是课程中首次提到的概念,它是指在神经网络训练过程中,以概率p随机地“沉默”一部分神经元的技术。具体来说,当整体神经元数量为N时,Dropout层会随机选择p * N个神经元,让这些神经元在这一次训练中不再有效,当相遇使选出的神经元的权重变为0,使神经元失活。在每次训练中,都有一组随机挑选的神经元被沉默,这样会减弱全体神经元之间的联合适应性,减少过拟合的可能性。在进行测试时,dropout会对所有神经元上的系数都乘以概率ppp,用以模拟在训练中这些神经元只有ppp的概率被用于向前传播的状况。

对于卷积神经网络来说,我们需要使用的类是Dropout2d,唯一需要输出的参数是p,其输入数据同样是带有samples维度的四维数据。不过在卷积中,Dropout不会以神经元为单位执行“沉默”,而是一次性毙掉一个通道。因此,当通道总数不多时,使用Dropout或Dropout中的p值太大都会让CNN丧失学习能力,造成欠拟合。通常来说,使用Dropout之后模型需要更多的迭代才能够收敛,所以我们总是从p=0.1,0.25开始尝试,最多使用p=0.5,否则模型的学习能力会出现明显下降。

CLASS torch.nn.Dropout2d (p=0.5, inplace=False)

data = torch.ones(size=(10,1,28,28))
conv1 = nn.Conv2d(1,32,5,padding=2)
dp1 = nn.Dropout2d(0.5)
dp1(conv1(data)).shape #不会改变feature map的形状

Dropout层本身不带有任何需要学习的参数,因此不会影响参数量。

接下来,我们来实现一些由卷积层和池化层组成的神经网络架构,帮助大家回顾一下神经网络的定义过程,同时也加深对卷积、池化等概念的印象。

Lesson 16.5 在Pytorch中实现卷积网络(上):卷积核、输入通道与特征图在PyTorch中实现卷积网络(中):步长与填充相关推荐

  1. 卷积网络中的通道、特征图、过滤器和卷积核

    卷积网络中的通道.特征图.过滤器和卷积核 1.feature map 1 feature map 在cnn的每个卷积层,数据都是以三维形式存在的.你可以把它看成许多个二维图片叠在一起(像豆腐皮一样), ...

  2. CNN卷积中卷积层,卷积核,通道概念及卷积过程详解

    1.卷积层,卷积核,通道概念及作用 卷积层:又称滤波器(filter)或者内核(kernel),TensorFlow文档中称之为滤波器(filter).用于对输入的图像结构进行特征提取. 卷积核:同上 ...

  3. cnn池化层输入通道数_(pytorch-深度学习系列)CNN中的池化层-学习笔记

    CNN中的池化层 首先,池化(pooling)层的提出是为了缓解卷积层对位置的过度敏感性. 什么意思? 比如在图像边缘检测问题中,实际图像里,我们的目标物体不会总出现在固定位置,即使我们连续拍摄同一个 ...

  4. 卷积计算,反卷积计算,特征图大小计算,空洞卷积计算

    感受野大小计算.卷积参数量与计算量.空洞卷积计算量与参数量 卷积计算,反卷积计算,特征图大小计算,空洞卷积计算 转自:https://www.jianshu.com/p/09ea4df7a788?ut ...

  5. 3d卷积和2d卷积1d卷积运算-CNN卷积核与通道讲解

    全网最全的卷积运算过程:https://blog.csdn.net/Lucinda6/article/details/115575534?spm=1001.2101.3001.6661.1&u ...

  6. OpenImage冠军方案:在物体检测中为分类和回归任务使用各自独立的特征图

    点击上方"深度学习技术前沿",关注公众号,选择加"星标"或"置顶" 导读 这篇文章来自商汤科技,是OpenImage竞赛的冠军方案,本文对物 ...

  7. CNN笔记:通俗理解卷积神经网络--理解不同输入通道和卷积核通道关系(红色部分)

    1 前言 2012年我在北京组织过8期machine learning读书会,那时"机器学习"非常火,很多人都对其抱有巨大的热情.当我2013年再次来到北京时,有一个词似乎比&qu ...

  8. Lesson 16.1016.1116.1216.13 卷积层的参数量计算,1x1卷积核分组卷积与深度可分离卷积全连接层 nn.Sequential全局平均池化,NiN网络复现

    二 架构对参数量/计算量的影响 在自建架构的时候,除了模型效果之外,我们还需要关注模型整体的计算效率.深度学习模型天生就需要大量数据进行训练,因此每次训练中的参数量和计算量就格外关键,因此在设计卷积网 ...

  9. Lesson 16.4 卷积遇见深度学习

    4.1 通过学习寻找卷积核 在深度神经网络中,层与层之间存在着链接上层与下层的权重系数 .深度学习的核心思想之一,就是给与算法训练目标,让算法自己朝着目标函数最小化的方向进行学习,并自动求解出权重系数 ...

最新文章

  1. python好学吗 老程序员-今天面试了一个34岁大龄程序员,有感而发
  2. Java中如何引用另一个类里的集合_【18期】Java序列化与反序列化三连问:是什么?为什么要?如何做?...
  3. 自己身份信息泄漏了怎么办,别怕,带你了解身份管理与访问控制
  4. 万万没想到,线程居然被饿死了!
  5. uva 1623——Enter The Dragon
  6. git ssh配置文件 服务器_git登录ssh服务器
  7. 面试之 Python 进阶
  8. php代码编写直接插入排序算法,PHP排序算法之直接插入排序(Straight Insertion Sort)实例分析...
  9. 分布式系统认证方案_分布式系统介绍_Spring Security OAuth2.0认证授权---springcloud工作笔记134
  10. html页面色块布局代码,Html 实现动态显示颜色块的报表效果(实例代码)
  11. java每个月某天,java – 查找一年中的某一天
  12. Windows修改远程端口号
  13. yolo之---非极大值抑制
  14. 数字通信第六章——信道模型和信道容量
  15. 编程语言:8086汇编中int 16h接收alt+方向键
  16. co作为前缀的意思_注定孤独的前缀|英汉比较教学法·语音思维No.17
  17. 摄影基础知识——光学变焦和数码变焦
  18. 阿里巴巴国际站全屏分类产品切换效果怎么做?代码生成器产品分类效果切换轮播全屏代码
  19. 【硬件篇】计算机起源
  20. 流利阅读 2019.2.2 Barbie will soon be 60—and is still going strong

热门文章

  1. php证书格式,常用的证书格式转换 - niceguy_php的个人空间 - OSCHINA - 中文开源技术交流社区...
  2. python中哈希是什么意思_在python中向量化特征哈希
  3. 服务器怎么清除日志文件,如何清理服务器数据库日志文件
  4. mysql 二维表 查询_二维报表数据表设计
  5. This tutorial code needs the xfeatures2d contrib module to be run.
  6. jetson nano 摄像头购买
  7. 怎么学python知乎_你们都是怎么学 Python 的?
  8. javascript~callback回调函数
  9. numpy笔记:random.permutation
  10. 还在为快捷键烦恼吗?IntelliJ IDEA 快捷键大全 Mac 版