聊目标检测,R-FCN这个缩写你迟早会遇到。Region-based Fully Convolutional Networks,基于区域的全卷积网络,名字听起来比 Faster R-CNN 还绕,但它想解决的事情其实特别直白:Faster R-CNN 已经让检测框和分类共享了骨干网络的特征计算,可每个候选框在 RoI 之后还要单独过一遍自己的子网络,这一块成了当时检测器速度的最大瓶颈。R-FCN 的思路是连这块也干掉,做法叫“位置敏感得分图”,不熟悉的人第一次看会懵,但底层逻辑并不复杂:目标是有空间结构的,你在判断一个区域是不是目标时,应该同时知道正在看目标的哪个部位。这篇论文的中文解读,适合三类人看:准备复现经典模型的工程师、刚读完 Faster R-CNN 想顺着检测演进路线往下走的新手、还有需要拿 R-FCN 做对比基线做实验的同学。读完你会明白,为什么一篇没有花哨结构变化的论文,能把速度提上去、精度还不掉,甚至让后面很多检测器都延续了它的思想。
1. 为什么会有 R-FCN:检测器在快与准之间的拉扯
1.1 两阶段检测的黄金范式,卡点在哪
先回顾一下两阶段检测的基本范式。第一阶段是区域提议网络(RPN),在共享卷积特征上快速产生一堆候选框;第二阶段是分类和回归,对这些候选框做精判。Faster R-CNN 最大的贡献是让 RPN 和检测头共享了骨干卷积,所以“提特征”这件事只做一次。但注意,共享的只是前面的卷积层。到了 RoI Pooling 之后,每个候选区域仍然要经过自己的全连接层和分类器,这部分叫 RoI-wise 子网络。
问题就在这里。一张图在测试时通常会产生 300 个甚至上千个候选框,假设做一次 RoI 子网络前向要花一笔时间,那这笔时间要乘上候选框数量。候选框一多,检测器就算骨干再快,也被这一块拖住了。论文里当时给了一个很直观的对比:骨干网络用 ResNet-101 时,RoI-wise 部分的计算占了大头。这也是为什么很多人觉得 Faster R-CNN 精度好但跑不快,一到嵌入式设备或者视频流场景就露馅。
我习惯用一个食堂的类比来解释这件事。RPN 和骨干共享卷积,相当于菜已经在中央厨房洗好切好了;但每个 RoI 还要单独开火炒菜,因为分类和回归需要围绕这个区域单独做一次推理。如果来吃饭的桌子从 300 桌涨到 2000 桌,厨房就得炒 2000 次,那前面中央厨房的效率再高也没用。
1.2 把“RoI 后的私活”也变成全卷积
R-FCN 的出发点很干脆:整张图只前向一次卷积网络,候选框区域不再有独立的全连接子网。标题里的 Fully Convolutional 就是这个意思。它想把“检测”这件事也做得像语义分割里的全卷积网络一样,输入图像、输出直接就是某种打分图,而不是先取区域再过网络。
难点也在这里。卷积天然具有平移不变性,意思是同一个目标出现在图像左上角还是右下角,卷积特征理论上是一样的。但检测任务需要知道目标到底在哪个位置,如果直接把整张图做全局分类,就和“图像分类”没区别了,不可能在同一个人旁边再区分出他的自行车。R-FCN 的处理方式是:保留卷积的平移不变性来分类,但把“位置信息”编码到通道里,用通道来记录目标的部分空间结构。这就是位置敏感得分图要做的事。
一句话总结它的位置:R-FCN 是介于“慢而准的两阶段检测器”和“快但精度偏弱的单阶段检测器”之间的一架桥。它没有跳出“区域提议 + 区域分类”的两阶段框架,但通过全卷积设计,把第二阶段的计算成本压到了几乎为零,同时不动精度的根基。
1.3 理解 R-FCN,先理解 FCN
这里的 FCN,指的不只是论文标题里的术语,它本身是从图像分割领域迁移过来的方法。在分割任务里,FCN 把最后一个全连接层换成 1×1 卷积,让网络可以接受任意尺寸输入,并输出与输入分辨率相关的密集预测。R-FCN 借鉴了这个理念,但检测输出不能像分割那样对每个像素做分类,检测关心的是边界框。
于是作者做了一个很聪明的转换:不直接输出边界框,而是输出一组“区域打分图”。每个打分图对应目标的一种空间位置,比如“目标的左上角”“目标的正中间”“目标的右下角”。一个目标只要让它的几个部位在对应的打分图上都有高分响应,就可以认定这个区域是目标。这样,整条网络路径都是卷积,没有任何一个环节需要像以前那样把 RoI 取出来再单独过 FC 层。
2. 位置敏感得分图:把目标切块来投票
2.1 核心直觉:九宫格打分
先讲直觉,再讲公式。
假设你现在要在画面里找汽车。你不会只凭“这个东西像一辆车”就下结论,你还会看它有没有轮子、车窗、车灯,并且会注意这些部件出现的相对位置:轮子在下面,车窗在上面,车灯在两侧。如果某个候选区域里,每个部位都出现在该出现的位置,那它是车的概率就很大。R-FCN 就是照着这个直觉设计的。
论文里用一个超参数 K 控制位置分的粒度,默认取 K=3,也就是把目标划分成 3×3 的九个格子。严格说不是把像素切开放进模型,而是让网络为每个类别分别生成 9 张图,第 i 张图负责回答同一个问题:“这个像素点,是否属于某一个目标的第 i 个位置区块”。
对这 9 个位置分别打分,最后把 9 个分数平均起来,得到一个候选区域属于该类的整体得分。用生活化的话说,就是九宫格投票:九个格子都说像,那大概率是像;只有一个格子说像,其他格子没反应,那就存疑。正因为每个格子都被明确地“指定角色”,网络才能既保持纯卷积结构,又不丢失目标的空间位置信息。
2.2 具体通道数:K²(C+1) 是怎么算出来的
论文里的关键数字是:位置敏感得分图一共有 K²(C+1) 个通道。其中 C 是检测类别数,1 是背景类,K 取 3,所以 K²=9。也就是说,如果你的检测目标有 20 类,那分类分支的得分图就是 9 乘以 21,等于 189 个通道。这 189 个通道里,每个通道都对应“某类别的某个位置块”。
很多第一次看论文的人会在这里卡壳,觉得 189 个通道很抽象。其实可以理解成,网络不再是输出一个“这个像素属于哪一类”的图,而是同时输出 189 个“这个像素属于哪一类的哪一块”的图。每个通道只管一个非常窄的问题,问题变简单了,每个通道的语义也就更清晰、更容易被卷积学到。
回归分支的设计稍有不同,论文里用的是 4K² 个通道,对应边界框的四个参数(中心偏移、宽高缩放)。这里注意,它不是 4K²(C+1),而是类别共享的,这也是原始 R-FCN 的一个设计选择。后续有人改成了类别独立的回归,但那是后话。
2.3 PS RoI Pooling:取值、平均、投票
光有得分图还不够,因为你最终还是要对“每一个候选框”给出分类结果和回归结果。R-FCN 用了一个叫 Position-Sensitive RoI Pooling 的操作,把候选框和得分图对应起来。
过程是这样的:给定一个 RoI,先把它的区域均匀划分成 K×K 个 bin,对应之前九宫格的位置。然后对每个 bin,在得分图上找到对应通道的同一位置,做平均池化。比如第 (1,1) 个 bin,就去取“该类别第 1 个位置块”的那张得分图,在这个 bin 区域内取平均。遍历 K² 个 bin 之后,你会得到 K² 个响应值,把它们平均起来,得到该 RoI 对某个类别的最终分数,最后送进 softmax 进行分类。
这个过程里没有额外的可学习参数,纯粹是索引和平均操作。听起来简单,但它的精妙之处在于:每一个 bin 只去自己的那张得分图上取响应,通道和位置严格对应,谁也不会拿错数据。这就像一个投票大会,九个位置各自表态,最终结果就是九张票的均值。
回归分支也是同样的池化过程,只不过通道数是 4K²,池化后得到 K² 组、每组 4 个值,再取平均得到最终的 4 个边界框回归量。
2.4 位置敏感图为什么能解决平移矛盾
这里值得多说一句。图像分类希望网络具有平移不变性:一只猫不管在图的左上角还是右下角,都应该被识别成猫。检测则恰恰相反,它需要知道猫在图中的精确位置。全卷积结构天然具有平移不变性,直接拿来做检测会丢失位置信息;但如果像分割那样高频输出位置信息,又容易让分类结果变得不稳定。
R-FCN 的做法是把这两个需求解耦:基础卷积负责提取语义特征,保持平移不变性;位置敏感通道负责记录空间相对位置,保持平移可变性。当网络看到“猫的头”这个通道时,它要表达的是“猫头相对整只猫的空间位置”,而不是“猫头在整张图上的绝对坐标”。这种相对位置编码,让检测器既能识别目标类别,又能确定目标位置,两者不打架。
3. 论文里的关键实验与结论
3.1 骨干网络怎么改:去掉全局池化
论文的实验基本围绕 ResNet-101 展开,但也给了 ResNet-50 或更浅骨干的结果。核心改动有三处:第一,把 ResNet 最后的全局平均池化和全连接层去掉,保留前面的卷积部分作为特征提取器;第二,在卷积输出后接一个 1×1 卷积,把通道数降到 1024,目的是控制计算量和参数规模;第三,再接一个 1×1 卷积,直接生成位置敏感得分图,分类分支 K²(C+1) 通道,回归分支 4K² 通道。
训练时输入图像的短边统一缩放到 600 像素,这个尺度和当时的 Faster R-CNN 基本保持一致,方便直接对比。论文里还提到,为了提高感受野、覆盖更大范围的上下文,作者在部分设置中用了空洞卷积来扩大最后几层的感受野,这对检测大目标有帮助。整体训练采用 RPN 和检测网络联合优化的方式,并使用了当时比较成熟的迭代训练策略。
3.2 和 Faster R-CNN 对比:速度与精度
论文报告了一组很有代表性的结果:在相同骨干网络下,R-FCN 在 PASCAL VOC 2007 测试集上把 mAP 做到了大约 83.6%,同时单张图像的推理时间大约 170ms,比对应的 Faster R-CNN 方案快了约 2.5 倍。这个结果在当时相当能打,因为以前提升速度往往要牺牲精度,R-FCN 则是“速度明显提升、精度基本不掉甚至略涨”的状态。
速度提升的来源,前面已经分析过:RoI 之后不再有可学习的子网络,所有 RoI 共享一整份得分图计算结果,每多一个候选框只需要多做一次“取平均+投票”,计算量几乎可以忽略。所以它不怕 proposal 数量多,proposal 从 100 涨到 1000,前向推理的时间也只是投票阶段轻微增加,这给了检测器更大的候选框容错空间。
当然,也要客观看待这个对比。论文的对比对象主要是当时基于 ResNet-101 的 Faster R-CNN 标准实现。后来 Faster R-CNN 系列引入了 FPN、更优的 RoI 处理方式、更强的训练策略,精度又上了一截,但这不能否定 R-FCN 在当时提出的重要价值。它证明了检测器中大部分“区域私活”可以被共享,为后续思路开了路。
3.3 一个反直觉的发现:OHEM 竞然掉点
论文实验里有一个很容易被忽略但很有价值的细节:作者尝试在 R-FCN 上使用在线困难样本挖掘(OHEM),结果精度反而下降了。为什么?因为 OHEM 的工作机制是根据候选区域的 loss 值选出困难样本,再重新前向计算梯度。这个机制在 Faster R-CNN 里很有效,因为每个 RoI 的 loss 是独立计算的,选出来就能直接回传。
但 R-FCN 的 loss 在位置敏感池化之后才产生,而且一个 RoI 的得分来自 K² 个 bin 的平均投票,单独改变某个 bin 的通道值,会影响到所有候选框的投票结果。如果按照 RoI 的 loss 去挑困难样本,网络很难知道该重点优化哪个空间位置,困难样本的梯度反而不稳定,最终让训练过程更抖动。这个案例很值得复现者注意:不是所有在 Faster R-CNN 上好用的技巧都能直接搬到 R-FCN 上。
3.4 R-FCN 的局限,以及它给后人留下的东西
R-FCN 也有明显的短板。它对小目标的检测效果一般,因为位置敏感得分图把目标划分成 K×K 个格子后,每个格子在小目标上对应的像素区域非常小,平均池化的时候信息损失大。另外,设置 K=3 这种固定分块方式,对不规则目标也不够灵活,目标如果严重倾斜或者长宽比异常,九宫格的位置语义会变弱。
但它留下了两个很有价值的思想。第一,把“目标部位的位置关系”编码进通道,而不是依赖全连接层去隐式学习,这个思路在后来的很多检测器里都看得到影子,哪怕不是直接叫位置敏感得分图。第二,区域级操作不一定非要放在 ROI 之后单独计算,可以把部分区域逻辑前移到卷积阶段完成。理解这两点,再看后来的很多论文,包括特征金字塔、可变形卷积、甚至一些无锚框检测器,会有一种串起来的感觉。
4. 复现 R-FCN 时最容易踩的坑
4.1 维度写错、通道顺序搞反
我自己踩过的第一个坑,是位置敏感得分图的通道顺序。它到底应该是 KKC、KCK,还是 CKK,在不同实现里真的会不一样。如果通道顺序跟池化索引对不上,训练时 loss 能在前几步降到很低,但后面整个模型会表现出一种诡异的“能收敛但完全不会检测”的状态。
建议在动手之前,先把输出通道的排列方式在纸上明确写出来。比如如果选择通道顺序是class * K * K,那索引 i 对应第 i 类、第 (r, c) 个位置块的通道,就应该是i * K * K + r * K + c。然后写一个最小测试脚本,用随机生成的 RoI 去跑一遍前向,打印池化后的维度。很多框架的自定义算子不会帮你校验语义,维度对了不代表位置对了。
还有一个小点:回归分支是 4K² 通道,不是 4K²(C+1),这个最容易在改代码的时候顺手写错,导致回归分支参数暴涨、训练变慢。改了类别数之后,尤其要单独检查回归分支的通道数有没有被错误地跟着类别数一起乘。
4.2 proposal 数量与正负样本比例
R-FCN 虽然不怕 RoI 数量多,但训练时的正负样本比例依然敏感。如果 RPN 生成的 proposal 大多数是背景,分类分支很快会偏向预测“背景”,位置敏感图的正样本响应得不到充分训练。论文里的标准做法是在训练时对 RoI 采样,控制前景和背景的比例,我一般按 1:3 或者 1:2 来配,效果比较稳定。
另一个容易被忽略的是 RPN 和检测网络是否联合训练。如果只训练检测网络、冻结 RPN,而 RPN 的 proposal 质量不高,后面位置敏感池化拿到的输入就很差,整个检测器上限被锁死。建议先让 RPN 有基本的收敛状态,再开启联合训练;如果一开始就联合训练,梯度噪声会比较大,尤其在小数据集上很容易出现长时间不涨点的情况。
4.3 训练不收敛或者 mAP 迟迟不涨
遇到 R-FCN 训练不收敛,先别急着调学习率,按这个顺序排查:先查位置敏感池化的索引逻辑,确认通道与空间位置的对应关系;再查回归分支的 delta 归一化,边界框回归目标如果没做标准化,loss 很容易被大数值样本带偏;最后查背景类是否过采样。
学习率方面,用 ImageNet 预训练权重时,我习惯初始学习率设 0.001,batch size 大约 8,配合一个温和的学习率衰减策略。如果从头训练,学习率要更低,并且准备更多的迭代轮次,因为位置敏感得分图这种细粒度输出,收敛速度比普通分类头慢不少。对比实验时看到论文里 170ms 的成绩,先确认自己有没有开空洞卷积和推理时的输入尺度,这两项对最终速度和精度影响都很大。
4.4 从论文迁移到自己的数据集
在自己的数据集上复现,类别数一改,最后一层 1×1 卷积要重新初始化,前面骨干网络的权重可以加载预训练。这里有个建议:新初始化层的标准差稍微给大一点,避免刚开始训练时位置敏感得分图的所有通道输出都是零或者常数,导致 pool 之后梯度消失。
小目标占比较高的数据集,建议把输入短边从 600 提到 800 或者 1000,虽然速度会下降,但位置敏感池化的 bin 在像素空间里能覆盖更多有效区域。类别极度不平衡的数据集,除了调整采样比例,也可以在投票阶段做一个加权平均,不过这会偏离论文原始设定,最好作为后续优化项而不是复现目标,先把基线跑通了再做实验。
5. 常见问题与调试速查表
复现 R-FCN 的过程中,我整理了下面这张速查表,很多问题其实是同一个原因引起的。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 训练 loss 很快降到很低但 mAP 接近零 | 位置敏感池化索引通道顺序错误 | 写最小脚本验证通道与空间位置对应关系 |
| 训练不收敛,loss 震荡剧烈 | RPN 与检测头联合训练过早 | 先冻结 RPN 训练一段时间,再逐步放开 |
| mAP 报得特别低,远低于论文 | 输入尺度、是否使用空洞卷积、proposal 数量差异 | 确认推理尺度是否为 600,检查测试时 proposal 数量 |
| 回归分支 loss 占比异常大 | 边界框回归目标未做标准化 | 对 delta 做均值方差归一化 |
| 推理速度没有明显提升 | 测试时每张图的 RoI 数量仍太多且后处理低效 | 调低 top-K 候选数,优化 NMS 实现 |
| 背景类预测概率始终偏高 | 训练时负样本抽得太多 | 调整正负采样比例到 1:2 或 1:3 |
| 每次结果波动很大 | 训练 batch 太小、BN 层更新不稳定 | 增大 batch size,或冻结骨干 BN 参数 |
调试这类模型,我还有一个非常管用的习惯:把位置敏感得分图可视化。选出某几个通道,对着一张包含明确目标的测试图,把得分图的热力图直接画出来。正常情况下,第 (1,1) 位置对应的通道应该会在目标的左上区域产生高响应,而不是整个图到处都亮。如果热力图一片混乱,就不用再怀疑训练策略了,问题基本出在池化索引或者通道语义配错上。
另外,部署到推理引擎时,特别注意自定义算子兼容性。位置敏感池化在训练框架里实现方便,但很多部署框架不一定支持;我在实践中常常把它拆成“切片 + 平均池化 + 拼接”几个标准算子来实现,避免依赖自定义层。这样的实现方式更通用,也能利用部署框架的图优化,实测性能差距很小。
R-FCN 这篇文章,从原理解释到实验对比都体现了“把计算尽量前置到卷积阶段”的思路。我自己复现过一次之后,再回去看 Faster R-CNN 的那种逐 RoI 子网络设计,就会明显感觉到计算模式的差异。虽然在今天用 R-FCN 做新项目不是最优选,很多更强更快的检测器已经出来了,但如果你想真正理解检测器从“区域私有计算”到“全图共享计算”的演进逻辑,R-FCN 是绕不开的一课。