VGG: Very deep convolutional networks for large-scale image recognition大规模图像识别的深度卷积神经网络


在这里插入图片描述
在这里插入图片描述

VGG全文翻译

在这里插入图片描述

论文结构

在这里插入图片描述
在这里插入图片描述

摘要

在这里插入图片描述
在这项工作中,我们研究了卷积网络深度对其在大规模图像识别设置中的准确性的影响。 我们的主要贡献是使用具有非常小的(3×3)卷积滤波器的架构对增加深度的网络进行彻底评估,这表明通过将深度推至 16-19 权重层可以实现对现有技术配置的显着改进。 这些发现是我们提交 2014 年 ImageNet 挑战赛的基础,我们的团队分别在定位和分类领域获得了第一名和第二名。 我们还表明,我们的表示可以很好地推广到其他数据集,并取得最先进的结果。 我们公开了两个性能最好的 ConvNet 模型(VGG16和VGG19),以促进在计算机视觉中使用深度视觉表示的进一步研究。

在这里插入图片描述

1 引言

在这里插入图片描述
卷积网络(ConvNets)最近在大规模图像和视频识别方面取得了巨大成功(Krizhevsky et al., 2012; Zeiler & Fergus, 2013; Sermanet et al., 2014;Simonyan & Zisserman, 2014),这已成为可能,由于大型公共图像存储库,如 ImageNet(Deng 等,2009)和高性能计算系统,如 GPU 或大规模分布式集群(Dean 等,2012)。 特别是,ImageNet 大规模视觉识别挑战赛(ILSVRC)(Russakovsky 等,2014)在深度视觉识别架构的进步中发挥了重要作用,该挑战赛已成为几代大型视觉识别挑战赛的测试平台在大尺度图像分类系统,从高维浅层特征编码(Perronnin et al., 2010)(ILSVRC-2011 的获胜者)到深度 ConvNets(Krizhevsky et al., 2012)(ILSVRC-2012 的获胜者)。

随着 ConvNet 在计算机视觉领域变得越来越普及,人们做出了许多尝试来改进Krizhevsky 等人的原始架构(2012)以达到更好的准确性。 例如,ILSVRC-2013 中表现最好的提交(Zeiler & Fergus,2013;Sermanet et al.,2014)使用了较小的接收窗口大小和较小的第一个卷积层的步长。 另一条改进涉及在整个图像和多个尺度上密集地训练和测试网络(Sermanet et al., 2014;Howard, 2014)。 在本文中,我们讨论了ConvNet 架构设计的另一个重要方面——深度。 为此,我们固定了架构的其他参数,并通过添加更多的卷积层来稳定地增加网络的深度,由于在所有层中使用非常小的(3×3)卷积滤波器,这是可行的。

因此,我们提出了更加精确的 ConvNet 架构,它不仅在 ILSVRC 分类和定位任务上实现了最先进的精度,而且还适用于其他图像识别数据集,甚至在当用作相对简单的管道的一部分时(例如,由线性 SVM 分类的深层特征,无需微调)。 我们发布了两个性能最佳的模型1以促进进一步的研究。
在这里插入图片描述
本文的其余部分安排如下。 在第二部分 ,我们描述了我们的 ConvNet 配置。图像分类训练和评估的详细信息将在第 3 节中介绍。并在第 4 节中的 ILSVRC 分类任务上对配置进行了比较。第5节总结了本文。 为了完整起见,我们还在附录 A 中描述和评估了我们的 ILSVRC-2014 目标定位系统,并在附录 B 中讨论了非常深入的特征到其他数据集的泛化。最后,附录 C 包含了主要论文修订的列表。

2 卷积网络配置

在这里插入图片描述
为了在公平的环境中衡量增加的 ConvNet 深度所带来的改进,受 Ciresan 等人(2011)的启发,我们所有的 ConvNet 层配置都使用相同的原则进行设计; Krizhevsky 等人(2012)。 在本节中,我们首先描述 ConvNet 配置的通用布局(第 2.1 节),然后详细介绍评估中使用的具体配置(第 2.2 节)。然后讨论我们的设计选择,并与第 2.3节中的现有技术进行比较。

2.1 网络结构

在这里插入图片描述
在训练期间,ConvNet 的输入是固定大小的 224 × 224 RGB 图像。 我们所做的唯一预处理是从每个像素中减去在训练集上计算的平均 RGB 值。 图像通过一堆卷积 (conv.) 层,其中我们使用具有非常小的感受野的滤波器:3 × 3(这是捕捉左/右、上/下、中心概念的最小尺寸)。 在其中一种配置中,我们还利用 1 × 1 卷积滤波器,这可以看作是输入通道的线性变换(随后是非线性)。 卷积步幅固定为1像素; 卷积层输入的空间填充使得空间分辨率在卷积后得以保留,即对于 3 × 3 卷积层,填充为 1 像素。 空间池化由五个最大池化层执行,这些层位于一些转换层之后(并非所有转换层都遵循最大池化)。 最大池化在 2 × 2 像素窗口上执行,步长为 2。
在这里插入图片描述
一堆卷积层(在不同的架构中具有不同的深度)后面是三个全连接(FC)层:前两个层各有 4096 个通道,第三个执行 1000 路 ILSVRC 分类,因此包含 1000 个通道(每个针对一个类)。 最后一层是 soft-max 层。 所有网络中全连接层的配置都是相同的。

