跳过正文
  1. Posts/

从 Faster R-CNN 到 Anchor-Free:经典目标检测架构到底在解决什么问题

·5558 字·12 分钟

以前在商汤做 CV 的时候,Faster R-CNN、SSD、FPN、RetinaNet 这些名字是天天打交道的。模型训练、转换、部署,各种坑也踩了不少——TensorRT 版本间 softmax axis 默认值不一致RoI Align 的 num_roi_per_image 默认 300 导致多 batch 结果错乱,这类问题当年 debug 到怀疑人生。

但几年不做 CV 之后,发现这些模型的结构细节已经忘得差不多了。最近重新复习 CNN 的时候想把目标检测重新过一遍,发现比起重新记住每个网络有几层、anchor 设了几种 ratio,更值得重新理解的是:这些模型到底在逐步解决什么问题,以及每一步架构变化删除了什么、改变了什么。

这篇文章就是重新梳理这条线索。不会重复以前写过的实现细节——那些旧文都还在,有需要的地方直接链过去。这篇更像是给那些零散笔记补一个统一的入口和框架。

检测比分类多出了什么
#

图像分类的任务相对干净:输入一张图,输出一个固定维度的结果,比如"这是一只猫"。

目标检测不一样。它需要输出一个不定长的集合,每个元素包含类别、置信度、位置:

1(class, confidence, x1, y1, x2, y2) × N

这里 N 不是固定的。一张图里可能有 1 个 object,可能有 50 个,也可能一个都没有。

分类 vs 检测

把这个差异拆开来看,检测相比分类至少多出三个子问题:

  1. Classification — 这个位置的东西属于什么类别?
  2. Localization — 这个物体精确在哪里?具体的 bounding box 坐标是什么?
  3. Candidate generation — 到底应该在图像的哪些位置去尝试检测?

前两个问题相对直觉。第三个问题反而是我这次重新看下来觉得最关键的:在哪里做 prediction?

一张图里 object 的数量不固定、位置不固定、尺度不固定、宽高比不固定。如果不知道该在哪里预测,怎么办?最简单的回答就是——到处都试。

滑动窗口:最暴力的起点
#

最直接的思路:

1for every position:
2    for every scale:
3        for every aspect ratio:
4            crop a window → classifier(crop)

在每个位置、每个尺度、每个宽高比上裁一块图,扔进分类器判断有没有 object。

问题很明显:组合空间太大,大量窗口之间高度重叠,CNN 对重复区域做了大量重复计算。

这部分以前单独记过,具体可以看《目标检测领域的滑动窗口算法》

但滑动窗口引出一个很自然的想法:既然 CNN 本来就是在整张图上做卷积,feature map 天然覆盖了所有空间位置——那是不是应该先提取一次 feature map,然后在 feature map 上寻找 object,而不是对每个 crop 重复跑 CNN?

这就是 region-based detector 的出发点。

Faster R-CNN:把问题拆成两步
#

Faster R-CNN 的核心不是"多了一个 RPN",而是它对 detection 问题做了一次 two-stage decomposition

Faster R-CNN two-stage pipeline

Stage 1(RPN) 回答的是:这里有没有一个值得进一步看的 object?

它不关心具体类别,只做两件事:objectness 二分类(前景还是背景)和粗糙的 bounding box regression。它的输出是大约 300 个 region proposals——从几万个 candidate 里筛选出的少量"可疑区域"。

Stage 2 针对这少量 proposal,用 RoI Pooling(或后来改进的 RoI Align)从 feature map 上裁出对应区域的特征,然后做更精细的分类和 box regression。

这个结构最值得记住的 insight 其实是一种非常经典的系统设计思路:coarse-to-fine

1大量便宜判断(RPN:~20k anchors → ~300 proposals)
23少量昂贵判断(per-RoI classification)

先用轻量级网络对所有位置做一轮粗筛,把候选从几万缩减到几百,然后再对这几百个做计算量大得多的精细判断。这不只是 CV 里的技巧,搜索引擎的召回-排序、推荐系统的多阶段漏斗,都是类似的 coarse-to-fine 结构。

Faster R-CNN 的具体实现——RPN 网络结构、RoI Pooling vs RoI Align、proposal layer 的后处理细节——以前都详细记过,参考《Faster Rcnn 目标检测算法》。那篇文章里还包括了我当年踩过的几个工程坑:anchor 不一致、RoI Align 默认参数、softmax reshape 维度等。这些细节这里就不再重复了。

