第五篇 FastAI构建学习器

前面的几篇博客都是在介绍如何准备数据。实际上,在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()函数中的主要流程如下:

1. 从指定的网络结构中分割主干网络

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,本篇博文后续会介绍。

2. 添加头部子网络,构建模型

切割出主干网后,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 mapglobal pooling(即将 N × N × C N\times N\times C N×N×Cfeature map通过Max PoolingAvg 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

3. 对网络的各部分进行分组

在构建完模型后,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中关于ResNetsplit参数设置如下:

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]即为添加的headm[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']
4. 冻结主干网络、对添加的头部子网络进行初始化

如果使用的是预训练的网络(pretrained=True),冻结模型至model.layer_groups的倒数第二个部分。如果通过参数init指定了head的初始化,则使用该初始化方式。

二、使用其他网络结构

1. Fast AI内置的网络结构(文档链接)

实际上这些网络是由torchvision提供的。

  • resnet18resnet34resnet50resnet101resnet152
  • sqeezenet1_0sqeezenet1_1
  • densenet121densenet201
  • vgg11_bnvgg13_bnvgg16_bnvgg19_bn
  • alexnet

此外,Fast AI还提供了Yolo v3的主干网络Darknet,用于图像分割的网络Unet,以及宽度方向的残差网络WideResNet。相关内容会在例示具体视觉任务时介绍。

2. 使用其他网络结构

事实上,在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

3. 使用自定义的网络结构:使用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框架下使用非自带模型的示例

你可能感兴趣的:(Fast,AI文档,深度学习,计算机视觉)