1. 为什么还要回头细啃RPN这段时间有不少读者问我一些目标检测相关的实验问题聊到最后发现很多人对两阶段检测器里的“区域提议网络”始终处于一种“大概懂但说不透”的状态。能画出RPN接在Backbone后面、输出一堆框但一问到“为什么它能同时输出类别和坐标”“1x1卷积在这里到底起了什么作用”“训练时RPN的损失是怎么算的”就语焉不详了。这其实不怪大家。现在很多入门教程把RPN当作一个跳板几句话带过就开始讲Fast R-CNN那头的事情似乎RPN只是一个“会出框的模块”。但RPN在目标检测发展史上的地位非常特殊它是第一个把“候选区域生成”从传统的Selective Search、EdgeBoxes这类离线算法变成完全可微、可以端到端训练的神经网络模块的尝试。换句话说从RPN开始“找目标在哪”这件事第一次成了网络自己就能学会的本事。而且到了今天RPN的思想早就不只活在Faster R-CNN里。YOLO系列里直接回归先验框的做法、DETR里基于Query的稀疏候选、甚至一些跟踪算法里对目标进行初步定位的头都还能看到RPN的影子。把RPN吃透你再看这些后续工作会发现很多设计都有迹可循。这篇文章我就用一次完整的前向计算和反向传播的视角把RPN的结构、锚点机制、正负样本分配、损失函数、NMS后处理全部串一遍。看完之后你不仅能把结构图画出来还能自己算出每一层的输出尺寸以及理解每个设计背后的意图。2. 先从Faster R-CNN的整体框架里看RPN的位置2.1 RPN之前的检测器在愁什么在Faster R-CNN出现之前基于深度学习的检测器主要靠Selective Search这类方法先生成约2000个候选区域再把这些区域送入分类网络判断类别和微调坐标。这个流程有个明显的割裂感候选区域生成完全在深度学习之外用的是图像的颜色、纹理、区域相似度等底层特征速度慢不说生成的区域质量也不稳定。具体慢到什么程度呢在一张普通尺寸的图片上Selective Search生成大约2000个候选框耗时在2秒左右。这个耗时放到现在的实时检测任务中是无法接受的哪怕在当时它也成了整个检测流程的性能瓶颈。更麻烦的是分类网络的精度直接取决于候选框的质量。如果候选框对目标的位置覆盖不准确后续的RoI Pooling再厉害也补救不回来。这就倒逼大家思考一个问题能不能让网络自己学会生成候选区域2.2 RPN插入的位置和扮演的角色Faster R-CNN给出的答案是在Backbone输出的特征图上并联一个轻量级网络由它来产出候选区域。这个轻量级网络就是我们说的RPN。从结构位置上看RPN输入的是Backbone在Faster R-CNN原文里是VGG16的conv5_3特征图的输出输出的是两类东西一系列候选框的坐标以及每个候选框是“前景”还是“背景”的分数。这里有一个细节值得注意RPN其实是和Fast R-CNN那个分支共享Backbone特征的。在训练时Faster R-CNN采用了四步交替训练法——先训练RPN再用RPN的提议固定下来训练Fast R-CNN然后微调RPN最后微调Fast R-CNN。到了更后面的实现里不少人直接用联合训练的方式也就是把RPN和检测头的损失加在一起反向传播。我们如果只看推理阶段RPN的工作流程非常清晰特征图进来经过滑窗和两条1x1卷积分支得到一堆带分数的锚点框然后做一次NMS把重叠严重的候选框去掉留下大约2000个推理时通常取300个左右送给后面的检测头。2.3 从一张特征图到一堆候选框为了后面讲得具体我这里用一个具体的数值例子来说明。假设输入图片是800x600Backbone经过4次下采样后特征图尺寸是50x38实际算上stride和一些边缘效应Faster R-CNN的特征图尺寸是约60x40这里我取一个好算的数。RPN在这张特征图上做3x3滑窗每个滑窗位置生成k个锚点框。原文里k3对应3种尺度乘以3种长宽比也就是9个锚点。那么这条特征图上一共就有50x38x917100个锚点。RPN要做的事就是从这17100个框里挑出那些最有可能包含目标的框并且把它们的坐标调整到更贴合真实目标的位置。这里你可以把RPN理解成一个极其勤奋的“海选面试官”候选者非常多一万多个锚点框面试官先用最简单的条件快速筛掉绝大多数背景剩下的少数前景再做精细的坐标调整。整个海选过程要足够快所以RPN的结构必须轻量不能在分类或回归上做得太复杂。理解了这个定位你就能明白为什么RPN两条分支都是1x1卷积为什么它只分前景背景而不是具体类别以及为什么锚点框要设计那么多尺度和比例。3. 锚点机制RPN的立身之本3.1 锚点究竟是什么锚点这个概念现在很多检测器里都有但RPN是较早把锚点作为核心机制全面使用的网络结构之一。锚点的本质是在特征图的每个位置预先定义一组固定大小和长宽比的框。为什么要预先定义因为“目标在哪、目标多大”这个问题直接让网络回归出任意坐标是极其困难的——坐标空间太大网络无从下手。但如果先把候选框的位置和形状限定在一定的先验范围内网络只需要在这个范围内做微调问题就简单很多。举一个生活中的例子让你在操场上找一个足球你的策略肯定不是直接预测足球的经纬度而是先环顾四周确定几个可能的区域比如球门附近、跑道边、裁判脚边然后在这几个局部区域里细看。锚点就相当于这些“可能的区域”RPN要做的是判断哪些区域里真有球并且稍微调整一下搜索范围的中心和大小让框正好包住球。虽然这个例子不完全精确但直觉是通着的。3.2 锚点的尺度与长宽比设计在Faster R-CNN原文中锚点有3种尺度128、256、512像素和3种长宽比1:1、1:2、2:1组合出9个不同形状的锚点。这个设计考虑了目标检测的实际情况目标大小差异很大比如大卡车和小行人可能出现在同一张图里目标形状也有差异比如行人偏瘦长1:2车辆偏宽扁2:1。有一点需要特别注意锚点的尺度是相对于输入图片的像素而不是特征图的像素。也就是说在特征图上滑动一个位置对应回输入图片就是原图上一个位置这个位置上生成的9个锚点是以原图尺度计算的。RPN在回归的时候预测的坐标偏移量也是相对于锚点框的这个偏移量会在后续映射回原图。当Backbone的下采样倍率是16时比如VGG16的conv5_3特征图上一个像素对应原图16x16的区域。锚点的坐标在原图和特征图之间换算时需要除以这个stride。这一点在调试代码时特别容易出错我曾经见过有同学在自定义数据集的RPN实现里锚点坐标忘了除以stride导致训练时损失直接异常。3.3 滑窗与1x1卷积的关系RPN在特征图上做3x3滑窗。这里有个容易混淆的点3x3滑窗是卷积操作不是遍历提取。在具体实现里RPN通常用一个3x3卷积padding1、输出通道数与特征图一致先对特征图做一次局部上下文聚合然后再分成两条1x1卷积分支。3x3卷积的作用是让每个位置的滑动窗口能参考周围3x3区域的信息而不是只看单个像素点。这有点像“扩大了一点视野再下判断”。如果你直接用1x1卷积来滑窗每个位置就只能看到该位置的单点特征信息量不够分类和回归都容易不准确。两条1x1卷积分支的输出通道数设计也很有讲究。分类分支输出2k个通道对应每个位置k个锚点的前景/背景logits回归分支输出4k个通道对应每个锚点的中心点偏移和宽高缩放。之所以用1x1卷积而不是全连接层一方面是为了保持特征图的空间结构另一方面是因为1x1卷积参数量小、计算快而且本质上等同于在一个位置上的“全连接”但因为卷积的特性它可以高效地处理所有位置。我可以用一个具体的维度变化来强化理解。输入特征图是(B, C, H, W)经过3x3卷积后仍为(B, C, H, W)然后分类分支的1x1卷积把通道数变为(B, 2k, H, W)回归分支变为(B, 4k, H, W)。把空间维展平后分类分支变成(B, HWk, 2)回归分支变成(B, HWk, 4)。也就是说每个锚点有2个分类分数和4个回归偏移量。3.4 锚点匹配的IoU规则有了锚点还需要让锚点跟真实标注框Ground Truth建立对应关系才能算损失。RPN的做法是基于IoU交并比来分配标签。具体规则如下锚点与某个真实框的IoU最大的标为正样本即使IoU不高也强制选一个“最有希望”的锚点与任意真实框的IoU大于0.7的标为正样本锚点与所有真实框的IoU都小于0.3的标为负样本介于0.3到0.7之间的锚点不参与训练在损失计算中被忽略。这三条规则看似简单实际使用中有几个隐含的坑。第一条规则是为了保证每个真实框至少有一个正样本锚点不然小目标或者形状极端的目标可能一个匹配上的锚点都没有。第二条规则是为了让高质量锚点尽量多保证正样本的多样性。第三条规则是防止那些“似是而非”的锚点给训练带来噪声。很多人第一次看这个规则会觉得0.3和0.7这两个阈值拍脑袋定的其实它们是Faster R-CNN作者经过实验验证的对一般场景比较稳定的取值。阈值调大或调小会直接影响正负样本的比例进而影响训练稳定性和最终检测精度。4. RPN的损失函数与训练策略4.1 分类分支和回归分支的损失组合RPN的损失函数由两部分组成分类损失和回归损失。总损失是两个损失的加权和原文里把权重都设为1也就是L L_cls L_reg分类损失用的是二值交叉熵损失Binary Cross Entropy。因为RPN只区分前景和背景所以这是标准的二分类问题。每个正负样本锚点都会贡献分类损失被忽略的锚点不参与计算。回归损失要稍微复杂一些。RPN回归的不是框的绝对坐标而是锚点相对于真实框的偏移量。具体来说回归目标是这样定义的t_x (x_gt - x_anchor) / w_anchor t_y (y_gt - y_anchor) / h_anchor t_w log(w_gt / w_anchor) t_h log(h_gt / h_anchor)这里x、y代表中心点坐标w、h代表宽高。可以看到中心点偏移量除以锚点宽度和高度是为了归一化不同大小的锚点在损失计算中拥有相近的尺度宽高比取对数是为了让缩放因子变得对称比如宽高放大2倍和缩小2倍在log空间里表现为1和-1幅度相当。回归损失用的是Smooth L1损失。这个损失函数在误差较大时梯度饱和不容易被离群点带偏在误差较小时梯度更平滑有利于精细微调。4.2 正负样本采样的平衡问题训练RPN时锚点的数量极其庞大。假设一张图有50x381900个位置每个位置9个锚点总共17100个锚点。但正样本锚点IoU大于0.7通常只有几十到几百个负样本却多得多。这种极端不平衡会导致训练偏向于预测背景因为网络只要把所有样本都判成背景损失就很小。为了解决这个问题RPN在训练时会对样本进行随机采样。具体做法是在一张图中从所有正样本里随机选一些从所有负样本里随机选一些凑成一个mini-batch通常是256个样本其中正负样本比例尽量接近1:1。如果正样本数量不足128个就用负样本填充如果负样本不够就用正样本或忽略的样本来补。这里有一个细节2k个分类通道对应的是每个位置的k个锚点。在实际的PyTorch等框架实现中通常会先在特征图上生成所有锚点的坐标计算每个锚点与真实框的IoU然后根据IoU分配标签再根据标签采样样本最后提取对应位置的分类分数和回归偏移量来计算损失。如果你只是把特征图输入卷积层然后直接对全部输出算损失没有做样本采样正负样本极度不平衡训练基本不会收敛到理想的检测效果。4.3 边界锚点的处理方式锚点框可能超出图像边界。比如锚点中心点靠近图像边缘时锚点框的一部分会落在图像外部。这些边界外的锚点在计算IoU和回归目标时可能产生不合理的数值甚至在某些实现中会导致NaN问题。Faster R-CNN原文的做法是训练时直接忽略所有超出边界的锚点因为它们没有有效的真实信息可供学习但在测试时先把超界的锚点裁切到图像边界内再参与后续计算。关于这一点不同实现有不同细节。很多开源代码在训练时把超出边界的锚点标记为“忽略”-1不计算损失。我自己实践下来这个边界处理看似不起眼但如果处理不当比如让边界锚点参与了损失计算而它的回归目标又是个巨大的负值或正值损失曲线会直接变得异常甚至训练发散。4.4 RPN与检测头的共享特征训练Faster R-CNN最大的创新之一就是RPN和检测头共享Backbone的特征提取结果。共享特征带来两个好处一是计算量大幅减少Backbone只需要前向一次二是RPN学到的“哪里可能有目标”的能力能反过来引导Backbone学习更有利于目标检测的特征。但共享特征也给训练带来了新的挑战RPN希望特征能突出目标的边缘、轮廓和显著性区域而检测头希望特征更能区分不同类别的语义差异。这两种需求在早期训练阶段可能产生冲突导致收敛变慢。在实际操作中原论文使用四步交替训练法来缓解这个冲突第一步训练RPN第二步用RPN生成的候选框训练Fast R-CNN检测头第三步固定共享的Backbone层只微调RPN层第四步固定RPN层只微调Fast R-CNN检测头。这样做的好处是每一步的网络都有相对明确的学习目标训练更稳定。现在的很多复现和实现中我见过有人直接用联合训练joint training把RPN和检测头的损失加起来一起反向传播。这个方法在大多数情况下也能收敛而且训练效率更高但如果你发现训练不稳定可以考虑切回交替训练模式进行排查。5. 推理阶段如何从RPN的输出得到最终候选区域5.1 从得分到预筛选推理阶段RPN的输出是一大堆锚点框以及它们的前景得分。假设输入特征图是50x38每个位置9个锚点总共有17100个候选框。不可能把这17100个框全部送到检测头去原因有两方面一是计算量太大检测头需要对每个候选框执行RoI Pooling和全连接计算17100个框会让推理慢到不可接受二是这些框高度重叠大量是冗余的没有必要都保留。所以推理时RPN会先做一个按得分排序的预筛选。具体做法是对所有锚点的前景得分从高到低排序取前大约6000个锚点框不同实现里数量略有差异比如有些代码里取12000有些取6000。这一步是纯粹为了减少后续NMS的计算量。5.2 NMS非极大值抑制NMS是RPN推理流程中极其关键的一步。经过预筛选得到的框仍然大量重叠——同一个目标上可能有几个甚至十几个锚点都被判定为前景它们的框位置和大小略有不同。目标检测最终只需要一个最贴合目标的框而不是一堆重叠框。NMS的工作原理按得分从高到低依次处理每个框凡是与当前最高得分框的IoU超过一定阈值的框都被抑制掉留下的框进入最终结果列表然后继续处理剩余框中得分最高的重复这个过程。对于RPN来说NMS的IoU阈值在原文中设置为0.7。这个阈值设置有一个权衡阈值越高抑制越少保留的框越多召回率更高但冗余更多阈值越低抑制越激进保留的框更少计算效率高但可能误删一些真实目标。0.7是经过实验验证的一个平衡点。NMS执行完之后RPN会选择得分最高的前2000个框训练时或300个框测试时作为最终候选区域。2000和300这两个数字也是性能与精度的折中。5.3 从RPN候选框到RoI PoolingRPN输出的候选框坐标需要映射回原图尺度然后送给检测头做RoI Pooling。这里需要特别注意坐标系的变换。训练Faster R-CNN时Backbone输入的是原始图像或者resize后的图像。RPN在特征图上预测的回归偏移量需要先映射回特征图的锚点坐标再乘以stride比如16得到原图坐标。如果网络输入时做了resize最终还原到原始图像尺寸还要再做一次等比缩放逆变换。我在实际项目中遇到过不少因为坐标变换不一致导致的检测框偏移问题。比如在自定义数据Pipeline里图像先被resize到800像素又做了normalize但候选框坐标还原时用了错误的缩放比例结果检测框老是出现系统性偏移。这种问题排查起来很费时间建议从一开始就明确坐标系的流转路径每一步输入输出都做好尺寸记录。5.4 RPN与类别无关的设定RPN最终输出的候选框是不带类别信息的它只告诉后续检测网络“这些区域里可能包含某个目标”但具体是什么目标由Fast R-CNN检测头来判定。这个设计对整个系统来说非常优雅因为RPN不需要知道类别它的学习目标更纯粹就是前景和背景的二分类加坐标回归。但这也带来一个局限如果某些目标的外观特征导致RPN漏检了后面的检测头再强也无计可施。因此在实际系统里RPN的召回率是一个关键指标。调参时如果你发现最终检测精度上不去不要急着调检测头的参数先看看RPN的召回率是不是有问题——可以用一些可视化方法把RPN输出得分最高的几百个框叠加到图像上直观判断漏检发生在哪个环节。我在项目排查中常用的一个做法是单独把RPN的输出保存下来包括候选框坐标和得分然后在图上画出得分前50的框。如果这些框基本覆盖了所有目标说明RPN没问题问题在于检测头的分类或回归如果框本身就明显偏离目标那问题就在RPN。这个经验帮我节省了很多排查时间。6. 常见问题与调参经验实录6.1 锚点参数对检测效果的影响很多人直接用Faster R-CNN默认的锚点尺度128、256、512和比例1:1、1:2、2:1但在自己的数据集上效果不理想。这很正常因为默认参数是针对Pascal VOC这类常见目标设计的目标尺度分布相对均匀。如果你的数据集里目标普遍很小比如航拍图像里的车辆或者显微图像里的细胞默认的128像素最小锚点可能就太大了。这时候可以通过统计训练集里所有真实框的面积分布来确定适合的锚点尺度。具体做法把所有真实框按面积排序看它们的中位数和分布区间然后把锚点尺度设计成几组能覆盖这个分布区间的值。比如统计发现目标面积集中在32、64、128像素左右就把锚点尺度设为16、32、64、128、256几个级别。长宽比也类似统计目标的宽高比分布集中在细长目标就偏向1:3甚至1:5集中在方形目标就多设几个1:1。锚点数量也不一定固定为9个。现代很多检测器会使用更多尺度和比例的组合比如RetinaNet使用3个尺度乘以3个比例但尺度级数更多。原则是锚点数量越多召回率越高但计算量也越大正负样本分配更加复杂。需要根据GPU显存和任务需求权衡。6.2 训练不稳定时的排查方向如果你训练RPN时发现损失曲线震荡剧烈或者直接发散优先检查以下几个点第一学习率是否过大。RPN的分类和回归分支是同时训练的回归分支的初始损失往往比分类大不少如果学习率过大回归分支的梯度会主导更新导致特征被破坏。建议初始化时先把分类分支和回归分支的损失权重分别观察一下如果两者量级差异明显可以调整权重让它们处于同一量级。第二正样本数量是否过少。如果一张图里只有几个正样本锚点RPN几乎学不到有效信息。这时可以尝试降低正样本的IoU阈值比如从0.7降到0.6或者增加锚点数量让更多锚点有机会匹配到真实框。第三回归目标是否异常。检查一下锚点和真实框匹配后的回归目标t_x, t_y, t_w, t_h如果出现特别大的值比如t_w超过了5说明锚点尺度和目标尺度严重不匹配需要重新设计锚点尺度或者检查坐标变换代码是否有bug。第四Batch Size是否合理。RPN在每张图上采样256个样本如果Batch里只有一两张图整个Batch的正样本可能太少梯度噪声很大。适当调大Batch Size通常能稳定训练。6.3 RPN与后续检测头的联动调参RPN输出的候选框质量直接决定最终检测精度但实际调参时很多人会忽略RPN和检测头两者之间的配合。一个常见的问题是RPN的NMS阈值设置过严比如0.5导致经过NMS后保留的框数量变少虽然这些框质量高但召回率下降了。此时检测头能看到的候选区域不足再强的分类器也白搭。反过来如果NMS阈值设置过松比如0.9检测头需要处理大量高度重叠的候选框不仅计算浪费还可能导致最终输出多个互相重叠的检测框。另外一个实践经验是在训练初期不要把RPN的NMS阈值设得太严。因为训练开始时RPN的回归分支还不成熟预测的框位置不准确此时需要保留更多候选框来确保正样本覆盖率。等训练稳定后再逐步收紧NMS阈值让检测头在更精炼的候选集上做精细分类。6.4 候选框坐标精度与现代FPN的配合原始的Faster R-CNN是单层特征图接RPN但现代检测器普遍使用FPN特征金字塔网络在多层特征图上分别做RPN。FPN的优势是不同尺度的目标可以分配到不同分辨率的特征层上检测小目标用高分辨率特征层大目标用低分辨率特征层这样能显著提升多尺度目标的检测精度。如果你想在项目里把RPN迁移到FPN结构上每个特征层都需要单独的RPN头吗实际上不需要那么复杂常见做法是让不同层共享同一个RPN头但是每层负责不同尺度的锚点。比如P2层负责32x32的锚点P3层负责64x64的锚点P4层负责128x128的锚点。这样做既平衡了计算量又大大增强了RPN对多尺度目标的适应能力。关于候选框坐标精度我还想补充一句。RPN回归的是锚点框与真实框之间的偏移量但在两层甚至三层的级联检测器如Cascade R-CNN中第一阶段的RPN框会经过不断修正。如果你负责的项目追求高精度检测可以考虑在RPN输出后增加一次BBox Regression头来框坐标精修这个头可以用RoI Align后的特征来预测精度会有明显提升。7. RPN思想在现代框架中的延续前面聊了那么多RPN的细节最后我想把它放进一个更大的坐标轴里看。RPN这个结构今天依然有生命力但它在各种新框架里被“改头换面”地继承了。比如在YOLO系列中锚点机制被保留下来但不是放在独立的RPN网络里而是直接作为检测头的回归目标。YOLOv5、YOLOv8里仍然有基于锚点的设计思路只不过不再显式地做前景背景二分类和NMS的中间过程而是把分类和回归放在同一个输出张量里。YOLOv8之后甚至转向了Anchor-Free但“特征图上预先定义位置、回归目标中心与尺寸”这个思想和RPN其实是一脉相承的。跟踪算法中也常见RPN的影子。比如一些基于孪生网络的跟踪器SiamRPN其核心思路就是用一个类似RPN的模块在搜索区域上预测目标框的得分和回归偏移量。虽然架构细节不同但“预定义锚点分类回归双分支”这个骨架就是从Faster R-CNN的RPN里来的。DETR这类基于Transformer的检测器虽然用Query取代了锚点框但它在训练时也需要通过匈牙利匹配将Query与真实目标一一对应本质上还是在解决“如何从大量候选中选出正确的那一个”这个RPN时代就存在的基本问题。所以如果你理解了RPN的匹配、采样、分类回归联合训练这套逻辑再去看DETR的训练过程很多困惑会迎刃而解。回到实践上来我在自己的实验里反复体会到一个道理真正重要的不是背下RPN的各种数值参数而是理解它“如何在极大冗余中筛选出有限有效信息”的思维方式。这个概念在几乎所有现代检测、分割、跟踪算法中都有体现。你在自己的数据集上调参时只要抓住“锚点是否匹配目标尺度、正负样本是否平衡、候选框质量是否够格送给下一阶段”这几个关键点就能把RPN调到一个相当可用的状态。如果后面有机会我再把RPN在FPN上的实现细节、以及它和RoI Align的配合机制单独拆一篇文章详细展开。这次先到这里希望对你能有帮助。
阅读完成 · 觉得有帮助?