(一)ResNet提出背景
更深的网络可以进行更加复杂的特征模式的提取,从而理论上更深的网络可以得到更好的结果。
- 但是通过简单的叠加层的方式来增加网络深度,可能引来梯度消失/梯度爆炸的问题,目前**batch normalization等方法可以解决梯度消失和梯度爆炸的问题。**但是使用了各种normalization的方法也不能是深层网络的效果好于浅层网络。
- 理论上,若A为浅层网络,B为深层网络,且B的浅层结构完全复制于A,后几层为线性层(identity mapping),那么B网络的效果应该是和A的相同的。但是实验发现,A网络的训练准确率反而比浅层网络要低,这说明在实际应用时,高层的这种线性关系很难学到,也就是出现了degradation problem(退化)。训练集准确率下降的原因肯定不是过拟合,因为过拟合的话训练集的准确率应该很高。由此出发,我们将这种线性关系加到网络的学习中,最后学出来的网络效果应该大于等于浅层网络的效果,也可以认为,学习这种线性映射会更加容易。
(二)ResNet结构理解
而残差网络就可以让网络层次很深的时候,网络依然有很好的性能和效率。
残差块的结构如下图:
可以看到一个“弯弯的弧线“这个就是所谓的”shortcut connection“,也是identity mapping(恒等映射),就是这个恒等映射可以让网络随深度增加而不退化。这反映了多层非线性网络无法逼近恒等映射网络。
但是,不退化不是我们的目的,我们希望有更好性能的网络。ResNet学习的是残差函数F(x) = H(x) - x, 这里如果F(x) = 0, 那么就是上面提到的恒等映射。
identity mapping:x->x
residual mapping:F(x)=H(x)-x
H (x) :desired underlying mapping——期望拟合的特征图,一个building block要拟合的就是这个特征图,未使用残差网络时,F (x) 的目标是拟合H(x)。使用参差网络后F (x)的目标是拟合H(x)-x,后者比前者更容易优化!
如果网络已经到达最优,继续加深网络,residual mapping将被push为0,只剩下identity mapping,这样理论上网络一直处于最优状态了,网络的性能也就不会随着深度增加而降低了。
具体过程见下面:
左边的过程如下图:
右边的残差学习过程如下图:
所以,a[l+2]=g(z[l+2]+a[l]),如果z[l+2]=0,那么a[l+2]=a[l]。这说明即使增加两层,它的效率也不逊色于简单的神经网络。所以给大型神经网络增加两层,不论是把残差块添加到神经网络的中间还是末端位置都不会影响网络的实现。此外,如果这些隐层单元学到一些有用信息,那么它可能比学习恒等函数变现的更好。
(三)ResNet的简单实现
(1)残差块
import d2lzh as d2l
from mxnet import gluon, init, nd
from mxnet.gluon import nn
class Residual(nn.Block): # 本类已保存在d2lzh包中方便以后使用
def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs):
super(Residual, self).__init__(**kwargs)
self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1,
strides=strides)
self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1) #默认strides为1
if use_1x1conv:
self.conv3 = nn.Conv2D(num_channels, kernel_size=1,
strides=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm()
self.bn2 = nn.BatchNorm()
def forward(self, X):
Y = nd.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
return nd.relu(Y + X)
查看输入的形状:
不变型:
blk = Residual(3) # num_channels=3
blk.initialize()
X = nd.random.uniform(shape=(4, 3, 6, 6)) # 输入为(4,3,6,6)
blk(X).shape
从残差块的结构上分析,输出的num_channels不变,
(X)图像大小6×6,经过第一个卷积层,6-3+2+1=6,输出为6×6,经过第一个批量归一化层,再经过第一个激活函数,经过第二个卷积层,6-3+2+1=6,6×6,默认不存在1×1卷积层,所以直接经过批量归一化层,输出为6×6(Y),然后(X+Y)一起经过激活函数后输出,为(4,3,6,6)
减半型:
blk = Residual(6, use_1x1conv=True, strides=2) #通道数为6,使用1×1的卷积层,步幅为2
blk.initialize()
blk(X).shape
同样对残差块结构进行分析,
X图像为(3×6×6),经过第一个卷积层,(6-3+1×2+2)/ 2=3,输出为(6×3×3),经过第一个批量归一化层,再经过第一个激活函数,经过第二个卷积层,3-3+1×2+1=3,输出为Y(6×3×3),X经过1×1卷积层后输出为(6-1+2)/2=3,输出为X(6×3×3),所以最后(X+Y)一起经过激活函数输出,为(4,6,3,3)
(2)ResNet模型
ResNet的前两层跟之前介绍的GoogLeNet中的一样:在输出通道数为64、步幅为2的 7×7卷积层后接步幅为2的 3×3的最大池化层。不同之处在于ResNet每个卷积层后增加的批量归一化层。
net = nn.Sequential()
net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3),
nn.BatchNorm(), nn.Activation('relu'),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
def resnet_block(num_channels, num_residuals, first_block=False):
blk = nn.Sequential()
for i in range(num_residuals):
if i == 0 and not first_block:
blk.add(Residual(num_channels, use_1x1conv=True, strides=2)) #非第一个模块的第一个残差块使用的是减半型
else:
blk.add(Residual(num_channels)) #其余采用的是不变型
return blk
# 一共有四个模块,每个模块里有两个残差块
net.add(resnet_block(64, 2, first_block=True), #第一个模块特殊处理
resnet_block(128, 2),
resnet_block(256, 2),
resnet_block(512, 2))
第一个模块(两个残差块都是不变型):
GoogLeNet在后面接了4个由Inception块组成的模块。ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。
第二三四模块(第一个残差块是减半型,第二个残差块是不变型):
对于第二三四模块:
首先64×56×56的输入进入第1个block的conv1,这个conv1的stride变为2,这是为了降低输入尺寸,减少数据量,输出尺寸为128×28×28。
到第1个block的末尾处,需要在output加上residual,但是输入的尺寸为64×56×56,所以在输入和输出之间加一个 1×1卷积层,stride=2(图4红圈标注),作用是使输入和输出尺寸统一,(顺带一提,这个部分就是PyTorch ResNet代码中的downsample)
由于已经降低了尺寸,第2个block的conv1的stride就设置为1。由于该block没有降低尺寸,residual和输出尺寸相同,所以也没有downsample部分。
最后,与GoogLeNet一样,加入全局平均池化层后接上全连接层输出。
net.add(nn.GlobalAvgPool2D(), nn.Dense(10))
这里每个模块里有4个卷积层(不计算 1×1卷积层),加上最开始的卷积层和最后的全连接层,共计18层。这个模型通常也被称为ResNet-18。
(4)获取数据和训练集
lr, num_epochs, batch_size, ctx = 0.05, 5, 256, d2l.try_gpu()
net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier())
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx,
num_epochs)
虽然ResNet的主体架构跟GoogLeNet的类似,但ResNet结构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用。