Faster R-CNN 全精读:实时目标检测的里程碑之作
论文信息
- 标题:Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
- 会议:NIPS 2015
- 单位:微软研究院、Facebook AI Research、中国科学技术大学
- 代码:github.com/shaoqingren/faster_rcnn (MATLAB)、github.com/rbgirshick/py-faster-rcnn (Python)
- 论文:https://arxiv.org/pdf/1506.01497.pdf
一、引言:目标检测的"等餐难题"终于被解决了
如果你在2015年之前做过目标检测,一定有过这样的痛苦经历:
- 跑一张图要2秒,其中1.5秒都花在生成区域提议上
- 检测网络只需要300ms,却要等提议生成完才能工作
- 就像你去餐厅吃饭,等餐要1小时,吃饭只要10分钟
这就是当时两阶段目标检测的最大痛点:区域提议成为了计算瓶颈。
从R-CNN到Fast R-CNN,检测网络的速度已经提升了几十倍,但区域提议还是依赖传统的计算机视觉方法:
- Selective Search:2秒/张图(CPU)
- EdgeBoxes:0.2秒/张图(CPU)
这些方法不仅慢,还不能利用GPU加速,更不能和检测网络共享计算。
而Faster R-CNN的出现彻底改变了这一切。它提出了区域提议网络(RPN),让区域提议和检测网络共享卷积特征,生成提议只需要10毫秒!这使得两阶段检测第一次达到了接近实时的速度(5fps),同时精度还更高。
毫不夸张地说,Faster R-CNN是目标检测领域的一个里程碑,它奠定了之后几乎所有两阶段检测模型的基础,也是2015年ImageNet和COCO比赛多个赛道冠军的核心技术。
二、Faster R-CNN核心架构:两个网络,一个灵魂
Faster R-CNN由两个模块组成:
- 区域提议网络(RPN):快速生成高质量的候选框
- Fast R-CNN检测器:对候选框进行分类和精修
最关键的是,这两个模块共享同一套卷积特征。这意味着生成提议几乎没有额外的计算成本。
用一个形象的比喻:
- RPN就像警察局的"巡警",快速在全城巡逻,找出可疑的人
- Fast R-CNN就像"法官",对这些可疑的人进行详细审问,最终定罪
- 两者共享"城市地图"(卷积特征),不需要各自重新绘制

