ResNet——深度学习必会模型及PyTorch实现


1. 为什么要引入ResNet?

  • 对于卷积神经网络,深度是一个很重要的因素。深度卷积网络自然的整合了低中高不同层次的特征,特征的层次可以靠加深网络的层次来丰富。因此在构建卷积网络时,网络的深度越高,可抽取的特征层次就越丰富越抽象。所以一般我们会倾向于使用更深层次的网络结构,以便取得更高层次的特征。但是更深层的网络结构真的带来了更好的表现吗?我们看下面这张图:

  • 可以看到,拥有56层数的网络结构表现明显差于拥有20层数的网络结构,造成这一现象的原因大概有:过拟合、梯度消失/爆炸和深度网络的退化,我们来一一剖析。
  1. 过拟合所体现的应该是在训练集效果很好,但是在测试集效果却很差。但是图中无论是在训练还是测试的时候深层网络均不如浅层网络,而且训练和测试的误差相差并不大,所以很明显不是因为过拟合。

  2. 梯度消失/爆炸是因为神经网络在反向传播的时候,反向连乘的梯度小于1(或大于1),导致连乘的次数多了之后(网络层数加深),传回首层的梯度过小甚至为0(过大甚至无穷大),这就是梯度消失/爆炸的概念。但我们知道,如今我们已经习惯加入BN层(Batch Normalize),他可以通过规整数据的分布基本解决梯度消失/爆炸的问题,所以这个问题也不是导致深层网络退化的原因。

  3. 我们选择加深网络的层数,是希望深层的网络的表现能比浅层好,或者是希望它的表现至少和浅层网络持平(相当于直接复制浅层网络的特征),可实际的结果却让我们大吃一惊(深度网络退化)。因为,假设一个比较浅的网络已经可以达到不错的效果,那么即使之后堆上去的网络什么也不做,模型的效果也不会变差。然而事实上,“什么都不做”恰好是当前神经网络最难做到的东西之一。赋予神经网络无限可能性的“非线性”让神经网络模型走得太远,却也让它忘记了为什么出发。这也使得特征随着层层前向传播得到完整保留(什么也不做)的可能性都微乎其微。用学术点的话说,这种神经网络丢失的“不忘初心”的品质叫做恒等映射(identity mapping)。

  • 因此,可以认为ResNet的初衷,其实是让模型的内部结构至少有恒等映射的能力。以保证在堆叠网络的过程中,网络至少不会因为继续堆叠而产生退化!

2. 什么是ResNet?

  • 前面分析得出,如果深层网络后面的层都是是恒等映射,那么模型就可以转化为一个浅层网络,就可以做到效果不小于浅层网络。那现在的问题就是如何得到恒等映射了。于是便引入了残差网络中最重要的结构:“shortcut connections”。
  • 通过shortcut connections,在激活函数之前将本模块的输入与本层计算的输出相加,将求和的结果输入到激活函数中做为本层的输出。我们假设求和的结果为 H ( x ) H(x) H(x),那么可以得到 H ( x ) = F ( x ) + x H(x)= F(x)+x H(x)=F(x)+x,所以我们现在要学习的 F ( x ) = H ( x ) − x F(x)=H(x)-x F(x)=H(x)x,最后实验也证明学习残差比直接学习所有的特征要容易得多。这里也利用到了一种思想就是把相同的主体去掉,突出细小的差别,避免信息丢失

下面说一些自己的观点:

  1. 自适应深度:网络退化问题就体现了多层网络难以拟合恒等映射这种情况,也就是说 H ( x ) H(x) H(x)难以拟合 x x x,但使用了残差结构之后,拟合恒等映射变得很容易,直接把网络参数全学习到为0,只留下那个恒等映射的跨层连接即可。于是当网络不需要这么深时,中间的恒等映射就可以多一点,反之就可以少一点。

  2. 差分放大器:假设最优 H ( x ) H(x) H(x)更接近恒等映射,那么网络更容易发现除恒等映射之外微小的波动。

  3. 模型集成:整个ResNet类似于多个网络的集成,原因是删除ResNet的部分网络结点不影响整个网络的性能,但VGGNet会崩溃,具体可以看这篇NIPS论文。Reference: Residual Networks Behave Like Ensembles of Relatively Shallow Networks

  4. 我们知道残差网络可以表示成 H ( x ) = F ( x ) + x H(x)= F(x)+x H(x)=F(x)+x,这就说明了在求输出 H ( x ) H(x) H(x)对输入 x x x的梯度,也就是在反向传播的时候, H ′ ( x ) = F ′ ( x ) + 1 H^{\prime}(x)= F^{\prime}(x)+1 H(x)=F(x)+1,残差结构的这个常数1也能保证在求梯度的时候梯度不会消失。

