【卷积神经网络系列】五、VGG-Net

本文深入剖析了VGG-16网络的结构,包括卷积层、全连接层和池化层的配置。解释了1x1和3x3卷积核的作用,如降维、非线性增加和参数量减少。介绍了全卷积网络在测试阶段的重要性,以及固定尺度和多尺度训练对模型精度的影响。此外,探讨了迁移学习的概念,强调了预训练模型在小数据集上的应用,并给出了PyTorch实现VGGNet的代码示例。
摘要由CSDN通过智能技术生成

参考资料

论文地址:Very Deep Convolutional Networks for Large-Scale Image Recognition

参考博客:

手撕 CNN 经典网络之 VGGNet(理论篇)

图像处理必读论文之二:VGG网络

VGG学习与论文解读

带你读懂VGG 超详细描述

手撕 CNN 经典网络之 VGGNet(PyTorch实战篇)

一、前言

 VGG 的结构与 AlexNet 类似,区别是深度更深,但形式上更加简单。VGG由5层卷积层、3层全连接层、1层softmax输出层构成,层与层之间使用Maxpooling(最大化池)分开,所有隐藏层的激活单元都采用ReLU函数。作者在原论文中,根据卷积层不同的子层数量,设计了A、A-LRN、B、C、D、E这6种网络结构。

  • VGG-16:包含16个隐藏层(13个卷积层+3个全连接层),下图D列。
  • VGG-19:包含19个隐藏层(16个卷积层+3个全连接层),下图E列。

在这里插入图片描述

VGG优点:

  • VGG-Net的结构非常简洁,整个网络都使用了同样大小的卷积核尺寸(3 x 3)和最大池化尺寸(2 x 2)。
  • 几个小滤波器(3 x 3)卷积层的组合比一个大滤波器(5 x 5或7 x 7)卷积层好:
  • 验证了通过不断加深网络结构可以提升性能。

VGG缺点:

  • VGG耗费更多计算资源,并且使用了更多的参数(这里不是3 x 3卷积的锅),导致更多的内存占用(140 M)。其中绝大多数的参数都是来自于第一个全连接层。VGG可是有3个全连接层啊!

二、VGG-16网络结构

 VGG-16总共包含16个子层,第1层卷积层由2个conv3-64组成,第2层卷积层由2个conv3-128组成,第3层卷积层由3个conv3-256组成,第4层卷积层由3个conv3-512组成,第5层卷积层由3个conv3-512组成,然后是2个FC-4096,1个FC-1000。总共16层,这也就是VGG-16名字的由来。

VGG-16网络结构

​ VGG-16的可视化
在这里插入图片描述
在这里插入图片描述

1. 输入层

 VGG输入图片的尺寸是224 x 224 x 3。

2. 第1层卷积层

 第1层卷积层由2个conv3-64组成。该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化

3. 第2层卷积层

 第2层卷积层由2个conv3-128组成。该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化

4. 第3层卷积层

 第3层卷积层由3个conv3-256组成。该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化

5. 第4层卷积层

 第4层卷积层由3个conv3-512组成。该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化

6. 第5层卷积层

 第5层卷积层由3个conv3-512组成。该层的处理流程是:卷积–>ReLU–> 卷积–>ReLU–>池化

7. 第1层全连接层

 第1层全连接层FC-4096由4096个神经元组成。该层的处理流程是:FC–>ReLU–>Dropout

8. 第2层全连接层

 第2层全连接层FC-4096由4096个神经元组成。该层的处理流程是:FC–>ReLU–>Dropout

9. 第3层全连接层

 第3层全连接层FC-1000由1000个神经元组成,对应ImageNet数据集的1000个类别。该层的处理流程是:FC

三、全卷积网络

 VGG-16在训练的时候使用的是全连接网络。然而在测试验证阶段,网络结构稍有不同,作者将全连接全部替换为卷积网络。

在这里插入图片描述

 为什么要在模型测试的时候将全连接层转变为全卷积层呢:能够让网络模型可以接受任意大小的尺寸。

 我们在前面介绍的时候限定了网络输入图片的尺寸是224 x 224 x 3。如果后面三个层都是全连接,遇到宽高大于224的图片就需要进行图片的剪裁、缩放或其它处理,使图片尺寸统一到224 x 224 x 3,才能符合后面全连接层的输入要求。但是,我们并不能保证每次裁剪都能将图片中的关键目标保留下来,可能裁剪去的部分恰好包含了目标,造成裁减丢失关键目标信息,影响模型的测试精度。