所有隐藏层都配备了非线性校正(ReLU(Krizhevsky et al., 2012))。我们注意到我们的网络(除了一个)都没有包含局部响应归一化(LRN)归一化(Krizhevsky et al., 2012) ):如第 4 节所示,这种归一化并没有提高ILSVRC数据集上的性能,而是导致内存消耗和计算时间增加。 在适用的情况下,LRN 层的参数是(Krizhevsky 等人,2012)的参数。

2.2 网络配置

在这里插入图片描述
表 1 概述了本文评估的 ConvNet 配置,每列一个。 下面我们将用它们的名称(A-E)来指代网络。 所有配置均遵循第 2 .1节中介绍的通用设计,仅在深度上有所不同:从网络 A 中的 11 个权重层(8 个卷积层和 3 个 FC 层)到网络 E 中的 19 个权重层(16 个卷积层和 3 个 FC 层)。 卷积层的宽度(通道数)相当小,从第一层的 64 开始,然后在每个最大池化层之后增加 2 倍,直到达到 512。

在表 2 中,我们报告了每种配置的参数数量。 尽管深度很大,但我们网络中的权重数量并不大于具有较大卷积层宽度和感受野的较浅网络中的权重数量(Sermanet et al., 2014)中的 144M 权重)
在这里插入图片描述
表 1:ConvNet 配置(如列所示)。 随着更多层的添加(添加的层以粗体显示),配置的深度从左 (A) 到右 (E) 增加。 卷积层参数表示为“conv<感受野大小> -<通道数>”。 为了简洁起见,未显示 ReLU 激活函数。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

VGG16参数计算

在这里插入图片描述

VGG16结构示意图

在这里插入图片描述

VGG特点

在这里插入图片描述
在这里插入图片描述

2.3 探讨

在这里插入图片描述
我们的 ConvNet 配置与 ILSVRC-2012(Krizhevsky 等人,2012)和 ILSVRC-2013 竞赛(Zeiler & Fergus,2013;Sermanet 等人,2014)的最佳表现参赛作品中使用的配置有很大不同。 而不是在第一个卷积层中使用相对较大的感受野(例如,(Krizhevsky et al., 2012)中的 11×11,步幅为 4,或者(Zeiler & Fergus,2013;Sermanet 等人,中的 7×7,步幅为 2), 2014)),我们在整个网络中使用非常小的 3 × 3 感受野,这些感受野与每个像素的输入进行卷积(步幅为 1)。 很容易看出,两个 3×3 卷积层的堆叠(中间没有空间池化)的有效感受野为 5×5; 三个这样的3x3的卷积层具有 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的权重,与此同时,一个7x7卷积层将需要 7 2 C 2 = 49 C 2 7^2C^2=49C^2 72C2=49C2个参数,即多81%. 这可以看作是对 7 × 7 卷积滤波器进行正则化,迫使它们通过 3 × 3 滤波器进行分解(在其间注入非线性)。

参数(设输入输出的通道数为C):
三个3×3的卷积核所需参数:
计算其实就是输入通道数x卷积核大小x卷积核大小x输出通道数
3×3×C×C+3×3×C×C+3×3×C×C= 27 C 2 27C^2 27C2
一个7×7的卷积核所需参数:
7×7×C×C= 49 C 2 49C^2 49C2
很明显使用多个小卷积核堆叠在一起,比使用一个大卷积核所需要的参数少。

在这里插入图片描述

在这里插入图片描述
合并 1 × 1 卷积层(配置 C,表 1)是一种增加决策函数非线性而不影响卷积层感受野的方法。 尽管在我们的例子中,1 × 1 卷积本质上是相同维度空间的线性投影(输入和输出通道的数量相同),但校正函数会引入额外的非线性。 值得注意的是,Lin 等人的“Network in Network”架构最近使用了 1×1 卷积层(2014)

在这里插入图片描述
Ciresan 等人之前曾使用过小尺寸卷积滤波器 (2011),但他们的网络深度明显低于我们的网络,并且他们没有在大规模 ILSVRC 数据集上进行评估。 Goodfellow等人 (2014) 将深度 ConvNets(11 个权重层)应用于街道号码识别任务,并表明深度的增加可以带来更好的性能。 GoogLeNet (Szegedy et al., 2014) 是 ILSVRC-2014 分类任务中表现最好的一个,它是独立于我们的工作而开发的,但相似之处在于它基于非常深的 ConvNets(22 个权重层)和小卷积滤波器(除了 3 × 3 之外,它们还使用 1 × 1 和 5 × 5 卷积)。 然而,他们的网络拓扑比我们的更复杂,并且在第一层中更积极地降低特征图的空间分辨率以减少计算量。 正如将在第4.5节中所示,我们的模型优于 Szegedy 等人的模型(2014)在单网络分类精度方面。

