
1. 为什么目标检测绕不开RPN从先找候选区说起做目标检测的人早晚都会撞上同一个问题一张图摆在你面前目标到底框在哪儿早年的做法是滑动窗口——把整张图按不同尺寸切出几万个窗口挨个送进分类器判断是不是目标。这个方法笨得可以计算量爆炸不说窗口位置稍微偏一点分类结果就崩。后来大家改用Selective Search靠颜色、纹理之类的底层特征把可能的目标区域先聚出来再逐个分类。Selective Search比滑动窗口聪明但问题在于它是个独立的离线模块跟后面的检测网络完全脱节。更关键的是它的候选区域是在CPU上跑的一张图要折腾一两秒放到实时场景里根本没法用。Faster R-CNN在2015年被提出来的时候核心思路就是把候选区域生成这件事从CPU搬到GPU上并且让网络自己学会哪里可能有目标。这个承担候选区域生成的模块就是RegionProposalNetwork简称RPN。RPN的神奇之处在于它并不是一个独立于检测网络之外的东西而是跟后面的检测头共享卷积特征、一起训练。这样设计之后候选区域生成不再是预处理步骤而是整个检测流程里一个可学习、可微分的环节。你如果去读Faster R-CNN那篇论文标题直接就叫《Towards Real-Time Object Detection with Region Proposal Networks》可见RPN在当时就是奔着提速端到端去的。这篇文章我想把RPN从结构到训练、从理论到工程踩坑完整地捋一遍。适合刚接触目标检测、想知道Faster R-CNN里面到底发生了什么的同学也适合那些已经跑通代码、但对anchor分配和损失函数细节还一知半解的工程师。看完你应该能回答一个问题RPN到底是怎么从一张图上找出可能包含目标的候选框的。1.1 传统候选区域生成的瓶颈在RPN出现之前主流检测框架走的都是两条腿先用Selective Search或EdgeBoxes生成大约2000个候选区域再对每个区域做缩放、分类。这里面有个致命的效率问题2000个候选区域有大量重叠每个区域都要单独走一遍卷积网络提取特征计算冗余非常严重。Fast R-CNN虽然通过RoI Pooling让2000个区域共享整图的卷积特征但候选区域的生成依然在CPU上一张图仍要花掉12秒。整条pipeline被Selective Search卡着变成CPU拖GPU后腿的尴尬局面。你可以试想一下你的GPU利用率可能只有20%剩下的时间全在等CPU把候选框算出来这放到现在简直是灾难。更重要的是Selective Search的候选框是基于颜色、纹理、尺寸等底层视觉线索生成的它根本不知道目标长什么样。它可能在一个大目标的内部切出几十个小窗口也可能把前后景杂糅在一起。这种盲目生成的候选区域质量上限就摆在那里分类器再强也救不回来。RPN的思路恰恰相反让网络先学会这东西看着像目标再根据这些粗筛结果去做精确分类。候选区域生成从手工特征工程变成了网络学习的任务这是质的改变。1.2 Faster R-CNN选择RPN的核心理由Faster R-CNN之所以坚持用RPN而不是继续优化Selective Search理由其实很朴素要让整个检测框架真正端到端可训练唯一的办法就是把候选区域生成也变成一个网络模块。RPN本质上是一个轻量的全卷积网络它输入的是共享的特征图输出的是候选框以及每个框的前景分数。因为卷积特征已经被前面的骨干网络算好了RPN在上面做操作几乎不增加额外计算量。整张图的前向过程从图像输入到最终检测结果全部发生在GPU上没有CPU参与没有离线步骤。这个设计带来的另一个好处是RPN和后面的检测头能互相促进。RPN需要的特征是哪里有目标——这是粗粒度的、语义级别的信息而检测头关心的这个目标具体是猫还是狗——这是细粒度的。两者共享同一个特征提取器训练时梯度会同时优化这两方面的能力结果就是特征表达比单独训练任何一个任务都更鲁棒。后来大量实验也证明在VOC和COCO上RPNFast R-CNN的联合训练效果显著优于任何外部候选区域方法分类器的组合。1.3 RPN的输入输出先弄清楚边界RPN的输入和输出听起来很简单但很多初学者会在这一步懵掉。输入部分RPN接收的是骨干网络比如VGG16或ResNet最后一层卷积输出的特征图。假设输入图像是800×600经过VGG16的4次池化stride 16特征图尺寸就是50×37左右通道数是512。输出部分RPN输出两类东西。一类是每个anchor作为前景的概率分数另一类是anchor相对于ground truth的坐标修正量。也就是说RPN并不直接输出最终的候选框坐标它输出的是预设框anchor的偏移量。这两者在训练过程中的角色完全不同分数用来筛选哪些anchor值得留下来偏移量用来把这些anchor推到更准确的位置。RPN之后的RoI Pooling层拿到的就是经过筛选和坐标修正后的候选框。2. RPN的骨架anchor机制与全卷积设计RPN的核心在设计思想上就两件事一个叫anchor一个叫全卷积。理解了这两个词RPN的一半就算拿下了。anchor解决的是框从哪来的问题全卷积解决的是怎么高效地处理这些框的问题。这两件事配合起来RPN才能在保持精度的同时做到几乎不拖慢整体速度。2.1 anchor到底是什么滑动窗口上的预设框anchor这个概念第一次看特别容易绕晕但你只要抓住一句话anchor就是预先铺在特征图每个位置上的、尺寸比例固定的矩形框。RPN在特征图上做3×3的卷积滑窗滑窗的中心点对应到原图上的位置就在那个位置放置k个不同尺寸、不同长宽比的anchor。Faster R-CNN原文里k9也就是3种尺度128、256、512像素乘以3种长宽比1:1、1:2、2:1。这9个框cover了图像里常见的物体大小和形状——从瘦高的人到矮胖的车再到方方正正的标志牌基本都逃不出这个组合。你可以把anchor理解成一组先验假设网络不需要从零开始预测目标的绝对位置和大小只需要在预设框的基础上做微调。这个先验微调的范式后来被几乎所有现代目标检测器继承包括YOLOv2之后的YOLO系列。那anchor到底有多少个算一下特征图上每一个滑窗位置都有k个anchor整张特征图有H×W个位置所以anchor总数为H×W×k。拿前面说的50×37的特征图来算9倍之后大概有16650个anchor。一张800×600的图会产生超过一万六千个候选位置这还只是在RPN阶段。听起来很多但由于这些anchor之间共享卷积计算实际增加的耗时并没有想象中那么可怕。这里有个细节值得注意RPN在做滑窗的时候3×3卷积的每个位置并不是每次拿一个框去卷积而是把整个特征图统一过一遍卷积再在每一个位置接上两条1×1卷积的分支。这样设计的好处是anchor的数量再多计算量也只跟特征图大小有关跟anchor数量无关。这就是全卷积设计最巧妙的地方——空间上共享权重计算上独立于anchor数量。2.2 为什么RPN是全卷积网络共享计算的工程智慧“全卷积网络”这个词听起来高大上其实核心就一句话网络里只有卷积层和池化层没有全连接层。全连接层会把特征图压缩成一维向量丢失空间位置信息而卷积层的每个输出点天然对应原图的一个局部区域。RPN的判断必须基于框里面的内容所以空间位置信息绝不能丢。此外全卷积网络对输入图像尺寸没有固定要求——你给它一张任意尺寸的图它都能输出对应尺寸的特征图这在检测任务里非常重要因为检测框的尺寸差异太大了。更关键的是计算共享。骨干网络已经算好了特征图RPN接着在这张特征图上做3×3卷积和两个1×1卷积分支。如果RPN是独立的网络就得把特征图重新喂一遍计算量直接翻倍。但因为它是在已有特征图上接着算整个增加的计算量几乎可以忽略。论文里给过一个数字RPN的额外计算量在VGG16上大约只占总计算量的10%。这就是为什么Faster R-CNN能做到接近实时的底气所在。2.3 9个anchor的来龙去脉与平移不变性很多人会问为什么一定是3个尺度×3个比例而不是5个尺度×5个比例这个问题没有绝对答案。3×39是权衡了召回率和计算量的结果。如果你把anchor数量提高到25个5×5召回率确实会上去一点但正样本数量大爆炸训练时会引入大量低质量的匹配还会让NMS的处理时间直线上升。反过来如果只用1个尺度和1个比例k1遇到长宽比极端的目标就完全抓瞎。所以9这个数字是实践出来的平衡点后来很多工作比如Cascade RPN、GA-RPN都尝试过更复杂的anchor设计但收敛速度、显存占用、训练稳定性都比原始的9个anchor方案更难调。RPN另一个被反复提及的性质是平移不变性如果你把图像里的目标平移几个像素RPN给出的anchor对应的分类概率和回归偏移量也会平移相同像素而不会变化。这个性质来自全卷积结构——卷积的权重在空间上是共享的不管你滑窗滑到哪个位置用的是同一套参数。对比一下Selective Search就没有这个性质你把图像平移一下它分出来的区域完全可能变样。平移不变性意味着RPN学到的规则是看到这个形状就认为它是前景而不是在坐标(100,200)附近的是前景。这对模型的泛化能力至关重要。3. 分类与回归RPN如何输出靠谱的候选框RPN在具体实现上分成两条分支一条判断anchor里有没有物体分类一条修正anchor的位置回归。两条分支共享同一个3×3卷积的输出但各自接了一个1×1卷积层。这听起来简单实际实现的时候有不少值得展开的细节。3.1 二分类分支前景与背景的博弈分类分支的输出维度是2k对应k个anchor各自的前景分数和背景分数但在具体实现里很多人用的是1个输出通道 Sigmoid也就是只输出前景概率背景概率用1减去它。两种做法数学上是等价的但工程上Sigmoid版本更好调——它不会像Softmax那样强制两个类别互斥而在这张图上几乎所有anchor都是背景的场景下Softmax两个输出头的梯度经常不平衡反而更难收敛。判断一个anchor是前景还是背景标准很简单它和某个ground truth框的IoU大于0.7就是正样本和所有ground truth框的IoU都小于0.3就是负样本。那IoU在0.3到0.7之间的anchor呢在训练时直接忽略掉不参与损失计算。为什么因为这部分anchor边界模糊说它是正样本吧框得不太准说它是负样本吧里面又确实有目标。硬塞进训练只会让分类器学得犹犹豫豫不如直接放弃。这个灰区忽略的思路在目标检测里非常常见很多trick本质上都是在界定哪些样本值得学、哪些不值得学。3.2 边界框回归分支4个偏移量的精细调节回归分支的输出维度是4k对应每个anchor的四个参数tx, ty, tw, th。这四个参数不是直接回归目标框的绝对坐标而是回归相对于anchor的偏移。具体公式如下tx (x - xa) / wa ty (y - ya) / ha tw log(w / wa) th log(h / ha)其中xa、ya、wa、ha是anchor的中心坐标和宽高x、y、w、h是ground truth框的坐标和宽高。也就是说RPN的回归目标是从anchor到真实框的位移和缩放。为什么要用log而不是直接用w/ha因为直接回归宽高比值的话小目标的微小变化会被放大大目标的同比例变化反而被忽略导致训练不稳定。log变换把乘除关系变成加减关系让不同尺度的目标回归难度保持一致。这里有个容易踩的坑回归分支的梯度跟anchor的尺度是耦合的。如果anchor的尺度过大它覆盖的正样本范围就大回归分支要去预测的位移范围也大训练初期很难收敛。所以正样本的筛选不能只看IoU还得对回归目标做归一化——Faster R-CNN的做法是在计算损失时用均值方差标准化也就是把tx、ty、tw、th除以对应的标准差。后来在mmdetection等框架里这个标准化系数被直接固化成了超参数默认值来自COCO数据集的统计结果。你在自己数据集上如果发现loss降不下去第一件该检查的事就是回归目标的分布而不是急着换骨干网络。3.3 NMS与Top-N从2万个框到2千个框RPN的输出在送入后续检测头之前还得经过一道筛选。特征图上每个位置都有9个anchor加起来约2万个框。这些框有大量重叠如果不处理就直接送去RoI Pooling计算量会非常浪费而且还可能让后面的分类器看到一堆几乎一样的输入。筛选的第一步是NMS非极大值抑制。具体流程是先按前景分数从高到低排序取分数最高的框把所有跟它IoU大于某个阈值的框全部删掉然后重复这个过程。在RPN里NMS的IoU阈值一般设在0.7。阈值设太高重叠框删不干净后续计算压力大设太低可能会把一些真正的目标框误删导致recall下降。经过NMS之后再从剩下的框里取分数最高的Top-N。训练阶段一般取2000个测试阶段取300个。Faster R-CNN论文里有张图展示了这个筛选过程的效果——NMS之前一个目标上可能叠着几十个框NMS之后基本每个目标只剩一个质量最高的框。需要特别注意的是RPN阶段做完NMS和Top-N筛选后这些框还是候选区域它们会经过RoI Pooling固定到统一尺寸然后送进分类分支和回归分支做最终的类别判断和精细位置修正。所以整个检测框架其实有两层分类回归结构RPN一层做粗筛检测头一层做精判。理解这个由粗到精的思想是理解整个Faster R-CNN体系的关键。4. RPN训练与正负样本分配最容易翻车的地方RPN的训练细节是复现论文时最容易出问题的地方。很多人在跑通代码后发现RPN的loss掉了但检测精度上不去或者训练过程剧烈震荡八成都是正负样本分配和数据采样策略没搞对。这一节讲清楚RPN是怎么训练的附带我对这些细节的理解。4.1 正负样本定义IoU阈值从哪来上面提到过IoU大于0.7的anchor是正样本小于0.3的是负样本中间的直接忽略。但这里有几个细节需要展开。第一个细节如果一个anchor和多个ground truth框的IoU都超过了0.7那它应该分配给哪个目标来回归答案是取IoU最大的那个。这个策略保证了anchor只学一个目标的位置不会左右逢源学出一个四不像。实现时一般是这样计算所有anchor和所有gt之间的IoU矩阵对每个anchor找出与之IoU最大的gt用它作为回归目标当出现两个anchor抢一个gt时按IoU排序分先后。第二个细节如果一个gt框非常小可能没有任何anchor跟它的IoU达到0.7那它就会被漏掉。Faster R-CNN处理这种情况的策略是对于那些跟任何anchor的IoU都达不到0.7的gt框把与之IoU最大的那个anchor强行标记为正样本。这个保底机制保证了每个gt框至少有一个anchor负责召回。在实际工程里如果你的数据集小目标特别多这个保底操作几乎决定了召回率的下限。第三个细节0.3和0.7这两个阈值不是随便拍的。它等价于要求anchor和gt的中心距离不超过某个范围且大小比不能太离谱。你可以想象一下当IoU刚好是0.7时anchor和gt的大小偏差大约在30%以内中心偏移大约在框对角线长度的20%以内。这个范围对应粗筛的容忍度太严比如0.8以上会导致正样本太少、训练数据不足太松比如0.5会导致anchor定位精度太差回归分支压力过大后面的检测头根本救不回来。4.2 采样策略为什么每张图只取256个anchor前面说了一张图会产生几万个anchor但在训练时并不全用而是从中采样256个。这里面有多个层面的考量。第一个层面是正负样本平衡。直接拿全部anchor训练的话正负比例大概是1:1000甚至更夸张分类器会完全被负样本淹没变成永远预测背景的废物。采样时保持正负样本1:1的比例分类分支学到的决策边界会更合理。如果一张图里的正样本不够128个就用负样本补足到256个。第二个层面是Batch大小的合理分配。RPN是在特征的每一层上做预测每个位置都产出一组预测。如果单张图的anchor数量太多梯度更新幅度会被大量简单负样本主导影响优化效率。256是经过实验验证的平衡数——既足够提供稳定的梯度估计又不会让单张图的训练时间被RPN分支拖死。第三个层面是随机性。如果总是挑分数最高的正样本和负样本去训练模型会觉得所有目标都很清晰、所有背景都很干净泛化能力会很差。随机采样的目的就是让模型在训练时见识各种难度的样本尤其是那些跟背景很像的前景和跟前景很像的背景。有些人会把Hard Negative Mining直接用在这里但RPN的训练一般不这么做——因为RPN本身只是粗筛太难的样本留着给后面的检测头去啃没必要在这里就卷起来。4.3 损失函数与训练轮次细节里的魔鬼RPN的多任务损失函数长这样L({pi}, {ti}) (1/N_cls) * Σ L_cls(pi, pi*) λ * (1/N_reg) * Σ pi* * L_reg(ti, ti*)前半部分是分类损失用的是一般是二分类交叉熵后半部分是回归损失用的是Smooth L1。Smooth L1在误差较小的时候是二次函数梯度平滑误差较大的时候是一次函数梯度不爆炸比单纯的L2损失稳健得多——因为回归目标里偶尔会有离群值L2会对这些离群值给超大梯度把训练带偏。两个细节值得注意。第一回归损失前面乘了一个pi*意思是只有正样本才计算回归损失负样本的回归损失置零。负样本本来就没有对应的gt框硬让网络去回归一个没有意义的目标只会让特征变乱。第二λ的作用是平衡分类和回归两个任务的量级。原文里λ10但因为N_cls和N_reg做了归一化都除到底数256和正样本数量λ10几乎是在平衡分类贡献和回归贡献的权重。实际操作中这个值大多不用调但如果你自己实现了RPN且发现训练时回归loss下降得异常慢可以试着调大λ。RPN和Fast R-CNN的联合训练方式原文用的是四步交替法第一步单独训练RPN第二步用训练好的RPN生成候选框拿去训练Fast R-CNN检测头第三步固定检测头的参数只微调RPN的共享卷积层第四步固定共享卷积层微调检测头特有层。这套流程在当年GPU算力有限的情况下非常实用。现在的主流框架基本都采用端到端联合训练一步到位——因为你有了足够大的batch size和足够多的迭代轮次交替训练的稳定性优势不再明显。如果你在mmdetection里跑Faster R-CNN落地训练就是一个完整passRPN和检测头的loss加权求和之后一起回传。5. 工程实战RPN的调参与边界经验跑通RPN不是终点真正让人头大的是把它调好。我在实际项目里踩过不少坑也总结了一些不一定写在论文里、但真实有用的经验。这一节拿出来分享针对几个高频问题。5.1 anchor的尺寸和比例怎么定不是玄学anchor的默认配置是128、256、512三种尺度和1:1、1:2、2:1三种比例这些值是基于PASCAL VOC数据集的统计结果设定出来的。如果你换到别的数据集直接沿用这套配置会出问题。比如检测卫星遥感图像里的飞机场那长宽比可能到1:5以上检测工业质检里的微小划痕目标尺寸可能只有8×8像素。这时候还拿512尺度的anchor去套正样本数量会少到没法训练。我的做法是先拿标注数据做一次统计分析把GT框的宽度、高度、长宽比分布画出来然后把anchor的尺度和比例设定在分布的“主峰”附近。具体来说anchor的尺度应该覆盖GT宽高分布的中位数上下几个数量级最好让大部分GT框跟某个anchor的IoU都高于0.5。如果你的GT框宽度在10到50像素之间那anchor尺度可以设成8、16、32、64四档而不是原来那三个大尺寸。还有个trick是直接在训练中动态学习anchor的尺度和比例——这就是后面GA-RPN和自适应anchor的思路但实现复杂度更高训练也更不稳定前期还是建议手工统计稳一稳。5.2 正负样本严重失衡为什么RPN能扛住但别太自信RPN的整体结构对正负样本不均衡有一定的天然鲁棒性因为它在每个滑窗位置独立做预测背景anchor再多也只是增加了负样本的采样池随机采样256个能维持一个相对均衡的训练分布。但这不意味着你可以对失衡问题完全放任不管。极端情况下比如一张高分辨率的航拍图里只有一栋楼正样本可能就十几个负样本几十万采样比例再均衡模型能学到的目标特征也极其有限。应对方法有几个。一是减小NMS的IoU阈值别让正样本之间互相竞争得太狠二是增加anchor的尺度档位让更多anchor能跟目标框产生有效IoU三是在采样时给正样本更高的权重强行让正样本的比例超过0.5四是做数据增强时多用crop和scale变换给目标制造更多“不同尺度下的出现机会”。我自己在标注数据不足一年且类别极度不平衡的任务上还试过用Focal Loss替换掉二分类交叉熵。Focal Loss的核心是压低“已经很好分类”的负样本的loss贡献让模型把注意力集中在难以分类的样本上。效果确实有——至少RPN的recall上去了几个点。5.3 NMS阈值和输出数量的影响测试老翻车NMS的IoU阈值在RPN里默认是0.7但这个值不是固定的。不同数据集的最优值差异挺大。比如行人检测数据集目标高度重叠阈值就得放宽调大到0.80.9不然挨得近的两个人会被NMS误杀成一个。又比如像物体比较稀疏的场景阈值保持在0.7就挺好。怎么找最优值我的方法是单独把RPN拎出来在验证集上测一下recall曲线——统计在IoU0.5、0.75这两个标准下前100/300/500个候选框能覆盖多少GT框。如果recall很高但后面检测头精度上不去说明候选框质量虽然够但数量太多重复框太多拖累了最终结果如果NMS之后recall偏低说明阈值太严了需要调整。输出数量方面训练时用2000个候选框是因为RPN需要尽量多的高质量正样本来喂后面的检测头测试时用300个是平衡了精度和速度。在工程落地时如果追求极致速度可以再压到100个你会发现mAP掉的幅度通常在1个点以内但推理速度能提升10%以上。如果追求极致精度可以保留2000个但检测头的RoI Pooling会变成计算瓶颈需要配合TensorRT之类的东西做加速。5.4 从Faster R-CNN到现代检测器RPN的演进方向RPN的思想影响深远衍生出了一大批改进方向。最直接的是FPN特征金字塔网络骨干网络不同层级的特征图分辨率不同语义信息也不同FPN把多层特征合并起来RPN就同时在每一层特征图上做预测。这样原本只在一个尺度上做检测的RPN扩展成了多尺度检测对小目标的召回率提升显著。目前主流检测框架里用的RPN基本都是FPN版的原始的在最后一层特征图上做RPN已经成为过去式。另一个值得关注的是Cascade RPN它在RPN的多次迭代上做了改进第一次RPN产生粗糙的候选框后不直接送去检测头而是再做一次回归精修。它的核心发现是RPN输出的候选框质量其实不高尤其是IoU分布在0.5左右的框占了大多数这些框喂给后面的检测头会产生大量低质量训练样本。通过多个stage的级联回归候选框的IoU分布被逐步推向更高区间后面的检测头就能学到更有区分度的特征。这套思路后来也被Cascade R-CNN发扬光大。还有GA-RPNGuided Anchoring它让网络先预测目标可能出现在哪、应该用什么尺度的anchor再根据预测结果动态生成anchor而不是全图均匀铺anchor。这种方法在密集小目标场景下效果很惊艳但训练也很不稳定调参成本比较高。如果你在做一个标准的通用检测任务我还是建议先把手动anchor的RPN调好再考虑这些花哨的变体——baseline稳了才有资格谈优化。5.5 几个RPN调试的实战检查清单最后分享一份我自己用的排查清单当RPN表现不对时按顺序检查这几项大部分问题都能定位出来检查anchor和GT的IoU分布如果大部分GT框连最高的anchor IoU都不到0.5说明anchor配置肯定有问题重新统计GT尺寸并调整anchor。检查正样本数量训练早期如果每张图的正样本数量长期停留在个位数说明GT框太小或anchor尺度太大召回率会非常差。检查RPN的召回率单独测RPN阶段Top-N候选框覆盖了多少GT框。如果Top-300的recall低于0.9检测头再强也白搭问题铁定出在RPN。检查NMS前后的候选框分布NMS之后如果还有大量IoU接近1的重复框NMS阈值需要调低如果NMS之后候选框整体偏移严重回归分支的训练可能有问题看看学习率和Smooth L1的sqrt_delta参数是否合理。检查RPN和检测头的loss数值比例如果RPN的loss占了整体loss的80%以上说明RPN任务太简单或权重失衡导致梯度主导了共享特征的更新方向检测头学不到有效特征需要调低λ。就我自己跑了这么多年检测模型的经验RPN是最不值得你去“魔改”的模块但也是最值得你“吃透”的模块。anchor机制、正负样本分配、NMS处理这套逻辑几乎是所有现代检测器共享的底层语言。你把RPN的每个细节都弄明白了后面无论去读YOLO、SSD、DETR哪一派的代码都能快速理解它们的设计动机和差异点。说到底目标检测这碗饭RPN就是那张必须坐稳的饭桌。