VGG网络模型及代码分析

发布时间 2023-12-27 10:07:50作者: Yuxi001
1.VGG网络模型介绍
2014年由牛津大学著名研究组Oxford Visual Geometry Group提出,VGGNet获得了ILSVRC 2014年比赛的亚军和定位项目的冠军。
论文名称:Very Deep Convolutional Networks for Large-Scale Image Recognition
网络的亮点在于可以通过堆叠多个33的卷积核来替代大尺度卷积核,用以减少参数。论文中提到可以堆叠两个33的卷积核替代55的卷积核,堆叠3个33的卷积核替代7*7的卷积核。(即拥有相同的感受野)
 
注:感受野?
CNN感受野:在卷积神经网络中,决定某一层输出结果中一个元素所对应输入层的区域大小,被称作感受野。通俗的解释就是输出特征图(feature map)上的一个单元对应输入层上的大小。
注:感受野计算?
F(i)= (F(i+1) - 1 )*stride + Ksize
F(i)为第i层感受野
stride为第i层步距
Ksize为卷积核和池化核大小
 
2.VGG论文原文翻译
论文原文
题目:用于大规模图像识别的非常深层的网络
摘要
  在这项工作中,我们调查了在大规模图像识别设置中,卷积神经网络的深度对其精确度的影响。我们的主要贡献是使用具有非常小(3 * 3)卷积滤波器的架构对于增加了深度的网络的全面评估,这表明将通过将深度推到16-19个权重层可以实现对现有技术配置的显著改进。这些发现是我们ImageNet Challenge 2014提交的基础,我们的团队分别在定位和分类问题中获得了第一和第二名。我们还表明,我们的方法很好地推广到了其他数据集上,在那里他们实现了最好的结果。我们已经使我们的两个性能最好的ConvNet模型公开可用,以便进一步研究在计算机视觉中使用深度视觉表示。
 
1 引言
  卷积网络(ConvNets)最近在大规模图像和视频识别方面取得了巨大的成功,由于大型的公共图像库,高性能计算系统,例如GPU或者大规模的分布式集群。特别的,ImageNet大规模视觉识别挑战(ILSVRC)在深度视觉识别架构中发挥了重要的作用,它已经作为几代大型图像分类系统的实验台了,从高维的浅特征编码到深的ConvNets。
  随着ConvNets在计算机视觉领域变得越来越流行,已经进行了许多尝试来改进Krizhevsky等人的原始结构,以达到更好的准确性。例如,ILSVRC-2013中最好的提交使用较小的接受窗口大小和在第一个卷积层更小的步长。另一个改进涉及在整个图像和多个尺度上密集地训练和测试网络。在本文中,我们讨论了ConvNet架构设计的另一个重要方面-深度。最后,我们固定了结构的其他参数,并通过添加更多的卷积层来稳定地增加网络的深度,这种方法的可行性是因为在所有层中都使用了非常小的(3*3)的卷积滤波器。
  结果,我们提出了更加准确的ConvNet的结构,它不仅实现了在ILSVRC分类和定位任务的最好的精度,而且也适用于其他的图像识别数据集。在这些数据集中本文的方法即使是用作相对简单的管道(例如,由没有经过微调的线性SVM分类的深度特征)的一部分,也实现了非常卓越的性能表现。我们已经发布了两个最好的模型,以促进进一步的研究。
  本文的其余部分组织如下。在第2部分,我们描述我们的ConvNet的配置。图像分类训练和评估的细节展示在第3部分。关于ILSVRC分类任务的配置比较是在第4部分。第5节总结本文。为了完整性,我们还在附录A中描述和评估了我们的ILSVRC-2014目标定位系统,并在附录B中讨论了对其他数据集的深层特征的泛化能力。最后,附录C包含了主要论文修定版本的列表。
 
2 ConvNet配置
  为了测量在公平的环境中增加的ConvNet深度带来的改进,我们所有的ConvNet层的配置使用与Ciresan(2011)和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个像素。空间池化是由5个最大池化层执行的,基在一些卷积层后面(不是所有的卷积层之后都跟着最大池化层)。最大池化是在2*2的像素窗口上执行的,步长为2。
  一堆卷积层(在不同的架构中具有不同的深度)之后是三个全连接层(FC):前两个每一个都有4096个通道,第三个执行1000类的ILSVRC分类,因此包含1000个通道(每个类一个通道)。最后一层是soft-max层。全连接层的配置在所有网络中都是相同的。
  所有的隐藏层都配有修正的非线性单元(ReLU)。注意到我们的网络只有一个包含有局部响应正则化(LRN):会在第4节中展示,这种正则化不会改善在ILSVRC数据集中的性能,但会导致内存消耗和计算时间增加。在适用的情况下,LRN层的参数是(Krizhevsky et al., 2012)等人的参数。
 
