Bootstrap

深度学习——残差网络(ResNet)原理讲解+代码(pytroch)

残差网络论文地址:https://arxiv.org/abs/1512.03385

代码位置可以看到最后,直接跑的那种,数据集需要自己准备,数据集的格式也是有的。

一、开发背景

残差神经网络(ResNet)是由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出的, 斩获2015年ImageNet竞赛中分类任务第一名, 目标检测第一名。残差神经网络的主要贡献是发现了“退化现象(Degradation)”,并针对退化现象发明了 “直连边/短连接(Shortcut connection)”,极大的消除了深度过大的神经网络训练困难问题。神经网络的“深度”首次突破了100层、最大的神经网络甚至超过了1000层。

一、根据上述内容,引出问题,什么是退化现象?

随着CNN的发展和普及,人们发现增加神经网络的层数可以提高训练精度,但是如果只是单纯的增加网络的深度,可能会出现“梯度弥散”“梯度爆炸”等问题。传统的解决方法则是权重的初始化(normalized initializatiton)和批标准化(batch normlization),虽然解决了梯度问题,但是深度加深了,却带来了另外的问题,就是网络性能的退化现象,可以简单的理解为,随着训练轮数(epoch)的增加,精度到达一定程度后,就开始下架了。

由上图可以看出,56层的神经网络比22层的神经网络在训练集和测试集的准确率都要差得多,也就是所说的退化现象,也就是说如果只是简单的增加网络的深度,可能会导致神经网络模型退化,进而丢失网络前面获取的特征。可以这样想,假如当层数为40层的时候已经达到了最佳的模型,然后继续增加网络的层数,因为激活函数以及卷积等效果,只会增加整体(网络)的非线性,所以效果会变得更差。

该现象的实质:通过多个非线性层来近似恒等映射可能是困难的(恒等映射亦称恒等函数:是一种重要的映射,对任何元素,象与原象相同的映射)。为什么恒等映射是困难的,你也可以这么理解一下,因为随着网络层数的增多,而我们优化参数需要反向传播,这一过程会不断地传播梯度,但是假设在层数较深的位置梯度很小或者很大,那么传到浅层的时候是不是就会变得非常小,那样还能优化参数吗?也就导致了无法有效地对前面网络进行有效调整。

从信息论的角度讲,由于DPI(数据处理不等式)的存在,在前向传输的过程中,随着层数的加深,Feature Map包含的图像信息会逐层减少,而ResNet的直接映射的加入,保证了 l+1 层的网络一定比 l 层包含更多的图像信息。基于这种使用直接映射来连接网络不同层直接的思想,残差网络应运而生。

二、为什么深度过大而神经网络难以训练?

原因也很简单主要就是出现了梯度消失和梯度爆炸的现象,其他原因比如说是计算资源的消耗,这类原因可以通过GPU集群来解决,而过拟合的问题,可以通过采集海量的数据集和Dropout正则化可以有效解决,梯度消失和爆炸虽然也可以利用Batch Normalization解决,但根据实验发现并不如人愿望。

Batch Normalization(批量归一化):由Google在2015年提出,是近年来DL(深度学习)领域最重要的进步之一,该方法依靠两次连续的线性变换,希望转化后的数值满足一定的特性(分布),不仅可以加快了模型的收敛速度,也一定程度缓解了特征分布较散的问题,使深度神经网络(DNN)训练更快、更稳定。(这里讲的相对松散一些,详细的内容以及原理大家可以参考其他博客),具体的过程就是通过方法将该层的特征值分布重新拉回到标准正态分布,特征值降落在激活函数对于输入较为敏感的区间,输入的小变化可导致损失函数较大的变化,使得梯度变大,避免梯度消失,同时也可加快收敛。

梯度消失:我们知道神经网络在进行反向传播(BP算法)的时候会对参数W进行更新,梯度消失就是靠后面网络层(如layer3)能够正常的得到一个合理的偏导数,但是靠近输入层的网络层,计算的到的偏导数近乎零,W几乎无法得到更新。

原因:反向传播的时候的链式法则,越是浅层的网络,其梯度表达式可以展现出来连乘的形式,而这样如果都是小于1的,这样的话,浅层网络参数值的更新就会变得很慢,这就导致了深层网络的学习就等价于了只有后几层的浅层网络的学习了。

梯度爆炸:靠近输入层的网络层,计算的到的偏导数极其大,更新后W变成一个很大的数(爆炸)。