在这里插入图片描述
在这里插入图片描述

3 分类框架

在这里插入图片描述
在上一节中,我们介绍了网络配置的详细信息。 在本节中,我们将描述分类 ConvNet 训练和评估的细节。

在这里插入图片描述

3.1 训练

在这里插入图片描述
ConvNet 训练程序通常遵循 Krizhevsky 等人的方法(2012)(除了从多尺度训练图像中对输入crops进行采样,稍后将对此进行解释)。 也就是说,训练是通过使用带有动量的小批量梯度下降(基于反向传播(LeCun et al., 1989))优化多项逻辑回归目标来进行的。 批量大小设置为 256,动量设置为 0.9。 训练通过权重衰减( L 2 L_2 L2 惩罚乘数设置为 5 ⋅ 1 0 − 4 5·10^{−4} 5104)和前两个全连接层的 dropout 正则化(dropout 比率设置为 0.5)进行正则化。学习率最初设置为 1 0 − 2 10^{−2} 102, 然后当验证集准确性停止提高时,学习率就下降 10 倍。 总共,学习率降低了3次,并且在370K迭代(74个epoch)后停止学习。 我们推测,尽管与(Krizhevsky et al., 2012)相比,我们的网络有更多的参数和更大的深度,但网络需要更少的时间来收敛由于 (a) 更大的深度和更小的卷积滤波器尺寸所施加的隐式正则化; (b) 某些层的预初始化

网络权重的初始化很重要,因为糟糕的初始化可能会由于深度网络中梯度的不稳定而导致学习停滞。 为了解决这个问题,我们从训练配置 A(表 1)开始,该配置足够浅,可以通过随机初始化进行训练。 然后,在训练更深层次的架构时,我们用网络 A 的层初始化前四个卷积层和最后三个全连接层(中间层随机初始化)。 我们没有降低预初始化层的学习率,允许它们在学习过程中发生变化。 对于随机初始化(如果适用),我们从均值为零、方差为 1 0 − 2 10^{−2} 102 的正态分布中对权重进行采样。 偏差初始化为零。 值得注意的是,在论文提交后,我们发现可以使用 Glorot & Bengio (2010) 的随机初始化程序来初始化权重,而无需预训练。

为了获得固定大小的 224×224 ConvNet 输入图像,它们是从重新缩放的训练图像中随机裁剪的(每次 SGD 迭代每个图像一次裁剪)为了进一步增强训练集the crops经历了随机水平翻转和随机 RGB 颜色偏移(Krizhevsky 等人,2012)。 训练图像缩放解释如下。

训练图像尺寸

令 S 为各向同性重新缩放训练图像的最小边,从中裁剪 ConvNet 输入(我们也将 S 称为训练尺度)。 虽然裁剪尺寸固定为 224 × 224,但原则上 S 可以采用不小于 224 的任何值:对于 S = 224,裁剪将捕获整个图像统计数据,完全跨越训练图像的最小边;对于 S ≫ 224,裁剪将对应于图像的一小部分,包含小对象或对象部分。

我们考虑两种设置训练尺度 S 的方法。第一种是固定 S,它对应于单尺度训练(请注意,采样crops中的图像内容仍然可以代表多尺度图像统计数据)。 在我们的实验中,我们评估了在两个固定尺度上训练的模型:S = 256(在现有技术中已广泛使用(Krizhevsky et al., 2012; Zeiler & Fergus, 2013; Sermanet et al., 2014))和 S = 384。给定 ConvNet 配置,我们首先使用 S = 256 训练网络。为了加速 S = 384 网络的训练,它使用 S = 256 预训练的权重进行初始化,并且我们使用了较小的初始值学习率为 1 0 − 3 10^{−3} 103

设置 S 的第二种方法是多尺度训练,其中每个训练图像通过从一定范围 [Smin,Smax] 中随机采样 S 来单独重新缩放(我们使用 S m i n S_{min} Smin = 256 和 S m a x S_{max} Smax = 512)。 由于图像中的对象可能具有不同的大小,因此在训练期间考虑到这一点是有益的。 这也可以看作是通过尺度抖动来增强训练集,其中训练单个模型来识别各种尺度的物体。 出于速度原因,我们通过对具有相同配置的单尺度模型的所有层进行微调来训练多尺度模型,并使用固定的 S = 384 进行预训练。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2 测试

