第3周学习:ResNet+ResNeXt


1. ResNet 详解

原论文地址:Deep Residual Learning for Image Recognition(作者是CV大佬何凯明团队)

ResNet 网络是在 2015年 由微软实验室提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。

在ResNet网络的创新点:

  • 提出 Residual 结构(残差结构),并搭建超深的网络结构(可突破1000层)
  • 使用 Batch Normalization 加速训练(丢弃dropout)

下图是ResNet-34层模型的结构简图:
ResNet-34层模型

1.1 什么是残差网络

为了解决深层网络中的退化问题,可以人为地让神经网络某些层跳过下一层神经元的连接,隔层相连,弱化每层之间的强联系。这种神经网络被称为 残差网络 (ResNets)。

残差网络由许多隔层相连的神经元子模块组成,我们称之为 残差块 (Residual block)。单个残差块的结构如下图所示:
在这里插入图片描述

1.2 残差网络的作用

在ResNet网络提出之前,传统的卷积神经网络都是通过将一系列卷积层与池化层进行堆叠得到的。

一般我们会觉得网络越深,特征信息越丰富,模型效果应该越好。但是实验证明,当网络堆叠到一定深度时,会出现两个问题:

1、梯度消失或梯度爆炸

关于梯度消失和梯度爆炸,其实看名字理解最好: 若每一层的误差梯度小于1,反向传播时,网络越深,梯度越趋近于0
反之,若每一层的误差梯度大于1,反向传播时,网路越深,梯度越来越大

2、退化问题(degradation problem):在解决了梯度消失、爆炸问题后,仍然存在深层网络的效果可能比浅层网络差的现象

如下图所示,20层网络 反而比 56层网络 的误差更小:
在这里插入图片描述

  • 对于梯度消失或梯度爆炸问题,ResNet论文提出通过数据的预处理以及在网络中使用 BN(Batch Normalization)层来解决。
  • 对于退化问题,ResNet论文提出了 residual结构(残差结构)来减轻退化问题。实验发现,随着网络的不断加深,效果并没有变差,而是变的更好了。 在这里插入图片描述
    上图中红色部分称为 short cut 或者 skip connection(也称 捷径分支),直接建立 a [ l ] a^{[l]} a[l] a [ l + 2 ] a^{[l+2]} a[l+2]之间的隔层联系。其前向传播的计算步骤为:
  • z [ l + 1 ] = W [ l + 1 ] a [ l ] + b [ l + 1 ] z^{[l+1]}=W^{[l+1]}a^{[l]}+b^{[l+1]} z[l+1]=W[l+1]a[l]+b[l+1]
  • a [ l + 1 ] = g ( z [ l + 1 ] ) a^{[l+1]}=g(z^{[l+1]}) a[l+1]=g(z[l+1])
  • z [ l + 12 ] = W [ l + 2 ] a [ l + 1 ] + b [ l + 2 ] z^{[l+12]}=W^{[l+2]}a^{[l+1]}+b^{[l+2]} z[l+12]=W[l+2]a[l+1]+b[l+2]
  • a [ l + 2 ] = g ( z [ l + 2 ] + a [ l ] ) a^{[l+2]}=g(z^{[l+2]}+a^{[l]}) a[l+2]=g(z[l+2]+a[l])
  • a [ l ] a^{[l]} a[l]直接隔层与下一层的线性输出相连,与 z [ l + 2 ] z^{[ l + 2 ]} z[l+2]共同通过激活函数(ReLU)输出 a [ l + 2 ] a^{[ l + 2 ]} a[l+2]
    由多个 残差块 组成的神经网络就是 残差网络 。其结构如下图所示:
    在这里插入图片描述
    实验表明,这种模型结构对于训练非常深的神经网络,效果很好。另外,为了便于区分,我们把 非残差网络 称为 Plain Network。

1.3 ResNet中的残差结构

实际应用中,残差结构的 short cut 不一定是隔一层连接,也可以中间隔多层,ResNet所提出的残差网络中就是隔多层。

跟VggNet类似,ResNet也有多个不同层的版本,而残差结构也有两种对应浅层和深层网络:

  ResNet 残差结构
浅层网络 ResNet18/34 BasicBlock
深层网络 ResNet50/101/152 Bottleneck

下图中左侧残差结构称为 BasicBlock,右侧残差结构称为 Bottleneck
在这里插入图片描述