原因:类似于上述的原因,假如都是大于1的时候,那么浅层的网络的梯度过大,更新的参数变量也过大,所以无论是梯度消失还是爆炸都是训练过程会十分曲折的,都应该尽可能避免。

 

三、残差网络解决的问题

神经网络越来越深的时候,反传回来的梯度之间的相关性会越来越差,最后接近白噪声。因为我们知道图像具有局部相关性,可以认为梯度也应该具备类似的相关性,这样更新的梯度才有意义,如果梯度接近白噪声,那梯度更新可能根本就是在做随机扰动。基于这种退化问题,作者通过浅层网络等同映射构造深层模型,结果深层模型并没有比浅层网络有等同或更低的错误率,推断退化问题可能是因为深层的网络并不是那么好训练,也就是求解器很难去利用多层网络拟合同等函数

如果深层网络的后面那些层是恒等映射,那么模型就退化为一个浅层网络。那当前要解决的就是学习恒等映射函数了。这段话也是可以这样理解一下,如果更深层次的网络都能够学习到恒等映射,那么准确率起码不会降低,所以这正是残差网络的解决的问题。

二、残差块原理


一个残差快的数学模型如下图所示,残差网络和之前的网络最大的不同之处就是多了一条自身的捷径分支,正是因为这一个分支的存在,使得网络在反向传播的时候,损失可以通过这条捷径将梯度直接传向更前的网络,从而减缓网络退化问题,我们前边了解到梯度之间具有相关性的。我们在有个梯度相关性这个指标之后,作者分析了许多的结构和激活函数,发现了正是我们这个网络在保持梯度相关性上是很强的,除了这一点之外,残差网络并没有增加新的参数,只是多了一步加法,计算量相对而言也没有增加。

 下边这句话我不是太懂,就是我们可以转换为学习一个残差函数:F(x) = H(x)- x,主要F(x)= 0 就构成了一个恒等变换,而且拟合残差肯定更容易。

 F是求和前网络映射,H是从输入到求和后的网络映射。比如把5映射到5.1,那么引入残差前是:

F(5)′=5.1

引入残差后是:H(5)=5.1,H(5.1)=F(5)+5,F(5)=0.1

这里的F′和F都表示网络参数映射,引入残差后的映射对输出的变化更敏感。比如S输出从5.1变到5.2,映射的输出F′增加了2%,而对于残差结构输出从5.1到5.2,映射F是从0.1到0.2,增加了100%。明显后者输出变化对权重的调整作用更大,所以效果更好。残差的思想都是去掉相同的主体部分,从而突出微小的变化。

三、残差网络讨论

至于为何shortcut(捷径)的输入是X,而不是X/2或是其他形式。作者的另一篇文章中探讨了这个问题,对以下6种结构(图2)的残差结构进行实验比较,shortcut是X/2的就是第二种,结果发现还是第一种效果好。

这种残差学习结构可以通过前向神经网络+shortcut连接实现,如结构图1所示。而且shortcut连接相当于简单执行了同等映射,不会产生额外的参数,也不会增加计算复杂度。 而且,整个网络可以依旧通过端到端的反向传播训练。

