前言:深层神经网络的每一层分别对应于提取不同层次的特征信息,有低层,中层和高层,而网络越深的时候,提取到的不同层次的信息会越多,而不同层次间的层次信息的组合也会越多。可是深层神经网络却出现了梯度消失和梯度爆炸的问题,网络的效果变得越来越差,甚至出现了网络的退化问题,传统对应的解决方案则是数据的初始化(normlized initializatiton)和(batch normlization)正则化,但是这样虽然解决了梯度的问题,却带来了另外的问题,就是网络性能的退化问题,深度加深了,错误率却上升了。
可以看出当网络到一定深度之后,模型准确率会趋于饱和不再上升,并且当网络深度继续加深时,准确率反而会下降。究其原因,在训练深度模型时,使用反向传播不断更新参数,但是神经网络在反向传播的时候梯度会不停的衰减,(以 Sigmod函数为例,对于幅度为 1 的信号,每向后传递一层,梯度就衰减为原来的 0.25),因此当网络深度过深时,导致梯度消失,进而模型准确率下降。
4、各种模型的搭建
(1)Resnet 网络介绍
Resnet 深度残差神经网络。与Alexnet和VGG不同的是,深度残差网络的设计是为了克服由于网络深度加深而产生的学习效率变低与准确率无法有效提升的问题,所以网络结构上就有很大的改变。它的结构是: 将靠前若干层的某一层数据输出直接跳过多层引入到后面数据层的输入部分。意味着后面的特征层的内容会有一部分由其前面的某一层线性贡献。残差网络的特点是容易优化,并且能够通过增加相当的深度来提高准确率。其内部的残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题 。其结构如下:
上面的结构中,通过捷径连接,直接将上层特征图 x 作为部分输出的初始结果,可以转换为学习一个残差函数 H(x) = F(x)+x,当 F(x)=0 时,即变为恒等映射H(x) = x 。因此,假如本来要优化的目标是H(x)=F(x)+ x,这样的结构相当于把优化的目标由H(x)转化成H(x)- x,学习残差函数F(x)= H(x)- x 的部分,即为残差,后面的层次就是为了将残差结果逼近于 0,使得模型深度加深的同时,模型效果也越好,在 ResNet 模型中使用的残差结构如下图。
两种结构分别应用于不同深度的 ResNet 模型,左图应用较浅的 34 层模型,右图主要应用于较深的 50、101 和 152 层模型,在 3*3 的卷积核前后应用 1*1 的卷积核进行降维和升维操作,实现降低参数量目的,进而加快模型的训练速度。
通过改变结构在一个浅层网络基础上叠加恒等映射层,可以让网络随着深度增加而不退化。把训练目标由H(x)转变为H(x)- x,这样就不是把网络训练到一个等价映射H(x) = F(x)+x,而是将其逼近于0,这样训练的难度比训练一个等价映射小。 如果已经学习到较饱和的准确率时,那么接下来的学习目标就转变为恒等映射的学习,也就是使输入x近似于输出H(x),以保持在后面的层次中不会造成精度下降。例如,在一个5层的网络中,如果前面四层已经达到一个最优的函数,那这时通过跳跃结构,我们的优化目标就从一个等价映射变为逼近0了,逼近其他任何函数都会造成网络退化。通过这种方式就可以解决网络太深难训练的问题。
总结:ResNets学习的是残差函数F(x)=H(x)-x,这里如果F(x)=0,那么就是上面提及的恒等映射。但实际上只是逼近,而不是使得F(x)=0,因为学习一个F(x)=0的恒等映射很难。
ResNet50的残差网络结构。有两个基本的块,(如果主干和残差部分的输出维度不同,需要卷积操作来整维度):
Conv Block的结构如下,由图可以看出,Conv Block可以分为两个部分, 左边部分为主干部分,存在两次卷积、标准化、激活函数和一次卷积、标准化;右边部分为残差边部分,存在一次卷积、标准化 ,由于残差边部分存在卷积,所以我们可以利用Conv Block改变输出特征层的宽高和通道数来改变网络的维度:
Identity Block的结构如下,由图可以看出,Identity Block可以分为两个部分, 左边部分为主干部分,存在两次卷积、标准化、激活函数和一次卷积、标准化;右边部分为残差边部分,直接与输出相接 ,由于残差边部分不存在卷积,所以Identity Block的输入特征层和输出特征层的shape是相同的,可用于加深网络:
(2)ResNet50网络模型搭建
from torch.hub import load_state_dict_from_url
model_urls = {
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
}
#基本结构模块
class BasicBlock(nn.Module):
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)
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 = 4
def __init__(self, in_channel, out_channel, stride=1, downsample=None,
groups=1, width_per_group=64):
super(Bottleneck, self).__init__()
# 输入56x56x256
width = int(out_channel * (width_per_group / 64.)) * groups
#卷积层一和第一层正则化与原来一样。卷积核数量是128,
#输出56x56x128。深度缩小一半
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)
#卷积层二kernel_size=3,stride=2则处理后的图像长宽缩小一半。stride=1则处理后的图像长宽不变。
#卷积核数量是128,输出28x28x128。长宽缩小一半。
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不改变图像大小,但卷积核数量变成4倍。作用是增加图像的深度。
# 卷积核数量是128x4 = 512,输出28x28x512。深度增加4倍。
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)
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,
blocks_num,
num_classes=10, # 种类修改的地方,是几种就把这个改成几
include_top=True,
groups=1,
width_per_group=64):
super(ResNet, self).__init__()
self.include_top = include_top
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)
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')
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))
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
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)
net = resnet50()
# load pretrain weights
#download url: https://download.pytorch.org/models/resnet34-333f7ec4.pth
#model_weight_path = "./resnet34-pre.pth" # 加载resnet的预训练模型
#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=device))
state_dict = load_state_dict_from_url(model_urls['resnet50'], model_dir='./model_data')
net .load_state_dict(state_dict)
net.to(device)
print(net.to(device)) # 输出模型结构
一、对于类模块:class BasicBlock(nn.Module) 和 class Bottleneck(nn.Module) 的正向传播
(1)首先判断,如果下采样不是None,则对输入矩阵x进行下采样。并赋值给identity。
(2)然后将输入矩阵依次进行卷积(3*3),正则化,Relu激活,卷积(3*3),正则化->得到的输出加上下采样输出求和,作为下一层Relu激活函数的输入。(正则化结构作用:防止梯度消失,使网络可以变得更深)。
(3)class Bottleneck(nn.Module) 的正向传播为了减少计算量,引入了两个1*1的卷积层结构是:卷积(1*1),正则化,Relu激活,卷积(3*3),正则化,Relu激活,卷积(1*1),正则化->得到的输出加上下采样输出求和,作为下一层Relu激活函数的输入。
其中,卷积层之后总会添加BatchNorm2d进行数据的归一化处理,这使得数据在进行Relu之前不会因为数据过大而导致网络性能的不稳定。
对于一个50层、101层和152层的参差结构来说,假设输入为56x56x256:
- 数据经过卷积层一和正则化层一之后,尺寸大小还和原来一样,但卷积核数量、即输出特征维度是128,输出56x56x128。深度缩小了一半。
- 数据经过卷积层二后,kernel_size=3,stride=2,则处理后的图像尺寸大小缩小一半。卷积核数量、即输出特征维度是128,输出28x28x128。长宽缩小一半。
- 数据经过卷积层三后,kernel_size=1,不改变图像大小,但卷积核数量变成4倍。作用是增加图像的深度。卷积核数量、即输出特征维度是128x4=512,输出28x28x512。深度增加4倍。
- 最后是是Relu激活和下采样配置。
二、_make_layer(self, block, channel, block_num, stride=1) 的内结构
- block -> 选择参差结构,是选择18,34层结构(BasicBlock)还是50,101、152层结构(Bottleneck)。
- channel -> 第一层卷积核数量也是输出通道数。
- blocks_num -> 残差层结构数量,是一个参数列表,如ResNet34传入[3,4,6,3]。
- include_top=True -> 扩展模型所用
- self.include_top = include_top -> 将参数传入到类变量当中。
- 图中的conv1解释说明,第一层卷积层: self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,padding=3, bias=False)。
- 第二层池化层:self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
- 后面四层对应下面图片中的conv2_x、conv3_x、conv4_x、conv5_x。可以由_make_layer -> 结构层生成函数生成。
对于一个34、18层的残差结构来说,第一个大layer层,即conv-2层,不做downsample下采样操作,因为在它的残差结构中,stride=1 ,输入(64维)和输出(64维)的尺寸维度一致。而对50、101、152层的残差结构来说,in_channel != channel * block. expansion。 所有layer层都只在第一个block里的第一个1x1, 用stride=2 做downsample下采样操作(将输入维度扩展四倍)。
特点:(1*1)的卷积层只是为了改变维度而减少运算量。(3*3)的卷积层在第一层的虚线残差模块时步距是2,所以改变了图片的大小,但是在之后循环加入的实线残差模块时步距是1,不改变图片的大小。layer1 层的步距始终为1,未涉及到尺寸大小的变换,始终为(56*56)。
在 if 判断语句中可以看到,layer1时,传进去stride = 1,不执行downsample。而后面的layer2,layer3传进去的stride都是2,执行downsample。这样图象的尺寸不仅发生改变,也改变了维度的大小,使得shortout能和输出匹配。
然后将第一层的 虚线 残差结构添加进去,因为对每个layer层的第一层的downsample操作不全相同,所以单独拿出来添加。layers.append(block(self.in_channel, channel, downsample=downsample, stride=stride))
中间还有一部操作就是变更输入特征矩阵的深度,因为layer和layer之间的连接处维度不同,所以上一个残差结构的输出维度需要经过一个(1*1的卷积层)降维作为下一个残差结构的输入。(BasicBlock不变block.expansion=1,Bottleneck增加4倍block.expansion=4)。
根据残差模块的个数将剩下的 实线 残差结构添加进去。即根据block_num数量循环添加添加残差层: for _ in range(1, block_num):
layers.append(block(self.in_channel, channel))
最后返回完整一个层结构:return nn.Sequential(*layers)
三、对于类模块:class ResNet(nn.Module) 的类内结构及正向传播
- 这个是在代码方面对输入数据的张量尺度变换(resnet50):
#(3,224,224)
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
#(64,112,112)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
#(64,56,56)
self.layer1 = self._make_layer(block, 64, blocks_num[0])
#downsample后 (256,56,56) 或者(64,56,56)》(64,56,56)》(256,56,56)——》(64,56,56)》(64,56,56)》(256,56,56)...
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
#downsample后 (512,28,28) 或者(128,56,56)》(128,28,28)》(512,28,28)——》(128,28,28)》(128,28,28)》(512,28,28)...
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
#downsample后(1024,14,14)或者(256,28,28)》(256,14,14)》(1024,14,14)——》(256,14,14)》(256,14,14)》(1024,14,14)...
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
# downsample后(2048,7,7)或者(512,14,14)》(512,7,7)》(2048,7,7)——》(512,7,7)》(512,7,7)》(2048,7,7)...
if self.include_top:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
# output size = (2048,1, 1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 2048》10
- 模型的结构搭建好以后,接着是平均池化下采样层(图片尺寸大小变为1*1)和全连接层(维度由2048变为num_classes):
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')
- 正向传播过程
最终残差网络完整结构是:卷积+池化、四层残差卷积层(降维,卷积,升维)、平均池化+全连接层
resnet网络单独的构造方法:
def resnet34(num_classes=1000, include_top=True):
return ResNet(BasicBlock, [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)