2.2 配置
  本文评估的ConvNet配置在表1中罗列,每列一个。在下文中我们将会通过他们的名字(A-E)来指代网络。所有配置都遵循2.1节中提出的通用设计,仅在深度上有所不同:从网络A的11个权重层(8个卷积层和3个全连接层)到网络E的19个权重层(16个卷积层和3个全连接层)。卷积层的宽度(通道的数量)越来越大(PS:感觉论文说错了,论文是rather small),从第一层的64个开始,然后在每个最大池化层之后以2次幂增加,直到其达到512。
  在表2中,我们报告了每个配置的参数数量。尽管有较大的深度,我们网络中权重的数量不大于具有较大的卷积层宽度和接收野的较浅的网络中的权重的数目(114M个权重在(Sermanet等人,2014))。
 
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 et al., 2014)中是7 * 7 步长是2, 而是在整个网络中使用了非常小的3 * 3 的感受野,对输入中的每一个像素进行卷积处理,步长为1。很容易看到两个3 * 3 的卷积层(中间不带空间池化)和一个5 * 5的卷积层具有相同的感受野(PS: 就是说假如输入是55的图像,用33的filter卷积之后输入图像变成33,再来一个3 * 3 的filter卷积后输入图像变成11 。这和直接用一个55的filter卷积图像是一样的效果;)。3个这样的层就会相当于一个77的感受野。所以我们通过使用,例如,三个33卷积层的堆叠而不是单个的77的卷积层,我们获得了什么?我们引入了三个非线性修正层,而不是一个,这使得决策函数更具有辨别力。第二,我们减少了参数的数量:假设三层33的卷积层的一个堆的输入和输出都具有C个通道(PS:通道就是卷积层的输出数量,还需要注意的是输入也是C个通道,后面的公式就好理解了)。这个堆就由个参数,比上面的多了81%还多(27 的81%是21.87,加上27 是48.87,所以比81%还多)。这可以被看成是对77的卷积过滤器强加了一个正则化,迫使它们通过3*3滤波器(在其间注入非线性)进行分解。
  
   表一:ConvNet配置(以列展示)。配置的深度从左(A)向右(E)增加,因为添加了更多的层(添加的层以粗体表示)。卷积层参数表示成“conv<感受野大小>-<通道数量>”。为了简洁,未展示ReLU激活函数
 
 
 
引入11卷积层(表1,配置C)是增加决策树的非线性而不影响卷积层的感受野的方式。即使在我们的情况下,11卷积本质上是在相同维度空间上的线性映射(输入和输出通道的数量是相同的),由修正函数引入附加的非线性。应当注意,11的卷积层最近已经被用于Lin等人(2014)的“Network in Network”架构中。小尺寸的卷积滤波器之前已由 Ciresan(2011)等人使用,但是他们的网络没有我们的网络深,并且他们没有在大规模ILSVRC数据集上进行评估。Goodfellow等人(2014)将深层ConvNets(11个权重层)应用到街道号码识别任务中,并且表明增加的深度能产生更好的性能。GoogLeNet(Szegedy等人2014)是ILSVRC-2014分类任务中表现最好的一个模型,它是独立于我们的工作开发的,但是类似的是它基于非常深的ConvNets(22个权重层)和非常小的卷积过滤器(除了33的,他们也使用11和55的卷积)。然而,他们的网络拓扑结构比我们的更复杂,在第一层中更剧烈的减少特征映射的空间分辨率以减少计算量。如第4.5节所示,我们的模型在单网络分类精度方面优于Szegedy等人(2014)的模型。
 
3 分类框架
  在上一节中,我们展示了我们网络配置的细节。在本节中,我们描述分类ConvNet训练和评估的细节。ConvNet的训练过程一般遵循Krizhevsky等人的方法(除了从不同尺寸的训练图像中抽取剪裁的图像,会在正文中解释)。也就是通过使用带有momentum的mini-batch梯度下降算法(基于(LeCun,1989)等人的反向传播)来优化多项逻辑回归目标函数,以此来达到训练目的。批量(batch)大小被设置成256,momentum设置成0.9。训练通过权重衰减(L2惩罚系数设置成=512)随机采样S来单独地重新缩放每个训练图像。因为图像中的对象可以具有不同的大小,因此在训练期间将这一点考虑进去是非常有益的。这也可以看成是通过尺寸抖动的训练集增加,其中单个模型被训练以识别大范围尺度上的对象。出于速度上的考虑,我们通过微调具有相同配置的单尺度模型的所有层来训练多尺度模型,用固定的S = 384来预训练。
 
3.2 测试
  在测试时,给定训练的ConvNet和一个输入图像,它按以下方式分类。首先,它被各向同性地重新缩放到预定义的图像的最小边,表示为Q(我们也称为测试尺寸)。我们注意到Q不一定等于训练尺寸S(如我们将在第4节中展示的,对每个S使用几个Q值来改善性能)。然后,以类似于(Sermanet等人,2014)的方式将网络密集地应用于重新缩放的测试图像。也就是说,全连接层首先被转换成卷积层(第一个FC层转换成77的卷积层,最后两个FC层转换成11的卷积层)。然后将所得到的全卷积网络应用于整个(未剪裁)图像。结果是类别分数图(其通道数量等于类的数量)和一个变化的空间大小(取决于输入图像的大小)。最后,为了获得图像的类分数的固定大小的向量,类别分数图被空间平均了(sum-pooled)。我们也通过水平翻转图像来增加测试集;将原始图像和翻转图像的soft-max分类后验概率进行平均以获得图像的最终分数。
  由于全卷积网络是应用于整个图像上的,因此在测试的时候不需要抽取多个剪裁 (Krizhevsky et al., 2012),因为它需要对每个裁剪进行网络的重新计算,所以效率低下。同时,如Szegedy et al. (2014)做的一样,使用大量的裁剪,可以导致精度的提升,因为它导致输入图像比全卷积网络有更精细的采样。此外,由于不同的卷积边界条件,多裁剪评估与密集评价互补:当将ConvNet应用于剪裁的图像时,卷积特征图用零填充,而在密集评估时,用于相同的裁剪的图像的填充自然来自于图像的相邻部分(由于卷积和空间池化),这大大增加了整个网络的感受野,因此捕获了更多的上下文信息。虽然我们相信在实践中由多裁剪增加的计算时间并不能证明准确性的潜在增加,但是做为参考,我们也评估了我们的网络在每个尺寸上使用了50个剪裁(25个规则网格和2个翻转),在3个尺寸上总共150个裁剪图像,这与Szegedy et al. (2014)等人使用的的4个尺寸上的144个裁剪图像相对比。
 
