VGG 论文总结
- 一、论文翻译
- 摘要
-
- (一)引言
- (二)ConvNet 配置
-
- 1. 架构
- 2. 配置
- 3. 讨论
- (三)分类框架
-
- 1. 训练
- 2. 测试
- 3. 实现细节
- (四)分类实验
-
- 1. 单尺度评估
- 2. 多尺度评估
- 3. 多裁剪图像评估
- 4. 卷积网络融合
- 5. 与最新技术比较
- (五)结论
- 二、论文笔记
-
- (一)网络结构
- (二)训练细节
- (三)测试细节
- 三、代码实现
-
- (一)VGG网络
- (二)utils.py
- (三)train.py
- (四)网络配置
-
- 1. VGG11
- 2. VGG13
- 3. VGG16
- 4. VGG19
- (五)实验结果
-
- 1. VGG11
- 2. VGG13
- 3. VGG16
- 4. VGG19
一、论文翻译
摘要
在这项工作中,我们研究了卷积网络深度在大规模的图像识别环境下对准确性的影响。我们的主要贡献是使用非常小的(3×3)卷积滤波器架构对网络深度的增加进行了全面评估,这表明通过将深度推到16-19加权层可以实现对现有技术配置的显著改进。这些发现是我们的ImageNet Challenge 2014提交论文的基础,我们的团队在定位和分类过程中分别获得了第一名和第二名。我们还表明,我们的表示对于其他数据集泛化的很好,在其它数据集上取得了最好的结果。我们使我们的两个性能最好的ConvNet模型可公开获得,以便进一步研究计算机视觉中深度视觉表示的使用。
(一)引言
卷积网络(ConvNets)近来在大规模图像和视频识别方面取得了巨大成功(Krizhevsky等,2012;Zeiler&Fergus,2013;Sermanet等,2014;Simonyan&Zisserman,2014),由于大的公开图像存储库,例如ImageNet,以及高性能计算系统的出现,例如GPU或大规模分布式集群(Dean等,2012)使这成为可能。特别是,在深度视觉识别架构的进步中,ImageNet大型视觉识别挑战(ILSVRC)(Russakovsky等,2014)发挥了重要作用,它已经成为几代大规模图像分类系统的测试平台,从高维度浅层特征编码(Perronnin等,2010)(ILSVRC-2011的获胜者)到深层ConvNets(Krizhevsky等,2012)(ILSVRC-2012的获奖者)。
随着ConvNets在计算机视觉领域越来越商品化,为了达到更好的准确性,已经进行了许多尝试来改进Krizhevsky等人(2012)最初的架构。例如,ILSVRC-2013(Zeiler&Fergus,2013;Sermanet等,2014)表现最佳的论文使用了更小的感受野窗口尺寸和第一卷积层更小的步长。另一条改进措施在整个图像和多个尺度上对网络进行密集地训练和测试(Sermanet等,2014;Howard,2014)。在本文中,我们讨论了ConvNet架构设计的另一个重要方面——其深度。为此,我们修正了架构的其它参数,并通过添加更多的卷积层来稳定地增加网络的深度,这是可行的,因为在所有层中使用非常小的(3×3)卷积滤波器。
因此,我们提出了更为精确的ConvNet架构,不仅可以在ILSVRC分类和定位任务上取得的最佳的准确性,而且还适用于其它的图像识别数据集,它们可以获得优异的性能,即使使用相对简单流程的一部分(例如,通过线性SVM分类深度特征而不进行微调)。我们发布了两款表现最好的模型1,以便进一步研究。
本文的其余部分组织如下。在第2节,我们描述了我们的ConvNet配置。图像分类训练和评估的细节在第3节,并在第4节中在ILSVRC分类任务上对配置进行了比较。第5节总结了论文。为了完整起见,我们还将在附录A中描述和评估我们的ILSVRC-2014目标定位系统,并在附录B中讨论了非常深的特征在其它数据集上的泛化。最后,附录C包含了主要的论文修订列表。
(二)ConvNet 配置
为了衡量ConvNet深度在公平环境中所带来的改进,我们所有的ConvNet层配置都使用相同的规则,灵感来自Ciresan等(2011);Krizhevsky等人(2012年)。在本节中,我们首先描述我们的ConvNet配置的通用设计(第2.1节),然后详细说明评估中使用的具体配置(第2.2节)。最后,我们的设计选择将在2.3节进行讨论并与现有技术进行比较。
1. 架构
在训练期间,我们的ConvNet的输入是固定大小的224×224 RGB图像。我们唯一的预处理是从每个像素中减去在训练集上计算的RGB均值。图像通过一堆卷积(conv.)层,我们使用感受野很小的滤波器:3×3(这是捕获左/右,上/下,中心概念的最小尺寸)。在其中一种配置中,我们还使用了1×1卷积滤波器,可以看作输入通道的线性变换(后面是非线性)。卷积步长固定为1个像素;卷积层输入的空间填充要满足卷积之后保留空间分辨率,即3×3卷积层的填充为1个像素。空间池化由五个最大池化层进行,这些层在一些卷积层之后(不是所有的卷积层之后都是最大池化)。在2×2像素窗口上进行最大池化,步长为2。
一堆卷积层(在不同架构中具有不同深度)之后是三个全连接(FC)层:前两个每个都有4096个通道,第三个执行1000维ILSVRC分类,因此包含1000个通道(一个通道对应一个类别)。最后一层是softmax层。所有网络中全连接层的配置是相同的。
所有隐藏层都配备了修正(ReLU(Krizhevsky等,2012))非线性。我们注意到,我们的网络(除了一个)都不包含局部响应归一化(LRN)(Krizhevsky等,2012):将在第4节看到,这种规范化并不能提高在ILSVRC数据集上的性能,但增加了内存消耗和计算时间。在应用的地方,LRN层的参数是(Krizhevsky等,2012)的参数。
2. 配置
本文中评估的ConvNet配置在表1中列出,每列一个。接下来我们将按网络名称(A-E)来表示网络。所有配置都遵循2.1节提出的通用设计,并且仅是深度不同:从网络A中的11个加权层(8个卷积层和3个全连接层)到网络E中的19个加权层(16个卷积层和3个全连接层)。卷积层的宽度(通道数)相当小,从第一层中的64开始,然后在每个最大池化层之后增加2倍,直到达到512。
表1. ConvNet配置(以列显示)。随着更多的层被添加,配置的深度从左(A)增加到右(E)(添加的层以粗体显示)。卷积层参数表示为“conv⟨感受野大小⟩-⟨通道数⟩”。为了简洁起见,不显示ReLU激活功能。
表2:参数数量(百万级别)
在表2中,我们列出了每个配置的参数数量。尽管深度很大,我们的网络中权重数量并不大于具有更大卷积层宽度和感受野的较浅网络中的权重数量(144M的权重在(Sermanet等人,2014)中)。
3. 讨论
我们的ConvNet配置与ILSVRC-2012(Krizhevsky等,2012)和ILSVRC-2013比赛(Zeiler&Fergus,2013;Sermanet等,2014)表现最佳的参赛提交中使用的ConvNet配置有很大不同。不是在第一卷积层中使用相对较大的感受野(例如,在(Krizhevsky等人,2012)中的11×11,步长为4,或在(Zeiler&Fergus,2013;Sermanet等,2014)中的7×7,步长为2),我们在整个网络使用非常小的3×3感受野,与输入的每个像素(步长为1)进行卷积。很容易看到两个3×3卷积层堆叠(没有空间池化)有5×5的有效感受野;三个这样的层具有7×7的有效感受野。那么我们获得了什么?例如通过使用三个3×3卷积层的堆叠来替换单个7×7层。首先,我们结合了三个非线性修正层,而不是单一的,这使得决策函数更具判别性。其次,我们减少参数的数量:假设三层3×3卷积堆叠的输入和输出有C个通道,堆叠卷积层的参数为 3 ( 3 2 C 2 ) = 27 C 2 3(3^2C^2)=27C^2 3(32C2)=27C2个权重;同时,单个7×7卷积层将需要 7 2 C 2 = 49 C 2 7^2C^2=49C^2 72C2=49C2个参数,即参数多81%。这可以看作是对7×7卷积滤波器进行正则化,迫使它们通过3×3滤波器(在它们之间注入非线性)进行分解。
结合1×1卷积层(配置C,表1)是增加决策函数非线性而不影响卷积层感受野的一种方式。即使在我们的案例下,1×1卷积基本上是在相同维度空间上的线性投影(输入和输出通道的数量相同),由修正函数引入附加的非线性。应该注意的是1×1卷积层最近在Lin等人(2014)的“Network in Network”架构中已经得到了使用。
Ciresan等人(2011)以前使用小尺寸的卷积滤波器,但是他们的网络深度远远低于我们的网络,并且他们没有在大规模的ILSVRC数据集上进行评估。Goodfellow等人(2014)在街道号码识别任务中采用深层ConvNets(11个权重层),并且其表明增加深度取得了更好的性能。GooLeNet(Szegedy等,2014)是ILSVRC-2014分类任务的表现最好的项目,是独立于我们工作之外开发的,但是类似的是它也是基于非常深的卷积网络(22个权重层)和小卷积滤波器(除了3×3,它们也使用了1×1和5×5卷积)。然而,它们的网络拓扑结构比我们的更复杂,并且在第一层中特征图的空间分辨率被大幅度地减少,以减少计算量。正如将在第4.5节显示的那样,我们的模型在单网络分类精度方面胜过Szegedy等人(2014)。
(三)分类框架
在上一节中,我们介绍了我们的网络配置的细节。在本节中,我们将介绍分类卷积网络训练和评估的细节。
1. 训练
ConvNet训练过程基本上遵循Krizhevsky等人(2012)的做法(除了从多尺度训练图像中对输入裁剪图像进行采样外,如下文所述)。也就是说,通过使用具有动量的小批量梯度下降(基于反向传播(LeCun等人,1989))优化多项式逻辑回归目标函数来进行训练。批量大小设为256,动量为0.9。训练通过权重衰减(L2惩罚乘子设定为 5 ⋅ 1 0 − 4 5⋅10^{−4} 5⋅10−4)进行正则化,前两个全连接层采取dropout正则化(dropout比率设定为0.5)。学习率初始设定为 1 0 − 2 10^{−2} 10−2,然后当验证集准确率停止改善时,学习率以10倍的比率进行减小。学习率总共降低3次,学习在37万次迭代后停止(74个epochs)。我们推测,尽管与(Krizhevsky等,2012)的网络相比我们的网络参数更多,网络的深度更深,但网络需要更小的epoch就可以收敛,这是由于(a)更大的深度和更小的卷积滤波器尺寸引起的隐式正则化,(b)某些层的预初始化。
网络权重的初始化是重要的,由于深度网络中梯度的不稳定,不好的初始化可能会阻碍学习。为了规避这个问题,我们开始训练配置A(表1)的网络,其深度足够浅故以随机初始化进行训练。然后,当训练更深的网络架构时,我们用网络A的层初始化前四个卷积层和最后三个全连接层(中间层被随机初始化)。我们没有减少预初始化层的学习率,允许他们在学习过程中改变。对于随机初始化(如果应用),我们从均值为0和方差为 1 0 − 2 10^{−2} 10−2的正态分布中采样权重。偏置初始化为零。值得注意的是,在提交论文之后,我们发现可以通过使用Glorot&Bengio(2010)的随机初始化程序来初始化权重而不进行预训练。
为了获得固定大小的 224×224 ConvNet 输入图像,它们从归一化的训练图像中被随机裁剪(每个图像每次SGD迭代进行一次裁剪)。为了进一步增强训练集,裁剪图像经过了随机水平翻转和随机RGB颜色偏移(Krizhevsky等,2012)。下面解释训练图像归一化。
训练图像大小。令S是等轴归一化的训练图像的最小边,ConvNet输入从S中裁剪(我们也将S称为训练尺度)。虽然裁剪尺寸固定为224×224,但原则上S可以是不小于224的任何值:对于S=224,裁剪图像将捕获整个图像的统计数据,完全扩展训练图像的最小边;对于S≫224,裁剪图像将对应于图像的一小部分,包含一个小对象或对象的一部分。
我们考虑两种方法来设置训练尺度S。第一种是修正对应单尺度训练的S(注意,采样裁剪图像中的图像内容仍然可以表示多尺度图像统计)。在我们的实验中,我们评估了以两个固定尺度训练的模型:S=256(已经在现有技术中广泛使用(Krizhevsky等人,2012;Zeiler&Fergus,2013;Sermanet等,2014))和S=384。
给定一个ConvNet配置,我们首先使用S=256来训练网络
。为了
加速S=384网络的训练,用S=256预训练的权重来进行初始化
,我们使用较小的初始学习率 1 0 − 3 10^{−3} 10−3。
设置S的第二种方法是多尺度训练,其中每个训练图像通过从一定范围[ S m i n , S m a x S_{min}, S_{max} Smin,Smax](我们使用 S m i n S_{min} Smin=256和 S m a x S_{max} Smax=512)随机采样S来单独进行归一化。由于图像中的目标可能具有不同的大小,因此在训练期间考虑到这一点是有益的。这也可以看作是通过尺度抖动进行训练集增强,其中单个模型被训练在一定尺度范围内识别对象。为了速度的原因,我们通过对具有相同配置的单尺度模型的所有层进行微调,训练了多尺度模型,并用固定的S=384进行预训练。
2. 测试
在测试时,给出训练的ConvNet和一个输入图像,它按以下方式分类。首先,将其等轴地归一化到预定义的最小图像边,表示为Q(我们也将其称为测试尺度)。我们注意到,Q不一定等于训练尺度S(正如我们在第4节中所示,每个S使用Q的几个值会改进性能)。然后,网络以类似于(Sermanet等人,2014)的方式密集地应用于归一化的测试图像上。即全连接层首先被转换成卷积层(第一FC层转换到7×7卷积层,最后两个FC层转换到1×1卷积层)。然后将所得到的全卷积网络应用于整个(未裁剪)图像上。结果是类得分图的通道数等于类别的数量,以及取决于输入图像大小的可变空间分辨率。最后,为了获得图像的类别分数的固定大小的向量,类得分图在空间上平均(和池化)。我们还通过水平翻转图像来增强测试集;将原始图像和翻转图像的softmax类后验进行平均,以获得图像的最终分数。
由于全卷积网络被应用在整个图像上,所以不需要在测试时对采样多个裁剪图像(Krizhevsky等,2012),因为它需要网络重新计算每个裁剪图像,这样效率较低。同时,如Szegedy等人(2014)所做的那样,使用大量的裁剪图像可以提高准确度,因为与全卷积网络相比,它使输入图像的采样更精细。此外,由于不同的卷积边界条件,多裁剪图像评估是密集评估的补充:当将ConvNet应用于裁剪图像时,卷积特征图用零填充,而在密集评估的情况下,相同裁剪图像的填充自然会来自于图像的相邻部分(由于卷积和空间池化),这大大增加了整个网络的感受野,因此捕获了更多的上下文。虽然我们认为在实践中,多裁剪图像的计算时间增加并不足以证明准确性的潜在收益,但作为参考,我们还在每个尺度使用50个裁剪图像(5×5规则网格,2次翻转)评估了我们的网络,在3个尺度上总共150个裁剪图像,与Szegedy等人(2014)在4个尺度上使用的144个裁剪图像。
3. 实现细节
我们的实现来源于公开的C++ Caffe工具箱(Jia,2013)(2013年12月推出),但包含了一些重大的修改,使我们能够对安装在单个系统中的多个GPU进行训练和评估,也能训练和评估在多个尺度上(如上所述)的全尺寸(未裁剪)图像。多GPU训练利用数据并行性,通过将每批训练图像分成几个GPU批次,每个GPU并行处理。在计算GPU批次梯度之后,将其平均以获得完整批次的梯度。梯度计算在GPU之间是同步的,所以结果与在单个GPU上训练完全一样。
最近提出了更加复杂的加速ConvNet训练的方法(Krizhevsky,2014),它们对网络的不同层之间采用模型和数据并行,但是我们发现我们概念上更简单的方案与使用单个GPU相比,在现有的4-GPU系统上已经达到3.75倍的加速。在配备四个NVIDIA Titan Black GPU的系统上,根据架构训练单个网络需要2-3周时间。
(四)分类实验
数据集。在本节中,我们介绍了ConvNet架构在ILSVRC-2012数据集(用于ILSVRC 2012-2014挑战)上实现的图像分类结果。数据集包括1000个类别的图像,并分为三组:训练集(130万张图像)、验证集(5万张图像)和测试集(留有类标签的10万张图像)。使用两个措施评估分类性能:top-1和top-5错误率。前者是多分类误差,即没有被正确分类图像的比例;后者是ILSVRC中使用的主要评估标准,即计算为图像真实类别在前5个预测类别之外的比例。
对于大多数实验,我们使用验证集作为测试集。在测试集上也进行了一些实验,并将其作为ILSVRC-2014竞赛(Russakovsky等,2014)“VGG”小组的输入提交到了官方的ILSVRC服务器。
1. 单尺度评估
我们首先评估单个ConvNet模型在单尺度上的性能,其层结构配置如2.2节中描述。测试图像大小设置如下:对于固定S的Q = S,对于抖动S ∈ [ S m i n S_{min} Smin, S m a x S_{max} Smax],Q = 0.5( S m i n S_{min} Smin + S m a x S_{max} Smax)。结果如表3所示。
表3:测试图像单尺度的ConvNet性能
首先,我们注意到,使用局部响应归一化网络(A-LRN网络)在没有任何归一化层的情况下,对模型A没有改善。因此,我们在较深的架构(B-E)中不采用归一化。
第二,我们观察到分类误差随着ConvNet深度的增加而减小:从A中的11层到E中的19层。值得注意的是,尽管深度相同,配置C(包含三个1×1卷积层)比在整个网络层中使用3×3卷积的配置D更差。这表明,虽然额外的非线性确实有帮助(C优于B),但也可以通过使用具有非平凡感受野(D比C好)的卷积滤波器来捕获空间上下文。当深度达到19层时,我们架构的错误率饱和,但更深的模型可能有益于较大的数据集。我们还将网络B与具有5×5卷积层的浅层网络进行了比较,这个浅层网络可以通过用单个5×5卷积层替换B中每对3×3卷积层得到(如第2.3节所述其具有相同的感受野)。测量的浅层网络top-1错误率比网络B的top-1错误率(在中心裁剪图像上)高7%,这证实了具有小滤波器的深层网络优于具有较大滤波器的浅层网络。
最后,训练时的尺度抖动(S ∈ [256;512])与固定最小边(S = 256 or S = 384)的图像训练相比更好的结果,即使在测试时使用单尺度。这证实了通过尺度抖动进行的训练集增强确实有助于捕获多尺度图像统计。
2. 多尺度评估
在单尺度上评估ConvNet模型后,我们现在评估测试时尺度抖动的影响。它包括在一张测试图像的几个归一化版本上运行模型(对应于不同的Q值),然后对所得到的类别后验进行平均。考虑到训练和测试尺度之间的巨大差异会导致性能下降,用固定S训练的模型在三个测试图像尺度上进行了评估,接近于训练一次:Q = {S−32, S, S+32}。同时,训练时的尺度抖动允许网络在测试时应用于更广的尺度范围,所以用变量S ∈ [ S m i n S_{min} Smin; S m a x S_{max} Smax]训练的模型在更大的尺寸范围Q = { S m i n S_{min} Smin, 0.5( S m i n S_{min} Smin + S m a x S_{max} Smax), S m a x S_{max} Smax}上进行评估。
表4中给出的结果表明,测试时的尺度抖动导致了更好的性能(与在单一尺度上相同模型的评估相比,如表3所示)。如前所述,最深的配置(D和E)表现最好,并且尺度抖动优于使用固定最小边S的训练。我们在验证集上的最佳单网络性能为24.8%/7.5% top-1/top-5的错误率(在表4中用粗体突出显示)。在测试集上,配置E实现了7.3% top-5的错误率。
表4:在多个测试尺度上的ConvNet性能
3. 多裁剪图像评估
在表5中,我们将密集ConvNet评估与多裁剪图像评估进行比较(细节参见第3.2节)。我们还通过平均其softmax输出来评估两种评估技术的互补性。可以看出,使用多裁剪图像表现比密集评估略好,而且这两种方法确实是互补的,因为它们的组合优于其中的每一种。如上所述,我们假设这是由于卷积边界条件的不同处理所造成的。
表5:ConvNet评估技术比较。在所有的实验中训练尺度S从[256;512]采样,采用三个测试适度Q:{256, 384, 512}。
4. 卷积网络融合
到目前为止,我们评估了ConvNet模型的性能。在这部分实验中,我们通过对softmax类别后验概率进行平均,结合了几种模型的输出。由于模型的互补性,提高了性能,并且将其在2012年(Krizhevsky等,2012)和2013年(Zeiler&Fergus,2013;Sermanet等,2014)ILSVRC的顶级提交中使用。
结果如表6所示。在ILSVRC提交的时候,我们只训练了单尺度网络,以及一个多尺度模型D(仅在全连接层进行微调而不是所有层)。由此产生的7个网络集成具有7.3%的ILSVRC测试误差。在提交之后,我们采用只有两个表现最好的多尺度模型(配置D和E)进行组合,使用密集评估将测试误差降低到7.0%,使用密集评估和多裁剪图像评估组合将测试误差降低到6.8%。作为参考,我们表现最佳的单模型达到7.1%的误差(模型E,表5)。
表6:多个卷积网络融合结果
5. 与最新技术比较
最后,我们在表7中与最新技术比较了我们的结果。在ILSVRC-2014竞赛的分类任务(Russakovsky等,2014)中,我们的“VGG”团队获得了第二名,使用7个模型集成取得了7.3%测试误差。提交后,我们使用2个模型集成将错误率降低到6.8%。
表7:在ILSVRC分类中与最新技术比较。我们的方法表示为“VGG”。报告的结果没有使用外部数据。
从表7可以看出,我们非常深的ConvNets显著优于前几代在ILSVRC-2012和ILSVRC-2013竞赛中取得了最好结果的模型。我们的结果相对于分类任务获胜者(GoogLeNet具有6.7%的错误率)也具有竞争力,并且大大优于ILSVRC-2013获胜者Clarifai的提交,其使用外部训练数据取得了11.2%的错误率,没有外部数据则为11.7%。这是非常显著的,考虑到我们最好的结果是仅通过组合两个模型实现的——明显少于大多数ILSVRC提交。在单网络性能方面,我们的架构取得了最好结果(7.0%测试误差),超过单个GoogLeNet 0.9%。值得注意的是,我们并没有偏离LeCun(1989)等人经典的ConvNet架构,但通过大幅增加深度改善了它。
(五)结论
在这项工作中,我们评估了非常深的卷积网络(最多19个权重层)用于大规模图像分类。已经证明,表示深度有利于分类精度,并且深度大大增加的传统ConvNet架构(LeCun等,1989;Krizhevsky等,2012)可以实现ImageNet挑战数据集上的最佳性能。在附录中,我们还呈现了我们的模型很好地泛化到各种各样的任务和数据集上,可以匹敌或超越更复杂的识别流程,其构建围绕不深的图像表示。我们的结果再次证实了深度在视觉表示中的重要性。
二、论文笔记
(一)网络结构
Notes:
1、VGG 使用 3 × 3 的卷积核和 2 × 2(stride=2)的池化。
去掉了大尺度卷积,改用小尺度卷积是因为,多个小尺度卷积的感受野,可以和一个大卷积的感受野相同,模型表达效果更强,且参数量更少。
2、网络分为(5大块卷积层 + 3个全连接层),不同的版本的 VGG 全连接层个数不发生改变,卷积层发生的个数改变。
- A(VGG11):11 - 3 = 8,前五层有8个卷积层(1 + 1 + 2 + 2 + 2)
- A - LRN:在A的基础上在第一个卷积层后加了LRN层
- B(VGG13):13 - 3 = 10,前五层有10个卷积层(2 + 2 + 2 + 2 + 2)
- C:16 - 3 = 13,前五层有13个卷积层(2 + 2 + 3 + 3 + 3)–> 在后三层加上 1 × 1 卷积
- D(VGG16):在C基础上把 1 × 1 卷积换成 3 × 3 卷积。
- E(VGG19):在D的后三层再加上 3 × 3 卷积
3、每一大块卷积层后接池化层,后通道数翻倍,最高翻倍为 512 × 512。
(二)训练细节
两种方式设置训练尺度S:
-
单尺度
固定训练尺度S大小(256, 384),训练时随机截取 224 × 224 的图像放入网络进行训练
-
多尺度(尺度抖动)
设置尺度区间[ S m i n , S m a x S_{min}, S_{max} Smin,Smax],在这个区间对原始图像进行resize,并随机截取 224 × 224 的图像放入网络训练
(三)测试细节
将网络最后的三个FC层换为卷积层(全卷积网络),实现可以接收不同尺度的输入图像。
具体原理如下:
- 第一个FC层:用和从前一层输出的feature map大小相同的卷积核进行卷积(参数量和原来相同)
- 第二个FC层:用 1 × 1 卷积
- 第三个FC层:用 1 × 1 卷积
也就是,FC层转卷积层分为两种情况
-
当前FC层前面为卷积层
使用和前一层输出的feature map相同大小的卷积核进行卷积
-
当前FC层前面为FC层
使用 1 × 1 卷积核进行卷积
为什么要这样做?
举个例子:
假设最后一个卷积层输出的卷积核的大小为7 × 7,我们可以用 7 × 7 的卷积层来替换FC层。
如果输入图像的大小发生改变,我们可能拿到了 8 × 8 或 14 × 14 的卷积,不要紧,我们可以调整原来 7 × 7 卷积核的步长,把步长调成2就行了,后面网络可以继续使用。
如果使用之前的FC层,假设最后一个卷积层的输出为 7 × 7 × 256,那么我们首先需要将这些 feature map 拉长,拉成一长条,也就是 7 × 7 × 256 = 12544 个神经元和最后一层比如1024个神经元进行全连接。那么需要的参数矩阵的形状就是(12544 × 1024),如果我们改变了图像的输入大小,那么最后一个卷积层的输出的feature map 的形状就不是 7 × 7,比如是 8 × 8。那么和第一个FC层进行全连接就需要8 × 8 × 256 也就是我们需要( 16384 × 1024)这样形状的权重矩阵,这样就没法实现图像不同大小的输入,因为不能保证最后一个卷积层的输出一定是 7 × 7 的。
三、代码实现
(一)VGG网络
因为 CIRAE10 数据集比较小,为了防止过拟合,FC层我们只是用1层(512)
import torch.nn as nn
__all__ = ["vgg11", "vgg13", "vgg16", "vgg19"]
config = {
'A': [64, 'M', 128, 'M', \
256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'B': [64, 64, 'M', 128, 128, 'M', \
256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'D': [64, 64, 'M', 128, 128, 'M', \
256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'E': [64, 64, 'M', 128, 128, 'M', \
256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M']
}
class VGG(nn.Module):
def __init__(self, config, num_classes, batch_norm=True):
super(VGG, self).__init__()
self.features = self.__make_layers(config, batch_norm)
self.classifer = nn.Linear(512, num_classes)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifer(x)
return x
def __make_layers(self, config, batch_norm):
layers = list()
in_channels = 3
for v in config:
if isinstance(v, int):
conv = nn.Conv2d(in_channels, v, 3, 1, 1)
if batch_norm:
layers += [conv, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv, nn.ReLU(inplace=True)]
in_channels = v
else:
layers.append(nn.MaxPool2d(2))
return nn.Sequential(*layers)
def vgg11(num_classes):
return VGG(config['A'], num_classes)
def vgg13(num_classes):
return VGG(config['B'], num_classes)
def vgg16(num_classes):
return VGG(config['D'], num_classes)
def vgg19(num_classes):
return VGG(config['E'], num_classes)
(二)utils.py
import time
import torch
import torchvision
import torch.optim as optim
import torch.utils.data as Data
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
def init_dataset(config, transform_train, transform_test, train_set=None, test_set=None):
"""
Load data set and preprocess
:param config: Configuration file's edict
:param transform_train: transforms.Compose()
:param transform_test: transforms.Compose()
:param train_set: Custom training set
:param test_set: Custom test set
:return: DataLoader objects of train set and test set
"""
if hasattr(torchvision.datasets, config.data_name):
dataset = getattr(torchvision.datasets, config.data_name)
train_set = dataset(
root=config.train_set_path, train=True,
download=True, transform=transform_train
)
test_set = dataset(
root=config.test_set_path, train=False,
download=True, transform=transform_test
)
else:
assert train_set and test_set is not None, \
"If config.data_name is not in torchvision.datasets\
you need to pass in the custom train_set and test_set!"
assert config.train_annotation and config.test_annotation is not None, \
"Custom datasets need to be filled with \
config.train_annotation and config.test_annotation"
train_set = train_set(
root=config.train_set_path,
transform=transform_train,
annotation_path=config.train_annotation
)
test_set = test_set(
root=config.test_set_path,
transform=transform_test,
annotation_path=config.test_annotation
)
train_iter = torch.utils.data.DataLoader(
train_set, batch_size=config.batch_size,
shuffle=True, num_workers=config.num_workers
)
test_iter = torch.utils.data.DataLoader(
test_set, batch_size=config.test_batch,
shuffle=True, num_workers=config.num_workers
)
return train_iter, test_iter
def data_augmentation(config, is_train=True):
"""
Generate a series of data enhancement operations from the configuration file
:param config: Configuration file's edict
:param is_train: Whether the training
:return: transforms.Compose()
"""
aug = list()
if is_train:
if config.augmentation.random_crop:
aug.append(transforms.RandomCrop(config.input_size, 4))
if config.augmentation.random_horizontal_filp:
aug.append(transforms.RandomHorizontalFlip())
aug.append(transforms.ToTensor())
if config.augmentation.normalize:
if config.data_name == 'CIFAR10':
aug.append(transforms.Normalize(
(0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)))
else:
aug.append(transforms.Normalize(
(0.5071, 0.4867, 0.4408), (0.2675, 0.2565, 0.2761)))
return transforms.Compose(aug)
def get_optimizer(config, net):
"""
Generate the optimizer from the configuration file
:param config: Configuration file's edict
:param net: Your network
:return: nn.optim
"""
optimizer = getattr(optim, config.optimizer.name)
return optimizer(net.parameters(), **config.optimizer.params)
def train(config, net, train_iter, criterion, optimizer, test_iter=None):
"""
Training neural network
:param config: Configuration file's edict
:param net: Your network
:param train_iter: torch.utils.data.DataLoader
:param criterion: Loss function
:param optimizer: nn.optim
:param test_iter: torch.utils.data.DataLoader (If you don't fill not tested)
:return: None
"""
global best_acc
best_acc = 0
if config.eval_freq > 0:
assert test_iter is not None, \
"If you need to test, pass in test_iter"
net.train()
record_train = list()
record_test = list()
for epoch in range(config.num_epochs):
print("========== epoch: [{}/{}] ==========".format(epoch + 1, config.num_epochs))
total, correct, train_loss = 0, 0, 0
start = time.time()
for i, (X, y) in enumerate(train_iter):
X, y = X.to(config.device), y.to(config.device)
output = net(X)
loss = criterion(output, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
total += y.size(0)
correct += (output.argmax(dim=1) == y).sum().item()
train_acc = 100.0 * correct / total
if (i + 1) % config.num_print == 0:
print("step: [{}/{}], train_loss: {:.3f} | train_acc: {:6.3f}% | lr: {:.6f}" \
.format(i + 1, len(train_iter), train_loss / (i + 1), \
train_acc, get_cur_lr(optimizer)))
print("--- cost time: {:.4f}s ---".format(time.time() - start))
adjust_learning_rate(config, epoch, optimizer)
if (epoch + 1) % config.eval_freq == 0:
record_test.append(test(config, net, test_iter, criterion))
record_train.append(train_acc)
if (epoch + 1) % config.learning_curve.draw_freq == 0:
learning_curve(config, record_train, record_test)
print("########## best accuracy: {:6.3f}% ##########".format(best_acc))
def test(config, net, test_iter, criterion):
"""
Test the training results
:param config: Configuration file's edict
:param net: Your network
:param test_iter: torch.utils.data.DataLoader
:param criterion: Loss function
:return: Accuracy on the test set
"""
global best_acc
total, correct = 0, 0
net.eval()
with torch.no_grad():
print("*************** test ***************")
for X, y in test_iter:
X, y = X.to(config.device), y.to(config.device)
output = net(X)
loss = criterion(output, y)
total += y.size(0)
correct += (output.argmax(dim=1) == y).sum().item()
test_acc = 100.0 * correct / total
print("test_loss: {:.3f} | test_acc: {:6.3f}%"\
.format(loss.item(), test_acc))
print("************************************\n")
best_acc = max(best_acc, test_acc)
net.train()
return test_acc
def learning_curve(config, record_train, record_test=None):
"""
Draw a learning curve
:param config: Configuration file's edict
:param record_train: Training set accuracy record
:param record_test: Test set accuracy record
:return: None
"""
plt.style.use(config.learning_curve.style)
plt.plot(range(1, len(record_train) + 1), record_train, marker='.', label="train acc")
if record_test is not None:
plt.plot(range(config.eval_freq, len(record_train) + config.eval_freq, config.eval_freq), \
record_test, marker='.', label="test acc")
plt.legend(loc=4)
plt.title("{} learning curve ({})".format(config.architecture, config.data_name))
plt.xticks(range(0, len(record_train) + 1, 5))
plt.yticks(range(0, 101, 5))
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.show()
def get_cur_lr(optimizer):
"""
Get the current learning rate
:param optimizer: nn.optim
:return: None
"""
for param_group in optimizer.param_groups:
return param_group['lr']
def adjust_learning_rate(config, epoch, optimizer):
"""
Adjust the learning rate according to the configuration file
:param config: Configuration file's edict
:param epoch: current epoch
:param optimizer: nn.optim
:return: None
"""
lr = get_cur_lr(optimizer)
if config.lr_scheduler.type == "STEP":
if (epoch + 1) in config.lr_scheduler.lr_epochs:
lr *= config.lr_scheduler.lr_mults
for param_group in optimizer.param_groups:
param_group['lr'] = lr
(三)train.py
import os
import yaml
import torch
import argparse
from model import *
from utils import *
import torch.nn as nn
import torch.optim as optim
from easydict import EasyDict as edict
parser = argparse.ArgumentParser(description="=== PyTorch Image Classification ===")
parser.add_argument("-c", "--config_path", required=True, help="configuration file path")
parser.add_argument("-r", "--resume", action="store_true", help="resume from checkpoint")
args = parser.parse_args()
with open(os.path.join(args.config_path, 'config.yaml')) as f:
config = edict(yaml.load(f, yaml.FullLoader))
def main():
net = get_model(config)
net = net.to(config.device)
transform_train = data_augmentation(config)
transform_test = data_augmentation(config, False)
train_iter, test_iter = init_dataset(config, transform_train, transform_test)
criterion = nn.CrossEntropyLoss()
optimizer = get_optimizer(config, net)
train(config, net, train_iter, criterion, optimizer, test_iter)
if __name__ == '__main__':
main()
(四)网络配置
以下四种网络的参数设置相同
1. VGG11
# net architecture
architecture: vgg11
# log and checkpoint
log_path: ./
ckpt_path: ./
ckpt_name: vgg11
# dataset
data_name: CIFAR10
num_classes: 10
train_set_path: ./data/cifar-10
test_set_path: ./data/cifar-10
train_annotation: ~
test_annotation: ~
# data augmentation
augmentation:
normalize: True
random_crop: True
random_horizontal_filp: True
# train
device: cuda
input_size: 32
batch_size: 128
num_epochs: 40
num_print: 100
num_workers: 4
eval_freq: 1
# test
test_batch: 200
# optimizer
optimizer:
name: SGD
params:
lr: 0.1
momentum: 0.9
weight_decay: 0.0008
nesterov: True
# learning rate scheduler
lr_scheduler:
type: STEP
lr_epochs: [20, 30]
lr_mults: 0.1
# draw
learning_curve:
style: ggplot
draw_freq: 10
2. VGG13
# net architecture
architecture: vgg13
# log and checkpoint
log_path: ./
ckpt_path: ./
ckpt_name: vgg13
# dataset
data_name: CIFAR10
num_classes: 10
train_set_path: ./data/cifar-10
test_set_path: ./data/cifar-10
train_annotation: ~
test_annotation: ~
# data augmentation
augmentation:
normalize: True
random_crop: True
random_horizontal_filp: True
# train
device: cuda
input_size: 32
batch_size: 128
num_epochs: 40
num_print: 100
num_workers: 4
eval_freq: 1
# test
test_batch: 200
# optimizer
optimizer:
name: SGD
params:
lr: 0.1
momentum: 0.9
weight_decay: 0.0008
nesterov: True
# learning rate scheduler
lr_scheduler:
type: STEP
lr_epochs: [20, 30]
lr_mults: 0.1
# draw
learning_curve:
style: ggplot
draw_freq: 10
3. VGG16
# net architecture
architecture: vgg16
# log and checkpoint
log_path: ./
ckpt_path: ./
ckpt_name: vgg16
# dataset
data_name: CIFAR10
num_classes: 10
train_set_path: ./data/cifar-10
test_set_path: ./data/cifar-10
train_annotation: ~
test_annotation: ~
# data augmentation
augmentation:
normalize: True
random_crop: True
random_horizontal_filp: True
# train
device: cuda
input_size: 32
batch_size: 128
num_epochs: 40
num_print: 100
num_workers: 4
eval_freq: 1
# test
test_batch: 200
# optimizer
optimizer:
name: SGD
params:
lr: 0.1
momentum: 0.9
weight_decay: 0.0008
nesterov: True
# learning rate scheduler
lr_scheduler:
type: STEP
lr_epochs: [20, 30]
lr_mults: 0.1
# draw
learning_curve:
style: ggplot
draw_freq: 10
4. VGG19
# net architecture
architecture: vgg19
# log and checkpoint
log_path: ./
ckpt_path: ./
ckpt_name: vgg19
# dataset
data_name: CIFAR10
num_classes: 10
train_set_path: ./data/cifar-10
test_set_path: ./data/cifar-10
train_annotation: ~
test_annotation: ~
# data augmentation
augmentation:
normalize: True
random_crop: True
random_horizontal_filp: True
# train
device: cuda
input_size: 32
batch_size: 128
num_epochs: 40
num_print: 100
num_workers: 4
eval_freq: 1
# test
test_batch: 200
# optimizer
optimizer:
name: SGD
params:
lr: 0.1
momentum: 0.9
weight_decay: 0.0008
nesterov: True
# learning rate scheduler
lr_scheduler:
type: STEP
lr_epochs: [20, 30]
lr_mults: 0.1
# draw
learning_curve:
style: ggplot
draw_freq: 10
(五)实验结果
模型名称 | 最高准确率(40 epoch) |
---|---|
VGG11 | 90.400% |
VGG13 | 92.170% |
VGG16 | 92.560% |
VGG19 | 91.470% |
我们可以看到,当我们控制超参数相同,且FC层相同时,VGG16的准确率最高,VGG19的准确率甚至还没VGG16高,这说明,过多的高维度特征对于cifar10来说是多余的。和官方的结论一样,VGG16是性价比很高的模型。
1. VGG11
========== epoch: [38/40] ==========
step: [100/391], train_loss: 0.093 | train_acc: 96.812% | lr: 0.001000
step: [200/391], train_loss: 0.093 | train_acc: 96.875% | lr: 0.001000
step: [300/391], train_loss: 0.093 | train_acc: 96.826% | lr: 0.001000
--- cost time: 7.3621s ---
*************** test ***************
test_loss: 0.263 | test_acc: 90.210%
************************************
========== epoch: [39/40] ==========
step: [100/391], train_loss: 0.085 | train_acc: 97.414% | lr: 0.001000
step: [200/391], train_loss: 0.086 | train_acc: 97.250% | lr: 0.001000
step: [300/391], train_loss: 0.087 | train_acc: 97.161% | lr: 0.001000
--- cost time: 7.1707s ---
*************** test ***************
test_loss: 0.375 | test_acc: 90.390%
************************************
========== epoch: [40/40] ==========
step: [100/391], train_loss: 0.085 | train_acc: 97.234% | lr: 0.001000
step: [200/391], train_loss: 0.085 | train_acc: 97.277% | lr: 0.001000
step: [300/391], train_loss: 0.087 | train_acc: 97.143% | lr: 0.001000
--- cost time: 7.1803s ---
*************** test ***************
test_loss: 0.346 | test_acc: 90.400%
************************************
########## best accuracy: 90.400% ##########
2. VGG13
========== epoch: [38/40] ==========
step: [100/391], train_loss: 0.064 | train_acc: 97.977% | lr: 0.001000
step: [200/391], train_loss: 0.062 | train_acc: 98.008% | lr: 0.001000
step: [300/391], train_loss: 0.063 | train_acc: 97.969% | lr: 0.001000
--- cost time: 10.9730s ---
*************** test ***************
test_loss: 0.235 | test_acc: 92.160%
************************************
========== epoch: [39/40] ==========
step: [100/391], train_loss: 0.064 | train_acc: 97.891% | lr: 0.001000
step: [200/391], train_loss: 0.061 | train_acc: 98.031% | lr: 0.001000
step: [300/391], train_loss: 0.061 | train_acc: 98.052% | lr: 0.001000
--- cost time: 10.9629s ---
*************** test ***************
test_loss: 0.184 | test_acc: 92.080%
************************************
========== epoch: [40/40] ==========
step: [100/391], train_loss: 0.057 | train_acc: 98.055% | lr: 0.001000
step: [200/391], train_loss: 0.057 | train_acc: 98.125% | lr: 0.001000
step: [300/391], train_loss: 0.056 | train_acc: 98.141% | lr: 0.001000
--- cost time: 10.9675s ---
*************** test ***************
test_loss: 0.352 | test_acc: 92.110%
************************************
########## best accuracy: 92.170% ##########
3. VGG16
========== epoch: [38/40] ==========
step: [100/391], train_loss: 0.072 | train_acc: 97.609% | lr: 0.001000
step: [200/391], train_loss: 0.072 | train_acc: 97.691% | lr: 0.001000
step: [300/391], train_loss: 0.073 | train_acc: 97.630% | lr: 0.001000
--- cost time: 12.8894s ---
*************** test ***************
test_loss: 0.303 | test_acc: 92.290%
************************************
========== epoch: [39/40] ==========
step: [100/391], train_loss: 0.071 | train_acc: 97.578% | lr: 0.001000
step: [200/391], train_loss: 0.070 | train_acc: 97.695% | lr: 0.001000
step: [300/391], train_loss: 0.070 | train_acc: 97.648% | lr: 0.001000
--- cost time: 13.1164s ---
*************** test ***************
test_loss: 0.354 | test_acc: 92.560%
************************************
========== epoch: [40/40] ==========
step: [100/391], train_loss: 0.070 | train_acc: 97.672% | lr: 0.001000
step: [200/391], train_loss: 0.068 | train_acc: 97.762% | lr: 0.001000
step: [300/391], train_loss: 0.068 | train_acc: 97.781% | lr: 0.001000
--- cost time: 12.8782s ---
*************** test ***************
test_loss: 0.219 | test_acc: 92.200%
************************************
########## best accuracy: 92.560% ##########
4. VGG19
========== epoch: [38/40] ==========
step: [100/391], train_loss: 0.093 | train_acc: 97.086% | lr: 0.001000
step: [200/391], train_loss: 0.095 | train_acc: 97.000% | lr: 0.001000
step: [300/391], train_loss: 0.096 | train_acc: 96.971% | lr: 0.001000
--- cost time: 14.7486s ---
*************** test ***************
test_loss: 0.290 | test_acc: 91.470%
************************************
========== epoch: [39/40] ==========
step: [100/391], train_loss: 0.094 | train_acc: 96.969% | lr: 0.001000
step: [200/391], train_loss: 0.090 | train_acc: 97.152% | lr: 0.001000
step: [300/391], train_loss: 0.090 | train_acc: 97.094% | lr: 0.001000
--- cost time: 14.7461s ---
*************** test ***************
test_loss: 0.252 | test_acc: 91.250%
************************************
========== epoch: [40/40] ==========
step: [100/391], train_loss: 0.081 | train_acc: 97.359% | lr: 0.001000
step: [200/391], train_loss: 0.085 | train_acc: 97.223% | lr: 0.001000
step: [300/391], train_loss: 0.085 | train_acc: 97.203% | lr: 0.001000
--- cost time: 14.7547s ---
*************** test ***************
test_loss: 0.296 | test_acc: 91.300%
************************************
########## best accuracy: 91.470% ##########