1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
| from typing import Any, Callable, List, Optional, Type, Union
import torch
import torch.nn as nn
from torch import Tensor
# 定义3x3的卷积块
def conv3x3(in_planes: int, out_planes: int, stride: int = 1, groups: int = 1, dilation: int = 1) -> nn.Conv2d:
"""3x3 convolution with padding"""
return nn.Conv2d(
in_planes,
out_planes,
kernel_size=3,
stride=stride,
padding=dilation,
groups=groups,
bias=False,
dilation=dilation,
)
# 定义1x1的卷积块
def conv1x1(in_planes: int, out_planes: int, stride: int = 1) -> nn.Conv2d:
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
# ResNet18和34使用的残差块
class BasicBlock(nn.Module):
# 输出通道相对于输入通道的倍数
expansion: int = 1
def __init__(
self,
inplanes: int, # 输入通道数
planes: int, # 输出通道数
stride: int = 1, # 步幅
downsample: Optional[nn.Module] = None, # 分支为虚线时,使得分支输出与主干输出形状一致
groups: int = 1, # 分组卷积数量
base_width: int = 64, # 基础的通道数
dilation: int = 1, # 卷积时每间隔dilation取值,而不是连续取值
norm_layer: Optional[Callable[..., nn.Module]] = None, # 正则化层
) -> None:
super().__init__()
# 正则化层为None时,默认使用批量标准化层
if norm_layer is None:
norm_layer = nn.BatchNorm2d
# Basicblock不支持分组卷积和64之外的基础通道数
if groups != 1 or base_width != 64:
raise ValueError("BasicBlock only supports groups=1 and base_width=64")
# 也不支持dilation > 1
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
# 第一个3x3卷积层
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
# 第二个3x3卷积层
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
# 虚线的下采样层
self.downsample = downsample
self.stride = stride
def forward(self, x: Tensor) -> Tensor:
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class Bottleneck(nn.Module):
# Bottleneck in torchvision places the stride for downsampling at 3x3 convolution(self.conv2)
# while original implementation places the stride at the first 1x1 convolution(self.conv1)
# according to "Deep residual learning for image recognition"https://arxiv.org/abs/1512.03385.
# This variant is also known as ResNet V1.5 and improves accuracy according to
# https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch.
expansion: int = 4 # 即输出通道是输入通道的4倍
def __init__(
self,
inplanes: int,
planes: int,
stride: int = 1,
downsample: Optional[nn.Module] = None,
groups: int = 1,
base_width: int = 64, # 经过conv1后的输出通道,默认为64
# 空洞卷积只会应用在BasicBlock的第一个卷积,
# 或者是BottleneckBlock的第2个卷积。并且padding == dilation,使得输入输出特征图大小不变
dilation: int = 1,
norm_layer: Optional[Callable[..., nn.Module]] = None,
) -> None:
super().__init__()
# 正则化层
if norm_layer is None:
norm_layer = nn.BatchNorm2d
# 计算bottleneck网络中的宽度
width = int(planes * (base_width / 64.0)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
# 1x1卷积层
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
# 3x3卷积层
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
# 1x1卷积层
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x: Tensor) -> Tensor:
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
# 当分支是虚线连接时,需要进行1x1卷积,使得分支输出与主干输出形状一致
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(
self,
block: Type[Union[BasicBlock, Bottleneck]], # 使用的残差块
layers: List[int], # 传入一个整型列表,为每层的残差块数量
num_classes: int = 1000, # 分类类别
zero_init_residual: bool = False, # 是否初始化为0
groups: int = 1, # 分组卷积
width_per_group: int = 64, #
replace_stride_with_dilation: Optional[List[bool]] = None,
norm_layer: Optional[Callable[..., nn.Module]] = None,
) -> None:
super().__init__()
# 初始化正则化层
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
# 默认conv1的输出通道为64
self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError(
"replace_stride_with_dilation should be None "
f"or a 3-element tuple, got {replace_stride_with_dilation}"
)
# 分组卷积组数
self.groups = groups
# block中经过第一个卷积层后的输出通道
self.base_width = width_per_group
# 对应结构表中的conv1
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
# 对应conv1后的最大池化层
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 对应conv2.x,输入通道为64
self.layer1 = self._make_layer(block, 64, layers[0])
# 对应conv3.x,输入通道为128
self.layer2 = self._make_layer(block, 128, layers[1], stride=2, dilate=replace_stride_with_dilation[0])
# 对应conv4.x,输入通道为256
self.layer3 = self._make_layer(block, 256, layers[2], stride=2, dilate=replace_stride_with_dilation[1])
# 对应conv5.x,输入通道为512
self.layer4 = self._make_layer(block, 512, layers[3], stride=2, dilate=replace_stride_with_dilation[2])
# 对应conv5.x后的自适应平均池化层,使得输出大小为1x1
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
# 最后一层为全连接层,结合softmax输出概率分布
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
# 如果是卷积层,就使用kaiming_norm来初始化卷积层权重
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
# 如果是正则化层
# 则初始化权重为1
# 初始化偏置为0
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
# 初始化残差块最后一层的批量归一化层的权重为0,能提高0.2~0.3%的准确率
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck) and m.bn3.weight is not None:
nn.init.constant_(m.bn3.weight, 0) # type: ignore[arg-type]
elif isinstance(m, BasicBlock) and m.bn2.weight is not None:
nn.init.constant_(m.bn2.weight, 0) # type: ignore[arg-type]
# 构建conv1~5
def _make_layer(
self,
block: Type[Union[BasicBlock, Bottleneck]],
planes: int,
blocks: int,
stride: int = 1,
dilate: bool = False,
) -> nn.Sequential:
# 初始化正则化层
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
# 如果块的输入通道与输出通道不同 或 步幅不为1改变了大小
# 需要经过一个1x1的卷积层和正则化层来调整大小
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(
block(
self.inplanes, planes, stride, downsample, self.groups, self.base_width, previous_dilation, norm_layer
)
)
# 更新block的输出通道数,作为接下来block的输入通道数
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(
block(
self.inplanes,
planes,
groups=self.groups,
base_width=self.base_width,
dilation=self.dilation,
norm_layer=norm_layer,
)
)
return nn.Sequential(*layers)
# 实现正向传播方法
def _forward_impl(self, x: Tensor) -> Tensor:
# See note [TorchScript super()]
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
# 实现正向传播
def forward(self, x: Tensor) -> Tensor:
return self._forward_impl(x)
def _resnet(
block: Type[Union[BasicBlock, Bottleneck]],
layers: List[int],
**kwargs: Any,
) -> ResNet:
model = ResNet(block, layers, **kwargs)
return model
def resnet18(**kwargs: Any) -> ResNet:
return _resnet(BasicBlock, [2, 2, 2, 2], **kwargs)
def resnet34(**kwargs: Any) -> ResNet:
return _resnet(BasicBlock, [3, 4, 6, 3], **kwargs)
def resnet50(**kwargs: Any) -> ResNet:
return _resnet(Bottleneck, [3, 4, 6, 3], **kwargs)
def resnet101(**kwargs: Any) -> ResNet:
return _resnet(Bottleneck, [3, 4, 23, 3], **kwargs)
def resnet152(**kwargs: Any) -> ResNet:
return _resnet(Bottleneck, [3, 8, 36, 3], **kwargs)
|