3.3 实现细节
   我们的实现源自公开提供的C++Caffe工具箱(2013年12月提供),但是包含了一些重要的修改,允许我们在安装在单个系统中的多个GPU上执行训练和评估,并且也可以在多尺度(如上所述)上对全尺寸(未裁剪)的图像进行训练和评估。多GPU训练利用了数据的并行性,并且通过将每批训练图像分割成若干GPU小批块来执行,并且在每块GPU上并行处理。在计算GPU小批块梯度之后,将它们平均以获得整个批次的梯度。梯度计算在多个GPU上是同步的,因此结果与在单个GPU上训练时的结果完全相同。虽然最近提出了加速ConvNet训练的更复杂的方法(Krizhevsky, 2014),它们对网络的不同层采用模型和数据的并行处理,但是我们发现,我们概念上更简单的方案在现成的4-GPU系统上已经提供了3.75倍的加速,与使用单个GPU相比。在配备有四个NVIDIA Titan Black GPU的系统上,根据架构不同训练一个单独的网络需要2-3周。
 
分类实验
  数据集。在本节中,我们介绍了通过上文描述的ConvNet结构在ILSVRC-2012数据集(用于ILSVRC2012-2014挑战赛)上实现的图像分类结果。这个数据集包含了1000类的图像,并且被分成了3个集合:训练集(1.3M个图像),验证集(50K的图像),和测试集(100K图像带有类标签)。使用两种测量方法来评估分类性能:top-1和top-5的误差。前者是多类分类误差,例如,不正确分类的图像的比例。后者是ILSVRC中使用的主要评估标准,按如下方法计算,正确类别在前5个预测类别之外的的图像所占的比例。对于大多数实验,我们使用验证集作为测试集。在测试集上也进行了一些实验,并作为“VGG”团队进入ILSVRC-2014比赛(Russakovsky et al., 2014)提交给官方ILSVRC服务器。
4.1 单一尺度评估
  我们开始评估单个ConvNet模型在单个尺度上的性能,用的上2.2节描述的层配置。测试图像大小设置如下:Q=S,S固定,和 Q=0.5()比在具有固定最小边(S=256或S=384)的图像上训练产生明显的更好的结果。这证实了通过尺寸抖动的训练集的增加确实有助于捕获多尺度的图像统计数据。
 
4.2 多尺度评估
  已经在单个尺度上评估了ConvNet模型后,我们现在在测试的时候评估尺度抖动的影响。它包括在一个测试图像(对应于不同的Q值)的几个重新缩放的版本上运行一个模型,然后平均所得到的类的后验概率。考虑到训练和测试尺度之间的大的差异会导致性能的下降,用固定的S训练的模型在三个测试图像尺度(接近于训练时的尺度:Q={S-32,S,S+32})上进行评估。与此同时,训练时候的尺度抖动允许网络在测试的时候应用于更宽的尺度范围,所以用变化的S()。
  表4中呈现的结果表明测试时候的尺度抖动会导致更好的性能(对比于表3中所示的在单个尺度上评估相同的模型)。与以前一样,最深的配置(D和E)表现最好,尺度抖动的训练比用固定最小边S训练效果要好。我们在验证集上的最佳单一网络的性能是24.8%/7.5% 对应于top-1/top-5 错误率(在表4中以粗体显示)。在测试集上,配置E实现了7.3%的Top-5错误率。
  
 
4.3 多裁剪评估
  在表5中我们比较了密集ConvNet评估与多裁剪评估(详见第3.2节)。我们还通过平均Softmax输出来评估两种评估技术的互补性。可以看出,使用多种剪裁表现要略好于密集评估,并且这两种方法确实是互补的,因为它们的结合优于他们中的每一种。如上所述,我们假设这是由于卷积边界条件的不同处理造成的。
  
   表5:ConvNet评估技术对比。在所有的实验中训练尺度S是从[256; 512]中抽取的,并且3个测试尺度Q被认为是:{256,384,512}。1
 
 
4.4 ConvNet融合
  到目前为止,我们只计算了一个ConvNet模型的性能。在实验部分,我们通过平均几个模型的soft-max类的后验概率来结合输出。由于模型之间的互补,这种方法提高了性能,并且在2012年 (Krizhevsky et al., 2012) 和2013年 (Zeiler & Fergus, 2013; Sermanet et al., 2014)被顶级的ILSVRC提交使用。
  结果展示在表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。提交之后,我们使用2个模型的融合将错误率减少到了6.8%。
 
 
在表7中可以看出,我们的非常深的ConvNets明显优于上一代模型,它们在ILSVRC-2012和ILSVRC-2013竞赛中实现了最好的结果。我们的结果对于分类任务的冠军(GoogLeNet with 6.7% error)也是非常具有竞争性的,并且大大优于ILSVRC-2013的冠军提交的模型Clarifai,它在用了外部训练数据的情况下实现了11.2%,在没用外部数据的情况下实现了11.7%。值得注意的是我们最好的结果是仅仅融合了两个模型实现的-显著小于大多数ILSVRC提交中使用的。关于单一网络性能,我们的结构实现了最好的结果(7.0%的测试错误率),比超过单一GoogLeNet0.9%。值得注意的是,我们没有偏离 LeCun et al. (1989)的经典结构,而且大大提升了它的深度。
 
5 结论
  在这次工作中我们评估了非常深的卷积神经网络(达到19层)用于大规模的图像分类。证明了深度有益于分类准确度,在ImageNet挑战数据集上的最先进的表现可以使用一个ConvNet架构(LeCun et al., 1989; Krizhevsky et al., 2012)加上深度的增加来实现。在附录中,我们还显示我们的模型适用于各种各样的任务的数据集,匹配或超过了构建在较深图像表示上的更复杂的管道。我们的结果再次证实了在视觉表示中深度的重要性。
 
3.VGG模型
VGG模型图(图为VGG16)
 
此表格为不同深度的VGG相关参数
 
以上表中D(VGG16)为例来解读VGG模型,并且在过程中会解释一部分卷积神经网络的知识。
 