输出是一个分类得分图,通道的数量和类别的数量相同,空间分辨率依赖于输入图像尺寸。 这种策略不限制输入图片的大小,最终输出结果是一个 w × h × n w \times h \times n w×h×n的score map。其中,w和h与输入图片大小有关,而n与所需类别数相同。 使用全卷积层,即使图片尺寸大于224 x 224 x 3,最终经过softmax层得到的得分图就不是1 x 1 x 1000,例如是2 x 2 x 1000,这里的通道1000与类别的数量相同,空间分辨率2 x 2依赖于输入图像尺寸。然后将得分图2 x 2 x 1000在通道维度上进行空间平均化(求和池化),得到的还是1 x 1 x 1000。最后对1000个通道的得分进行比较,取较大值作为预测类别。 这样做的好处就是大大减少特征位置对分类带来的影响。

在这里插入图片描述

四、网络相关细节

1. 1 X 1的卷积核的作用

(1)降维/升维

一文读懂卷积神经网络中的1x1卷积核
 从卷积层流程图中可以清楚的看到卷积后的特征图通道数与卷积核的个数是相同的。所以,如果想要升维或降维,只需要通过修改卷积核的个数即可。

在这里插入图片描述

(2)增加非线性

 每使用 1 * 1的卷积核,及增加一层卷积层,所以网络深度得以增加。 而使用 1 * 1的卷积核后,可以保持特征图大小与输入尺寸相同,卷积层卷积过程会包含一个激活函数,从而增加了非线性。在输入尺寸不发生改变的情况下而增加了非线性,所以会增加整个网络的表达能力。

(3)参数量减少,降低计算量

在这里插入图片描述

  • 只使用32个192 * 5 * 5的卷积核,输入为192 * 28 * 28,输出为32 * 28 * 28,参数量为(192 * 5 * 5 + 1)* 32 = 153632
  • 先使用16个192 * 1 * 1的卷积核,输入为192 * 28 * 28,输出为16 * 28 * 28,参数量为(192 * 1 * 1 + 1)16 = 3088,再使用32个16 * 5 * 5的卷积核,参数量为(16 * 5 * 5 + 1) 32 = 12832,所以总数量为15920,参数量少了一个数量级。

(4)跨通道信息交互(通道的变换)

 1 * 1的卷积核一般只改变输出通道数(C),而不改变输出的宽度(W)和高度(H)。实现降维和升维的操作其实就是 Channel 间信息的线性组合变化。比如:在尺寸 3 * 3,64通道个数的卷积核后面添加一个尺寸1 * 1,28通道个数的卷积核,就变成了尺寸3 * 3,28尺寸的卷积核。 原来的64个通道就可以理解为跨通道线性组合变成了28通道,这就是通道间的信息交互。

2. 3 X 3的卷积核的作用

(1)感受野:

 2个3 * 3的卷积核的感受野相当于5 * 5的卷积核,3个3 * 3的卷积核的感受野相当于7 * 7的卷积核;

感受野的计算

(2)并且经过多个3 * 3卷积核的后面都跟有非线性函数,使得网络的非线性能力增强:

 在保证相同感受野的情况下,多个小卷积层堆积可以提升网络深度,增加特征提取能力(非线性层增加)。

(3)极大地减少了参数量:

 比如 1个大小为5的感受野 等价于 2个步长为1,3 X 3大小的卷积核堆叠。(即1个5 X 5的卷积核等于2个3 X 3的卷积核)。而1个5 X 5卷积核的参数量为 5 * 5 * C * C。而2个3 X 3卷积核的参数量为 2 * 3 * 3 * C * C。很显然,18 * C * C < 25 * C * C。

3. 网络参数量计算

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

五、训练过程

 使用小批量梯度下降(mini-batch gradient descent):

  • batch设为256,动量设为0.9;
  • 除最后一层外的全连接层都使用了丢弃率0.5的dropout;
  • learning rate初始化为0.01,权重衰减系数为 5 × 1 0 − 4 {5}\times10^{-4} 5×104;
  • 对于权重层采用了随机初始化,初始化为均值0,方差0.01的正态分布。
  • 训练的图像数据方面,为了增加数据集,和AlexNet一样,这里也采用了随机水平翻转随机RGB色差进行数据扩增。对经过重新缩放的图片随机排序并进行随机剪裁得到固定尺寸大小为224×224的训练图像。