图1:Faster R-CNN整体架构(来源:论文Figure 2)
从图1可以看出,RPN和Fast R-CNN共享前面的卷积层。RPN在卷积特征图上滑动,生成候选框,然后这些候选框被送入RoI池化层,提取固定长度的特征,最后进行分类和回归。
三、RPN:最天才的设计——锚点机制
RPN的核心创新是锚点(Anchor)机制。它解决了一个困扰目标检测多年的问题:如何高效地检测不同尺度和长宽比的物体。
3.1 传统多尺度方法的痛点
在Faster R-CNN之前,处理多尺度物体主要有两种方法:
图2:不同多尺度处理方案对比(来源:论文Figure 1)
- 图像金字塔(图2a):把图像缩放到不同大小,分别提取特征。效果好,但速度极慢。
- 滤波器金字塔(图2b):用不同大小的卷积核检测不同大小的物体。参数多,训练困难。
这两种方法都有一个共同的问题:计算量随着尺度数量线性增长。
3.2 锚点金字塔:一次计算,多尺度检测
Faster R-CNN提出了第三种方法:锚点金字塔(图2c)。
通俗易懂的解释:
我们在卷积特征图的每个位置,预先画好9个不同大小和形状的方框,这些方框就叫"锚点"。
- 3个尺度:128×128、256×256、512×512
- 3个长宽比:1:1、1:2、2:1
然后网络不需要从零开始预测物体的位置和大小,只需要预测:
- 每个锚点里有没有物体(分类)
- 这个锚点需要怎么调整才能刚好框住物体(回归)
这样一来,我们只需要对原图做一次卷积,就能检测所有尺度和长宽比的物体,计算量几乎没有增加!
3.3 锚点的两大优势
平移不变性
如果一个物体在图像中平移了,对应的锚点也会平移,网络仍然能检测到它。这是锚点机制的固有属性,不需要额外的训练。
而之前的MultiBox方法用k-means生成锚点,不具备平移不变性。如果物体平移了,可能就检测不到了。
参数极少
RPN的输出层只有(4+2)×9=54个卷积核,参数数量只有2.8×10⁴个。而MultiBox的输出层有6.1×10⁶个参数,是RPN的200多倍!
这意味着RPN不容易过拟合,在小数据集上也能表现很好。
四、损失函数:让RPN学会找物体
为了训练RPN,我们需要给每个锚点分配一个标签,然后定义一个多任务损失函数。
4.1 锚点标签分配规则
- 正样本:满足以下两个条件之一的锚点
- 与某个真实框的IoU最高的锚点
- 与任意真实框的IoU大于0.7的锚点
- 负样本:与所有真实框的IoU都小于0.3的锚点
- 忽略样本:既不是正也不是负的锚点,不参与损失计算
4.2 多任务损失函数
RPN的损失函数同时优化分类和回归两个任务:
L({pi},{ti})=1Ncls∑iLcls(pi,pi∗)+λ1Nreg∑ipi∗Lreg(ti,ti∗) L\left( \left\{ p_{i}\right\} ,\left\{ t_{i}\right\} \right) =\frac {1}{N_{cls }}\sum _{i}L_{cls }\left( p_{i},p_{i}^{*}\right) +\lambda \frac {1}{N_{reg }}\sum _{i}p_{i}^{*}L_{reg }\left( t_{i},t_{i}^{*}\right) L({pi},{ti})=Ncls1i∑Lcls(pi,pi∗)+λNreg1i∑pi∗Lreg(ti,ti∗)
其中:
- iii:一个mini-batch中锚点的索引
- pip_ipi:锚点iii包含物体的预测概率
- pi∗p_i^*pi∗:锚点iii的真实标签,正样本为1,负样本为0
- tit_iti:预测的边界框坐标偏移量,是一个4维向量
- ti∗t_i^*ti∗:真实的边界框坐标偏移量
- LclsL_{cls}Lcls:分类损失,二分类的对数损失
- LregL_{reg}Lreg:回归损失,使用smooth L₁损失
- NclsN_{cls}Ncls:分类损失的归一化因子,等于mini-batch大小(256)
- NregN_{reg}Nreg:回归损失的归一化因子,等于锚点位置的数量(约2400)
- λ\lambdaλ:平衡两个损失的权重,默认取10
通俗易懂的解释:
这个损失函数就像给RPN的考试打分,分类和回归各占50分。只有正样本需要考回归,负样本只需要考分类。λ\lambdaλ就是用来调整两门课的权重,让它们对总损失的贡献差不多。
4.3 边界框回归参数化
边界框回归不是直接预测框的绝对坐标,而是预测相对于锚点的偏移量:
tx=(x−xa)/wa,ty=(y−ya)/ha,tw=log(w/wa),th=log(h/ha),tx∗=(x∗−xa)/wa,ty∗=(y∗−ya)/ha,tw∗=log(w∗/wa),th∗=log(h∗/ha), \begin{array} {rl}{t_{x}}&{=(x-x_{a})/w_{a}, t_{y}=(y-y_{a})/h_{a},}\\ {t_{w}}&{=log (w/w_{a}), t_{h}=log (h/h_{a}),}\\ {t_{x}^{*}}&{=(x^{*}-x_{a})/w_{a}, t_{y}^{*}=(y^{*}-y_{a})/h_{a},}\\ {t_{w}^{*}}&{=log \left(w^{*}/w_{a}\right) , t_{h}^{*}=log \left(h^{*}/h_{a}\right) ,}\end{array} txtwtx∗tw∗=(x−xa)/wa,ty=(y−ya)/ha,=log(w/wa),th=log(h/ha),=(x∗−xa)/wa,ty∗=(y∗−ya)/ha,=log(w∗/wa),th∗=log(h∗/ha),
其中:
- x,y,w,hx, y, w, hx,y,w,h:预测框的中心坐标和宽高
- xa,ya,wa,hax_a, y_a, w_a, h_axa,ya,wa,ha:锚点的中心坐标和宽高
- x∗,y∗,w∗,h∗x^*, y^*, w^*, h^*x∗,y∗,w∗,h∗:真实框的中心坐标和宽高
通俗易懂的解释:
这就像你在地图上找一个地方,别人告诉你"从这个路口往东走100米,往北走50米",而不是直接告诉你经纬度。这样更容易理解,也更容易学习。
五、共享特征训练:让两个网络默契配合
RPN和Fast R-CNN都需要卷积特征,但如果分别训练,它们会修改卷积层的参数,导致无法共享。
Faster R-CNN提出了4步交替训练法,完美解决了这个问题:
- 第一步:用ImageNet预训练的权重初始化RPN,训练RPN
- 第二步:用第一步训练好的RPN生成提议,训练一个独立的Fast R-CNN
- 第三步:用第二步训练好的Fast R-CNN初始化RPN,固定共享的卷积层,只训练RPN特有的层
- 第四步:固定共享的卷积层,只训练Fast R-CNN特有的层
经过这四步,两个网络就共享了同一套卷积特征,形成了一个统一的检测系统。
有趣的类比:
这就像一对双人滑冰运动员,先各自练习自己的动作,然后再配合练习,最后达到完美的同步。
六、实验结果:速度和精度的双重飞跃
Faster R-CNN在PASCAL VOC和MS COCO数据集上进行了全面的评估,结果令人震撼。
6.1 PASCAL VOC 2007结果对比
表1:不同区域提议方法的结果对比(ZF网络,来源:论文Table 2)
| 训练时提议方法 | 测试时提议方法 | 提议数量 | mAP (%) |
|---|---|---|---|
| Selective Search | Selective Search | 2000 | 58.7 |
| EdgeBoxes | EdgeBoxes | 2000 | 58.6 |
| RPN+ZF(共享) | RPN+ZF(共享) | 300 | 59.9 |
结果分析:
- RPN只用300个提议,就达到了59.9%的mAP,比Selective Search的2000个提议还高1.2%
- 这说明RPN生成的提议质量比传统方法高得多
6.2 VGG16网络的性能
表2:VGG16网络的检测结果(来源:论文Table 3)
| 方法 | 提议数量 | 训练数据 | mAP (%) |
|---|---|---|---|
| SS+Fast R-CNN | 2000 | VOC 07 | 66.9 |
| SS+Fast R-CNN | 2000 | VOC 07+12 | 70.0 |
| RPN+VGG(共享) | 300 | VOC 07 | 69.9 |
| RPN+VGG(共享) | 300 | VOC 07+12 | 73.2 |
| RPN+VGG(共享) | 300 | COCO+07+12 | 78.8 |
结果分析:
- 用VGG16网络,Faster R-CNN达到了73.2%的mAP,比Fast R-CNN高3.2%
- 如果用COCO数据集预训练,mAP可以达到惊人的78.8%
6.3 速度对比
表3:不同检测系统的速度对比(K40 GPU,来源:论文Table 5)
| 模型 | 系统 | 卷积时间(ms) | 提议时间(ms) | 区域处理时间(ms) | 总时间(ms) | 帧率(fps) |
|---|---|---|---|---|---|---|
| VGG | SS+Fast R-CNN | 146 | 1510 | 174 | 1830 | 0.5 |
| VGG | RPN+Fast R-CNN | 141 | 10 | 47 | 198 | 5 |
| ZF | RPN+Fast R-CNN | 31 | 3 | 25 | 59 | 17 |
震撼的结果:
- Faster R-CNN比SS+Fast R-CNN快了近10倍!
- 提议时间从1.5秒缩短到了10毫秒,几乎可以忽略不计
- 用ZF网络可以达到17fps,已经可以用于很多实时场景了
6.4 锚点设置的消融实验
表4:不同锚点设置的结果对比(VGG16,来源:论文Table 8)
| 锚点设置 | 尺度数量 | 长宽比数量 | mAP (%) |
|---|---|---|---|
| 1尺度1长宽比 | 1 | 1 | 65.8 |
| 1尺度3长宽比 | 1 | 3 | 68.8 |
| 3尺度1长宽比 | 3 | 1 | 69.8 |
| 3尺度3长宽比 | 3 | 3 | 69.9 |
结果分析:
- 多尺度和多长宽比都能显著提升性能
- 只用3个尺度就达到了69.8%的mAP,和3尺度3长宽比几乎一样
- 这说明尺度是影响性能的主要因素,长宽比的影响相对较小
七、核心代码实现
下面是Faster R-CNN最核心的RPN模块实现,包括锚点生成和损失函数计算:
"""
Faster R-CNN核心RPN模块实现
基于PyTorch,简化版,保留所有关键逻辑
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
# -------------------------- 核心超参数 --------------------------
ANCHOR_SCALES = [128, 256, 512] # 锚点尺度
ANCHOR_RATIOS = [0.5, 1, 2] # 锚点长宽比
FEATURE_STRIDE = 16 # 特征图相对于原图的步长
# ---------------------------------------------------------------
def generate_anchors(base_size=16, scales=ANCHOR_SCALES, ratios=ANCHOR_RATIOS):
"""生成基础锚点模板"""
base_anchor = np.array([1, 1, base_size, base_size]) - 1
ratio_anchors = _ratio_enum(base_anchor, ratios)
anchors = np.vstack([_scale_enum(ratio_anchors[i, :], scales)
for i in range(ratio_anchors.shape[0])])
return anchors
def _ratio_enum(anchor, ratios):
"""枚举不同长宽比的锚点"""
w, h, x_ctr, y_ctr = _whctrs(anchor)
size = w * h
size_ratios = size / ratios
ws = np.round(np.sqrt(size_ratios))
hs = np.round(ws * ratios)
anchors = _mkanchors(ws, hs, x_ctr, y_ctr)
return anchors
def _scale_enum(anchor, scales):
"""枚举不同尺度的锚点"""
w, h, x_ctr, y_ctr = _whctrs(anchor)
ws = w * scales
hs = h * scales
anchors = _mkanchors(ws, hs, x_ctr, y_ctr)
return anchors
def _whctrs(anchor):
"""将(x1,y1,x2,y2)转换为(宽,高,中心x,中心y)"""
w = anchor[2] - anchor[0] + 1
h = anchor[3] - anchor[1] + 1
x_ctr = anchor[0] + 0.5 * (w - 1)
y_ctr = anchor[1] + 0.5 * (h - 1)
return w, h, x_ctr, y_ctr
def _mkanchors(ws, hs, x_ctr, y_ctr):
"""根据宽高和中心坐标生成锚点(x1,y1,x2,y2)"""
ws = ws[:, np.newaxis]
hs = hs[:, np.newaxis]
anchors = np.hstack((x_ctr - 0.5 * (ws - 1),
y_ctr - 0.5 * (hs - 1),
x_ctr + 0.5 * (ws - 1),
y_ctr + 0.5 * (hs - 1)))
return anchors
class RPN(nn.Module):
"""区域提议网络"""
def __init__(self, in_channels=512, mid_channels=512):
super().__init__()
# 3x3卷积提取特征
self.conv = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
# 分类头:每个锚点2个输出(物体/背景)
self.cls_head = nn.Conv2d(mid_channels, len(ANCHOR_SCALES)*len(ANCHOR_RATIOS)*2, 1, 1, 0)
# 回归头:每个锚点4个输出(坐标偏移)
self.reg_head = nn.Conv2d(mid_channels, len(ANCHOR_SCALES)*len(ANCHOR_RATIOS)*4, 1, 1, 0)
# 生成基础锚点
self.anchors = torch.from_numpy(generate_anchors()).float()
def forward(self, x, img_size):
"""
Args:
x: 卷积特征图 [B, C, H, W]
img_size: 原图尺寸 (H, W)
Returns:
cls_scores: 分类分数 [B, H*W*9, 2]
reg_preds: 回归预测 [B, H*W*9, 4]
anchors: 所有锚点 [1, H*W*9, 4]
"""
B, C, H, W = x.shape
# 特征提取
x = F.relu(self.conv(x))
# 分类和回归预测
cls_scores = self.cls_head(x) # [B, 18, H, W]
reg_preds = self.reg_head(x) # [B, 36, H, W]
# 调整形状
cls_scores = cls_scores.permute(0, 2, 3, 1).reshape(B, -1, 2)
reg_preds = reg_preds.permute(0, 2, 3, 1).reshape(B, -1, 4)
# 生成所有位置的锚点
shift_x = np.arange(0, W) * FEATURE_STRIDE
shift_y = np.arange(0, H) * FEATURE_STRIDE
shift_x, shift_y = np.meshgrid(shift_x, shift_y)
shifts = np.vstack((shift_x.ravel(), shift_y.ravel(),
shift_x.ravel(), shift_y.ravel())).transpose()
shifts = torch.from_numpy(shifts).float()
A = self.anchors.shape[0]
K = shifts.shape[0]
anchors = self.anchors.reshape(1, A, 4) + shifts.reshape(K, 1, 4)
anchors = anchors.reshape(1, K*A, 4)
return cls_scores, reg_preds, anchors
def rpn_loss(cls_scores, reg_preds, anchors, gt_boxes, img_size):
"""计算RPN损失函数"""
B = cls_scores.shape[0]
total_loss = 0
for b in range(B):
# 计算每个锚点与真实框的IoU
ious = compute_iou(anchors[0], gt_boxes[b])
# 分配标签
labels = torch.full((anchors.shape[1],), -1, dtype=torch.long)
max_iou, max_gt = ious.max(dim=1)
# 正样本1:IoU>0.7
labels[max_iou >= 0.7] = 1
# 正样本2:每个真实框对应的IoU最大的锚点
gt_max_iou, gt_max_anchor = ious.max(dim=0)
labels[gt_max_anchor] = 1
# 负样本:IoU<0.3
labels[max_iou < 0.3] = 0
# 采样256个锚点,正负比例1:1
pos_idx = torch.where(labels == 1)[0]
neg_idx = torch.where(labels == 0)[0]
num_pos = min(128, len(pos_idx))
num_neg = 256 - num_pos
if len(pos_idx) > 0:
pos_idx = pos_idx[torch.randperm(len(pos_idx))[:num_pos]]
if len(neg_idx) > 0:
neg_idx = neg_idx[torch.randperm(len(neg_idx))[:num_neg]]
keep_idx = torch.cat([pos_idx, neg_idx])
# 分类损失
cls_loss = F.cross_entropy(cls_scores[b, keep_idx], labels[keep_idx])
# 回归损失(只计算正样本)
reg_loss = 0
if len(pos_idx) > 0:
# 计算真实偏移量
gt_reg = compute_targets(anchors[0, pos_idx], gt_boxes[b, max_gt[pos_idx]])
reg_loss = F.smooth_l1_loss(reg_preds[b, pos_idx], gt_reg)
total_loss += cls_loss + 10 * reg_loss
return total_loss / B
def compute_iou(boxes1, boxes2):
"""计算两组框的IoU"""
N = boxes1.shape[0]
M = boxes2.shape[0]
x1 = torch.max(boxes1[:, 0].unsqueeze(1), boxes2[:, 0].unsqueeze(0))
y1 = torch.max(boxes1[:, 1].unsqueeze(1), boxes2[:, 1].unsqueeze(0))
x2 = torch.min(boxes1[:, 2].unsqueeze(1), boxes2[:, 2].unsqueeze(0))
y2 = torch.min(boxes1[:, 3].unsqueeze(1), boxes2[:, 3].unsqueeze(0))
w = torch.clamp(x2 - x1 + 1, min=0)
h = torch.clamp(y2 - y1 + 1, min=0)
inter = w * h
area1 = (boxes1[:, 2] - boxes1[:, 0] + 1) * (boxes1[:, 3] - boxes1[:, 1] + 1)
area2 = (boxes2[:, 2] - boxes2[:, 0] + 1) * (boxes2[:, 3] - boxes2[:, 1] + 1)
union = area1.unsqueeze(1) + area2.unsqueeze(0) - inter
return inter / union
def compute_targets(anchors, gt_boxes):
"""计算真实的边界框偏移量"""
w_a = anchors[:, 2] - anchors[:, 0] + 1
h_a = anchors[:, 3] - anchors[:, 1] + 1
x_ctr_a = anchors[:, 0] + 0.5 * (w_a - 1)
y_ctr_a = anchors[:, 1] + 0.5 * (h_a - 1)
w_gt = gt_boxes[:, 2] - gt_boxes[:, 0] + 1
h_gt = gt_boxes[:, 3] - gt_boxes[:, 1] + 1
x_ctr_gt = gt_boxes[:, 0] + 0.5 * (w_gt - 1)
y_ctr_gt = gt_boxes[:, 1] + 0.5 * (h_gt - 1)
tx = (x_ctr_gt - x_ctr_a) / w_a
ty = (y_ctr_gt - y_ctr_a) / h_a
tw = torch.log(w_gt / w_a)
th = torch.log(h_gt / h_a)
return torch.stack([tx, ty, tw, th], dim=1)
# -------------------------- 示例用法 --------------------------
if __name__ == '__main__':
# 初始化RPN
rpn = RPN()
rpn.eval()
# 模拟输入:VGG16的conv5_3特征图
x = torch.randn(1, 512, 38, 50) # 对应原图600×800
img_size = (600, 800)
# 模拟真实框
gt_boxes = torch.tensor([[[100, 100, 300, 300], [400, 200, 600, 500]]])
# RPN前向传播
cls_scores, reg_preds, anchors = rpn(x, img_size)
# 计算损失
loss = rpn_loss(cls_scores, reg_preds, anchors, gt_boxes, img_size)
print(f"分类输出形状: {cls_scores.shape}") # [1, 17100, 2]
print(f"回归输出形状: {reg_preds.shape}") # [1, 17100, 4]
print(f"锚点数量: {anchors.shape[1]}") # 17100
print(f"RPN损失: {loss.item():.4f}")
八、结论与影响
Faster R-CNN是目标检测领域的一个划时代的工作,它的主要贡献有三点:
- 提出了RPN网络,让区域提议和检测网络共享卷积特征,生成提议几乎免费
- 发明了锚点机制,高效解决了多尺度和长宽比物体的检测问题
- 实现了第一个接近实时的两阶段检测系统,速度比之前的方法快了10倍
Faster R-CNN的影响是深远的:
- 它成为了之后几乎所有两阶段检测模型的基础,包括Mask R-CNN、Cascade R-CNN等
- 锚点机制被广泛应用于目标检测、实例分割、人脸检测等领域
- 在2015年ImageNet和COCO比赛中,基于Faster R-CNN的方法获得了多个赛道的冠军
- 被Pinterest等公司用于商业系统,显著提升了用户参与度
虽然现在已经有了YOLO、RT-DETR等更快的单阶段检测器,但Faster R-CNN的思想仍然在影响着这个领域。它证明了深度学习可以端到端地解决目标检测的所有问题,为后来的研究指明了方向。
更多推荐


所有评论(0)