3.1 input(224*224 RGB image)
在输入层要做的是输入224*224大小的三通道彩色图像
注:图片数据如何输入?
彩色图像有RGB三个颜色通道,分别是红、绿、蓝三个通道,这三个通道的像素可以用二维数组来表示,像素值由0~255的数字来表示。比如一张160160的彩色图片,可以用160160*3的数组表示。
 
 
3.2 conv3-64
第一层卷积经过64个大小为3*3的卷积核,步距(padding)为1,
经过第一个卷积层后图片的大小变为22422464
计算过程:
N = (224 - 3 + 21)/ 1 + 1 = 224
因为卷积操作用到了64个卷积核,所以图像的深度变为64
综上,图片的大小变为224224*3
 
注:什么是卷积?
1.卷积过程是使用一个卷积核(Filter),在每层像素矩阵上不断按步长扫描下去,每次扫到的数值和卷积核中对应位置的数进行相乘,然后相加求和,得到的值将会生成一个新的矩阵。
2.卷积核相当于卷积操作中的一个过滤器,用于提取我们图像的特征,特征提取完后会得到一个特征图
3.卷积核的大小一般为33和55,比较常用的是33,训练效果会更好,卷积核里每个值就是我们需要训练模型过程中的神经元参数(权重)。
4.是卷积神经网络(CNN)中独特的网络结构,目的是进行图像特征的提取
5.卷积的特性在于拥有局部感知机制,权值共享
6.卷积核深度与输入特征矩阵深度保持一致,输出的特征矩阵channel(深度)与卷积核个数相同
7.在卷积操作过程中,矩阵经过卷积操作后的尺寸计算方法如下:
N = (W - F + 2P)/S + 1
其中输入图片大小为WW;filter大小为F*F,步长(Stride)为S,padding的像素数为P
8.不同的卷积核的功能不一样(有兴趣的可以自己看)
(具体的卷积运算过程可以在网上自己找)
 
注:什么是padding?
在进行卷积操作的过程中,处于位置中间的数值容易被进行多次的提取,但是边界数值的特征提取次数相对较少,为了能更好的把边界数值也利用上,所以给原始数据矩阵的四周补0,这就是padding操作。并且在进行卷积操作后维度会变少,得到的矩阵会变小,不方便计算,原矩阵再加上一层0的padding操作可以很好的解决这个问题
 
3.3 conv3-64
在第二层同样经过64个大小为33的卷积核,步距为1
经过第二层卷积后图片的大小变为224224*64
计算同上
 
3.4 maxpool
经过两层卷积层后的输出作为输入数据输入到maxpool层(大小为22,步距为1)
经过计算后的输出为112112*64
由于池化操作只改变输入数据的高度和宽度,不改变深度,所以经过池化操作后的宽度和高度变为原来的一半,而在深度不发生改变。
注:什么是池化(pooling)
池化操作相当于降维操作,有最大池化和平均池化,其中最大池化(max pooling)是我们这里需要用到的。经过卷积操作后我们提取到的特征信息,相邻区域会有相似特征信息,这是可以相互代替的,如果全部保留这些特征信息会存在信息冗余,增加计算难度。通过池化层会不断的减小数据的空间大小,参数的数量和计算量会有相应的下降,在一定程度上控制了过拟合
注:池化的特点?
没有训练参数;一般poolsize(池化核大小)和stride(步距)相同
注:什么是过拟合?
根本原因就是特征维度过多,模型假设过于复杂,参数过多,训练数据过少,噪声过多,导致拟合的函数完美的预测训练集,但对新数据的测试集的预测结果差,过度的拟合了训练数据,而没有考虑到泛化能力。
注:池化的目的?
对特征图进行稀疏处理,减少运算量
 
3.5 conv3-128
经过128个大小为33的卷积核后输出数据大小为112112*28
计算过程同上一个卷积操作
 
3.6 conv3-128
经过128个大小为33的卷积核后输出数据大小为112112*128
计算过程同上一个卷积操作
 
3.7 maxpool
再经过大小为22,步距为2的卷及操作后输出数据大小为5656*128
计算过程同上一个池化操作
 
3.8 conv3-256
数据经过256个大小为33的卷积操作后输出数据大小为5656*256
 
3.9 conv3-256
数据经过256个大小为33的卷积操作后输出数据大小为5656*256
 
3.10 conv3-256
数据经过256个大小为33的卷积操作后输出数据大小为5656*256
 
3.11 maxpool
再经过大小为22,步距为2的卷及操作后输出数据大小为2828*256
 
3.12 conv3-512
数据经过512个大小为33的卷积操作后输出数据大小为2828*512
 
3.13 conv3-512
数据经过512个大小为33的卷积操作后输出数据大小为2828*512
 
3.14 conv3-512
数据经过512个大小为33的卷积操作后输出数据大小为2828*512
 
3.15 maxpool
再经过大小为22,步距为2的卷及操作后输出数据大小为1414*512
 
3.16 conv3-512
数据经过512个大小为33的卷积操作后输出数据大小为1414*512
 
3.17 conv3-512
数据经过512个大小为33的卷积操作后输出数据大小为1414*512
 
3.18 conv3-512
数据经过512个大小为33的卷积操作后输出数据大小为1414*512
 
3.19 maxpool
再经过大小为22,步距为2的卷及操作后输出数据大小为77*512
 
3.20 FC-4096
在这一层输入的数据展开成一维向量,向量的元组个数有77512个
经过全连接层后数据的大小为1*4096
 
注:什么是全连接层?
这一层的每个结点在进行计算的时候,激活函数的输入是上一层所有结点的加权(暂时先这么写,没有找到一个特别清晰的定义,或者自己百度也可以明白)
注:数据展开?
在进行全连接之前要对数据进行展开,常用的方法为one-hot编码
 
3.21 FC-4096
再经过一次全连接层后数据的大小为1*4096
 
3.22 FC-1000
经过全连接层后数据的大小为1*1000
最后的输出节点数应该是要进行分类的种类个数,所以设置为最后的输出节点数为1000
 
3.23 soft-max
对上一层输出结果进行处理,得到最可能的分类并输出
 
注:soft-max函数?
经过soft-max处理后所有输出结点概率和为1
 
4 代码部分(源码)
VGG模型代码
 