训练图片尺寸解析:

 这里定义S为经过isotropically-rescaled(宽高等比例缩放,对图像进行成比例处理,即图片不会变形)后的图片的最小边长度。原则上,缩放后的图像中只有S > = 224的部分才可以被用来做随机剪裁,进行训练。论文中,将S 称为training scale。举个例子,这里设training scale S = 224。有三幅经缩放后的图片:A,B,C。尺寸长×宽分别为:A:200×400,B:224×600 C:600×900 则A的最小边200<224,不可以进行裁剪;B、C可以(且对B的任何裁剪范围,都只能在宽600所在的边上移动)。

 在论文实现中,采用了两种方式来设定S:

  • 固定尺度fix scale;
  • 多尺度multi scale;

 固定尺度fix scale 训练中评估了两种scale:S = 256和S = 384;多尺度multi scale 训练中设置了[Smin, Smax]的浮动尺度,范围设为[256,512]。作者认为,对于即使同一类别的物体,其在不同图片上的大小也不尽相同,所以浮动尺度会更好,更接近真实情况。

六、模型评估

 图像的最小边被各向同性的缩放成预定义的尺寸,设为Q(我们也将此称为测试尺寸)。我们注意到Q并不一定要与训练尺寸S相同。

(1)single scale:

 测试时所用的scale固定。这里把训练scale和测试的scale分别用S和Q表示。当S为固定值时,令Q = S固定;当S为[Smin,Smax]浮动时,Q固定为 = 0.5[Smin + Smax]。 测试发现LRN局部响应归一化并没有带来精度提升,故在A-LRN之后的B~E类VGG网络中,都没有使用。
在这里插入图片描述

 结论,Q固定的情况下:

  • 变动的S比固定的S准确率高。在训练中,采用浮动尺度效果更好,因为这有助于学习分类目标在不同尺寸下的特征。
  • 卷积网络越深,损失越小,效果越好。
  • C优于B,表明增加的非线性relu有效
  • D优于C,表明了卷积层3×3对于捕捉空间特征有帮助
  • E深度达到19层后达到了损失的最低点,但是对于其他更大型的数据集来说,可能更深的模型效果更好。
  • B和同类型filter size为5×5的网络进行了对比,发现其top-1错误率比B高7%,表明小尺寸filter效果更好。

(2)multi scale:

 multi scale表示测试时的scale不固定。 这里当训练时的S固定时,Q取值是{S - 32, S, S+32}这三个值,进行测试过后取平均结果。 当S为[Smin,Smax]浮动时,Q取{Smin, 0.5(Smin+Smax), Smax},测试后取平均。

在这里插入图片描述

 结论,Q不固定的情况下:

  • 同single scale一样,模型越深,效果越好;
  • 同深度下,浮动scale效果好于固定scale

(3)dense evaluation 与multi-crop evaluation两种预测方法的区别以及效果:

  • multi-crop:
     即对图像进行多样本的随机裁剪,然后通过网络预测每一个样本的结构,最终对所有结果平均。GoogleNet中使用了很多multi crop的技巧,可以显著提升精度,因为有更精细的采样。
  • densely:
     利用FCN的思想,将原图直接送到网络进行预测,将最后的全连接层改为1x1的卷积(全卷积网络),这样最后可以得出一个预测的score map,再对结果求平均。
    在这里插入图片描述

七、迁移学习与预训练

 使用训练好的神经网络模型,来训练自己的数据集合。即使用训练好的权重来初始化网络,并不是随机初始化。在实践中,我们通常不会完全重头开始随机初始化训练,能满足深度网络需求的足够大小的数据集相当少见。作为替代,通常是在一个大型数据集上预训练一个网络,然后使用该网络的权重作为初始设置或作为相关任务的固定的特征提取器。