Anchor 到底是什么
#

理解 anchor 是理解后面 anchor-free 为什么会出现的前提。

很多介绍把 anchor 和最终的 predicted box 混在一起说,但实际上它们是不同的东西。

Anchor 不是预测框。Anchor 是 bounding box regression 的 reference——一个预先定义好的初始假设。

Anchor 与 bbox regression

在 feature map 的每个位置上,会预先放置若干组不同尺度和宽高比的 anchor,比如:

1small square, large square, wide rectangle, tall rectangle, ...

网络实际预测的不是 box 的绝对坐标,而是相对于这个 anchor 的偏移量:

1Δx, Δy, Δw, Δh

也就是说,网络在回答:从这个 reference box 出发,应该怎么平移和缩放,才能变成 object 的 box?

训练的时候,每个 anchor 和 ground truth 算 IoU,根据阈值分成 positive / negative / ignore,positive 的 anchor 负责学习到对应 GT 的 regression offset。

这套机制其实引入了三个后面会反复出现的问题:

  1. Anchor 数量巨大 — 比如一个 feature map 上每个位置 9 种 anchor,整张图轻松几万个
  2. Foreground / background 极端不平衡 — 几万个 anchor 里真正 positive 的可能只有几百个
  3. Anchor 的 scale、ratio、数量是人工先验 — 需要根据数据集和任务手动调整

这三个问题分别在后面催生了 Focal Loss 和 Anchor-Free。

以前写过一篇关于 anchor 的记录,具体细节见《Anchor Box Algorithm》

SSD:删除 proposal stage
#

Faster R-CNN 精度不错,但 two-stage 的计算链路在部署时偏重。实际业务上也是这样——当时公有云上检测模型几乎都是 Faster R-CNN,但到了嵌入式设备和端侧盒子,就全换成 SSD 了。

SSD 做的关键变化不是"换了一个 backbone",而是删除了 proposal stage,把 detection 直接变成 dense prediction

Faster R-CNN 是:

Faster R-CNN pipeline

SSD 是:

SSD pipeline

区别一目了然:SSD 在多个 feature map 的每个位置上直接预测 class + bbox,不再经过一个专门的 proposal 筛选阶段。

需要注意的是,SSD 里的 prior box 和 Faster R-CNN 的 anchor 在思想上没有本质区别。都是预定义的 reference box,网络预测的都是相对于 reference 的 offset。SSD 并没有摆脱 anchor 机制。

SSD 的 prior box、decode box 实现、multi-scale feature map 的具体细节以前记过,参考《SSD: Single Shot MultiBox Detector 学习笔记》。那篇里也提到了 variance 这个原始 paper 没有详细说明的实现参数,以及 pytorch 和 caffe 之间 prior box 顺序不一致的问题。

为什么检测特别需要 feature hierarchy
#

SSD 在多个不同 scale 的 feature map 上做检测,这不是随便选择的——是因为目标检测有一个分类任务中不存在的现实问题:object 的尺度差异非常大。

同一张图里可能同时出现占 300×300 像素的大 object 和只有 15×15 像素的小 object。

CNN 的不同深度天然对应不同的 scale 特性:

1浅层 → spatial resolution 高, receptive field 小, semantic abstraction 弱
2深层 → spatial resolution 低, receptive field 大, semantic abstraction 强

SSD 的做法是直接在不同深度的 feature map 上分别做 detection:浅层检测小 object,深层检测大 object。

但这里有一个矛盾:浅层 feature map 虽然 resolution 高,适合定位小 object,但它的 semantic information 相对弱。也就是说,它看得清细节但"理解力"不够。

这个矛盾指向了下一个关键的架构变化。

FPN:不只是"多几个 feature map"
#

我第一次看 FPN 的时候,直觉反应是"不就是用了多个 scale 的 feature map 嘛,SSD 也这么做了"。重新看一遍才意识到,FPN 真正解决的问题和 SSD 不一样。

SSD 确实用了多个 scale 的 feature map,但每一层只用自己的特征——浅层有 resolution 但缺 semantics,深层有 semantics 但丢了 resolution。

FPN 的做法是:通过 top-down pathway 把深层的 strong semantics 传回高 resolution 的 feature map。

FPN 结构

Bottom-up 就是普通的 backbone forward,逐层降低分辨率、增强语义。Top-down 则反过来,把深层特征 upsample 后和浅层特征通过 lateral connection 做逐元素相加。

