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,在复现过程中解答了一些疑问。通过针对训练不稳定的问题,也指明了可能解决的方案。

YOLOF 速度和效果均超过YOLOv4的检测模型相关推荐

  1. python 实现显著性检测_使用python轻松实现高大上的YOLOV4对象检测算法

    YOLO系列对象检测算法,算是人工智能技术领域的一匹黑马,当开发者宣布不再为YOLO系列检测算法更新时,很多开发者瞬间失去了"精神食粮".突然,当YOLOV4检测算法发布的时候,让 ...

  2. 2021年12月中国车企新能源汽车销量排行榜:Top前三的车企销量远超于其他车企,且市场份额占比均超过10%(附月榜TOP68详单)

    榜单解读: 2021年12月,中国有68家车企有销量,共销售新能源汽车530939辆,销量同比增长了113.94%,销量环比增长了18.02%,产销率为102.49%,清仓库存12900辆:2021年 ...

  3. 股价涨幅均超过200% 揭开三大牛股的画皮

    太行水泥.科力远.鲁信高新是本轮反弹行情中市场出现的三只超级大牛股,股价涨幅均超过200%.他们为何会出现如此猛烈的上涨?2008年业绩究竟怎样?是不是大幅上涨? 随着三家公司2008年年报的披露,罩 ...

  4. 精度45.9%,推理速度72.9FPS,百度飞桨推出工业级目标检测模型 PP-YOLO

    允中 发自 凹非寺 量子位 编辑 | 公众号 QbitAI 工业视觉.自动驾驶.安防.新零售等我们身边熟知的各行各业都需要目标检测技术,由于其很好的平衡了标注成本.检测精度和速度等,成为当前智能制造产 ...

  5. 睿智的目标检测30——Pytorch搭建YoloV4目标检测平台

    睿智的目标检测30--Pytorch搭建YoloV4目标检测平台 学习前言 什么是YOLOV4 代码下载 YOLOV4改进的部分(不完全) YOLOV4结构解析 1.主干特征提取网络Backbone ...

  6. 论文翻译YOLOv4: Optimal Speed and Accuracy of Object Detection YOLOv4:目标检测的最佳速度和精度 论文中文翻译

    摘要 有大量的特征被认为可以提高卷积神经网络(CNN)的精度.需要在大型数据集上对这些特性的组合进行实际测试,并对结果进行理论验证.某些特性在特定的建模中起决定性作用,而在特定的强制确定问题中起决定性 ...

  7. 超轻量目标检测模型NanoDet(速度很快)PyTorch版本实践

    文章目录 前言 NanoDet 模型介绍 1)NanoDet 模型性能 2)NanoDet 模型架构 3)NanoDet损失函数 4)NanoDet 优势 基于PyTorch 实现NanoDet 1) ...

  8. yolov4 火灾检测,烟雾检测、 古文预训练语言模型等AI开源项目分享

    ~ 文末免费送书 ~ 项目一:FinBERT基于 BERT 架构的金融领域预训练语言模型 项目地址: https://github.com/valuesimplex/FinBERT 为了促进自然语言处 ...

  9. YOLO-v4目标检测实时手机端实现

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 转自 | 计算机视觉研究院 由美国东北大学王言治教授研究团队与美国 ...

最新文章

  1. 20162311 算法复杂度-3
  2. 安装和配置本地maven(三)
  3. jQuery 内容文本值|| 案例:购物车案例模块-增减商品数量 || 案例:购物车案例模块-修改商品小计
  4. wpf 用户自定义事件传参
  5. 在iOS的XCode工程配置中为什么要用-all_load-ObjC
  6. PAT-乙级-1020. 月饼 (25)
  7. mysql特性举例_MySQL事务的四大特性和隔离级别
  8. [js] 你有使用过pjax吗?它的原理是什么?
  9. mysql的几种模式_MYSQL复制的几种模式
  10. c++获取子类窗口句柄位置_干货分享:用一百行代码做一个C/C++表白小程序,程序员的浪漫!...
  11. java 错误码设计_关于Java中异常的设计
  12. eclipse—安装ADT插件搭建安卓开发环境
  13. 借助Intent实现Android工程中Activity之间Java对象的传递——实现Serializable接口
  14. win10北通手柄没反应_赛博朋克2077正式发售,光靠键鼠可不行,试试北通阿修罗3...
  15. 坐飞机还是尽量早点出发(差点误机)
  16. 截止11月5日,30日内累计跌幅最大的200只股票
  17. DelphiXE10.4安卓编程初学者心得
  18. 【博客427】通过redfish协议操控服务器
  19. 一路山水到了这僻静的温柔乡
  20. 大牛书单 | C++ 好书推荐

热门文章

  1. Linux Watchdog 机制
  2. linux shell case语句
  3. html5制作交互式课件,用flash制作交互式课件.ppt
  4. java close 方法,用Layman的术语解释Java中的close()方法
  5. 使用ELK 搭建core文件展示平台
  6. 部门名称部门结构叠用_DYT|部门名称创意设计比赛,你pick哪一个?
  7. pyqt5切换python版本_PyQt5每天必学之切换按钮_python
  8. 弹性理论法研究桩基受力计算公式_收藏!桩基检测的7种方法
  9. css里dom宽度,2019-08-23 DOM中各种高度、宽度
  10. lighttpd php7 源码安装,如何在CentOS 7上安装Lighttpd与PHP-FPM和MariaDB