2015年,何恺明等人在《Deep Residual Learning for Image Recognition》中提出了ResNet的新型网络结构,ResNet一经提出便以接连斩获ImageNet目标检测、图像分类,COCO目标检测、图像分割比赛的多项冠军,为深层网络模型的训练开辟了新的思路。

从文章中看

深度卷积神经网络的不断发展为图像分类带来了一系列突破,研究表明,网络深度的增加可以使模型学到更深层的图像特征,而这对于许多视觉任务来说是至关重要的。但随着层数的加深,模型的训练变得愈加困难,甚至出现了深层网络在训练和测试上的效果不如浅层网络的情况,作者在文中给出了20层网络和56层网络的训练和测试误差情况,如下图所示。

resnet python实现代码_resnet python实现代码

面对这一问题,作者对传统的网络架构进行了改进,由此提出了残差网络的设计思想,进而摆脱了深层网络面临的困境。

网络结构

ResNet的整体结构相较于传统的深层网络进入了残差块的结构,作者在文章中以34层网络为例,给出了传统网络与残差网络的设计差异。

resnet python实现代码_计算机视觉_02

从上图中可以看出,与传统网络相比,ResNet最大的特点是,在网络中的某些层的输入前,引入了其前层的输出。这一设计被称为残差块结构。

正是这一巧妙的设计,克服了深层网络的训练难题,作者在文中对残差网络的层数不断探索尝试,甚至提出了1202层的深层网络,均取得了较为良好的效果。

残差块是什么

既然残差块的设计如此巧妙,下面简单说说这一结构究竟是什么。单独将每一个残差块从整体网络中抽出,可以看到其抽象模型如下图所示。

resnet python实现代码_resnet python实现代码_03

作者将残差块的输出表示为H(x)=F(x)+x,其中F(x)表示残差映射,右侧标有x的曲线表示恒等映射。即:对于前层的输出x,在这一层的学习中,学习在前层中没有学到的F(x),学习到的F(x)加上前层的输出x,构成了这一层的输出H(x)。

理论上讲,残差块的设计解决了模型训练时的梯度消失问题。同时,当模型达到一定深度后,训练达到最优效果时,将残差映射的权重置为0,残差块的输出将只保留恒等映射,网络的性能便不会随着深度增加而降低。

为了训练更深层的网络,作者提到残差块的设计有以下两种形式。

resnet python实现代码_resnet python实现代码_04

通过引入1*1的卷积层(bottleneck)改变特征维度,进而减少深层网络训练时间。

PyTorch简易实现

作者在文中给出了不同层数残差网络的结构参数(如下图所示),考虑到现有GPU条件有限,在这里我们尝试实现最简单的ResNet-18,更深的网络实现步骤与ResNet-18类似,通过修改参数不断调试即可。

resnet python实现代码_pytorch_05

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》