1. 目标检测简介
- ETA: Estimate Time of Arrival (估计剩余时间)
- 两阶段AP不如一阶段AP的原因:样本的类别不均衡导致的
- 样本不均衡导致的问题:
- 训练低效,因为大多数位置都是易学负样本,没有提供有用的学习信号
- 由易学负样本主导整个训练,使得模型的优化方向并不是我们所希望的那样
- 正负样本匹配策略或者称为标签匹配策略 Label Assignment 是目标检测模型训练中最核心的问题之一, 更好的标签匹配策略往往能够使得网络更好学习到物体的特征以提高检测能力。
- Backbone + Neck:生成多尺度特征图
- Head:输出检测框/检测框偏移量、置信度
- 模型配置文件名:retinanet_r50_fpn_1x_coco.py
- retinanet:表示算法名称
- r50:等表示骨架网络名
- caffe: 和 PyTorch 是指 Bottleneck 模块的区别,省略情况下表示是 PyTorch。Bottleneck 是标准的 1x1-3x3-1x1 结构,考虑 stride=2 下采样的场景,caffe 模式下,stride 参数放置在第一个 1x1 卷积上,而 Pyorch 模式下,stride 放在第二个 3x3 卷积上
- fpn:表示 Neck 模块采用了 FPN 结构
- mstrain:表示多尺度训练,一般对应的是 pipeline 中 Resize 类
- 1x: 表示 1 倍数的 epoch 训练即 12 个 epoch,2x 则表示 24 个 epcoh
- coco:表示在 COCO 数据集上训练
if self.style == 'pytorch':
self.conv1_stride = 1
self.conv2_stride = stride
else:
self.conv1_stride = stride
self.conv2_stride = 1
1.0 常用工具
1.0.0 实用工具
python tools/analysis_tools/browse_coco_json.py --data-root ./data/cat --img-dir images --ann-file annotations/annotations_all.json --disp-all
python tools/analysis_tools/browse_coco_json.py --img-dir ./data/cat/images --ann-file ./data/cat/annotations/annotations_all.json
python ./tools/analysis_tools/browse_dataset.py configs/yolov5/yolov5_s-v61_syncbn_fast_1xb4-300e_balloon.py --phase train --out-dir temp --mode pipeline
mim run mmdet print_config configs/yolov5/yolov5_s-v61_syncbn_fast_1xb4-300e_balloon.py --save-path ./temp/whole.py
python tools/analysis_tools/dataset_analysis.py configs/yolov5/yolov5_s-v61_syncbn_fast_1xb4-300e_balloon.py
-
优化锚框尺寸
-
提取 COCO 子集
-
可视化优化器参数策略
-
数据集转换
-
数据集下载
-
日志分析
-
模型转换
1.0.1 MMYOLO 自定义数据集从标注到部署
- 数据集准备:tools/misc/download_dataset.py
- 使用 labelme 和算法进行辅助标注:demo/image_demo.py + labelme
python demo/image_demo.py ./data/cat/images ./work/yolov5_s-v61_syncbn_fast_8xb16-300e_coco.py ./work/yolov5_s-v61_syncbn_fast_8xb16-300e_coco_20220918_084700-86e02187.pth --out-dir ./data/cat/labels --class-name cat --to-labelme
conda create -n labelme python=3.8
conda activate labelme
pip install labelme==5.1.1
--output ${label文件所处的文件夹路径(即上一步的 --out-dir)} \
--autosave \
--nodata
labelme ./data/cat/images --output ./data/cat/labels --autosave --nodata
- 使用脚本转换成 COCO 数据集格式:tools/dataset_converters/labelme2coco.py
python tools/dataset_converters/labelme2coco.py --img-dir ./data/cat/images --labels-dir ./data/cat/labels --out ./data/cat/annotations/annotations_all.json --class-id-txt ./data/cat/class_with_id.txt
- 数据集划分为训练集、验证集和测试集:tools/misc/coco_split.py
python tools/misc/coco_split.py --json ./data/cat/annotations/annotations_all.json --out-dir ./data/cat/annotations --ratios 0.8 0.2 --shuffle --seed 10
_base_ = '../yolov5/yolov5_s-v61_syncbn_fast_8xb16-300e_coco.py'
max_epochs = 100
data_root = './data/cat/'
work_dir = './work_dirs/yolov5_s-v61_syncbn_fast_1xb32-100e_cat'
load_from = './work_dirs/yolov5_s-v61_syncbn_fast_8xb16-300e_coco_20220918_084700-86e02187.pth'
train_batch_size_per_gpu = 32
train_num_workers = 4
save_epoch_intervals = 2
base_lr = _base_.base_lr / 4
anchors = [
[(68, 69), (154, 91), (143, 162)],
[(242, 160), (189, 287), (391, 207)],
[(353, 337), (539, 341), (443, 432)]
]
class_name = ('cat', )
num_classes = len(class_name)
metainfo = dict(
classes=class_name,
palette=[(220, 20, 60)]
)
train_cfg = dict(
max_epochs=max_epochs,
val_begin=20,
val_interval=save_epoch_intervals
)
model = dict(
bbox_head=dict(
head_module=dict(num_classes=num_classes),
prior_generator=dict(base_sizes=anchors),
loss_cls=dict(loss_weight=0.5 *
(num_classes / 80 * 3 / _base_.num_det_layers))))
train_dataloader = dict(
batch_size=train_batch_size_per_gpu,
num_workers=train_num_workers,
dataset=dict(
_delete_=True,
type='RepeatDataset',
times=5,
dataset=dict(
type=_base_.dataset_type,
data_root=data_root,
metainfo=metainfo,
ann_file='annotations/trainval.json',
data_prefix=dict(img='images/'),
filter_cfg=dict(filter_empty_gt=False, min_size=32),
pipeline=_base_.train_pipeline)))
val_dataloader = dict(
dataset=dict(
metainfo=metainfo,
data_root=data_root,
ann_file='annotations/trainval.json',
data_prefix=dict(img='images/')))
test_dataloader = val_dataloader
val_evaluator = dict(ann_file=data_root + 'annotations/trainval.json')
test_evaluator = val_evaluator
optim_wrapper = dict(optimizer=dict(lr=base_lr))
default_hooks = dict(
checkpoint=dict(
type='CheckpointHook',
interval=save_epoch_intervals,
max_keep_ckpts=5,
save_best='auto'),
param_scheduler=dict(max_epochs=max_epochs),
logger=dict(type='LoggerHook', interval=10))
- 数据集可视化分析:tools/analysis_tools/dataset_analysis.py
python tools/analysis_tools/dataset_analysis.py configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py --out-dir work_dirs/dataset_analysis_cat/train_dataset
python tools/analysis_tools/dataset_analysis.py configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py --out-dir work_dirs/dataset_analysis_cat/val_dataset --val-dataset
- 优化 anchor 尺寸:tools/analysis_tools/optimize_anchors.py
python tools/analysis_tools/optimize_anchors.py configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py --algorithm v5-k-means --input-shape 640 640 --prior-match-thr 4.0 --out-dir work_dirs/dataset_analysis_cat
- 可视化数据处理部分:tools/analysis_tools/browse_dataset.py
python tools/analysis_tools/browse_dataset.py configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py --show-interval 3
pip install wandb
wandb login
visualizer = dict(vis_backends=[dict(type='LocalVisBackend'), dict(type='WandbVisBackend')])
pip install tensorboard
visualizer = dict(vis_backends=[dict(type='LocalVisBackend'),dict(type='TensorboardVisBackend')])
tensorboard --logdir=work_dirs/yolov5_s-v61_syncbn_fast_1xb16-100e_cat
python tools/train.py configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py
CUDA_VISIBLE_DEVICES=0 python tools/train.py configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py
CUDA_VISIBLE_DEVICES=0,2 python tools/train.py configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py
python demo/image_demo.py ./data/cat/images ./configs/custom_dataset/yolov5_s-v61_syncbn_fast_1xb16-100e_cat.py ./work_dirs/yolov5_s-v61_syncbn_fast_1xb16-100e_cat/best_coco/bbox_mAP_epoch_84.pth --out-dir ./data/cat/pred_images
1.1 目标检测与图像分类的区别
- 一个目标框至少6个参数(矩形框<4>、类别<1>、置信度<1>)
- 两阶段目标检测的开山之作: Faster R-CNN (也是目标检测任务中的一个开山之作)
- 单阶段目标检测算法的代表作:YOLO<推荐使用V5>、SSD