#model.py
import torch.nn as nn
import torch

class VGG(nn.Module):
    def __init__(self, features, num_classes=1000, init_weights=False):
        super(VGG, self).__init__()
        self.features = features
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(512*7*7, 4096),
            nn.ReLU(True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Linear(4096, num_classes)
        )
        if init_weights:
            self._initialize_weights()


    def forward(self, x):
        # N x 3 x 224 x 224
        x = self.features(x)
        # N x 512 x 7 x 7
        x = torch.flatten(x, start_dim=1)
        # N x 512*7*7
        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')
                nn.init.xavier_uniform_(m.weight)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight)
                # nn.init.normal_(m.weight, 0, 0.01)
                nn.init.constant_(m.bias, 0)




def make_features(cfg: list):
    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)
            layers += [conv2d, nn.ReLU(True)]
            in_channels = v
    return nn.Sequential(*layers)




cfgs = {
    'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}




def vgg(model_name="vgg16", **kwargs):
    try:
        cfg = cfgs[model_name]
    except:
        print("Warning: model number {} not in cfgs dict!".format(model_name))
        exit(-1)
    model = VGG(make_features(cfg), **kwargs)
    return model
 
训练模型代码
 
train.py
import torch.nn as nn
from torchvision import transforms, datasets
import json
import os
import torch.optim as optim
from model import vgg
import torch


device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)


data_transform = {
    "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                 transforms.RandomHorizontalFlip(),
                                 transforms.ToTensor(),
                                 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
    "val": transforms.Compose([transforms.Resize((224, 224)),
                               transforms.ToTensor(),
                               transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}




data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  # get data root path
image_path = data_root + "/data_set/flower_data/"  # flower data set path


train_dataset = datasets.ImageFolder(root=image_path+"train",
                                     transform=data_transform["train"])
train_num = len(train_dataset)


# {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
flower_list = train_dataset.class_to_idx
cla_dict = dict((val, key) for key, val in flower_list.items())
# write dict into json file
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
    json_file.write(json_str)


batch_size = 9
train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size=batch_size, shuffle=True,
                                           num_workers=0)


validate_dataset = datasets.ImageFolder(root=image_path + "val",
                                        transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
                                              batch_size=batch_size, shuffle=False,
                                              num_workers=0)


# test_data_iter = iter(validate_loader)
# test_image, test_label = test_data_iter.next()


model_name = "vgg16"
net = vgg(model_name=model_name, num_classes=6, init_weights=True)
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0001)


best_acc = 0.0
save_path = './{}Net.pth'.format(model_name)
for epoch in range(30):
    # train
    net.train()
    running_loss = 0.0
    for step, data in enumerate(train_loader, start=0):
        images, labels = data
        optimizer.zero_grad()
        outputs = net(images.to(device))
        loss = loss_function(outputs, labels.to(device))
        loss.backward()
        optimizer.step()


        # print statistics
        running_loss += loss.item()
        # print train process
        rate = (step + 1) / len(train_loader)
        a = "*" * int(rate * 50)
        b = "." * int((1 - rate) * 50)
        print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss), end="")
    print()


    # validate
    net.eval()
    acc = 0.0  # accumulate accurate number / epoch
    with torch.no_grad():
        for val_data in validate_loader:
            val_images, val_labels = val_data
            optimizer.zero_grad()
            outputs = net(val_images.to(device))
            predict_y = torch.max(outputs, dim=1)[1]
            acc += (predict_y == val_labels.to(device)).sum().item()
        val_accurate = acc / val_num
        if val_accurate > best_acc:
            best_acc = val_accurate
            torch.save(net.state_dict(), save_path)
        print('[epoch %d] train_loss: %.3f  test_accuracy: %.3f' %
              (epoch + 1, running_loss / step, val_accurate))


print('Finished Training')
 
预测部分代码
 
predict.py
import torch
from model import vgg
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
import json


data_transform = transforms.Compose(
    [transforms.Resize((224, 224)),
     transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])


# load image
img = Image.open("C:\\Users\\39205\\Desktop\\1.jpg")
plt.imshow(img)
# [N, C, H, W]
img = data_transform(img)
# expand batch dimension
img = torch.unsqueeze(img, dim=0)


# read class_indict
try:
    json_file = open('./class_indices.json', 'r')
    class_indict = json.load(json_file)
except Exception as e:
    print(e)
    exit(-1)


# create model
model = vgg(model_name="vgg16", num_classes=6)
# load model weights
model_weight_path = "./vgg16Net.pth"
model.load_state_dict(torch.load(model_weight_path))
model.eval()
with torch.no_grad():
    # predict class
    output = torch.squeeze(model(img))
    predict = torch.softmax(output, dim=0)
    predict_cla = torch.argmax(predict).numpy()
print(class_indict[str(predict_cla)],predict[predict_cla].numpy())
plt.show()
 
5 代码逐行分析
由于编辑器的原因,对代码进行逐行分析的时候格式不会完全正确。对代码的分析是按照自己的理解分成了几段,在整段的代码展示中是不会有格式问题的,也可以对照着 4 代码部分(源码)进行阅读。
 
5.1 model.py
import torch.nn as nn
import torch
1
2
import torch.nn as nn导入torch.nn并取别名为nn
import torch导入pytorch
关于pytorch包的一些解释可以到pytorch官网进行查看,官网链接
class VGG(nn.Module):
    def __init__(self, features, num_classes=1000, init_weights=False):
        super(VGG, self).__init__()
        self.features = features
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(512*7*7, 4096),
            nn.ReLU(True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(True),
            nn.Linear(4096, num_classes)
        )
        if init_weights:
            self._initialize_weights()
 
class VGG(nn.Module):
创建一个类,类名为VGG,VGG继承于 nn.module父类,实现在搭建网络过程中所需要的用到的一些网络层结构
torch.nn.module:所有神经网络模块的基类;模型也应该继承此类;模块也可以包含其他模块,从而可以将它们嵌套在树结构中。
 
