目录

1.引言

2.网络创新

 Residual-残差块

Batch Normalization 批范规划层

迁移学习 

3.网络架构

4.代码实现

5.总结

1.引言

ResNet 是在 2015 年由何凯明提出来的,斩获当年 ImageNet 竞赛中分类任务第一名,目标检测任务第一名,获得 COCO 数据集中目标检测第一名,图像分割第一名,NB。 原始论文为:Deep Residual Learning for Image Recognition。

随着网络越深,出现的问题

  • 产生梯度消失的现象
  • 网络返回的梯度相关性会越来越差,接近白噪声,导致梯度更新也接近于随机扰动
  • 下面这个是退化(在训练和测试集上都会表现很差),不是过拟合(训练集上表现很好,测试集很差)

下图所示网络越深,错误率反而越高 

10层的resnet网络框架 resnet网络结构图_10层的resnet网络框架

 

2.网络创新

Residual-残差块

引入深度残差框架,

  • 让卷积网络取学习残差映射
  • 而不是每一个对叠层网络都完整的拟合潜在的拟合函数

相较于直接优化潜在映射H(x),优化残差映射是更为容

10层的resnet网络框架 resnet网络结构图_计算机视觉_02

 

10层的resnet网络框架 resnet网络结构图_10层的resnet网络框架_03

10层的resnet网络框架 resnet网络结构图_计算机视觉_04

 

  • 注意:加了之后再经过 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 批范规划层

迁移学习 

优势

  1. 能够快速的训练出一个理想的结果
  2. 当数据集较小时也能训练出理想的效果

理解:

将前面的网络学习到的浅层信息,变为通用的识别方式

注意: 使用别人预训练模型的参数时,要注意别人的预处理方式

 

10层的resnet网络框架 resnet网络结构图_迁移学习_05

 常见见的迁移学习方式:一般推荐载入权重后训练所有参数

10层的resnet网络框架 resnet网络结构图_10层的resnet网络框架_06

3.网络架构

 

10层的resnet网络框架 resnet网络结构图_pytorch_07

10层的resnet网络框架 resnet网络结构图_pytorch_08

对于需要进行下采样的残差结构(conv_3, conv_4, conv_5 的第一个残差结构),论文使用如下形式(原论文有多个形式,我们这里说的是最后作者选择的形式),主线部分 3×3 的卷积层使用 stride = 2,实现下采样;虚线部分的 1 × 1 1 \times 1 1×1 卷积且 stride = 2:

 

10层的resnet网络框架 resnet网络结构图_迁移学习_09

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)对随后的深层神经网络设计产生了深远影响。