> For the complete documentation index, see [llms.txt](https://openmmlabbook.gitbook.io/openmmlab-book/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://openmmlabbook.gitbook.io/openmmlab-book/object-detection/new-trend/fcos.md).

# FCOS

FCOS 是目前最经典优雅的一阶段 anchor-free 目标检测算法，其模型结构主流、设计思路清晰、超参极少和不错的性能使其成为后续各个改进算法的 baseline，和 retinanet 一样影响深远，故非常有必要对其进行深入分析。

一个大家都知道的问题，anchor-base 的缺点是：**超参太多，特别是 anchor 的设置对结果影响很大,不同项目这些超参都需要根据经验来确定，难度较大。** 而 anchor-free 做法虽然还是有超参，但是至少去掉了 anchor 设置这个最大难题。fcos 算法可以认为是 point-base 类算法也就是特征图上面每一个点都进行分类和回归预测，简单来说就是 anchor 个数为 1 的且为正方形 anchor-base 类算法。

在目前看来，任何一个目标检测算法的核心组件都包括 backbone+neck+多尺度 head+正负样本定义+正负样本平衡采样+loss 设计，除了正负样本平衡采样不一定有外，其他每个环节都是目前研究重点，到处存在不平衡问题，而本文重点是在正负样本定义上面做文章。

## 1 fcos 和 retinanet 算法对比分析

FCOS 结构和 retinanet 几乎相同，但是有细微差别，下面会细说。

![](http://static.zybuluo.com/huanghaian/smpspiu0hhaj97fc98brvtwu/image.png)

不清楚 retinanet 结构的请看前续解读。retinanet 的结构大概可以总结为：

* resnet 输出是 4 个特征图，按照特征图从大到小排列，分别是 c2 c3 c4 c5，stride=4,8,16,32。Retinanet 考虑计算量仅仅用了 c3 c4 c5。
* 先对这三层进行 1x1 改变通道，全部输出 256 个通道；然后经过从高层到底层的最近邻上采样 add 操作进行特征融合，最后对每个层进行 3x3 的卷积，得到 p3,p4,p5 特征图。
* 还需要构建两个额外的输出层 stride=64,128,首先对 c5 进行 3x3 卷积且 stride=2 进行下采样得到 P6，然后对 P6 进行同样的 3x3 卷积且 stride=2，得到 P7

下面介绍 fcos 和 retinanet 算法的区别。

### 1.1 resnet 的 style 模式区别

![](http://static.zybuluo.com/huanghaian/8yv6h6vhm565xsp1i0ei3t5e/image.png)

左边是 fcos 配置，右边是 retinanet 配置。

**(1) resnet 骨架区别**

在 resnet 骨架中，style='caffe'参数和 style='pytorch'的差别就在 Bottleneck 模块，该模块的结构如下：

![](http://static.zybuluo.com/huanghaian/58x96v297h8roswueehq32bl/image.png)

主干网络是标准的 1x1-3x3-1x1 结构，考虑 stride=2 进行下采样的场景，对于 caffe 模式来说，stride 参数放置在第一个 1x1 卷积上，对于 pytorch 模式来说，stride 放在第二个 3x3 卷积上：

```python
if self.style == 'pytorch':
    self.conv1_stride = 1
    self.conv2_stride = stride
else:
    self.conv1_stride = stride
    self.conv2_stride = 1
```

唯一区别就是这个。至于为啥存在 caffe 模式，是因为早期 mmdetection 采用了 detectron 权重(不想重新训练 imagenet)，其早期采用了 caffe2 来构建模型，后续 detectron2 才切换到 pytorch 中，属于历史遗留问题。

**(2) 骨架训练策略区别**

注意看上面的对比配置，可以发现除了上面说的不同外，**在 caffe 模式下，requires\_grad=False**，也就是说 resnet 的所有 BN 层参数都不更新并且全局均值和方差也不再改变，而**在 pytorch 模式下，除了 frozen\_stages 的 BN 参数不更新外，其余层 BN 参数还是会更新的**。我不清楚为啥要特意区别。

总的来说，在 caffe 模式下训练的内存肯定会少一些，但是效果究竟谁的更好，看了下对比结果发现差不多。

### 1.2 FPN 结构区别

![](http://static.zybuluo.com/huanghaian/24piejoyfnqfjdsvdm1tfg4o/image.png)

和 retinanet 相比，fcos 的 FPN 层抽取层也是不一样的，需要注意。

retinanet 在得到 p6,p7 的时候是采用 c5 层特征进行 maxpool 得到的(对应参数是 add\_extra\_convs='on\_input',)，而 fcos 是从 p5 层抽取得到的(对应参数是 extra\_convs\_on\_inputs=False)，而且其还有 relu\_before\_extra\_convs=True 参数，也就是 p6 和 p7 进行卷积前，还会经过 relu 操作，retinanet 的 FPN 没有这个算子(**C5 不需要是因为 resnet 输出最后就是 relu**)。从实验结果来看，从 p5 抽取的效果是好于 c5 的，baseline 的 mAP 从 38.5 提高到 38.9。

### 1.3 数据归一化区别

当切换 style='caffe'时候，需要注意图片均值和方差也改变了：

```python
# pytorch
mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True
# caffe
mean=[103.530, 116.280, 123.675], std=[1.0, 1.0, 1.0], to_rgb=False
```

### 1.4 head 模块区别

和 retinanet 相比，fcos 的 head 结构多了一个 centerness 分支，其余也是比较重量级的两条不共享参数的 4 层卷积操作，然后得到分类和回归两条分支。

## 2 fcos 算法深入分析

### 2.1 fcos 输出格式

![](http://static.zybuluo.com/huanghaian/uxaw6bdb35vl3jv76deszncz/image.png)

fcos 是 point-base 类算法，对于特征图上面任何一点都回归其距离 bbox 的 4 条边距离，

![](http://static.zybuluo.com/huanghaian/u4szcfpi3ck4fa3ebxvoj8sq/image.png)

假设 x 是特征图上任意点坐标，x0y0x1y1 是某个 gt bbox 在原图上面的坐标，那么 4 条边的边界(都是正数)其实很好算，公式如上所示。需要注意的是由于大小 bbox 且数值问题，一般都会对值进行变换，也就是除以 s，主要目的是**压缩预测范围，容易平衡分类和回归 Loss 权重**。

如果某个特征图上面点处于多个 bbox 重叠位置，则该 point 负责小 bbox 的预测。

### 2.2 正负样本定义

一个目标检测算法性能的优异性，最大影响因素就是如何定义正负样本。而 fcos 的定义方式非常 mask sense，通俗易懂。主要分为两步：

1. 设置 regress\_ranges=((-1, 64), (64, 128), (128, 256), (256, 512),(512, INF)，用于将不同大小的 bbox 分配到不同的 FPN 层进行预测即距离 4 条边的最大值在给定范围内
2. 设置 center\_sampling\_ratio=1.5,用于确定对于任意一个输出层距离 bbox 中心多远的区域属于正样本，该值越大，扩张比例越大，正样本区域越大

为了说明其重要性，作者还做了很多分析实验。

**(1) 为啥需要第一步？**

regress\_ranges 的意思是对于不同层仅仅负责指定范围大小的 gt bbox，例如最浅层输出，其范围是 0-64，表示对于该特征图上面任何一点，假设其负责的 gt bbox 的 label 值是 left、top、right 和 bottom，那么这 4 个值里面取 max 必须在 0-64 范围内，否则就算背景样本。

为啥需要限制层回归范围？目的应该有以下几点：

1. 在 SNIP 论文中说到，CNN 对尺度是非常敏感的，一个层负责处理各种尺度，难度比较大，采用 FPN 来限制回归范围可以减少训练难度
2. 提高 best possible recall

通常我们知道 anchor 设置的越密集，理论上召回率越高，现在 fcos 是 anchor-free，那么需要考虑理论上的最好召回率是多少，如果理论值都比较低，那肯定性能不行。best possible recall 定义为训练过程中所有 GT 的最大召回率，如果训练过程中一个 GT 被某个 anchor 或者 location 匹配上那么就认为被召回了。

![](http://static.zybuluo.com/huanghaian/eqctrr9zx1mwckzibn8fdbsl/image.png)

low-quality matches 意思是 retinanet 配置中的 min\_pos\_iou 参数，默认是 0。在不使用 low-quality matches 时候，对于某些 gt，如果其和 anchor 的最大 iou 没有超过正样本阈值，那么就是背景样本，一开启 low-quality matches 则可能能够捞回来，开启这个操作可以显著提高 BPR，从表中也可以看出，从 88.16 变成 99.32。至于在 ALL 模式下为啥不是 1，作者分析原因是有些 bbox 特别小并且挨着，经过下采样后在同一个位置，导致出现相互覆盖。

对于 FCOS，即使在不开启 FPN 时候，其 BPR 也很高，因为其 bbox 中心范围内都算正样本，BPR 肯定比较高，在开启 FPN 后和 retinanet 相比差别不大，对最终性能没有影响。

为了强调 FPN 层的作用性，作者还对模糊样本数进行分析：

![](http://static.zybuluo.com/huanghaian/7r2pgrh2tgrw8i6uxqt14lz4/image.png)

模糊样本就是某个特征图位置处于多个 gt bbox 重叠位置，也就是图表中的大于 1 个数。可以发现在开启 FPN 后，模糊样本明显减少，因为不同大小的 gt bbox 被强制分配到不同层预测了，当结合中心采样策略后可以进一步提高。

**(2) 为啥需要第二步？**

中心采样的作用有两个

1. 减少模糊样本数目
2. 减少标注噪声干扰

![](http://static.zybuluo.com/huanghaian/k8xfgwx3lk6voemuu1s3sd18/image.png)

bbox 标注通常都有噪声或者会框住很多无关区域，如果这些无关区域也负责回归则比较奇怪，这其实是目前非常常用的处理策略，在文本检测领域广泛应用。

![](http://static.zybuluo.com/huanghaian/99ekcr43nfazqw3djstua9jl/image.png)

(a)是早期做法也就是对于本文不采用中心采样策略的做法；(b)是本文做法；(c)是 centernet 做法；(d)是 centernet 改进论文做法。从这里可以看出，(d)的做法应该比 fcos 更加 mask sense，可解释性更强。

总的来说，作者认为**FCOS 的功臣可以归功于 FPN 层的多尺度预测和中心采样策略**。

**(3) 核心代码分析**

为了方便理解中心采样策略代码，我对代码进行了分解，具体在 tools/fcos\_analyze/center\_samper\_demo.py。其核心代码逻辑是：

1. 利用 meshgrid 函数获取特征图上面每个点的 2d 坐标
2. 扩展维度，使得所有参数计算的 tensor 维度相同，方便后续计算
3. 如果不采用中心采样策略，则直接对特征点上面每个点计算和所有 gt bbox 的 left/top/right/bottom 值。然后取 4 个值中的最小值，如果小于 0，则该 point 至少有一条边不再 bbox 内部，属于背景 point 样本，否则就是正样本
4. 如果采用中心采用策略，则基于 gt bbox 中心点进行扩展出正方形，扩展范围是 center\_sample\_radius×stride，正方形区域就当做新的 gt bbox，然后和(3)步骤一样计算正样本即可。还需要注意一个细节：如果扩展比例过大，导致中心采样区域超过了 gt bbox 本身范围了，此时需要截断操作

以上操作就可以确定哪些区域是正样本了，仿真的结果如下所示：

![](http://static.zybuluo.com/huanghaian/t3pjhwp5vvkee3wx8zf46o4h/image.png)

蓝色是 gt bbox，白色区域是正样本区域。从这里也可以看出，其实 FCOS 中心采样策略还有改进空间，因为他的正样本区域没有考虑 gt bbo 宽高信息，对于图片中包含人这种长宽比比较大的场景，这种做法其实不好。需要注意的是为了可视化代码简单，我是直接对特征图 mask 进行上采样得到的，实际上在原图上对应的白色区域是一个点，而不是一个白块。

**(4) 理解纠正**

我们再来看下上面的一句话：

1. 设置 regress\_ranges=((-1, 64), (64, 128), (128, 256), (256, 512),(512, INF)，用于将不同大小的 bbox 分配到不同的 FPN 层进行预测
2. 设置 center\_sampling\_ratio=1.5,用于确定对于任意一个输出层距离 bbox 中心多远的区域属于正样本，该值越大，扩张比例越大，正样本区域越大

看起来好像没问题，后来我**通过正样本可视化分析发现我上面说法是错误的**。正确的应该是：

1. 对于任何一个 gt bbox，首先映射到每一个输出层，利用 center\_sampling\_ratio 值计算出该 gt bbox 在每一层的正样本区域以及对应的 left/top/right/bottom 的 target
2. 对于每个输出层的正样本区域，遍历每个 point 位置，计算其 max(left/top/right/bottom 的 target)值是否在指定范围内，不再范围内的认为是背景

上面两个写法的顺序变了，结果也差别很大，最大区别是第二种写法可以使得某一个 gt bbox 在多个预测层都是正样本，而第一种做法先把 gt bbox 映射到预测层后面才进行中心采样，第二种做法相比第一种做法可以增加很多正样本区域。很明显，第二种才是正确的。

通过正样本可视化分析可以发现：

![](http://static.zybuluo.com/huanghaian/0b7ew287xq2dh588umvlpx7b/image.png)

红色点表示正样本 point ，其中 0-4 表示 `stride=[8,16,32,64,128]` 的输出特征图，代表从小到大物体的预测。

![](http://static.zybuluo.com/huanghaian/nu8v8ihqrqs9g9c9g36d88h7/image.png)

可以看出，其是完全基于回归距离来定义正负样本位置的，但是从语义角度来看，这种设计不够 make sense，因为红色点不是对称分布，特别是上标为 2 的小象内部不算正样本，比较奇怪。从语义角度理解不合理，但是从回归数值范围理解是很合理的。

### 2.3 loss 计算

在确定了每一层预测层的正负样本后就可以计算 loss 了，对于分类分支(class\_num,h',w')，其采用的是 FocalLoss，参数和 retinanet 一样。对于回归分支(4,h',w')，其采用的是 GIou loss，也是常规操作，回归分支仅仅对正样本进行监督。

在这种设置情况下，作者发现训推理时候会出现一些奇怪的 bbox，原因是对于回归分支，正样本区域的权重是一样的，同等对待，导致那些虽然是正样本但是离 gt bbox 中心比较远的点对最终 loss 产生了比较大的影响，其实这个现象很容易想到，但是解决办法有多种。

![](http://static.zybuluo.com/huanghaian/4wgxu1noeot2oik2k8dzihfs/image.png)

现象应该和(b)图一致，在靠近物体中心的四周会依然会产生大量高分值的输出。

作者解决办法是引入额外的 centerness 分类分支(1,h',w')来抑制，该分支和 bbox 回归分支共享权重，仅仅在 bbox head 最后并行一个 centerness 分支，其 target 的设置是离 gt bbox 中心点越近，该值越大，范围是 0-1。虽然这是一个回归问题，但是作者采用的依然是 ce loss。很多人反映该分支训练时候 loss 下降的特别慢，其实这是非常正常的，这其实是整图回归问题，在每个点处都要回归出对应值其实是一个密集预测问题，难度是很大的，loss 下降慢非常正常。

需要特别注意的是**centerness 分支也是仅仅对正样本 point 进行回归**。

![](http://static.zybuluo.com/huanghaian/l30jpl59mtlvc3kcg74mlwet/image.png)

通上图可以看出，在引入 centerness 分支后，将该预测值和分类分支结果相乘得到横坐标分支图，纵坐标是 bbox 和 gt bbox 的 iou，可以明显发现两者的一致性得到了提高，对最终性能有很大影响。至于这个图自己如何绘制，有个比较简单的办法：

1. 利用框架进行测试，保存 coco 验证集的预测 json 格式
2. 在写一份离线代码读取该预测 json 和标注的 json，遍历每张图片的预测 bbox 和 gt bbox
3. 对于每张图片的结果，遍历预测 bbox，然后和所有 gt bbox 计算最大 iou，该值就是纵坐标，横坐标就是预测分值(cls\*centerness)
4. 所有数据都处理完成就可以上述图表

由于代码比较简单，我这里就不写了。

对于任何一个点，其 centerness 的 target 值通过如下公式计算：

![](http://static.zybuluo.com/huanghaian/vjfk41e4z9qfk1d974f6jjg5/image.png)

tools/fcos\_analyze/center\_samper\_demo.py 仿真效果如下所示：

![](http://static.zybuluo.com/huanghaian/qti7do814895p9i664g5qlmi/image.png)

越靠近中心,min(l,r)和 max(l,r)越接近 1，也就是越大。

### 2.4 附加内容

fcos 代码在第一版和最终版上面修改了很多训练技巧，对最终 mAP 有比较大的影响，主要是：

**(1) centerness 分支的位置** 早先是和分类分支放一起，后来和回归分支放一起

**(2) 中心采样策略** 早先版本是没有中心采样策略的

**(3) bbox 预测范围** 早先是采用 exp 进行映射，后来改成了对预测值进行 relu，然后利用 scale 因子缩放

**(4) bbox loss 权重** 早先是所有正样本都是同样权重，后来将样本点对应的 centerness target 作为权重，离 GT 中心越近，权重越大

**(5) bbox Loss 选择** 早先采用的是 iou，后面有更优秀的 giou，或许还可以尝试 ciou

**(6) nms 阈值选取** 早先版本采用的是 0.5，后面改为 0.6

这些 trick，将整个 coco 数据集 mAP 从 38.6 提高到 42.5，提升特别大，新版本的论文中有非常详细的对比实验报告，有兴趣的可以自行阅读。

## 参考资料

[FCOS: A simple and strong anchor-free object detector](https://arxiv.org/pdf/2006.09214v3.pdf) [FCOS: Fully Convolutional One-Stage Object Detection](https://arxiv.org/pdf/1904.01355v1.pdf) [代码地址](https://github.com/aim-uofa/AdelaiDet/)
