这是 PyTorch 对《用于图像识别的深度残差学习》论文的实现。
ResNet 将图层训练为残差函数以克服退化问题。退化问题是当层数变得非常高时,深度神经网络的精度会降低。随着层数的增加,精度会增加,然后饱和,然后开始降级。
该论文认为,深层模型的性能至少应与较浅的模型一样好,因为额外的层只能学会进行身份映射。
如果是需要几层学习的映射,他们会训练残差函数
相反。原来的函数变成。
在这种情况下,学习身份映射等同于学会成为,后者更容易学习。
在参数化形式中,这可以写成,
当和的特征图大小不同时,论文建议使用已学到的权重进行线性投影。
Paper 尝试用零填充代替线性投影,发现线性投影效果更好。此外,当要素图大小匹配时,他们发现身份映射比线性投影更好。
应该有多个层,否则总和也不会有非线性,就像线性图层一样。
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))
这实现了本文中描述的残留块。它有两个卷积层。
第一个卷积图层映射从in_channels
到out_channels
,其中高out_channels
于我们使用步幅缩小要素地图大小in_channels
时的值大于。
第二个卷积图层从映射out_channels
到out_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)
Shortcut connection should be a projection if the stride length is not or if the number of channels change
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)
这实现了本文中描述的瓶颈块。它有、和卷积层。
第一个卷积图层bottleneck_channels
使用卷积从映射in_channels
到,其中小bottleneck_channels
于in_channels
。
第二个卷积图层从映射bottleneck_channels
到bottleneck_channels
。这可以使步幅长度大于我们想要压缩要素贴图大小的步长。
第三个也是最后一个卷积层映射到out_channels
。out_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)
Shortcut connection should be a projection if the stride length is not or if the number of channels change
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 模型的基础,没有最终的线性层和用于分类的 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:
292 blocks.append(ResidualBlock(prev_channels, channels, stride=stride))
293 else:
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:
堆叠方块
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)