在这里插入图片描述
在这里插入图片描述
在测试时,给定一个经过训练的 ConvNet 和一个输入图像,它按以下方式分类。 首先,它被各向同性地重新缩放到预定义的最小图像边,表示为 Q(我们也将其称为测试比例)。 我们注意到 Q 不一定等于训练规模 S(正如我们将在第 4 节中展示的那样,对每个 S 使用多个 Q 值可以提高性能)。 然后,以类似于(Sermanet et al., 2014)的方式将网络密集地应用于重新缩放的测试图像上。 即,全连接层首先转换为卷积层(第一个 FC 层转换为 7 × 7 卷积层,最后两个 FC 层转换为 1 × 1 卷积层)。 然后将所得的全卷积网络应用于整个(未裁剪的)图像结果是一个类别分数图,其通道数等于类别数,并且空间分辨率可变,具体取决于输入图像的大小。 最后,为了获得图像的类别分数的固定大小向量,对类别分数图进行空间平均(求和)。 我们还通过水平翻转图像来增强测试集; 对原始图像和翻转图像的 soft-max 类后验进行平均以获得图像的最终分数。

由于全卷积网络应用于整个图像,因此无需在测试时对multiple crops进行采样(Krizhevsky et al., 2012),这效率较低,因为它需要对每个crop进行网络重新计算。 同时,使用大量裁剪(如 Szegedy 等人(2014)所做的那样)可以提高准确性,因为与全卷积网络相比,它可以对输入图像进行更精细的采样。 此外,由于不同的卷积边界条件,multi-crop评估与dense评估是互补的:当将ConvNet应用于crop时,卷积特征图会用零填充,而在dense评估的情况下,同一crop的填充自然会出现来自图像的相邻部分(由于卷积和空间池化),这大大增加了整体网络感受野,因此捕获了更多上下文。 虽然我们认为,在实践中,multiple crops的计算时间增加并不能证明准确性的潜在收益是合理的,但作为参考,我们还使用每个尺度 50 crops(5 × 5 常规网格,2 次翻转)来评估我们的网络,总共 150 crops 3 个尺度以上,与 Szegedy 等人(2014)使用的 4 个尺度以上的 144 crops相当。
在这里插入图片描述
在这里插入图片描述

Dense test(稠密测试)

在这里插入图片描述
在这里插入图片描述

Multi-crop测试

在这里插入图片描述
在这里插入图片描述

3.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 周,具体取决于架构。

4 分类实验

在这里插入图片描述

数据集

在这里插入图片描述
在本节中,我们展示了通过所描述的 ConvNet 架构在 ILSVRC-2012 数据集(用于 ILSVRC 2012-2014 挑战)上实现的图像分类结果。 该数据集包含 1000 个类别的图像,并分为三组:训练(130 万张图像)、验证(5 万张图像)和测试(带有保留类别标签的 10 万张图像)。 使用两种度量来评估分类性能:top-1 和 top-5 错误。 前者是多类分类误差,即错误分类图像的比例; 后者是 ILSVRC 中使用的主要评估标准,计算为图像的比例,使得真实类别位于前 5 个预测类别之外。

关于top-5 error不记得了可以看我这篇文章
在这里插入图片描述
对于大多数实验,我们使用验证集作为测试集。 还在测试集上进行了某些实验,并作为“VGG”团队参加 ILSVRC-2014 竞赛提交到官方 ILSVRC 服务器(Russakovsky et al., 2014)

4.1 单尺度评估

在这里插入图片描述
我们首先使用 2.2 节中描述的层配置在单一尺度上评估各个 ConvNet 模型的性能。 测试图像尺寸设置如下:对于固定S,Q = S;对于抖动S ∈ [Smin,Smax],Q = 0.5(Smin + Smax)。 结果如表3所示。
在这里插入图片描述
首先,我们注意到,使用局部响应归一化(A-LRN 网络)并不能改善没有任何归一化层的模型 A。 因此,我们不在更深层次的架构(B-E)中采用标准化。

其次,我们观察到分类误差随着 ConvNet 深度的增加而减小:从 A 中的 11 层到 E 中的 19 层。值得注意的是,尽管深度相同,但配置 C(包含三个 1 × 1 卷积层), 性能比配置 D 差,配置 D 在整个网络中使用 3 × 3 卷积层。 这表明虽然额外的非线性确实有帮助(C 比 B 更好),但通过使用具有非平凡感受野的卷积滤波器来捕获空间上下文也很重要(D 比 C 更好)。 当深度达到 19 层时,我们的架构的错误率就会饱和,但更深的模型可能对更大的数据集有益。 我们还将网络 B 与具有 5 个 5 × 5 卷积层的浅网络进行了比较,该网络源于B,是通过将每对 3 × 3 卷积层替换为单个 5 × 5 卷积层(其具有与所解释的相同的感受野,见第 2.3 节)。 经测量,浅层网络的 top-1 误差比 B(在中心裁剪上)高 7%,这证实了具有小滤波器的深网络优于具有较大滤波器的浅网络。

最后,即使在测试时使用单个尺度,训练时的尺度扰动 (S ∈ [256; 512]) 也会比在具有固定最小边的图像 (S = 256 或 S = 384) 上进行训练产生明显更好的结果。 这证实了通过尺度扰动来增强训练集确实有助于捕获多尺度图像统计数据。
在这里插入图片描述

