YOLOF 速度和效果均超过YOLOv4的检测模型

YOLOF(You Only Look One-level Feature)是一种无需复杂FPN模块的检测模型,通过单尺度特征实现与YOLOv4相当的效果和更快的推理速度。研究发现FPN的主要优势在于其分治策略而非多尺度融合,因此YOLOF提出Dilated Encoder和Uniform Matching策略,以简化结构并提高效率。Dilated Encoder利用不同空洞率的残差模块扩大感受野,Uniform Matching策略确保不同大小物体有相似数量的正样本,实现更好的分治效果。
摘要由CSDN通过智能技术生成

 

YOLOF 全称是 You Only Look One-level Feature, 其通过详细的实验指出特征金字塔 FPN 模块的成功在于其对目标优化问题的分治解决方案,而不是我们常说的多尺度特征融合。针对该结论,设计了一个简洁优雅的无需复杂 FPN 的网络结构,仅仅依靠单尺度特征即可取得相匹配的效果,并且具备极快的推理速度,是一个不错的算法。

YOLOF 论文核心可以总结如下:

  • 设计了多组实验,深入探讨了 FPN 模块成功的主要因素

  • 基于实验结论,设计了无需 FPN 模块,单尺度简单高效的 Neck 模块 Dilated Encoder

  • 基于 FPN 分治处理多尺度问题,配合 Neck 模块提出 Uniform Matching 正负样本匹配策略

  • 由于不存在复杂且耗内存极多的 FPN 模块,YOLOF 可以在保存高精度的前提下,推理速度快,消耗内存也相对更小


项目地址:github.com/open-mmlab/mmdetection,欢迎 star~

1 FPN 模块分析

 

图片

 


首先目标检测算法可以简单按照上述结构进行划分,网络部分主要分为 Backbone、Encoder 和 Decoder,或者按照我们前系列解读文章划分方法分为 Backbone、Neck 和 Head。对于单阶段算法来说,常见的 Backbone 是 ResNet,Encoder 或者 Neck 是 FPN,而 Head 就是对应的输出层结构。

一般我们都认为 FPN 层作用非常大,不可或缺,其通过特征多尺度融合,可以有效解决尺度变换预测问题。而本文认为 FPN 至少有两个主要作用:

  • 多尺度特征融合

  • 分治策略,可以将不同大小的物体分配到不同大小的的输出层上,克服尺度预测问题

作者试图分析上述两个作用中,最核心的部分,故选择最常用的 RetinaNet 进行 FPN 模块深入分析。

图片

 

 

对 FPN 模块进一步抽象,如上图所示,可以分成 4 种结构 MiMo、SiMo、MiSo 和 SiSo,其中 MiMo 即为标准的 FPN结构,输入和输出都包括多尺度特征图。将 FPN 替换为上述 4 个模块,然后基于 RetinaNet 重新训练,计算 mAP 、 GFLOPs 和 FPS 指标

  • 从 mAP 角度分析,SiMo 结果和 MiMo 差距不大,说明 C5 (Backbone 输出)包含了足够的检测不同尺度目标的上下文信息;而 MiSo 和 SiSo 则和 MiMo 差距较大,说明 FPN 分治优化作用远远大于 多尺度特征融合

  • 从下表 GFLOPs 和 FPS 可以看出,MiMo 结构由于存在高分辨率特征图 C3 会带来较大的计算量,并且拖慢速度

综上所示,可以得到一些结论:

  • FPN 模块的主要增益来自于其分治优化手段,而不是多尺度特征融合

  • FPN 模块中存在高分辨率特征融合过程,导致消耗内存比较多,训练和推理速度也比较慢,对部署不太优化

  • 如果想在抛弃 FPN 模块的前提下精度不丢失,那么主要问题是提供分治优化替代手段

2 YOLOF 原理简析

作者为了克服 FPN 存在的内存占用多,速度慢问题,采用了 SiSo 结构,但是精度下降比较严重,从 35.9 变成了 24.6,故后续有针对性的改进,主要包括两个部分: Dilated Encoder 和 Uniform Matching。

2.1 Dilated Encoder

