Deep Residual Learning for Image Recognition
1. 简介
《Deep Residual Learning for Image Recognition》是2015年由何凯明等人提出的一篇论文,该论文提出了一种新的深度神经网络模型——残差网络(ResNet),该模型在当时在多个计算机视觉任务中均取得了最先进的性能表现。在本篇阅读笔记中,我将深入阅读这篇论文,并总结出其主要贡献、方法和实验结果。
2.背景和主要贡献
深度神经网络在计算机视觉领域中取得了重大的进展,但是由于网络深度的增加,出现了梯度消失和梯度爆炸的问题。在此背景下,本文提出了一种新的深度神经网络结构——残差网络(ResNet),该结构可以有效地解决梯度消失和梯度爆炸问题,并可以让网络达到更深的层数,从而提高网络的性能表现。
该论文的主要贡献如下:
提出了残差学习的概念,通过引入残差块来构建更深的神经网络模型,并且证明了残差学习可以有效地解决梯度消失和梯度爆炸的问题。
通过在ImageNet数据集上的实验验证了残差网络的优越性能,达到了当时最先进的结果。
3.方法
3.1 残差块
传统的卷积神经网络通常是通过堆叠多个卷积层和池化层来构建的。而残差网络则是在这些卷积层和池化层之间添加了一些残差块(residual block),如下图所示。
如图所示,残差块由两个卷积层和一个跨越连接(shortcut connection)组成。传统的卷积神经网络中,每个卷积层的输入都是上一层的输出,即\(h_l=f(h_{l-1})\),其中\(f(\cdot)\)是该层的特定函数,\(h_{l-1}\)和\(h_{l}\)分别表示上一层和当前层的特征图。而在残差网络中,我们不仅要学习到一个映射\(f(\cdot)\),还要学习到一个残差映射,即\(h_l=f(h_{l-1})+h_{l-1}\)。残差块的作用就是将\(h_{l-1}\)加到\(f(h_{l-1})\)上,从而得到\(h_l\)。这个跨越连接的作用是将输入特征图\(h_{l-1}\)直接添加到输出特征图\(h_l\)中,从而构建一个残差映射。这样可以避免在深度神经网络中出现梯度消失和梯度爆炸的问题,同时可以使得网络更深。
3.2 残差网络结构
在残差网络中,作者提出了两种不同的残差块结构,分别是普通的残差块和瓶颈残差块。其中普通的残差块包含两个 \(3\times3\) 的卷积层,每个卷积层后面都跟着一个 Batch Normalization 和 ReLU 激活函数。瓶颈残差块则由三个卷积层组成,分别是一个 \(1\times1\) 的卷积层、一个 \(3\times3\) 的卷积层和一个 \(1\times1\) 的卷积层,这样可以降低计算复杂度,同时保持相同的网络深度。
在实际应用中,作者采用了一个具有34层的残差网络(ResNet-34)和一个具有152层的残差网络(ResNet-152)。其中ResNet-34采用了普通的残差块,而ResNet-152采用了瓶颈残差块。作者还提出了一个比较特殊的残差网络结构——ResNet-50,该网络结构结合了普通的残差块和瓶颈残差块,共有50层。
3.3 全局平均池化层
在传统的卷积神经网络中,通常会在最后一层使用全连接层进行分类。然而,在深度神经网络中,全连接层的参数数量非常多,容易过拟合。为了避免这个问题,作者提出了使用全局平均池化层代替全连接层,全局平均池化层的作用是将最后一个卷积层的输出特征图转换为一个向量,然后直接将该向量输入到softmax分类器中进行分类。这样可以大大减少网络的参数数量,从而降低过拟合的风险。
4. 实验结果
从图中可以看出,ResNet在测试集上的Top-1和Top-5的错误率均低于其他网络。尤其是在网络深度较大的情况下,ResNet相对于其他网络的性能提升非常明显。
5.核心网络架构代码实现(pytorch)
import torch
import torch.nn as nn
# ResNet Basic Block
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(in_channels=out_channels, out_channels=out_channels,
kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
identity = self.shortcut(identity)
out += identity
out = self.relu(out)
return out
# ResNet Bottleneck Block
class Bottleneck(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernel_size=1, stride=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(in_channels=out_channels, out_channels=out_channels,
kernel_size=3, stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.conv3 = nn.Conv2d(in_channels=out_channels, out_channels=out_channels * 4,
kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels * 4)
self.relu = nn.ReLU(inplace=True)
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels * 4:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels * 4,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels * 4)
)
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
identity = self.shortcut(identity)
out += identity
out = self.relu(out)
return out
# ResNet
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
super(ResNet, self).__init__()
self.in_channels = 64
self.conv1 = nn.Conv2d(in_channels=3, out_channels=64,
kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self.make_layer(block, out_channels=64, num_blocks=layers[0], stride=1)
self.layer2 = self.make_layer(block, out_channels=128, num_blocks=layers[1], stride=2)
self.layer3 = self.make_layer(block, out_channels=256, num_blocks=layers[2], stride=2)
self.layer4 = self.make_layer(block, out_channels=512, num_blocks=layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
def make_layer(self, block, out_channels, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1)
layers = []
for stride in strides:
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels * block.expansion
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.reshape(x.shape[0], -1)
x = self.fc(x)
return x
6.总结
本文介绍了深度残差网络(ResNet)在图像识别中的应用。ResNet通过引入跨越连接和残差块结构,解决了深度神经网络中的梯度消失和梯度爆炸问题,并且在网络深度较大的情况下仍然能够保持较高的分类性能。此外,作者还提出了全局平均池化层代替全连接层,从而大大减少网络的参数数量,降低过拟合的风险。
总的来说,ResNet的提出对深度神经网络的发展具有重要的意义,不仅在图像识别中取得了非常好的性能,而且在其他领域的应用也取得了很多进展。