MODEL_TECHNICAL.md 15 KB

新增模型算法

为了让用户更好的使用PaddleDetection,本文档中,我们将介绍PaddleDetection的主要模型技术细节及应用

目录

1.简介

PaddleDetecion中的每一种模型对应一个文件夹,以yolov3为例,yolov3系列的模型对应于configs/yolov3文件夹,其中yolov3_darknet的总配置文件configs/yolov3/yolov3_darknet53_270e_coco.yml的内容如下:

_BASE_: [
  '../datasets/coco_detection.yml', # 数据集配置文件,所有模型共用
  '../runtime.yml', # 运行时相关配置
  '_base_/optimizer_270e.yml', # 优化器相关配置
  '_base_/yolov3_darknet53.yml', # yolov3网络结构配置文件
  '_base_/yolov3_reader.yml', # yolov3 Reader模块配置
]

# 定义在此处的相关配置可以覆盖上述文件中的同名配置
snapshot_epoch: 5
weights: output/yolov3_darknet53_270e_coco/model_final

可以看到,配置文件中的模块进行了清晰的划分,除了公共的数据集配置以及运行时配置,其他配置被划分为优化器,网络结构以及Reader模块。PaddleDetection中支持丰富的优化器,学习率调整策略,预处理算子等,因此大多数情况下不需要编写优化器以及Reader相关的代码,而只需要在配置文件中配置即可。因此,新增一个模型的主要在于搭建网络结构。

PaddleDetection网络结构的代码在ppdet/modeling/中,所有网络结构以组件的形式进行定义与组合,网络结构的主要构成如下所示:

  ppdet/modeling/
  ├── architectures
  │   ├── faster_rcnn.py # Faster Rcnn模型
  │   ├── ssd.py         # SSD模型
  │   ├── yolo.py      # YOLOv3模型
  │   │   ...
  ├── heads       # 检测头模块
  │   ├── xxx_head.py    # 定义各类检测头
  │   ├── roi_extractor.py #检测感兴趣区域提取
  ├── backbones          # 基干网络模块
  │   ├── resnet.py      # ResNet网络
  │   ├── mobilenet.py   # MobileNet网络
  │   │   ...
  ├── losses             # 损失函数模块
  │   ├── xxx_loss.py    # 定义注册各类loss函数
  ├── necks     # 特征融合模块
  │   ├── xxx_fpn.py  # 定义各种FPN模块
  ├── proposal_generator # anchor & proposal生成与匹配模块
  │   ├── anchor_generator.py   # anchor生成模块
  │   ├── proposal_generator.py # proposal生成模块
  │   ├── target.py   # anchor & proposal的匹配函数
  │   ├── target_layer.py   # anchor & proposal的匹配模块
  ├── tests  # 单元测试模块
  │   ├── test_xxx.py  # 对网络中的算子以及模块结构进行单元测试
  ├── ops.py  # 封装各类PaddlePaddle物体检测相关公共检测组件/算子
  ├── layers.py  # 封装及注册各类PaddlePaddle物体检测相关公共检测组件/算子
  ├── bbox_utils.py # 封装检测框相关的函数
  ├── post_process.py # 封装及注册后处理相关模块
  ├── shape_spec.py # 定义模块输出shape的类

2.新增模型

接下来,以单阶段检测器YOLOv3为例,对建立模型过程进行详细描述,按照此思路您可以快速搭建新的模型。

2.1新增网络结构

2.1.1新增Backbone

PaddleDetection中现有所有Backbone网络代码都放置在ppdet/modeling/backbones目录下,所以我们在其中新建darknet.py如下:

import paddle.nn as nn
from ppdet.core.workspace import register, serializable

@register
@serializable
class DarkNet(nn.Layer):

    __shared__ = ['norm_type']

    def __init__(self,
                 depth=53,
                 return_idx=[2, 3, 4],
                 norm_type='bn',
                 norm_decay=0.):
        super(DarkNet, self).__init__()
        # 省略内容

    def forward(self, inputs):
        # 省略处理逻辑
        pass

    @property
    def out_shape(self):
        # 省略内容
        pass

然后在backbones/__init__.py中加入引用:

from . import darknet
from .darknet import *

几点说明:

  • 为了在yaml配置文件中灵活配置网络,所有Backbone需要利用ppdet.core.workspace里的register进行注册,形式请参考如上示例。此外,可以使用serializable以使backbone支持序列化;
  • 所有的Backbone需继承paddle.nn.Layer类,并实现forward函数。此外,还需实现out_shape属性定义输出的feature map的channel信息,具体可参见源码;
  • __shared__为了实现一些参数的配置全局共享,这些参数可以被backbone, neck,head,loss等所有注册模块共享。
2.1.2新增Neck

特征融合模块放置在ppdet/modeling/necks目录下,我们在其中新建yolo_fpn.py如下:

import paddle.nn as nn
from ppdet.core.workspace import register, serializable

@register
@serializable
class YOLOv3FPN(nn.Layer):
    __shared__ = ['norm_type']

    def __init__(self,
                in_channels=[256, 512, 1024],
                norm_type='bn'):
        super(YOLOv3FPN, self).__init__()
        # 省略内容

    def forward(self, blocks):
        # 省略内容
        pass

    @classmethod
    def from_config(cls, cfg, input_shape):
        # 省略内容
        pass

    @property
    def out_shape(self):
        # 省略内容
        pass

然后在necks/__init__.py中加入引用:

from . import yolo_fpn
from .yolo_fpn import *

几点说明:

  • neck模块需要使用register进行注册,可以使用serializable进行序列化;
  • neck模块需要继承paddle.nn.Layer类,并实现forward函数。除此之外,还需要实现out_shape属性,用于定义输出的feature map的channel信息,还需要实现类函数from_config用于在配置文件中推理出输入channel,并用于YOLOv3FPN的初始化;
  • neck模块可以使用__shared__实现一些参数的配置全局共享。
2.1.3新增Head

Head模块全部存放在ppdet/modeling/heads目录下,我们在其中新建yolo_head.py如下

import paddle.nn as nn
from ppdet.core.workspace import register

@register
class YOLOv3Head(nn.Layer):
    __shared__ = ['num_classes']
    __inject__ = ['loss']

    def __init__(self,
                 anchors=[[10, 13], [16, 30], [33, 23],
                   [30, 61], [62, 45],[59, 119],
                   [116, 90], [156, 198], [373, 326]],
                 anchor_masks=[[6, 7, 8], [3, 4, 5], [0, 1, 2]],
                 num_classes=80,
                 loss='YOLOv3Loss',
                 iou_aware=False,
                 iou_aware_factor=0.4):
        super(YOLOv3Head, self).__init__()
        # 省略内容

    def forward(self, feats, targets=None):
        # 省略内容
        pass

然后在heads/__init__.py中加入引用:

from . import yolo_head
from .yolo_head import *

几点说明:

  • Head模块需要使用register进行注册;
  • Head模块需要继承paddle.nn.Layer类,并实现forward函数。
  • __inject__表示引入全局字典中已经封装好的模块。如loss等。
2.1.4新增Loss

Loss模块全部存放在ppdet/modeling/losses目录下,我们在其中新建yolo_loss.py

import paddle.nn as nn
from ppdet.core.workspace import register

@register
class YOLOv3Loss(nn.Layer):

    __inject__ = ['iou_loss', 'iou_aware_loss']
    __shared__ = ['num_classes']

    def __init__(self,
                 num_classes=80,
                 ignore_thresh=0.7,
                 label_smooth=False,
                 downsample=[32, 16, 8],
                 scale_x_y=1.,
                 iou_loss=None,
                 iou_aware_loss=None):
        super(YOLOv3Loss, self).__init__()
        # 省略内容

    def forward(self, inputs, targets, anchors):
        # 省略内容
        pass

然后在losses/__init__.py中加入引用:

from . import yolo_loss
from .yolo_loss import *

几点说明:

  • loss模块需要使用register进行注册;
  • loss模块需要继承paddle.nn.Layer类,并实现forward函数。
  • 可以使用__inject__表示引入全局字典中已经封装好的模块,使用__shared__可以实现一些参数的配置全局共享。
2.1.5新增后处理模块

后处理模块定义在ppdet/modeling/post_process.py中,其中定义了BBoxPostProcess类来进行后处理操作,如下所示:

from ppdet.core.workspace import register

@register
class BBoxPostProcess(object):
    __shared__ = ['num_classes']
    __inject__ = ['decode', 'nms']

    def __init__(self, num_classes=80, decode=None, nms=None):
        # 省略内容
        pass

    def __call__(self, head_out, rois, im_shape, scale_factor):
        # 省略内容
        pass

几点说明:

  • 后处理模块需要使用register进行注册
  • __inject__注入了全局字典中封装好的模块,如decode和nms等。decode和nms定义在ppdet/modeling/layers.py中。
2.1.6新增Architecture

所有architecture网络代码都放置在ppdet/modeling/architectures目录下,meta_arch.py中定义了BaseArch类,代码如下:

import paddle.nn as nn
from ppdet.core.workspace import register

@register
class BaseArch(nn.Layer):
     def __init__(self):
        super(BaseArch, self).__init__()

    def forward(self, inputs):
        self.inputs = inputs
        self.model_arch()

        if self.training:
            out = self.get_loss()
        else:
            out = self.get_pred()
        return out

    def model_arch(self, ):
        pass

    def get_loss(self, ):
        raise NotImplementedError("Should implement get_loss method!")

    def get_pred(self, ):
        raise NotImplementedError("Should implement get_pred method!")