就是因为多个层在理论上可以拟合任何的函数,恒等函数仍然可以是用多层网络去拟合的,但是拟合残差函数相对来说比那种更加简单,也就是更加利于学习。作者说,这种残差形式是由退化问题激发的。根据前文,如果增加的层被构建为同等函数,那么理论上,更深的模型的训练误差不应当大于浅层模型,但是出现的退化问题表明,求解器很难去利用多层网络拟合同等函数。但是,残差的表示形式使得多层网络近似起来要容易的多,如果`同等函数可被优化近似,那么多层网络的权重就会简单地逼近0来实现同等映射,即 F(x)=0。

一、网络结构

残差网络是由一系列的残差块加上一些其他层组成的,残差快由两部分组成直接映射部分和残差部分。

整体的网络塔尖分为两步:

  1. 使用VGG公式搭建Plain VGG网络
  2. 在Plain VGG的卷积网络之间插入Identity Mapping,注意需要升维或者降维的时候加入 1×1 卷积。

至于如何解决的梯度问题,需要公式去推,我也在苦恼这方面,希望有大佬可以给与解答,感谢。

四、代码实现,我跑了一遍,然后有四个类别,效果很好

  • spilit_data.py:划分给定的数据集为训练集和测试集
  • model.py :定义ResNet网络模型
  • train.py:加载数据集并训练,计算loss和accuracy,保存训练好的网络参数
  • predict.py:用自己的数据集进行分类测试

(1)spilit_data.py 实现数据集的划分,只有训练集和验证集的划分没有其他的

import os
from shutil import copy, rmtree
import random


def mk_file(file_path: str):
    if os.path.exists(file_path):
        # 如果文件夹存在,则先删除原文件夹再重新创建
        rmtree(file_path)
    os.makedirs(file_path)


def main():
    # 保证随机可复现
    random.seed(0)

    # 将数据集中10%的数据划分到验证集中
    split_rate = 0.1

    # 指向解压后的flower_photos文件夹
    # getcwd():该函数不需要传递参数,获得当前所运行脚本的路径
    cwd = os.getcwd()
    # join():用于拼接文件路径,可以传入多个路径
    data_root = os.path.join(cwd, "flower_data")
    origin_flower_path = os.path.join(data_root, "flower_photos")
    # 确定路径存在,否则反馈错误
    assert os.path.exists(origin_flower_path), "path '{}' does not exist.".format(origin_flower_path)
    # isdir():判断某一路径是否为目录
    # listdir():返回指定的文件夹包含的文件或文件夹的名字的列表
    flower_class = [cla for cla in os.listdir(origin_flower_path)
                    if os.path.isdir(os.path.join(origin_flower_path, cla))]

    # 创建训练集train文件夹,并由类名在其目录下创建子目录
    train_root = os.path.join(data_root, "train")
    mk_file(train_root)
    for cla in flower_class:
        # 建立每个类别对应的文件夹
        mk_file(os.path.join(train_root, cla))

    # 创建验证集val文件夹,并由类名在其目录下创建子目录
    val_root = os.path.join(data_root, "val")
    mk_file(val_root)
    for cla in flower_class:
        # 建立每个类别对应的文件夹
        mk_file(os.path.join(val_root, cla))

    # 遍历所有类别的图像并按比例分成训练集和验证集
    for cla in flower_class:
        cla_path = os.path.join(origin_flower_path, cla)
        # iamges列表存储了该目录下所有图像的名称
        images = os.listdir(cla_path)
        num = len(images)
        # 随机采样验证集的索引
        # 从images列表中随机抽取k个图像名称
        # random.sample:用于截取列表的指定长度的随机数,返回列表
        # eval_index保存验证集val的图像名称
        eval_index = random.sample(images, k=int(num * split_rate))
        for index, image in enumerate(images):
            if image in eval_index:
                # 将分配至验证集中的文件复制到相应目录
                image_path = os.path.join(cla_path, image)
                new_path = os.path.join(val_root, cla)
                copy(image_path, new_path)
            else:
                # 将分配至训练集中的文件复制到相应目录
                image_path = os.path.join(cla_path, image)
                new_path = os.path.join(train_root, cla)
                copy(image_path, new_path)
                # '\r'回车,回到当前行的行首,而不会换到下一行,如果接着输出,本行以前的内容会被逐一覆盖
                # end="":将print自带的换行用end中指定的str代替
            print("\r[{}] processing [{}/{}]".format(cla, index + 1, num), end="")
        print()

    print("processing done!")


if __name__ == '__main__':
    main()

(2)model.py模型的构建

import torch.nn as nn
import torch

# 定义残差网络18/34的残差结构,为2个3*3的卷积
class BasicBlock(nn.Module):
    # 判断残差结构中,主分支的卷积核个数是否发生变化,不变则为1
    expansion = 1
    def __init__(self,in_channel,out_channel,stride=1,downsample=None,**kwargs):
        super(BasicBlock,self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channel,out_channels=out_channel,
                               kernel_size=3,stride=stride,padding=1,bias=False
        )
        # 使用批量归一化
        self.bn1 = nn.BatchNorm2d(out_channel)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(in_channels=out_channel,out_channels=out_channel,
                               kernel_size=3,stride=1,padding=1,bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.downsample =downsample
    def forward(self,x):

        identity = x
        if self.downsample is not None:
            identity=self.downsample(x)  # self当前对象实例,用于访问对象的属性和方法
        out = self.conv1(x)
        out =self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out +=identity
        out = self.relu(out)
        return out
class Bottleneck(nn.Module):
    # expansion是指在每个小残差块内,减少尺度增加维度的倍数
    # 该类输出的通道是输入的四倍
    expansion = 4
    def __init__(self,in_channel,out_channel,strdie=1,downsample=None,
                 groups=1,width_per_group=64):
        super(Bottleneck,self).__init__()

        width = int(out_channel*(width_per_group/64.))*groups

        self.conv1 = nn.Conv2d(in_channels=in_channel,out_channels=width,kernel_size=1,stride=1,bias=False)
        self.bn1 = nn.BatchNorm2d(width)
        self.conv2= nn.Conv2d(in_channels=width,out_channels=width,groups=groups
                              ,kernel_size=3,stride=strdie,bias=False,padding=1)
        self.bn2 = nn.BatchNorm2d(width)

        self.conv3 = nn.Conv2d(in_channels=width,out_channels=width,groups=groups,
                               kernel_size=3,stride=strdie,bias=False,padding=1)
        self.bn3 = nn.BatchNorm2d(out_channel*self.expansio)

        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
    def forward(self,x):
        identify = x
        if self.downsample is not None:
            identify = self.downsample(x)
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out+=identify
        out = self.relu(out)
        return out
#定义ResNet类
class ResNet(nn.Module):
    def __init__(self,block,blocks_num,num_classes=1000,include_top=True,groups=1,width_per_group=64):
        super(ResNet,self).__init__()
        self.include_top = include_top
        # maxpool的输出通道数为64,残差结构的输入通道为64
        self.in_channel = 64
        self.groups = groups
        self.width_per_group = width_per_group
        self.conv1 = nn.Conv2d(3,self.in_channel,kernel_size=7,stride=2,padding=3,bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
        # 浅层步长为1,深层的步长为2
        # block:定义了两种残差模块
        # block_num:定义了残差快的个数
        self.layer1 = self._make_layer(block,64,blocks_num[0])
        self.layer2 = self._make_layer(block, 128, blocks_num[1],stride=2)
        self.layer3 = self._make_layer(block, 256, blocks_num[2],stride=2)
        self.layer4 = self._make_layer(block, 512, blocks_num[3],stride=2)
        if self.include_top:
            self.avgpool = nn.AdaptiveAvgPool2d((1,1)) # 自适应平均池化,指定输出(H,w),通道数不变
            self.fc = nn.Linear(512*block.expansion,num_classes)
        # 遍历网络中的每一层
        # 继承nn.Moudle类中的一个方法:self.Moudle(),它会返回该网络中所有的moudles
        for m in self.modules():
            if isinstance(m,nn.Conv2d):
                # kaiming正态分布初始化,使得卷积层反向传播的输出的方差都为1
                # fan_in权重是通过线性层隐形确定
                # fan_out:通过创建随机矩阵显式创建权重
                nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
    def _make_layer(self,block,channel,block_num,stride=1):
        downsample = None
        if stride !=1 or self.in_channel !=channel*block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel,channel*block.expansion,kernel_size=1,stride=stride,bias=False),
                nn.BatchNorm2d(channel*block.expansion)
            )
        layers = []
        layers.append(block(self.in_channel,
                            channel,
                            downsample=downsample,
                            stride=stride,
                            groups=self.groups,
                            width_per_group = self.width_per_group))
        self.in_channel = channel*block.expansion

        for _ in range(1,block_num):
            layers.append(block(self.in_channel,
                                channel,
                                groups=self.groups,
                                width_per_group = self.width_per_group))
        # Sequential:自定义顺序连接成模型,生成网络结构
        return nn.Sequential(*layers)
    def forward(self,x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
        # 上述都是静态层,下边是动态层
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        if self.include_top:
            x = self.avgpool(x)
            x = torch.flatten(x,1)
            x = self.fc(x)
        return x


# ResNet()中block参数对应的位置是BasicBlock或Bottleneck
# ResNet()中blocks_num[0-3]对应[3, 4, 6, 3],表示残差模块中的残差数
# 34层的resnet
def resnet34(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet34-333f7ec4.pth
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


# 50层的resnet
def resnet50(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet50-19c8e357.pth
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


# 101层的resnet
def resnet101(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)

(3)利用模型进行训练

import os
import sys
import json

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets
from tqdm import tqdm
# 训练resnet34
from model import resnet34


def main():
    # 如果有NVIDA显卡,转到GPU训练,否则用CPU
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print("using {} device.".format(device))

    data_transform = {
        # 训练
        # Compose():将多个transforms的操作整合在一起
        "train": transforms.Compose([
            # RandomResizedCrop(224):将给定图像随机裁剪为不同的大小和宽高比,然后缩放所裁剪得到的图像为给定大小
            transforms.RandomResizedCrop(224),
            # RandomVerticalFlip():以0.5的概率竖直翻转给定的PIL图像
            transforms.RandomHorizontalFlip(),
            # ToTensor():数据转化为Tensor格式
            transforms.ToTensor(),
            # Normalize():将图像的像素值归一化到[-1,1]之间,使模型更容易收敛
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
        # 验证
        "val": transforms.Compose([transforms.Resize(256),
                                   transforms.CenterCrop(224),
                                   transforms.ToTensor(),
                                   transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}
    # abspath():获取文件当前目录的绝对路径
    # join():用于拼接文件路径,可以传入多个路径
    # getcwd():该函数不需要传递参数,获得当前所运行脚本的路径
    data_root = os.path.abspath(os.getcwd())
    # 得到数据集的路径
    image_path = os.path.join(data_root, "data")
    # exists():判断括号里的文件是否存在,可以是文件路径
    # 如果image_path不存在,序会抛出AssertionError错误,报错为参数内容“ ”
    assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
    train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
                                         transform=data_transform["train"])
    # 训练集长度
    train_num = len(train_dataset)

    # {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
    # class_to_idx:获取分类名称对应索引
    flower_list = train_dataset.class_to_idx
    # dict():创建一个新的字典
    # 循环遍历数组索引并交换val和key的值重新赋值给数组,这样模型预测的直接就是value类别值
    cla_dict = dict((val, key) for key, val in flower_list.items())
    # 把字典编码成json格式
    json_str = json.dumps(cla_dict, indent=4)
    # 把字典类别索引写入json文件
    with open('class_indices.json', 'w') as json_file:
        json_file.write(json_str)

    # 一次训练载入16张图像
    batch_size = 16
    # 确定进程数
    # min():返回给定参数的最小值,参数可以为序列
    # cpu_count():返回一个整数值,表示系统中的CPU数量,如果不确定CPU的数量,则不返回任何内容
    nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])
    print('Using {} dataloader workers every process'.format(nw))
    # DataLoader:将读取的数据按照batch size大小封装给训练集
    # dataset (Dataset):输入的数据集
    # batch_size (int, optional):每个batch加载多少个样本,默认: 1
    # shuffle (bool, optional):设置为True时会在每个epoch重新打乱数据,默认: False
    # num_workers(int, optional): 决定了有几个进程来处理,默认为0意味着所有的数据都会被load进主进程
    train_loader = torch.utils.data.DataLoader(train_dataset,
                                               batch_size=batch_size, shuffle=True,
                                               num_workers=nw)
    # 加载测试数据集
    validate_dataset = datasets.ImageFolder(root=os.path.join(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=nw)

    print("using {} images for training, {} images for validation.".format(train_num,
                                                                           val_num))

    # 模型实例化
    net = resnet34()
    net.to(device)
    # 加载预训练模型权重
    # model_weight_path = "./resnet34-pre.pth"
    # exists():判断括号里的文件是否存在,可以是文件路径
    # assert os.path.exists(model_weight_path), "file {} does not exist.".format(model_weight_path)
    # net.load_state_dict(torch.load(model_weight_path, map_location='cpu'))
    # 输入通道数
    # in_channel = net.fc.in_features
    # 全连接层
    # net.fc = nn.Linear(in_channel, 5)

    # 定义损失函数(交叉熵损失)
    loss_function = nn.CrossEntropyLoss()

    # 抽取模型参数
    params = [p for p in net.parameters() if p.requires_grad]
    # 定义adam优化器
    # params(iterable):要训练的参数,一般传入的是model.parameters()
    # lr(float):learning_rate学习率,也就是步长,默认:1e-3
    optimizer = optim.Adam(params, lr=0.0001)

    # 迭代次数(训练次数)
    epochs = 10
    # 用于判断最佳模型
    best_acc = 0.0
    # 最佳模型保存地址
    save_path = './resNet34.pth'
    train_steps = len(train_loader)
    for epoch in range(epochs):
        # 训练
        net.train()
        running_loss = 0.0
        # tqdm:进度条显示
        train_bar = tqdm(train_loader, file=sys.stdout)
        # train_bar: 传入数据(数据包括:训练数据和标签)
        # enumerate():将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在for循环当中
        # enumerate返回值有两个:一个是序号,一个是数据(包含训练数据和标签)
        # x:训练数据(inputs)(tensor类型的),y:标签(labels)(tensor类型)
        for step, data in enumerate(train_bar):
            # 前向传播
            images, labels = data
            # 计算训练值
            logits = net(images.to(device))
            # 计算损失
            loss = loss_function(logits, labels.to(device))
            # 反向传播
            # 清空过往梯度
            optimizer.zero_grad()
            # 反向传播,计算当前梯度
            loss.backward()
            optimizer.step()

            # item():得到元素张量的元素值
            running_loss += loss.item()

            # 进度条的前缀
            # .3f:表示浮点数的精度为3(小数位保留3位)
            train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                     epochs,
                                                                     loss)

        # 测试
        # eval():如果模型中有Batch Normalization和Dropout,则不启用,以防改变权值
        net.eval()
        acc = 0.0
        # 清空历史梯度,与训练最大的区别是测试过程中取消了反向传播
        with torch.no_grad():
            val_bar = tqdm(validate_loader, file=sys.stdout)
            for val_data in val_bar:
                val_images, val_labels = val_data
                outputs = net(val_images.to(device))
                # torch.max(input, dim)函数
                # input是具体的tensor,dim是max函数索引的维度,0是每列的最大值,1是每行的最大值输出
                # 函数会返回两个tensor,第一个tensor是每行的最大值;第二个tensor是每行最大值的索引
                predict_y = torch.max(outputs, dim=1)[1]
                # 对两个张量Tensor进行逐元素的比较,若相同位置的两个元素相同,则返回True;若不同,返回False
                # .sum()对输入的tensor数据的某一维度求和
                acc += torch.eq(predict_y, val_labels.to(device)).sum().item()

                val_bar.desc = "valid epoch[{}/{}]".format(epoch + 1,
                                                           epochs)

        val_accurate = acc / val_num
        print('[epoch %d] train_loss: %.3f  val_accuracy: %.3f' %
              (epoch + 1, running_loss / train_steps, val_accurate))

        # 保存最好的模型权重
        if val_accurate > best_acc:
            best_acc = val_accurate
            # torch.save(state, dir)保存模型等相关参数,dir表示保存文件的路径+保存文件名
            # model.state_dict():返回的是一个OrderedDict,存储了网络结构的名字和对应的参数
            torch.save(net.state_dict(), save_path)

    print('Finished Training')


if __name__ == '__main__':
    main()

(4)预测图片

import os
import json

import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt

from model import resnet34


def main():
    # 如果有NVIDA显卡,转到GPU训练,否则用CPU
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    # 将多个transforms的操作整合在一起
    data_transform = transforms.Compose(
        [transforms.Resize(256),
         transforms.CenterCrop(224),
         transforms.ToTensor(),
         transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])

    # 加载图片
    img_path = "../tulip.jpg"
    # 确定图片存在,否则反馈错误
    assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
    img = Image.open(img_path)
    # imshow():对图像进行处理并显示其格式,show()则是将imshow()处理后的函数显示出来
    plt.imshow(img)
    # [C, H, W],转换图像格式
    img = data_transform(img)
    # [N, C, H, W],增加一个维度N
    img = torch.unsqueeze(img, dim=0)

    # 获取结果类型
    json_path = './class_indices.json'
    # 确定路径存在,否则反馈错误
    assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
    # 读取内容
    with open(json_path, "r") as f:
        class_indict = json.load(f)

    # 模型实例化,将模型转到device,结果类型有5种
    model = resnet34(num_classes=5).to(device)

    # 载入模型权重
    weights_path = "./resNet34.pth"
    # 确定模型存在,否则反馈错误
    assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
    # 加载训练好的模型参数
    model.load_state_dict(torch.load(weights_path, map_location=device))

    # 进入验证阶段
    model.eval()
    with torch.no_grad():
        # 预测类别
        # squeeze():维度压缩,返回一个tensor(张量),其中input中大小为1的所有维都已删除
        output = torch.squeeze(model(img.to(device))).cpu()
        # softmax:归一化指数函数,将预测结果输入进行非负性和归一化处理,最后将某一维度值处理为0-1之内的分类概率
        predict = torch.softmax(output, dim=0)
        # argmax(input):返回指定维度最大值的序号
        # .numpy():把tensor转换成numpy的格式
        predict_cla = torch.argmax(predict).numpy()

    # 输出的预测值与真实值
    print_res = "class: {}   prob: {:.3}".format(class_indict[str(predict_cla)],
                                                 predict[predict_cla].numpy())

    # 图片标题
    plt.title(print_res)
    for i in range(len(predict)):
        print("class: {:10}   prob: {:.3}".format(class_indict[str(i)],
                                                  predict[i].numpy()))
    plt.show()


if __name__ == '__main__':
    main()

五、训练结果

1000多张训练集,四个类别,效果如下。

如果有错误的地方,请大家批评指正,感谢。

;