用于图像识别的深度残差学习 (ResNet)

这是 PyTorch 对《用于图像识别的深度残差学习》论文的实现。

ResNet 将图层训练为残差函数以克服退化问题。退化问题是当层数变得非常高时,深度神经网络的精度会降低。随着层数的增加,精度会增加,然后饱和,然后开始降级。

该论文认为,深层模型的性能至少应与较浅的模型一样好,因为额外的层只能学会进行身份映射。

剩余学习

如果是需要几层学习的映射,他们会训练残差函数

相反。原来的函数变成

在这种情况下,学习身份映射等同于学会成为,后者更容易学习。

在参数化形式中,这可以写成,

当和的特征图大小不同时,论文建议使用已学到的权重进行线性投影

Paper 尝试用零填充代替线性投影,发现线性投影效果更好。此外,当要素图大小匹配时,他们发现身份映射比线性投影更好。

应该有多个层,否则总和也不会有非线性,就像线性图层一样。

以下是在 CIFAR-10 上训练 ResNet 的训练代码

55from typing import List, Optional
56
57import torch
58from torch import nn
59
60from labml_helpers.module import Module

用于快捷连接的线性投影

这是上面描述的投影。

63class ShortcutProjection(Module):
  • in_channels 是其中的频道数量
  • out_channels 是其中的频道数量
  • stride 是的卷积运算中的步长。我们在快捷方式连接上做同样的步伐,以匹配要素映射的大小。
70    def __init__(self, in_channels: int, out_channels: int, stride: int):
77        super().__init__()

线性投影的卷积层

80        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)

论文建议在每次卷积操作后添加批量归一化

82        self.bn = nn.BatchNorm2d(out_channels)
84    def forward(self, x: torch.Tensor):

卷积和批量归一化

86        return self.bn(self.conv(x))

剩余方块

这实现了本文中描述的残留块。它有两个卷积层。

Residual Block

第一个卷积图层映射从in_channelsout_channels ,其中高out_channels 于我们使用步幅缩小要素地图大小in_channels 时的值大于

第二个卷积图层从映射out_channelsout_channels ,步长始终为 1。

两个卷积层之后都是批量归一化。

89class ResidualBlock(Module):
  • in_channels 是其中的频道数量
  • out_channels 是输出声道的数量
  • stride 是卷积运算中的步长。
110    def __init__(self, in_channels: int, out_channels: int, stride: int):
116        super().__init__()

第一个卷积层,它映射到out_channels

119        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)

第一次卷积后的批量归一化

121        self.bn1 = nn.BatchNorm2d(out_channels)

第一个激活函数 (ReLU)

123        self.act1 = nn.ReLU()

第二个卷积层

126        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)

第二次卷积后的批量归一化

128        self.bn2 = nn.BatchNorm2d(out_channels)

如果步长不是,如果通道数发生变化,则快捷连接应为投影

132        if stride != 1 or in_channels != out_channels:

投影

134            self.shortcut = ShortcutProjection(in_channels, out_channels, stride)
135        else:

身份

137            self.shortcut = nn.Identity()

第二个激活功能(RelU)(添加快捷方式后)

140        self.act2 = nn.ReLU()
  • x 是形状的输入[batch_size, in_channels, height, width]
142    def forward(self, x: torch.Tensor):

获取快捷方式连接

147        shortcut = self.shortcut(x)

第一次卷积和激活

149        x = self.act1(self.bn1(self.conv1(x)))

第二次卷积

151        x = self.bn2(self.conv2(x))

添加快捷键后激活功能

153        return self.act2(x + shortcut)

瓶颈残留块

这实现了本文中描述的瓶颈块。它有、和卷积层。

Bottlenext Block

第一个卷积图层bottleneck_channels 使用卷积从映射in_channels 到,其中小bottleneck_channelsin_channels

第二个卷积图层从映射bottleneck_channelsbottleneck_channels 。这可以使步幅长度大于我们想要压缩要素贴图大小的步长。

第三个也是最后一个卷积层映射到out_channelsout_channels in_channels 如果步幅长度大于,则大于;否则等in_channels

bottleneck_channels 小于in_channels卷积是在这个缩小的空间上执行的(因此是瓶颈)。两个卷积会减少并增加通道的数量。

156class BottleneckResidualBlock(Module):
  • in_channels 是其中的频道数量
  • bottleneck_channels卷积的通道数
  • out_channels 是输出声道的数量
  • stride卷积运算中的步长。
184    def __init__(self, in_channels: int, bottleneck_channels: int, out_channels: int, stride: int):
191        super().__init__()

第一个卷积层,它映射到bottleneck_channels

194        self.conv1 = nn.Conv2d(in_channels, bottleneck_channels, kernel_size=1, stride=1)

