简介:R-FCN目标检测模型源代码包,面向需要在Caffe框架下配置和使用R-FCN的深度学习研究者与工程师。R-FCN以ResNet-50/101为骨干,将检测转化为像素级分类问题,借助位置敏感得分图同时完成分类与边界框回归,省去逐区域特征提取环节,适合从理论走向工程实现的读者。源码包共94个文件,以64个Matlab实验脚本和12个prototxt网络定义为主,辅以cpp/cu扩展、jpg可视化示例和mat数据文件,压缩包仅349KB;目录涵盖experiments训练脚本、rfcn_prototxts模型结构、imdb数据接口及utils工具箱,方便按需查阅。已有550人学习。通过VOC0712上的RPN/OHEM配置与Caffe接口,可复现完整训练与验证流程,理解预训练模型加载、网络构建、数据预处理、损失与优化器设置等关键环节,为在自动驾驶、视频监控等场景中定制和部署R-FCN提供直接参考。
1. 项目概述与背景
R-FCN,全称是Region-based Fully Convolutional Network,翻译过来就是基于区域的全卷积目标检测网络。先说结论,这玩意儿是Faster R-CNN之后、FPN和YOLO系列大规模流行之前,目标检测领域一个非常有代表性的过渡方案,它的核心价值在于“又快又准”,尤其是相比Faster R-CNN在速度上有明显提升。
我第一次接触R-FCN源码是在2017年左右,那时候刚啃完Faster R-CNN的源码,被一堆RPN、Anchor、RoIPooling来回折腾。后来看到R-FCN的论文,第一反应是“这名字挺绕”,但读完代码之后发现思路极其清晰。它的作者是戴景文(Jifeng Dai)等人,来自微软研究院,发表在NIPS 2016上。R-FCN最大的贡献是用“位置敏感得分图”(position-sensitive score maps)替代了传统检测网络中RoI之后的昂贵全连接层,从而把几乎整个检测网络做成了全卷积结构。
这篇文章不是论文精读,是站在“源码阅读者”的角度来拆解R-FCN。适合谁看?两类人:一类是刚开始接触目标检测、想在Faster R-CNN之外找一个结构清晰、代码量适中的项目来练手的学生或初学者;另一类是想深入理解“全卷积检测网络”设计思路的在职算法工程师。我尽量把源码里的关键逻辑、参数设置和工程坑都摊开来聊。
需要提前说明的一点是:R-FCN官方开源代码基于Caffe框架,不建议新手直接用Caffe跑,建议先读源码逻辑,再迁移到PyTorch或者直接用现成的复现版本。这一点后面单独聊。
2. 整体架构与核心设计思路
2.1 从Faster R-CNN到R-FCN,到底改了什么
想搞懂R-FCN源码,必须先理解它到底在解决什么问题。Faster R-CNN的结构是:共享卷积层提取特征 -> RPN生成候选框 -> RoIPooling把每个候选框对应的特征图抠出来 -> 送入两个全连接层做分类和bbox回归。
这里的痛点在于:RoI之后的“全连接层”很贵。每个候选框都要过一遍全连接,训练和推理时都拖速度。另外,Fast R-CNN和Faster R-CNN里RoIPooling之后的全连接层天然带有“位置敏感性”的损失——全连接层把整张特征图拉平,空间信息被压缩,对小目标和遮挡目标的判别能力受限。
R-FCN的解决方案非常直接:把“每个RoI内部做分类”这件事,从“RoI之后”提前到“共享卷积层最后”。具体做法是,在共享特征图之后接一个1x1卷积层,生成一组特殊的得分图,每个图对应“原图上某个特定相对位置属于某个类别”的响应。比如你想检测20个类,加上背景就是21类,再设定一个位置划分参数(论文和代码里默认是3x3,即9个位置),那最终生成的特征图通道数就是21 x 9 = 189。后面再借用一个叫PSROIPooling的操作,把这9个位置上的响应合并起来,得到每个RoI在21个类别上的得分。
这么一改,后面就没有全连接层了,整个网络从输入到输出都是卷积和池化操作,结构上清爽很多,速度也上去了。
2.2 位置敏感得分图的直观理解
“位置敏感”这个词有点晦涩,我当初啃代码的时候也是绕了挺久。后来自己画了个示意图才彻底想明白。
假设你要检测一只猫。传统Faster R-CNN的思路是:先锁定可能是猫的区域,然后让全连接网络看这个区域整体长什么样,判断是不是猫。R-FCN的思路换了一条路:它把猫这个类别又细分为“猫的左上角”、“猫的右上角”、“猫的正中间”、“猫的左下角”等等9个子区域,然后训练9个分类器,每个分类器只负责判断“候选框里对应位置是不是猫的一部分”。最后把这9个位置的判定结果投票汇总,得到最终得分。
这就像一群人围着一个物体,每个人只负责看自己正前方那一片区域,最后汇总每个人看到的局部信息来判断整体是什么。这9个分类器对应到源码里,就是189个通道的得分图。
PSROIPooling的操作也很朴实:对每个候选框,先划分成3x3的子区域,每个子区域只从对应的通道里取响应值做平均池化。最终每个RoI得到21个9维向量,加起来求平均就是每个类别的置信度。
2.3 为什么R-FCN能保持高精度
从直觉上讲,去掉全连接层应该会损失精度才对,因为全连接层的容量大、参数多。但R-FCN在VOC和COCO上的精度不降反升,原因在于“位置敏感设计”本质上弥补了全连接层丢失的空间信息。
全连接层的问题在于它几乎不保留空间结构。而R-FCN用“把位置信息编码进通道”的方式,既保留了空间信息,又没引入贵的全连接参数。这算是一个很聪明的小技巧,后面很多工作(比如后来的CenterNet、FCOS这类anchor-free方法)都能看到这种“让特征保持空间感知”思路的影子。
而且全卷积结构的另一个好处是,共享特征图只计算一次,所有RoI共用,计算量被摊薄,这点在候选框数量多的时候尤其明显。
3. 源码模块拆解与核心实现解析
3.1 官方源码目录结构解析
R-FCN官方Caffe版本代码仓库不大,核心代码集中在几个位置。我先给出一张目录级的拆解,方便大家对照分析。
rfcn/ ├── lib/ │ ├── datasets/ │ │ ├── pascal_voc.py │ │ ├── coco.py │ ├── rfcn/ │ │ ├── rfcn_net.py │ │ ├── psroi_pooling_layer.py │ │ ├── proposal_layer.py │ ├── nms/ │ ├── utils/ ├── experiments/ │ ├── cfgs/ │ │ ├── resnet_v1_101_voc0712_rfcn_end2end.yaml ├── models/ │ ├── pascal_voc/ │ │ ├── ResNet-101/ │ │ │ ├── rfcn_end2end/ │ │ │ │ ├── test.prototxt │ │ │ │ ├── train.prototxt │ │ │ │ ├── solver.prototxt │ │ │ │ └── ...这个结构是典型的Caffe项目风格,关键就三个地方:rfcn_net.py负责整个网络流程的编排,psroi_pooling_layer.py实现位置敏感RoI池化,proposal_layer.py负责从RPN输出生成候选框。
值得注意的是,官方仓库并没有把RPN重新实现一遍,而是直接复用了Fast R-CNN里的RPN逻辑,这点在你读源码的时候会发现几乎不用改动。所以R-FCN的“新”主要集中在PSROIPooling和网络末尾的分类/回归分支设计上。
3.2 PSROIPooling层源码的核心逻辑
PSROIPooling是整个R-FCN源码里最值得反复细看的模块。这里我把官方Caffe实现中的核心步骤简化整理一下,保留主要逻辑。因为网络里面输入的得分图是一整张的,对每个RoI都要从这张大图上“抠”出属于自己位置子区域的响应,所以需要仔细控制索引。
实际实现中,输入是一个四维tensor:(batch, 189, H, W),输出是两个部分:分类得分和bbox回归得分。分类得分输出形状是(batch, 21, 7, 7),这里7x7是池化后的空间尺寸。
核心循环大概长这样:
# 伪代码,展示PSROIPooling的空间映射逻辑 for each roi in rois: # 把roi在原图上的坐标映射到特征图尺度 roi_start_w = round(roi[1] * spatial_scale) roi_start_h = round(roi[2] * spatial_scale) roi_end_w = round(roi[3] * spatial_scale) roi_end_h = round(roi[4] * spatial_scale) roi_height = max(roi_end_h - roi_start_h, 1) roi_width = max(roi_end_w - roi_start_w, 1) bin_size_h = roi_height / pooled_height # pooled_height = 7 bin_size_w = roi_width / pooled_width # 对每个输出位置计算对应子区域的均值 for ph in range(pooled_height): for pw in range(pooled_width): # 当前子区域在原roi内的起止位置 hstart = int(math.floor(ph * bin_size_h)) wstart = int(math.floor(pw * bin_size_w)) hend = int(math.ceil((ph + 1) * bin_size_h)) wend = int(math.ceil((pw + 1) * bin_size_w)) hstart = min(max(hstart + roi_start_h, 0), fea_height) hend = min(max(hend + roi_start_h, 0), fea_height) wstart = min(max(wstart + roi_start_w, 0), fea_width) wend = min(max(wend + roi_start_w, 0), fea_width) # 关键:从C个类别 × 9个位置中选择当前子区域对应的通道 # c是类别索引,position = ph * pooled_width + pw for c in range(num_classes): channel_index = c * 9 + ph * pooled_width + pw # 对sub_region内的特征值求和取平均 output[roi_idx, c, ph, pw] = avg_pool(feature_map[channel_index, hstart:hend, wstart:wend])这里最关键的一行就是channel_index = c * 9 + ph * pooled_width + pw,它把“输出特征图上的空间位置索引”和“输入通道索引”绑定在一起,这就是位置敏感得分的物理含义所在。
我在第一次读这段代码的时候,对“通道索引”和“空间位置索引”的关系一直没绕明白,后来是自己手动拿笔画了个3x3的格子,标上每个格子对应的通道范围,才算彻底清楚。
3.3 分类与回归分支的设计
R-FCN的输出分成两个分支,分别对应类别和边界框回归。位置敏感得分图的数量也因此分成两组:一组是(类别数 x 9)的得分图,用于分类;另一组是(4 x 9)的得分图,用于边界框回归。
为什么是4?因为每个候选框的回归需要预测4个值:中心点x偏移、y偏移、宽度缩放、高度缩放。所以用于回归分支的通道数是4 x 9 = 36。在PyTorch版本里,这一般体现为最后一层卷积的输出通道数是(21 + 4) x 9 = 225(以VOC类别为例)。
两个分支共享前面所有卷积层的特征,最后各接一个1x1卷积,再各自过PSROIPooling。这个设计说明作者当时追求的就是极致的简洁——分类和回归之间不设额外交互层,全凭共享特征图和位置敏感得分图上的分工完成。
但是这种“完全共享特征”的方式在后来的实践中被证明是有局限的,分类和回归对特征的需求不完全一致,所以后来的Cascade R-CNN等工作又专门做了拆分分支设计。回过头来读R-FCN源码时,可以对比着看这个演进过程,理解起来会更有意思。
4. 训练流程与关键配置解析
4.1 端到端训练的参数细节
R-FCN在源码里支持两种训练模式:一种是“分步训练”,先训RPN,再固定RPN训练R-FCN;另一种是“端到端训练”,整个网络一起反传。官方推荐使用端到端模式,具体配置文件是resnet_v1_101_voc0712_rfcn_end2end.yaml。
配置文件中几个关键参数:
TRAIN.BATCH_SIZE: 通常是256,256个RoI,其中正样本最多占25%,也就是64个。TRAIN.FG_THRESH: 和Fast R-CNN一致,RoI与某个真实框的IoU大于等于0.5时视为正样本。TRAIN.BG_THRESH_HI: IoU在0.1到0.5之间的作为负样本候选,低于0.1的直接忽略。TRAIN.SNAPSHOT_ITERS: 默认5000,每5000次迭代保存一次模型快照。TEST.RPN_POST_NMS_TOP_N: 测试阶段RPN非极大值抑制后保留的候选框数量,默认2000个。
训练过程中,RPN的损失和R-FCN的损失会直接相加作为总损失进行反向传播。源码里rfcn_net.py调用了add_losses函数,把RPN分类损失、RPN回归损失、R-FCN分类损失、R-FCN回归损失四部分组合起来。
4.2 ResNet-101与空洞卷积的配合
R-FCN源码默认backbone是ResNet-101,但不是直接用ImageNet预训练的ResNet-101结构,而是做了改造:把最后一个阶段(即res5,也就是通常的layer5)的下采样步长从32像素改到16像素,同时引入了空洞卷积(atrous convolution),又叫扩张卷积,来保持感受野。
具体做法是,在ResNet-101的最后一个残差块中,把stride=2改成stride=1,并在所有3x3卷积中设置dilation=2。这样做的目的是让最终特征图的分辨率更高,小目标检测效果更好。对应的Caffe prototxt里你会看到dilation: 2这种参数。
源码里这一部分在模型的prototxt里体现得很明显,搜索dilation关键字就能找到。
提示:如果把res5的stride改为1但不同时引入空洞卷积,会导致感受野变小,BatchNorm统计量也会剧烈波动,训练极容易崩。R-FCN源码的做法等价于用空洞卷积保住感受野,这是一个非常重要的工程细节。
4.3 权值初始化与学习率策略
R-FCN采用ImageNet上的ResNet-101预训练模型做初始化。Pointwise和1x1的新增卷积层采用标准差为0.01的高斯分布初始化,偏置初始化为0。对于PSROIPooling后面的输出层,由于通道数很大,参数初始化时特别小心,一般会用标准差较小的高斯分布,避免初期损失出现极端大的数值。
学习率策略是典型的“warmup + 分段下降”:
- 初始学习率
0.001 - Warmup阶段:前500次迭代从0.000333线性升到0.001
- 每60000次迭代衰减为原来的0.1倍,共衰减两次
这套学习率策略在Faster R-CNN源码里就是标准配置,R-FCN沿用但微调了衰减节点。跑VOC数据集时,总迭代数大概在110k左右。如果显存不够,batch size从256降到128,学习率也应该等比调整,不然很容易发散。
5. 复现过程中的经验汇总
5.1 复现时的踩坑记录
我分别用官方Caffe代码和一个PyTorch复现版本做过实验,说一下遇到过的典型坑。
第一个坑是PSROIPooling的反向传播实现。这个层的前向很简单,就是区域求平均,但反向传播要记录每个输出位置的“平均系数”,把梯度均匀分配回对应的输入区域。源码里如果坐标处理不当(比如hstart或hend越界),反向时会梯度爆炸。后来我加了一些边界检查日志,发现VOC图片的边框标注偶尔会超出图像边界,导致坐标负值出现,处理不及时就会nan。
第二个坑是数据增强比例。官方配置里训练时只做了水平翻转,没有裁剪、缩放等增强。但在小数据集上训练时,这个配置收敛得偏慢,loss波动很大。我后来加了多尺度训练,后发现虽然每轮迭代时间变长,但最后mAP提升很明显,尤其是小目标那几类。
第三个坑是BatchNorm的使用。R-FCN论文里使用的是“固定参数的BatchNorm”,也就是训练时用的统计量,而测试时直接沿用训练集累计的均值和方差。在PyTorch复现时需要注意设置track_running_stats和参数的requires_grad,否则就会出现在Caffe上mAP正常、迁移到PyTorch后掉2~3个点的怪问题。
5.2 新手复现路线建议
我不建议新手直接从Caffe入手,Caffe的环境配置在今天的操作系统上容易出各种兼容性问题,尤其是protobuf版本和CUDA版本的匹配。我自己在新机器上编译官方Caffe花了差不多一整天,各种libprotobuf版本冲突,非常折磨。
比较推荐的路线是,先用PyTorch的社区复现版本跑通一遍训练,例如jwyang/faster-rcnn.pytorch这个仓库里就带R-FCN的实现,代码风格清晰,改动不多,适合学习。跑通之后,再回头去看Caffe官方源码里的psroi_pooling_layer.cpp,理解底层实现,这时你的关注点就会放在算法逻辑本身,而不是被环境问题带跑偏。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| loss直接变成nan | PSROIPooling坐标越界,或者学习率过大 | 检查RoI坐标边界,减小初始学习率,确认warmup设置 |
| mAP在20以下 | 类别数和位置数没对上 | 检查score map通道数是否等于(类别数+K*K),K为3x3的位置分块数 |
| 训练速度极慢 | 候选框数量设置过大 | 适当减小RPN的post_nms_top_n,训练阶段选1200以内即可 |
| 小目标检测效果差 | res5层分辨率不够 | 确认是否已把res5步长改为1并设置dilation=2 |
| 测试时分类得分异常集中 | 分类分支和回归分支的score map输出顺序搞混 | 检查网络最后一层输出的channel排列顺序,先分类后回归,或者反过来,要与pooling的索引逻辑一致 |
| 迁移到PyTorch后精度下降 | BatchNorm统计量处理不对 | 用eval模式,并固定running stats |
5.4 关于K值的理解
R-FCN源码里K默认是3,也就是把roi区域划分为3x3个子区域。很多人会问:K越大是不是效果越好?从理论上讲,K越大,位置粒度越细,分类的区分度越高,但K值太大会让每个子区域包含的像素变少,统计噪声增加,而且得分图通道数会平方级增长,内存开销增大。我试过K=4和K=5,在VOC数据集上mAP提升几乎可以忽略,但显存占用多了不少。所以官方默认的3x3其实是性价比最高的选择,不建议新手盲目去调这个参数。
如果你要调整K值,需要同步修改三处:分类得分图通道数、回归得分图通道数、以及PSROIPooling的pooled_height和pooled_width。这三处对不上,网络跑都跑不起来,报错信息通常还很晦涩,多半是维度不匹配。
6. 源码阅读的心得体会
R-FCN源码在目标检测开源项目里算是“小而美”的典范。代码量不大,但结构清晰,创新点集中,非常适合想做检测方向研究或者想入工程一线的同学去仔细阅读。我自己回过头来看,读透R-FCN源码带来几个很实际的收益:一是彻底理解了RoI-based检测 pipeline的完整链路,二是对“如何用通道编码空间信息”有了直观认识,这对后来理解FCOS、CenterNet等anchor-free方法帮助很大。
如果时间有限,建议把重心放在psroi_pooling_layer的前向和反向推导上。用笔推一遍坐标映射关系,再对照代码里的循环看一遍,效果会比单纯跑通训练好很多。我当年就是在草稿纸上画了好几张3x3格子图,才把这个操作彻底刻进脑子里。不要嫌麻烦,这种底层操作的直觉,后面做其他任务时迟早会用到。
最后再说一个操作层面的小建议:如果你只想快速验证R-FCN的效果,用COCO预训练模型直接做迁移测试就够了,没必要从头训。从头训一版VOC要差不多一天时间,工程收益不高。先跑通测试流程,再回头研究训练曲线,节奏会舒服很多。
本文还有配套的精品资源,点击获取