深入浅出理解ResNet网络模型+PyTorch实现

温故而知新,可以为师矣!

一、参考资料

论文:Identity Mappings in Deep Residual Networks
论文:Deep Residual Learning for Image Recognition
ResNet详解+PyTorch实现
PyTorch官方实现ResNet
【pytorch】ResNet18、ResNet20、ResNet34、ResNet50网络结构与实现
残差网络ResNet笔记
ResNet详解与实现
Highway Networks
重读《Deep Residual Learning for Image Recognition》之进一步理解残差网络的神秘(附Pytorch代码)

二、相关介绍

1. 深度网络

随着网络层数的加深,网络的表征能力会更强,这是因为卷积核的作用是提取图像的特征,然而一个卷积核是不够的,一个卷积核只能反应图像的某一个特征,所以需要多个卷积核,这些不同的卷积核可以提取图像不同的特征,从而让模型学习图像特征的能力更强。因此,有足够的卷积核和足够的参数,才可以更好表达原始图像的特征。

因此,深度网络有两个优势特点:

  1. 网络越深,特征的等级越高;
  2. 网络越深,表征能力越强。

2. 网络模型命名

现在很多网络结构都是一个"命名+数字",数字代表网络深度,网络深度指的是网络的权重层,包括卷积层和全连接层,不包括池化层和BN层

3. BN批量规范化层

批量规范化层(Batch Normalization,简称BN),将一批数据的feature map满足均值为0,方差为1的分布规律。

在图像预处理过程中,通常会对图像进行BN操作,这样能够加速网络的收敛。如下图所示,对于Conv1来说,输入是满足某一分布的特征矩阵;但对于Conv2而言,输入的feature map就不一定满足某一分布规律(注意这里所说满足某一分布规律并不是指某一个feature map的数据要满足分布规律,理论上是指整个训练样本集所对应feature map的数据要满足分布规律)。而BN的目的就是使feature map满足均值为0,方差为1的分布规律。
在这里插入图片描述

三、ResNet相关介绍

ResNet详解

深度残差网络(Deep residual network, ResNet)是在 2015年由微软实验室提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名,获得COCO数据集中目标检测第一名,图像分割第一名。ResNet的提出是CNN图像史上的一件里程碑事件,由于其在公开数据上展现的优势,作者何凯明也因此摘得CVPR2016最佳论文奖。

1. 引言

网络的深度为什么重要?

因为CNN能够提取 low/mid/high-level 的特征,网络的层数越多,意味着能够提取到不同level的特征越丰富。并且,越深的网络提取的特征越抽象,越具有语义信息。

为什么不能简单地增加网络层数?

在ResNet网络提出之前,传统的卷积神经网络都是通过将一系列卷积层与池化层进行堆叠得到的。通常,我们认为网络越深,特征信息越丰富,模型效果应该越好。但实验证明,传统的卷积网络或者全连接网络在信息传递的时候或多或少会存在信息丢失信息损耗等问题,简单地增加网络深度存在网络退化问题,同时还有导致梯度消失或者梯度爆炸,导致很深的网络无法训练。

1.1 梯度消失/爆炸问题

随着网络层数加深,反向传播过程中出现梯度消失或者梯度爆炸的问题。反向传播是用来对网络的权重进行调整,包括卷积核的值,隐藏层的权重和偏置,这些都需要反向传播来调整;反向传播主要是计算变化因子来调整权重,而变化因子的计算首先需要计算目标函数(预测值和真实值的差的平方和)对每层网络权重的偏导数。因此,在求反向传播求梯度时利用了链式法则,梯度值会进行一系列的连乘,也就会出现剧烈的缩减或者变大,这种现象就阻碍了模型收敛。

梯度消失:0.99^1000=0.00004317

梯度爆炸:1.01^1000=20959.155

若每一层的误差梯度小于1,在反向传播过程中,每向前传播一次,都要乘以一个小于1的误差梯度,网络越深,所乘的小于1的系数越多,梯度越趋近于0,则会发生“梯度消失”;反之,若每一层的误差梯度大于1,在反向传播过程中,每向前传播一次,都要乘以一个大于1的误差梯度,网络越深,梯度越来越大,则会发生“梯度爆炸”

解决办法:为了解决梯度消失或梯度爆炸问题,ResNet论文提出通过数据预处理(数据标准化处理),使用标准权重初始化,在网络中使用 BN层来解决。

1.2 网络退化问题(Degradation problem)

随着网络越来越深,训练变得原来越难,网络的优化变得越来越难。理论上,越深的网络,效果应该更好;但实际上,由于训练难度,过深的网络会产生退化问题,效果反而不如相对较浅的网络。随着网络层数增多,网络准确度出现饱和,甚至出现下降,这被称为退化问题
在这里插入图片描述

