跟着问题学7——ResNet网络详解及代码实战

ResNet概述——解决什么问题?

网络层数过深导致的退化问题。

自从深度神经网络AlexNet在ImageNet大放异彩之后,后来问世的深度神经网络就朝着网络层数越来越深的方向发展。直觉上我们不难得出结论:增加网络深度后,网络可以进行更加复杂的特征提取,因此更深的模型可以取得更好的结果。

但事实并非如此,人们发现随着网络深度的增加,模型精度并不总是提升,并且这个问题显然不是由过拟合(overfitting)造成的,因为网络加深后不仅测试误差变高了,它的训练误差也变高了。作者提出,这可能是因为更深的网络会伴随梯度消失/爆炸问题,从而阻碍网络的收敛。作者将这种加深网络深度但网络性能却下降的现象称为退化问题(degradation problem),如下图所示。

由于非线性激活函数Relu的存在,每次输入到输出的过程都几乎是不可逆的,这也造成了许多不可逆的信息损失。一个特征的一些有用的信息损失了,得到的结果肯定不尽人意。

ResNet网络结构

残差基本单元

随着网络层数的增加,梯度爆炸和梯度消失问题严重制约了神经网络的性能,研究人员通过提出包括Batch normalization在内的方法,已经一定程度上缓解了这个问题,但依然不足以满足需求。

作者想到了构建恒等映射(Identity mapping)来解决这个问题,问题解决的标志是:增加网络层数,但训练误差不增加。为什么是恒等映射呢:20层的网络是56层网络的一个子集,56层网络的解空间包含着20层网络的解空间。如果我们将56层网络的最后36层全部短接,这些层进来是什么出来也是什么(也就是做一个恒等映射),那这个56层网络不就等效于20层网络了吗,至少效果不会相比原先的20层网络差。

同样是56层网络,不引入恒等映射为什么就不行呢?因为梯度消失现象使得网络难以训练,虽然网络的深度加深了,但是实际上无法有效训练网络,训练不充分的网络不但无法提升性能,甚至降低了性能。

那怎么构建恒等映射呢?以残差学习基本单元来说,

以前方法

对于输入x,希望残差网络输出H(x),这样传递给下一层网络的输入是 H(x)=F(x),即直接拟合H(x)

ResNet中,

令H(x)=F(x)+x,那么残差网络就只需要学习输出一个残差F(x)=H(x)-x。(注意传递给下一层网络的输入仍是H(x))。作者提出,网络学习残差F(x)=H(x)-x会比直接学习原始特征H(x)简单的多。

残差模块:一条线路不变(恒等映射x);另一条线路负责拟合相对于原始网络的残差F(x),去纠正原始网络的偏差,而不是让整体网络去拟合全部的底层映射,这样网络只需要纠正偏差。

图2 残差学习基本单元

加了残差结构后,给了输入x一个多的选择。若神经网络学习到这层的参数是冗余的时候,它可以选择直接走这条“跳接”曲线(shortcut connection),只需要把主线路的F(x)变为0就可以跳过这个冗余层,而不需要再去拟合参数使得H(x)=F(x)=x。

输出变为F(x)+x=0+x=x很明显,将网络的输出F(x)优化为0比将其做一个恒等变换F(x)=x要容易得多。

模型在训练过程中,F(x)是训练出来的,如果F(x)对于提高模型的训练精度无作用,自然梯度下降算法就调整该部分的参数,使该部分的效果趋近于0.这样整个模型就不会出现深度越深反而效果越差的情况了。

残差结构之:残差基本单元F(x)的层数怎么设置比较合理?论文中设置了两种方式,一中是卷积核通道数完全相同的2层网络;第二种是卷积核为1*1,3*3,1*1,通道数也不完全相同的3层网络。

残差结构之Shortcuts Connection的两种方式:

首先将残差模块的输入X赋值给identity,然后X经过残差模块主结构得到output,比较output和identity的通道数和特征长宽尺寸,

根据连接前后的