4.2 多尺度评估

在这里插入图片描述
在评估了单尺度的 ConvNet 模型后,我们现在评估测试时尺度抖动的影响。 它包括在测试图像的多个重新缩放版本(对应于不同的 Q 值)上运行模型,然后对所得的类后验进行平均。 考虑到训练和测试规模之间的巨大差异会导致性能下降,使用固定 S 训练的模型在三种测试图像尺寸上进行评估,接近于训练尺寸:Q = {S − 32, S, S + 32} 。 同时,训练时的尺度抖动使得网络在测试时可以应用于更广泛的尺度,因此用变量S ∈ [Smin; Smax]在更大的尺寸范围Q = {Smin,0.5(Smin + Smax),Smax}上进行评估。

表 4 中显示的结果表明,测试时的尺度抖动会带来更好的性能(与在单尺度下评估相同模型相比,如表 3 所示)。 和以前一样,最深的配置(D 和 E)表现最好,并且尺度抖动优于使用固定的最小边 S 进行训练。 我们在验证集上的最佳单网络性能为24.8%/7.5% top-1/top -5 错误(在表 4 中以粗体突出显示)。 在测试集上,配置 E 实现了 7.3% 的 top-5 错误率
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

4.3 Multi-crop evaluation

在这里插入图片描述
在表 5 中,我们将dense ConvNet 评估与multi-crop评估进行了比较(详细信息请参见第 3.2 节)。 我们还通过对两种评估技术的 softmax 输出进行平均来评估它们的互补性。 可以看出,使用多种裁剪的效果比密集评估稍好,并且这两种方法确实是互补的,因为它们的组合优于它们中的每一种。 如上所述,我们假设这是由于对卷积边界条件的不同处理造成的。

在这里插入图片描述
在这里插入图片描述

4.4 convnet fusion

在这里插入图片描述
到目前为止,我们评估了各个 ConvNet 模型的性能。 在这部分实验中,我们通过平均softmax类后验来组合多个模型的输出。 由于模型的互补性,这提高了性能,并被用于顶级 ILSVRC 2012 年(Krizhevsky 等人,2012 年)和 2013 年(Zeiler & Fergus,2013 年;Sermanet 等人,2014 年)提交的材料

结果如表 6 所示。到 ILSVRC 提交时,我们只训练了单尺度网络以及多尺度模型 D(通过仅微调全连接层而不是所有层)。 由此产生的 7 个网络的集合具有 7.3% ILSVRC 测试误差。 提交后,我们考虑了仅两个性能最佳的多尺度模型(配置 D 和 E)的集成,使用密集评估将测试误差降低至 7.0%,使用密集评估和多作物评估相结合将测试误差降低至 6.8%。 作为参考,我们性能最佳的单模型实现了 7.1% 的误差(模型 E,表 5)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.5 与现有技术的比较

在这里插入图片描述
最后,我们将我们的结果与表 7 中的最先进的技术进行比较。在 ILSVRC-2014 挑战赛(Russakovsky et al., 2014)的分类任务中,我们的“VGG”团队使用 7 个模型的集合,以 7.3% 的测试误差获得第二名。 提交后,我们使用 2 个模型的组合将错误率降低至 6.8%。
在这里插入图片描述
从表7中可以看出,我们的深度ConvNet明显优于上一代模型,在ILSVRC-2012和ILSVRC-2013比赛中取得了最好的成绩。 我们的结果相对于分类任务获胜者(GoogLeNet,错误率 6.7%)也具有竞争力,并且大大优于 ILSVRC-2013 获胜提交的 Clarifai,后者在使用外部训练数据的情况下达到了 11.2%,在没有外部训练数据的情况下达到了 11.7%。 考虑到我们的最佳结果是通过仅组合两个模型来实现的,这非常引人注目——这比大多数 ILSVRC 提交中使用的模型要少得多。 在单网络性能方面,我们的架构取得了最好的结果(7.0%的测试误差),比单个GoogLeNet高0.9%。 值得注意的是,我们并没有偏离 LeCun 等人的经典 ConvNet 架构(1989),但通过大幅增加深度对其进行了改进。
在这里插入图片描述

5 结论

在这里插入图片描述
在这项工作中,我们评估了用于大规模图像分类的非常深的卷积网络(最多 19 个权重层)。 事实证明,表示深度有利于分类精度,并且使用传统的深度大幅增加的 ConvNet 架构可以在 ImageNet 挑战数据集上实现最先进的性能(LeCun 等人,1989 年;Krizhevsky 等人, 2012)。 在附录中,我们还表明我们的模型可以很好地推广到各种任务和数据集,匹配或优于围绕较不深度的图像表示构建的更复杂的识别管道。 我们的结果再次证实了视觉表示中深度的重要性。