解决办法:为了解决深层网络中的退化问题,使神经网络某些层跳过下一层神经元的连接,隔层相连,弱化每层之间的强联系,这种神经网络被称为残差网络 (ResNet)。ResNet论文提出了 residual结构(残差结构)来减轻退化问题,下图是使用residual结构的卷积网络,可以看到随着网络的不断加深,效果并没有变差,而是变的更好。(虚线是train error,实线是test error)。
在这里插入图片描述

2. 残差映射

在这里插入图片描述

如上图所示,左图称为恒等映射,右图称为残差映射。左图中,假设原始输入为x,理想映射为f(x),左图虚线框中的部分需要直接拟合该映射 f(x),而右图虚线框中的部分需要拟合残差映射 f(x)-x,残差映射在现实中往往更容易优化。右图中的 f(x)理想映射,当右图虚线框内上方的加权运算(如放射)的权重和偏置参数设为0,f(x)即为恒等映射。实际中,当理想映射f(x)极限接近恒等映射时,残差映射也易于捕捉恒等映射的细微波动

3. ResNet与VGG

ResNet网络是参考了VGG19网络,在其基础上进行了修改,并通过短路机制加入了残差单元,如下图所示。ResNet相比普通网络每两层间增加了短路机制,这就形成了残差学习
在这里插入图片描述

ResNet相对于VGG19网络,主要变化体现在:ResNet直接使用stride=2的卷积做下采样(特征图的大小减半,通道数翻倍),并且用 global average pool 层替换全连接层。这体现了ResNet的一个重要设计原则:当feature map大小降低一半时,feature map的通道数增加一倍,这保持了网络层的复杂度

四、Residual残差结构

1. plain与residual网络

由多个 残差块组成的神经网络就是残差网络 。其结构如下图所示:
在这里插入图片描述

实验表明,这种模型结构对于训练非常深的神经网络,效果很好。另外,为了便于区分,我们把 非残差网络 称为 Plain Network。

2. Residual残差结构

2.1 short cut结构简介

ResNet相比于VGGNet,最大的区别在于有很多的旁路将输入直接连接到后面的层,这种结构也被称为 short cut 或者 skip connections(也可理解为“捷径”)。

  • 采用short cut结构的残差块,其输入可通过跨层数据路线更快地向前传播

  • short cut路径上的分支,称为“捷径分支”,区别于“主分支”。

如下图所示,residual残差结构采用 short cut 的连接方式,让特征矩阵隔层相加,所谓相加是特征矩阵相同位置上的数值进行相加。实际应用中,残差结构的 short cut 不一定是隔一层连接,也可以中间隔多层,ResNet所提出的残差网络中就是隔多层。
在这里插入图片描述
一般称x为 identity Function,它是一个跳跃连接;称F(x)为ResNet Function,注意F(x)和x形状要相同。

2.2 Residual残差结构简介

如下图所示,ResNet中两种不同的residual残差结构,左侧残差结构称为 BasicBlock,右侧残差结构称为 Bottleneck。ResNet18/34的残差结构是 BasicBlock,用的是2个3x3的卷积。ResNet50/101/152的残差结构是 Bottleneck,用的是 1x1+3x3+1x1 的卷积。
在这里插入图片描述
ResNet沿用了VGG完整的3×3卷积层设计。 首先,BasicBlock残差结构有2个相同输出通道数的3×3卷积层。 每个卷积层后接一个BN批量规范化层和ReLU激活函数。 然后,通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。 这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加。 如果想改变通道数,就需要引入一个额外的1×1卷积层将输入变换成需要的形状后再做相加运算。
在这里插入图片描述

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

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

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

ResNet 18/34

在这里插入图片描述

ResNet 50/101/152

在这里插入图片描述

3. BasicBlock残差结构

对于18-layer、34-layer网络层数较少的ResNet,由BasicBlock构成,其进行两层间的残差学习,两层卷积核分别是3x3,3x3。basic_block=identity_block,此结构保证了输入和输出相等,实现网络的串联
在这里插入图片描述

