前面的几篇博客都是在介绍如何准备数据。实际上,在Fast AI
框架下,数据准备好后,构建深度神经网络学习器的任务已经完成了80%
(手动狗头)。其余的10%
是构建网络,并将网络和数据封装成学习器(即Fast AI
中的Learner
对象);还有10%
是对训练过程和结果的分析。后面的10%
主要依赖于Fast AI
中的回调系统(各种Callbacks
),这部分会在后续博文中涉及。而本篇博文主要介绍如何构建学习器。相关文档见文档链接,代码见fastai.vision.learner.py
。
cnn_learner()
方法cnn_learner()
是基于预训练的网络,自动构建分类器的工厂类方法:
cnn_learner(
data:DataBunch, # 传入的数据包
base_arch:Callable, # 要进行Finetune的主干网络
cut:Union[int, Callable]=None, # 在哪一层切割网络
pretrained:bool=True, # 是否使用预训练的模型
lin_ftrs:Optional[Collection[int]]=None, # 添加的头部子网络中的线性层的特征数目。
ps:Floats=0.5, # 添加的头部子网络中的dropout层的丢弃概率
custom_head:Optional[Module]=None, # 自定义的头部子网络
split_on:Union[Callable, Collection[ModuleList], NoneType]=None,
bn_final:bool=False, # 在分类之前是否添加Batch Normalization层
init='kaiming_normal_', # 添加的头部子网络的初始化方法
concat_pool:bool=True,
**kwargs:Any) → Learner
上述函数看起来很复杂,但其实大部分参数使用默认值即可。比如要使用Resnet34
作为主干网络:
learn = cnn_learner(data, models.resnet34)
cnn_learner()
函数中的主要流程如下:
cnn_learner()
会调用create_cnn_model()
来构建网络。该函数首先从指定的网络结构中将主干网络分割出来,所依据的是cut
参数。如果是Fast AI
库内置的网络结构,cut
参数有默认值,存储在model_meta
字典中;否则,会尝试在网络结构的最后一个包含pooling
层的模块处进行切割。实际上,Fast AI
中的所谓内置的网络,其实都是引用了torchvision
中的实现(所以关键的网络配置参数,其实只有一个pretrained
,用于表示网络是否使用预训练的模型);而Fast AI
只是提供了model_meta
中的两个关键参数:一个就是本处所说的cut
,用于决定torchvision
里的model
哪一部分是用于提取特征的主干网络(这一部分会得以保留),哪一部分是用于进行分类的头部子网络(header
,这一部分将会被替换);另一个关键参数是split
,本篇博文后续会介绍。
切割出主干网后,create_cnn_model()
添加头部(head
)子网络。如果传入cnn_learner()
的参数中包含custom_head
,则会使用所指定的头部子网络;否则,create_cnn_model()
函数会自动添加如下结构作为头部子网络:
AdaptiveConcatPool2d
层,该层将对主干网输出的特征分别使用MaxPool()
和AvgPool()
,并将所得特征拼接在一起。Flatten
层,该层将特征整合为向量形式。[BatchNorm1d, Dropout, Linear, (ReLU)]
模块,最后一个模块不包含ReLU
层。其中中间的Linear
层的结点数为512
。该值以及[BatchNorm1d, Dropout, Linear, (ReLU)]
模块的数目均可通过设定lin_ftrs
参数来调整。如设置lin_ftrs=[128, 128]
,则会有三个相应的模块,且其中两个Linear
层的结点数均为128
。
如下为一个使用自定义的头部的示例:
head = nn.Sequential(
AdaptiveConcatPool2d(),
Flatten(),
nn.Linear(n_feature, n_class)
)
learn = cnn_learner(data, models.resnet34, custom_head=head)
其中AdaptiveConcatPool2d()
和Flatten()
都是Fast AI
提供的层,作用分别是将主干网提取的feature map
做global pooling
(即将 N × N × C N\times N\times C N×N×C的feature map
通过Max Pooling
和Avg Pooling
变成 1 × 1 × 2 C 1\times 1\times 2C 1×1×2C的特征),和将特征扯直(即将通过AdaptiveConcatPool2d()
所得的 1 × 1 × 2 C 1\times 1\times 2C 1×1×2C特征变为一个向量)。
最终的网络模型可通过访问learn.model
查看,其为一个nn.Sequential
对象,所包含的两元素分别对应主干网body
和自定义的头部子网络head
。
在构建完模型后,cnn_learner()
会将模型model
与数据包databunch
封装成学习器Learner
。然后Learner
对象会对model
进行分组,以便于在进行迁移学习时,对处于不同深度的模块采用不同的学习速率。分割所依据的参数是split_on
,最直接的方式是提供nn.Module
类型的列表,用于指定在网络结构的哪些层处进行分割;另一种方式是提供一个接受网络模型、返回nn.Module
列表的函数,Fast AI
针对自带模型就是使用这种方式来提供划分信息的,该函数以键值split
存储在model_meta
字典中。最终分组的结果存储在learn.layer_groups
中。
以Fast AI
中自带的ResNet
为例。其网络结构以及所对应的序号如下表所示:
序号 | 模块 | 序号 | 模块 | 序号 | 模块 |
---|---|---|---|---|---|
0 | conv1 | 1 | bn1 | 2 | relu |
3 | maxpool | 4 | resblock_layer1 | 5 | resblock_layer2 |
6 | resblock_layer3 | 7 | resblock_layer4 | 8 | avgpool |
9 | fc |
model_meta
中关于ResNet
的split
参数设置如下:
def _resnet_split(m:nn.Module): return (m[0][6], m[1])
_resnet_meta = {"cut":-2, "split": _resnet_split}
model_meta = {... models.resnet34: {**_resnet_meta} ...}
即model_meta
会使用模型作为键,所对应的值为{"cut": ..., "split": ...}
。其中cut
的作用已解释过,其是针对torchvision.models
所提供的resnet34
来设定的,具体而言就是保留到(但不包括)倒数第二层。split
则是针对添加head
后的网络结构来设定的。其中m[1]
即为添加的head
,m[0][6]
则表明在主干网络(即m[0]
)的序号为6
的组件前进行划分,因此,主干网络会被划分为两部分。所以整个网络结构被划分成了三部分:
Group 1: ['Conv2d', 'BatchNorm2d', 'ReLU', 'MaxPool2d', 'Sequential', 'Sequential']
Group 2: ['Sequential', 'Sequential']
Group 3: ['AdaptiveConcatPool2d', 'Flatten', 'BatchNorm1d', 'Dropout', 'Linear', 'ReLU', 'BatchNorm1d', 'Dropout', 'Linear']
如果使用的是预训练的网络(pretrained=True
),冻结模型至model.layer_groups
的倒数第二个部分。如果通过参数init
指定了head
的初始化,则使用该初始化方式。
Fast AI
内置的网络结构(文档链接)实际上这些网络是由torchvision
提供的。
resnet18
、resnet34
、resnet50
、resnet101
、resnet152
。sqeezenet1_0
、sqeezenet1_1
。densenet121
、densenet201
。vgg11_bn
、vgg13_bn
、vgg16_bn
、vgg19_bn
。alexnet
。此外,Fast AI
还提供了Yolo v3
的主干网络Darknet
,用于图像分割的网络Unet
,以及宽度方向的残差网络WideResNet
。相关内容会在例示具体视觉任务时介绍。
事实上,在Fast AI
框架下,使用非自带的网络结构十分便捷。假设生成模型的函数为new_arch()
,那么仅需在model_meta
字典中提供以new_arch
为键值的元数据{cut: where_to_cut, split: where_to_split}
。以Cadene
库所提供的ResNeXt
为例(见此notebook):
# 定义生成网络的函数
def resnext101_32x4d(pretrained=False):
pretrained = 'imagenet' if pretrained else None
model = pretrainedmodels.resnext101_32x4d(pretrained=pretrained)
all_layers = list(model.children())
return nn.Sequential(*all_layers[0], *all_layers[1:])
# 提供cut和split参数
_resnext_meta = {'cut': -2, 'split': lambda m: (m[0][6], m[1]) }
model_meta[resnext101_32x4d] = _resnext_meta
# 使用cnn_learner()构建模型,无需任何其他设置
learn = cnn_learner(databunch, resnext101_32x4d)
需要注意的是,如前所述,所定义的构建网络模型的函数new_arch()
只能具有唯一的布尔型参数pretrained
。
Learner
类如果要使用自己定义的网络结构,并且不通过上述先定义主干网body
,再定义头部子网络head
的方式,那么参考cnn_learner()
内部代码会发现,可以通过使用Learner
类来实现。
如下展示了一个使用LeNet
做手写字体识别的示例:
model = nn.Sequential(
nn.Conv2d(3, 6, 5),
nn.MaxPool2d(2,2),
nn.Conv2d(6, 16, 5),
nn.MaxPool2d(2,2),
Flatten(),
nn.Linear(16*4*4, 120),
nn.ReLU(),
nn.Linear(120, 2) # 使用的是mnist_sample数据集,只有两类
)
learn = Learner(data, model, metrics=[accuracy])
learn.fit_one_cycle(10)
Cadene
的预训练模型库pytorchcv
的预训练模型库](https://github.com/osmr/imgclsmob/tree/master/- pytorch)Fast AI
框架下使用非自带模型的示例