目录
1.引言
2.网络创新
Residual-残差块
Batch Normalization 批范规划层
迁移学习
3.网络架构
4.代码实现
5.总结
1.引言
ResNet 是在 2015 年由何凯明提出来的,斩获当年 ImageNet 竞赛中分类任务第一名,目标检测任务第一名,获得 COCO 数据集中目标检测第一名,图像分割第一名,NB。 原始论文为:Deep Residual Learning for Image Recognition。
随着网络越深,出现的问题
- 产生梯度消失的现象
- 网络返回的梯度相关性会越来越差,接近白噪声,导致梯度更新也接近于随机扰动
- 下面这个是退化(在训练和测试集上都会表现很差),不是过拟合(训练集上表现很好,测试集很差)
下图所示网络越深,错误率反而越高
2.网络创新
Residual-残差块
引入深度残差框架,
- 让卷积网络取学习残差映射
- 而不是每一个对叠层网络都完整的拟合潜在的拟合函数
相较于直接优化潜在映射H(x),优化残差映射是更为容
- 注意:加了之后再经过 ReLU 激活函数。
- 注意:主分支与 shortcut 分支的输出特征矩阵的shape必须一致才能进行元素加法。回顾,GoogleNet 是在深度方向进行拼接。
- 通过最大池化下采样后得到的输出是[56,56,64]
- 刚好是实线残差结构所需的输入shape
如何理解 Residual 呢?
假设我们要求解的映射为: H ( x ) H(x) H(x)。现在咱们将这个问题转换为求解网络的残差映射函数,也就是 F ( x ) F(x) F(x),其中 F ( x ) = H ( x ) − x F(x) = H(x)-x F(x)=H(x)−x。残差即观测值与估计值之间的差。这里 H ( x ) H(x) H(x) 就是观测值, x x x 就是估计值(也就是上一层Residual 输出的特征映射)。我们一般称 x x x 为 Identity Function(恒等变换),它是一个跳跃连接;称 F ( x ) F(x) F(x) 为 Residual Function。
那么我们要求解的问题变成了 H ( x ) = F ( x ) + x H(x) = F(x)+x H(x)=F(x)+x。有小伙伴可能会疑惑,我们干嘛非要经过 F ( x ) F(x) F(x) 之后在求解 H ( x ) H(x) H(x) 啊!整这么麻烦干嘛,直接搞不好吗,神经网络那么强(三层全连接可以拟合任何函数)!我们来分析分析:如果是采用一般的卷积神经网络的话,原先我们要求解的是 H ( x ) = F ( x ) H(x) = F(x) H(x)=F(x) 这个值对不?那么,我们现在假设,在我的网络达到某一个深度的时候,咱们的网络已经达到最优状态了,也就是说,此时的错误率是最低的时候,再往下加深网络的化就会出现退化问题(错误率上升的问题)。我们现在要更新下一层网络的权值就会变得很麻烦,权值得是一个让下一层网络同样也是最优状态才行,对吧?我们假设输入输出特征尺寸不变,那么下一层最优的状态就是学习一个恒等映射,不改变输入特征是最好的,这样后续的计算会保持,错误了就和浅一层一样了。但是这是很难的,试想一下,给你个 3 × 3 3 \times 3 3×3 卷积,数学上推导出来恒等映射的卷积核参数有一个,那就是中间为 1,其余为 0。但是它不是想学就能学出来的,特别是初始化权重离得远时。
但是采用残差网络就能很好的解决这个问题。还是假设当前网络的深度能够使得错误率最低,如果继续增加咱们的 ResNet,为了保证下一层的网络状态仍然是最优状态,咱们只需要把令 F ( x ) = 0 F(x)=0 F(x)=0 就好啦!因为 x x x 是当前输出的最优解,为了让它成为下一层的最优解也就是希望咱们的输出 H ( x ) = x H(x)=x H(x)=x 的话,是不是只要让 F ( x ) = 0 F(x)=0 F(x)=0 就行了?这个太方便了,只要卷积核参数都足够小,乘法加法之后就是 0 呀。当然上面提到的只是理想情况,咱们在真实测试的时候 x x x 肯定是很难达到最优的,但是总会有那么一个时刻它能够无限接近最优解。采用 Residual 的话,也只用小小的更新 F ( x ) F(x) F(x) 部分的权重值就行啦!不用像一般的卷积层一样大动干戈!
注意:如果残差映射( F ( x ) F(x) F(x))的结果的维度与跳跃连接( x x x)的维度不同,那咱们是没有办法对它们两个进行相加操作的,必须对 x x x进行升维操作,让他俩的维度相同时才能计算。升维的方法有两种:
全 0 填充
采用 1×1 卷积
Bottleneck类的实现
import torch
from torch import nn
class Bottleneck(nn.Module):
#残差块中的两个卷积的通道会由64->256,256->1024,所以乘4即可
def __init__(self,in_dim,out_dim,stride = 1):
super(Bottleneck,self).__init__()
#网络堆叠层使用的1*1 3*3 1*1这三个卷积组成,中间有BN层
self.bottleneck = nn.Sequential(
nn.Conv2d(in_channels=in_dim,out_channels=in_dim,kernel_size=1,bias=False),
nn.BatchNorm2d(in_dim),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=in_dim,out_channels=in_dim,kernel_size=3,padding=1,bias=False),
nn.BatchNorm2d(in_dim),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=in_dim,out_channels=out_dim,kernel_size=1,padding=1,bias=False),
nn.BatchNorm2d(out_dim)
)
self.relu =nn.ReLU(inplace=True)
#Downsample 部分是一个包含BN层的1*1卷积组成
'''利用DownSample结构将恒等映射的通道数变为与卷积堆叠层相同,从而保证可以相加'''
self.downslape = nn.Sequential(
nn.Conv2d(in_channels=in_dim,out_channels=out_dim,kernel_size=1,padding=1,stride=1),
nn.BatchNorm2d(out_dim)
)
def forward(self,x):
identity = x
out = self.bottleneck(identity)
identity = self.downslape(x)
#将identity(恒等映射)与网络堆叠层输出进行相加。并经过Relu后输出
out +=identity
out =self.relu(out)
return out
bottleneck_1 = Bottleneck(64,256)
print(bottleneck_1)
input = torch.randn(1,64,56,56)
out = bottleneck_1(input)
print(out.shape)
Batch Normalization 批范规划层
迁移学习
优势
- 能够快速的训练出一个理想的结果
- 当数据集较小时也能训练出理想的效果
理解:
将前面的网络学习到的浅层信息,变为通用的识别方式
注意: 使用别人预训练模型的参数时,要注意别人的预处理方式
常见见的迁移学习方式:一般推荐载入权重后训练所有参数
3.网络架构
对于需要进行下采样的残差结构(conv_3, conv_4, conv_5 的第一个残差结构),论文使用如下形式(原论文有多个形式,我们这里说的是最后作者选择的形式),主线部分 3×3 的卷积层使用 stride = 2,实现下采样;虚线部分的 1 × 1 1 \times 1 1×1 卷积且 stride = 2:
4.代码实现
class ResNet(nn.Module):
def __init__(self,block,blocks_num,num_class=1000,include_top =True,groups =1):
super(ResNet, self).__init__()
self.include_top = include_top
self.in_channel = 64
self.groups = groups
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)
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.layer1 = self._make_layer(block, 512, blocks_num[0],stride=2)
if self.include_top:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_class)
def _make_layer(self,block,channel,block_num,stride=1) ->nn.Sequential:
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))
self.in_channel = channel*block.expansion
#将实线的残差结构搭进去
for _ in range(1,block_num):
layers.append(block(self.in_channel,channel))
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_class=1000,include_top=True)->ResNet:
return ResNet(BasicBlock,[3,4,6,3],num_class=num_class,include_top=include_top)
def resNet50(num_class=1000,include_top=True):
return ResNet(Bottleneck,[3,4,5,6],num_class=num_class,include_top=include_top)
def resNet101(num_class=1000,include_top=True):
return ResNet(Bottleneck,[3,4,23,3],num_class=num_class,include_top=include_top)
5.总结
- 学习嵌套函数(nested function)是训练神经网络的理想情况。在深层神经网络中,学习另一层作为恒等映射(identity function)较容易(尽管这是一个极端情况)。
- 残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零。
- 利用残差块(residual blocks)可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播。
- 残差网络(ResNet)对随后的深层神经网络设计产生了深远影响。