(1)shortcuts映射后output和identity的通道数和特征长宽尺寸相同,F(x)与x相加就是就是每个通道逐元素(长宽)相加:

    output=F(x,Wi)+x

    F=W2σ(W1x)

其中 x(identity) 和 output分别表示层的输入和输出。函数 F(x,Wi)代表着学到的残差映射,σ 代表ReLU

这种方式通过shortcuts直接传递输入x,不会引入额外的参数也不会增加模块的计算复杂性,因此可以公平地将残差网络和plain网络作比较。

(2)如果两者维度不同(改变了输入/输出的通道或长宽尺寸),需要给x执行一个线性映射(代码里的下采样downsample)来匹配通道和尺寸。

    output=F(x,Wi)+Wsx.

    F=W2σ(W1x)

这种方式的目的仅仅是为了保持x与F(x)之间的维度一致,所以通常只在相邻残差块之间通道数改变时使用,绝大多数情况下仅使用第一种方式。

网络框架结构

Plain网络

论文的plain网络结构(Fig.3,中)主要受VGG网络 (Fig.3,左)的启发。

卷积层主要为3*3的滤波器,并遵循以下两点要求:

(i) 输出特征尺寸相同的层含有相同数量的滤波器;

(ii) 如果特征尺寸减半,则滤波器的数量增加一倍来保证每层的时间复杂度相同。

直接通过stride 为2的卷积层来进行下采样。在网络的最后是一个全局的平均pooling层和一个1000 类的包含softmax的全连接层。加权层的层数为34,如Fig.3(中)所示。

值得注意的是,ResNet模型比VGG网络(Fig.3,左)有更少的滤波器和更低的计算复杂度。34层的结构含有36亿个FLOPs(乘-加),而这仅仅只有VGG-19 (196亿个FLOPs)的18%。

残差网络

在以上plain网络的基础上,插入shortcut连接(Fig.3,右),将网络变成了对应的残差版本。如果输入和输出的维度相同时,可以直接使用恒等shortcuts (Eq.1)(Fig.3中的实线部分)。当维度增加时(Fig.3中的虚线部分),考虑两个选项:

(A) 零填充:shortcut仍然使用恒等映射,在增加的维度上使用0来填充,这样做不会增加额外的参数;

(B) 线性投影变换:使用Eq.2的映射shortcut来使维度保持一致(通过1*1的卷积,参数需要学习,精度比zero-padding更好,但是耗时更长,占用更多内存)。

对于这两个选项,当shortcut跨越两种尺寸的特征图时,均使用stride为2的卷积。

Fig.3 对应于ImageNet的网络框架举例。 左:VGG-19模型 (196亿个FLOPs)作为参考。中:plain网络,含有34个参数层(36 亿个FLOPs)。右:残差网络,含有34个参数层(36亿个FLOPs)。虚线表示的shortcuts增加了维度。Table 1展示了更多细节和其它变体。

图像维度变化

Layer34

in-channel

out-channel

kernel-size

stride

padding

down sample

size

conv2.1

64

64

3

1

1

None

b*64*56*56

conv2.x

64

64

3

1

1

None

b*64*56*56

conv3.1

64

128

3

2

1

true

b*128*28*28

conv3.x

128

128

3

1

1

none

b*128*28*28

conv4.1

128

256

3

2

1

true

b*256*14*14

conv4.x

256

256

3

1

1

none

b*256*14*14

conv5.1

256

512

3

2

1

true

b*512*7*7

conv5.x

512

512

3

1

1

none

b*512*7*7

Layer50

in-channel

out-channel

kernel-size

stride

padding

down sample

size

conv2.1.1

64

64

1

1

0

true

b*64*56*56

conv2.1.2

64

64

3

1

1

None

b*64*56*56

conv2.1.3

64

256

1

1

0

None

b*256*56*56

conv2.x.1

256

64

1

1

0

None

b*64*56*56

conv2.x.2

64

64

3

1

1

None

b*64*56*56

conv2.x.3