import torch.nn as nn
import math
import torch.utils.model_zoo as model_zoo# 这个文件内包括6中不同的网络架构
__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101','resnet152']# 每一种架构下都有训练好的可以用的参数文件
model_urls = {'resnet18': 'https://s3.amazonaws.com/pytorch/models/resnet18-5c106cde.pth','resnet34': 'https://s3.amazonaws.com/pytorch/models/resnet34-333f7ec4.pth','resnet50': 'https://s3.amazonaws.com/pytorch/models/resnet50-19c8e357.pth','resnet101': 'https://s3.amazonaws.com/pytorch/models/resnet101-5d3b4d8f.pth','resnet152': 'https://s3.amazonaws.com/pytorch/models/resnet152-b121ed2d.pth',
}# 常见的3x3卷积
def conv3x3(in_planes, out_planes, stride=1):"3x3 convolution with padding"return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,padding=1, bias=False)class BasicBlock(nn.Module):# 残差结构中,主分支的卷积核个数是否发生变化,不变则为1expansion = 1def __init__(self, inplanes, planes, stride=1, downsample=None):  # downsample对应虚线残差结构# inplanes代表输入通道数,planes代表输出通道数。super(BasicBlock, self).__init__()# Conv1self.conv1 = conv3x3(inplanes, planes, stride)# stride=1为实线残差结构,不需要改变大小,stride=2为虚线残差结构# stride=1,output=(input-3+2*1)/ 1 + 1 = input   输入和输出的shape不变# stride=2,output=(input-3+2*1)/ 2 + 1 = input = input/2 + 0.5 = input/2(向下取整)self.bn1 = nn.BatchNorm2d(planes)  # 使用BN时不使用偏置self.relu = nn.ReLU(inplace=True)# Conv2self.conv2 = conv3x3(planes, planes)self.bn2 = nn.BatchNorm2d(planes)# 下采样self.downsample = downsampleself.stride = stridedef forward(self, x):residual = xout = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)if self.downsample is not None:  # 虚线残差结构,需要下采样residual = self.downsample(x)  # 捷径分支 short cut# F(x)+xout += residualout = self.relu(out)return out

BasicBlock类中的 init() 函数定义网络架构,forward() 函数定义前向传播,实现的功能是残差块。
在这里插入图片描述

4. Bottleneck残差结构

对于50-layer、101-layer和152-layer网络层数较多的ResNet,由Bottleneck构成,其进行三层间的残差学习,三层卷积核分别是1x1,3x3和1x1。对于深层的 Bottleneck,1×1的卷积核起到降维和升维(特征矩阵深度)的作用,同时可以大大减少网络参数。具体来说,第一层的1× 1的卷积核的作用是对特征矩阵进行降维操作,将特征矩阵的深度由256降为64;第三层的1× 1的卷积核是对特征矩阵进行升维操作,将特征矩阵的深度由64升成256。降低特征矩阵的深度主要是为了减少参数的个数。先降维后升维,是为了主分支上输出的特征矩阵和捷径分支上输出的特征矩阵形状相同,以便进行加法操作
在这里插入图片描述

值得注意的是,隐含层的feature map的通道数量是比较小的,并且是输出feature map通道数量的1/4。如下图所示,三层卷积核中的前两个卷积核对应的隐含层通道数为64,最后一个卷积核对应的输出层通道数为256,隐含层的通道数是输出层通道数的1/4。
在这里插入图片描述

