1. 辅助函数(helpers)
@des: 封装nn.Conv2D,便捷地生成步幅可控的3×3卷积块。
@p1: 输出通道数
@p2: 步幅
@p3: 输入通道数
def _conv3x3(channels, stride, in_channels):
return nn.Conv2D(channels, kernel_size=3, strides=stride, padding=1,
use_bias=False, in_channels=in_channels)
2. 基本残差单元
一共分为四种,用于resnet18、resnet34的BasicBlock和用于resnet50、resnet101、resnet152的BottleneckV1。以及根据残差块的结构分为V1和V2。
resnet结构参见:resnet-n
残差块结构参见:resnetV1 and resnetV2
在gluoncv源码中采用4个类进行表示。下面介绍其中一个,其他类似。
@des: cov3*3 + BN + Relu + conv3*3 + BN
@p1: 输出通道数
@p2: 首个小2d卷积的步幅(决定了输出形状)
@p3: 是否降采样(当输入和输出通道数不相等的时候需要)
@p4: 输入通道数(默认为0,表示根据输入自适应)
@p5: trick
@p6: trick
@p7: Normalization层
@p8: Normalization层参数
class BasicBlockV1(HybridBlock):
def __init__(self, channels, stride, downsample=False, in_channels=0,
last_gamma=False, use_se=False, norm_layer=BatchNorm, norm_kwargs=None, **kwargs):
super(BasicBlockV1, self).__init__(**kwargs)
self.body = nn.HybridSequential(prefix='')
##--第一个卷积块是可指定步幅的,也就决定了输出的尺寸。
self.body.add(_conv3x3(channels, stride, in_channels))
self.body.add(norm_layer(**({} if norm_kwargs is None else norm_kwargs)))
self.body.add(nn.Activation('relu'))
self.body.add(_conv3x3(channels, 1, channels))
##--多半是个trick
if not last_gamma:
self.body.add(norm_layer(**({} if norm_kwargs is None else norm_kwargs)))
else:
self.body.add(norm_layer(gamma_initializer='zeros',
**({} if norm_kwargs is None else norm_kwargs)))
##--多半又是个trick
if use_se:
self.se = nn.HybridSequential(prefix='')
self.se.add(nn.Dense(channels // 16, use_bias=False))
self.se.add(nn.Activation('relu'))
self.se.add(nn.Dense(channels, use_bias=False))
self.se.add(nn.Activation('sigmoid'))
else:
self.se = None
##--当上述第一个3*3的卷积块stride不是1时,就需要这样一个降采样层去将分支大小变成一致,之后才可以相加。
if downsample:
self.downsample = nn.HybridSequential(prefix='')
self.downsample.add(nn.Conv2D(channels, kernel_size=1, strides=stride,
use_bias=False, in_channels=in_channels))
self.downsample.add(norm_layer(**({} if norm_kwargs is None else norm_kwargs)))
else:
self.downsample = None
2. 网络(Nets)
一个ResNet网络由多个卷积层(layers)构成,一个卷积层由多个残差块(block)构成,一个残差块由多个残差基本单元(BasicBlock)构成。
@des: 根据不同的resnet结构构建不同的Net
@p1: 残差基本单元类型
@p2: 每个卷积层内(总共4个)含有的残差块数量(列表)
@p3: 每个卷积层输出的通道数(列表)
@p4: 最终的分类数
@p5: trick
@p6: trick
@p7: trick
@p8: Normalization层
@p8: Normalization层参数
class ResNetV1(HybridBlock):
def __init__(self, block, layers, channels, classes=1000, thumbnail=False,
last_gamma=False, use_se=False, norm_layer=BatchNorm, norm_kwargs=None, **kwargs):
super(ResNetV1, self).__init__(**kwargs)
##--第一个channel为第一个残差block的输入通道
assert len(layers) == len(channels) - 1
with self.name_scope():
self.features = nn.HybridSequential(prefix='')
if thumbnail:
self.features.add(_conv3x3(channels[0], 1, 0))
else:
self.features.add(nn.Conv2D(channels[0], 7, 2, 3, use_bias=False))
self.features.add(norm_layer(**({} if norm_kwargs is None else norm_kwargs)))
self.features.add(nn.Activation('relu'))
self.features.add(nn.MaxPool2D(3, 2, 1))
for i, num_layer in enumerate(layers):
##--残差块步幅stage1不会改变数据形状,其他stage则会缩小数据形状并增加通道数。
stride = 1 if i == 0 else 2
self.features.add(self._make_layer(block, num_layer, channels[i+1],
stride, i+1, in_channels=channels[i],
last_gamma=last_gamma, use_se=use_se,
norm_layer=norm_layer, norm_kwargs=norm_kwargs))
self.features.add(nn.GlobalAvgPool2D())
self.output = nn.Dense(classes, in_units=channels[-1])
@des: 构建卷积层
@p1: 残差基本单元类型
@p2: 该卷积层残差块数量
@p3: 该卷积层输出通道数
@p4: 残差块首个残差基本单元的输入strid
@p5: 卷积层的一个序列,每个卷积层是一个stage。
@p6: 该卷积层输入通道数
@p7: trick
@p8: trick
@p9: Normalization层
@p10: Normalization层参数
def _make_layer(self, block, layers, channels, stride, stage_index, in_channels=0,
last_gamma=False, use_se=False, norm_layer=BatchNorm, norm_kwargs=None):
layer = nn.HybridSequential(prefix='stage%d_'%stage_index)
with layer.name_scope():
##--首个残差基本单元使用可控步幅用以调整数据形状。
layer.add(block(channels, stride, channels != in_channels, in_channels=in_channels,
last_gamma=last_gamma, use_se=use_se, prefix='',
norm_layer=norm_layer, norm_kwargs=norm_kwargs))
##--后面的残差基本单元都使用固定步幅,因此数据形状不发生改变
for _ in range(layers-1):
layer.add(block(channels, 1, False, in_channels=channels,
last_gamma=last_gamma, use_se=use_se, prefix='',
norm_layer=norm_layer, norm_kwargs=norm_kwargs))
return layer
def hybrid_forward(self, F, x):
x = self.features(x)
x = self.output(x)
return x
3. 指定方式
1、构建resnet_spec字典,可以用resnet-n的编号指定不同resnet的网络结构。
2、构建resnet versions列表指定版本。这样idx=0则是版本1,idx=1则是版本2。
3、构建block versions字典列表,此时resnet_spec字典的第一个value可以作为key。
resnet_spec = {18: ('basic_block', [2, 2, 2, 2], [64, 64, 128, 256, 512]),
34: ('basic_block', [3, 4, 6, 3], [64, 64, 128, 256, 512]),
50: ('bottle_neck', [3, 4, 6, 3], [64, 256, 512, 1024, 2048]),
101: ('bottle_neck', [3, 4, 23, 3], [64, 256, 512, 1024, 2048]),
152: ('bottle_neck', [3, 8, 36, 3], [64, 256, 512, 1024, 2048])}
resnet_net_versions = [ResNetV1, ResNetV2]
resnet_block_versions = [{'basic_block': BasicBlockV1, 'bottle_neck': BottleneckV1},
{'basic_block': BasicBlockV2, 'bottle_neck': BottleneckV2}]
4. 构造网络
@des: 构建残差网络
@p1: 残差网络版本-resnet_net_versions列表中指定
@p2: 层数-resnet_spec字典中指定
@p3: 该卷积层输出通道数
@p4: 残差块首个残差基本单元的输入strid
@p5: 卷积层的一个序列,每个卷积层是一个stage。
@p6: 该卷积层输入通道数
def get_resnet(version, num_layers, pretrained=False, ctx=cpu(),
root='~/.mxnet/models', use_se=False, **kwargs):
assert num_layers in resnet_spec, \
"Invalid number of layers: %d. Options are %s"%(
num_layers, str(resnet_spec.keys()))
##--数据结构的巧妙!仅仅通过一个int就指定了3个参数!
block_type, layers, channels = resnet_spec[num_layers]
assert 1 <= version <= 2, \
"Invalid resnet version: %d. Options are 1 and 2."%version
resnet_class = resnet_net_versions[version-1]
block_class = resnet_block_versions[version-1][block_type]
##--使用网络类构建网络
net = resnet_class(block_class, layers, channels, use_se=use_se, **kwargs)
##--加载resnet的预训练参数
if pretrained:
from .model_store import get_model_file
if not use_se:
##--首先在root目录中要有为resnet%d_v%d'%(num_layers, version)-short_hash.params文件
##--get_model_file()首先会根据传参的名字生成一个short_hash,结合成上述文件名。
##--将文件名和预设的sha1对比,通过则返回该文件路径,否则从网络上下载。
net.load_parameters(get_model_file('resnet%d_v%d'%(num_layers, version),
tag=pretrained, root=root), ctx=ctx)
else:
net.load_parameters(get_model_file('se_resnet%d_v%d'%(num_layers, version),
tag=pretrained, root=root), ctx=ctx)
##--社么鬼
from ..data import ImageNet1kAttr
attrib = ImageNet1kAttr()
net.synset = attrib.synset
net.classes = attrib.classes
net.classes_long = attrib.classes_long
return net
5. 定向接口
没什么好解释的
def resnet18_v1(**kwargs):
return get_resnet(1, 18, use_se=False, **kwargs)
..