图片

 

 

虽然 FPN 的主要作用是分治优化思想,但是多尺度融合也有一定作用,从上述实验也可以看出。并且虽然 C5 提供了足够的上下文,但是其感受野所对应的目标尺寸范围是有限的,无法应对目标检测场景中变化剧烈的目标尺寸。简要理解如上图所示,绿色点表示数据集中的多种目标尺寸,粉红色区域代表特征图能够有效表达的目标尺寸范围

  • 如果仅仅使用 C5 特征,会出现图(a)所示的情况

  • 若使用空洞卷积操作来增大 C5 特征图的感受野,则会出现图(b)所示的情况,感受野变大,能够有效地表达尺寸较大的目标,但是对小目标表达能力会变差

  • 如果采用不同空洞率的叠加,则可以有效避免上述问题

为此,作者设计了 Dilated Encoder 结构,串联多个不同空洞率的模块以覆盖不同大小物体,改善感受野单一问题,如下所示:

图片

 

对 C5 特征先进行压缩通道,然后串联 4 个不同空洞率的残差模块,从而得到不同感受野的特征图。其实这种做法非常场景,在语义分割算法 ASPP 中和目标检测算法 RFBNet 都采用了类似思想,只不过这两个都是并联结构,而本文是串联, RFBNet 结构如下所示:

 

图片

 

 

2.2 Uniform Matching
前面说过 FPN 的核心功能是分治手段,但是我们知道虽然其输出多个尺度特征图,但是要想发挥分治功能则主要依靠 bbox 正负样本分配策略,也就是说 FPN 和优异的 bbox 正负样本分配策略结合才能最大程度发挥功效,大部分最新的单阶段目标检测算法都在 bbox 分配策略上面做文章,可以借用 AutoAssign 论文中的图说明:

 

 


为了充分发挥 FPN 功效,一般会从 scale 和 spatial 两个方面着手进行设计,scale 用于处理不同尺度大小的 gt bbox 应该属于哪些输出层负责,而 spatial 用于处理在某个输出特征图上哪些位置才是最合适的正样本点。不同的 bbox 正负样本分配策略对最终性能影响极大。

一般来说,由于自然场景中,大小物体分布本身就不均匀,并且大物体在图片中所占区域较大,如果不设计好,会导致大物体的正样本数远远多于小物体,最终性能就会偏向大物体,导致整体性能较差。YOLOF 算法采用单尺度特征图输出,锚点的数量会大量的减少(比如从 100K 减少到 5K),导致了稀疏锚点,如果不进行重新设计,会加剧上述现象。为此作者提出了新的均匀匹配策略,核心思想就是不同大小物体都尽量有相同数目的正样本。

图片

 

所提两个模块的作用如下所示:

图片

 

 

  • Uniform Matching 作用非常大,说明该模块其实发挥了 FPN 的分治作用

  • Dilated Encoder 配合 Uniform Matching 可以提供额外的变感受野功能,有助于多尺度物体预测

需要特别注意:论文中所描述的 Uniform Matching 和代码中实现的 Uniform Matching 有一定差距,在下一节源码解读时会详细说明。

3 YOLOF 源码解析

和前系列解读一样,依然按照 Backbone、Neck、Head、Bbox Coder、Bbox Assigner 和 Loss 顺序解读。

3.1 Backbone
Backbone 采用了 ResNet50,caffe 模式,和 FCOS 算法配置相同,只不过这里只需要输出 C5 特征图即可,不需要多尺度。

pretrained='open-mmlab://detectron/resnet50_caffe', 
backbone=dict( 
    type='ResNet', 
    depth=50, 
    num_stages=4, 
    out_indices=(3, ), 
    frozen_stages=1, 
    norm_cfg=dict(type='BN', requires_grad=False), 
    norm_eval=True, 
    style='caffe'), 

 

3.2 Neck
Neck 模块是本文新提出的 Dilated Encoder 模块,包括一个通道压缩模块,然后串联 4 个不同空洞率的残差模块,提供灵活变尺度的感受野。

