ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

R-FCN源码解析:位置敏感得分图与全卷积目标检测网络实现

R-FCN源码解析:位置敏感得分图与全卷积目标检测网络实现 简介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: 通常是256256个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的最后一个残差块中把stride2改成stride1并在所有3x3卷积中设置dilation2。这样做的目的是让最终特征图的分辨率更高小目标检测效果更好。对应的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.001Warmup阶段前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直接变成nanPSROIPooling坐标越界或者学习率过大检查RoI坐标边界减小初始学习率确认warmup设置mAP在20以下类别数和位置数没对上检查score map通道数是否等于(类别数K*K)K为3x3的位置分块数训练速度极慢候选框数量设置过大适当减小RPN的post_nms_top_n训练阶段选1200以内即可小目标检测效果差res5层分辨率不够确认是否已把res5步长改为1并设置dilation2测试时分类得分异常集中分类分支和回归分支的score map输出顺序搞混检查网络最后一层输出的channel排列顺序先分类后回归或者反过来要与pooling的索引逻辑一致迁移到PyTorch后精度下降BatchNorm统计量处理不对用eval模式并固定running stats5.4 关于K值的理解R-FCN源码里K默认是3也就是把roi区域划分为3x3个子区域。很多人会问K越大是不是效果越好从理论上讲K越大位置粒度越细分类的区分度越高但K值太大会让每个子区域包含的像素变少统计噪声增加而且得分图通道数会平方级增长内存开销增大。我试过K4和K5在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要差不多一天时间工程收益不高。先跑通测试流程再回头研究训练曲线节奏会舒服很多。本文还有配套的精品资源点击获取
返回列表