64

256

1

1

0

None

b*256*56*56

conv3.1.1

256

128

1

1

0

true

b*128*56*56

conv3.1.2

128

128

3

2

1

None

b*128*28*28

conv3.1.3

128

512

1

1

0

None

b*512*28*28

conv3.x.1

512

128

1

1

0

None

b*128*28*28

conv3.x.2

128

128

3

1

1

None

b*128*28*28

conv3.x.3

128

512

1

1

0

None

b*512*28*28

conv4.1.1

512

256

1

1

0

true

b*256*28*28

conv4.1.2

256

256

3

2

1

None

b*256*14*14

conv4.1.3

256

1024

1

1

0

None

b*1024*14*14

conv4.x.1

1024

256

1

1

0

None

b*256*14*14

conv4.x.2

256

256

3

1

1

None

b*256*14*14

conv4.x.3

256

1024

1

1

0

None

b*1024*14*14

conv5.1.1

1024

512

1

1

0

true

b*512*14*14

conv5.1.2

512

512

3

2

1

None

b*512*7*7

conv5.1.3

512

2048

1

1

0

None

b*2048*7*7

conv5.x.1

2048

512

1

1

0

None

b*512*7*7

conv5.x.2

512

512

3

1

1

None

b*512*7*7

conv5.x.3

512

2048

1

1

0

None

b*2048*7*7

从中可以看到经过不同网络层时输入输出通道数和特征尺寸的一些变化规律:

1.每个layer模块(图中的conv2.x等)中的子结构是相同的,在后面乘上重复的系数。但是在代码编写过程中第一个子结构和后面的是分开的,主要是因为第一个子结构涉及到上一层layer的输入输出通道变化,而后面的则是在同一layer里;

2.具体来说以50层(往上)为例,conv残差模块的第三个输出通道数256,但因为*3,所以下一个子结构第一层的输入通道数是256,但输出通道数是64;

3.从3.x开始的残差模块,stride=2,特征图的尺寸会缩小一倍,最终的输出通道数会扩大1倍,利用通道数的增加等效特征图尺寸的缩小。

4..18和34层的每个残差模块(不管系数乘以多少)只在第一个卷积层stride=2,降低尺寸,其它网络层都是stride=1;50层以上的则是在第一个子结构的3*3卷积层stride=2,降低尺寸,其它网络层都是stride=1;

代码实现

vision/torchvision/models/resnet.py at main · pytorch/vision · GitHub

ResNet主要有五种主要形式:Res18,Res34,Res50,Res101,Res152;

如下图所示,每个网络都包括三个主要部分:输入部分、输出部分和中间卷积部分(中间卷积部分包括如图所示的Stage1到Stage4共计四个stage)。尽管ResNet的变种形式丰富,但都遵循上述的结构特点,网络之间的不同主要在于中间卷积部分的block参数和个数存在差异

输入部分首先经过为一个size=7x7,stride为2的卷积处理,然后经过size=3x3, stride=2的最大池化处理,通过这一步,一个224x224的输入图像就会变56x56大小的特征图,极大减少了存储所需大小。

接下来的四个阶段,也就是代码中的layer1,layer2,layer3,layer4,这也是网络的主干部分。这里用make_layer函数产生四个layer,需要用户输入每个layer的block数目(即layers列表)以及采用的block类型(basicblock还是bottleblock)

代码整体结构

特征尺寸变化

model.py

import torch
import torch.nn as nn
import random
import torch.nn.functional as F