def init(self, features, num_classes=1000, init_weights=False):
在VGG中定义__init__函数
features:在之后的代码中会生成网络特征结构
num_classes=1000:将要进行的分类的个数
init_weights=False:init_weights是一个bool型的参数;用于决定是否对网络进行权重的初始化,如果bool值为True,则进入到定义好的初始化权重中
 
super(VGG, self).init()
在多重继承中,调用父类的过程中可能出现一系列问题,super在涉及到多继承时使用
 
self.features = features
用于提取图像的特征
 
self.classifier = nn.Sequential
将一系列层结构进行打包,组成一个新的结构,生成分类网络结构,nn.Sequential在网络层次多的时候,可以用于代码的精简。
torch.nn.Squential:一个连续的容器。模块将按照在构造函数中传递的顺序添加到模块中。或者,也可以传递模块的有序字典。
 
nn.Dropout(p=0.5)
随机失活一部分神经元,用于减少过拟合,默认比例为0.5,仅用于正向传播。
 
nn.Linear(51277, 4096)
在进行全连接之间需要将输入的数据展开成为一位数组。51277是展平之后得到的一维向量的个数。4096是全连接层的结点个数。
 
nn.ReLU(True)
定义ReLU函数
relu函数:f(x) = Max (0 ,x)
 
nn.Dropout(p=0.5)
随机失活一部分神经元
 
nn.Linear(4096, 4096)
定义第二个全连接层,输入为4096,本层的结点数为4096
 
nn.ReLU(True):
定义全连接层
 
nn.Linear(4096, num_classes)
最后一个全连接层。num_classes:分类的类别个数。
 
if init_weights:
是否对网络进行参数初始化
 
self._initialize_weights()
若结果为真,则进入到_initialize_weights函数中
 
  
  def forward(self, x):
        # N x 3 x 224 x 224
        x = self.features(x)
        # N x 512 x 7 x 7
        x = torch.flatten(x, start_dim=1)
        # N x 512*7*7
        x = self.classifier(x)
        return x
 
def forward(self, x):
定义前向传播过程
x为输入的图像数据(x代表输入的数据)
 
x = self.features(x)
将x输入到features,并赋值给x
 
x = torch.flatten(x, start_dim=1)
进行展平处理,start_dim=1,指定从哪个维度开始展平处理,因为第一个维度是batch,不需要对它进行展开,所以从第二个维度进行展开。
 
x = self.classifier(x)
展平后将特征矩阵输入到事先定义好的分类网络结构中。
 
return x
返回值为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')
                nn.init.xavier_uniform_(m.weight)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight)
                # nn.init.normal_(m.weight, 0, 0.01)
                nn.init.constant_(m.bias, 0) 
 
def _initialize_weights(self):
定义初始化权重函数
 
for m in self.modules():
用m遍历网络的每一个子模块,即网络的每一层
 
if isinstance(m, nn.Conv2d):
若m的值为 nn.Conv2d,即卷积层
Conv2d:对由多个输入平面组成的输入信号应用2D卷积。
# nn.init.kaiming_normal_(m.weight, mode=‘fan_out’, nonlinearity=‘relu’)
凯明初始化方法
 
nn.init.xavier_uniform_(m.weight)
用xavier初始化方法初始化卷积核的权重
 
if m.bias is not None:
nn.init.constant_(m.bias, 0)
若偏置不为None,则将偏置初始化为0
 
elif isinstance(m, nn.Linear):
若m的值为nn.Linear,即池化层
# nn.init.normal_(m.weight, 0, 0.01)
用一个正太分布来给权重进行赋值,0为均值,0.01为方差
 
nn.init.xavier_uniform_(m.weight)
nn.init.constant_(m.bias, 0)
对权重进行赋值,并且将偏置初始化为0
 
def make_features(cfg: list):
    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)
            layers += [conv2d, nn.ReLU(True)]
            in_channels = v
    return nn.Sequential(*layers)
 
def make_features(cfg: list):
提取特征网络结构,
cfg.list:传入配置变量,只需要传入对应配置的列表
 
layers = []
空列表,用来存放所创建的每一层结构
 
in_channels = 3
输入数据的深度,因为输入的是彩色RGB图像,通道数为3
 
for v in cfg:
遍历cfg,即遍历配置变量列表
 
if v == “M”:
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
若为最大池化层,创建池化操作,并为卷积核大小设置为2,步距设置为2,并将其加入到layers
 
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
layers += [conv2d, nn.ReLU(True)]
in_channels = v
否则创建卷积操作,定义输入深度,配置变量,卷积核大小为3,padding操作为1,并将Conv2d和ReLU激活函数加入到layers列表中
in_channels = v 经卷积后深度为v
 
*return nn.Sequential(layers)
将列表通过非关键字参数的形式传入进去,*代表通过非关键字形式传入
 