# ResNet50/101/152的残差结构,用的是1x1+3x3+1x1的卷积核
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"""# 残差结构中第三层卷积核个数是第一/二层卷积核个数的4倍expansion = 4      # 输出通道数的倍乘def __init__(self, inplanes, planes, stride=1, downsample=None):super(Bottleneck, self).__init__()# conv1   1x1self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)self.bn1 = nn.BatchNorm2d(planes)# conv2   3x3self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,padding=1, bias=False)# stride=stride根据传入的进行调整,因为实线中的第二层是1,虚线中是2self.bn2 = nn.BatchNorm2d(planes)# conv3   1x1  self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)self.bn3 = nn.BatchNorm2d(planes * 4)self.relu = nn.ReLU(inplace=True)self.downsample = downsampleself.stride = stridedef forward(self, x):residual = xout = 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)if self.downsample is not None:  # 捷径分支 short cutresidual = self.downsample(x)out += residualout = self.relu(out)return out

Bottleneck 类是另一种blcok类型,init() 函数定义网络架构,forward() 函数定义前向传播。该block中有三个卷积,分别是1x1,3x3,1x1,分别完成的功能是压缩维度,卷积,恢复维度。因此,Bottleneck 类实现的功能是对通道数进行压缩,再放大。注意:这里的plane不再是输出的通道数,输出通道数应该就是 p l a n e ∗ e x p a n s i o n plane*expansion planeexpansion,即 4 ∗ p l a n e 4*plane 4plane
在这里插入图片描述

5. 残差结构计算量

假设两个残差结构的输入特征和输出特征矩阵的通道数都是256维,如下图:
在这里插入图片描述

如果不考虑bias偏置项,CNN参数量计算公式为: D K ∗ D K ∗ M ∗ N D_K*D_K*M*N DKDKMN

如果采用BasicBlock残差结构,参数量为:3×3x256×256+3×3x256×256=1179648。
如果采用Bottleneck残差结构,参数量为:1×1×256×64+3×3×64×64+1×1×64×256=69632。

总结:很显然,使用Bottleneck残差结构参数量更少,更合适搭建深层网络。

五、ResNet网络

1. ResNet网络结构

  • resnet18: ResNet(BasicBlock, [2, 2, 2, 2])
  • resnet34: ResNet(BasicBlock, [3, 4, 6, 3])
  • resnet50:ResNet(Bottleneck, [3, 4, 6, 3])
  • resnet101:ResNet(Bottleneck, [3, 4, 23, 3])
  • resnet152:ResNet(Bottleneck, [3, 8, 36, 3])

如下图所示,ResNet50分为 conv1,conv2_x,conv3_x,conv4_x,conv5_x共5大层,网络层数为:1+1+3x3+4x3+6x3+3x3=50,前面一层卷积层+一层池化层+4组卷积,不考虑最后面的全连接、池化层。
在这里插入图片描述
ResNet一般有4个stage,每一个stack里面都是block的堆叠,例如 [3, 4, 6, 3] 就是每个stage堆叠block的个数,因此生成了不同深度的ResNet。

在这里插入图片描述
图片来源:ResNet50网络结构图及结构详解,
图片下载链接:链接:百度网盘 提取码:1ojd

2. ResNet网络创新点

  • 搭建超深的网络结构(可突破1000层)。

  • 提出 Residual 结构(残差结构 )来减轻退化问题。

  • 使用 BN层来解决梯度消失或梯度爆炸问题。使用 BN 加速训练(丢弃dropout)。

    在图像预处理过程中通常会对图像进行标准化处理,这样能够加速网络的收敛。BN的目的就是使feature map 满足均值为0,方差为1的分布规律。

3. 核心代码

# 整个网络的框架部分
class ResNet(nn.Module):# block = BasicBlock or Bottleneck# layers为残差结构中conv2_x~conv5_x中残差块个数,是一个列表,如34层中的是[3,4,6,3]def __init__(self, block, layers, num_classes=1000):  self.inplanes = 64 super(ResNet, self).__init__()# 1.conv1self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,bias=False)self.bn1 = nn.BatchNorm2d(64)self.relu = nn.ReLU(inplace=True)# 2.conv2_xself.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)self.layer1 = self._make_layer(block, 64, layers[0])# 3.conv3_xself.layer2 = self._make_layer(block, 128, layers[1], stride=2)# 4.conv4_xself.layer3 = self._make_layer(block, 256, layers[2], stride=2)# 5.conv5_xself.layer4 = self._make_layer(block, 512, layers[3], stride=2)self.avgpool = nn.AvgPool2d(7)self.fc = nn.Linear(512 * block.expansion, num_classes)# 初始化权重for m in self.modules():if isinstance(m, nn.Conv2d):n = m.kernel_size[0] * m.kernel_size[1] * m.out_channelsm.weight.data.normal_(0, math.sqrt(2. / n))elif isinstance(m, nn.BatchNorm2d):m.weight.data.fill_(1)m.bias.data.zero_()def _make_layer(self, block, planes, blocks, stride=1):downsample = Noneif stride != 1 or self.inplanes != planes * block.expansion:downsample = nn.Sequential(nn.Conv2d(self.inplanes, planes * block.expansion,kernel_size=1, stride=stride, bias=False),nn.BatchNorm2d(planes * block.expansion),)layers = []layers.append(block(self.inplanes, planes, stride, downsample))# 每个blocks的第一个residual结构保存在layers列表中。self.inplanes = planes * block.expansionfor i in range(1, blocks):# 通过循环将剩下的一系列实线残差结构添加到layerslayers.append(block(self.inplanes, planes))   # 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)x = self.avgpool(x)x = x.view(x.size(0), -1)   # 将输出结果展成一行x = self.fc(x)return x

ResNet一共有5个阶段,第一阶段是一个7x7的卷积,stride=2,然后再经过池化层,得到的特征图大小变为原图的1/4。_make_layer() 函数用来产生4个layer,可以根据输入的layers列表来创建网络。

# resnet18
def resnet18(pretrained=False):"""Constructs a ResNet-18 model.# https://download.pytorch.org/models/resnet18-f37072fd.pthArgs:pretrained (bool): If True, returns a model pre-trained on ImageNet"""model = ResNet(BasicBlock, [2, 2, 2, 2])if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))return model# resnet34
def resnet34(pretrained=False):"""Constructs a ResNet-34 model.# https://download.pytorch.org/models/resnet34-333f7ec4.pthArgs:pretrained (bool): If True, returns a model pre-trained on ImageNet"""model = ResNet(BasicBlock, [3, 4, 6, 3])if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))return model# resnet50
def resnet50(pretrained=False):"""Constructs a ResNet-50 model.# https://download.pytorch.org/models/resnet50-19c8e357.pthArgs:pretrained (bool): If True, returns a model pre-trained on ImageNet"""model = ResNet(Bottleneck, [3, 4, 6, 3])if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))return model# resnet101
def resnet101(pretrained=False):"""Constructs a ResNet-101 model.# https://download.pytorch.org/models/resnet101-5d3b4d8f.pthArgs:pretrained (bool): If True, returns a model pre-trained on ImageNet"""model = ResNet(Bottleneck, [3, 4, 23, 3])if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))return model# resnet152
def resnet152(pretrained=False):"""Constructs a ResNet-152 model.# https://download.pytorch.org/models/resnet152-394f9c45.pthArgs:pretrained (bool): If True, returns a model pre-trained on ImageNet"""model = ResNet(Bottleneck, [3, 8, 36, 3])if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))return model

