首页 / 资讯中心 / 文章详情

RPN区域建议网络原理与实战:从Anchor设计到正负样本划分

RPN区域建议网络原理与实战:从Anchor设计到正负样本划分 ★ FEATURED ARTICLE
1. RPN到底在做什么从一张图到一堆候选框Region Proposal Network简称RPN这玩意儿最早是跟着Faster R-CNN一起出来的。很多人第一次看论文的时候会被“区域建议”这四个字唬住觉得是个多高深的模块。其实你把它的职责拆开看就一句话给后续的检测头喂一堆“可能有东西”的框并且告诉它这些框里大概是什么前景还是背景。我刚开始接触RPN的时候脑子里一直有个疑问为什么不能直接让分类网络在全图上滑一遍后来自己动手复现了一遍才明白全图滑窗的代价太大了。假设输入是一张1000×600的图如果用3×3的滑窗、步长1那就是将近60万个位置每个位置都要过一遍分类器算力根本扛不住。RPN的思路是先用一个轻量的卷积网络在特征图上滑每个位置只出k个预设框anchor然后对这些框做一次二分类前景/背景和一次回归微调位置把明显不靠谱的框扔掉剩下的才交给后面的检测头。这个设计的好处在于把“找目标”和“认目标”这两件事解耦了。RPN只负责找不负责认。它不需要知道这是猫还是狗只需要知道“这里有个东西大概在这个位置”。这种分工让整个检测流程变得非常高效也是两阶段检测器能跑出实时速度的关键。适合谁来读这篇内容如果你正在复现Faster R-CNN或者想自己搭一个自定义的检测框架RPN是绕不过去的一环。哪怕你用的是YOLO这类单阶段检测器理解RPN的anchor机制和正负样本划分逻辑对你调参也有直接帮助。下面我会从设计思路、核心细节、实操流程、常见问题四个维度把RPN拆干净。2. 整体设计思路与方案选型2.1 为什么是“卷积滑窗Anchor”而不是“选择性搜索”在RPN出现之前主流的两阶段检测器比如Fast R-CNN用的是选择性搜索来生成候选框。选择性搜索是个纯CPU算法一张图要跑好几秒根本没法端到端训练。RPN的核心突破就是把候选框生成也变成一个可学习的神经网络模块直接挂在特征图上用GPU并行算。具体做法是在骨干网络输出的特征图上用一个3×3的卷积核滑一遍每个位置输出一个256维或者512维取决于骨干通道数的特征向量。然后这个向量分两路一路接1×1卷积做二分类输出2k个分数k是anchor数量每个anchor两个分数前景、背景另一路接1×1卷积做回归输出4k个坐标偏移量。这里有个设计细节值得说为什么用3×3而不是1×1或者5×53×3的感受野刚好能覆盖一个中等大小的目标区域同时计算量可控。1×1的话感受野太小每个位置只能看到自己缺乏上下文信息5×5计算量上去了但收益不明显。这个3×3卷积相当于在特征图上做了一次局部特征聚合让每个位置的表示更有判别力。2.2 Anchor的设计哲学用先验框覆盖所有可能Anchor本质上是一组预设的矩形框每个位置都有一组。假设你设了9个anchor3种尺度×3种长宽比那特征图上每个位置都会生成9个框。这些框的中心点就是当前特征图位置映射回原图的坐标。为什么要有多个尺度和长宽比因为目标的大小和形状是多样的。一个128×128的anchor适合检测中等大小的物体256×256的适合大物体64×64的适合小物体。长宽比11、12、21分别对应正方形、竖长条、横长条。这9个组合基本能覆盖常见的目标形态。注意anchor的尺度和长宽比不是随便设的要根据你的数据集统计来。如果你检测的是行人长宽比集中在12到13之间那你就没必要设21的横长条anchor纯属浪费算力。2.3 正负样本划分谁该负责哪个框RPN训练的核心问题之一就是哪些anchor算正样本哪些算负样本论文里的策略是正样本和某个真实框的IoU大于0.7的anchor或者和某个真实框IoU最大的anchor即使没到0.7负样本和所有真实框的IoU都小于0.3的anchor剩下的0.3到0.7之间直接扔掉不参与训练这个策略的逻辑是IoU大于0.7的框已经和真实框很接近了让它们负责回归是合理的IoU最大的那个框即使质量不高也得给它一个正样本身份否则某些小目标可能一个正样本都没有。0.3到0.7之间的框处于“模棱两可”的状态强行训练反而会引入噪声。我实测下来正负样本比例控制在11到13之间比较稳。如果负样本太多模型会倾向于把所有框都预测成背景召回率上不去。如果正样本太多误检会明显增加。Faster R-CNN原论文里是随机采样256个anchor正负各一半这个策略在大多数场景下都够用。3. 核心细节解析与实操要点3.1 特征图到原图的坐标映射RPN输出的回归量是相对于anchor的偏移量格式通常是(dx, dy, dw, dh)。要把这些偏移量还原成原图上的框需要做两步第一步把特征图上的位置映射回原图。假设骨干网络的总步长是16比如VGG16的conv4_3之后那特征图上的一个点(x_f, y_f)对应原图的中心就是(x_f * 16, y_f * 16)。这个16就是感受野的步长也叫stride。第二步用偏移量调整anchor。公式是这样的x_center x_anchor dx * w_anchor y_center y_anchor dy * h_anchor w w_anchor * exp(dw) h h_anchor * exp(dh)这里dx和dy是中心点的偏移dw和dh是宽高的缩放。用exp是为了保证宽高永远是正数而且缩放是对称的——dw0时宽不变dw0时宽变大dw0时宽变小。实操心得很多人在复现的时候会忘记做clip操作导致预测出来的框跑到图像外面去了。记得在还原坐标之后把框的边界裁剪到[0, W]和[0, H]范围内否则后续的ROI Pooling会报错。3.2 损失函数分类损失回归损失RPN的损失函数是两部分加权和L (1/N_cls) * Σ L_cls λ * (1/N_reg) * Σ L_reg分类损失用的是二分类交叉熵只对正负样本计算被扔掉的样本不参与。回归损失用的是smooth L1只对正样本计算。λ通常设成10用来平衡两个损失的量级。为什么回归损失只对正样本算因为负样本根本没有对应的真实框你让它回归到哪儿去强行算的话只会让模型混乱。这个细节在复现的时候特别容易搞错我见过不少人把负样本也塞进回归损失里结果训练loss一直震荡。smooth L1的定义是这样的|x| 1: 0.5 * x^2 |x| 1: |x| - 0.5它在零点附近是二次函数远离零点时是线性的。这个设计比纯L2鲁棒因为L2对离群点太敏感一个预测偏得离谱的框会把整个loss拉爆。smooth L1相当于给梯度加了个上限训练更稳定。3.3 Anchor的生成与筛选在实际代码里anchor的生成通常是在特征图上按位置遍历每个位置生成k个框。假设特征图大小是H×W那总共就是H×W×k个anchor。对于一张1000×600的图stride16特征图大概是63×389个anchor的话就是21546个框。这2万多个框不可能全部送进后续网络所以RPN会先做一次筛选按分类分数排序取前N个比如12000个用NMS非极大值抑制去掉重叠度高的框IoU阈值通常设0.7再取前M个比如2000个作为最终的候选框训练的时候取2000个推理的时候可以取300个或者1000个看你对速度的要求。NMS的作用是去掉冗余框因为同一个目标附近可能会有很多个高分框保留一个就够了。注意NMS的阈值不要设太低否则相邻的两个目标会被误删。比如两个人站得很近IoU可能到0.5如果你把NMS阈值设成0.3那其中一个就被干掉了。0.7是个比较安全的默认值。4. 实操过程与核心环节实现4.1 环境准备与骨干网络选择我这次复现用的是PyTorch骨干网络选了ResNet-50。为什么不用VGG16因为ResNet有残差连接训练更稳定而且同样的层数下参数量更少。如果你追求速度可以用MobileNetV2或者ShuffleNet但精度会掉一些。环境配置如下pip install torch torchvision pip install numpy opencv-python pip install matplotlib tqdm数据集我用的是一个自定义的小规模检测数据集大概2000张图5个类别。如果你手头没有数据可以用COCO或者VOC的先跑通流程。4.2 特征图提取与RPN头部搭建骨干网络输出的是C×H×W的特征图我这里是1024×63×38ResNet-50的conv4_x输出。RPN头部的结构是这样的class RPNHead(nn.Module): def __init__(self, in_channels1024, mid_channels512, num_anchors9): super().__init__() self.conv nn.Conv2d(in_channels, mid_channels, 3, padding1) self.cls_logits nn.Conv2d(mid_channels, num_anchors * 2, 1) self.bbox_pred nn.Conv2d(mid_channels, num_anchors * 4, 1) def forward(self, x): t F.relu(self.conv(x)) cls_logits self.cls_logits(t) bbox_pred self.bbox_pred(t) return cls_logits, bbox_pred这里3×3卷积的输出通道我设成了512和原论文一致。分类分支输出18个通道9个anchor×2回归分支输出36个通道9个anchor×4。注意输出是逐anchor排列的后面reshape的时候要小心维度顺序。4.3 Anchor生成的具体实现Anchor的生成我写了一个函数输入是特征图大小、stride、尺度和长宽比列表def generate_anchors(feature_size, stride, scales, ratios): anchors [] for y in range(feature_size[0]): for x in range(feature_size[1]): cx x * stride stride / 2 cy y * stride stride / 2 for scale in scales: for ratio in ratios: w scale * np.sqrt(1.0 / ratio) h scale * np.sqrt(ratio) anchors.append([cx - w/2, cy - h/2, cx w/2, cy h/2]) return np.array(anchors)尺度和长宽比我设的是[64, 128, 256]和[0.5, 1.0, 2.0]一共9个组合。这个配置在大多数数据集上都能work。如果你的目标特别小比如小于32像素可以把64换成32再加一个16的尺度。4.4 正负样本划分与损失计算正负样本划分我写了一个函数输入是anchor和真实框输出是每个anchor的标签和回归目标def assign_targets(anchors, gt_boxes, pos_thresh0.7, neg_thresh0.3): ious compute_iou(anchors, gt_boxes) max_iou ious.max(axis1) labels np.zeros(len(anchors)) labels[max_iou neg_thresh] 0 labels[max_iou pos_thresh] 1 # 每个真实框对应的最大IoU anchor也设为正样本 max_idx ious.argmax(axis0) labels[max_idx] 1 # 计算回归目标 reg_targets compute_regression(anchors, gt_boxes, max_idx) return labels, reg_targets这里有个细节max_idx ious.argmax(axis0)得到的是每个真实框对应的最佳anchor索引。这个操作保证了每个真实框至少有一个正样本anchor避免小目标被漏掉。损失计算的时候我采样了256个anchor正负各128个。如果正样本不够128个就用负样本补齐。分类损失用BCEWithLogitsLoss回归损失用smooth_l1_loss权重λ设成10。4.5 训练过程与参数设置训练参数如下参数值说明优化器SGDmomentum0.9初始学习率0.001每5个epoch衰减0.1batch size2显存限制epoch20小数据集够用权重衰减0.0005防止过拟合正负样本比例11各128个训练到第10个epoch的时候RPN的分类loss大概降到0.15左右回归loss在0.08附近。这个水平已经能生成质量不错的候选框了。我可视化了一下对于中等大小的目标RPN生成的框和真实框的IoU基本都在0.6以上。实操心得如果你的loss不下降先检查anchor的尺度和你的数据集是否匹配。我一开始用默认的[128, 256, 512]结果我的数据集里全是小目标RPN一个正样本都匹配不上loss直接卡在0.69不动。后来把尺度改成[32, 64, 128]才正常。5. 常见问题与排查技巧实录5.1 Loss不下降或者震荡这是最常见的问题原因通常有三个第一anchor尺度和数据集不匹配。解决办法是统计你数据集中所有真实框的宽高分布用k-means聚类出几个代表性的尺度和长宽比。这个操作在YOLO系列里叫“anchor聚类”对RPN同样适用。第二正负样本划分阈值设得不对。如果你的数据集里目标特别密集0.7的正样本阈值可能太高了导致正样本太少。可以适当降到0.6或者0.5。反过来如果目标很稀疏可以升到0.8。第三学习率太大。RPN的回归分支对学习率很敏感我试过用0.01的学习率loss直接飞了。0.001是个比较安全的起点。5.2 生成的候选框大量重叠这个问题通常是NMS没做好。检查你的NMS阈值是不是设得太高比如0.9或者NMS之前有没有按分数排序。另外如果你的分类分数普遍偏低比如都在0.5左右说明模型没学好需要回头检查训练过程。还有一个容易被忽略的点NMS是在每个类别内独立做的还是所有类别一起做的RPN阶段只有前景和背景两类所以直接对前景分数做NMS就行。但如果你在RPN之后还接了别的分类器那NMS要在那个阶段按类别分别做。5.3 小目标检测效果差小目标在RPN里天然吃亏因为经过多次下采样之后小目标在特征图上可能只剩几个像素anchor根本覆盖不到。解决办法有几个用FPN特征金字塔把浅层特征也利用起来浅层特征图分辨率高小目标信息保留得更好降低stride比如把骨干网络的总步长从16降到8增加小尺度的anchor比如加一个32甚至16的尺度我实测下来FPN是最有效的方案。加了FPN之后小目标的召回率能从40%左右提到70%以上。代价是计算量增加大概30%但收益远大于成本。5.4 训练和推理结果不一致这个问题我踩过坑。训练的时候RPN输出的框看起来很正常但推理的时候框的位置全偏了。后来发现是训练时用了随机采样推理时用了全部anchor两者的分布不一致。解决办法是在推理的时候也做一次筛选按分数取前N个再做NMS。另外训练时的回归目标计算和推理时的坐标还原要用同一套公式不要一个用log一个用exp。5.5 常见问题速查表问题现象可能原因排查方法解决方案Loss卡在0.69正样本数为0打印正样本数量调整anchor尺度或降低正样本阈值Loss震荡学习率过大观察loss曲线降低学习率到0.0001候选框大量重叠NMS阈值过高可视化NMS前后框数量降低NMS阈值到0.7小目标漏检特征图分辨率不足检查小目标在特征图上的尺寸加FPN或降低stride训练推理不一致采样策略不同对比训练和推理的框分布统一采样和筛选逻辑回归框跑出图像缺少clip操作检查框坐标范围还原坐标后做clip6. 一些个人体会和后续扩展方向RPN这个模块我前前后后复现过三四次每次都有新的理解。第一次是照着论文硬撸结果各种维度对不上调了一周才跑通。第二次是读别人的开源代码发现很多实现细节论文里根本没提比如anchor的生成顺序、正负样本的采样策略、NMS的执行时机。第三次是自己从头写了一遍才真正把每个环节的逻辑理顺。我个人觉得RPN最核心的设计思想不是某个具体的网络结构而是“用先验框可学习偏移”这套范式。这个范式后来被几乎所有的检测器继承了包括单阶段的SSD、RetinaNet甚至Anchor-Free的FCOS和CenterNet本质上都是在解决“如何从特征图上生成高质量候选框”这个问题。如果你已经把RPN跑通了后续可以往这几个方向扩展一是换不同的骨干网络比如用EfficientNet或者ConvNeXt看看精度和速度的权衡。二是加FPN或者PAN提升多尺度检测能力。三是把RPN的输出接到不同的检测头上比如Cascade R-CNN的多级检测头看看能不能进一步提升精度。四是尝试Anchor-Free的方案对比一下和Anchor-Based的差异理解各自的优劣。最后分享一个小技巧调试RPN的时候一定要把中间结果可视化出来。把anchor画在原图上把正样本框标红把负样本框标蓝把最终生成的候选框画出来。很多时候你看loss曲线看不出问题但一可视化就一目了然了。我当初就是靠可视化才发现anchor尺度设错了白白浪费了两天时间。
阅读完成 · 觉得有帮助?
咨询建站