1.2 目标检测汇总 (2022)

1.3 网络结构

网络部件 |
功能 |
详细描述 |
主干网络 |
产生多级特征图 |
空间分辨率会越来越小,通道数通常会越来越大,信息会越来越集中,通常就集中到channel上,channel就会越来越多 |
颈部 |
融合不同级的特征 |
越靠近输入的级,其细节更好,但语义信息比较弱,输出多尺度(多种分辨率)的特征图。不同分辨率的特征图通常用于检测不同大小的物体 |
检测头 |
|
通常是由一个或多个卷积构成的模块,只要在这个特征图上扫一下, 在对应位置如果有物体就可以给一个输出 |
1.4 滑窗
-
工作原理


-
不足之外 (效率太代、计算量太大)

-
改进思路 1:区域提议
- 依赖外部算法,Selective Search不是神经网络的一部分,不可训练,不符合学习的理念。

-
改进思路 2:分析滑窗中的重复计算

-
消除滑窗中的重复计算

-
在特征图上进行密集预测

-
目标检测技术的演进


1.5 损失的种类(分类、回归、Objectness)
- 一般的检测算法:
- 回归损失只有正样本有
- 分类损失的标签直接就非1即0,正样本的标签是1,负样本的标签是0(可以把背景作为一种类别一起算),这些标签根据anchor和GT框的比较就可以直接得到。比如标签是[0,1,0,0],代表当前样本属于背景或其他三种类别的标签。
- YOLOV5中 :
- 回归损失只有正样本有
- 分类损失分为两种:
- 一种是正样本的分类损失(不包括背景的onehot标签,比如[1,0,0])。
- 另一种是正负样本的前景背景预测的分类损失,这里叫Objectness loss,比起以往算法前景背景损失的标签非1即0,这里采用预测框和GT框的IOU作为Objectness的标签,也就是说预测框和GT框重合度越高,当前样本属于前景的可能性就越大。一般算法对于不同的anchor接近GT框的程度不同,标签都是非1即0,统一对待。另外注意这里的Objectness loss的标签在得到预测值之前是不知道的,也就是说根据预测框的结果才实时的计算出来的,这种方法直观上来讲更合适些。
1.6 正负样本匹配策略
- 静态标签匹配策略(基于先验)
- FCOS
- RetinaNet (Anchor与GT的最在IoU)
- YOLOv5(Anchor与GT的宽高比)
- 动态标签匹配策略
1.7 单阶段、双阶段、anchor-based、anchor-free之间的关系
1.7.1 划分依据
- 单阶段、双阶段、anchor-based、anchor-free 4者的联系应该分成两个并行维度来讲
- 单阶段和双阶段的划分
- 划分依据:是是否存在显式的 ROI 特征提取过程
- 典型的双阶段算法是 Faster R-CNN,其包括 RPN 和 R-CNN 两个模块,RPN 模块负责提取 ROI,然后通过 ROI-Pool或者 ROI-Align 进行 ROI 特征切割提取,最后在输入到 R-CNN 中进行识别和定位
- 如果没有显式 ROI 提取过程则认为是单阶段算法,例如典型的 YOLO、RetinaNet 等等。
- 一般而言,双阶段算法精度高但是速度慢一些,单阶段精度稍微低一点但是速度容易优化到比较快
- Anchor-Based、Anchor-Free
- 划分依据:是否需要显式定义先验 anchor 角度区分,如果需要定义 anchor 那么就是 anchor-base
- 目前主流算法大部分都是这个类型,例如 Faster R-CNN、RetinaNet和YOLOv5 等等
- 而 anchor-free 是从 2019 年开始慢慢流行,其最大优势是不需要设置麻烦且有重大影响的 anchor,输出是对每个输出特征图上点进行分类和定位建模,参数稍微少一些,更加容易理解,典型算法是FCOS、ATSS 和 SABL 等等
1.7.2 四者的联系
- 四者的联系是Label>的编码方式。也就是一张图中若干个Label,(x0, y0, x1, y1,c)的GT检测框,是如何变为一张(1,C,H,W)的Feature Map的
- 先从Anchor Free方法讲起。这类方法通过一些自定义的,简单的映射关系,将一个检测框的坐标直接映射到Feature Map中,比如CenterNet大致的思路是:
- 求出GT框的中心,然后将这个中心编码成一个Heat Map对应坐标的一个点(一般这个点附近带有高斯分布)
- 将这个GT框的宽高编码成对应wh Map对应坐标的一个点
- 从而形成Heatmap + wh map,简单来说,检测器通过heatmap学习对目标的分类,wh map学习对目标宽高的回归(实际过程中会加上offset,但这里为了简化问题不做详细讨论,详见CenterNet论文)
- 可以看到,在Anchor Free方法的Label编码过程中,始终是利用了目标本身的性质(图像坐标、属性),没有引入额外的信息
- Anchor Based方法是在Anchor Free的基础上,为目标的宽高引入了若干个先验值
- 对于Heatmap Label Feature Map中的一个点,只有当以这个点为中心,先验值的宽高所组成的“假设框"与真值很相近(比如IoU足够大),才会将检测框编码到这个点上
- 对于wh Map中的一个点,则在编码的过程中加入anchor的宽高信息,例如Raster R-CNN在RPN中,对应的wh map值是GT框的宽高除以anchor宽高,再取对数 (编码值)
- Two Stage方法大多使用Anchor Based这种Label 编码方式,并且将GT框的类别和坐标的编码分离。以Faster R-CNN为例,将GT框的坐标框单独编码,在RPN中做对其做回归+一个简单的正负样本的二分类;而具体的类别则在R-CNN部分对RPN中得到的bbox candidate进行分类
- One Stage方法和Two Stage方法的不同,本质是没有分离坐标和类别属性,可以理解为就是一个加入多类别属性分类的RPN。显然,不管是One Stage还是Two Stage方法,可以用Anchor Based也可以用Anchor Free对坐标进行编码
- 不同的编码方式,决定了模型真正学习的目标(Label Feature Map)的样本数值分布,决定了任务的难易程度,也赋予了不同检测方式的“某种天赋“。比如Anchor Based方法在不同大小的目标下具有更好的鲁棒性,Anchor Free计算量更少,等等。
1.8 MMLab配置文件名的含义
- yolov5_n-v61_syncbn_fast_8xb16-300e_coco.py
- yolov5:算法名
- l/m/n/s:网络大小
- v61:版本号,6.1版本
- 8xb16:训练时采用的是8卡,每张卡的Batch Size是16
- 300e:表示训练300个epoch
- coco:表示COCO2017目标检测数据集
1.9 主干网络Backbone
1.9.1 MMDetection实现的Backbone
- VGG (ICLR’2015)
- ResNet (CVPR’2016)
- ResNeXt (CVPR’2017)
- MobileNetV2 (CVPR’ 2018)
- HRNet (CVPR’2019)
- Generalized Attention (ICCV’2019)
- GCNet (ICCVW’2019)
- Res2Net (TPAMI’2020)
- RegNet (CVPR’2 2020)
- ResNeSt (ArXiv’2020)
- PVT (ICCV’2021)
- SwinTransformer (CVPR’2021)
- PVTv2 (ArXiv’2021)
- ResNet Strikes back (ArXiv’2021)
- EfficientNet (ArXiv’2021)
- ConvNeXt (CVPR’2022)
1.9.2 MMYOLO实现的Backbone
- YOLOv5CSPDarknet
- PPYOLOECSPResNet
- CSPNeXt
- YOLOv6EfficientRep
- YOLOv7Backbone
1.9.3 MMClassification实现的Backbone
2. 基础知识
- Anchor(锚框):以特征图上的位置在原图上对应的位置为中心设定不同大小、不同长宽比,同时又重合的基准框。
2.1 边界框(Bounding Box)


2.2 交并比 Intersection Over Union

2.3 置信度 Confidence Score