class BasicBlock(nn.Module):
    expansion=1
    #change_channels:输入通道数。out_channels:输出通道数。
    # stride:第一个卷积层的 stride。downsample:从 make_layer 中传入的 downsample 层。
    def __init__(self,change_channels,out_channels,stride=1,downsample=None):
        super(BasicBlock,self).__init__()
        #定义第 1 组 conv3x3 -> norm_layer -> relu,这里使用传入的 stride 和 change_channels。
        # (如果是 layer2 ,layer3 ,layer4 里的第一个 BasicBlock,那么 stride=2,这里会降采样和改变通道数)。
        self.conv1=nn.Conv2d(in_channels=change_channels,out_channels=out_channels,kernel_size=3,stride=stride,padding=1)
        self.bn1=nn.BatchNorm2d(num_features=out_channels)
        #注意,2 个卷积层都需要经过 relu 层,但它们使用的是同一个 relu 层。
        self.relu=nn.ReLU()
        #定义第 2 组 conv3x3 -> norm_layer -> relu,这里不使用传入的 stride (默认为 1),
        # 输入通道数和输出通道数使用out_channels,也就是不需要降采样和改变通道数。
        self.conv2 = nn.Conv2d(in_channels=out_channels, out_channels=out_channels, kernel_size=3, stride=1,padding=1)
        self.bn2 = nn.BatchNorm2d(num_features=out_channels)
        self.downsample=downsample
# 输入数据分成两条路,一条路经过两个3 * 3卷积,另一条路直接短接,二者相加经过relu输出。
# 核心就是判断短接的路要不要downsample,这点在make_layer 里面通过stride是否!=1(即特征尺寸变化)或者输入输出通道数是否一致来判断
    def forward(self,x):
        # x 赋值给 identity,用于后面的 shortcut 连接。
        identity=x
        # x 经过第 1 组 conv3x3 -> norm_layer,得到 out。
        # 如果是 layer2 ,layer3 ,layer4 里的第一个 BasicBlock,那么 downsample 不为空,会经过 downsample 层,得到 identity。
        if self.downsample:
            identity=self.downsample(x)
#x 经过第 1 组 conv3x3 -> norm_layer -> relu,如果是 layer2 ,layer3 ,layer4 里的第一个 BasicBlock,那么 stride=2,第一个卷积层会降采样。
        x=self.conv1(x)
        x=self.bn1(x)
        x=self.relu(x)

        x=self.conv2(x)
        x=self.bn2(x)
        print("identity",identity.size())
        print("x",x.size())
#最后将 identity 和 out 相加,经过 relu ,得到输出。
        x=x+identity
        x=self.relu(x)
        return x
class BottleBlock(nn.Module):
    expansion=4
    #in_channel:输入通道数。out_channel:输出通道数。
    #stride:第一个卷积层的 stride。downsample:从 layer 中传入的 downsample 层。
    def __init__(self,in_channel,out_channel,stride=1,downsample=None):
        super(BottleBlock,self).__init__()
        #·定义第 1 组 conv1x1 -> norm_layer,使用 out_channel,作用是进行降维,减少通道数。stride默认为1。
        self.conv1=nn.Conv2d(in_channels=in_channel,out_channels=out_channel,kernel_size=1, stride=1)
        self.bn1=nn.BatchNorm2d(num_features=out_channel)
       #定义第 2 组 conv3x3 -> norm_layer,这里使用传入的 stride,输入通道数和输出通道数使用width。
        # (如果是 layer2 ,layer3 ,layer4 里的第一个 Bottleneck,那么 stride=2,这里会降采样)。
        self.conv2=nn.Conv2d(in_channels=out_channel,out_channels=out_channel,kernel_size=3,stride=stride,padding=1)
        self.bn2=nn.BatchNorm2d(num_features=out_channel)
       #定义第 3 组 conv1x1 -> norm_layer,使用 planes * self.expansion,作用是进行升维,增加通道数,stride默认为1。
        self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel*self.expansion, kernel_size=1, stride=1)
        self.bn3 = nn.BatchNorm2d(num_features=out_channel*self.expansion)
        self.relu=nn.ReLU(inplace=True)
        self.downsample=downsample






    def forward(self,x):
        identity=x
        if self.downsample:
            identity=self.downsample(x)
       #1x1 的卷积是为了降维,减少通道数
        x=self.conv1(x)
        x=self.bn1(x)
        x = self.relu(x)
        # 3x3 的卷积是为了改变图片大小,不改变通道数
        x=self.conv2(x)
        x=self.bn2(x)
        x = self.relu(x)
        # 1x1 的卷积是为了升维,增加通道数,增加到 out_channel * 4
        x=self.conv3(x)
        x=self.bn3(x)

        print("identity", identity.size())
        print("x", x.size())

        x=x+identity
        x = self.relu(x)
        return x