对于深层的 Bottleneck,1×1的卷积核起到降维和升维(特征矩阵深度)的作用,同时可以大大减少网络参数。

可以计算一下,假设两个残差结构的输入特征和输出特征矩阵的深度都是256维,如下图:(注意左侧结构的改动)
在这里插入图片描述
那么两个残差结构所需的参数为:

  • 左侧:3 × 3 × 256 × 256 + 3 × 3 × 256 × 256 = 1 , 179 , 648
  • 右侧:1 × 1 × 256 × 64 + 3 × 3 × 64 × 64 + 1 × 1 × 64 × 256 = 69 , 632

注:CNN参数个数 = 卷积核尺寸×卷积核深度 × 卷积核组数 = 卷积核尺寸 × 输入特征矩阵深度 × 输出特征矩阵深度

明显搭建深层网络时,使用右侧的残差结构更合适。

1.4 降维时的 short cut

观察下图的 ResNet18层网络,可以发现有些残差块的 short cut 是实线的,而有些则是虚线的。

这些虚线的 short cut 上通过1×1的卷积核进行了维度处理(特征矩阵在长宽方向降采样,深度方向调整成下一层残差结构所需要的channel)。
在这里插入图片描述
下图是原论文给出的不同深度的ResNet网络结构配置,注意表中的残差结构给出了主分支上卷积核的大小与卷积核个数,表中 残差块×N 表示将该残差结构重复N次。
在这里插入图片描述
原文的表注中已说明,conv3_x, conv4_x, conv5_x所对应的一系列残差结构的第一层残差结构都是虚线残差结构。因为这一系列残差结构的第一层都有调整输入特征矩阵shape的使命(将特征矩阵的高和宽缩减为原来的一半,将深度channel调整成下一层残差结构所需要的channel)

需要注意的是,对于ResNet50/101/152,其实conv2_x所对应的一系列残差结构的第一层也是虚线残差结构,因为它需要调整输入特征矩阵的channel。根据表格可知通过3x3的max pool之后输出的特征矩阵shape应该是[56, 56, 64],但conv2_x所对应的一系列残差结构中的实线残差结构它们期望的输入特征矩阵shape是[56, 56, 256](因为这样才能保证输入输出特征矩阵shape相同,才能将捷径分支的输出与主分支的输出进行相加)。所以第一层残差结构需要将shape从[56, 56, 64] --> [56, 56, 256]。注意,这里只调整channel维度,高和宽不变(而conv3_x, conv4_x, conv5_x所对应的一系列残差结构的第一层虚线残差结构不仅要调整channel还要将高和宽缩减为原来的一半)。

下面是 ResNet 18/34 和 ResNet 50/101/152 具体的实线/虚线残差结构图:

  • ResNet 18/34
    在这里插入图片描述
  • ResNet 50/101/152s
    在这里插入图片描述

1.5 分组卷积

在这里插入图片描述
分组卷积的作用:
在这里插入图片描述

1.6 BN-Batch Normalization

目的是为了是一个batch的feature map,满足均值为0方差为1的分布规律。该方法能够加速网络训练并且提升准确率。
使用BN算法应该注意的问题:

  1. 训练时要将traning参数设置为True,在验证时将trainning参数设置为False(因为训练时需要对每一批batch统计相关的参数,而在验证时,每一次只是输入1张图片,不需要统计相关的参数)。在pytorch中可通过创建模型的model.train()和model.eval()方法控制。
  2. batch size尽可能设置大点,设置小后表现可能很糟糕,设置的越大求的均值和方差越接近整个训练集的均值和方差。
  3. 建议将bn层放在卷积层(Conv)和激活层(例如Relu)之间,且卷积层不要使用偏置bias

2. 迁移学习

在迁移学习中,我们希望利用源任务(Source Task)学到的知识帮助学习目标任务 (Target Task)。例如,一个训练好的图像分类网络能够被用于另一个图像相关的任务。再比如,一个网络在仿真环境学习的知识可以被迁移到真实环境的网络。迁移学习一个典型的例子就是载入训练好VGG网络,这个大规模分类网络能将图像分到1000个类别,然后把这个网络用于另一个任务,如医学图像分类。

