2015年,何恺明等人在《Deep Residual Learning for Image Recognition》中提出了ResNet的新型网络结构,ResNet一经提出便以接连斩获ImageNet目标检测、图像分类,COCO目标检测、图像分割比赛的多项冠军,为深层网络模型的训练开辟了新的思路。
从文章中看
深度卷积神经网络的不断发展为图像分类带来了一系列突破,研究表明,网络深度的增加可以使模型学到更深层的图像特征,而这对于许多视觉任务来说是至关重要的。但随着层数的加深,模型的训练变得愈加困难,甚至出现了深层网络在训练和测试上的效果不如浅层网络的情况,作者在文中给出了20层网络和56层网络的训练和测试误差情况,如下图所示。
面对这一问题,作者对传统的网络架构进行了改进,由此提出了残差网络的设计思想,进而摆脱了深层网络面临的困境。
网络结构
ResNet的整体结构相较于传统的深层网络进入了残差块的结构,作者在文章中以34层网络为例,给出了传统网络与残差网络的设计差异。
从上图中可以看出,与传统网络相比,ResNet最大的特点是,在网络中的某些层的输入前,引入了其前层的输出。这一设计被称为残差块结构。
正是这一巧妙的设计,克服了深层网络的训练难题,作者在文中对残差网络的层数不断探索尝试,甚至提出了1202层的深层网络,均取得了较为良好的效果。
残差块是什么
既然残差块的设计如此巧妙,下面简单说说这一结构究竟是什么。单独将每一个残差块从整体网络中抽出,可以看到其抽象模型如下图所示。
作者将残差块的输出表示为H(x)=F(x)+x,其中F(x)表示残差映射,右侧标有x的曲线表示恒等映射。即:对于前层的输出x,在这一层的学习中,学习在前层中没有学到的F(x),学习到的F(x)加上前层的输出x,构成了这一层的输出H(x)。
理论上讲,残差块的设计解决了模型训练时的梯度消失问题。同时,当模型达到一定深度后,训练达到最优效果时,将残差映射的权重置为0,残差块的输出将只保留恒等映射,网络的性能便不会随着深度增加而降低。
为了训练更深层的网络,作者提到残差块的设计有以下两种形式。
通过引入1*1的卷积层(bottleneck)改变特征维度,进而减少深层网络训练时间。
PyTorch简易实现
作者在文中给出了不同层数残差网络的结构参数(如下图所示),考虑到现有GPU条件有限,在这里我们尝试实现最简单的ResNet-18,更深的网络实现步骤与ResNet-18类似,通过修改参数不断调试即可。
import torch
from torch import nn
from torch.nn import functional as F
from torch import optim
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
import torchvision.transforms as transforms
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, strides=1):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=strides, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
self.shortcut = None
if strides != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=strides, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.bn1(self.conv1(x))
out = F.relu(out)
out = self.bn2(self.conv2(out))
if self.shortcut:
x = self.shortcut(x)
out += x
out = F.relu(out)
return out
class ResNet18(nn.Module):
def __init__(self):
super(ResNet18, self).__init__()
self.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3)
self.bn1 = nn.BatchNorm2d(64)
self.pool1 = nn.MaxPool2d(kernel_size=3, padding=1)
self.layer1 = self.make_layer(64, 64, 2, strides=1)
self.layer2 = self.make_layer(64, 128, 2, strides=2)
self.layer3 = self.make_layer(128, 256, 2, strides=2)
self.layer4 = self.make_layer(256, 512, 2, strides=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, 10)
def make_layer(self, in_channels, out_channels, num_blocks, strides=1):
layers = []
layers.append(ResidualBlock(in_channels, out_channels, strides=strides))
for i in range(num_blocks - 1):
layers.append(ResidualBlock(out_channels, out_channels, strides=strides))
return nn.Sequential(*layers)
def forward(self, x):
out = self.conv1(x)
out = self.bn1(out)
out = nn.ReLU(inplace=True)(out)
out = self.pool1(out)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.avgpool(out)
out = out.view(out.size(0), -1)
out = self.fc(out)
return out
# 定义数据预处理函数
transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
])
# 加载训练集和验证集
train_dataset = MNIST(root='../data/', train=True, transform=transform, download=True)
validation_dataset = MNIST(root='../data/', train=False, transform=transform)
batch_size = 32
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
validation_loader = DataLoader(dataset=validation_dataset, batch_size=batch_size, shuffle=False)
model = ResNet18()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# 训练模型
num_epochs = 10
for epoch in range(num_epochs):
for i, (inputs, labels) in enumerate(train_loader):
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 打印训练进度
if (i + 1) % 100 == 0:
print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch + 1,
num_epochs, i + 1,
len(train_loader),
loss.item()))
# 在验证集上测试模型
with torch.no_grad():
correct = 0
total = 0
for inputs, labels in validation_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / total
print('Accuracy of the network on the validation images: %d %%' % (accuracy))
参考文献
《Deep Residual Learning for Image Recognition》《Dive Into Deep Learning》