目录
1. Resnet解决的问题
2. 网络结构和参数
3. pytorch搭建resnet
4. 资料
一、Resnet解决的问题
上一篇我们学习了vgg网络,vgg16 vgg19我们看到加深网络带来更好的准确度。但是随着网络层数进一步加深,会有两个问题:
1、在比较深的网络反向传播过程中,梯度可能变得很小(梯度消失)或者变得很大(梯度爆炸);
2、传统的网络即使使用了Batch noramlaztion 和非线性激活函数,但是随着网络的层数增加 准确性变得饱和,然后迅速退化,如下图所示。
Resnet创新的引入了残差学习(Residual learning), 对于每一个残差块(Residual Block),输入不仅传递到下一层,还通过跳跃连接(skip connection)加到后面几层的输出上,这样,层不是直接学习输出,而是学习输入和输出的差值(残差),这有助于缓解梯度消失/爆炸的问题。
通过跳跃连接,即使网络很深,在反向传播时梯度也可以通过这些直接连接传递到较低的层,从而保证网络的稳定性,有助于加快网络的收敛。
模块化设计,通过重复的使用相同的残差块模块化的构建网络,快速的加深网络,并且不会导致性能下降。
二、网络结构和参数
2.1 网络结构图
2.2 残差结构
2.3 Resnet34结构和Vgg网络对比
2.4 不同层在CIFAR-10数据集 训练和推理的错误率
上述图片来自ResNet论文:https://openaccess.thecvf.com/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf
三、 pytorch搭建resnet (代码详细解读,见代码中的注释)
3.1 网络模型搭建
"""
ResNet模型
"""
import torch
import torch.nn as nn
"""
通过网络结构图可以看出ResNet18/34的残差结构和RestNet50/101/152不同
ResNet18/34残差块(basic block)用的是2个3x3的卷积:
输入 --> 3x3 卷积 --> BN--> ReLU --> 3x3 卷积 (+) --> 输出
^
|
输入
RestNet50/101/152的残差块(bottleneck block)用的是1x1 + 3x3 + 1x1 的卷积:
输入 --> 1x1 卷积 --> BN --> ReLU --> 3x3 卷积 --> BN --> ReLU --> 1x1 卷积 (+) --> 输出
^
|
输入
这里的1X1卷积用于降维和升维,可以有效的减少更深网络的计算量和参数量
* 第一个1x1卷积用于降维,图像的宽高不变,减少输入特征的深度,从而减少3x3卷积的参数量和计算量
* 中间的3x3卷积进行空间特征提取
* 第二个1x1卷积用于升维,将深度较低的特征图转换回较高的唯独,以便与残差链接的输出对齐
"""
#ResNet18/34使用的残差模块
class BasicBlock(nn.Module):
expansion = 1
#downsample 对应结构图中的虚线的残差结构
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)
#相比较VGG网络,这里在conv和relu之间加了bn(Batch normalization 批量归一化)
#bn+relu 有助于减少内部协变量(internal Convariate shift)偏移:即模型训练过程中,网络参数的更新导致激活分布发生变化的问题
#内部协变量偏移会带来两个问题:
# 1. 训练难度增加,由于每层输入的分布不断变化,使得网络的训练变得更加困难.每层都需要不断的调整自己以适应输入分布的变化
# 2. 收敛速度变慢
#通过在relu前进行批量归一化(BN),通过规范每层的输入,使得每层输入的均值和方差在训练的过程中保持相对稳定,加速收敛,减少对初始化权重的敏感性,并允许使用更高的学习率
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
#这里注意到第二个卷积+bn后并没有紧跟着relu,而是在残差和恒等映射(identity)之后再进行relu
#原因是:避免负的残差值被置为0(因为relu会屏蔽负值),造成不必要的负影响;
out = self.conv2(out)
out = self.bn2(out)
out += identity
out =self.relu(out)
ret out
#ResNet50/101使用的残差模块
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
"""
# ResNet50/101残差结构中第三层卷积核个数是第1/2层卷积核个数的4倍
expansion = 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
# 第一个1x1卷积,下采样
#Con2d中的bias参数的意义是是否添加偏置项
self.conv1=nn.Conv2d(in_channels=in_channel,out_channels=width,kernel_size=1,stride=1,bias=False)
self.bn1=nn.BatchNorm2d(width)
#3x3卷积
#治理的groups,是Resnext需要的,对Resnet做了扩展,提取特征时进行分组卷积
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)
#第二个1x1卷积,上采样
#注意这里的输出channel乘以了4,对应网络结构中第三层卷积核个数是前两层的4倍
self.conv3=nn.Conv2d(in_channels=width,out_channels=out_channel*self*expansion,kernel_size=1,stride=1,bias=False)
self.bn3=nn.BatchNorm2d(out_channel*self.expansion)
#Relu参数的inplace=Rrue的含义是,可以直接修改输入张量的值,并且输出张量的值和修改后的输入张量值相同,可以减少内存的使用
self.relu=nn.ReLU(inplace=True)
self.downsample=downsample
#基本和Basicblock中的forward一致,不再做解释
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)
#残差网络
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
self.first_resblock_in_channel=64
self.groups=groups
self.width_per_group=width_per_group
self.conv1 == nn.Conv2d(3,self.in_first_resblock_in_channelchannel,kernel_size=7,stride=2,padding=3,bias=False)
self.bn1 = nn.BatchNorm2d(first_resblock_in_channel)
self.relu=nn.ReLU(inplace=True)
self.maxpool=nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
self.resblocklayer1 = self._make_layer(block,64,blocks_num[0])
self.resblocklayer2 = self._make_layer(block,128,blocks_num[1],stride=2)
self.resblocklayer3 = self._make_layer(block,256,blocks_num[2],stride=2)
self.resblocklayer4 = self._make_layer(block,512,blocks_num[3],stride=2)
if self.include_top:
#outputsize=(1,1)
#适应性平均池化(AdaptiveAvgPool2d),接受任意大小的输入并产生规定大小的输出(这里为(1,1),意味着每个通道都会被池化为一个单独的数值)
self.avgpool = nn.AdaptiveAvgPool2d((1,1))
# 全连接层
self.fc=nn.Linear(512*block.expansion,num_classes)
#对卷积层的权重进行初始化
#在ResNet等深度学习模型中,合适的权重初始化是确保网络训练有效、稳定的关键因素之一。#特别是在使用深层网络结构时,如果初始化不当,可能会导致梯度消失或梯度爆炸问题,从而阻碍网络的收敛。#这段代码的作用是对所有卷积层(nn.Conv2d)进行权重初始化,使用了Kaiming He初始化方法(也称为He初始化),
#这种初始化方法特别适用于配合ReLU激活函数的网络
"""
为什么使用Kaiming He初始化?
针对ReLU激活函数优化:Kaiming He初始化是专为ReLU(和类似的ReLU变种)激活函数设计的。ReLU函数在输入大于0时,导数为1,这意味着在正区间内,激活函数对输入的放大或缩小作用较小。然而,ReLU在输入小于0时输出为0,这使得网络的有效输入数据范围可能减小。He初始化通过考虑输入单元的数量(fan-in)或输出单元的数量(fan-out)来调整权重的标准差,从而帮助维持激活数据在训练初期的分布,防止信号在通过每层时逐渐消失或爆炸。fan_out模式:在使用ReLU激活时,选择fan_out是因为它假设层的输出方差应保持不变,从而帮助避免前向传播中的激活值分布过宽或过窄。这在理论上有助于避免梯度消失的问题,尤其是在网络较深时。网络的深度和复杂度:深层网络(如ResNet)需要特别关注权重初始化,因为错误的初始化会随着层的增加被放大,导致网络训练失败。He初始化通过精心设计的方差校正帮助深层网络在训练初期保持有效的信息流动。"""
for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(nn.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:
#stride为非1,对特征图的宽高进行下采样.卷积核的大小增加到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))
#这里in_chanel是downsample的输出层深度
self.in_channel=channel*block.expansion
#由于上面已经加了一个block,这里从1开始
for _ in range(1,block_num):
layers.append(block(self.in_channel,channel,groups=self.groups,width_per_group=self.width_per_group))
return nn.Sequential(*layers)
def forward(self,x):
#第一层 7x7的卷积核
x=self.conv1(x)
x=self.bn1(x)
x=self.relu(x)
x=self.maxpool(x)
#下面进入残差layers
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 resnet34(num_classes=1000,include_top=True):
#34层的reset使用BasicBlock残差块, 其中4层resblock分别重复3,4,6,3次
return ResNet(BasicBlock,[3,4,6,3],num_classes=num_classes,include_top=include_top)
def resnet50(num_classes=1000,include_top=True):
#和34层唯一的区别就是残差block
return ResNet(Bottleneck,[3,4,6,3],num_classes=num_classes,include_top=include_top)
def resnet101(num_classes=1000,include_top=True):
#使用了更深的网络
return ResNet(Bottleneck,[3,4,23,3],num_classes=num_classes,include_top=include_top)
def resnext50_32x4d(num_classes=1000,include_top=True):
#这里使用基于Resnet扩展的resnext网络, 引入了一种新的维度,称为“分组维度”(group dimension),来增加网络的容量和性能
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)
3.2 模型训练和推理
模型训练和推理代码和上一篇VGG网络的基本完全一致.有一点区别就是使用了基于Imagenet训练的预训练模型,在训练和推理是对数据归一化采用了Imagenet数据集的均值和标准差
data_transform = {
"train": transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
#[0.485, 0.456, 0.406]是ImageNet数据集的RGB通道的均值
#[0.229, 0.224, 0.225]是ImageNet数据集的RGB通道的标准差
#resnet使用了imagenet进行预训练,我们在该预训练模型上train,需要使用相同的归一化方法来归一我们的输入数据,从而提高模型的表现和泛化能力
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]),
])
}
四、资料
1、论文:https://openaccess.thecvf.com/content_cvpr_2016/papers/He_Deep_Residual_Learning_CVPR_2016_paper.pdf
2、代码实现:https://github.com/WZMIAOMIAO/deep-learning-for-image-processing
3、霹雳吧啦Wz https://space.bilibili.com/18161609/channel/series
感谢你的阅读
接下来我们继续学习输出深度学习相关内容,欢迎关注公众号“音视频开发之旅”,一起学习成长。
欢迎交流