为什么可以这么做呢?如下图所示,神经网络逐层提取图像的深层信息,这样,预训练网络就相当于一个特征提取器。
在这里插入图片描述
使用迁移学习的优势:

  1. 能够快速的训练出一个理想的结果
  2. 当数据集较小时也能训练出理想的效果
    注意:使用别人预训练好的模型参数时,要注意别人的预处理方式。

常见的迁移学习方式:

  1. 载入权重后训练所有参数
  2. 载入权重后只训练最后几层参数
  3. 载入权重后在原网络基础上再添加一层全连接层,仅训练最后一个全连接层
    在这里插入图片描述

3. 编程

尝试搭建了ResNet网络,并修改得到ResNext网络

import torch.nn as nn
import torch

'''
BasicBlock---18、34layer对应的残差结构
'''


class BasicBlock(nn.Module):
    expansion = 1  # 参加结构主分支的卷积核个数有没有发生变化,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)  # 使用Batch Normalization时,不需要使用偏置
        self.bn1 = nn.BatchNorm2d(out_channel)  # 卷积层1-conv1所输出的特征矩阵的深度
        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

    # 正向传播的过程(x:特征矩阵)
    def forward(self, x):
        identity = x  # 捷径分支上的输出值
        # 下采样参数是none,对应的是实线的残差结构,跳过这一部分
        # 下采样参数不为空,将输入的特征矩阵x输入到下采样函数中,得到捷径分支的输出
        if self.downsample is not None:
            identity = self.downsample(x)

        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


'''
BasicBlock---50、101、152layer对应的残差结构
'''