六、最基本的ResNet18

ResNet18网络的具体构成
PyTorch实现ResNet18
ResNet18结构、各层输出维度
ResNet 18 的结构解读「建议收藏」
Resnet 18网络模型[通俗易懂]
Resnet 18网络模型
Resnet-18网络图示理解

ResNet18网络结构

18-layer的ResNet命名为ResNet18,其网络深度是18层,具体是指带有权重的18层,包括:卷积层和全连接层,不包括池化层和BN层。如下图所示,卷积层有17个,FC层1个,所以是18层。
在这里插入图片描述

虚线的 short cut 通过1×1的卷积核进行了维度处理(特征矩阵在长宽方向下采样,深度方向调整成下一层残差结构所需要的channel)。

  • channel通道翻倍。通过1x1卷积调整通道数,实线表示残差块中的通道数没有变化,虚线表示通道数变化,例如64->128。
  • 特征矩阵shape减半。将步长调整成2,实现下采样。
    在这里插入图片描述

提示:

BN 表示批量归一化RELU 表示激活函数lambda x:x 这个函数的意思是输出等于输入identity 表示残差1个resnet block 包含2个basic block
1个resnet block 需要添加2个残差在resnet block之间残差形式是1*1conv,在resnet block内部残差形式是lambda x:x
resnet block之间的残差用实线箭头表示,resnet block内部的残差用虚线箭头表示3*3conv s=2,p=1 特征图尺寸会缩小
3*3conv s=1,p=1 特征图尺寸不变

(1)conv1卷积层

首先,经过一个卷积层。该卷积层的卷积核的大小为7x7,步长为2,padding为3,输出通道为64。根据公式:
n o u t = ⌊ n i n + 2 p − k s ⌋ + 1 n_{out}=\left\lfloor\frac{n_{in}+2p-k}{s}\right\rfloor+1 nout=snin+2pk+1
我们可以算出最后输出数据的大小为64x112x112。

(2)maxpooling池化层

在这里插入图片描述

最大池化层,这一层的卷积核的大小是3x3,步长为2,padding为1。最后输出数据的大小为64x56x56。也就是说,这一层不改变数据的通道数量,而特征矩阵shape减半。

(3)conv2_x卷积层(Resnet block1)

该卷积层的卷积核大小为3x3,步长为1,padding为1。最后通过两次卷积计算,输出数据大小为64x56x56,这一层不改变数据的大小和通道数。
在这里插入图片描述

(4)conv3_x卷积层(Resnet block2)

通过一个1x1的卷积层升维,并经过一个下采样。最终输出为128x28x28。输出的channel通道翻倍,输出的特征矩阵shape减半。
在这里插入图片描述

(5)conv4_x卷积层(Resnet block3)

通过一个1x1的卷积层,并经过一个下采样。最终输出为256x14x14。输出的channel通道翻倍,输出的特征矩阵shape减半。
在这里插入图片描述

(6)conv5_x卷积层(Resnet block4)

和上述同理,最终输出为512x7x7。输出的channel通道翻倍,输出的特征矩阵shape减半。
在这里插入图片描述

(7)avgpooling层

最终输出为512x1x1。

(8)FC层

七、源码分析

ResNet网络结构详解,网络搭建,迁移学习
pytorch图像分类篇:6. ResNet网络结构详解与迁移学习简介

1. model.py