所有的architecture需要继承BaseArch类,如yolo.py中的YOLOv3定义如下:

@register
class YOLOv3(BaseArch):
    __category__ = 'architecture'
    __inject__ = ['post_process']

    def __init__(self,
                 backbone='DarkNet',
                 neck='YOLOv3FPN',
                 yolo_head='YOLOv3Head',
                 post_process='BBoxPostProcess'):
        super(YOLOv3, self).__init__()
        self.backbone = backbone
        self.neck = neck
        self.yolo_head = yolo_head
        self.post_process = post_process

    @classmethod
    def from_config(cls, cfg, *args, **kwargs):
        # 省略内容
        pass

    def get_loss(self):
        # 省略内容
        pass

    def get_pred(self):
        # 省略内容
        pass

几点说明:

  • 所有的architecture需要使用register进行注册
  • 在组建一个完整的网络时必须要设定__category__ = 'architecture'来表示一个完整的物体检测模型;
  • backbone, neck, yolo_head以及post_process等检测组件传入到architecture中组成最终的网络。像这样将检测模块化,提升了检测模型的复用性,可以通过组合不同的检测组件得到多个模型。
  • from_config类函数实现了模块间组合时channel的自动配置。

2.2新增配置文件

2.2.1网络结构配置文件

上面详细地介绍了如何新增一个architecture,接下来演示如何配置一个模型,yolov3关于网络结构的配置在configs/yolov3/_base_/文件夹中定义,如yolov3_darknet53.yml定义了yolov3_darknet的网络结构,其定义如下:

architecture: YOLOv3
pretrain_weights: https://paddledet.bj.bcebos.com/models/pretrained/DarkNet53_pretrained.pdparams
norm_type: sync_bn

YOLOv3:
  backbone: DarkNet
  neck: YOLOv3FPN
  yolo_head: YOLOv3Head
  post_process: BBoxPostProcess

DarkNet:
  depth: 53
  return_idx: [2, 3, 4]

# use default config
# YOLOv3FPN:

YOLOv3Head:
  anchors: [[10, 13], [16, 30], [33, 23],
            [30, 61], [62, 45], [59, 119],
            [116, 90], [156, 198], [373, 326]]
  anchor_masks: [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
  loss: YOLOv3Loss

YOLOv3Loss:
  ignore_thresh: 0.7
  downsample: [32, 16, 8]
  label_smooth: false

BBoxPostProcess:
  decode:
    name: YOLOBox
    conf_thresh: 0.005
    downsample_ratio: 32
    clip_bbox: true
  nms:
    name: MultiClassNMS
    keep_top_k: 100
    score_threshold: 0.01
    nms_threshold: 0.45
    nms_top_k: 1000

可以看到在配置文件中,首先需要指定网络的architecture,pretrain_weights指定训练模型的url或者路径,norm_type等可以作为全局参数共享。模型的定义自上而下依次在文件中定义,与上节中的模型组件一一对应。对于一些模型组件,如果采用默认 的参数,可以不用配置,如上文中的yolo_fpn。通过改变相关配置,我们可以轻易地组合出另一个模型,比如configs/yolov3/_base_/yolov3_mobilenet_v1.yml将backbone从Darknet切换成MobileNet。

2.2.2优化器配置文件

优化器配置文件定义模型使用的优化器以及学习率的调度策略,目前PaddleDetection中已经集成了多种多样的优化器和学习率策略,具体可参见代码ppdet/optimizer.py。比如,yolov3的优化器配置文件定义在configs/yolov3/_base_/optimizer_270e.yml,其定义如下:

epoch: 270

LearningRate:
  base_lr: 0.001
  schedulers:
  - !PiecewiseDecay
    gamma: 0.1
    milestones:
    # epoch数目
    - 216
    - 243
  - !LinearWarmup
    start_factor: 0.
    steps: 4000

OptimizerBuilder:
  optimizer:
    momentum: 0.9
    type: Momentum
  regularizer:
    factor: 0.0005
    type: L2

几点说明:

  • 可以通过OptimizerBuilder.optimizer指定优化器的类型及参数,目前支持的优化器可以参考PaddlePaddle官方文档
  • 可以设置LearningRate.schedulers设置不同学习率调整策略的组合,PaddlePaddle目前支持多种学习率调整策略,具体也可参考PaddlePaddle官方文档。需要注意的是,你需要对于PaddlePaddle中的学习率调整策略进行简单的封装,具体可参考源码ppdet/optimizer.py
2.2.3Reader配置文件

关于Reader的配置可以参考Reader配置文档

看过此文档,您应该对PaddleDetection中模型搭建与配置有了一定经验,结合源码会理解的更加透彻。关于模型技术,如您有其他问题或建议,请给我们提issue,我们非常欢迎您的反馈。