研究背景、成果和意义

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ZFNet只是对于AlexNet一些参数的小改动,表示不用再用那么大的卷积核了
在这里插入图片描述
OverFeat用到了Dense稠密测试,把全连接层替换成1x1的卷积层来实现对任意尺寸图像的预处理
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
几乎看不到5x5、7x7卷积
在这里插入图片描述

图表分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

小结

在这里插入图片描述
在这里插入图片描述

关键点&创新点

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

论文代码复现准备工作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

VGG推理代码及运行结果

在这里插入图片描述

# -*- coding: utf-8 -*-

import os
os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8'
import time
import json
import torch.nn as nn
import torch
import torchvision.transforms as transforms
from PIL import Image
from matplotlib import pyplot as plt
import torchvision.models as models
from tools.common_tools import get_vgg16
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 图片预处理
def img_transform(img_rgb, transform=None):
    """
    将数据转换为模型读取的形式
    :param img_rgb: PIL Image
    :param transform: torchvision.transform
    :return: tensor
    """

    if transform is None:
        raise ValueError("找不到transform!必须有transform对img进行处理")

    img_t = transform(img_rgb)
    return img_t


def process_img(path_img):

    # hard code
    norm_mean = [0.485, 0.456, 0.406]
    norm_std = [0.229, 0.224, 0.225]
    inference_transform = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(norm_mean, norm_std),
    ])

    # path --> img
    img_rgb = Image.open(path_img).convert('RGB')

    # img --> tensor
    img_tensor = img_transform(img_rgb, inference_transform)
    img_tensor.unsqueeze_(0)        # chw --> bchw
    # 输入到GPU上
    img_tensor = img_tensor.to(device)

    return img_tensor, img_rgb


def load_class_names(p_clsnames, p_clsnames_cn):
    """
    加载标签名
    :param p_clsnames:
    :param p_clsnames_cn:
    :return:
    """
    with open(p_clsnames, "r") as f:
        class_names = json.load(f)
    with open(p_clsnames_cn, encoding='UTF-8') as f:  # 设置文件对象
        class_names_cn = f.readlines()
    return class_names, class_names_cn


if __name__ == "__main__":

    # config
    path_state_dict = os.path.join(BASE_DIR, "vgg16-397923af.pth")
    path_img = os.path.join(BASE_DIR, "..", "..", "alexNet/data", "Golden Retriever from baidu.jpg")
    # path_img = os.path.join(BASE_DIR, "..", "..", "alexNet/data", "tiger cat.jpg")
    path_classnames = os.path.join(BASE_DIR, "..", "..", "alexNet/data", "imagenet1000.json")
    path_classnames_cn = os.path.join(BASE_DIR, "..", "..", "alexNet/data", "imagenet_classnames.txt")

    # load class names
    cls_n, cls_n_cn = load_class_names(path_classnames, path_classnames_cn)

    # 1/5 load img
    img_tensor, img_rgb = process_img(path_img)

    # 2/5 load model
    vgg_model = get_vgg16(path_state_dict, device, True)

    # 3/5 inference  tensor --> vector
    with torch.no_grad():
        time_tic = time.time()
        outputs = vgg_model(img_tensor)
        time_toc = time.time()

    # torch.Size([1, 1000])
    # print(outputs.data.shape)
    # values = tensor([15.5579], device='cuda:0'),
    # indices = tensor([207], device='cuda:0'))
    # print(torch.max(outputs.data, 1))

    # 4/5 index to class names
    _, pred_int = torch.max(outputs.data, 1)
    _, top5_idx = torch.topk(outputs.data, 5, dim=1)

    # .cpu()方法用于将张量从GPU内存中移动到CPU内存中
    # .numpy()方法用于将张量转换为NumPy数组
    pred_idx = int(pred_int.cpu().numpy()[0])
    pred_str, pred_cn = cls_n[pred_idx], cls_n_cn[pred_idx]
    print("img: {} is: {}\n{}".format(os.path.basename(path_img), pred_str, pred_cn))
    print("time consuming:{:.2f}s".format(time_toc - time_tic))

    # 5/5 visualization
    plt.imshow(img_rgb)
    plt.title("predict:{}".format(pred_str))
    top5_num = top5_idx.cpu().numpy().squeeze()
    text_str = [cls_n[t] for t in top5_num]
    for idx in range(len(top5_num)):
        plt.text(5, 15+idx*30, "top {}:{}".format(idx+1, text_str[idx]), bbox=dict(fc='yellow'))
    plt.show()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

VGG训练代码及运行结果

在这里插入图片描述
my_dataset.py

# -*- coding: utf-8 -*-

import numpy as np
import torch
import os
import random
from PIL import Image
from torch.utils.data import Dataset

random.seed(1)
rmb_label = {"1": 0, "100": 1}