import torch.nn as nn
import torch# ResNet18/34的残差结构,用的是2个3x3的卷积核
class BasicBlock(nn.Module):expansion = 1  # 残差结构中,主分支的卷积核个数是否发生变化,不变则为1def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):  # downsample对应虚线残差结构super(BasicBlock, self).__init__()self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,kernel_size=(3, 3), stride=(stride, stride),padding=1, bias=False)# stride=1为实线残差结构,不需要改变大小,stride=2为虚线残差结构# stride=1,output=(input-3+2*1)/ 1 + 1 = input   输入和输出的shape不变# stride=2,output=(input-3+2*1)/ 2 + 1 = input = input/2 + 0.5 = input/2(向下取整)self.bn1 = nn.BatchNorm2d(out_channel)   # 使用BN时不使用偏置self.relu = nn.ReLU()self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,kernel_size=(3, 3), stride=(1, 1), padding=1, bias=False)self.bn2 = nn.BatchNorm2d(out_channel)self.downsample = downsampledef forward(self, x):identity = xif self.downsample is not None:  # 虚线残差结构,需要下采样identity = self.downsample(x)  # 捷径分支 short cutout = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)out += identityout = self.relu(out)return out# ResNet50/101/152的残差结构,用的是1x1+3x3+1x1的卷积核
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  # 残差结构中第三层卷积核个数是第一/二层卷积核个数的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.)) * groupsself.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,kernel_size=(1, 1), stride=(1, 1), bias=False)  # squeeze channelsself.bn1 = nn.BatchNorm2d(width)# -----------------------------------------self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,kernel_size=(3, 3), stride=(stride, stride), bias=False, padding=1)# stride=stride根据传入的进行调整,因为实线中的第二层是1,虚线中是2self.bn2 = nn.BatchNorm2d(width)# -----------------------------------------self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel * self.expansion,  # 卷积核个数变为4倍kernel_size=(1, 1), stride=(1, 1), bias=False)  # unsqueeze channelsself.bn3 = nn.BatchNorm2d(out_channel * self.expansion)self.relu = nn.ReLU(inplace=True)self.downsample = downsampledef forward(self, x):identity = xif self.downsample is not None:identity = self.downsample(x)  # 捷径分支 short cutout = 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 += identityout = self.relu(out)return out# 整个网络的框架部分
class ResNet(nn.Module):# block = BasicBlock or Bottleneck# block_num为残差结构中conv2_x~conv5_x中残差块个数,是一个列表,如34层中的是[3,4,6,3]def __init__(self,block,blocks_num,num_classes=1000,include_top=True,  # 方便在resnet网络的基础上搭建其他网络,这里用不到groups=1,width_per_group=64):super(ResNet, self).__init__()self.include_top = include_topself.in_channel = 64self.groups = groupsself.width_per_group = width_per_groupself.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=(7, 7), stride=(2, 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)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))  # output size = (1, 1),自适应平均池化下采样self.fc = nn.Linear(512 * block.expansion, num_classes)for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')# channel为残差结构中第一层卷积核个数,block_num表示该层一共包含多少个残差结构,如34层中的是3,4,6,3def _make_layer(self, block, channel, block_num, stride=1):downsample = None# ResNet50/101/152的残差结构,block.expansion=4if stride != 1 or self.in_channel != channel * block.expansion:  # layer2,3,4都会经过这个结构downsample = nn.Sequential(  # 生成下采样函数,这里只需要调整conv2的特征矩阵的深度nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=(1, 1), stride=(stride, stride), bias=False),nn.BatchNorm2d(channel * block.expansion))layers = []# 首先将第一层残差结构添加进去,block = BasicBlock or Bottlenecklayers.append(block(self.in_channel,  # 输入特征矩阵的深度64channel,  # 残差结构对应主分支上的第一个卷积层的卷积核个数downsample=downsample,  # 50/101/152对应的是高宽不变,深度4倍,对应的虚线残差结构stride=stride,groups=self.groups,width_per_group=self.width_per_group))self.in_channel = channel * block.expansionfor _ in range(1, block_num):# 通过循环将剩下的一系列实线残差结构添加到layerslayers.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 xdef resnet18(num_classes=1000, include_top=True):# https://download.pytorch.org/models/resnet18-f37072fd.pthreturn 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.pthreturn 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.pthreturn 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.pthreturn ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)def resnet152(num_classes=1000, include_top=True):# https://download.pytorch.org/models/resnet152-394f9c45.pthreturn ResNet(Bottleneck, [3, 8, 36, 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.pthgroups = 32width_per_group = 4return 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.pthgroups = 32width_per_group = 8return ResNet(Bottleneck, [3, 4, 23, 3],num_classes=num_classes,include_top=include_top,groups=groups,width_per_group=width_per_group)

2. train.py