neck=dict( 
    type='DilatedEncoder', 
    in_channels=2048, 
    out_channels=512, 
    block_mid_channels=128, 
    num_residual_blocks=4), 

由于比较简单,就不展开分析了。


3.3 Head

 

图片

 

Head 模块即上图的 Decoder 结构,包括分类和回归分支,借鉴 AutoAssign 算法,在回归分支上并行引入一个 Objectness 分支,用于抑制背景区域的高响应,然后将其和分类分支相乘。故对外实际上两个分支,Objectness是没有监督 label 的。其 Head forward 如下所示

def forward_single(self, feature): 
    # 分类分支 
    cls_score = self.cls_score(self.cls_subnet(feature)) 
    N, _, H, W = cls_score.shape 
    cls_score = cls_score.view(N, -1, self.num_classes, H, W) 
 
    # 回归分支 
    reg_feat = self.bbox_subnet(feature) 
    bbox_reg = self.bbox_pred(reg_feat) 
    objectness = self.object_pred(reg_feat) 
    # implicit objectness 
    objectness = objectness.view(N, -1, 1, H, W) 
 
    normalized_cls_score = cls_score + objectness - torch.log( 
        1. + torch.clamp(cls_score.exp(), max=INF) + 
        torch.clamp(objectness.exp(), max=INF)) 
    normalized_cls_score = normalized_cls_score.view(N, -1, H, W) 
    return normalized_cls_score, bbox_reg

上述得到 normalized_cls_score 的计算过程看起来非常复杂,但是其实是为了能够对融合后的 normalized_cls_score 采用 sigmoid 函数而已,其对应的公式是:

 
     

也就是说 normalized_cls_score 是不含 sigmoid 的包括 cls_score 和 objectness 融合的值。可以通过如下简单代码验证:

import torch 
 
if __name__ == '__main__': 
    INF = 1e8 
    N = 1 
    num_classes = 2 
    H = W = 3 
    cls_score = torch.rand((N, 1, num_classes, H, W)) 
    objectness = torch.rand(N, 1, 1, H, W) 
 
    normalized_cls_score = cls_score + objectness - torch.log( 
        1. + torch.clamp(cls_score.exp(), max=INF) + 
        torch.clamp(objectness.exp(), max=INF)) 
 
    cls_score_s = torch.sigmoid(cls_score) * torch.sigmoid(objectness) 
 
    assert torch.allclose(cls_score_s, torch.sigmoid(normalized_cls_score)) 


3.4 Bbox Coder
YOLOF 输出格式采用 RetinaNet 算法中定义的 deltaXYWH ,即回归分支输出的 4 个值表示相对于 anchor 的偏移

anchor_generator=dict( 
    type='AnchorGenerator', 
    ratios=[1.0], 
    scales=[1, 2, 4, 8, 16], 
    strides=[32]), 
bbox_coder=dict( 
    type='DeltaXYWHBBoxCoder', 
    target_means=[.0, .0, .0, .0], 
    target_stds=[1., 1., 1., 1.], 
    add_ctr_clamp=True, 
    ctr_clamp=32), 

只有一个输出特征图,每个位置铺设了 5 个 anchor,宽高比是 1,设置了 5 种 scale。为了稳定训练过程,作者在 DeltaXYWHBBoxCoder 中引入了 add_ctr_clamp 参数即当中心坐标预测相比 anchor 偏离大于 32 个像素,则强制裁剪为 32,防止产生较大的梯度。

3.5 Bbox Assigner
这个部分是 YOLOF 的核心,需要重点分析。首先分析论文中描述,然后再基于代码说明代码和论文的差异。
论文中描述的非常简单,核心目的是保证不同尺度物体都尽可能有相同数目的正样本

  • 遍历每个 gt bbox,然后选择 topk 个距离最近的 anchor 作为其匹配的正样本

  • 由于存在极端比例物体和小物体,上述强制 topk 操作可能出现 anchor 和 gt bbox 的不匹配现象,为了防止噪声样本影响,在所有正样本点中,将 anchor 和 gt bbox 的 iou 低于 0.15 的正样本(因为不管匹配情况,topk 都会选择出指定数目的正样本)强制认为是忽略样本,在所有负样本点中,将 anchor 和 gt bbox 的 iou 高于 0.75 的负样本(可能该物体比较大,导致很多 anchor 都能够和该 gt bbox 很好的匹配,这些样本就不适合作为负样本了)强制认为是忽略样本

