提示:仅仅是学习记录笔记,搬运了学习课程的ppt内容,本意不是抄袭!望大家不要误解!纯属学习记录笔记!!!!!!
在自建架构时,我们需要时刻牢记模型评估的三角:效果、效率(运算速度)、可解释性。对于深度学 习架构而言,可解释性几乎可以忽略不计,但是如果我们能够使用可解释的方式对数据进行处理,我们也不会刻意避开。
在讲解AlexNet时,我们已经说明,“小卷积核、多通道、更深层”的规则在实践中被证明是有效的,这个规则可以一定程度上保证我们架构的效果, 但围绕这种架构的疑问还很多,比如,为什么这样的架构会有效?能够严谨证明吗?如果深度会更有 效,我能够自由修改/增删层的结构吗?假设更深的架构有效,我们能否承担深层架构的训练成本呢?在自建卷积网络的时候,这些都是我们要考虑的因素。接下来,我们就从架构的执行效率以及学习能力两个方面进行更深入的说明。
更深的网络会展现出更强大的学习能力,这是深度学习领域的一个普遍认知,因此在建立自己的神经网络时,一个常用的思路就是“加深现有模型的深度”来提升模型效果。
理想是美好的,但在追寻“深度”的路上,我们总是会遭遇理论和实践上的重重困难。第一个难以忽视的 问题,就是输入图像的尺寸会限制我们可以选择的深度。即便你知道继续加深会获得更好的效果,我们能够建立的网络远远没有想象中的那么深。
在卷积神经网络由浅至深的过程中,特征图的整体趋势是缩小的。如果最终进入FC层的特征图太小,则 会损失掉过多的信息,如果进入FC层的特征图太大,则会导致FC层的参数量过于巨大,因此在特征图进 入最终的全连接层(FC层)之前,其最小尺寸大约需要被控制在5x5-9x9之间,常见尺寸为5x5,7x7, 9x9。不巧的是,卷积层和池化层都是特征图尺寸杀手。
一个不重叠的池化层可以将特征图的宽高折半,而任意卷积层,只要步长为2,也可以轻松将特征图的宽 高折半。以224x224尺寸的图像为例,只要池化层和步长为2的卷积层出现5次(2**5=32, 32*7=224),特征图尺寸就会变成7x7,不再具备“追求深度”的空间。池化层的各项 相关参数很少小于(2,2),因此我们只能依赖卷积层来控制特征图尺寸的缩小速度。但卷积层的选项也没 有想象中那么丰富。
首先,为了特征图尺寸不“折半”,步长只能是1。同时,根据计算机视觉的惯例,卷积核的尺寸只能在 3x3,5x5,最多7x7之间进行挑选,而卷积核的尺寸在特征图计算公式中是在分子上被减去的数,因此 其他参数不变时,卷积核越大,生成的特征图尺寸越小。
所以为了深度,卷积核最好是保持小尺寸。然而,卷积核很小的时候,“填充”就无法被设定得太大。在 PyTorch中,池化层的padding的参数值必须小于池化核尺寸的1/2,否则程序会报错、无法运行(以下图为例,对池化而言,kernel3padding2以及kernel3padding3的组合几乎没有意义,只有当padding小于1/2kernel的时候,池化核才会捕捉到足够的信息。因此池化层在实践中,99%的情况下都不会使得特 征图变大)。卷积层的padding参数值虽然不受程序强制限制,但一般也会设置为至少是小于卷积核尺 寸,当kernel=padding时,卷积层会增加不必要的计算,甚至会单纯增加噪音(如kernel3padding3的情况)。
所以不难发现,在追求“深度”的过程中,卷积层的参数看似选择多多,其实处处受限。在卷积核尺寸、 填充、步长的限制下,我们常见的特征图尺寸变化其实只有以下三种:
1、宽高分别折半、或缩小更多:当步长为2以上,或者卷积核较大时,可以实现大幅度缩小特征图的尺 寸。例如AlexNet第一个卷积层,利用步长4,11x11的卷积核,就直接将宽高缩小至原来的1/4。
2、不使用池化层,利用填充与卷积核尺寸的搭配,令特征图每经过一次卷积层,就缩小2个或4个像素,例如:
3、利用填充与卷积核尺寸的搭配,令特征图每经过一次卷积层,尺寸不变,将缩小特征图的工作完全交 给池化层来做,例如:
还有一种很罕见的操作,就是令特征图在经过卷积层之后变大,不难注意到,这就是padding的取值大 于1/2卷积核的情况:
但这意味着给特征图增加了噪音,也增加了更多无效的计算。
可见,从加深深度的角度来说,我们能够做的就是“递减2或4”,或者“保持特征图不变、并将减小特征图 的工作较给池化层去做”这两种选项。在输入的图像的结构都为(128,1,224,244),且每层输出的 特征图数目相同,且进入FC层之前的特征图尺寸被缩减至相似大小(7x7,8x8)的前提下,逐层递减的 架构(1)与保持特征图不变的“重复”架构(2)分别如下:
在Fashion-MNIST数据集上,两者在训练准确率上可以达到的平均极限几乎没有差别(都接近99%),但重复架构的鲁棒性会更好,在测试集上的表现会更高。
VGGNet的论文中提出了6种重复架构,其中每重复几个卷积就会跟一个最大池化层,这种“n个卷积+池 化”的结构在VGG中被称之为“块”(block)。在深度卷积网路中,我们常常会划分“块”,VGG是按照特征 图尺寸的变化来进行划分(每池化一次,就分一次块),6种架构都有五个块。
受启发于VGG架构,在过去数年的研究中,人们通过实验发现了这样的结论:在不改变原始卷积层输入 输出机制的前提下,增加卷积层的数目来增加深度,会很快让模型效果和性能都达到上限。深度并不能 高效提升模型的效果,需要先降低模型的训练成本,才能够追求更深的神经网络。如果想要通过“加深” 卷积神经网络来实现网络效果的飞跃,那必须是从16层加到160层,而不是从16层加到19层。事实上, 在2014年后的ILSVRC上,赢得冠军的网络架构变得越来越复杂,深度也越来越深,在2017年ILSVRC闭 幕之前,网络深度大约停留在了220层左右,这是得益于研究者们发现了更高效地提升深度的方法。
50x50的图像而言,若追求测试准确率在90%以上,只需要小于20层的神经网络就足够了。
输入→(卷积x2+池化)x2 →(卷积x3+池化)x3 → FC层x3 →输出
其中每组卷积+池化算一个block。同时,架构图中没有显示出来的内容包括:
1、除了输出层外,所有的激活函数都是ReLU函数;
2、最后三个全连接层中的前两个全连接层前有Dropout层,p=0.5
import torch
import torch.nn as nn
from torchinfo import summary
data = torch.ones(10, 3, 224, 224)
class VGG16(nn.Module):
def __init__(self):
super(VGG16, self).__init__()
#block1
self.conv1 = nn.Conv2d(3, 64, 3,padding=1) #(224+2-3)/1 + 1 = 224
self.conv2 = nn.Conv2d(64, 64, 3, padding=1 )
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # 224/2 =112
#block2
self.conv3 = nn.Conv2d(64, 128, 3, padding=1) #112+2-3+1 =112
self.conv4 = nn.Conv2d(128, 128, 3, padding=1)
self.conv5 = nn.Conv2d(128, 128, 3, padding=1)
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) #112/2= 56
#block3
self.conv6 = nn.Conv2d(128, 256, 3, padding=1)
self.conv7 = nn.Conv2d(256, 256, 3, padding=1)
self.conv8 = nn.Conv2d(256, 256, 3, padding=1)
self.conv9 = nn.Conv2d(256, 256, 3, padding=1)
self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2) #56/2 = 28
#block4
self.conv10 = nn.Conv2d(256, 512, 3, padding=1)
self.conv11 = nn.Conv2d(512, 512, 3, padding=1)
self.conv12 = nn.Conv2d(512, 512, 3, padding=1)
self.conv13 = nn.Conv2d(512, 512, 3, padding=1)
self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2) #28/2= 14
#block5
self.conv14 = nn.Conv2d(512, 512, 3, padding=1)
self.conv15 = nn.Conv2d(512, 512, 3, padding=1)
self.conv16 = nn.Conv2d(512, 512, 3, padding=1)
self.conv17 = nn.Conv2d(512, 512, 3, padding=1)
self.pool5 = nn.MaxPool2d(kernel_size=2, stride=2) #14/2 =7
#block6
self.drop1 = nn.Dropout(0.5)
self.fc1 = nn.Linear(7*7*512, 4096)
self.drop2 = nn.Dropout(0.5)
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, 1000)
self.output = nn.Softmax(dim=1)
def forward(self, x):
#block1
x = torch.relu(self.conv1(x))
x = torch.relu(self.conv2(x))
x = self.pool1(x)
#block2
x = torch.relu(self.conv3(x))
x = torch.relu(self.conv4(x))
x = torch.relu(self.conv5(x))
x = self.pool2(x)
#block3
x = torch.relu(self.conv6(x))
x = torch.relu(self.conv7(x))
x = torch.relu(self.conv8(x))
x = torch.relu(self.conv9(x))
x = self.pool3(x)
#block4
x = torch.relu(self.conv10(x))
x = torch.relu(self.conv11(x))
x = torch.relu(self.conv12(x))
x = torch.relu(self.conv13(x))
x = self.pool4(x)
#block5
x = torch.relu(self.conv14(x))
x = torch.relu(self.conv15(x))
x = torch.relu(self.conv16(x))
x = torch.relu(self.conv17(x))
x = self.pool5(x)
#block6
x = x.reshape(-1, 7*7*512)
x = self.drop1(x)
x = torch.relu(self.fc1(x))
x = self.drop2(x)
x = torch.relu(self.fc2(x))
out_put = self.output(self.fc3(x))
return out_put
vgg= VGG16()
vgg(data)
summary(vgg, input_size=(10, 3, 224, 224))
1、在同样的资源支持下,深度网络解决复杂问题的能力高于浅层网络。深度网络展现出对复合函数的拟 合能力,更深的网络能够拟合更复杂的复合函数,而浅层网络的这种能力却不明显。
2、深度网络可以快速降低优化算法进入一个很大的局部最小值的概率,这或许能够解释为什么深度网络的效果更好。
从数学上可以推断,神经网络的深度越深时,权重空间 更加复杂,但损失函数的众多局部最小值的大小将会变得比较接近(鞍点会变平、整个函数的图像会变 得平滑),并且随着深度加深,局部最小值的数量会越来越少、数值也越来越接近,这就降低了优化算 法走入一个值很大的局部最小值的可能性。同时,寻找全局最小值是没有意义的,因为在实际应用神经 网络时,挣扎着找全局最小值一般都会走入过拟合的结局。
3、更深的网络能带来更大的感受野,而更大的感受野能带来更好的模型效果
这几乎是卷积神经网络的学术界公认的事实,虽然人们还没有从理论层面对此进行证明,但从实践的结果来看,感受野、深度与模型效果之间的相关性是确实存在的。接下来,我们就来认识感受野。
由于卷积神经网络有“稀疏交互”的特性,CNN中的神经元只受到原始图像上一部分数据的影响,而这部分数据其实就是神经 元在生成过程中、使用卷积操作时扫描到的那部分原始数据,这部分数据所在的区域也就是感受野。由 于大多数时候,图像和卷积核都是正方形,如果padding=0,步长为1,因我们可以只考虑行/列中的一 个维度来看感受野的变化。
如果是一张巨大的图像,有4个卷积层,每个卷积层的步长都为1,kernel_size=3x3,则有:
对于卷积层4上的任意神经元(像素)而言,它的感受野就是原始图像上的9x9的区域,仿佛最下面一层 的神经元就是光源,而图像上的感受野就是神经元的光能够照射到的地方。不难发现,随着深度的加 深,神经元上的感受野会越来越大,这意味着这个神经元上所携带的原始数据的信息会越来越多。而由 于卷积神经网络是稀疏交互的,为了让神经元在做出判断之前能够获取尽量多的信息,在被输入到FC层 之前的感受野越大越好。理论研究证明,一个表现优异的模型在FC层前的感受野一定是非常大的,巨大 的感受野是模型表现好的必要条件。和“深度”一样,通常我们认为,较大的感受野意味着较好的模型效果,但稍微增加一些感受野的尺寸,并不能对整个模型的预测效果带来巨大的改变。
不难发现,下面这张图就是每次将特征图尺寸缩小2个像素的递减架构,在经历4个卷积层后,图像的尺寸由22x22下 降到了14x14,感受野尺寸为9x9。
如果使用VGG中的重复架构使用的参数,即每个卷积层都使用kernel_size=3,padding=1的架构,且每 3个卷积层后跟一个(2,2)参数的最大池化层,在4个卷积层之后我们可以得到下图:
注意:每个卷积层的padding参数影响的是上一层输出的特征图。可以看 到,重复架构中,经过4层卷积层和1个池化层之后,图像的尺寸下降到了11x11,且第四个卷积层生成 的特征图上的神经元的感受野达到了12x12的尺寸,比递减架构相同卷积层下的感受野要大。在两种架构中,只要卷积核的尺寸保持3x3,那每经过一个卷积层,感受野的尺寸实际上只会增加2,但池化层在 将特征图尺寸减半的同时,却能够将感受野的宽和高都扩大一倍。池化层的存在令重复架构放大感受野的效率更高,这让重复架构更有优势。
事实上,感受野的尺寸没有上限。
超出图像的部分在最终的特征图的像素来看是什么样的呢?就相当于是没有值,全为0,表现在图像上就 是黑框。对于各个卷积层上的神经元们来说,他们的感受野会有如下差别:
那这样做,有什么意义呢?当感受野大小超出图像大小时,增加黑框不就是增加噪音吗?为什么要让感 受野越来越大,让图像信息越缩越小呢?这和感受野的第三个性质有关。
卷积神经网络的感受野也是一样。对于特征图来说,每个神经元所捕捉到的感受野区域不同,但这些区域是会有重叠的,并且很好理解的是,越是位于中间的像素,被扫描的次数越多,感受野重叠也就会越多。
对整个特征图来说,重叠越多的部分,信息就越饱满,“看得就越清晰”,而重叠较少的部分,信息就比较稀疏,因此就会“模糊”。因此,位于图像中间的像素有更多可以影响最终特征图的“路径”,他们对最终 特征图的影响更大,对卷积网络的分类造成的影响也会更大。
因为中间清晰两边模糊的性质,也因为有效感受野的存在,在卷积神经网络开始分类之前,我们必须尽 量让图像的有效信息集中在感受野的中心,这些有效信息越集中,感受野就越能“看清”这些信息,越能 提取出有效的特征。总之,使用远远超出图像尺寸的感受野,而将图像信息“锁”在感受野中心,让本来 应该“模糊”的部分全都被黑边所替代,就是最有效的做法。由于现在还无法精确地计算出有效感受野的 大小,理论上来说感受野更大会更好。唯一的例外就是,你在图像中想要识别的对象在图像的边缘,而 巨大的干扰项却在图像中间的时候。此时,你把感受野放得越大,干扰就会越大。
有数个方法可以扩大感受野:
1、加深卷积神经网络的深度,理论上来说,每增加一个卷积层,感受野的宽和高就会按照卷积核的尺 寸-1线性增加;
2、使用池化层或其他快速消减特征图尺寸的技术;
3、使用更加丰富的卷积操作,如膨胀卷积dilated convolution、残差连接等等。
膨胀卷积又叫做空洞卷积,它通过在感受野上使需要计算点“膨胀”的方式来“扩大”卷积核可以扫描的区域。所谓膨胀的概念如下图所示。
在感受野上,需要参与卷积计算的任意一个像素都是计算点。以计算点为中心向外“扩充”像素点的行为 就叫做“膨胀”。注意,膨胀和填充非常相似,但膨胀只是扩大以计算点为中心的某个面积,并不会改变 计算点相邻像素点的值,填充则是需要对计算点相邻像素点的值进行修改。以计算点为中心,膨胀率 (dilation rate)为1时,计算点自身就是全部面积。膨胀率为2时,在计算点周边扩充一圈像素,当膨 胀率为3时,在计算点周边填充2圈像素,以此类推。膨胀卷积就是在每个参与卷积计算的计算点上做“膨胀”操作,让计算点与计算点之间出现“空洞”,并跳过空洞进行计算的卷积方式。描述上来说有些难以理解,我们来看图:
膨胀率为1时,3x3的卷积核所对应的感受野大小也是3x3(绿色区域),全部9个被感受野所包括的像素点都会和卷积核上的值执行卷积操作,并得到特征图中的像素点。
当膨胀率为2时,每个计算点所覆盖的 面积都会向外拓展一圈,将原来的计算点向右向下“挤”,构成如图所示的感受野。此时,卷积核的尺寸 为3x3,但感受野的尺寸为5x5,感受野中白色的格子都是计算点“膨胀”的结果,不参与计算,绿色的格子依然按照无膨胀时的规则与卷积核进行计算。
当膨胀率为3时,计算点向外膨胀的像素值为2圈,感受野的大小则变成7x7,但执行计算的计算点数依 然是9个。
很明显,膨胀卷积会改变输出的特征图的尺寸,其计算公式如下:
通过膨胀卷积,我们可以在不增加卷积核参数的情况下放大感受野。不过这里有一个很大的问题:虽然看上去感受野的面积是被放大了,但是跳过其中的像素点不进行计算,真的算是放大了本应该用来捕获 信息的感受野吗?这样的放大是有效的吗?从直觉来看,这样做应该会产生非常多的信息损失,要了解膨胀卷积真正的力量,还需要将多个卷积层连起来考虑。
对于没有膨胀的原始卷积,在第三层卷积层的特征图上,任意两个相邻的神经元的感受野如下所示。其 中绿色是绿色神经元的感受野,黄色是黄色神经元的感受野,橙色是两个神经元在各层上感受野的交叉 部分。不难看出,对于普通卷积,相邻神经元的感受野大概率是重复的,两个神经元的感受野合起来尺 寸有8x8的大小,而其中7x7的部分都是重复的。
假设从第一个卷积层开始,我们就采用dilation=2的设置,在第三个卷积层生成的特征图上,相邻的两 个神经元所涉及到的计算点如图所示:每次进行计算时,都是上一层不相邻的9个像素被扫描到。虽然在 生成单个神经元时,上一层的特征图上留下了不少未计算的空隙,但相邻的神经元却很好地补上了这些 没有被计算的部分,使得相邻两个神经元之间没有重复进行扫描。此时,两个相邻神经元的感受野合起 来就有14x14的大小,比起没有膨胀卷积时的感受野面积大了约3倍左右。这个性质可以被很好地利用, 当我们将膨胀率调大,并且让膨胀卷积层与普通卷积层串联使用时,单个像素的感受野可以被持续放大 (这种情况下,两个像素共同的感受野自然更大了)。如下图所示:
在这个图中,我们第一层卷积层是普通卷积(膨胀率为1),因此这一层的每一个像素点都对应原始图像 中3x3的面积。第二个卷积层我们使用膨胀率=2的卷积,即每个计算点之间相隔1个像素。第三个卷积层 我们让膨胀率=6,即每个计算点之间相隔5个像素,可以看到,在第三个卷积层输出的特征图上,一个 像素能够覆盖到原始图像的感受野尺寸变成了19x19。如果再结合池化层进行使用,那膨胀卷积放大感 受野的性质会更加明显。
对于第L个卷积层/池化层输出的特征图而言,假设卷积核尺寸为正方形、输入架构的图像尺寸也为正方形,则该图上任意一个神经元的感受野大小为:
注意,全连接层一般不计算感受野,因为全连接层的神经元就是前一个卷积层输出的特征图上的全部神 经元拉平后的结果,因此全连接层的感受野就与最后一个卷积层上输出的特征图的感受野一致。
你认为下面的图像中是同一只猫咪吗?大部分的人会认为下面的图像中只是花纹相 近、姿势相仿的猫咪,而不是同一只猫。但对卷积神经网络来说,下面的图像才更像是“同一只猫”。对 于Narnia的照片来说,当训练集中只有一张猫脸正面照时,卷积网络很难识别出各种仰头、侧躺、侧脸 的生物是什么。但下面这张图像中,虽然画风、背景、甚至猫咪的品种都发生了变化,但图中的物体位 置、像素排列(轮廓、颜色)等信息有很高的相似度,因此更容易被判断成是“同一只猫”。
人类和卷积网络对图像的“理解”方式不同,导致我们对同样的图像有不同的判断。在这种情 况下,假设我们在相似背景中绘制一个颜色类似的花瓶,很可能也会被模型判断成是“猫”,假设我们将训练数据的图像水平翻转,模型反而无法判断这就是“猫”了。此时,模型就是处于过拟合状态之中,只 认识“自己见过的东西”,泛化水平较低,鲁棒性较低。
在视觉领域,有非常多从不同角度定义的不变性,其中基础的不变性有4种:平移不变性(Translation Invariance)、旋转/视野不变性(Rotation/ViewPoint Inviariance)、尺寸不变性(size Inviarance) 与明度不变性(Illumination Invariance),其他常见的不变性包括镜面不变性(镜面翻转图像),颜色不变性等。
大部分深层卷积网络的架构自带一定的“平移不变性”,只要对象的轮廓一致,无论对象出现在图像 的哪个位置,卷积网络都能够判断出来。这是怎么实现的呢?来看下面这张图:
原始图像是一张字母C的图像,绿色区域就是字母C的像素。在卷积层中,我们使用4x4的卷积核对原始 图像进行扫描,并在卷积层之后跟上核尺寸为2x2的最大池化层。不难发现,当我们将字母C整个向下平 移一个像素时,虽然卷积层在捕捉特征时将字母C的特征信息放置在了不同的神经元上,如果我们将字 母C向右平移少许像素,很自然的,在卷积层后的特征图上,相关信息就会被放置在不同的神经元上。 然而,对于大部分图像而言,有效信息较为集中的区域的像素值会更大,在卷积操作后得到的特征图上 的值也会更大,这是最大池化层能够有效的基础。因此,无论有效信息位于特征图的什么位置,在经过 (也许不止一个)最大池化层之后,有效信息都能够被顺利筛选出来,也因此,在卷积神经网络中,关 键像素被平移后对模型整体准确率的影响相对较小。
不难发现,在这个过程中,卷积层的“无差别扫描”起到了重要作用,但真正让信息保持不变、不产生损 失的其实是池化层。一些观点认为,池化层的存在是CNN自带一定的平移不变性的主要理由,但有些观 点正相反,但无论如何,公认的是:更深层的卷积网络的平移不变性更强。下面这篇博文很好地完成了 卷积网络在不同平移程度以及不同深度下的平移不变性的实验:https://divsoni2012.medium.com/tra nslation-invariance-in-convolutional-neural-networks-61d9b6fa03df。
简单说说这篇博文的内容。在这篇博文中,作者持有两个重要观点:
1、CNN的平移不变性只能够应对“微小的平移”,当物体横向或纵向平移的像素过多时,CNN的平移不变性会衰减。
2、卷积+池化层的叠加可以增强CNN的平移不变性(更深的网络拥有更强的平移不变性),同时增强模型的鲁棒性。
对于图像数据而言,可用的数据增强技术数不胜数,例如:
如果做了数据增强,模型可能拥有了各种的不变性,那是否就高枕无忧了呢?并不是如此。为模型引入 不变性的确很有用,但也会有一些尴尬的情况。例如,在卷积神经网络不断将特征图缩小的过程中,像 素之间的“位置”信息是会逐渐损失掉的,当这些信息进入全连接层之后,网络就再也没有“相对位置”的概 念了,因此在人脸识别中,平移不变性会让卷积神经网络将下面两张图像都判断成人脸,但左侧明显只 是一些元素的堆积,并不是真正的人脸。大多数时候平移不变性会提升模型的效果,但对于密集任务 (需要对每个像素进行预测的任务)而言,平移不变性可能导致灾难。
在自建架构的时候,除了模型效果之外,我们还需要关注模型整体的计算效率。深度学习模型天生就需 要大量数据进行训练,因此每次训练中的参数量和计算量就格外关键,因此在设计卷积网络时,我们希 望相似预测效果下,参数量越少越好。为此我们必须理解卷积中的每层会如何影响模型整体的参数量和计算量。
模型参数是需要学习的参数,例如权重 和常数项 ,任何不需要学习、人为输入的超参数都不在“参数 量”的计算范围内。对于卷积神经网络中的任意元素(层或函数),有两种方式影响模型的参数量:
1、这个层自带参数,其参数量与该层的超参数的输入有关
2、这个层会影响feature map的尺寸,影响整体像素量和计算量,从而影响全连接层的输入
个卷积网络的卷积层究竟包含多少参数量,就是由卷积核的尺寸kernel_size、输入的通道数 in_channels,输出的通道数out_channels(卷积核的数量)共同决定的。其参数量如下:
import torch
import torch.nn as nn
#K_h * K_w * C_in * C_out + C_out
conv1 = nn.Conv2d(3,6,3) #(3 * 3* 3) * 6+ 6 = 168
conv2 = nn.Conv2d(6,4,3) #(3 * 3 * 6) * 4 + 4 =220
#检查一下结果
print(conv1.weight.numel())#162
print(conv1.bias.numel())#6
print(conv2.weight.numel())#216
print(conv2.bias.numel())#4
相对的,padding以及stride这些参数,不影响卷积层的所需要的参数量:
conv3 = nn.Conv2d(4, 16, 5,stride=2,padding=1) # (5*5*4)*16 + 16
conv4 = nn.Conv2d(16, 3, 5,stride=3,padding=2) # (5*5*16)*3 + 3
print(conv3.weight.numel())#1600
print(conv3.bias.numel())#16
print(conv4.weight.numel()) #1200
print(conv4.bias.numel())#3
从卷积层的参数计算公式来看,较大的卷积核、较多的输入和输出都会对参数量影响较大,由于实际中 使用的卷积核都很小,所以真正对卷积核参数有影响力的是输出和输入的特征图的数量。在较为复杂的 架构中,卷积层的输出数量可能达到256、512、甚至更大的数字,巨大的数字足以让一个卷积层包含的 参数达到百万级别。例如VGG16中,比较深的几个卷积层,他们的参数都在百万以上。
通常来说,如果我们希望减小卷积神经网络的参数量,那我们优先会考虑减少的就是输出的特征图数 量。但随着网络加深,特征图是越来越小的,学习到更多深入的信息,特征图数量必然会增加(依照惯 例,每经过一个池化层,就将特征图数量翻倍)。因此,如果希望消减卷积层的参数量,可以考虑不使 用那么多卷积+池化的组合(不要那么深的深度),如果一定要保持深度,则在第一层时就使用较小的特征图数量,例如32。
在讲解感受野时我们曾经使用过卷积层的俯视图,假设我们有两层核尺寸为3x3的卷积层,对于第二个 卷积层输出的特征图而言,一个神经元映射到原始图像上的感受野尺寸为5x5。同样的图像,假设我们 使用一层5x5的卷积层,也可以得到5x5的感受野。同样的,2个3x3卷积层将10x10的特征图缩小为了 6x6,一个5x5卷积层也将特征图缩小到了6x6。可以说,在“捕获的信息量”、“压缩尺寸”这两个层次上, 两个3x3的卷积层和一个5x5的卷积层获得了一样的结果。同理,我们也可以用三层3x3卷积核的卷积层 替代一层7x7的卷积核,更大的卷积核亦然。
两个3x3的卷积层总共需要7万+参数,而一个5x5的卷积层却需要10万+参数。对于VGG16这种重复架构 的网络而言,如果将所有的3x3卷积核都替换成5x5卷积核,那整体参数量将增加3个亿。可见,3x3的两 个卷积层不仅加深了深度,一定程度上让提取出的特征信息更“抽象”、更“复杂”,同时也让参数量大幅减 少。这又给了我们一个坚定使用小卷积核的理由。
1x1的卷积核上只有一个权重,每次进行卷积操作时,该权重会与原始图像中每个像素相乘,并得到特 征图上的新像素,因此1x1卷积也被叫做“逐点卷积”(Pointwise Convolution)。
在实际中,1x1卷积的重要作用之一就是加深CNN的深度。1x1卷积不会改变特征图的尺寸,因此可以被 用于加深CNN的深度,让卷积网络获得更好的特征表达。这个性质被论文《Network in Network》所使 用,并在架构NiN中发挥了重要的作用。NiN是AlexNet诞生不久之后被提出的架构,虽然也是2014年的 论文,但早于VGG之前诞生,其架构如下:
在NiN的架构中,存在着一种特殊的层:MLP layer。虽然在NiN的论文中,MLP layer是被看成是一个 独立的单元来说明,但从其结构、操作和输出的特征图来看,MLP layer毫无疑问就是1x1的卷积层。 NiN是以每个3x3卷积层后紧跟2个1x1卷积层组成一个block,并重复3个block达成9层卷积层架构的网络。
1x1卷积层不会改变特征图的尺寸,这个性质虽然有用,但和使用padding的卷积层差异不是特别大。从今天的眼光来看,1x1卷积核在加深深度方面最关键的作用还是用在卷积层之间,用于调整输出的通道数,协助大幅度降低计算量和参数量,从而协助加深网络深度,这一作用又被称为“跨通道信息交互”。
可以看到,虽然最后都输出了256个相同尺寸的特征图,并且所有信息都经过了3x3的卷积核的扫描,但 瓶颈架构所需要的参数量只有2.6万个,一个3x3卷积层所需要的参数却有59万个。对于百层以上的深层 神经网络来说,这个参数差异足以让人放弃一些性能,也要坚持使用瓶颈设计。
若考虑偏置,则:
不难发现,分组的存在不影响偏置,偏置只与输出的特征图数量有关。
import torch.nn as nn
conv1 = nn.Conv2d(4, 8, 3) #(3 * 3 * 4)*8 + 8 = 296
conv1_ = nn.Conv2d(4,8,3,groups=2) # ((3 * 3 * 4)*8)/2 + 8 = 152
print(conv1.weight.numel()) #288
print(conv1.bias.numel()) #8
print(conv1_.weight.numel()) #144
print(conv1_.bias.numel()) #8
特征图数量巨大时,分组卷积可以节省非常多的参数。当表 示在图像上,深度卷积所展示的链接方式为:
我们还可以将深度卷积与1x1卷积核结合使用。对输入特征图,我们首先进行深度卷积,产出一组特征 图,然后再这组特征图的基础上执行1x1卷积,对特征图进行线性变换。两种卷积打包在一起成为一个 block,这个block就叫做“深度可分离卷积”(Depthwise separable convolution),也被称为“分离卷 积”(separable convolution)。对于深度可分离卷积的一个block,若不考虑偏置,则整个block的参数量为:
import torch.nn as nn
conv1 = nn.Conv2d(4, 8, 3, bias=False) #(3 * 3 * 4) *8 =228
conv_depthwise = nn.Conv2d(4, 8, 3, groups=4, bias=False) #1/4 * (3 * 3 * 4) *8 = 72
conv_pairwise = nn.Conv2d(8, 8, 1, bias=False) # 1* 1 * 8 * 8 =64
print((conv_pairwise.weight.numel()+ conv_depthwise.weight.numel())/ conv1.weight.numel())
#0.4722222222222222
全连接层的作用主要有以下两个:
(1)作为分类器,实现对数据的分类。本质上来说,卷积层提供了一系列有意义且稳定的特征值,构成了一个与输入图像相比维数更少的特征空间,而全连接层负责学习这个空间上的(可能是非线性的)函数关 系,并输出预测结果。(其他可能的选择是,在卷积层后面放置一个SVM,或者放置其他机器学习算法 作为分类器。)
(2)作为整合信息的工具,将特征图中的信息进行整合。全连接层能够确保所有信息得到恰当的“混合”,以保证预测的效 果。
更多层,还是更多神经元?
对于CNN中的全连接层来说,在一个层上增加足够多的神经元,会比增加层效果更好。一般来说,CNN 中的全连接层最多只有3-4层(包括输出层),过于多的层会增加计算的负担,还会将模型带入过拟合的深渊。对于小型网络,3层全连接层已是极限了。需要注意的是,在卷积层和全连接层的连接中,通常全 连接的输出神经元个数不会少于输入的通道数。对于全连接层之间的连接,只要不是输出层,也很少出 现输出神经元少于输入神经元的情况。对全连接层而言,更大的参数代表了更高的复杂度、更强的学习 能力、更大的过拟合可能,因此对于小型网络来说,除非你的数据量庞大或数据异常复杂,尽量不使用 1024以上的参数。
在之前的课程中,我们使用torchinfo包中的summary来自动计算特征图尺寸,不难发现,要使用 summary函数,前提是已经建好了能够顺利运行的model,但尴尬的是,当我们不知道架构中红色箭头 处应该填写什么数字时,model是不可能跑通的。那怎么在模型架构不完整的情况下,找出最后一个池 化层/卷积层上输出的特征图的尺寸呢?一种简单的方法是,将Model中所有的线性层都注释掉,只留下 卷积层,然后将model输入summary进行计算,但有更简单的方法,使用另一种构筑神经网路架构的方 式:nn.Sequential。
nn.Sequential是一种非常简单的构筑神经网络的方式,它可以将“以序列方式从前往后运行的层”打包起 来,组合成类似于机器学习中的管道(Pipeline)的结构,以此避开建立类、继承类等稍微有些抽象的 python代码。大多数深度学习课程和教材在最开始的时候就会介绍它,并且一直以它作为例子运行各类 神经网络,我们来看具体的例子:
import torch
import torch.nn as nn
from torchinfo import summary
data = torch.ones(size=(10, 3, 229, 229))
#不使用类,直接将需要串联的网络、函数等信息写在一个序列里
net = nn.Sequential(nn.Conv2d(3, 6, 3),
nn.ReLU(inplace=True),
nn.Conv2d(6, 4, 3),
nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Conv2d(4, 16, 5, stride=2, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(16, 3, 5, stride=3, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(2))
print(net(data).shape)
#torch.Size([10, 3, 9, 9])
在较为复杂的网络架构中,我们通常利用nn.Sequential来区分网络的不同部分:例如,在普通CNN中, 卷积层、池化层负责的是特征提取,全连接层负责的是整合信息、进行预测,因此我们可以使用 nn.Sequential来区别这两部分架构。以VGG16为例:
import torch
import torch.nn as nn
from torchinfo import summary
feature_capture = nn.Sequential(nn.Conv2d(3, 64, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(64, 64, 3, padding=1),nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(128, 128, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(128, 128, 3, padding=1),nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Conv2d(128, 256, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(256, 256, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(256, 256, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(256, 256, 3, padding=1),nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Conv2d(256, 512, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1),nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2))
data = torch.ones(10, 3, 224, 224)
#print(feature_capture(data).shape)
#torch.Size([10, 512, 7, 7])
merge = nn.Sequential(nn.Dropout(0.5),
nn.Linear(7*7*512, 4096),nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(inplace=True),
nn.Linear(4096, 1000), nn.Softmax(dim=1))
x = feature_capture(data).reshape(-1, 7*7*512)
merge(x)
class VGG16(nn.Module):
def __init__(self):
super(VGG16, self).__init__()
self.features_ = feature_capture
self.merge = merge
def forward(self,x):
x = self.features_(x)
x = x.view(-1, 7*7*512)
output = self.merge(x)
return output
vgg = VGG16()
summary(vgg, input_size=(10, 3, 224, 224))
虽然全连接层很有用,但它的参数量带来的计算成本的确是一个很大的问题。因此,研究者们曾经尝试 找出各种方法,用来替代全连接层。其中流传比较广泛的方法之一,就是使用1x1卷积核来进行替代全 连接层。虽然大部分持有此观点的材料的描述都模糊不清、甚至有胡言乱语之嫌,但是人们还是对1x1 卷积核替代全连接层的效果深信不疑。
从数学公式来看,全连 接层和1x1的卷积层之间是可以互相转换的。对于卷积层来说,只要让特征图的尺寸为1x1,再让卷积核 的尺寸也为1x1,就可以实现和普通全连接层一模一样的计算了。
在计算机视觉中,不包含全连接层,只有卷积层和池化层的卷积网络被叫做全卷积网络(fully- convolutional network,FCN)。在无数减少全连接层的努力中,1x1卷积核可以在架构上完全替代 掉全连接层,来看下面的例子:
从之前1x1卷积核的例子来看,不难发现,只要在网络架构的最后能够将输出结果变成softmax函数可接 受的格式,比如(n_class,1),并且确定用于生成这些输入值的信息是从之前的特征图中整合出来的, 那任意架构在理论上来说都足以替代全连接层。GAP层就是这样的一个例子。GAP层的本质是池化层, 它使用池化方式是平均池化,它的职责就是将上一层传入的无论多少特征图都转化成(n_class,1, 1)结 构。为了能够将无论什么尺寸的特征图化为1x1的尺寸,GAP层所使用的核尺寸就等于输入的特征图尺寸。
在PyTorch中,没有专门的GAP类,但我们可以使用普通的平均池化层,并令这个池化层的核尺寸为上 层输入的特征图尺寸,以此来模拟全局平均池化。
data = torch.ones(10,7,7)
gap = nn.AvgPool2d(7)
print(gap(data).shape)
import torch
import torch.nn as nn
from torchinfo import summary
data = torch.ones(10, 3, 32, 32)
class NiN(nn.Module):
def __init__(self):
super(NiN, self).__init__()
self.block1 = nn.Sequential(nn.Conv2d(3, 192, 5, padding=2), nn.ReLU(inplace=True),
nn.Conv2d(192, 160, 1), nn.ReLU(inplace=True),
nn.Conv2d(160, 96, 1), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Dropout(0.25))
self.block2 = nn.Sequential(nn.Conv2d(96, 192, 5, padding=2), nn.ReLU(inplace=True),
nn.Conv2d(192, 192, 1), nn.ReLU(inplace=True),
nn.Conv2d(192, 192, 1), nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Dropout(0.5))
self.block3 = nn.Sequential(nn.Conv2d(192, 192, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(192, 192, 1), nn.ReLU(inplace=True),
nn.Conv2d(192, 10, 1), nn.ReLU(inplace=True),
nn.AvgPool2d(kernel_size=7),
nn.Softmax(dim=1))
def forward(self, x):
output = self.block3(self.block2(self.block1(x)))
return output
net = NiN()
print(net(data).shape)
#torch.Size([10, 10, 1, 1])
summary(net,(10, 3, 32, 32))
作为9层卷积层、最大特征图数目达到192的网络,NiN的参数量在百万之下,可以说都是归功于没有使 用全连接层。不过,1x1卷积层所带来的参数量也不少,因此NiN可以说是在各方面都中规中矩的网络。 从今天的眼光来看,NiN网络最大的贡献就是在于让人们意识到了1x1卷积层可能的用途,并且将“舍弃 线性层”的议题摆在了研究者面前。受到NiN网络启发而诞生的GoogLeNet以及ResNet都使用了1x1卷积 层,并且在各种消减参数的操作下使网络变得更加深。