cfgs = {
    'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
 
cfgs为字典文件,其中vgg11,vgg13,vgg16,vgg19为字典的key值,对应的value为模型的配置文件
 
以vgg11为例
64 卷积层卷积核个数
‘M’, 池化层的结构
128, 卷积层卷积核个数
‘M’, 池化层的结构
256, 卷积层卷积核个数
256, 卷积层卷积核个数
‘M’, 池化层的结构
512, 卷积层卷积核个数
512, 卷积层卷积核个数
‘M’, 池化层的结构
512, 卷积层卷积核个数
512, 卷积层卷积核个数
‘M’ 池化层的结构
 
def vgg(model_name="vgg16", **kwargs):
    try:
        cfg = cfgs[model_name]
    except:
        print("Warning: model number {} not in cfgs dict!".format(model_name))
        exit(-1)
    model = VGG(make_features(cfg), **kwargs)
    return model
 
**def vgg(model_name=“vgg16”, kwargs):
定义函数vgg,通过此函数实例化锁给定的配置模型,
**kwargs:定义vgg函数时传入的字典变量,包含所需的分类个数以及是否初始化权重的布尔变量
 
try:
cfg = cfgs[model_name]
except:
print(“Warning: model number {} not in cfgs dict!”.format(model_name))
exit(-1)
cfg = cfgs[model_name]:将vgg16的key值传入到字典中,得到所对应的配置列表,命名为cfg
 
**model = VGG(make_features(cfg), kwargs)
return model
通过VGG函数来说实例化VGG网络
make_features(cfg):将cfg中的数据传入到make_features中
**kwargs:一个可变长度的字典变量
 
5.2 train.py
import torch.nn as nn
from torchvision import transforms, datasets
import json
import os
import torch.optim as optim
from model import vgg
import torch
 
这一部分为导入需要的包,和model.py中不同的是from model import vgg,这是自己定义的包,而model中的两个导入是导入pytorch的包
 
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
 
device用于指定我们在训练过程中所使用的设备,打印使用的设备信息。
if torch.cuda.is_available() else "cpu"
当前有可使用的GPU设备,默认使用当前设备列表的第一块GPU设备,否则使用CPU
 
data_transform = {
    "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                 transforms.RandomHorizontalFlip(),
                                 transforms.ToTensor(),
                                 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
    "val": transforms.Compose([transforms.Resize((224, 224)),
                               transforms.ToTensor(),
                               transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
 
data_transform
数据预处理函数
 
transforms.Compose
将我们所使用的预处理方法打包成为一个整体
 
transforms.RandomResizedCrop(224),
随机裁剪为224*224像素大小
RandomResizedCrop为随机裁剪
注:预处理第一步会将RGB三个通道分别减去[123.68,116.78,103.94]这三个值,这三个值对应的是ImageNet的所有图片RGB三个通道值的均值,这里我们进行的是从头开始训练。若基于迁移学习的方式训练就需要减去这三个值,因为它预训练的模型是基于ImageNet数据集进行训练的,所以使用预训练模型的时候,需要将它的图像减去对应的RGB值分量。
 
transforms.RandomHorizontalFlip(),
随机翻转(在水平的方向上随机饭翻转)
 
transforms.ToTensor(),
将数据转化为Tensor,将原本的取值范围0-255转化为0.1~1.0,并且将原来的序列(H,W,C)转化为(C,H,W)
 
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))]),
标准化处理,Normalize使用均值和标准差对Tensor进行标准化
 
data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  # get data root path
image_path = data_root + "/data_set/flower_data/"  # flower data set path
 
这一部分为获取我们训练所需要使用的数据集,
“…/…”:…表示返回上一层目录,…/…表示返回上上层目录
(os.getcwd() 获取当前文件所在目录
os.path.join 将两个路径连在一起
 
train_dataset = datasets.ImageFolder(root=image_path+"train",
                                     transform=data_transform["train"])
train_num = len(train_dataset)
 
root=image_path+“train”,
加载数据集的路径
 
transform
数据预处理
 
data_transform[“train”]
将训练集传入,返回训练集对应的处理方式
 
train_num = len(train_dataset)
打印训练集的图片数量
 
# {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
flower_list = train_dataset.class_to_idx
cla_dict = dict((val, key) for key, val in flower_list.items())
# write dict into json file
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
    json_file.write(json_str)
 
flower_list = train_dataset.class_to_idx
获取分类名称所对应的索引
 
cla_dict = dict((val, key) for key, val in flower_list.items())
遍历字典,将key val 值返回
 
json_str = json.dumps(cla_dict, indent=4)
通过json将cla_dict字典进行编码
 
with open(‘class_indices.json’, ‘w’) as json_file:
json_file.write(json_str)
‘class_indices.json’, 将字典的key值保存在文件中,方便在之后的预测中读取它的信息
 
batch_size = 9
1
训练的batch大小,我的GPU是4G的,一般设置在20以下都可以正常使用
 
train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size=batch_size, shuffle=True,
                                           num_workers=0)
validate_dataset = datasets.ImageFolder(root=image_path + "val",
                                        transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
                                              batch_size=batch_size, shuffle=False,
                                              num_workers=0)
 
train_loader = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size,shuffle=True,
num_workers=0)
通过DataLoader将train-dataset载入进来
shuffle=True, 随机参数,随机从样本中获取数据
num_workers=0 加载数据所使用的线程个数,线程增多,加快数据的生成速度,也能提高训练的速度,在Windows环境下线程数为0,在Linux系统下可以设置为多线程,一般设置为8或者16
 
validate_dataset = datasets.ImageFolder(root=image_path + “val”,
transform=data_transform[“val”])
载入测试集,transform为预处理
 
val_num = len(validate_dataset)
测试集文件个数
 
model_name = "vgg16"
net = vgg(model_name=model_name, num_classes=6, init_weights=True)
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0001)
 
net = vgg(model_name=model_name, num_classes=6, init_weights=True)
调用vgg16,输入model的名称,指定所需要的vgg配置参数,以及权重的初始化,其中num_classes可以按照自己的数据集进行修改
 
net.to(device)
将网络指认到设备上
 
loss_function = nn.CrossEntropyLoss()
损失函数,使用的是针对多类别的损失交叉熵函数
 
optimizer = optim.Adam(net.parameters(), lr=0.0001)
优化器,优化对象为网络所有的可训练参数,且学习率设置为0.0001
 
best_acc = 0.0
save_path = './{}Net.pth'.format(model_name)
 
定义最佳准确率,在训练过程中保存准确率最高的一次训练的模型
保存权重的路径
 
for epoch in range(30):
    # train
    net.train()
    running_loss = 0.0
    for step, data in enumerate(train_loader, start=0):
        images, labels = data
        optimizer.zero_grad()
        outputs = net(images.to(device))
        loss = loss_function(outputs, labels.to(device))
        loss.backward()
        optimizer.step()


        # print statistics
        running_loss += loss.item()
        # print train process
        rate = (step + 1) / len(train_loader)
        a = "*" * int(rate * 50)
        b = "." * int((1 - rate) * 50)
        print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss), end="")
    print()
 