2.4 非极大值抑制 Non-Maximum Suppression

2.5 边界框回归 Bounding Box Regression

2.6 边界框编码 Bbox Coding

{ b x = p x + p w ∗ t x b y = p y + p h ∗ t y b w = p w ∗ e x p ( t w ) b h = p h ∗ e x p ( t h ) \left\{ \begin{array}{ll} b_x = p_x + p_w * t_x \\ b_y = p_y + p_h * t_y \\ b_w = p_w * exp(t_w) \\ b_h = p_h * exp(t_h) \end{array} \right. ⎩ ⎨ ⎧bx=px+pw∗txby=py+ph∗tybw=pw∗exp(tw)bh=ph∗exp(th)
3. 两阶段目标检测算法
3.1 两阶段算法概述

3.2 Region-based CNN (2013)

3.2.1 R-CNN的训练

3.2.2 R-CNN相比于传统方法(如DPM)的提升

3.2.3 R-CNN的不足(重复卷积)

3.3 Fast R-CNN (2014)

3.3.1 RoI Pooling

3.3.2 RoI Align

3.3.3 Fast R-CNN的训练

3.3.4 Fast R-CNN 的速度提升

3.3.5 Fast R-CNN 的精度提升

3.3.6 Fast R-CNN 的速度瓶颈(产生提议框)

3.3.7 降低区域提议的计算成本

3.3.8 朴素方法的局限

3.3.9 锚框 Anchor

3.4 Faster R-CNN (2015)

3.4.1 Faster R-CNN 的训练

3.5 两阶段方法的发展与演进 (2013~2017)


3.6 多尺度检测技术
3.6.1 多尺度检测必要性

3.6.2 图像金字塔 Image Pyramid

3.6.3 层次化特征

3.6.4 特征金字塔网络 Feature Pyramid Network (2016)

3.6.5 在 Faster R-CNN 模型中使用 FPN

4. 单阶段目标检测算法
4.1 两阶段与单阶段的比较
- 两阶段

- 单阶段

4.2 单阶段检测算法概述

4.3 YOLO: You Only Look Once (2015)

4.3.1 YOLO 的分类和回归目标

4.3.2 YOLO 的损失函数

4.3.3 YOLO 的优点和缺点

4.4 SSD: Single Shot MultiBox Detetor (2016)

4.4.1 SSD 的损失函数

4.5 正负样本不均衡问题


4.5.1 解决样本不均衡问题

4.5.2 困难负样本 Hard Negative

4.5.3 不同负样本对损失函数的贡献

4.5.4 降低简单负样本的损失

4.5.5 Focal Loss (焦点损失函数)
- Focal Loss:几乎已经成为 one-stage 算法的标配, 因为它解决了正负样本极不平衡的问题

4.6 RetinaNet (2017)
- 简介
- 深入分析了极度不平衡的正负(前景背景)样本比例导致 one-stage 检测器精度低于 two-stage 检测器
- 基于上述分析,提出了一种简单但是非常实用的 Focal Loss 焦点损失函数,并且 Loss 设计思想可以推广到其他领域
- 同时针对目标检测领域特定问题,设计了 RetinaNet 网络,结合 Focal Loss 使得 one-stage 检测器在精度上能够达到乃至超过 two-stage 检测器。
- 创新点
- Focal Loss (焦点损失函数)
- RetinaNet网络

4.6.1 RetinaNet 的性能

4.7 YOLO v3 (2018)


4.8 YOLO v5
4.8.1 网络结构
- YOLOv5 网络结构是标准的 CSPDarknet + PAFPN + 非解耦 Head。
- YOLOv5 网络结构大小由 deepen_factor 和 widen_factor 两个参数决定。其中 deepen_factor 控制网络结构深度,即 CSPLayer 中 DarknetBottleneck 模块堆叠的数量;widen_factor 控制网络结构宽度,即模块输出特征图的通道数。以 YOLOv5-l 为例,其 deepen_factor = widen_factor = 1.0 。P5 和 P6 的模型整体结构分别如下图所示。
- YOLOv5-l-P5 模型结构
- YOLOv5-l-P6 模型结构
- 图的上半部分为模型总览;下半部分为具体网络结构,其中的模块均标有序号,方便用户与 YOLOv5 官方仓库的配置文件对应;中间部分为各子模块的具体构成。
- 特征维度(shape):都为 (B, C, H, W)
4.8.2 正负样本匹配策略
- 正负样本匹配策略的核心是确定预测特征图的所有位置中哪些位置应该是正样本,哪些是负样本,甚至有些是忽略样本。
- 匹配策略是目标检测算法的核心,一个好的匹配策略可以显著提升算法性能。
- YOLOV5 的匹配策略简单总结为:采用了 anchor 和 gt_bbox 的 shape 匹配度作为划分规则,同时引入跨邻域网格策略来增加正样本。 其主要包括如下两个核心步骤:
- 对于任何一个输出层,抛弃了常用的基于 Max IoU 匹配的规则,而是直接采用 shape 规则匹配,也就是该 GT Bbox 和当前层的 Anchor 计算宽高比,如果宽高比例大于设定阈值,则说明该 GT Bbox 和 Anchor 匹配度不够,将该 GT Bbox 暂时丢掉,在该层预测中该 GT Bbox 对应的网格内的预测位置认为是负样本
- 对于剩下的 GT Bbox(也就是匹配上的 GT Bbox),计算其落在哪个网格内,同时利用四舍五入规则,找出最近的两个网格,将这三个网格都认为是负责预测该 GT Bbox 的,可以粗略估计正样本数相比之前的 YOLO 系列,至少增加了三倍
4.8.3 Anchor设置
- YOLOv5 是 Anchor-based 的目标检测算法,其 Anchor size 的获取方式与 YOLOv3 类似,也是使用聚类获得,其不同之处在于聚类使用的标准不再是基于 IoU 的,而是使用形状上的宽高比作为聚类准则(即 shape-match )。
- 在用户更换了数据集后,可以使用 MMYOLO 里带有的 Anchor 分析工具,对自己的数据集进行分析,确定合适的 Anchor size。
python tools/analysis_tools/optimize_anchors.py ${CONFIG} --algorithm v5-k-means
--input-shape ${INPUT_SHAPE [WIDTH HEIGHT]} --output-dir ${OUTPUT_DIR}
- 然后在 config 文件 里修改默认 Anchor size:
anchors = [[(10, 13), (16, 30), (33, 23)], [(30, 61), (62, 45), (59, 119)],
[(116, 90), (156, 198), (373, 326)]]
4.8.4 Bbox编解码过程
- 在 Anchor-based 算法中,预测框通常会基于 Anchor 进行变换,然后预测变换量,这对应 GT Bbox 编码过程,而在预测后需要进行 Pred Bbox 解码,还原为真实尺度的 Bbox,这对应 Pred Bbox 解码过程。
- 在 YOLOv3 中,回归公式为:
{ b x = σ ( t x ) + c x b y = σ ( t y ) + c y b w = a w ⋅ e t w b h = a h ⋅ e t h \left\{ \begin{array}{ll} b_x = \sigma(t_x) + c_x \\ b_y = \sigma(t_y) + c_y \\ b_w = a_w \cdot e^{t_w} \\ b_h = a_h \cdot e^{t_h} \end{array} \right. ⎩ ⎨ ⎧bx=σ(tx)+cxby=σ(ty)+cybw=aw⋅etwbh=ah⋅eth
- a w a_w aw:Anchor的宽度
- c x c_x cx:Grid所处的坐标
- σ \sigma σ:Sigmoid公式
- 而在 YOLOv5 中,回归公式为:
{ b x = ( 2 ⋅ σ ( t x ) − 0.5 ) + c x b y = ( 2 ⋅ σ ( t y ) − 0.5 ) + c y b w = a w ⋅ ( 2 ⋅ σ ( t w ) ) 2 b h = a h ⋅ ( 2 ⋅ σ ( t h ) ) 2 \left\{ \begin{array}{ll} b_x = (2 \cdot \sigma(t_x) - 0.5) + c_x \\ b_y = (2 \cdot \sigma(t_y) - 0.5) + c_y \\ b_w = a_w \cdot (2 \cdot \sigma(t_w))^2 \\ b_h = a_h \cdot (2 \cdot \sigma(t_h))^2 \end{array} \right. ⎩ ⎨ ⎧bx=(2⋅σ(tx)−0.5)+cxby=(2⋅σ(ty)−0.5)+cybw=aw⋅(2⋅σ(tw))2bh=ah⋅(2⋅σ(th))2
- YOLOv5改进之处主要有以下两点:
- 中心点坐标范围从 (0, 1) 调整至 (-0.5, 1.5)
- 宽高范围从 ( 0 , + ∞ ) (0, + \infty) (0,+∞)调整至 ( 0 , 4 a w h ) (0, 4a_{wh}) (0,4awh)
- 这个改进具有以下好处:
- 新的中心点设置能更好的预测到 0 和 1。这有助于更精准回归出 box 坐标。
- 宽高回归公式中 exp(x) 是无界的,这会导致梯度失去控制,造成训练不稳定。YOLOv5 中改进后的宽高回归公式优化了此问题。

4.8.5 匹配策略
- 在 MMYOLO 设计中,无论网络是 Anchor-based 还是 Anchor-free,我们统一使用 prior 称呼 Anchor。
- 正样本匹配包含以下两步:
- 步骤1:“比例”比较
- 步骤2:为步骤 1 中 match 的 GT 分配对应的正样本
4.8.5.1 步骤1:“比例”比较
- 将 GT Bbox 的 WH 与 Prior(Anchor) 的 WH 进行“比例”比较。
- 比较流程:
{ r w = w _ g t / w _ p t r h = h _ g t / h _ p t r w m a x = m a x ( r w , 1 / r w ) r h m a x = m a x ( r h , 1 / r h ) r m a x = m a x ( r w m a x , r h m a x ) \left\{ \begin{array}{ll} r_w = w\_gt/w\_pt \\ r_h = h\_gt/h\_pt \\ r_w^{max} = max(r_w, 1/r_w) \\ r_h^{max} = max(r_h, 1/r_h) \\ r^{max} = max(r_w^{max}, r_h^{max}) \end{array} \right. ⎩ ⎨ ⎧rw=w_gt/w_ptrh=h_gt/h_ptrwmax=max(rw,1/rw)rhmax=max(rh,1/rh)rmax=max(rwmax,rhmax)
- if r m a x r^{max} rmax < prior_match_thr: match!
- 此处用一个 GT Bbox 与 P3 特征图的 Prior 进行匹配的案例进行讲解和图示:

- prior1 匹配失败的原因:
h _ g t / h _ p t = h _ g t / h _ p r i o r = 24 / 5 = 4.8 > p r i o r _ m a t c h _ t h r h\_gt/h\_pt = h\_gt/h\_prior=24/5=4.8 > prior\_match\_thr h_gt/h_pt=h_gt/h_prior=24/5=4.8>prior_match_thr
4.8.5.2 步骤2:为步骤 1 中 match 的 GT 分配对应的正样本
-
依然沿用上面的例子:
GT Bbox (cx, cy, w, h) 值为 (26, 37, 36, 24),
-
Prior WH 值为 [(15, 5), (24, 16), (16, 24)],在 P3 特征图上,stride 为 8。通过计算,prior2 和 prior3 能够 match。
-
计算过程如下:
- 步骤1:将 GT Bbox 的中心点坐标对应到 P3 的 grid 上
{ G T x c e n t e r _ g r i d = 26 / 8 = 3.25 G T y c e n t e r _ g r i d = 37 / 8 = 4.625 \left\{ \begin{array}{ll} GT_x^{center\_grid} = 26/8 = 3.25 \\ GT_y^{center\_grid} = 37/8 = 4.625 \end{array} \right. {GTxcenter_grid=26/8=3.25GTycenter_grid=37/8=4.625

- 步骤2:将 GT Bbox 中心点所在的 grid 分成四个象限,由于中心点落在了左下角的象限当中,那么会将物体的左、下两个 grid 也认为是正样本

-
下图展示中心点落到不同位置时的正样本分配情况:

-
那么 YOLOv5 的 Assign 方式具体带来了哪些改进?
- 一个 GT Bbox 能够匹配多个 Prior
- 一个 GT Bbox 和一个Prior 匹配时,能分配 1-3 个正样本
- 以上策略能适度缓解目标检测中常见的正负样本不均衡问题
4.8.6 回归方式
- 而 YOLOv5 中的回归方式,和 Assign 方式是相互呼应的:
4.8.6.1 中心点回归方式
4.8.6.1 WH 回归方式

4.8.7 Loss 设计
- YOLOv5 中总共包含 3 个 Loss,分别为:
- Classes loss:使用的是 BCE loss
- Objectness loss:使用的是 BCE loss
- Location loss:使用的是 CIoU loss
- 三个 loss 按照一定比例汇总:
L o s s = λ 1 L c l s + λ 2 L o b j + λ 3 L l o c Loss = \lambda_1 L_{cls} + \lambda_2 L_{obj} + \lambda_3 L_{loc} Loss=λ1Lcls+λ2Lobj+λ3Lloc
- P3、P4、P5 层对应的 Objectness loss 按照不同权重进行相加,默认的设置是:
obj_level_weights=[4., 1., 0.4]
L o b j = 4.0 ⋅ L o b j s m a l l + 1.0 ⋅ L o b j m e d i u m + 0.4 ⋅ L o b j l a r g e L_{obj} = 4.0 \cdot L_{obj}^{small} + 1.0 \cdot L_{obj}^{medium} + 0.4 \cdot L_{obj}^{large} Lobj=4.0⋅Lobjsmall+1.0⋅Lobjmedium+0.4⋅Lobjlarge
4.8.8 优化策略和训练过程
- OLOv5 对每个优化器的参数组进行非常精细的控制,简单来说包括如下部分。
- 优化器分组
- 将优化参数分成 Conv/Bias/BN 三组,在 WarmUp 阶段,不同组采用不同的 lr 以及 momentum 更新曲线。 同时在 WarmUp 阶段采用的是 iter-based 更新策略,而在非 WarmUp 阶段则变成 epoch-based 更新策略,可谓是 trick 十足。
- MMYOLO 中是采用 YOLOv5OptimizerConstructor 优化器构造器实现优化器参数分组。优化器构造器的作用就是对一些特殊的参数组初始化过程进行精细化控制,因此可以很好的满足需求。
- 而不同的参数组采用不同的调度曲线功能则是通过 YOLOv5ParamSchedulerHook 实现。而不同的参数组采用不同的调度曲线功能则是通过 YOLOv5ParamSchedulerHook 实现。
- weight decay 参数自适应
- 作者针对不同的 batch size 采用了不同的 weight decay 策略,具体来说为:
- 当训练 batch size <= 64 时,weight decay 不变
- 当训练 batch size > 64 时,weight decay 会根据总 batch size 进行线性缩放
- MMYOLO 也是通过 YOLOv5OptimizerConstructor 实现。
- 梯度累加
- 为了最大化不同 batch size 情况下的性能,作者设置总 batch size 小于 64 时候会自动开启梯度累加功能。
- 训练过程和大部分 YOLO 类似,包括如下策略:
- 没有使用预训练权重
- 没有采用多尺度训练策略,同时可以开启 cudnn.benchmark 进一步加速训练
- 使用了 EMA 策略平滑模型
- 默认采用 AMP 自动混合精度训练
- 需要特意说明的是:YOLOv5 官方对于 small 模型是采用单卡 v100 训练,bs 为 128,而 m/l/x 等是采用不同数目的多卡实现的, 这种训练策略不太规范,为此在 MMYOLO 中全部采用了 8 卡,每卡 16 bs 的设置,同时为了避免性能差异,训练时候开启了 SyncBN。
4.9 RTMDet
- RTMDet:Real-time Models for Object Detection (Release to Manufacture)
- 最近一段时间,开源界涌现出了大量的高精度目标检测项目,其中最突出的就是 YOLO 系列,OpenMMLab 也在与社区的合作下推出了 MMYOLO。 在调研了当前 YOLO 系列的诸多改进模型后,MMDetection 核心开发者针对这些设计以及训练方式进行了经验性的总结,并进行了优化,推出了高精度、低延时的单阶段目标检测器 RTMDet
- RTMDet 由 tiny/s/m/l/x 一系列不同大小的模型组成,为不同的应用场景提供了不同的选择。 其中,RTMDet-x 在 52.6 mAP 的精度下达到了 300+ FPS 的推理速度。
- 下面的推理速度和精度测试(不包含 NMS)是在 1 块 NVIDIA 3090 GPU 上的 TensorRT 8.4.3, cuDNN 8.2.0, FP16, batch size=1 条件里测试的。
- 而最轻量的模型 RTMDet-tiny,在仅有 4M 参数量的情况下也能够达到 40.9 mAP,且推理速度 < 1 ms。

- 上图中的精度是和 300 epoch 训练下的公平对比,为不使用蒸馏的结果。

4.9.1 模型结构
- RTMDet 模型整体结构和 YOLOX 几乎一致,由 CSPNeXt + CSPNeXtPAFPN + 共享卷积权重但分别计算 BN 的 SepBNHead 构成。内部核心模块也是 CSPLayer,但对其中的 Basic Block 进行了改进,提出了 CSPNeXt Block。
4.9.2 正负样本匹配策略
-
正负样本匹配策略或者称为标签匹配策略 Label Assignment 是目标检测模型训练中最核心的问题之一, 更好的标签匹配策略往往能够使得网络更好学习到物体的特征以提高检测能力。
-
早期的样本标签匹配策略一般都是基于 空间以及尺度信息的先验 来决定样本的选取。 典型案例如下(静态匹配策略):
- FCOS 中先限定网格中心点在 GT 内筛选后然后再通过不同特征层限制尺寸来决定正负样本
- RetinaNet 则是通过 Anchor 与 GT 的最大 IOU 匹配来划分正负样本
- YOLOV5 的正负样本则是通过样本的宽高比先筛选一部分, 然后通过位置信息选取 GT 中心落在的 Grid 以及临近的两个作为正样本
-
但是上述方法都是属于基于 先验 的静态匹配策略, 就是样本的选取方式是根据人的经验规定的。 不会随着网络的优化而进行自动优化选取到更好的样本, 近些年涌现了许多优秀的动态标签匹配策略:
- OTA 提出使用 Sinkhorn 迭代求解匹配中的最优传输问题
- YOLOX 中使用 OTA 的近似算法 SimOTA , TOOD 将分类分数以及 IOU 相乘计算 Cost 矩阵进行标签匹配等等
-
这些算法将 预测的 Bboxes 与 GT 的 IoU 和 分类分数 或者是对应分类 Loss 和 回归 Loss 拿来计算 Matching Cost 矩阵再通过 top-k 的方式动态决定样本选取以及样本个数。通过这种方式, 在网络优化的过程中会自动选取对分类或者回归更加敏感有效的位置的样本, 它不再只依赖先验的静态的信息, 而是使用当前的预测结果去动态寻找最优的匹配, 只要模型的预测越准确, 匹配算法求得的结果也会更优秀。但是在网络训练的初期, 网络的分类以及回归是随机初始化, 这个时候还是需要 先验 来约束, 以达到 冷启动 的效果。
-
RTMDet 作者也是采用了动态的 SimOTA 做法,不过其对动态的正负样本分配策略进行了改进。 之前的动态匹配策略( HungarianAssigner 、OTA )往往使用与 Loss 完全一致的代价函数作为匹配的依据,但我们经过实验发现这并不一定时最优的。 使用更多 Soften 的 Cost 以及先验,能够提升性能。
4.9.3 Bbox 编解码过程
- RTMDet 的 BBox Coder 采用的是 mmdet.DistancePointBBoxCoder。
- 该类的 docstring 为 This coder encodes gt bboxes (x1, y1, x2, y2) into (top, bottom, left, right) and decode it back to the original.
- 编码器将 gt bboxes (x1, y1, x2, y2) 编码为 (top, bottom, left, right),并且解码至原图像上。
4.9.4 匹配策略
- RTMDet 提出了 Dynamic Soft Label Assigner 来实现标签的动态匹配策略, 该方法主要包括使用 位置先验信息损失 , 样本回归损失 , 样本分类损失 , 同时对三个损失进行了 Soft 处理进行参数调优, 以达到最佳的动态匹配效果。
4.9.5 Loss 设计
- 参与 Loss 计算的共有两个值:loss_cls 和 loss_bbox,其各自使用的 Loss 方法如下:
- loss_cls:mmdet.QualityFocalLoss
- loss_bbox:mmdet.GIoULoss
权重比例是:loss_cls : loss_bbox = 1 : 2
4.10 YOLOv8
- YOLOv8 是 Ultralytics 公司在 2023 年 1月 10 号开源的 YOLOv5 的下一个重大更新版本,目前支持图像分类、物体检测和实例分割任务,在还没有开源时就收到了用户的广泛关注。
- 按照官方描述,YOLOv8 是一个 SOTA 模型,它建立在以前 YOLO 版本的成功基础上,并引入了新的功能和改进,以进一步提升性能和灵活性。具体创新包括一个新的骨干网络、一个新的 Ancher-Free 检测头和一个新的损失函数,可以在从 CPU 到 GPU 的各种硬件平台上运行。 不过 Ultralytics 并没有直接将开源库命名为 YOLOv8,而是直接使用 Ultralytics 这个词,原因是 Ultralytics 将这个库定位为算法框架,而非某一个特定算法,一个主要特点是可扩展性。其希望这个库不仅仅能够用于 YOLO 系列模型,而是能够支持非 YOLO 模型以及分类分割姿态估计等各类任务。
- 总而言之,Ultralytics 开源库的两个主要优点是:
- 融合众多当前 SOTA 技术于一体
- 未来将支持其他 YOLO 系列以及 YOLO 之外的更多算法

- 下表为官方在 COCO Val 2017 数据集上测试的 mAP、参数量和 FLOPs 结果。可以看出 YOLOv8 相比 YOLOv5 精度提升非常多,但是 N/S/M 模型相应的参数量和 FLOPs 都增加了不少,从上图也可以看出相比 YOLOV5 大部分模型推理速度变慢了。

- 额外提一句,现在各个 YOLO 系列改进算法都在 COCO 上面有明显性能提升,但是在自定义数据集上面的泛化性还没有得到广泛验证,至今依然听到不少关于 YOLOv5 泛化性能较优异的说法。对各系列 YOLO 泛化性验证也是 MMYOLO 中一个特别关心和重点发力的方向。
4.10.1 YOLOv8算法的核心特性和改动
4.10.2 模型结构设计
- 在暂时不考虑 Head 情况下,对比 YOLOv5 和 YOLOv8 的 yaml 配置文件可以发现改动较小。
- Head 部分变化最大,从原先的耦合头变成了解耦头,并且从 YOLOv5 的 Anchor-Based 变成了 Anchor-Free。其结构如下所示:

- 可以看出,不再有之前的 objectness 分支,只有解耦的分类和回归分支,并且其回归分支使用了 Distribution Focal Loss 中提出的积分形式表示法。
4.10.3 Loss计算
- Loss 计算过程包括 2 个部分: 正负样本分配策略和 Loss 计算。
- 现代目标检测器大部分都会在正负样本分配策略上面做文章,典型的如 YOLOX 的 simOTA、TOOD 的 TaskAlignedAssigner 和 RTMDet 的 DynamicSoftLabelAssigner,这类 Assigner 大都是动态分配策略,而 YOLOv5 采用的依然是静态分配策略。
- 考虑到动态分配策略的优异性,YOLOv8 算法中则直接引用了 TOOD 的 TaskAlignedAssigner。 TaskAlignedAssigner 的匹配策略简单总结为: 根据分类与回归的分数加权的分数选择正样本。
t = s α + u β t = s^{\alpha} + u^{\beta} t=sα+uβ
- s s s:标注类别对应的预测分值
- u u u:是预测框和 GT框的 IoU
- 两者相加就可以衡量对齐程度
- 对于每一个 GT,对所有的预测框基于 GT 类别对应分类,预测框与 GT 的 IoU 的加权得到一个关联分类以及回归的对齐分数 alignment_metrics
- 对于每一个 GT,直接基于 alignment_metrics 对齐分数选取 topK 大的作为正样本
- Loss 计算包括 2 个分支: 分类和回归分支,没有了之前的 objectness 分支:
- 分类分支依然采用 BCE Loss
- 回归分支需要和 Distribution Focal Loss 中提出的积分形式表示法绑定,因此使用了 Distribution Focal Loss, 同时还使用了 CIoU Loss
5. 无锚框目标检测算法 (Anchor-free Detectors)
5.1 锚框 vs 无锚框


5.2 FCOS, Fully Convolutional One-Stage (2019)

5.2.1 FCOS 的多尺度匹配

5.2.2 FCOS 的预测目标

5.2.3 中心度 Center-ness

5.2.4 FCOS 的损失函数

5.3 CenterNet (2019)

- 主要流程

6. Detection Transformers
6.1 DETR(2020)

6.2 Deformable DETR (2021)

7. 目标检测模型的评估方法
7.1 检测结果的正确/错误类型

7.2 准确率 Precision 与 召回率 Recall

7.3 准确率与召回率的平衡

7.4 PR 曲线 与 AP(Average Precision) 值
- 计算AP的方法:AP=PR曲线下的面积

7.5 完整数据集上的例子
- PR曲线计算公式如下:
- AccTP:当前所有TP的总和
- AccTP:当前所有FP的总和
- 真值框总数:在下面的示例中,此值为15
P r e c i s i o n = A c c T P A c c T P + A c c F P R e c a l l = A c c T P 真值框总数 Precision = \frac{AccTP}{AccTP + AccFP} \\ Recall = \frac {AccTP}{真值框总数} Precision=AccTP+AccFPAccTPRecall=真值框总数AccTP

7.6 PR 曲线的起伏

7.7 Mean AP (mAP)

8. COCO数据集及配置文件
8.1 COCO 数据集的标注格式
8.2 标注、类别、图像 id 的对应关系

8.3 在 MMDetection 中配置 COCO 数据集

8.4 数据处理流水线


8.5 MMDetection 中的常用训练策略

9. MMDetection实战
9.1 常用命令
mim search mmdet --model "mask r-cnn"
mim download mmdet --config mask_rcnn_r50_fpn_2x_coo --dest .
from mmdet.apis import init_detector, inference_detector, show_result_pyplot
config_file = "mask_rcnn_r50_fpn_2x_coco.py"
checkpoint_file = "mask_rcnn_r50_fpn_2x_coco_bbox_mAP-0.354_20200505_003907-3e542a40.pth"
model = init_detector(config_file, checkpoint_file)
result = inference_detector(model, "demo.jpg")
show_result_pyplot(model, "demo.jpg", result)
_base_ = ['yolov3_mibilenetv2_mstrain-416_300e_coco.py']
from mmcv import Config
config = Config.fromfile('fruit.py')
print(config.pretty_text)
mim train mmdet fruit.py
- 训练自己的数据集及类别

- 修改训练epoch(轮数)、学习率(若在预训练的基础上,则改小)和日志

- 测试在所有测试数据集上的效果
mim test mmdet fruit.py --checkpoint /home/work_dirs/fruit/latest.pth -- show-dir work_dirs/fruit/
9.2 MMDetection环境搭建

9.3 OpenMMLab 项目中的重要概念——配置文件
- 深度学习模型的训练涉及几个方面:
- 模型结构:模型有几层、每层多少通道数等等
- 数据集:即用什么数据训练模型,数据集划分、数据文件路径、数据增强策略等等
- 训练策略:梯度下降算法、学习率参数、batch_size、训练总轮次、学习率变化策略等等
- 运行时: GPU、分布式环境配置等等
- 一些辅助功能 :如打印日志、定时保存checkpoint等等
- 在 OpenMMLab 项目中,所有这些项目都涵盖在一个配置文件中,一个配置文件定义了一个完整的训练
过程
- model:字段定义模型
- data:字段定义数据
- optimizer、lr_config: 等字段定义训练策略
- load_from:字段定义预训练模型的参数文件
9.4 MMDetection代码库结构

9.5 配置文件的运作方式

9.6 两阶段检测器的构成

9.7 单阶段检测器的构成

9.8 RetinaNet 模型配置
9.8.1 主干网络

9.8.2 颈部

9.8.3 bbox head 1

9.8.4 bbox head 2

9.9 训练自己的检测模型
- 训练:寻找最小梯度,使得损失函数最小的这样一组参数的过程

10、常用知识点
10.1 AnchorGenerator (锚框生成器)
anchor_generator=dict(
type='AnchorGenerator',
octave_base_scale=4,
scales_per_octave=3,
ratios=[0.5, 1.0, 2.0],
strides=[8, 16, 32, 64, 128]),
- 每个特征图上有 3 种尺度和 3 种宽高比,每个位置一共 9 个 anchor,并且通过 octave_base_scale 参数来控制全局 anchor 的 base scales ,如果自定义数据集中普遍都是大物体或者小物体,则可能修改更改 octave_base_scale 参数。
- 可视化指定特征图位置的 anchor 情况

- 相同颜色表示在该特征图中基本尺度是相同的,只是宽高比不一样而已
10.2 BBox Assigner(计算每个锚框的样本属性:正、背景、忽略)
- 计算得到输出特征图上每个点对应的原图 anchor 坐标后,就可以和 gt 信息计算每个 anchor 的正负样本属性,对应配置如下:
assigner=dict(
type='MaxIoUAssigner',
pos_iou_thr=0.5,
neg_iou_thr=0.4,
min_pos_iou=0,
ignore_iof_thr=-1)
- 仅从上面的描述可能比较难理解参数含义,通过下面的流程分析就比较容易理解每个参数含义了。MaxIoUAssigner 操作包括 4 个步骤:
- 初始化所有 anchor 为忽略样本
- 计算背景样本
- 计算高质量正样本
- 适当增加更多正样本
10.2.1 初始化所有 anchor 为忽略样本 (1)
- 假设所有输出特征的所有 anchor 总数一共 n 个,对应某张图片中 gt bbox 个数为 m,首先初始化长度为 n 的 assigned_gt_inds,全部赋值为 -1,表示当前全部设置为忽略样本
assigned_gt_inds = overlaps.new_full((num_bboxes, ),
-1,
dtype=torch.long)
10.2.2 计算背景样本(2)
- 将每个 anchor 和所有 gt bbox 计算 IoU,找出最大IoU,如果该最大IoU小于 neg_iou_thr 或者在背景样本阈值范围内,则该 anchor 对应索引位置的 assigned_gt_inds 设置为 0,表示是负样本(背景样本)
max_overlaps, argmax_overlaps = overlaps.max(dim=0)
gt_max_overlaps, gt_argmax_overlaps = overlaps.max(dim=1)
if isinstance(self.neg_iou_thr, float):
assigned_gt_inds[(max_overlaps >= 0)
& (max_overlaps < self.neg_iou_thr)] = 0
elif isinstance(self.neg_iou_thr, tuple):
assert len(self.neg_iou_thr) == 2
assigned_gt_inds[(max_overlaps >= self.neg_iou_thr[0])
& (max_overlaps < self.neg_iou_thr[1])] = 0
10.2.3 计算高质量正样本(3)
- 将每个 anchor 和所有 gt bbox 计算 IoU,找出最大IoU,如果其最大IoU大于等于 pos_iou_thr,则设置该 anchor 对应所有的 assigned_gt_inds 设置为当前匹配 gt bbox 的编号 +1(后面会减掉 1),表示该 anchor 负责预测该 gt bbox,且是高质量 anchor。之所以要加 1,是为了区分背景样本(背景样本的 assigned_gt_inds 值为 0)
pos_inds = max_overlaps >= self.pos_iou_thr
assigned_gt_inds[pos_inds] = argmax_overlaps[pos_inds] + 1
10.2.4 适当增加更多正样本(4)
- 在第三步计算高质量正样本中可能会出现某些 gt bbox 没有分配给任何一个 anchor (由于IoU低于pos_iou_thr),导致该 gt bbox 不被认为是前景物体,此时可以通过 self.match_low_quality=True 配置进行补充正样本。
- 对于每个 gt bbox 需要找出和其最大 IoU 的 anchor 索引,如果其 IoU 大于 min_pos_iou,则将该 anchor 对应索引的 assigned_gt_inds 设置为正样本,表示该 anchor 负责预测对应的 gt bbox。通过本步骤,可以最大程度保证每个 gt bbox 都有相应的 anchor 负责预测,但是如果其最大 IoU 值还是小于 min_pos_iou,则依然不被认为是前景物体。
if self.match_low_quality:
for i in range(num_gts):
if gt_max_overlaps[i] >= self.min_pos_iou:
if self.gt_max_assign_all:
max_iou_inds = overlaps[i, :] == gt_max_overlaps[i]
assigned_gt_inds[max_iou_inds] = i + 1
else:
assigned_gt_inds[gt_argmax_overlaps[i]] = i + 1
- 从这一步可以看出,3 和 4 有部分 anchor 重复分配了,即当某个 gt bbox 和 anchor 的最大 IoU 大于等于 pos_iou_thr,那肯定大于 min_pos_iou,此时 3 和 4 步骤分配的同一个 anchor,并且从上面注释可以看出本步骤可能会引入低质量 anchor,是否需要开启本步骤需要根据不同算法来确定。
- 此时可以可以得到如下总结:
- 如果 anchor 和所有 gt bbox 的最大 IoU 值小于 0.4,那么该 anchor 就是背景样本
- 如果 anchor 和所有 gt bbox 的最大 IoU 值大于等于 0.5,那么该 anchor 就是高质量正样本
- 如果 gt bbox 和所有 anchor 的最大 IoU 值大于等于 0(可以看出每个 gt bbox 都一定有至少一个 anchor 匹配),那么该 gt bbox 所对应的 anchor 也是正样本
- 其余样本全部为忽略样本即 anchor 和所有 gt bbox 的最大 IoU 值处于 [0.4,0.5) 区间的 anchor 为忽略样本,不计算 loss
10.3 BBox Encoder Decoder(平衡损失、加快收敛)
- 在 anchor-based 算法中,为了利用 anchor 信息进行更快更好的收敛,一般会对 head 输出的 bbox 分支 4 个值进行编解码操作,作用有两个:
- 更好的平衡分类和回归分支 loss,以及平衡 bbox 四个预测值的 loss
- 训练过程中引入 anchor 信息,加快收敛
- RetinaNet 采用的编解码函数是主流的 DeltaXYWHBBoxCoder,其配置如下:
bbox_coder=dict(
type='DeltaXYWHBBoxCoder',
target_means=[.0, .0, .0, .0],
target_stds=[1.0, 1.0, 1.0, 1.0]),
- target_means 和 target_stds 相当于对 bbox 回归的 4 个 t x , t y , t w , t h t_x, t_y, t_w, t_h tx,ty,tw,th 进行变换。在不考虑 target_means 和 target_stds 情况下,其编码公式如下:
{ t x = ( x t − x a ) / w a t y = ( y t − y a ) / h a t w = l o g ( w t / w a ) t h = l o g ( h t / h a ) \left\{ \begin{array}{ll} t_x = (x_t - x_a)/w_a \\ t_y = (y_t - y_a)/h_a \\ t_w = log(w_t/w_a) \\ t_h = log(h_t/h_a) \end{array} \right. ⎩ ⎨ ⎧tx=(xt−xa)/waty=(yt−ya)/hatw=log(wt/wa)th=log(ht/ha)
- x t , y t x_t,y_t xt,yt:是 gt bbox 的中心 xy 坐标
- w t , h t w_t,h_t wt,ht:是 gt bbox 的 wh值
- x a , y a x_a,y_a xa,ya:是 anchor 的中心 xy 坐标
- w a , h a w_a,h_a wa,ha:是 anchor 的 wh 值
- t = ( t x , t y , t w , t h ) t = (t_x, t_y, t_w, t_h) t=(tx,ty,tw,th):是 bbox 分支输出的 4 个值对应 targets
- t x , t y t_x, t_y tx,ty:预测值表示 gt bbox 中心相对于anchor 中心点的偏移,并且通过除以 anchor 的 wh 进行归一化
- t w , t h t_w, t_h tw,th:预测值表示 gt bbox 的 wh 除以 anchor 的 wh,然后取 log 非线性变换即可。
10.3.1 编码过程(Encoder)
- 考虑target_means 和 target_stds,其代码如下:
dx = (gx - px) / pw
dy = (gy - py) / ph
dw = torch.log(gw / pw)
dh = torch.log(gh / ph)
deltas = torch.stack([dx, dy, dw, dh], dim=-1)
means = deltas.new_tensor(means).unsqueeze(0)
stds = deltas.new_tensor(stds).unsqueeze(0)
deltas = deltas.sub_(means).div_(stds)
10.3.2 解码过程(Decoder)
means = deltas.new_tensor(means).view(1, -1).repeat(1, deltas.size(1) // 4)
stds = deltas.new_tensor(stds).view(1, -1).repeat(1, deltas.size(1) // 4)
denorm_deltas = deltas * stds + means
dx = denorm_deltas[:, 0::4]
dy = denorm_deltas[:, 1::4]
dw = denorm_deltas[:, 2::4]
dh = denorm_deltas[:, 3::4]
gw = pw * dw.exp()
gh = ph * dh.exp()
gx = px + pw * dx
gy = py + ph * dy
x1 = gx - gw * 0.5
y1 = gy - gh * 0.5
x2 = gx + gw * 0.5
y2 = gy + gh * 0.5
10.4 Focal Loss (易学样本权重比较低,难样本权重比较高)
10.4.1 核心思想
- 出发点:希望one-stage detector(YOLO、SSD)可以达到two-stage detector(Faster R-CNN)的准确率,同时不影响原有的速度。
- 两阶段AP不如一阶段AP的原因:样本的类别不均衡导致的
- 样本不均衡导致的问题:
- 训练低效,因为大多数位置都是易学负样本,没有提供有用的学习信号
- 由易学负样本主导整个训练,使得模型的优化方向并不是我们所希望的那样
- Focal Loss的核心思想:通过减少易分类样本的权重,使得模型在训练时更专注于难分类的样本
- Focal Loss的有效性:为了证明focal loss的有效性,作者设计了一个dense detector:RetinaNet,并且在训练时采用Focal Loss训练。实验证明RetinaNet不仅可以达到one-stage detector的速度,也能有two-stage detector的准确率。
10.4.2 重要性质
- 调制系数(modulating factor):
( 1 − p t ) γ (1-p_t)^{\gamma} (1−pt)γ
- γ \gamma γ:称作focusing parameter ( γ ≥ 0 \gamma \ge 0 γ≥0)
- 性质1:
- 当一个样本被分错的时候, p t p_t pt是很小的,那么调制因子( 1 − p t 1-p_t 1−pt)接近1,损失不被影响
- 当 p t → 1 p_t→1 pt→1,因子( 1 − p t 1-p_t 1−pt)接近0,那么分的比较好的(well-classified)样本的权值就被调低了
- 因此调制系数就趋于1,也就是说相比原来的loss是没有什么大的改变的。当 p t p_t pt趋于1的时候(此时分类正确而且是易分类样本),调制系数趋于0,也就是对于总的loss的贡献很小。
- 性质2:
- 当 γ \gamma γ=0的时候,Focal Loss就是传统的交叉熵损失,当 γ \gamma γ增加的时候,调制系数也会增加。 专注参数 γ \gamma γ平滑地调节了易分样本调低权值的比例。
- γ \gamma γ增大能增强调制因子的影响,实验发现 γ \gamma γ取2最好。
- 直觉上来说,调制因子减少了易分样本的损失贡献,拓宽了样例接收到低损失的范围。当 γ \gamma γ一定的时候,比如等于2,一样easy example( p t = 0.9 p_t=0.9 pt=0.9)的loss要比标准的交叉熵loss小100+倍,当 p t p_t pt=0.968时,要小1000+倍,但是对于hard example( p t p_t pt < 0.5),loss最多小了4倍。这样的话hard example的权重相对就提升了很多。这样就增加了那些误分类的重要性
10.4.3 具体实现
- RetinaNet 一个非常大的亮点就是提出了 Focal Loss
- γ = 0 \gamma = 0 γ=0:表示标准的交叉熵损失(即蓝色线)

- Focal Loss 属于 CE Loss 的动态加权版本,其可以根据样本的难易程度(预测值和 label 的差距可以反映)对每个样本单独加权,易学样本权重比较低,难样本权重比较高。因为在前面的 bbox assigner 环节,大部分样本都是背景易学样本,虽然其本身 loss 比较小,但是由于数目众多最终会主导梯度,从而得到次优模型,而 Focal Loss 通过指数效应把大量易学样本的权重大大降低,从而避免上述问题。
- 完整的Focal Loss为:
l o s s f o c a l = − α t ( 1 − p t ) γ l o g ( p t ) loss_{focal} = - \alpha_t(1-p_t)^{\gamma} log(p_t) lossfocal=−αt(1−pt)γlog(pt)
- α \alpha α:属于正负样本的加权参数,值越大,正样本的权重越大
- γ \gamma γ:有focal效应,可以控制难易样本权重,值越大,对分类错误样本梯度越大(难样本权重大),focal 效应越大,这个参数非常关键;目的是通过减少易分类样本的权重,从而使得模型在训练时更专注于难分类的样本。
- 计算Focal Loss的代码
pred_sigmoid = pred.sigmoid()
target = target.type_as(pred)
pt = (1 - pred_sigmoid) * target + pred_sigmoid * (1 - target)
focal_weight = (alpha * target + (1 - alpha) *
(1 - target)) * pt.pow(gamma)
loss = F.binary_cross_entropy_with_logits(
pred, target, reduction='none') * focal_weight
loss = weight_reduce_loss(loss, weight, reduction, avg_factor)
return loss
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=2.0,
alpha=0.25,
loss_weight=1.0),
loss_bbox=dict(type='L1Loss', loss_weight=1.0))
10.5 交叉熵损失函数(Cross Entropy Loss Function)
- 不仅可以很好的衡量模型的效果,又可以很容易的的进行求导计算。

- SoftMax/Sigmoid:计算对于每个预测结果的概率值
- CrossEntropy损失函数:用于分类问题
10.5.1 二分类
-
在二分类的情况下,模型最后需要预测的结果只有两种情况,对于每个类别我们的预测得到的概率为 p p p 和 1 − p 1-p 1−p ,此时表达式为( l o g log log的底数为 e e e):
L = 1 N ∑ i L i = 1 N ∑ i − [ y i ⋅ l o g ( p i ) + ( 1 − y i ) ⋅ l o g ( 1 − p i ) ] L = \frac{1}{N} \sum_{i} L_i = \frac{1}{N} \sum_i -[y_i \cdot log(p_i) + (1-y_i) \cdot log(1-p_i)] L=N1i∑Li=N1i∑−[yi⋅log(pi)+(1−yi)⋅log(1−pi)]
-
y i y_i yi:表示样本 i i i 的label,正类为 1 1 1, 负类为 0 0 0
-
p i p_i pi:表示样本 i i i 预测为正类的概率
10.5.2 多分类
- 多分类的情况实际上就是对二分类的扩展:
L = 1 N ∑ i L i = − 1 N ∑ i ∑ c = 1 M y i c ⋅ l o g ( p i c ) L = \frac{1}{N} \sum_{i} L_i = - \frac{1}{N} \sum_i \sum_{c=1}^M y_{ic} \cdot log(p_{ic}) L=N1i∑Li=−N1i∑c=1∑Myic⋅log(pic)
- M M M:类别的数量
- y i c y_{ic} yic:符号函数(0或1 ),如果样本 i i i的真实类别等于 c c c取1,否则取0
- p i c p_{ic} pic:观测样本 i i i属于类别 c c c的预测概率
10.5.3 学习过程
- 交叉熵损失函数经常用于分类问题中,特别是在神经网络做分类问题时,也经常使用交叉熵作为损失函数,此外,由于交叉熵涉及到计算每个类别的概率,所以交叉熵几乎每次都和sigmoid(或softmax)函数一起出现。
- 我们用神经网络最后一层输出的情况,来看整个模型预测、获得损失和学习的流程:
- 神经网络最后一层得到每个类别的得分scores(也叫logits);
- 该得分经过sigmoid(或softmax)函数获得概率输出;
- 模型预测的类别概率输出( p i c p_{ic} pic)与真实类别的one hot( y i c y_{ic} yic)形式进行交叉熵损失函数的计算。
- 学习任务分为二分类和多分类情况,我们分别讨论这两种情况的学习过程。
10.5.3.1 二分类情况

- scores是输入的线性函数作用的结果
∂ L i ∂ w i = ∂ L i ∂ p i ⋅ ∂ p i ∂ s i ⋅ ∂ s i ∂ w i = [ σ ( s i ) − y i ] ⋅ x i ∂ s i ∂ w i = x i \frac{\partial L_i}{\partial w_i} = \frac{\partial L_i}{\partial p_i} \cdot \frac{\partial p_i}{\partial s_i} \cdot \frac{\partial s_i}{\partial w_i} = [\sigma(s_i) - y_i] \cdot x_i \\ \frac{\partial s_i}{\partial w_i} = x_i ∂wi∂Li=∂pi∂Li⋅∂si∂pi⋅∂wi∂si=[σ(si)−yi]⋅xi∂wi∂si=xi
- p i p_i pi:表示样本 i i i预测为正类的概率
- y i y_i yi:为符号函数,样本 i i i为正类时取1,否则取0
10.5.3.2 多分类情况

∂ L i ∂ w i c = ∂ L i ∂ p i k ⋅ ∂ p i k ∂ s i c ⋅ ∂ s i c ∂ w i c = [ σ ( s i ) − y i ] ⋅ x i \frac{\partial L_i}{\partial w_{ic}} = \frac{\partial L_i}{\partial p_{ik}} \cdot \frac{\partial p_{ik}}{\partial s_{ic}} \cdot \frac{\partial s_{ic}}{\partial w_{ic}} = [\sigma(s_i) - y_i] \cdot x_i ∂wic∂Li=∂pik∂Li⋅∂sic∂pik⋅∂wic∂sic=[σ(si)−yi]⋅xi
- y i y_i yi:为向量,若真实类别为 k k k,则有:
y i k = 1 y i c = 0 , c ≠ k y_{ik} = 1 \\ y_{ic} = 0, c \neq k yik=1yic=0,c=k
- 交叉熵损失函数对于二分类和多分类求导时,采用向量化的形式后,求导结果的形式是一致的。
10.5.4 优点
- 在用梯度下降法做参数更新的时候,模型学习的速度取决于两个值:
- 其中,学习率是我们需要设置的超参数,所以我们重点关注偏导值。从上面的式子中,我们发现,偏导值的大小取决于 x i x_i xi
和 [ σ ( s ) − y ] [\sigma(s)-y] [σ(s)−y] ,我们重点关注后者,后者的大小值反映了我们模型的错误程度,该值越大,说明模型效果越差,但是该值越大同时也会使得偏导值越大,从而模型学习速度更快。所以,使用逻辑函数得到概率,并结合交叉熵当损失函数时,在模型效果差的时候学习速度比较快,在模型效果好的时候学习速度变慢。
10.5.5 缺点
- Deng [4]在2019年提出了ArcFace Loss,并在论文里说了Softmax Loss的两个缺点:
- 随着分类数目的增大,分类层的线性变化矩阵参数也随着增大;
- 对于封闭集分类问题,学习到的特征是可分离的,但对于开放集人脸识别问题,所学特征却没有足够的区分性。对于人脸识别问题,首先人脸数目(对应分类数目)是很多的,而且会不断有新的人脸进来,不是一个封闭集分类问题。
- 另外,sigmoid(softmax)+cross-entropy loss 擅长于学习类间的信息,因为它采用了类间竞争机制,它只关心对于正确标签预测概率的准确性,忽略了其他非正确标签的差异,导致学习到的特征比较散。基于这个问题的优化有很多,比如对softmax进行改进,如L-Softmax、SM-Softmax、AM-Softmax等。
11 常见错误排除流程

- 参考:https://zhuanlan.zhihu.com/p/35709485