import os
import sys
import jsonimport torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets
from tqdm import tqdmfrom model import resnet34def 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),      #原图的长宽比固定不动,把最小边长缩放到256transforms.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(), "../"))  # get data root pathimage_path = os.path.join(data_root, "data_set", "flower_data")  # flower data set pathassert 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}flower_list = train_dataset.class_to_idxcla_dict = dict((val, key) for key, val in flower_list.items())# write dict into json filejson_str = json.dumps(cla_dict, indent=4)with open('class_indices.json', 'w') as json_file:json_file.write(json_str)batch_size = 4nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workersprint('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 = resnet34()# load pretrain weights# download url: https://download.pytorch.org/models/resnet34-333f7ec4.pthmodel_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 structurein_channel = net.fc.in_featuresnet.fc = nn.Linear(in_channel, 5)net.to(device)# define loss functionloss_function = nn.CrossEntropyLoss()# construct an optimizerparams = [p for p in net.parameters() if p.requires_grad]optimizer = optim.Adam(params, lr=0.0001)epochs = 3best_acc = 0.0save_path = './resNet34.pth'train_steps = len(train_loader)for epoch in range(epochs):# trainnet.train()running_loss = 0.0train_bar = tqdm(train_loader, file=sys.stdout)for step, data in enumerate(train_bar):images, labels = dataoptimizer.zero_grad()logits = net(images.to(device))loss = loss_function(logits, labels.to(device))loss.backward()optimizer.step()# print statisticsrunning_loss += loss.item()train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,epochs,loss)# validatenet.eval()acc = 0.0  # accumulate accurate number / epochwith torch.no_grad():val_bar = tqdm(validate_loader, file=sys.stdout)for val_data in val_bar:val_images, val_labels = val_dataoutputs = 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_numprint('[epoch %d] train_loss: %.3f  val_accuracy: %.3f' %(epoch + 1, running_loss / train_steps, val_accurate))if val_accurate > best_acc:best_acc = val_accuratetorch.save(net.state_dict(), save_path)print('Finished Training')if __name__ == '__main__':main()

3. predict.py

import os
import jsonimport torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as pltfrom model import resnet34def main():device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")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])])# load imageimg_path = "../tulip.jpg"assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)img = Image.open(img_path)plt.imshow(img)# [N, C, H, W]img = data_transform(img)# expand batch dimensionimg = torch.unsqueeze(img, dim=0)# read class_indictjson_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)# create modelmodel = resnet34(num_classes=5).to(device)# load model weightsweights_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))# predictionmodel.eval()with torch.no_grad():# predict classoutput = torch.squeeze(model(img.to(device))).cpu()predict = torch.softmax(output, dim=0)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()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/138792.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Go语言抓取酒店价格数据的技术实现

目录 一、引言 二、准备工作 三、抓取数据 四、数据处理与存储 五、数据分析与可视化 六、结论与展望 一、引言 随着互联网的快速发展,酒店预订已经成为人们出行的重要环节。在选择酒店时,价格是消费者考虑的重要因素之一。因此,抓取酒…

php性能追踪与分析

PHP扩展下载:https://pecl.php.net/package/xhprof php.ini配置 [xhprof] extensionxhprof xhprof.output_dir/temp/xhprof auto_prepend_file /temp/inject_xhprof.php if(php_sapi_name() cli) {return; }$xhprof_config[enabled]1;if(!empty($xhprof_config…

Maven-构建工具

一、背景 开发者编写完成源码,还需要进行编译、测试、打包、部署等一系列操作。在一些小型项目中,还可能通过手动方式进行以上操作。但是在大型项目中,难以确定以上操作的顺序,而且会耗费更高的时间成本。 1.构建工具 构建工具…