for epoch in range(30):
设置迭代次数
 
net.train()
启用dropout,使用Dropout随机失活神将元的操作,但是我们只希望在训练的过程中如此,并不希望在预测的过程中起作用,通过net.tarin和net.eval来管理Dropout方法,这两个方法不仅可以管理Dropout,也可以管理BN方法
 
running_loss = 0.0
用于统计训练过程中的平均损失
 
for step, data in enumerate(train_loader, start=0):
遍历数据集,返回每一批数据data以及data对应的step
 
images, labels = data
将数据分为图像和标签
 
optimizer.zero_grad()
情况之间的梯度信息
 
outputs = net(images.to(device))
清空之前的梯度信息(清空历史损失梯度)
 
outputs = net(images.to(device))
将输入的图片引入到网络,将训练图像指认到一个设备中,进行正向传播得到输出,
 
loss = loss_function(outputs, labels.to(device))
将网络预测的值与真实的标签值进行对比,计算损失梯度
 
loss.backward()
optimizer.step()
误差的反向传播以及通过优化器对每个结点的参数进行更新
 
running_loss += loss.item()
将每次计算的loss累加到running_loss中
 
rate = (step + 1) / len(train_loader)
计算训练进度,当前的步数除以训练一轮所需要的总的步数
 
a = “*” * int(rate * 50)
b = “.” * int((1 - rate) * 50)
print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss), end="")
用 * 和 . 来打印训练的进度
 
net.eval()
    acc = 0.0  # accumulate accurate number / epoch
    with torch.no_grad():
        for val_data in validate_loader:
            val_images, val_labels = val_data
            optimizer.zero_grad()
            outputs = net(val_images.to(device))
            predict_y = torch.max(outputs, dim=1)[1]
            acc += (predict_y == val_labels.to(device)).sum().item()
        val_accurate = acc / val_num
        if val_accurate > best_acc:
            best_acc = val_accurate
            torch.save(net.state_dict(), save_path)
        print('[epoch %d] train_loss: %.3f  test_accuracy: %.3f' %
              (epoch + 1, running_loss / step, val_accurate))
print('Finished Training')
 
net.eval()
关闭Dropout
 
with torch.no_grad():
with 一个上下文管理器, torch.no_grad()之后的计算过程中,不计算每个结点的误差损失梯度
 
for val_data in validate_loader:
val_images, val_labels = val_data
optimizer.zero_grad()
outputs = net(val_images.to(device))
遍历验证集;将数据划分为图片和对应的标签值,对结点进行参数的更新;指认到设备上并传入网络得到输出
 
predict_y = torch.max(outputs, dim=1)[1]
求得输出的最大值作为预测最有可能的类别
 
acc += (predict_y == val_labels.to(device)).sum().item()
predict_y == val_labels.to(device) 预测类别与真实标签值的对比,相同为1,不同为0
item()获取数据,将Tensor转换为数值,通过sum()加到acc中,求和可得预测正确的样本数
 
val_accurate = acc / val_num
计算测试集额准确率
 
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(net.state_dict(), save_path)
如果准确率大于历史最优的准确率,将当前值赋值给最优,并且保存当前的权重
 
print(’[epoch %d] train_loss: %.3f test_accuracy: %.3f’ %
(epoch + 1, running_loss / step, val_accurate))
打印训练到第几轮,累加的平均误差,以及最优的准确率
 
print(‘Finished Training’)
整个训练完成后打印提示信息
 
5.3 predict.py
import torch
from model import vgg
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
import json
导入所需要的包
 
data_transform = transforms.Compose(
    [transforms.Resize((224, 224)),
     transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
 
transforms.Compose
数据预处理
 
transforms.Resize((224, 224)),
将图片缩放到224224,因为vgg网络设置的传入图片大小的参数为224224
 
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
转化为Tensor并进行标准化
 
img = Image.open(“C:\Users\39205\Desktop\1.jpg”)
plt.imshow(img)
预测图片路径以及图片的展示
 
img = data_transform(img)
img = torch.unsqueeze(img, dim=0)
 
图片预处理,并且在最前面添加新的batch维度
 
try:
    json_file = open('./class_indices.json', 'r')
    class_indict = json.load(json_file)
except Exception as e:
    print(e)
    exit(-1)
 
读取json文件中对应的类别名称,载入之后对其进行解码成我们所需要的字典
 
model = vgg(model_name="vgg16", num_classes=6)
model_weight_path = "./vgg16Net.pth"
model.load_state_dict(torch.load(model_weight_path))
model.eval()
with torch.no_grad():
    output = torch.squeeze(model(img))
    predict = torch.softmax(output, dim=0)
    predict_cla = torch.argmax(predict).numpy()
print(class_indict[str(predict_cla)],predict[predict_cla].numpy())
plt.show()
 
model = vgg(model_name=“vgg16”, num_classes=6)
初始化网络
 
model_weight_path = "./vgg16Net.pth"
导入权重参数
 
model.load_state_dict(torch.load(model_weight_path))
载入网络模型
 
model.eval()
进入eval模式,即关闭Dropout
 
with torch.no_grad():
让pytorch不去跟踪变量的损失梯度
 
** output = torch.squeeze(model(img))**
model(img) 通过正向传播得到输出
torch.squeeze 将输出进行压缩,即压缩掉batch这个维度
output就是最终的输出
 
predict = torch.softmax(output, dim=0)
经过softmax函数将输出变为概率分布
 
** predict_cla = torch.argmax(predict).numpy()**
获取概率最大处所对应的索引值
 
print(class_indict[str(predict_cla)],predict[predict_cla].numpy())
输出预测的类别名称以及预测的准确率
 
plt.show()
展示进行分类的图片,这里只用了一张图片进行了预测,所以进行一下展示
 
 
B站教程
 
原文链接