class ResNet(nn.Module):
    def __init__(self,block,blocks_num,num_classes):
        super(ResNet,self).__init__()
        self.change_channels=64
        #输入部分首先经过为一个size=7x7,stride为2的卷积处理,
        self.conv1=nn.Conv2d(in_channels=3,out_channels=64,kernel_size=7,stride=2,padding=3)
        self.bn1=nn.BatchNorm2d(num_features=64)
        self.relu=nn.ReLU()
        # 然后经过size=3x3, stride=2的最大池化处理,通过这一步,一个224x224的输入图像就会变56x56大小的特征图,极大减少了存储所需大小。
        self.maxpool=nn.MaxPool2d(kernel_size=3,stride=2,padding=1)

        self.layer1=self._make_layer(block,blocks_num[0],64,stride=1)

        self.layer2=self._make_layer(block,blocks_num[1],128,stride=2)

        self.layer3=self._make_layer(block,blocks_num[2],256,stride=2)

        self.layer4=self._make_layer(block,blocks_num[3],512,stride=2)
       #通过全局自适应平滑池化,把所有的特征图拉成1*1,
        # 对于res18来说,就是1x512x7x7 的输入数据拉成 1x512x1x1,然后接全连接层输出,输出节点个数与预测类别个数一致。
        #动态池化层
        self.avgpool=nn.AdaptiveAvgPool2d((1,1))
        self.flateen=nn.Flatten()
        self.fc=nn.Linear(512*block.expansion,num_classes)

        # 遍历网络中的每一层
        # 继承nn.Module类中的一个方法:self.modules(), 他会返回该网络中的所有modules
        for m in self.modules():
            # isinstance(object, type):如果指定对象是指定类型,则isinstance()函数返回True
            # 如果是卷积层
            if isinstance(m, nn.Conv2d):
                # kaiming正态分布初始化,使得Conv2d卷积层反向传播的输出的方差都为1
                # fan_in:权重是通过线性层(卷积或全连接)隐性确定
                # fan_out:通过创建随机矩阵显式创建权重
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

    def forward(self,x):
        x=self.conv1(x)
        x=self.bn1(x)
        x=self.relu(x)
        x=self.maxpool(x)
        print("layer1")
        x=self.layer1(x)
        print("layer2")
        x=self.layer2(x)
        print("layer3")
        x=self.layer3(x)
        print("layer4")
        x=self.layer4(x)
        x=self.avgpool(x)
        x=self.flateen(x)
        x=self.fc(x)

        return x

    #block:每个layer里面使用的block,可以是 BasicBlock,BottleBlock。
    #block_num,一个整数,表示该层 layer 有多少个 block,根据给定的blocks_num里遍历的
    #out_channels输出的通道数
    #stride:第一个 block 的卷积层的 stride,默认为 1。注意,BasicBlock只有在每个 layer 的第一个 block 的第一层使用该参数。
    #BottleBlock只有在每个 layer 的第一个 block 的第二个层使用该参数。
    def _make_layer(self, block, block_num, out_channels, stride):
        layers = []
        #判断 stride 是否为 1,输入通道和输出通道是否相等。如果这两个条件都不成立,那么表明需要建立一个 1 X 1 的卷积层,
        # 来改变通道数和改变图片大小。具体是建立 downsample 层,包括 1x1卷积层和norm_layer,1x1卷积层注意输出通道数和stride。
        downsample=None
        if stride!=1 or self.change_channels!=out_channels*block.expansion:
            #建立第一个 block,把 downsample 传给 block 作为降采样的层,并且 stride 也使用传入的 stride(stride=2)
            downsample = nn.Sequential(
                nn.Conv2d(in_channels=self.change_channels,out_channels=out_channels*block.expansion,kernel_size=1, stride=stride),
                nn.BatchNorm2d(num_features=out_channels*block.expansion)
            )
        layers.append(block(self.change_channels, out_channels, stride, downsample))
        #改变输入通道数self.change_channels=out_channels*block.expansion,这个变量是整个类的全局变量
        #o在 BasicBlock 里,expansion=1,因此这一步不会改变通道数。在 BottleBlock 里,expansion=4,因此这一步会改变通道数。
        self.change_channels=out_channels*block.expansion
        #图片经过第一个 block后,就会改变通道数和图片大小。接下来 for 循环添加剩下的 block。
        # 从第 2 个 block 起,输入和输出通道数是相等的,因此就不用传入 downsample 和 stride(那么 block 的 stride 默认使用 1,
        for i in range(1, block_num):
            layers.append(block(self.change_channels, out_channels))

        return nn.Sequential(*layers)