实际上作者代码的写法如下所示

  • 遍历每个 gt bbox,然后选择 topk 个距离最近的 anchor 作为其匹配的正样本

  • 遍历每个 gt bbox,然后选择 topk 个距离最近的预测框作为补充的匹配正样本

  • 计算 gt bbox 和预测框的 iou,在所有负样本点中,将 iou 高于 0.75 的负样本强制认为是忽略样本

  • 计算 gt bbox 和 anchor 的 iou,在所有正样本点中,将 iou 低于 0.15 的正样本强制认为是忽略样本

可以发现相比于论文描述,实际上代码额外动态补充了一定量的正样本,同时也额外考虑了一些忽略样本。相比于纯粹采用 anchor 和 gt bbox 进行匹配,额外引入预测框,可以动态调整正负样本,理论上会更好。

# 全部任务是负样本 
assigned_gt_inds = bbox_pred.new_full((num_bboxes, ), 
                                      0, 
                                      dtype=torch.long) 
 
# 计算两两直接的距离,包括 预测框和 gt bbox,以及 anchor 和 gt bbox 
cost_bbox = torch.cdist( 
    bbox_xyxy_to_cxcywh(bbox_pred), 
    bbox_xyxy_to_cxcywh(gt_bboxes), 
    p=1) 
cost_bbox_anchors = torch.cdist( 
    bbox_xyxy_to_cxcywh(anchor), bbox_xyxy_to_cxcywh(gt_bboxes), p=1)    
 
# 分别提取 topk 个样本点作为正样本,此时正样本数会加倍 
index = torch.topk( 
    C,  
    k=self.match_times, 
    dim=0, 
    largest=False)[1] 
# self.match_times x n 
index1 = torch.topk(C1, k=self.match_times, dim=0, largest=False)[1] 
# (self.match_times*2) x n 
indexes = torch.cat((index, index1), 
                    dim=1).reshape(-1).to(bbox_pred.device) 
 
# 计算 iou 矩阵 
pred_overlaps = self.iou_calculator(bbox_pred, gt_bboxes) 
anchor_overlaps = self.iou_calculator(anchor, gt_bboxes) 
pred_max_overlaps, _ = pred_overlaps.max(dim=1) 
anchor_max_overlaps, _ = anchor_overlaps.max(dim=0) 
 
# 计算 gt bbox 和预测框的 iou,在所有负样本点中,将 iou 高于 0.75 的负样本强制认为是忽略样本 
ignore_idx = pred_max_overlaps > self.neg_ignore_thr 
assigned_gt_inds[ignore_idx] = -1 
 
# 计算 gt bbox 和 anchor 的 iou,在所有正样本点中,将 iou 低于 0.15 的正样本强制认为是忽略样本 
pos_gt_index = torch.arange( 
    0, C1.size(1), 
    device=bbox_pred.device).repeat(self.match_times * 2) 
pos_ious = anchor_overlaps[indexes, pos_gt_index] 
pos_ignore_idx = pos_ious < self.pos_ignore_thr 
pos_gt_index_with_ignore = pos_gt_index + 1 
pos_gt_index_with_ignore[pos_ignore_idx] = -1 
assigned_gt_inds[indexes] = pos_gt_index_with_ignore  


3.6 Loss
在确定了每个特征点位置哪些是正样本和负样本后,就可以计算 loss 了,分类采用 focal loss,回归采用 giou loss,都是常规操作。

loss_cls=dict( 
    type='FocalLoss', 
    use_sigmoid=True, 
    gamma=2.0, 
    alpha=0.25, 
    loss_weight=1.0), 
loss_bbox=dict(type='GIoULoss', loss_weight=1.0)) 


上述就是整个 YOLOF 核心实现过程。至于推理过程和 RetinaNet 算法完全相同。

4 YOLOF 复现心得和体会