class CatDogDataset(Dataset):
    def __init__(self, data_dir, mode="train", split_n=0.9, rng_seed=620, transform=None):
        """
        rmb面额分类任务的Dataset
        :param data_dir: str, 数据集所在路径
        :param transform: torch.transform,数据预处理
        """
        self.mode = mode
        self.data_dir = data_dir
        self.rng_seed = rng_seed
        self.split_n = split_n
        self.data_info = self._get_img_info()  # data_info存储所有图片路径和标签,在DataLoader中通过index读取样本
        self.transform = transform

    def __getitem__(self, index):
        path_img, label = self.data_info[index]
        img = Image.open(path_img).convert('RGB')     # 0~255

        if self.transform is not None:
            img = self.transform(img)   # 在这里做transform,转为tensor等等

        return img, label

    def __len__(self):
        if len(self.data_info) == 0:
            raise Exception("\ndata_dir:{} is a empty dir! Please checkout your path to images!".format(self.data_dir))
        return len(self.data_info)

    def _get_img_info(self):

        img_names = os.listdir(self.data_dir)
        # 获取所有图片文件名
        img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))

        # 保证每次随机的顺序一致,避免训练集与测试集有交叉
        random.seed(self.rng_seed)
        random.shuffle(img_names)

        # 根据文件名获取标签
        img_labels = [0 if n.startswith('cat') else 1 for n in img_names]

        # 选择划分点: 25000* 0.9 = 22500
        split_idx = int(len(img_labels) * self.split_n)
        # split_idx = int(100 * self.split_n)
        if self.mode == "train":
            img_set = img_names[:split_idx]     # 数据集90%训练
            # img_set = img_names[:22500]     #  hard code 数据集90%训练
            label_set = img_labels[:split_idx]
        elif self.mode == "valid":
            img_set = img_names[split_idx:]
            label_set = img_labels[split_idx:]
        else:
            raise Exception("self.mode 无法识别,仅支持(train, valid)")

        # 拼接路径
        path_img_set = [os.path.join(self.data_dir, n) for n in img_set]
        # 制作样本对
        data_info = [(n, l) for n, l in zip(path_img_set, label_set)]

        return data_info

pytorch中VGG的类定义代码

import torch
import torch.nn as nn


# 类定义
class VGG(nn.Module):

    def __init__(self, features, num_classes=1000, init_weights=True):
        super(VGG, self).__init__()
        self.features = features    # 核心:特征提取
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))  # 自适应池化至7*7
        self.classifier = nn.Sequential(   # 分类器
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Dropout(),
            nn.Linear(4096, num_classes),
        )
        if init_weights:
            self._initialize_weights()

    def forward(self, x):
        x = self.features(x)  # 
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.constant_(m.bias, 0)

VGG16的定义

# _vgg('vgg16', 'D', False, pretrained, progress, **kwargs)

def _vgg(arch, cfg, batch_norm, pretrained, progress, **kwargs):
    if pretrained:
        kwargs['init_weights'] = False
    model = VGG(make_layers(cfgs[cfg], batch_norm=batch_norm), **kwargs)  # 调用VGG类,进行实例化,核心在make_layers
    if pretrained:
        state_dict = load_state_dict_from_url(model_urls[arch],
                                              progress=progress)
        model.load_state_dict(state_dict)
    return model


def vgg16(pretrained=False, progress=True, **kwargs):
    r"""VGG 16-layer model (configuration "D")
    `"Very Deep Convolutional Networks For Large-Scale Image Recognition" <https://arxiv.org/pdf/1409.1556.pdf>`_

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
        progress (bool): If True, displays a progress bar of the download to stderr
    """
    return _vgg('vgg16', 'D', False, pretrained, progress, **kwargs)

make_layers函数



# [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M']
def make_layers(cfg, batch_norm=False):
    layers = []
    in_channels = 3
    for v in cfg:
        if v == 'M':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v
    return nn.Sequential(*layers)


cfgs = {
    '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'],}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
train_vgg.py

# -*- coding: utf-8 -*-

import os
import numpy as np
import torch.nn as nn
import torch
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
import torchvision.models as models
from tools.my_dataset import CatDogDataset
from tools.common_tools import get_vgg16
from datetime import datetime

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# log dir
now_time = datetime.now()
time_str = datetime.strftime(now_time, '%m-%d_%H-%M')
log_dir = os.path.join(BASE_DIR, "..", "results", time_str)
if not os.path.exists(log_dir):
    os.makedirs(log_dir)

