这是论文《用于图像识别的深度残差学习》的 PyTorch 实现。
ResNets 将层训练为残余函数,以克服降级问题。退化问题是,当层数变得非常高时,深度神经网络的准确性会降低。精度随着层数的增加而提高,然后饱和,然后开始降低。
该论文认为,更深层次的模型的表现应该至少与较浅的模型一样好,因为额外的层只能学会执行身份映射。
如果映射需要通过几层学习,则它们会训练残差函数
相反。原来的函数变成。
在这种情况下,学习的身份映射等同于学习成为,后者更容易学习。
在参数化形式中,这可以写成
当和的特征图大小不同时,论文建议使用学习的权重进行线性投影。
P@@
aper 尝试了零填充而不是线性投影,发现线性投影效果更好。此外,当要素地图大小匹配时,他们发现身份映射比线性投影更好。
应该有多个层,否则总和也不会有非线性,就像线性层一样。
57from typing import List, Optional
58
59import torch
60from torch import nn
61
62from labml_helpers.module import Module65class ShortcutProjection(Module):in_channels
是其中的频道数量out_channels
是其中的频道数量stride
是的卷积运算中的步长。我们在快捷方式连接上做同样的步伐,以匹配要素映射的大小。72    def __init__(self, in_channels: int, out_channels: int, stride: int):79        super().__init__()线性投影的卷积层
82        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)论文建议在每次卷积操作后添加批量归一化
84        self.bn = nn.BatchNorm2d(out_channels)86    def forward(self, x: torch.Tensor):卷积和批量归一化
88        return self.bn(self.conv(x))这实现了本文中描述的残留块。它有两个卷积层。
第一个卷积图层映射从in_channels
到out_channels
,其中高out_channels
于我们使用步幅缩小要素地图大小in_channels
时的值大于。
第二个卷积图层从映射out_channels
到out_channels
,步长始终为 1。
两个卷积层之后都是批量归一化。
91class ResidualBlock(Module):in_channels
是其中的频道数量out_channels
是输出声道的数量stride
是卷积运算中的步长。112    def __init__(self, in_channels: int, out_channels: int, stride: int):118        super().__init__()第一个卷积层,它映射到out_channels
121        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)第一次卷积后的批量归一化
123        self.bn1 = nn.BatchNorm2d(out_channels)第一个激活函数 (ReLU)
125        self.act1 = nn.ReLU()第二个卷积层
128        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)第二次卷积后的批量归一化
130        self.bn2 = nn.BatchNorm2d(out_channels)如果步长不是,如果通道数发生变化,则快捷连接应为投影
134        if stride != 1 or in_channels != out_channels:投影
136            self.shortcut = ShortcutProjection(in_channels, out_channels, stride)
137        else:身份
139            self.shortcut = nn.Identity()第二个激活功能(RelU)(添加快捷方式后)
142        self.act2 = nn.ReLU()x
是形状的输入[batch_size, in_channels, height, width]
144    def forward(self, x: torch.Tensor):获取快捷方式连接
149        shortcut = self.shortcut(x)第一次卷积和激活
151        x = self.act1(self.bn1(self.conv1(x)))第二次卷积
153        x = self.bn2(self.conv2(x))添加快捷键后激活功能
155        return self.act2(x + shortcut)这实现了本文中描述的瓶颈块。它有、和卷积层。
第一个卷积图层bottleneck_channels
使用卷积从映射in_channels
到,其中小bottleneck_channels
于in_channels
。
第二个卷积图层从映射bottleneck_channels
到bottleneck_channels
。这可以使步幅长度大于我们想要压缩要素贴图大小的步长。
第三个也是最后一个卷积层映射到out_channels
。out_channels
in_channels
如果步幅长度大于,则大于;否则等于in_channels
。
bottleneck_channels
小于in_channels
,卷积是在这个缩小的空间上执行的(因此是瓶颈)。两个卷积会减少并增加通道的数量。
158class BottleneckResidualBlock(Module):in_channels
是其中的频道数量bottleneck_channels
是卷积的通道数out_channels
是输出声道的数量stride
是卷积运算中的步长。186    def __init__(self, in_channels: int, bottleneck_channels: int, out_channels: int, stride: int):193        super().__init__()第一个卷积层,它映射到bottleneck_channels
196        self.conv1 = nn.Conv2d(in_channels, bottleneck_channels, kernel_size=1, stride=1)第一次卷积后的批量归一化
198        self.bn1 = nn.BatchNorm2d(bottleneck_channels)第一个激活函数 (ReLU)
200        self.act1 = nn.ReLU()第二个卷积层
203        self.conv2 = nn.Conv2d(bottleneck_channels, bottleneck_channels, kernel_size=3, stride=stride, padding=1)第二次卷积后的批量归一化
205        self.bn2 = nn.BatchNorm2d(bottleneck_channels)第二个激活函数 (ReLU)
207        self.act2 = nn.ReLU()第三个卷积层,映射到out_channels
。
210        self.conv3 = nn.Conv2d(bottleneck_channels, out_channels, kernel_size=1, stride=1)第二次卷积后的批量归一化
212        self.bn3 = nn.BatchNorm2d(out_channels)如果步长不是,如果通道数发生变化,则快捷连接应为投影
216        if stride != 1 or in_channels != out_channels:投影
218            self.shortcut = ShortcutProjection(in_channels, out_channels, stride)
219        else:身份
221            self.shortcut = nn.Identity()第二个激活功能(RelU)(添加快捷方式后)
224        self.act3 = nn.ReLU()x
是形状的输入[batch_size, in_channels, height, width]
226    def forward(self, x: torch.Tensor):获取快捷方式连接
231        shortcut = self.shortcut(x)第一次卷积和激活
233        x = self.act1(self.bn1(self.conv1(x)))第二次卷积和激活
235        x = self.act2(self.bn2(self.conv2(x)))第三次卷积
237        x = self.bn3(self.conv3(x))添加快捷键后激活功能
239        return self.act3(x + shortcut)这是 resnet 模型的基础,没有最终的线性层和用于分类的 softmax。
resnet 由堆叠的残差块或瓶颈残差块组成。要素地图的大小在经过几个方块的步长后减半。缩小要素图大小时,信道的数量会增加。最后,将要素地图平均合并以获得矢量制图表达。
242class ResNetBase(Module):n_blocks
是每个要素图大小的区块数的列表。n_channels
是每个要素映射大小的通道数。bottlenecks
是瓶颈的渠道数量。如果是None
,则使用残差块。img_channels
是输入中的声道数。first_kernel_size
是初始卷积层的内核大小256    def __init__(self, n_blocks: List[int], n_channels: List[int],
257                 bottlenecks: Optional[List[int]] = None,
258                 img_channels: int = 3, first_kernel_size: int = 7):267        super().__init__()每个要素映射大小的区块数和通道数
270        assert len(n_blocks) == len(n_channels)273        assert bottlenecks is None or len(bottlenecks) == len(n_channels)初始卷积层从映射img_channels
到第一个残差块中的通道数 (n_channels[0]
)
277        self.conv = nn.Conv2d(img_channels, n_channels[0],
278                              kernel_size=first_kernel_size, stride=2, padding=first_kernel_size // 2)初始卷积后的批量范数
280        self.bn = nn.BatchNorm2d(n_channels[0])区块清单
283        blocks = []来自上一层(或块)的通道数
285        prev_channels = n_channels[0]循环浏览每个要素地图大小
287        for i, channels in enumerate(n_channels):新要素地图大小的第一个方块的步幅长度将为除第一个方块外
290            stride = 2 if len(blocks) == 0 else 1
291
292            if bottlenecks is None:294                blocks.append(ResidualBlock(prev_channels, channels, stride=stride))
295            else:298                blocks.append(BottleneckResidualBlock(prev_channels, bottlenecks[i], channels,
299                                                      stride=stride))更改频道数量
302            prev_channels = channels添加其余方块-特征图大小或频道没有变化
304            for _ in range(n_blocks[i] - 1):
305                if bottlenecks is None:堆叠方块
313        self.blocks = nn.Sequential(*blocks)x
有形状[batch_size, img_channels, height, width]
315    def forward(self, x: torch.Tensor):初始卷积和批量归一化
321        x = self.bn(self.conv(x))残留(或瓶颈)块
323        x = self.blocks(x)x
从形状改[batch_size, channels, h, w]
为[batch_size, channels, h * w]
325        x = x.view(x.shape[0], x.shape[1], -1)全球平均汇集
327        return x.mean(dim=-1)