如果不仔细思考,可能看不出上述代码有啥问题,实际上在 Bbox Assigner 环节会存在重复索引分配问题,这个问题会带来几个影响。具体代码是:

# 对应 3.5 小节的源码分析第 44 行 
assigned_gt_inds[indexes] = pos_gt_index_with_ignore 

前面说过,YOLOF 会引入额外的预测框点作为补充正样本,当 2 次 topk 选择的位置相同时候就会出现意想不到问题。

举个简单例子,当前图片中仅仅有一个 gt bbox,且预测输出特征图大小是 10x10,设置 anchor 个数是 1,那么说明输出特征图上只有 10x10 个anchor,并且对应了 10x10 个预测框,topk 设置为 4

  • 计算该 gt bbox 和 100 个 anchor 的距离,然后选择最近的前 4 个位置作为正样本

  • 计算该 gt bbox 和 100 个预测框的距离,然后选择最近的前 4 个位置作为正样本,注意这里选择的 4个位置很可能和前面选择的 4 个位置有重复

  • 计算该 gt bbox 和预测框的 iou,在所有负样本点中,将 iou 高于 0.75 的负样本强制认为是忽略样本

  • 计算该 gt bbox 和 anchor 的 iou,在所有正样本点中,将 iou 低于 0.15 的正样本强制认为是忽略样本,注意和上一步的区别,由于 iou 计算的输入是不一样的,可能导致某个被重复计算的正样本位置出现 2 种情况:1. 两个步骤都认为是忽略样本;2. 一个认为是忽略样本,一个认为是正样本,而一旦出现第二种情况则在 CUDA 并行计算中出现不确定输出

简单来说:indexes 中可能存在重复值,并且重复值位置对应的 pos_gt_index_with_ignore 可能相同也可能不同,注意重复现象可能出现在两个不同类别物体有重叠的情况下,那么上述赋值操作在 CUDA 中是不可预知的,可能会出现同一份数据跑两次输出结果不一样。

这个操作会给后面的回归分支带来歧义,因为回归分支仅仅处理正样本,那么会出现以下几个情况:

  • 如果两个重复索引处对应的 gt bbox 是同一个,那么相当于该 gt bbox 对应的正样本 loss 权重加倍

  • 如果两个重复索引处对应的 gt bbox 不是同一个,那么就会出现歧义,因为特征图上同一个预测点,被同时分配给了两个不同的 gt bbox

总的来说,对于上述重复索引分配现象,会带来几个影响:

  • 读者理解代码运行流程会比较困惑

  • 同一个程序跑多次,可能输出结果不一致

  • 训练过程不稳定

  • 当重复索引出现时候,回归分支 loss 计算过程非常奇怪,难以理解

  • 低版本 CUDA 上会出现非法内存越界错误, 实验发现 CUDA9.0 会出现非法内存越界错误,但是 CUDA10.1 则正常,其余版本没有进行测试

关于第5点,原因暂时不清楚,但是现象是 pos_gt_index_with_ignore、indexes 和 assigned_gt_inds 都不存在越界情况,只不过 indexes 如果存在相同值,在赋值后会出现 4294967295(2^32 -1) 和 -4294967295 (-2^32 +1) 异常值,然后后续基于 assigned_gt_inds 取值后就出现出现 RuntimeError: CUDA error: an illegal memory access was encountered. 经过多次实验发现,错误是必现的。当时也试过其他几个方案,例如 1. 将上述赋值操作放置到 cpu 上进行;2. 将赋值后异常值全部设置为忽略样本,虽然可以避免报错,但是实验结果显示会存在一定程度的掉点,所以最终没有修改。这个问题我们也会持续关注,直到找到一个更加合适的方式以避免上述报错问题。

上述这个写法,给代码复现带来了些问题,并且由于 YOLOF 学习率非常高 lr=0.12,训练过程偶尔会出现 Nan 现象,训练不太稳定,可能对参数设置例如 warmup 比较敏感。

最后还是要感谢作者 Qiang Chen,在复现过程中解答了一些疑问。通过针对训练不稳定的问题,也指明了可能解决的方案。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值