if __name__=="__main__":
    x=torch.randn([1,3,224,224])
   # [3, 4, 6, 3] 等则代表了bolck的重复堆叠次数
    # blocks_num=[3,4,6,3]
    # model=ResNet(BasicBlock,blocks_num,num_classes=7)
    blocks_num = [3, 4, 6, 3]
    model = ResNet(BottleBlock, blocks_num, num_classes=7)
    y=model(x)
    print(y.size())
    print(model)

train.py

import torch
from torch import nn
from models import ResNet
from models.ResNet import *
from torch.optim import lr_scheduler
from  torchvision import transforms,datasets
from  torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
import os
import matplotlib.pyplot as plt

train_transform=transforms.Compose([
# RandomResizedCrop(224):将给定图像随机裁剪为不同的大小和宽高比,然后缩放所裁剪得到的图像为给定大小
    transforms.RandomResizedCrop(224),
# RandomVerticalFlip():以0.5的概率竖直翻转给定的PIL图像
    transforms.RandomHorizontalFlip(),
    # ToTensor():数据转化为Tensor格式
    transforms.ToTensor(),
# Normalize():将图像三个通道的像素值归一化到[-1,1]之间,使模型更容易收敛
    transforms.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])
])
test_transform=transforms.Compose([transforms.Resize((224, 224)),
                    transforms.ToTensor(),
                    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

#ImageFolder(root, transform``=``None``, target_transform``=``None``, loader``=``default_loader)
#root 指定路径加载图片;  transform:对PIL Image进行的转换操作,transform的输入是使用loader读取图片的返回对象
#target_transform:对label的转换   loader:给定路径后如何读取图片,默认读取为RGB格式的PIL Image对象
#label是按照文件夹名顺序排序后存成字典,即{类名:类序号(从0开始)},一般来说最好直接将文件夹命名为从0开始的数字,举例来说,两个类别,
#狗和猫,把狗的图片放到文件夹名为0下;猫的图片放到文件夹名为1的下面。
# 这样会和ImageFolder实际的label一致, 如果不是这种命名规范,建议看看self.class_to_idx属性以了解label和文件夹名的映射关系
train_dataset=ImageFolder(r"E:\计算机\data\fer2013_数据增强版本\train",train_transform)
test_dataset=ImageFolder(r"E:\计算机\data\fer2013_数据增强版本\test",test_transform)

train_dataloader=DataLoader(train_dataset,batch_size=32,shuffle=True)

test_dataloader=DataLoader(test_dataset,batch_size=32,shuffle=True)

device='cuda' if torch.cuda.is_available() else 'cpu'

blocks_num = [3, 4, 6, 3]
model = ResNet(BottleBlock, blocks_num, num_classes=7).to(device)
# 定义损失函数(交叉熵损失)
loss_fn=nn.CrossEntropyLoss()
# 定义adam优化器
# params(iterable):要训练的参数,一般传入的是model.parameters()
# lr(float):learning_rate学习率,也就是步长,默认:1e-3
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 迭代次数(训练次数)
epochs = 30
# 用于判断最佳模型
best_acc = 0.0
# 最佳模型保存地址
#save_path = './{}Net.pth'.format(model_name)
train_steps = len(train_dataloader)
def train(train_dataloader,model,loss_fn,optimizer):
    loss,acc,n=0.0,0.0,0
    for batch,(x,y) in enumerate(train_dataloader):
        x,y=x.to(device),y.to(device)
        output=model(x)
        cur_loss=loss_fn(output,y)
        _,pred=torch.max(output,axis=1)
        cur_acc=torch.sum(pred==y)/output.shape[0]
        optimizer.zero_grad()
        cur_loss.backward()
        optimizer.step()
        loss+=cur_loss.item()
        acc+=cur_acc.item()
        n=n+1
    train_loss=loss/n
    train_acc=acc/n
    print('train_loss==' + str(train_loss))
    # 计算训练的准确率
    print('train_acc' + str(train_acc))
    return train_loss, train_acc

def test(test_dataloader,model,loss_fn):
    loss,acc,n=0.0,0.0,0

    with torch.no_grad():
        for batch,(x,y) in enumerate(test_dataloader):
            x,y=x.to(device),y.to(device)
            output=model(x)
            cur_loss=loss_fn(output,y)
            _,pred=torch.max(output,axis=1)
            cur_acc=torch.sum(pred==y)/output.shape[0]
            optimizer.zero_grad()
            cur_loss.backward()
            optimizer.step()
            loss+=cur_loss.item()
            acc+=cur_acc.item()
            n=n+1
        test_loss=loss/n
        test_acc=acc/n
        print('test_loss==' + str(test_loss))
        # 计算训练的准确率
        print('test_acc' + str(test_acc))
        return test_loss, test_acc

def matplot_loss(train_loss, test_loss):

    # 参数label = ''传入字符串类型的值,也就是图例的名称
    plt.plot(train_loss, label='train_loss')
    plt.plot(test_loss, label='test_loss')
    # loc代表了图例在整个坐标轴平面中的位置(一般选取'best'这个参数值)
    plt.legend(loc='best')
    plt.xlabel('loss')
    plt.ylabel('epoch')
    plt.title("训练集和验证集的loss值对比图")
    plt.show()

    # 准确率


def matplot_acc(train_acc, test_acc):
    plt.plot(train_acc, label='train_acc')
    plt.plot(test_acc, label='test_acc')
    plt.legend(loc='best')
    plt.xlabel('acc')
    plt.ylabel('epoch')
    plt.title("训练集和验证集的acc值对比图")
    plt.show()

epochs=20
min_acc=0.0

loss_train=[]
acc_train=[]
loss_test=[]
acc_test=[]

for t in range(epochs):
    #不同的优化函数不同的使用方法
   # lr_scheduler.step()
    print(f"{t+1}\n------")
    train_loss,train_acc=train(train_dataloader,model,loss_fn,optimizer)
    test_loss,test_acc=test(test_dataloader,model,loss_fn)
    loss_train.append(train_loss)
    acc_train.append(train_acc)
    loss_test.append(test_loss)
    acc_test.append(test_acc)

    if test_acc>min_acc:
        folder="save_model"
        if not os.path.exists(folder):
            os.mkdir(folder)
        min_acc=test_acc
        print(f"保存{t+1}轮")
        torch.save(model.state_dict(),"save_model/resnet-best-model.pth")
    if t==epochs-1:
        torch.save(model.state_dict(), 'save_model/resnet-best-model.pth')
matplot_loss(loss_train,loss_test)
matplot_acc(acc_train,acc_test)

print('DONE!')

参考资料

[1]《动手学深度学习》 — 动手学深度学习 2.0.0 documentation

[2]https://zhuanlan.zhihu.com/p/54289848

[3]He, K., Zhang, X., Ren, S., & Sun, J. (2016). Deep residual learning for image recognition. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 770-778).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值