经典卷积神经网络-ResNet
一、背景介绍
残差神经网络(ResNet)是由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出的。ResNet 在2015 年的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中取得了冠军。残差神经网络的主要贡献是发现了“退化现象(Degradation)”,并针对退化现象发明了 “快捷连接(Shortcut connection)”,极大的消除了深度过大的神经网络训练困难问题。神经网络的“深度”首次突破了100层、最大的神经网络甚至超过了1000层。
二、ResNet网络结构
2.0 残差块
配合吴恩达深度学习视频中的图片进行讲解:
如图所示,Residual block就是将 a [ l ] a^{[l]} a[l]传送到 z l + 2 z^{l+2} zl+2上,其相加之后再进行激活得到 a [ l + 2 ] a^{[l+2]} a[l+2]。这一步骤称为"skip connection",即指 a [ l ] a^{[l]} a[l]跳过一层或好几层,从而将信息传递到神经网络的更深层。所以构建一个ResNet网络就是通过将很多这样的残差块堆积在一起,形成一个深度神经网络。
那么引入残差块为什么有效呢? 一个直观上的理解:
如图所示,假设我们给我们的神经网络再增加两层,我们要得到 a [ l + 2 ] a^{[l+2]} a[l+2],我们通过增加一个残差块来完成。这时 a [ l + 2 ] = g ( z [ l + 2 ] + a [ l ] ) = g ( w [ l + 2 ] a [ l + 1 ] + a [ l ] ) a^{[l+2]}=g(z^{[l+2]}+a^{[l]})=g(w^{[l+2]}a^{[l+1]}+a^{}[l]) a[l+2]=g(z[l+2]+a[l])=g(w[l+2]a[l+1]+a[l]),如果我们应用了L2正则化,此时权重参数会减小,我们可以极端的假设 w [ l + 2 ] = 0 , b [ l + 2 ] = 0 w^{[l+2]}=0,b^{[l+2]}=0 w[l+2]=0,b[l+2]=0,那么得到 a [ l + 2 ] = g ( a [ l ] ) = a [ l ] a^{[l+2]}=g(a^{[l]})=a^{[l]} a[l+2]=g(a[l])=a[l](因为使用的是ReLU激活函数,非负的值激活后为原来的值, a [ l ] a^{[l]} a[l]已经经过ReLU激活过了,所以全为非负值)。这意味着,即使给神经网络增加了两层,它的效果并不逊色于更简单的神经网络。所以给大型的神经网络添加残差块来增加网络深度,并不会影响网络的表现。如果我们增加的这两层碰巧能学习到一些有用的信息,那么它就比原来的神经网络表现的更好。
论文中ResNet层数在34及以下和50及以上时采用的是不同的残差块。下面我们分别介绍:
2.1 ResNet-34
如上图所示是ResNet-34以下采用的残差块,我们将其称作BasicBlock。
ResNet-34的网络结构如下:图中实线表示通道数没有变化,虚线表示通道数发生了变化。
其具体的网络结构如下表:
2.2 ResNet-50
如图所示是ResNet-50以上采用的残差块,我们将其称作Bottleneck,使用了1 × 1的卷积来进行通道数的改变,减小计算量。其具体的网络结构见上面的表。
三、论文部分解读
-
论文中的第一张图就表明了更深层的“plain”神经网络(即不用Residual Learning)的错误率在训练集和测试集上甚至比层数少的“plain”神经网络还要高,所以就引出了问题:训练很深的网络是一个很难的问题。
-
论文中的这张图就表明了,使用Residual Learning后训练更深层的神经网络效果会变得更好。
-
论文中处理残差连接中输入和输出不对等的解决方法:
- 添加一些额外的0,使得输入和输出的形状可以对应起来可以做相加。
- 输入和输出不对等的时候使用1 × 1的卷积操作来做投影(目前都是这种方法)
- 所有的地方都使用1 × 1的卷积来做投影,计算量太大没必要
-
论文最主要的是提出了residual结构(残差结构),并搭建超深的网络结构(突破1000层)
-
使用了大量BN来加速训练(丢弃dropout)
-
论文中对CIFAR10数据集做了大量实验验证其效果,最后还将ResNet用到了目标检测领域
三、ResNet-18的Pytorch实现
import torch
import torch.nn as nn
import torchsummary
# BasicBlock
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(BasicBlock, self).__init__()
# 第一个卷积有可能要进行下采样 即将输出通道翻倍 输出数据大小全部减半