下面以论文中展示的ResNet-34为例说一下它的结构:

  • plain network主要是受到VGG的影响,作者在文中叙述了两条设计的规则:(1)对于输出的feature map尺寸相同的层,设置相同的卷积核个数;(2)如果图像的尺寸减半那么就将卷积核的个数增大一倍来保持时间复杂度。而且可以发现34层的ResNet比19层的VGG有着更少的卷积核和复杂度,在浮点数的计算次数上仅仅是VGG-19的18%。最后在plain network上加入shortcut就变成了需要的残差网络。

  • 细心的同学就会发现,在ResNet中有的跳接线是实线,有的跳接线是虚线。虚线的代表这些模块前后的维度不一致。因为去掉残差结构的Plain网络还是和VGG一样,所以每种颜色之间图片的维度是不一样的。这里就有两个情况:空间尺寸不一样和图像深度不一样。如果深度不一样,很容易可以想到利用 1 × 1 1\times1 1×1的卷积核来调整深度;如果空间尺寸不一样,同样可以利用 1 × 1 1\times1 1×1的卷积核将其stride设置为2来实现。

  • 针对比较深的神经网络,作者也考虑到计算量,会先用 1 × 1 1\times1 1×1的卷积将通道数降低,最后通过 1 × 1 1\times1 1×1恢复。这样做的目的是减少参数量和计算量,作者也通过实验证明两者有着相似的时间复杂度。

  • 在这样的想法的加持下,在各种比赛中的效果也是非常的惊人,也为后人设计神经网络提供了一种思路。

  • 说一点关于残差单元题外话,上面我们说到了短路连接的几种处理方式,其实作者在他发表的另一篇文章中又对不同的残差单元做了细致的分析与实验,这里我们直接抛出最优的残差结构,如图8所示。改进前后一个明显的变化是采用pre-activation,BN和ReLU都提前了。而且作者推荐短路连接采用恒等变换,这样保证短路连接不会有阻碍。感兴趣的可以去读读这篇文章。Reference: Identity Mappings in Deep Residual Networks


3. 如何搭建ResNet?

import torch
import torch.nn as nn

# 搭建最基础的残差块,由卷积、BN、ReLU、shortcut构成
class Bottleneck(nn.Module):
    def __init__(self, in_channels, mid_channels, out_channels, stride):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(inplace=True)
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1),
            nn.BatchNorm2d(mid_channels),
            nn.ReLU(inplace=True)
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(mid_channels, out_channels, kernel_size=1, stride=1),
            nn.BatchNorm2d(out_channels)
        )

        if in_channels == out_channels:
            self.shortcut = nn.Sequential()
        else:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
                nn.BatchNorm2d(out_channels)
            )
            
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        residual = x
        residual = self.shortcut(residual)

        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x += residual

        return self.relu(x)


class resnet(nn.Module):
    def __init__(self, num_classes, num_block_lists=[3, 4, 6, 3]):
        super(resnet, self).__init__()
        self.basic_conv = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        )

        self.stage_1 = self._make_layer(64, 64, 256, nums_block=num_block_lists[0], stride=1)
        self.stage_2 = self._make_layer(256, 128, 512, nums_block=num_block_lists[1], stride=2)
        self.stage_3 = self._make_layer(512, 256, 1024, nums_block=num_block_lists[2], stride=2)
        self.stage_4 = self._make_layer(1024, 512, 2048, nums_block=num_block_lists[3], stride=2)

        self.gap = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Linear(2048, num_classes)

    def _make_layer(self, in_channels, mid_channels, out_channels, nums_block, stride):
        layers = [Bottleneck(in_channels, mid_channels, out_channels, stride=stride)]
        for _ in range(1, nums_block):
            layers.append(Bottleneck(out_channels, mid_channels, out_channels, stride=1))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.basic_conv(x)
        x = self.stage_1(x)
        x = self.stage_2(x)
        x = self.stage_3(x)
        x = self.stage_4(x)
        x = self.gap(x)
        x = x.view(x.size(0), -1)

        x = self.classifier(x)
        return x


def ResNet(num_classes, depth):
    key_blocks = {
        50: [3, 4, 6, 3],
        101: [3, 4, 23, 3],
        152: [3, 8, 36, 3]
    }
    model = resnet(num_classes, key_blocks[depth])
    return model

net = ResNet(1000, 152)
y = net(torch.randn(10,3,224,224))
print(y.size())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值