为什么要迁移学习?

  1. 站在巨人的肩膀上:前人花很大精力训练出来的模型在大概率上会比你自己从零开始搭的模型要强悍,没有必要重复造轮子。
  2. 训练成本可以很低:如果采用导出特征向量的方法进行迁移学习,后期的训练成本非常低,用CPU都完全无压力,没有深度学习机器也可以做。
  3. 适用于小数据集:对于数据集本身很小(几千张图片)的情况,从头开始训练具有几千万参数的大型神经网络是不现实的,因为越大的模型对数据量的要求越大,过拟合无法避免。这时候如果还想用上大型神经网络的超强特征提取能力,只能靠迁移学习。

参考:迁移学习——Fine-tune

1.迁移学习

 为了对迁移学习产生一个直观的认识,不妨拿老师与学生之间的关系做类比。一位老师通常在他所教授的领域有着多年丰富的经验,在这些积累的基础上,老师们能够在课堂上教授给学生们该领域最简明扼要的内容。这个过程可以看做是老手与新手之间的“信息转移”。

 这个过程在神经网络中也适用。我们知道,神经网络需要用数据来训练,它从数据中获得信息,进而把它们转换成相应的权重。这些权重能够被提取出来,迁移到其他的神经网络中,我们“迁移”了这些学来的特征,就不需要从零开始训练一个神经网络了 。

迁移学习(Transfer learning) 顾名思义就是把已训练好的模型(预训练模型)参数迁移到新的模型来帮助新模型训练。考虑到大部分数据或任务都是存在相关性的,所以通过迁移学习我们可以将已经学到的模型参数(也可理解为模型学到的知识)通过某种方式来分享给新模型从而加快并优化模型的学习效率,不用像大多数网络那样从零学习。
在这里插入图片描述
其中,实现迁移学习有以下三种手段:

  1. Transfer Learning:冻结预训练模型的全部卷积层,只训练自己定制的全连接层。
  2. Extract Feature Vector:先计算出预训练模型的卷积层对所有训练和测试数据的特征向量,然后抛开预训练模型,只训练自己定制的简配版全连接网络。
  3. Fine-tuning:冻结预训练模型的部分卷积层(通常是靠近输入的多数卷积层,因为这些层保留了大量底层信息)甚至不冻结任何网络层,训练剩下的卷积层(通常是靠近输出的部分卷积层)和全连接层。

2.预训练

  • 预训练模型就是已经用数据集训练好了的模型。
  • 现在我们常用的预训练模型就是他人用常用模型,比如VGG16/19,Resnet等模型,并用大型数据集来做训练集,比如Imagenet, COCO等训练好的模型参数;
  • 正常情况下,我们常用的VGG16/19等网络已经是他人调试好的优秀网络,我们无需再修改其网络结构。

3.Fine-tune

Fine-tune的原理就是利用已知的网络结构和已知的网络参数,修改output层为我们自己的层,微调最后一层前的若干层的参数,这样就有效利用了深度神经网络强大的泛化能力,又免去了设计复杂的模型以及耗时良久的训练,所以Fine-tune是当数据量不足时的一个比较合适的选择。

4.应用

 微调策略取决于很多因素,但最重要的两个是:(1)新数据集大小。(2)新数据集与原数据集的相似度。

  • 新数据集,内容上相似:这种情况下,通常只需要训练最后的输出层,即最后一层,因为可能分类的数量不同,最后一层需要做修改。
  • 新数据集,内容上相似:最理想情况,可以微调整个网络,因为数据集足够大不用担心过拟合。
  • 新数据集,内容不相似:使用微调效果不是很好,可以尝试冻结前面大部分卷积层,重新训练网络的高超卷积层及全连接层。
  • 新数据集,内容不相似:由于数据集很大,且相似度比较低,最好重头开始训练整个网络。

5.代码实现:

 在PyTorch中实现预训练首先需要导入经典模型,我们可以使用模型中已经存在的参数“pretrain”来帮助我们加载预训练模型上带的权重。PyTorch中所有模型的预训练都基于ImageNet数据集来完成,这个与训练参数在大多数实际照片上都可以有所帮助,但对表格数据和MNIST这类的数据集帮助不是很大。
模型的预训练/迁移学习

八、总结

  • LRN对网络性能提升没有帮助,因此在其他组网络中都没有使用;
  • 对于同一个网络结构多尺度训练可以提高网络精度;
  • D,E模型(VGG)效果最好,一定程度加深网络可以提升网络精度;
  • 多个小卷积核比单个大卷积核性能好(作者在论文中与ZFNet对比,调换掉了 7 × 7 7\times7 7×7 5 × 5 5\times5 5×5的卷积核)
  • VGG很简洁优美,从头到尾只有 3 × 3 3\times3 3×3卷积与 2 × 2 2\times2 2×2池化,因此后面很多网络用VGG作BackBone来提取特征。