最终每一层 P_i 都同时拥有:对应尺度的 spatial resolution 和来自更深层传递下来的 strong semantics。

FPN 真正解决的是 resolution 与 semantics 之间的矛盾,而不是简单的"多尺度"。

具体的代码实现以前记过,包括 bottom-up / top-down 的 PyTorch 代码、FPN 作用于 RPN 和 Fast R-CNN 的方式、roi 到 feature level 的分配策略。参考《FPN:Feature Pyramid Networks 学习笔记》

RetinaNet:one-stage 的真正瓶颈在哪
#

很长一段时间里,one-stage detector 的精度一直追不上 two-stage。RetinaNet 这篇论文最重要的贡献不是提出了一个新的 detector 架构,而是回答了一个关键问题:

为什么 one-stage 比 two-stage 难训练?

答案不是 one-stage 的 model capacity 不够,而是 dense detection 中 foreground / background 的极端不平衡。

Focal Loss 效果示意

一个 dense detector 可能产生 100,000 个候选位置。其中真正的 positive 可能只有几百个,剩下 99% 以上都是 background。而且这些 background 中绝大多数是 easy negative——天空、墙壁、空白区域——模型早就学会了它们是背景,但因为数量太大,它们仍然占据了大量的 loss 和 gradient。

Focal Loss 的公式是:

$$ FL(p_t) = -(1-p_t)^\gamma \log(p_t) $$

相比普通的 cross-entropy,多了一个 \((1-p_t)^\gamma\) 调制因子。

直觉很简单:如果 \(p_t\) 非常高(比如 0.999),说明模型对这个样本已经非常确信,\((1-p_t)^\gamma\) 就非常小,这个样本几乎不再贡献 loss。如果 \(p_t\) 比较低(比如 0.5),说明模型仍然困难,loss 基本保持不变。

也就是说:不让数量巨大的 easy samples 消耗训练预算,把学习 capacity 留给 hard examples。

Faster R-CNN 之所以没有这个问题,是因为 RPN 已经把绝大多数 easy background 在第一阶段就筛掉了——proposal 阶段本身就是一种 hard example mining。而 one-stage detector 没有这个筛选,所有候选位置直接参与最终 loss,不平衡就暴露出来了。

RetinaNet 的历史意义之一在于:它把 one-stage detector 的精度问题重新解释成了一个 class imbalance / optimization 问题,而不是 architecture capacity 问题。这是一种很重要的问题重构。

Focal Loss 的曲线和 RetinaNet 的整体架构(FPN backbone + class/box subnet)以前记过,参考《Focal Loss for Dense Object Detection(RetinaNet) 学习笔记》

重新审视 Anchor:它真的必要吗?
#

走到这一步,回头看 anchor 机制的全貌:

为了覆盖不同的 object,需要预先设计一整套 anchor template:

1scale: {32, 64, 128, 256, 512}
2aspect ratio: {1:1, 1:2, 2:1}
3FPN level: {P2, P3, P4, P5, P6}

训练的时候还需要一堆匹配规则:

1IoU threshold for positive
2IoU threshold for negative
3matching strategy (max IoU? top-k?)

这些全是 handcrafted prior。每换一个数据集、每换一种目标形态,可能都需要重新调。

自然就会想到一个问题:如果模型最终要预测的只是一个 bounding box 的位置,为什么一定要先人为放一个 box template,然后让网络去修正它?

能不能直接从 feature map 上的某个点出发,预测 object 的边界?

这就是 anchor-free 最自然的动机。它不是凭空出现的新技术,而是在逐步删除经典 detector 中的 handcrafted intermediate representation。

FCOS:从"修正一个 box"到"从一个 point 找四条边"
#

FCOS(Fully Convolutional One-Stage Object Detection)是 anchor-free 这条路线上最有代表性的工作之一。

Anchor-based detection 里,网络在 anchor 的基础上预测 offset:

Anchor-Based

FCOS 换了一种 representation:对于 feature map 上的每个 point,直接预测从该 point 到 object bounding box 四条边的距离。

Anchor-Free

也就是从"这个预定义好的 box 应该怎么变"变成了"从这个空间位置出发,object 的四条边界分别在哪里"。

这是一次很干净的 representation simplification

但 anchor-free 并不意味着没有新的问题需要解决。

