经典神经网络(6)ResNet及其在Fashion-MNIST数据集上的应用
1 ResNet的简述
-
ResNet
提出了一种残差学习框架来解决网络退化问题,从而训练更深的网络。这种框架可以结合已有的各种网络结构,充分发挥二者的优势。 -
ResNet
以三种方式挑战了传统的神经网络架构:-
ResNet
通过引入跳跃连接来绕过残差层,这允许数据直接流向任何后续层。这与传统的、顺序的
pipeline
形成鲜明对比:传统的架构中,网络依次处理低级feature
到高级feature
。 -
ResNet
的层数非常深,高达1202层。而ALexNet
这样的架构,网络层数要小两个量级。 -
通过实验发现,训练好的
ResNet
中去掉单个层并不会影响其预测性能。而训练好的AlexNet
等网络中,移除层会导致预测性能损失。
-
-
在
ImageNet
分类数据集中,拥有152层的残差网络,以3.75% top-5
的错误率获得了ILSVRC 2015
分类比赛的冠军。 -
很多证据表明:残差学习是通用的,不仅可以应用于视觉问题,也可应用于非视觉问题。
-
论文地址: https://arxiv.org/pdf/1512.03385.pdf
-
卷积神经网络领域的两次技术爆炸,第一次是AlexNet,第二次就是ResNet了。
1.1 网络退化问题
-
1、理论上来讲网络深度越深越好。网络越深,提取的图片特征越多越丰富,但随之会带来很多的问题(通过
Batch Normalization
在很大程度上解决),比如过拟合或者计算量爆炸、梯度消失、梯度爆炸等,导致网络在一定深度下就达到了局部最优解。 -
2、
ResNet
论文作者发现:随着网络的深度的增加,准确率达到饱和之后迅速下降,而这种下降不是由过拟合引起的。这称作网络退化问题
。如果更深的网络训练误差更大,则说明是由于优化算法引起的:越深的网络,求解优化问题越难。如下所示:更深的网络导致更高的训练误差和测试误差。
- 3、理论上讲,较深的模型不应该比和它对应的、较浅的模型更差。因为较深的模型是较浅的模型的超空间。较深的模型可以这样得到:先构建较浅的模型,然后添加很多恒等映射的网络层。实际上我们的较深的模型后面添加的不是恒等映射,而是一些非线性层。因此,退化问题表明:
通过多个非线性层来近似横等映射可能是困难的
。
- 4、针对这⼀问题,何恺明等⼈提出了残差⽹络(ResNet)。它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经⽹络的设计。残差⽹络的核⼼思想是:
每个附加层都应该更容易地包含原始函数作为其元素之⼀
。
1.2 残差块(residual blocks)
1.2.1 残差块的理解
1、假设需要学习的是映射 y = H(x),残差块使用堆叠的非线性层拟合残差:y = F(x,W) + x 。
其中:
- x 和 y 是块的输入和输出向量。
- F(x,W)是要学习的残差映射。因为 F(x,W) = H(x) - x,因此称F为残差。
+
:通过快捷连接
逐个元素相加来执行。快捷连接
指的是那些跳过一层或者更多层的连接。- 快捷连接简单的执行恒等映射,并将其输出添加到堆叠层的输出。
- 快捷连接既不增加额外的参数,也不增加计算复杂度。
- 相加之后通过非线性激活函数,这可以视作对整个残差块添加非线性,即 relu(y)
2、残差映射易于捕捉恒等映射的细微波动
。比如5正常映射为5.1,加入残差后变成 5+0.1。此时输入变成5.2,对于没有残差结构的结果,影响仅为0.1/5.1 = 2%。而对于残差结构,变成 5+0.2 , 由0.1变成了0.2 影响为100%。
3、残差映射 H ( x ) = F ( x ) + x ,在反向传播的时候就变成了 H ′ ( x ) = F ′ ( x ) + 1,这里的加1也可以保证梯度消失现象
4、作者也证明了退化问题在任何数据集上都普遍存在。在imagenet上拿到冠军之后,迁移学习用到了coco同样拿到了好几个赛道的冠军,说明残差结构是普适的。最后又和VGG比了一下,比VGG深了8倍,计算复杂性却还比VGG小 。
1.2.2 残差函数F的形式的可变性
-
层数可变:论文中的实验包含有两层堆叠、三层堆叠,实际任务中也可以包含更多层的堆叠。
如果
F
只有一层,则残差块退化线性层:y = Wx + x
。此时对网络并没有什么提升。 -
连接形式可变:不仅可用于全连接层,可也用于卷积层。此时F代表多个卷积层的堆叠,而最终的逐元素加法
+
在两个feature map
上逐通道进行。此时
x
也是一个feature map
,而不再是一个向量。
1.2.3 残差学习成功的原因
学习残差F(x,W)比学习原始映射H(x)要更容易。
-
1、当原始映射
H
就是一个恒等映射时, 就是一个F
零映射。此时求解器只需要简单的将堆叠的非线性连接的权重推向零即可。实际任务中原始映射
H
可能不是一个恒等映射:- 如果
H
更偏向于恒等映射(而不是更偏向于非恒等映射),则F
就是关于恒等映射的抖动,会更容易学习。 - 如果原始映射
H
更偏向于零映射,那么学习 本身要更容易。但是在实际应用中,零映射非常少见,因为它会导致输出全为0。
- 如果
-
2、如果原始映射
H
是一个非恒等映射,则可以考虑对残差模块使用缩放因子。如Inception-Resnet
中:在残差模块与快捷连接叠加之前,对残差进行缩放。注意:ResNet
作者在随后的论文中指出:不应该对恒等映射进行缩放。 -
3、可以通过观察残差
F
的输出来判断:如果F
的输出均为0附近的、较小的数,则说明原始映射H
更偏向于恒等映射;否则,说明原始映射H
更偏向于非横等映射。
1.2.4 残差块代码实现
from torch import nn
from torch.nn import functional as F
import torch
'''
⼀种是当use_1x1conv=False时,应⽤ReLU⾮线性函数之前,将输⼊添加到输出。
另⼀种是当use_1x1conv=True时,添加通过1 × 1卷积调整通道和分辨率
ResNet沿⽤了VGG完整的3 × 3卷积层设计。
残差块⾥⾸先有2个有相同输出通道数的3 × 3卷积层。
每个卷积层后接⼀个批量规范化层和ReLU激活函数。
然后我们通过跨层数据通路,跳过这2个卷积运算,将输⼊直接加在最后的ReLU激活函数前。
这样的设计要求2个卷积层的输出与输⼊形状⼀样,从⽽使它们可以相加。
如果想改变通道数,就需要引⼊⼀个额外的1 × 1卷积层来将输⼊变换成需要的形状后再做相加运算。
'''
class Residual(nn.Module):
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)
if __name__ == '__main__':
blk = Residual(3, 3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
print(Y.shape) # 输⼊和输出形状⼀致 torch.Size([4, 3, 6, 6])
blk = Residual(3, 6, use_1x1conv=True, strides=2)
Y = blk(X)
print(Y.shape) # 在增加输出通道数的同时,减半输出的高和宽 torch.Size([4, 6, 3, 3])
1.3 ResNet网络
1.3.1 四种plain
网络
plain
网络:一些简单网络结构的叠加,如下图所示。图中给出了四种plain
网络,它们的区别主要是网络深度不同。其中,输入图片尺寸 224x224 。
ResNet
简单的在plain
网络上添加快捷连接来实现。
FLOPs
:floating point operations
的缩写,意思是浮点运算量,用于衡量算法/模型的复杂度。
FLOPS
:floating point per second
的缩写,意思是每秒浮点运算次数,用于衡量计算速度。
相对于输入的feature map
,残差块的输出feature map
尺寸可能会发生变化:
-
输出
feature map
的通道数增加,此时需要扩充快捷连接的输出feature map
。否则快捷连接的输出feature map
无法和残差块的feature map
累加。有两种扩充方式:
- 直接通过 0 来填充需要扩充的维度。
- 通过
1x1
卷积来扩充维度。
-
输出
feature map
的尺寸减半。此时需要对快捷连接执行步长为 2 的池化/卷积:如果快捷连接已经采用1x1
卷积,则该卷积步长为2 ;否则采用步长为 2 的最大池化 。
1.3.2 模型预测能力
VGG-19 | 34层 plain 网络 | Resnet-34 | |
---|---|---|---|
计算复杂度(FLOPs) | 19.6 billion | 3.5 billion | 3.6 billion |
在ImageNet
验证集上执行10-crop
测试的结果。
A
类模型:快捷连接中,所有需要扩充的维度的填充 0 。B
类模型:快捷连接中,所有需要扩充的维度通过1x1
卷积来扩充。C
类模型:所有快捷连接都通过1x1
卷积来执行线性变换。
C
优于B
,B
优于A
。但是 C
引入更多的参数,相对于这种微弱的提升,性价比较低。所以后续的ResNet
均采用 B
类模型。
模型 | top-1 误差率 | top-5 误差率 |
---|---|---|
VGG-16 | 28.07% | 9.33% |
GoogleNet | - | 9.15% |
PReLU-net | 24.27% | 7.38% |
plain-34 | 28.54% | 10.02% |
ResNet-34 A | 25.03% | 7.76% |
ResNet-34 B | 24.52% | 7.46% |
ResNet-34 C | 24.19% | 7.40% |
ResNet-50 | 22.85% | 6.71% |
ResNet-101 | 21.75% | 6.05% |
ResNet-152 | 21.43% | 5.71% |
1.3.3 ResNet-18实现
import torch.nn as nn
import torch
from _06_Residual import Residual
class ResNet18(nn.Module):
def __init__(self):
super(ResNet18, self).__init__()
self.model = self.get_net()
def forward(self, X):
X = self.model(X)
return X
def get_net(self):
'''
ResNet的前两层跟GoogLeNet中的⼀样:
在输出通道数为64、步幅为2的7 × 7卷积层后,接步幅为2的3 × 3的最⼤汇聚层。
不同之处在于ResNet每个卷积层后增加了批量规范化层。
'''
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))
'''
GoogLeNet在后⾯接了4个由Inception块组成的模块。
ResNet则使⽤4个由残差块组成的模块,每个模块使⽤若⼲个同样输出通道数的残差块。
第⼀个模块的通道数同输⼊通道数⼀致。由于之前已经使⽤了步幅为2的最⼤汇聚层,所以⽆须减⼩⾼和宽。
之后的每个模块在第⼀个残差块⾥将上⼀个模块的通道数翻倍,并将⾼和宽减半。
'''
b2 = nn.Sequential(*self.resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*self.resnet_block(64, 128, 2))
b4 = nn.Sequential(*self.resnet_block(128, 256, 2))
b5 = nn.Sequential(*self.resnet_block(256, 512, 2))
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(), nn.Linear(512, 10))
return net
def resnet_block(self, 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
if __name__ == '__main__':
net = ResNet18()
X = torch.rand(size=(1, 1, 224, 224), dtype=torch.float32)
for layer in net.model:
X = layer(X)
print(layer.__class__.__name__, 'output shape:', X.shape)
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 128, 28, 28])
Sequential output shape: torch.Size([1, 256, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
Flatten output shape: torch.Size([1, 512])
Linear output shape: torch.Size([1, 10])
2 ResNet-18在Fashion-MNIST数据集上的应用示例
2.1 创建ResNet网络模型
如1.2.4及1.3.3代码所示。
2.2 读取Fashion-MNIST数据集
其他所有的函数,与经典神经网络(1)LeNet及其在Fashion-MNIST数据集上的应用完全一致。
batch_size = 256
# 为了使Fashion-MNIST上的训练短⼩精悍,将输⼊的⾼和宽从224降到96,简化计算
train_iter,test_iter = get_mnist_data(batch_size,resize=96)
2.3 在GPU上进行模型训练
from _06_ResNet18 import ResNet18
# 初始化模型
net = ResNet18()
lr, num_epochs = 0.05, 10
train_ch(net, train_iter, test_iter, num_epochs, lr, try_gpu())