九、论文复现

 在论文中AlexNet作者使用的是ILSVRC 2012比赛数据集,该数据集非常大(有138G),下载、训练都很消耗时间,我们在复现的时候就不用这个数据集了。由于MNIST、CIFAR10、CIFAR100这些数据集图片尺寸都较小,不符合AlexNet网络输入尺寸227x227的要求,因此我们改用kaggle比赛经典的“猫狗大战”数据集了。

手撕 CNN 经典网络之 VGGNet(理论篇)

手撕 CNN 经典网络之 VGGNet(PyTorch实战篇)
在这里插入图片描述

具体代码

import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import time
from matplotlib import pyplot as plt
import numpy as np
import math
from PIL import Image
from torch.utils.data import Dataset

class MyDataset(Dataset):
    def __init__(self, txt_path, transform = None, target_transform = None):
        fh = open(txt_path, 'r')
        imgs = []
        for line in fh:
            line = line.rstrip()
            words = line.split()
            imgs.append((words[0], int(words[1]))) # 类别转为整型int
            self.imgs = imgs 
            self.transform = transform
            self.target_transform = target_transform
    def __getitem__(self, index):
        fn, label = self.imgs[index]
        img = Image.open(fn).convert('RGB') 
        #img = Image.open(fn)
        if self.transform is not None:
            img = self.transform(img) 
        return img, label
    def __len__(self):
        return len(self.imgs)

1.加载数据集和数据预处理

