| # 新增模型算法 | |
| 为了让用户更好的使用PaddleDetection,本文档中,我们将介绍PaddleDetection的主要模型技术细节及应用 | |
| ## 目录 | |
| - [1.简介](#1.简介) | |
| - [2.新增模型](#2.新增模型) | |
| - [2.1新增网络结构](#2.1新增网络结构) | |
| - [2.1.1新增Backbone](#2.1.1新增Backbone) | |
| - [2.1.2新增Neck](#2.1.2新增Neck) | |
| - [2.1.3新增Head](#2.1.3新增Head) | |
| - [2.1.4新增Loss](#2.1.4新增Loss) | |
| - [2.1.5新增后处理模块](#2.1.5新增后处理模块) | |
| - [2.1.6新增Architecture](#2.1.6新增Architecture) | |
| - [2.2新增配置文件](#2.2新增配置文件) | |
| - [2.2.1网络结构配置文件](#2.2.1网络结构配置文件) | |
| - [2.2.2优化器配置文件](#2.2.2优化器配置文件) | |
| - [2.2.3Reader配置文件](#2.2.3Reader配置文件) | |
| ### 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`如下: | |
| ```python | |
| 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`中加入引用: | |
| ```python | |
| 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`如下: | |
| ``` python | |
| 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`中加入引用: | |
| ```python | |
| 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`如下 | |
| ``` python | |
| 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`中加入引用: | |
| ```python | |
| 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`下 | |
| ```python | |
| 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`中加入引用: | |
| ```python | |
| 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`类来进行后处理操作,如下所示: | |
| ``` python | |
| 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`类,代码如下: | |
| ``` python | |
| 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`定义如下: | |
| ``` python | |
| @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官方文档](https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/optimizer/Overview_cn.html) | |
| - 可以设置LearningRate.schedulers设置不同学习率调整策略的组合,PaddlePaddle目前支持多种学习率调整策略,具体也可参考[PaddlePaddle官方文档](https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/optimizer/Overview_cn.html)。需要注意的是,你需要对于PaddlePaddle中的学习率调整策略进行简单的封装,具体可参考源码`ppdet/optimizer.py`。 | |
| ##### 2.2.3Reader配置文件 | |
| 关于Reader的配置可以参考[Reader配置文档](./READER.md#5.配置及运行)。 | |
| > 看过此文档,您应该对PaddleDetection中模型搭建与配置有了一定经验,结合源码会理解的更加透彻。关于模型技术,如您有其他问题或建议,请给我们提issue,我们非常欢迎您的反馈。 | |