第一次卷积后的批量归一化

196        self.bn1 = nn.BatchNorm2d(bottleneck_channels)

第一个激活函数 (ReLU)

198        self.act1 = nn.ReLU()

第二个卷积层

201        self.conv2 = nn.Conv2d(bottleneck_channels, bottleneck_channels, kernel_size=3, stride=stride, padding=1)

第二次卷积后的批量归一化

203        self.bn2 = nn.BatchNorm2d(bottleneck_channels)

第二个激活函数 (ReLU)

205        self.act2 = nn.ReLU()

第三个卷积层,映射到out_channels

208        self.conv3 = nn.Conv2d(bottleneck_channels, out_channels, kernel_size=1, stride=1)

第二次卷积后的批量归一化

210        self.bn3 = nn.BatchNorm2d(out_channels)

如果步长不是,如果通道数发生变化,则快捷连接应为投影

214        if stride != 1 or in_channels != out_channels:

投影

216            self.shortcut = ShortcutProjection(in_channels, out_channels, stride)
217        else:

身份

219            self.shortcut = nn.Identity()

第二个激活功能(RelU)(添加快捷方式后)

222        self.act3 = nn.ReLU()
  • x 是形状的输入[batch_size, in_channels, height, width]
224    def forward(self, x: torch.Tensor):

获取快捷方式连接

229        shortcut = self.shortcut(x)

第一次卷积和激活

231        x = self.act1(self.bn1(self.conv1(x)))

第二次卷积和激活

233        x = self.act2(self.bn2(self.conv2(x)))

第三次卷积

235        x = self.bn3(self.conv3(x))

添加快捷键后激活功能

237        return self.act3(x + shortcut)

ResNet 模型

这是 resnet 模型的基础,没有最终的线性层和用于分类的 softmax。

resnet 由堆叠的残差块瓶颈残差块组成。要素地图的大小在经过几个方块的步长后减半。缩小要素图大小时,信道的数量会增加。最后,将要素地图平均合并以获得矢量制图表达。

240class ResNetBase(Module):
  • n_blocks 是每个要素图大小的区块数的列表。
  • n_channels 是每个要素映射大小的通道数。
  • bottlenecks 是瓶颈的渠道数量。如果是None ,则使用残差块
  • img_channels 是输入中的声道数。
  • first_kernel_size 是初始卷积层的内核大小
254    def __init__(self, n_blocks: List[int], n_channels: List[int],
255                 bottlenecks: Optional[List[int]] = None,
256                 img_channels: int = 3, first_kernel_size: int = 7):
265        super().__init__()

每个要素映射大小的区块数和通道数

268        assert len(n_blocks) == len(n_channels)

如果使用瓶颈残差块,则应为每个要素映射大小提供瓶颈中的通道数

271        assert bottlenecks is None or len(bottlenecks) == len(n_channels)

初始卷积层从映射img_channels 到第一个残差块中的通道数 (n_channels[0] )

275        self.conv = nn.Conv2d(img_channels, n_channels[0],
276                              kernel_size=first_kernel_size, stride=2, padding=first_kernel_size // 2)

初始卷积后的批量范数

278        self.bn = nn.BatchNorm2d(n_channels[0])

区块清单

281        blocks = []

来自上一层(或块)的通道数

283        prev_channels = n_channels[0]

循环浏览每个要素地图大小

285        for i, channels in enumerate(n_channels):

新要素地图大小的第一个方块的步幅长度将为除第一个方块外

288            stride = 2 if len(blocks) == 0 else 1
289
290            if bottlenecks is None:

从映射prev_channels 到的残差块channels

292                blocks.append(ResidualBlock(prev_channels, channels, stride=stride))
293            else:

从映射到prev_channels瓶颈残差块channels

296                blocks.append(BottleneckResidualBlock(prev_channels, bottlenecks[i], channels,
297                                                      stride=stride))

更改频道数量

300            prev_channels = channels

添加其余方块-特征图大小或频道没有变化

302            for _ in range(n_blocks[i] - 1):
303                if bottlenecks is None:
305                    blocks.append(ResidualBlock(channels, channels, stride=1))
306                else:
308                    blocks.append(BottleneckResidualBlock(channels, bottlenecks[i], channels, stride=1))

堆叠方块

311        self.blocks = nn.Sequential(*blocks)
  • x 有形状[batch_size, img_channels, height, width]
313    def forward(self, x: torch.Tensor):

初始卷积和批量归一化

319        x = self.bn(self.conv(x))

残留(或瓶颈)块

321        x = self.blocks(x)

x 从形状改[batch_size, channels, h, w][batch_size, channels, h * w]

323        x = x.view(x.shape[0], x.shape[1], -1)

全球平均汇集

325        return x.mean(dim=-1)