class Bottleneck(nn.Module):
    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    这么做的好处是能够在top1上提升大概0.5%的准确率。
    可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
    """
    expansion = 4  # 256 = 65 * 4

    def __init__(self, in_channel, out_channel, stride=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)  # squeeze channels
        self.bn1 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
                               kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel * self.expansion,
                               kernel_size=1, stride=1, bias=False)  # unsqueeze channels
        self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
        self.relu = nn.ReLU(inplace=True)  # inplace = True 时,会修改输入对象的值,所以打印出对象存储地址相同,类似于C语言的址传递,
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = 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 += identity
        out = self.relu(out)

        return out


class ResNet(nn.Module):

    def __init__(self,
                 block,  # 残差结构:第18、34layer是BasicBlock,第50、101、152layer是Bottleneck
                 blocks_num,  # 所使用的残差结构的数目,是一个list列表参数,例如34-layer是3,4,6,3
                 num_classes=1000,  # 训练集的分类个数
                 include_top=True,  # 方便在ResNet的基础上搭建更加复杂的网络(是否包含全连接层)
                 groups=1,  #
                 width_per_group=64  #
                 ):
        super(ResNet, self).__init__()
        self.include_top = include_top
        self.in_channel = 64  # 通过pooling之后的特征矩阵的深度,18、34-layer,50、101、152-layer max pooling后的特征矩阵的深度都是64

        self.groups = groups
        self.width_per_group = width_per_group
        # 输入的图片都是3通道的,所以Conv2d的第一个参数为3
        # 在进入ResNet网络之前,图片的大小为224*224,这里设置padding为3是为了令output_size为112*112,即满足下述公式:
        # output_size = (input_size - kernel_size + 2*padding)/stride + 1
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=self.in_channel, kernel_size=7, stride=2,  # 7*7
                               padding=3, bias=False)  # same模式下(在原有输入的基础上添加新的像素):padding=(filter_num-1)/2
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)  # 是否将计算得到的值直接覆盖之前的值
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)  # 112 --> 56

        # _make_layer函数中的第一个参数block为Bottleneck或者BasicBlock对应的残差结构,使用Bottleneck还是BasicBlock根据ResNet多少层决定
        # _make_layer函数中的第二个参数是残差结构中每个块的第一层的卷积层所使用的卷积核个数
        # _make_layer函数中的第三个参数是convx_x包含几个残差结构
        # layer1对应的是conv2_x中的一系列残差块,残差块的多少由blocks_num[0]决定
        # layer1不设值stride的原因是因为layer1经过maxpool之后宽和高已经和输出相同了,不需要在进行宽和高的变化了,所以使用默认的stride=1
        # conv2_x的结构是通过_make_layer函数完成的
        self.layer1 = self._make_layer(block, 64, blocks_num[0])  # conv2_x
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)  # conv3_x
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)  # conv4_x
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)  # conv5_x
        if self.include_top:
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # 自适应的平均池化下采样 output size = (1, 1)
            self.fc = nn.Linear(512 * block.expansion, num_classes)  # 全连接层

        # 循环是卷积层的初始化操作
        for m in self.modules():
            if isinstance(m, nn.Conv2d):  # 判断该层是否是Conv2d,如果当前遍历的层是Conv2d,则执行下面的语句
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None
        '''
         1、stride != 1:是stride=2时的(非conv3_1 conv4_1 conv5_1)需要加下采样函数;
         2、self.in_channel != channel * block.expansion:
         这里这个判断的意义在于:不管是ResNet18/34还是ResNet50/101/152,他们的conv3_1 conv4_1 conv5_1 都需要进行下采样,即第一个残差块的shortcut虚线连接,
         并且conv3_1 conv4_1 conv5_1的另一个意义就是进行宽和高的变化,所以这几个都是stride=2。但是ResNet18/34和ResNet50/101/152的有一个地方不同,
         对于conv2_1而言,ResNet18/34不需要进行维度和宽高的变化(因为已经经过了maxpool),所以ResNet18/34不需要执行这个判断里面的语句,
         但是ResNet50/101/152的conv2_1则不一样,ResNet50/101/152不需要进行宽高的变化[56x56x64->56x56x256],但是需要进行维度的变化,所以conv2_1的stride=1,
         但是self.in_channel != channel * block.expansion,(64!=64*4)所以还是要执行判断下的语句
        '''
        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(in_channels=self.in_channel, out_channels=channel * block.expansion, kernel_size=1,
                          stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))

        layers = []
        '''
        这个对应的是conv2_1 conv3_1 conv4_1 conv5_1,在这些残差块中都需要shortcut虚线连接
        (除了ResNet18/34的conv2_1,ResNet18/34的conv2_1对应的downsample为none),所以要传入downsample和stride
        '''
        layers.append(block(self.in_channel,  # 输入特征矩阵的深度
                            channel,  # 残差结构对应的主分支的第一个卷积层的卷积核个数
                            downsample=downsample,  # 下采样函数
                            stride=stride,
                            groups=self.groups,
                            width_per_group=self.width_per_group))
        '''
        在每一个大块的第一个残差结构执行完毕之后,维度已经发生了变化(ResNet18/34未发生变化 ResNet50/101/152已经变化了),
        所以在每一大块的后面的几个残差块的输入channel都变成了 channel * block.expansion
        '''
        self.in_channel = channel * block.expansion

        for _ in range(1, block_num):  # 因为convx_1已经搭建好了,所以这里不是从0,而是从1开始
            layers.append(block(self.in_channel,  # 输入特征矩阵的深度
                                channel,  # 残差结构对应的主分支的第一个卷积层的卷积核个数
                                groups=self.groups,
                                width_per_group=self.width_per_group))

        return nn.Sequential(*layers)  # 将layers列表转换成非关键字参数的形式,nn.Sequential将上面定义的一系列层结构组合在一起并返回

    # 正向传播过程
    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


def resnet18(num_classes=1000, include_top=True):
    return ResNet(BasicBlock, [2, 2, 2, 2], num_classes=num_classes, include_top=include_top)


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)


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)


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)


def resnext50_32x4d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth
    groups = 32
    width_per_group = 4
    return ResNet(Bottleneck, [3, 4, 6, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)


def resnext101_32x8d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth
    groups = 32
    width_per_group = 8
    return ResNet(Bottleneck, [3, 4, 23, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)

ResNet结果:
在这里插入图片描述
在这里插入图片描述

ResNext结果:
在这里插入图片描述
在这里插入图片描述
可以发现,ResNet和ResNext的预测结果都是对的,但是ResNext效果更好。

猫狗大战

LeNet

model.py

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


class LeNet(nn.Module):
    # 初始化方法:定义网络层结构
    def __init__(self):
        super(LeNet, self).__init__()  # 调用基类的构造函数
        self.conv1 = nn.Conv2d(3, 16, 5)  # (1)in_channel:图片RGB三通道(2)out_channel:filter组数(3)filter大小kernel_size:5x5
        self.pool1 = nn.MaxPool2d(2, 2)  # (1)kernel_size:2 (2)stride:2
        self.conv2 = nn.Conv2d(16, 32, 5)
        self.pool2 = nn.MaxPool2d(2, 2)  # output(32, 5, 5)
        # 全连接层的输入是一个一维的向量,所以需要把得到的特征矩阵展平成一维向量
        self.fc1 = nn.Linear(32 * 5 * 5, 120)  # 第一层的节点个数是120(leNet定义的)
        self.fc2 = nn.Linear(120, 84)  # 第二层的节点个数是84(leNet定义的)
        self.fc3 = nn.Linear(84, 2)  # 最后一个全连接层的输出要根据训练集进行修改,这里使用的训练集有10个类别

    # 定义正向传播的过程
    def forward(self, x):
        x = F.relu(self.conv1(x))  # input(3, 32, 32) output(16, 28, 28)
        x = self.pool1(x)  # output(16, 14, 14)
        x = F.relu(self.conv2(x))  # output(32, 10, 10)
        x = self.pool2(x)  # output(32, 5, 5)
        x = x.view(-1, 32 * 5 * 5)  # output(32*5*5)  -1:第一个维度,进行自动推理 32*5*5:展平后的节点个数
        x = F.relu(self.fc1(x))  # output(120)
        x = F.relu(self.fc2(x))  # output(84)
        x = self.fc3(x)  # output(10)
        return x

train.py

import torch.optim as optim
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim
import torch.utils.data
import torch.utils.data.distributed
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models
from model import LeNet
# from effnetv2 import effnetv2_s
from torch.autograd import Variable

# 设置全局参数
modellr = 1e-4  # 学习率
BATCH_SIZE = 2000
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 数据预处理
transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])

])

# 读取数据
dataset_train = datasets.ImageFolder('E:\\data_set\\cat_dog\\train', transform)
# print(dataset_train.imgs)  # List[Tuple[str,int]]
# 对应文件夹的label
print(dataset_train.class_to_idx)

dataset_test = datasets.ImageFolder('E:\\data_set\\cat_dog\\val', transform)
# 对应文件夹的label
print(dataset_test.class_to_idx)

# 导入数据
train_loader = torch.utils.data.DataLoader(dataset_train, batch_size=50, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset_test, batch_size=BATCH_SIZE, shuffle=False)

val_data_iter = iter(test_loader)
val_image, val_label = val_data_iter.next()

classes = {
    
    'cat', 'dog'}

net = LeNet()
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

for epoch in range(5):
    # print("epoch %d" % epoch)
    running_loss = 0.0
    for step, data in enumerate(train_loader, start=0):
        # get the inputs; data is a list of [inputs, labels]
        # print("step %d" % step)
        inputs, labels = data

        # zero the parameter gradients
        optimizer.zero_grad()
        # forward + backward + optimize
        outputs = net(inputs)
        loss = loss_function(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if step % 50 == 49:  # print every 500 mini-batches
            with torch.no_grad():
                outputs = net(val_image)  # [batch, 10]
                predict_y = torch.max(outputs, dim=1)[1]
                accuracy = torch.eq(predict_y, val_label).sum().item() / val_label.size(0)

                print('[%d, %5d] train_loss: %.3f  test_accuracy: %.3f' %
                      (epoch + 1, step + 1, running_loss / 500, accuracy))
                running_loss = 0.0

print('Finished Training')

save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path)

predict.py

import csv

import torch
import torchvision.transforms as transforms
import os
from PIL import Image
from torch.autograd import Variable
from model import LeNet


def main():
    transform = transforms.Compose(
        [transforms.Resize((32, 32)),
         transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    classes = ('cat', 'dog')

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

    model = LeNet()
    model.load_state_dict(torch.load("Lenet.pth"))

    model.eval()
    model.to(DEVICE)
    path = 'E:\\data_set\\cat_dog\\test\\'
    testList = os.listdir(path)
    csvfile = open('result.csv', 'w', newline='')
    writer = csv.writer(csvfile)

    # 计算正确率
    count = 0.0

    for file in testList:
        img = Image.open(path + file)
        img = transform(img)
        img.unsqueeze_(0)
        img = Variable(img).to(DEVICE)
        out = model(img)
        # Predict
        _, pred = torch.max(out.data, 1)
        #print(file, pred.toList()[0])
        print('Image Name:{},predict:{}'.format(file, classes[pred.data.item()]))
        writer.writerow([file, pred.data.item()])

if __name__ == '__main__':
    main()

然后手动处理一下csv文件,排下序
在这里插入图片描述

ResNet

Train.py

import torch.optim as optim
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim
import torch.utils.data
import torch.utils.data.distributed
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models
# from effnetv2 import effnetv2_s
from torch.autograd import Variable

# 设置全局参数
modellr = 1e-4  #学习率
BATCH_SIZE = 64
EPOCHS = 5
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 数据预处理
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])

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

# 读取数据
dataset_train = datasets.ImageFolder('E:\\data_set\\cat_dog\\train', transform)
print(dataset_train.imgs)
# 对应文件夹的label
print(dataset_train.class_to_idx)
dataset_test = datasets.ImageFolder('E:\\data_set\\cat_dog\\val', transform_test)
# 对应文件夹的label
print(dataset_test.class_to_idx)

# 导入数据
train_loader = torch.utils.data.DataLoader(dataset_train, batch_size=BATCH_SIZE, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset_test, batch_size=BATCH_SIZE, shuffle=False)

# 设置模型
# 实例化模型并且移动到GPU
criterion = nn.CrossEntropyLoss()
model = torchvision.models.resnet18(pretrained=False)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 2)
model.to(DEVICE)
# 选择简单暴力的Adam优化器,学习率调低
optimizer = optim.Adam(model.parameters(), lr=modellr)

def adjust_learning_rate(optimizer, epoch):
    """Sets the learning rate to the initial LR decayed by 10 every 30 epochs"""
    modellrnew = modellr * (0.1 ** (epoch // 5))
    print("lr:", modellrnew)
    for param_group in optimizer.param_groups:
        param_group['lr'] = modellrnew


# 定义训练过程

def train(model, device, train_loader, optimizer, epoch):
    model.train()
    sum_loss = 0
    total_num = len(train_loader.dataset)
    print(total_num, len(train_loader))
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = Variable(data).to(device), Variable(target).to(device)
        output = model(data)
        loss = criterion(output, target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        print_loss = loss.data.item()
        sum_loss += print_loss
        if (batch_idx + 1) % 50 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, (batch_idx + 1) * len(data), len(train_loader.dataset),
                       100. * (batch_idx + 1) / len(train_loader), loss.item()))
    ave_loss = sum_loss / len(train_loader)
    print('epoch:{},loss:{}'.format(epoch, ave_loss))


def val(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    total_num = len(test_loader.dataset)
    print(total_num, len(test_loader))
    with torch.no_grad():
        for data, target in test_loader:
            data, target = Variable(data).to(device), Variable(target).to(device)
            output = model(data)
            loss = criterion(output, target)
            _, pred = torch.max(output.data, 1)
            correct += torch.sum(pred == target)
            print_loss = loss.data.item()
            test_loss += print_loss
        correct = correct.data.item()
        acc = correct / total_num
        avgloss = test_loss / len(test_loader)
        print('\nVal set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
            avgloss, correct, len(test_loader.dataset), 100 * acc))


# 训练
for epoch in range(1, EPOCHS + 1):
    adjust_learning_rate(optimizer, epoch)
    train(model, DEVICE, train_loader, optimizer, epoch)
    val(model, DEVICE, test_loader)
torch.save(model, 'model.pth')

predict.py

import csv

import torch.utils.data.distributed
import torchvision.transforms as transforms

from torch.autograd import Variable
import os
from PIL import Image

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

DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = torch.load("model.pth")

model.eval()
model.to(DEVICE)
path = 'E:\\data_set\\cat_dog\\test\\'
testList = os.listdir(path)

csvfile = open('result.csv', 'w', newline='')
writer = csv.writer(csvfile)

for file in testList:
    img = Image.open(path + file)
    img = transform_test(img)
    img.unsqueeze_(0)
    img = Variable(img).to(DEVICE)
    out = model(img)
    # Predict
    _, pred = torch.max(out.data, 1)
    # print(file, pred.toList()[0])
    print('Image Name:{},predict:{}'.format(file, classes[pred.data.item()]))
    writer.writerow([file, pred.data.item()])

注意的小点:
csvfile = open('result.csv', 'w', newline='') 加上newline='',这样结果输入至csv文件时就不会两行之间有空行
在这里插入图片描述
在这里插入图片描述
的确效果很一般,可能是因为
1、训练时使用的数据比较少 ,猫2000张,狗2000张(小破电脑支撑不了,跑不动hhh)
2、没使用迁移学习的方法训练

对于以上的结果,我又重新使用迁移学习的方法来实现了一次
在这里插入图片描述
发现效果是非常符合预期的,这也证明了使用迁移学习会使模型的训练效果更好
训练部分的代码参考网课中的代码:

import os
import sys
import json

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
from torchvision import transforms, datasets
from tqdm import tqdm

def main():
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print("using {} device.".format(device))

    data_transform = {
    
    
        "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.ToTensor(),
                                     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])])}

    data_root = os.path.abspath(os.path.join(os.getcwd(), "E:\\"))  # get data root path
    image_path = os.path.join(data_root, "data_set", "cat_dog")  # flower data set path
    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)

    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 = 4
    nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
    print('Using {} dataloader workers every process'.format(nw))

    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 = torchvision.models.resnet34()
    # load pretrain weights
    # download url: https://download.pytorch.org/models/resnet34-333f7ec4.pth
    model_weight_path = "./resnet34-pre.pth"
    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'));
    for param in net.parameters():
        param.requires_grad = False

    # change fc layer structure
    in_channel = net.fc.in_features
    net.fc = nn.Linear(in_channel, 2)
    net.to(device)

    # define loss function
    loss_function = nn.CrossEntropyLoss()

    # construct an optimizer
    params = [p for p in net.parameters() if p.requires_grad]
    optimizer = optim.Adam(params, lr=0.0001)

    epochs = 3
    best_acc = 0.0
    save_path = './resNet34.pth'
    train_steps = len(train_loader)
    for epoch in range(epochs):
        # train
        net.train()
        running_loss = 0.0
        train_bar = tqdm(train_loader, file=sys.stdout)
        for step, data in enumerate(train_bar):
            images, labels = data
            optimizer.zero_grad()
            logits = net(images.to(device))
            loss = loss_function(logits, labels.to(device))
            loss.backward()
            optimizer.step()

            # print statistics
            running_loss += loss.item()

            train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                     epochs,
                                                                     loss)

        # validate
        net.eval()
        acc = 0.0  # accumulate accurate number / epoch
        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))
                # loss = loss_function(outputs, test_labels)
                predict_y = torch.max(outputs, dim=1)[1]
                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(net.state_dict(), save_path)

    print('Finished Training')


if __name__ == '__main__':
    main()

然后进行predict,将结果导入到csv文件并上传至网站~
在这里插入图片描述

4. 思考题

1、Residual learning
Residual learning解决了梯度消失的问题,可以用来训练比较深的网络。
2、Batch Normailization 的原理 参考链接

其实就是强行把特征做的均值为0、标准差为1,可以通过将数据标准化加速权重参数的收敛,有时可以舍弃作用不大的dropout。

参考链接
BN的关键公式:在这里插入图片描述
VAR(方差):在这里插入图片描述
E(期望):在这里插入图片描述
在前向传播中记录下来γ、β的值

没有BN层的情况下,网络没办法直接控制每层输入的分布,其分布前面层的权重共同决定,或者说分布的均值和方差“隐藏”在前面层的每个权重中,网络若想调整其分布,需要通过复杂的反向传播过程调整前面的每个权重实现,BN层的存在相当于将分布的均值和方差从权重中剥离了出来,只需调整γ和β两个参数就可以直接调整分布,让分布和权重的配合变得更加容易。

3、为什么分组卷积可以提升准确率?即然分组卷积可以提升准确率,同时还能降低计算量,分组数量尽量多不行吗?

(1)分组卷积能减少运算量和参数量,所以不容易过拟合,准确率相对较高;
(2)分组太多了也不好,我在网上查阅了一些资料,发现:分组卷积有一个矛盾在于 ”特征通信“,分组后不同组之间的特征图需要通信,否则就好像分了几个互不相干的路,大家各走各的,会降低网络的特征提取能力,所以分组数量要合理。

5. 最后

感觉最近编程的难度一下子上来了,根据课程我能明白网络的实现原理、看懂搭建网络的代码,但是关于模型的训练、测试等部分还是不太理解(ó﹏ò。) ,自己独立写不出来训练、测试以及预测的代码,真的很让人头大啊啊啊!!!感觉自己没有把之前所学的东西很好的串联在一起,所以需要再花一些时间补一补基础,复习一下前边的知识。

猜你喜欢

转载自blog.csdn.net/WKX_5/article/details/126005110