从RPN到RoI Align:图解Faster R-CNN中那些容易被忽略的细节(附PyTorch代码片段)
从RPN到RoI Align:图解Faster R-CNN中那些容易被忽略的细节(附PyTorch代码片段)
第一次接触Faster R-CNN时,我被它的两阶段设计深深吸引——先用RPN生成候选框,再对候选框进行分类和微调。但当我真正动手实现时,才发现那些看似简单的概念背后藏着无数魔鬼细节。比如,为什么RPN的锚框要设计成特定比例?RoI Align到底比RoI Pooling强在哪里?这些问题在论文和教程中往往一笔带过,却直接影响模型的实际表现。本文将用图解+代码的方式,带你深入这些关键但常被忽视的技术细节。
1. RPN中的锚框生成:不只是滑动窗口那么简单
大多数教程会告诉你RPN在特征图上滑动窗口生成锚框,但很少解释这些锚框是如何在特征图上分布的。实际上,锚框的生成涉及三个关键参数:基础尺寸(base size)、比例(ratios)和步长(strides)。
1.1 锚框的几何特性
假设我们使用ResNet作为backbone,在FPN的P3层(stride=8)上生成锚框。典型的配置如下:
anchor_generator = dict(
type='AnchorGenerator',
scales=[8], # 基础尺寸
ratios=[0.5, 1.0, 2.0], # 宽高比
strides=[8] # 对应特征图的步长
)
这会产生三种不同形状的锚框:
- 宽高比0.5:宽是高的两倍
- 宽高比1.0:正方形
- 宽高比2.0:高是宽的两倍
关键点:这些锚框的绝对尺寸是在原图空间计算的。例如,当基础尺寸为8且步长为8时,锚框在原图上的基础面积为8×8=64像素。
1.2 特征图坐标到原图坐标的映射
锚框的位置由特征图坐标决定。假设特征图上某点的坐标是(x', y'),对应原图坐标为:
x = x' * stride + stride // 2
y = y' * stride + stride // 2
这个+stride//2的偏移量常被忽略,但它确保了锚框中心对齐特征图的感受野中心。下图展示了这种对应关系:
特征图位置 (2,3) → 原图位置 (20,28) [当stride=8时]
+---------------------+
| 锚框 |
| 中心(20,28) |
+---------------------+
2. 正负样本分配:MaxIoUAssigner的运作细节
RPN需要为每个锚框分配标签(正样本、负样本或忽略)。这个过程由MaxIoUAssigner完成,但它的实现细节往往被简化描述。
2.1 IoU阈值与样本分配规则
典型的配置参数如下:
assigner=dict(
type='MaxIoUAssigner',
pos_iou_thr=0.7, # 正样本阈值
neg_iou_thr=0.3, # 负样本阈值
min_pos_iou=0.3, # 最低正样本要求
match_low_quality=True # 是否允许低质量匹配
)
分配过程实际上分为两个阶段:
- 初步匹配:每个真实框(GT)优先匹配与其IoU最高的锚框
- 阈值过滤:然后根据IoU值进行最终分配
注意:当match_low_quality=True时,即使IoU低于pos_iou_thr但属于最高IoU匹配的锚框仍会被标记为正样本
2.2 代码实现解析
以下是MaxIoUAssigner核心逻辑的简化实现:
def assign(self, anchors, gt_bboxes):
overlaps = bbox_overlaps(anchors, gt_bboxes) # 计算IoU矩阵
max_overlaps, argmax_overlaps = overlaps.max(dim=1)
# 第一阶段:确保每个GT至少有一个匹配
gt_max_overlaps, _ = overlaps.max(dim=0)
for i in range(len(gt_bboxes)):
if gt_max_overlaps[i] > 0:
j = (overlaps[:, i] == gt_max_overlaps[i]).nonzero()[0]
assigned_labels[j] = 1 # 强制为正样本
# 第二阶段:根据阈值分配
pos_inds = max_overlaps >= self.pos_iou_thr
neg_inds = max_overlaps < self.neg_iou_thr
assigned_labels[pos_inds] = 1
assigned_labels[neg_inds] = 0
assigned_labels[(max_overlaps >= self.neg_iou_thr) &
(max_overlaps < self.pos_iou_thr)] = -1 # 忽略
return assigned_labels
3. RoI Pooling vs RoI Align:量化误差从何而来
RoI Pooling被广泛认为是Faster R-CNN的瓶颈之一,而RoI Align的改进常被简单归结为"避免了量化误差"。但量化误差具体是如何产生的?RoI Align又如何解决这个问题?
3.1 RoI Pooling的两次量化问题
假设我们有一个665×665的RoI要pooling到7×7的输出:
- 第一次量化:将665/7≈94.98量化为94
- 第二次量化:在划分的每个bin内做max pooling时再次取整
这种量化会导致RoI与提取的特征错位,尤其对小目标影响显著。
3.2 RoI Align的双线性插值方案
RoI Align的核心改进是:
- 避免任何坐标量化,保留浮点数计算
- 在每个bin内采样固定点(通常是4个)
- 使用双线性插值计算采样点的值
PyTorch实现关键代码:
# RoI Align前向传播示例
def forward(ctx, features, rois, output_size):
h = (rois[:, 4] - rois[:, 2]) / output_size[0] # 保持浮点精度
w = (rois[:, 3] - rois[:, 1]) / output_size[1]
for i in range(output_size[0]):
for j in range(output_size[1]):
y = rois[:, 2] + i * h # 不取整
x = rois[:, 1] + j * w
# 对每个采样点进行双线性插值
output_val = bilinear_interpolate(features, y, x)
output[:, :, i, j] = output_val.mean(dim=0) # 平均多个采样点
下表对比了两种方法的差异:
| 特性 | RoI Pooling | RoI Align |
|---|---|---|
| 坐标计算 | 两次量化取整 | 保留浮点精度 |
| 采样方式 | 每个bin取最大值 | 多个点双线性插值 |
| 对小目标效果 | 较差 | 较好 |
| 计算复杂度 | 较低 | 较高 |
4. RPN与Fast R-CNN的联合训练策略
Faster R-CNN的一个精妙设计是RPN和Fast R-CNN共享特征提取网络,但两者的训练目标不同,如何协调?
4.1 四步交替训练法
原始论文采用的四步训练策略:
- 单独训练RPN
- 用RPN proposals训练Fast R-CNN
- 用Fast R-CNN的权重初始化RPN(固定共享卷积层)
- 微调RPN特有的层
现代实现通常采用端到端联合训练,关键在于梯度停止(stop_gradient)的应用:
# 伪代码展示联合训练流程
def forward(self, img, gt_bboxes):
# 共享特征提取
features = self.backbone(img)
# RPN分支
rpn_loss, proposals = self.rpn_head(features, gt_bboxes)
# 注意:proposals的生成过程不需要梯度
proposals = proposals.detach()
# Fast R-CNN分支
roi_loss = self.roi_head(features, proposals, gt_bboxes)
return rpn_loss + roi_loss
4.2 正样本比例控制
两个阶段的正样本比例需要仔细平衡:
-
RPN阶段:通常保持正负样本1:1(通过RandomSampler)
sampler=dict( type='RandomSampler', num=256, # 总样本数 pos_fraction=0.5, # 正样本占比 neg_pos_ub=-1, add_gt_as_proposals=False ) -
Fast R-CNN阶段:正样本比例通常更低(如1:3)
sampler=dict( type='RandomSampler', num=512, pos_fraction=0.25, # 仅25%正样本 neg_pos_ub=-1, add_gt_as_proposals=True )
这种差异设计是因为RPN需要大量负样本抑制背景提议,而Fast R-CNN阶段已经过滤了大部分低质量提议。
更多推荐


所有评论(0)