【Proteus仿真】【Arduino单片机】LCD1602-IIC液晶显示

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器,使用PCF8574、LCD1602液晶等。 主要功能: 系统运行后,LCD1602液晶显示各种效果。 二、软件设计 /* 作者:嗨小…

20道高频JavaScript面试题快问快答

※其他的快问快答,看这里! 10道高频Qiankun微前端面试题快问快答 10道高频webpack面试题快问快答 20道高频CSS面试题快问快答 20道高频JavaScript面试题快问快答 30道高频Vue面试题快问快答 面试中的快问快答 快问快答的情景在面试中非常常见。 在面试过…

day3 ARM

【昨日作业】 .text .global start _start: mov r0,#0 存放sum mov r1,#1 存放相加的数值 loop: cmp r1,#100 bhi wh add r0,r0,r1 add r1,r1,#1 b loop wh: b wh .end 【内存读写指令】 通过内存读写指令可以实现向内存中写入指定数据或者读取指定内存地址的数据 c语言内存…

自动驾驶学习笔记(七)——感知融合

#Apollo开发者# 学习课程的传送门如下,当您也准备学习自动驾驶时,可以和我一同前往: 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo Beta宣讲和线下沙龙》免费报名—>传送门 文章目录 前言 感知融合 卡尔曼滤波 融合策略 实…

数据分析实战 | 线性回归——女性身高与体重数据分析

目录 一、数据集及分析对象 二、目的及分析任务 三、方法及工具 四、数据读入 五、数据理解 六、数据准备 七、模型训练 八、模型评价 九、模型调参 十、模型预测 实现回归分析类算法的Python第三方工具包比较常用的有statsmodels、statistics、scikit-learn等&#…

工业镜头接口类型

现有产品主要有以下接口 1、C:最常见的工业相机接口,受限于接口物理尺寸大小,最大靶面目前是4/3” 2、M42:M421.0,2k和4k线阵相机使用 3、M58S:M580.75,大靶面相机使用,可以转C(限于CH080相机,靶面4/3”),可以转F,可以…

数据结构预算法--链表(单链表,双向链表)

1.链表 目录 1.链表 1.1链表的概念及结构 1.2 链表的分类 2.单链表的实现(不带哨兵位) 2.1接口函数 2.2函数的实现 3.双向链表的实现(带哨兵位) 3.1接口函数 3.2函数的实现 1.1链表的概念及结构 概念:链表是一种物理存储结…

论文阅读——Detection Hub(cvpr2023)

Detection Hub: Unifying Object Detection Datasets via Query Adaptation on Language Embedding 一、要解决的问题 大规模数据集可以提高模型性能,但是当训练多类别单一模型时,大规模数据集不能用在目标检测任务上,因为两个困难&#xff1…

开发知识点-Ant-Design-Vue

Ant-Design-Vue a-input a-input Vue组件 a-spin 加载中的效果 data字段 mounted钩子函数 Ant Design Vue 组件库 list-type“picture-card” 上传的图片作为卡片展示 name show-upload-list action :beforeUpload“handleBeforeUpload” :headers“customHeaders” :disabl…

C++ RBTree 理论

目录 这个性质可以总结为 红黑树的最短最长路径 红黑树的路径范围 code 结构 搞颜色 类 插入 插入逻辑 新插入节点 思考:2. 检测新节点插入后,红黑树的性质是否造到破坏? 解决方法 变色 旋转变色 第三种情况,如果根…

Fortran 中的指针

Fortran 中的指针 指针可以看作一种数据类型 指针存储与之关联的数据的内存地址变量指针:指向变量数组指针:指向数组过程指针:指向函数或子程序指针状态 未定义未关联 integer, pointer::p1>null() !或者 nullify(p1) 已关联 指针操作 指…

docker下的nginx代理转发到tomcat

多次尝试失败原因,修改nginx配置文件以后,需要./nginx.sh -s reload 下,之前一直不转发,好像完全没有跳转的意思,后来查了多篇文档,最简单的方法如下 docker 安装 nginx 和tomcat就不多说了,可…

交叉编译 mysql-connector-c

下载 mysql-connector-c $ wget https://downloads.mysql.com/archives/get/p/19/file/mysql-connector-c-6.1.5-src.tar.gz 注意:mysql-connector 的页面有很多版本,在测试过程中发现很多默认编译有问题,其中上面的 6.1.5 的版本呢是经过测…

4面百度软件测试工程师的面试经验总结

没有绝对的天才,只有持续不断的付出。对于我们每一个平凡人来说,改变命运只能依靠努力幸运,但如果你不够幸运,那就只能拉高努力的占比。 2023年7月,我有幸成为了百度的一名测试工程师,从外包辞职了历经100…

【h5 uniapp】 滚动 滚动条,数据跟着变化

uniapp项目 需求: 向下滑动时,数据增加,上方的日历标题日期也跟着变化 向上滑动时,上方的日历标题日期跟着变化 实现思路: 初次加载目前月份的数据 以及下个月的数据 this.getdate()触底加载 下个月份的数据 onReach…

CL-MVSNet论文精读

本文是对CL-MVSNet: Unsupervised Multi-View Stereo with Dual-Level Contrastive Learning Kaiqiang Xiong, Rui Peng, Zhe Zhang, Tianxing Feng, Jianbo Jiao, Feng Gao, Ronggang Wang的阅读记录 Proceedings of the IEEE/CVF International Conference on Computer Visio…

基于JavaWeb+SpringBoot+微信小程序的酒店商品配送平台系统的设计和实现

基于JavaWebSpringBoot微信小程序的酒店商品配送平台系统的设计和实现 源码传送入口前言主要技术系统设计功能截图Lun文目录订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码传送入口 前言 本章内容概括了基于微信小程序的酒店商品配送平台的可行性分析、系统功…