携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情
ResNet
深度网络退化问题
在前面的学习网络的学习中,我们可以发现VGG到GoogLeNet,这些网络深度都不高,可能最多20层左右。
当增加网络层数后,网络可以进行更加复杂的特征模式的提取,所以当模型更深时理论上可以取得更好的结果,但是我们从上图发现,结果恰恰相反,深度网络出现了退化问题,56 层的网络比20层的网络效果要差,这会不会是过拟合问题?首先排除,因为在测试集上56层网络误差,有收敛,依旧比较高。网络中存在梯度消失或者爆炸的问题,模型越深,越难训练。
残差
怎么解决梯度消失或者爆炸的问题呢?从一种角度考虑:如果我们在基础模型上加新的层,最坏的情况:如果新的层不学习,是不是新的模型和基础模型一样了,至少不会更差。“什么都不学习”,学术上被称为恒等映射(Identity Mapping),用数学表达式表示为:。
残差块结构图如下
假设输入为 ,学习到特征结果记为 。
我们可以根据结构发现:,是原始的学习特征,极端情况下原始学习特征,那么最后结果为 ,此时模型就什么都不学习,做了恒等映射。实际上不会为0,这也会使得堆积层在输入特征基础上学习到新的特征,从而拥有更好的性能,尽可能去拟合我们期望的值。
具体实现上,残差块有多种构建方式,如下图所示。
左侧模型:当经过 卷积层后如果没有改变输入 的维度,那么他们之间可以相加。
右侧模型:当经过 卷积层后如果改变输入 的维度,采用 卷积层增加维度,可以相加。
class Residual(nn.Module): #@save
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
网络结构
ResNet 网络是参考了VGG19网络,在其基础上进行了修改,加入残差单元。与 VGG19 不同的是,ResNet 用global average pool层替换了全连接层。在图中也会看到输入形状发生变化(虚线表示形状发生变化),残差就会使用第二种方式卷积层增加维度保证可以相加。ResNet的一个重要设计原则是:当输入形状大小降低一半时,输入通道的数量增加一倍,这保持了网络层的复杂度,这样就可以构建更深的网络。
不同深度ResNet结构图如下:
从表中可以看到,对于 18-layer 和 34-layer 的 ResNet,其进行的两层间()的残差学习,当网络更深时,其进行的是三层间的残差学习,三层卷积核分别是,和。
为什么会这样子?
选择64-layer 模型与152-layer模型 其中一个残差块比较参数量:
64-layer:
152-layer:
两者比较,深度更深的网络,参数量减少了,在算力有限的情况下,深层网络中的残差基础块应该减少算力消耗。
作者对比18-layer和34-layer的网络效果,如上图所示。可以看到普通的网络出现退化现象,但是 ResNet 很好的解决了退化问题。
其实还有更优的残差结构,就是 pre-activation 字面意思就是将激活函数提到前面来,改进的方向就是:对BatchNorm、ReLU和卷积层的位置进行调整。
根据作者们的证明和分析,图中的e能获得很大的收益,在CIFAR-10上的表现最好。
代码
第一个模块:输出通道数为64、步幅为2的卷积层后,接步幅为2的的最大汇聚层。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
构建ResNet模块部分,封装成函数形式:
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,
use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk
其实在每次输入形状发生变化时候,其实都是需要使用第二种残差单元,从ResNet-34可以看出第一个残差块是不需要使用第二种。
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(), nn.Linear(512, 10))