pipline_train = transforms.Compose([
    #随机旋转图片
    transforms.RandomHorizontalFlip(),
    #将图片尺寸resize到227x227
    transforms.Resize((227,227)),
    #将图片转化为Tensor格式
    transforms.ToTensor(),
    #正则化(当模型出现过拟合的情况时,用来降低模型的复杂度)
    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
pipline_test = transforms.Compose([
    #将图片尺寸resize到227x227
    transforms.Resize((227,227)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])

train_batchsize = 16    # 训练集batch大小
test_batchsize = 16    # 测试集batch大小

train_data = MyDataset('./data/catVSdog/train.txt', transform=pipline_train)
test_data = MyDataset('./data/catVSdog/test.txt', transform=pipline_test)

#加载数据集
trainloader = torch.utils.data.DataLoader(train_data, batch_size=train_batchsize, 
                                          shuffle=True, drop_last=False)
testloader = torch.utils.data.DataLoader(test_data, batch_size=test_batchsize, 
                                         shuffle=False, drop_last=False)
# 类别信息也是需要我们给定的
classes = ('cat', 'dog') # 对应label=0,label=1
examples = enumerate(trainloader)
batch_idx, (example_data, example_label) = next(examples)

# 批量展示图片
plt.figure(facecolor='white')
for i in range(4):
    plt.subplot(1, 4, i + 1)
    plt.tight_layout()  #自动调整子图参数,使之填充整个图像区域
    img = example_data[i]
    img = img.numpy() # FloatTensor转为ndarray
    print(img.shape)
    img = np.transpose(img, (1,2,0)) # 把channel那一维放到最后
    img = img * [0.5, 0.5, 0.5] + [0.5, 0.5, 0.5]
    plt.imshow(img)
    plt.title("label:{}".format(example_label[i]))
    plt.xticks([])
    plt.yticks([])
plt.show()

2.搭建VGGNet神经网络结构

  • 定义VGG类的时候,参数num_classes指的是类别的数量,由于我们这里的数据集只有猫和狗两个类别,因此这里的全连接层的神经元个数做了微调。
  • num_classes=2,输出层也是两个神经元,不是原来的1000个神经元。
  • FC4096由原来的4096个神经元分别改为500、20个神经元。这里的改动大家注意一下,根据实际数据集的类别数量进行调整。整个网络的其它结构跟论文中的完全一样。
class VGG(nn.Module):
    def __init__(self, features, num_classes=2, init_weights=False):
        super(VGG, self).__init__()
        # 定义特征提取层
        self.features = features
        
        # 定义线性分类器
        self.classifier = nn.Sequential(
            nn.Linear(512*7*7, 500), nn.ReLU(True), nn.Dropout(p=0.5),
            nn.Linear(500, 20), nn.ReLU(True), nn.Dropout(p=0.5),
            nn.Linear(20, num_classes)
        )
        # 初始化权重
        if init_weights:
            self._initialize_weights()

    # 定义前馈计算
    def forward(self, x):
        # 输入为:N x 3 x 224 x 224,输出为N x 512 x 7 x 7
        x = self.features(x)
        
        # 输入是Nx7x7x512的FeatureMap,展开为N个7*7*512的一维向量,即N组7*7*512个神经元
        x = torch.flatten(x, start_dim=1)   # 从第1个维度开始展平,即保留N,N就是batch_size个样本
        
        # 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)   # 将权重w安装
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)    # 将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)
# 构造不同类型的VGG网络
def make_features(cfg: list):
    layers = []
    in_channels = 3
    
    # 根据cfg参数列表来构造VGG网络
    for v in cfg:
        # 'M'表示maxpool,
        if v == "M":
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        # 数字代表不同数量的3x3的卷积核
        else:
            # 卷积->ReLU
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            layers += [conv2d, nn.ReLU(True)]
            in_channels = v
    return nn.Sequential(*layers)


"""
    首先,我们从VGG 6个结构中选择了A、B、D、E这四个来搭建模型,建立的cfg字典包含了这4个结构。
    例如对于vgg16:
        64表示conv3-64,
        'M'表示maxpool,
        128表示conv3-128,
        256表示conv3-256,
        512表示conv3-512。
"""
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'],
}

# 构建VGG网络函数
def vgg(model_name="vgg16", **kwargs):
    # 断言函数,确保输入的模型名称在cfg列表中
    assert model_name in cfgs, "Warning: model number {} not in cfgs dict!".format(model_name)
    cfg = cfgs[model_name]

    model = VGG(make_features(cfg), **kwargs)
    return model
# 将定义好的网络结构搭载到GPU/CPU,并定义优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_name = "vgg16"
model = vgg(model_name=model_name, num_classes=2, init_weights=True)
model.to(device)

3.定义优化器与损失函数

#定义优化器
# optimizer = optim.Adam(model.parameters(), lr=0.0005)
optimizer = optim.SGD(model.parameters(), lr=0.0001, momentum=0.9, weight_decay=5e-4)

# 损失函数
criterion = torch.nn.CrossEntropyLoss()

4.绘图参数列表

# 训练次数
epoch = 10

# 绘图所用
plt_epoch = []      # 横坐标,训练次数

Train_Loss = []     # 训练损失
Train_Accuracy = [] # 训练精度

Test_Loss = []      # 测试损失
Test_Accuracy = []  # 测试精度

5.训练过程

def train_runner(model, epoch):
    #训练模型, 启用 BatchNormalization 和 Dropout, 将BatchNormalization和Dropout置为True
    model.train()
    
    total = 0             # 总样本数量
    correct =0.0          # 每轮epoch分类正确样本数量
    epoch_avg_loss = 0.0  # 每轮epoch的平均损失
 
    #enumerate迭代已加载的数据集,同时获取数据和数据下标
    for batch_idx, data in enumerate(trainloader, 0):
        batch_avg_loss = 0.0                                    # 每个batch的平均损失
        
        inputs, labels = data                                   # 解包     
        inputs, labels = inputs.to(device), labels.to(device)   # 把模型部署到device上  
        optimizer.zero_grad()                                   # 梯度清零        
        outputs = model(inputs)                                 # 保存训练结果
        loss = criterion(outputs, labels)                       # 计算损失和
        
        #dim=1表示返回每一行的最大值对应的列下标
        predict = outputs.argmax(dim=1)                         #获取最大概率的预测结果
        total += labels.size(0)                                 # 总样本数
        correct += (predict == labels).sum().item()             # 统计正确分类样本个数
        
        epoch_avg_loss += loss.item()                           # 把每轮epoch的损失累加
        batch_avg_loss += loss.item()                           # 累加每100个batch的损失
        
        loss.backward()                                         # 反向传播
        optimizer.step()                                        # 更新参数
        
        # 每100个batch进行一次loss输出
        if batch_idx % 100 == 99:
            print('[epoch:%d, batch_idx:%5d] batch_avg_loss: %.6f' % (epoch, batch_idx+1, batch_avg_loss/100))
            batch_avg_loss = 0.0
    
    # 这里train_batchsize是64,向上取整,所有小数都是向着数值更大的方向取整
    batch_num = math.ceil(total/64)
        
    # 每完成一次训练epoch,打印当前平均Loss和精度
    epoch_avg_loss /= batch_num 
    print("Train Epoch{} \t epoch_avg_loss: {:.6f}, accuracy: {:.6f}%".format(epoch, epoch_avg_loss, 100*(correct/total)))
    
    # 加入列表,以便于绘图
    Train_Loss.append(epoch_avg_loss)
    Train_Accuracy.append(correct/total)

6.测试函数

def test_runner(model):
    #模型验证, 必须要写, 否则只要有输入数据, 即使不训练, 它也会改变权值
    #因为调用eval()将不启用 BatchNormalization 和 Dropout, BatchNormalization和Dropout置为False
    model.eval()
    
    #统计模型正确率, 设置初始值
    correct = 0.0
    test_loss = 0.0
    total = 0
    
    #torch.no_grad将不会计算梯度, 也不会进行反向传播
    with torch.no_grad():
        for data, label in testloader:
            data, label = data.to(device), label.to(device)
            output = model(data)
            test_loss += criterion(output, label).item()
            predict = output.argmax(dim=1)
            #计算正确数量
            total += label.size(0)
            correct += (predict == label).sum().item()
            
        # # 每完成一次训练epoch,打印当前平均Loss和精度
        test_loss /= total
            
        #计算损失值和精度
        print("test_avarage_loss: {:.6f}, accuracy: {:.6f}%".format(test_loss, 100*(correct/total)))
        
    # 加入列表,以便于绘图
    Test_Loss.append(test_loss)
    Test_Accuracy.append(correct/total)

7.训练模型并绘图

if __name__ == '__main__':
    
    print("start_time",time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
    for epoch in range(1, epoch+1):
        plt_epoch.append(epoch)
        train_runner(model, epoch)
        test_runner(model)
    print("end_time: ",time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())),'\n')
 
    print('Finished Training')
    plt.subplot(2,2,1), plt.plot(plt_epoch, Train_Loss), plt.title('Train_Loss'), plt.grid()
    plt.subplot(2,2,2), plt.plot(plt_epoch, Train_Accuracy), plt.title('Train_Accuracy'), plt.grid()
    plt.subplot(2,2,3), plt.plot(plt_epoch, Test_Loss), plt.title('Test_Loss'), plt.grid()
    plt.subplot(2,2,4), plt.plot(plt_epoch, Test_Accuracy), plt.title('Test_Accuracy'), plt.grid()
    plt.tight_layout()
    plt.show()