如果一个 point 只要位于 GT box 内就被视为 positive,那么靠近 box 边缘的 point 往往产生质量很差的预测——它离 object 中心太远,预测的 l、t、r、b 中总有一些值非常大,regression 精度下降。

因此 FCOS 引入了 centerness 分支:一个衡量该 location 距离 object 中心程度的值。推理时用 centerness 乘以 classification score,自动降低边缘位置低质量 box 的最终得分。

这里有一个很容易被过度简化的认知需要纠正:anchor-free 不等于 no prior。

FCOS 仍然有:

  • Feature pyramid level 分配策略(不同大小的 object 分配到不同 FPN level)
  • Stride(决定了 feature map 上相邻 point 在原图上的距离)
  • Center sampling(后续改进中只把 GT box 中心区域的 point 视为 positive)
  • Size range(每个 FPN level 负责的 object 大小范围)

它删除的是 explicit box template 和对应的 IoU matching 规则,但 detection 中的 spatial prior 并没有完全消失。

NMS:为什么经典 detector 最后还需要去重
#

不管是 anchor-based 还是 anchor-free,dense detector 都有一个共同的输出特征:对于同一个 object,周围多个 anchor 或 point 都可能产生高置信度的预测。

1同一只猫:
2
3box A  score 0.99
4box B  score 0.97
5box C  score 0.94
6box D  score 0.90

这些 box 彼此高度重叠,但 detector 并不会自动知道它们指的是同一个 object。于是需要 NMS(Non-Maximum Suppression)做一轮后处理:

1选最高分的 box
2  → 删除与它 IoU 超过阈值的其他 box
3  → 重复直到没有剩余

NMS 本质上是 dense prediction 输出表示方式的自然后果——既然允许多个位置同时为同一个 object 产生预测,就需要在最后合并。

NMS 的具体算法和实现以前写过,参考《非极大值抑制(Non-Maximum Suppression,NMS)》

回头看:五条演化线索
#

重新过完这条从 Sliding Window 到 FCOS 的路线,我觉得经典目标检测的演化可以用五条线索来概括。

架构演化总览

Search space 的变化:到底在哪里做 prediction?

1Sliding Window → 暴力枚举所有位置
2Faster R-CNN   → learned proposal generation (dense→sparse)
3SSD / FCOS     → 直接在 feature map 上 everywhere prediction

Bounding box representation 的变化:如何表示一个候选框?

1Anchor-based → 预定义 reference box + offset regression
2Anchor-free  → spatial point + (l, t, r, b) geometry

Multi-scale representation 的变化:如何处理不同尺度的 object?

1SSD → 不同层各自检测
2FPN → top-down 把 strong semantics 带回 high-res feature

Optimization 的变化:如何让 dense prediction 有效训练?

1Dense detection → 极端 fg/bg 不平衡
2Focal Loss     → 让 easy samples 不再消耗训练预算

Post-processing 的变化:如何从大量 candidate 得到最终 object?

1Dense prediction → 同一 object 产生一簇重叠 box → NMS 去重

这五条线索并不独立。比如,Faster R-CNN 的 proposal stage 本身就是一种 hard example mining,所以它不需要 Focal Loss;SSD 删除 proposal 后暴露了 class imbalance,才催生了 RetinaNet;anchor-free 删除了 box template 但仍然是 dense prediction,所以仍然需要 NMS。

经典目标检测的一条大趋势其实是:不断减少 handcrafted intermediate representation。 从暴力枚举到 learned proposal,从固定 anchor template 到直接从 point 预测几何——每一步都在删除一层人为设计的中间表示。

这还不是目标检测架构演化的终点。后来这个领域又开始重新思考 prediction set、后处理以及 detection 的整体 formulation,不过那已经是另一条线索了,等把相关工作重新学完之后再单独写。

参考资料
#

相关文章

SSD: Single Shot MultiBox Detector 学习笔记

·3374 字·7 分钟
概述 # SSD是一种单阶段目标检测算法.所谓单阶段,是指只使用了一个deep neural network,而不是像faster-rcnn这种两阶段网络. 为什么有了faster-rcnn还要使用SSD? 最主要是慢… 两阶段网络虽然准确率高,但是在嵌入式等算力不足的设备上做inference速度非常感人,很难达到real time的要求. (实际业务上也是这样,公有云上的检测模型几乎都是faster-rcnn,而到了一些盒子之类的硬件设备,检测模型就全是SSD等single stage 模型了)