if __name__ == "__main__":

    # config
    data_dir = os.path.join(BASE_DIR, "..", "..", "alexNet/data/train")
    path_state_dict = os.path.join(BASE_DIR, "vgg16-397923af.pth")

    num_classes = 2
    MAX_EPOCH = 3
    BATCH_SIZE = 32  # 16 / 8 / 4 / 2 / 1
    LR = 0.001
    log_interval = 2
    val_interval = 1
    classes = 2
    start_epoch = -1
    lr_decay_step = 1

    # ============================ step 1/5 数据 ============================
    # imagenet训练集中统计而来
    norm_mean = [0.485, 0.456, 0.406]
    norm_std = [0.229, 0.224, 0.225]

    train_transform = transforms.Compose([
        # 缩放最短的边到256
        transforms.Resize((256)),      # (256, 256) 区别
        transforms.CenterCrop(256),
        transforms.RandomCrop(224),
        transforms.RandomHorizontalFlip(p=0.5),
        transforms.ToTensor(),
        transforms.Normalize(norm_mean, norm_std),
    ])

    normalizes = transforms.Normalize(norm_mean, norm_std)
    valid_transform = transforms.Compose([
        transforms.Resize((256, 256)),
        transforms.TenCrop(224, vertical_flip=False),
        transforms.Lambda(lambda crops: torch.stack([normalizes(transforms.ToTensor()(crop)) for crop in crops])),
    ])

    # 构建MyDataset实例
    train_data = CatDogDataset(data_dir=data_dir, mode="train", transform=train_transform)
    valid_data = CatDogDataset(data_dir=data_dir, mode="valid", transform=valid_transform)

    # 构建DataLoder
    train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
    valid_loader = DataLoader(dataset=valid_data, batch_size=4)

    # ============================ step 2/5 模型 ============================
    vgg16_model = get_vgg16(path_state_dict, device, False)

    num_ftrs = vgg16_model.classifier._modules["6"].in_features
    vgg16_model.classifier._modules["6"] = nn.Linear(num_ftrs, num_classes)

    vgg16_model.to(device)
    # ============================ step 3/5 损失函数 ============================
    criterion = nn.CrossEntropyLoss()
    # ============================ step 4/5 优化器 ============================
    # 冻结卷积层
    flag = 0
    # flag = 1
    if flag:
        fc_params_id = list(map(id, vgg16_model.classifier.parameters()))  # 返回的是parameters的 内存地址
        base_params = filter(lambda p: id(p) not in fc_params_id, vgg16_model.parameters())
        optimizer = optim.SGD([
            {'params': base_params, 'lr': LR * 0.1},  # 0
            {'params': vgg16_model.classifier.parameters(), 'lr': LR}], momentum=0.9)

    else:
        optimizer = optim.SGD(vgg16_model.parameters(), lr=LR, momentum=0.9)  # 选择优化器

    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=lr_decay_step, gamma=0.1)  # 设置学习率下降策略
    # scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(patience=5)

# ============================ step 5/5 训练 ============================
    train_curve = list()
    valid_curve = list()

    for epoch in range(start_epoch + 1, MAX_EPOCH):

        loss_mean = 0.
        correct = 0.
        total = 0.

        vgg16_model.train()
        for i, data in enumerate(train_loader):

            # forward
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = vgg16_model(inputs)

            # backward
            optimizer.zero_grad()
            loss = criterion(outputs, labels)
            loss.backward()

            # update weights
            optimizer.step()

            # 统计分类情况
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).squeeze().cpu().sum().numpy()

            # 打印训练信息
            loss_mean += loss.item()
            train_curve.append(loss.item())
            if (i+1) % log_interval == 0:
                loss_mean = loss_mean / log_interval
                print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%} lr:{}".format(
                    epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total, scheduler.get_last_lr()))
                loss_mean = 0.

        scheduler.step()  # 更新学习率

        # validate the model
        if (epoch+1) % val_interval == 0:

            correct_val = 0.
            total_val = 0.
            loss_val = 0.
            vgg16_model.eval()
            with torch.no_grad():
                for j, data in enumerate(valid_loader):
                    inputs, labels = data
                    inputs, labels = inputs.to(device), labels.to(device)

                    bs, ncrops, c, h, w = inputs.size()
                    outputs = vgg16_model(inputs.view(-1, c, h, w))
                    outputs_avg = outputs.view(bs, ncrops, -1).mean(1)

                    loss = criterion(outputs_avg, labels)

                    _, predicted = torch.max(outputs_avg.data, 1)
                    total_val += labels.size(0)
                    correct_val += (predicted == labels).squeeze().cpu().sum().numpy()

                    loss_val += loss.item()

                loss_val_mean = loss_val/len(valid_loader)
                valid_curve.append(loss_val_mean)
                print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                    epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val_mean, correct_val / total_val))
            vgg16_model.train()

    train_x = range(len(train_curve))
    train_y = train_curve

    train_iters = len(train_loader)
    valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
    valid_y = valid_curve

    plt.plot(train_x, train_y, label='Train')
    plt.plot(valid_x, valid_y, label='Valid')

    plt.legend(loc='upper right')
    plt.ylabel('loss value')
    plt.xlabel('Iteration')
    plt.savefig(os.path.join(log_dir, "loss_curve.png"))
    plt.show()

运行结果

在这里插入图片描述
在这里插入图片描述

tips

在这里插入图片描述
关于为什么valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval,其实是根据训练集的epoch后的数据来对应的,每1个epoch都会有704代训练完,704即 22500 / 32 = 703.125 22500 / 32=703.125 22500/32=703.125向上取整得到的
在这里插入图片描述

答疑

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Q&A

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

  • 35
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值