print(model)
pathfile = 'C:\\Users\\LiZhangXun\\Desktop\\经典论文\Code\\4.VGG\\models'
save_filename = 'VGG16-catvsdog.pth'
model_path = os.path.join(pathfile, save_filename)
torch.save(model, model_path) #保存模型

5.测试模型精度

if __name__ == '__main__':
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = torch.load(model_path) #加载模型
    model = model.to(device)
    model.eval()    #把模型转为test模式

    #读取要预测的图片
    # 读取要预测的图片
    img = Image.open("./pic/test_cat.jpg") # 读取图像
    #img.show()
    plt.imshow(img) # 显示图片
    plt.axis('off') # 不显示坐标轴
    plt.show()

    # 导入图片,图片扩展后为[1,1,32,32]
    trans = transforms.Compose(
        [
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
        ])
    img = trans(img)
    img = img.to(device)
    img = img.unsqueeze(0)  #图片扩展多一维,因为输入到保存的模型中是4维的[batch_size,通道,长,宽],而普通图片只有三维,[通道,长,宽]

    # 预测 
    classes = ('cat', 'dog')
    output = model(img)
    prob = F.softmax(output,dim=1) #prob是2个分类的概率
    print("概率:",prob)
    
    value, predicted = torch.max(output.data, 1)
    predict = output.argmax(dim=1)
    pred_class = classes[predicted.item()]
    print("预测类别:",pred_class)

十、训练时出现的问题:

(1)CUDA out of memory:

 设置train_size=64,test_size=32时报错显存不足,把batch-size调小一些,如设置常用的256、128、64、32、16等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

travellerss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值