本文将从四个核心模块出发,系统梳理Faster R-CNN网络的设计思路与实现细节,帮助读者深入理解这一经典目标检测算法的原理与优化方法。
经过R-CNN和Fast R-CNN的铺垫,Ross B. Girshick在2016年提出了Faster R-CNN。这一版本的最大亮点在于:特征提取(feature extraction)、候选区域生成(proposal generation)、边界框回归(bounding box regression)以及最终分类(classification)被整合进一个统一的网络结构中,整体性能尤其是检测速度得到了显著提升。这一创新使得Faster R-CNN成为当时最快、最准确的目标检测框架之一。
目录
1 Conv layers
2 Region Proposal Networks (RPN)
- 2.1 多通道图像卷积基础知识介绍
- 2.2 anchors
- 2.3 softmax判定positive与negative
- 2.4 bounding box regression原理
- 2.5 对proposals进行bounding box regression
- 2.6 Proposal Layer
3 RoI pooling
- 3.1 为何需要RoI Pooling
- 3.2 RoI Pooling原理
4 Classification
5 Faster RCNN训练
- 5.1 训练RPN网络
- 5.2 通过训练好的RPN网络收集proposals
- 5.3 训练Faster RCNN网络
Questions and Answer

图1 Faster RCNN基本结构(来自原论文)
从结构上看,Faster R-CNN可以拆解为四个主要部分:
1. **Conv layers**:作为CNN目标检测的基础,先用一组conv+relu+pooling层提取图像的feature maps,这些feature maps后续被RPN层和全连接层共享。
2. **Region Proposal Networks (RPN)**:用于生成候选区域。通过softmax判断anchors是positive还是negative,再利用bounding box regression修正anchors,得到精确的proposals。
3. **RoI Pooling**:收集输入的feature maps和proposals,综合后提取proposal feature maps,送入后续全连接层判定目标类别。
4. **Classification**:利用proposal feature maps计算proposal的类别,同时再次进行bounding box regression,获得检测框的最终精确位置。
下面以这四个部分为线索,深入介绍Faster R-CNN网络。图2展示了python版本中VGG16模型下的faster_rcnn_test.pt网络结构,清晰反映了图像处理流程:
- 首先将任意大小PxQ的图像缩放至固定大小MxN;
- 送入Conv layers,包含13个conv层、13个relu层、4个pooling层;
- RPN网络先经过3x3卷积,再分别生成positive anchors和对应的bounding box regression偏移量,计算出proposals;
- RoI Pooling层利用proposals从feature maps中提取proposal feature,送入后续全连接和softmax网络进行分类。

图2 faster_rcnn_test.pt网络结构(pascal_voc/VGG16/faster_rcnn_alt_opt/faster_rcnn_test.pt)
本文不打算追溯R-CNN家族的完整历史,分析清楚最新的Faster R-CNN就够了。事实上,R-CNN的细节并非必需,新算法更值得关注。Faster R-CNN通过端到端训练实现了检测速度与精度的双重突破,成为目标检测领域的重要里程碑。
1 Conv layers
Conv layers由conv、pooling、relu三种层组成。以VGG16模型为例,Conv layers部分共有13个conv层、13个relu层、4个pooling层。这里有一个极易被忽略但至关重要的信息:
- 所有conv层:kernel_size=3,pad=1,stride=1
- 所有pooling层:kernel_size=2,pad=0,stride=2
为什么重要?因为Faster R-CNN的Conv layers对所有卷积都做了扩边处理(pad=1,即填充一圈0),原图变为(M+2)×(N+2)大小,再做3×3卷积后输出M×N。正是这种设置,使得conv层不改变输入和输出矩阵大小。如图3所示:

图3 卷积示意图
类似地,pooling层的kernel_size=2,stride=2,每个经过pooling层的M×N矩阵缩小为(M/2)×(N/2)。因此,在整个Conv layers中,只有pooling层改变尺寸,使输出长宽各变为输入的1/2。这样一来,一个M×N大小的矩阵经过Conv layers后固定变为(M/16)×(N/16),feature map上的每个点都能与原图对应起来。这种设计保证了特征图的尺寸一致性,为后续的RPN和RoI Pooling提供了稳定的输入。
2 Region Proposal Networks (RPN)
经典的检测方法生成检测框非常耗时,比如OpenCV adaboost使用滑动窗口+图像金字塔,或R-CNN使用Selective Search。而Faster R-CNN直接使用RPN生成检测框,抛弃了传统方法,大幅提升了检测框的生成速度。RPN网络通过全卷积结构,在特征图上直接预测目标候选区域,实现了高效的区域提议。

图4 RPN网络结构
图4展示了RPN的具体结构。RPN网络实际分为两条线:上面一条通过softmax分类anchors,得到positive和negative分类;下面一条计算针对anchors的bounding box regression偏移量,以获得精确的proposal。最后的Proposal层负责综合positive anchors和对应的偏移量,同时剔除太小或超出边界的proposals。至此,相当于完成了目标定位的功能。
2.1 多通道图像卷积基础知识介绍
在深入RPN之前,有必要回顾一下卷积的基础知识,已经熟悉的读者可以直接跳过。理解多通道卷积有助于后续理解RPN中特征图的处理方式。
对于单通道图像+单卷积核的卷积,图3已经展示过。对于多通道图像+多卷积核,计算方式如下:

图5 多通道卷积计算方式
如图5,输入有3个通道,同时有2个卷积核。每个卷积核先在输入3个通道分别做卷积,再将3个通道的结果相加得到卷积输出。所以,无论输入图像有多少个通道,输出图像的通道数总是等于卷积核的数量。对多通道图像做1×1卷积,相当于将输入图像每个通道乘以卷积系数后相加,即把原本独立的通道“联通”在一起。这种操作在RPN中用于调整特征图维度。
2.2 anchors
提到RPN,就不能不说anchors。所谓anchors,实际上就是一组由rpn/generate_anchors.py生成的矩形。直接运行demo中的generate_anchors.py可以得到以下输出:
[[ -84. -40. 99. 55.]
[-176. -88. 191. 103.]
[-360. -184. 375. 199.]
[ -56. -56. 71. 71.]
[-120. -120. 135. 135.]
[-248. -248. 263. 263.]
[ -36. -80. 51. 95.]
[ -80. -168. 95. 183.]
[-168. -344. 183. 359.]]
每行的4个值表示矩形左上角和右下角点坐标。9个矩形共有3种形状,长宽比约为三种,如图6所示。实际上,anchors引入了检测中常用的多尺度方法,使得网络能够处理不同大小的目标。

图6 anchors示意图
注意,上面的anchors size是根据检测图像设置的。在python demo中,任意大小的输入图像会被reshape成800×600(即图2中的M=800,N=600)。顶锚框的大小中,长宽比1:2的最大为352×704,长宽比2:1的最大为736×384,基本覆盖了800×600的各个尺度和形状。
那么这9个anchors是做什么的?借用Faster R-CNN论文中的原图(图7):遍历Conv layers计算出的feature maps,为每一个点都配备这9种anchors作为初始检测框。这样得到的检测框很不准确,但不用担心,后面还有两次bounding box regression可以修正位置。

图7
解释一下这张图的数字:
- 原文中使用的ZF模型,其Conv Layers最后的conv5层num_output=256,对应生成256张特征图,所以feature map每个点都是256维。
- 在conv5之后,做了rpn_conv/3×3卷积且num_output=256,相当于每个点又融合了周围3×3的空间信息(猜测这样更鲁棒),同时256维不变(如图4和图7的红框)。
- 假设在conv5 feature map上每个点有k个anchor(默认k=9),每个anchor要分positive和negative,所以每个点由256维特征转化为cls=2k scores;每个anchor有(x, y, w, h)对应4个偏移量,所以reg=4k coordinates。
- 补充一点:全部anchors拿去训练太多,训练程序会在合适的anchors中随机选取128个positive anchors + 128个negative anchors进行训练(合适的anchors定义见5.1)。
注意,本文讲解中使用的VGG conv5 num_output=512,所以是512维,其他类似。RPN最终就是在原图尺度上,设置了密密麻麻的候选Anchor,然后用CNN判断哪些Anchor里面有目标(positive anchor),哪些没有(negative anchor)。本质上,这是一个二分类问题!
那么Anchor一共有多少个?原图800×600,VGG下采样16倍,feature map每个点设置9个Anchor,所以:
ceil(800/16)×ceil(600/16)×9 = 50×38×9 = 17100个
其中ceil()表示向上取整,因为VGG输出的feature map size=50×38。

图8 Generate Anchors
2.3 softmax判定positive与negative
一副M×N大小的矩阵送入Faster R-CNN后,到RPN网络变为(M/16)×(N/16),不妨设W=M/16,H=N/16。在进入reshape与softmax之前,先做1×1卷积,如图9:

图9 RPN中判定positive/negative网络结构
该1×1卷积的caffe prototxt定义如下:
layer {
name: "rpn_cls_score"
type: "Convolution"
bottom: "rpn/output"
top: "rpn_cls_score"
convolution_param {
num_output: 18 # 2(positive/negative) * 9(anchors)
kernel_size: 1 pad: 0 stride: 1
}
}
可以看到num_output=18,输出图像为W×H×18大小。这正好对应feature map每个点有9个anchors,每个anchors又有positive和negative两种可能,所有信息保存在W×H×(9×2)的矩阵中。后面接softmax分类得到positive anchors,相当于初步提取了检测目标候选区域(一般认为目标在positive anchors中)。
为何要在softmax前后都接一个reshape layer?这是为了便于softmax分类,具体原因与caffe的数据存储格式有关。在caffe中,blob以[batch_size, channel, height, width]的形式保存数据。对应上述positive/negative anchors矩阵,其存储形式为[1, 2×9, H, W]。在softmax分类时需要进行positive/negative二分类,所以reshape layer将其变为[1, 2, 9×H, W]大小,单独腾出一个维度以便softmax分类,之后再reshape回原状。caffe softmax_loss_layer.cpp的reshape函数中有精辟解释:
"Number of labels must match number of predictions; e.g., if softmax axis == 1 and prediction shape is (N, C, H, W), label count (number of labels) must be N*H*W, with integer values in {0, 1, ..., C-1}."
综上所述,RPN网络利用anchors和softmax初步提取出positive anchors作为候选区域(也有实现用sigmoid代替softmax,输出[1, 1, 9×H, W]后接sigmoid进行二分类,原理相同)。
2.4 bounding box regression原理
如图10所示,绿色框为飞机的Ground Truth (GT),红色为提取的positive anchors。即便红色框被分类器识别为飞机,但定位不准,相当于没有正确检测出飞机。因此需要一种方法对红色框进行微调,使positive anchors和GT更接近。

图10
对于窗口,一般用四维向量表示中心点坐标和宽高。红框A代表原始positive anchors,绿框G代表目标GT,目标是寻找一种变换F,使得输入anchor A经过映射得到与GT更接近的回归窗口G':
给定anchor A = (A_x, A_y, A_w, A_h),寻找变换F,使得F(A) = G' ≈ G。
经过何种变换F?比较简单的思路是:
先做平移:
G'_x = A_x + A_w · d_x(A)
G'_y = A_y + A_h · d_y(A)
再做缩放:
G'_w = A_w · exp(d_w(A))
G'_h = A_h · exp(d_h(A))
观察上面4个公式,需要学习的是d_x, d_y, d_w, d_h这四个变换。当anchor A与GT相差较小时,可以认为这种变换是线性变换,用线性回归建模对窗口进行微调(注意,只有当anchors A和GT比较接近时,才能使用线性回归,否则就是复杂的非线性问题)。
接下来的问题是如何通过线性回归获得d_x, d_y, d_w, d_h。线性回归就是给定输入特征向量X,学习参数W,使经过线性回归后的值跟真实值Y接近,即Y=WX。对于该问题,输入X是CNN feature map,定义为Φ;同时还有训练传入A与GT之间的变换量,即t* = (t_x*, t_y*, t_w*, t_h*)。输出是d = (d_x, d_y, d_w, d_h)。目标函数可以表示为:
d_* = W_* · Φ
其中Φ是对应anchor的feature map组成的特征向量,W_*是需要学习的参数,d_*是预测值(*表示x, y, w, h,每个变换对应一个目标函数)。为了让预测值与真实值t*差距最小,设计L1损失函数(实际中常用smooth-L1损失):
Loss = |t* - d_*|
需要说明,只有在GT与需要回归框位置比较接近时,才可近似认为上述线性变换成立。
对应于Faster R-CNN原文,positive anchor与ground truth之间的平移量和尺度因子如下:
t_x = (G_x - A_x) / A_w
t_y = (G_y - A_y) / A_h
t_w = ln(G_w / A_w)
t_h = ln(G_h / A_h)
对于训练bounding box regression网络回归分支,输入是CNN feature Φ,监督信号是Anchor与GT的差距t*,即训练目标是:输入Φ的情况下使网络输出与监督信号接近。当bounding box regression工作时,再输入Φ,回归网络分支的输出就是每个Anchor的平移量和变换尺度,即可用来修正Anchor位置。
2.5 对proposals进行bounding box regression
了解bounding box regression后,再回头看RPN网络的第二条线路,如图12:

图12 RPN中的bbox reg
该1×1卷积的caffe prototxt定义:
layer {
name: "rpn_bbox_pred"
type: "Convolution"
bottom: "rpn/output"
top: "rpn_bbox_pred"
convolution_param {
num_output: 36 # 4 * 9(anchors)
kernel_size: 1 pad: 0 stride: 1
}
}
num_output=36,输出图像为W×H×36,在caffe blob存储为[1, 4×9, H, W],即feature map每个点有9个anchors,每个anchors有4个用于回归的变换量。
回到图8,VGG输出50×38×512的特征,对应设置50×38×k个anchors,而RPN输出:
- 大小为50×38×2k的positive/negative softmax分类特征矩阵;
- 大小为50×38×4k的regression坐标回归特征矩阵。
恰好满足RPN完成positive/negative分类和bounding box regression坐标回归。
2.6 Proposal Layer
Proposal Layer负责综合所有变换量和positive anchors,计算出精准的proposal,送入后续RoI Pooling Layer。其caffe prototxt定义:
layer {
name: 'proposal'
type: 'Python'
bottom: 'rpn_cls_prob_reshape'
bottom: 'rpn_bbox_pred'
bottom: 'im_info'
top: 'rois'
python_param {
module: 'rpn.proposal_layer'
layer: 'ProposalLayer'
param_str: "'feat_stride': 16"
}
}
Proposal Layer有三个输入:positive vs negative anchors分类器结果rpn_cls_prob_reshape,对应的bbox reg变换量rpn_bbox_pred,以及im_info;参数feat_stride=16,与图4对应。
首先解释im_info。对于任意大小P×Q图像,传入Faster R-CNN前首先reshape到固定M×N,im_info=[M, N, scale_factor]保存了缩放信息。经过Conv Layers,经过4次pooling变为W×H=(M/16)×(N/16),feature_stride=16保存了该信息,用于计算anchor偏移量。

图13
Proposal Layer forward按照以下顺序处理:
1. 生成anchors,利用对所有的anchors做bbox regression回归(这里的anchors生成与训练时完全一致)。
2. 按照输入的positive softmax scores由大到小排序anchors,提取前pre_nms_topN(例如6000)个anchors,即提取修正位置后的positive anchors。
3. 限定超出图像边界的positive anchors为图像边界,防止后续roi pooling时proposal超出图像边界。
4. 剔除尺寸非常小的positive anchors。
5. 对剩余的positive anchors进行NMS(non-maximum suppression)。
6. 输出前topN(例如300)个proposal。
之后输出proposal=[x1, y1, x2, y2],注意由于在第三步中将anchors映射回原图判断是否超出边界,所以这里输出的proposal对应M×N输入图像尺度,这点在后续网络中有用。严格来说,检测到这一步已经完成,后续部分属于识别。
RPN网络结构总结起来就是:
生成anchors → softmax分类器提取positive anchors → bbox reg回归positive anchors → Proposal Layer生成proposals
3 RoI pooling
RoI Pooling层负责收集proposal,并计算出proposal feature maps,送入后续网络。从图2中可以看到RoI Pooling层有两个输入:
- 原始的feature maps
- RPN输出的proposal boxes(大小各不相同)
3.1 为何需要RoI Pooling
对于传统的CNN(如AlexNet和VGG),网络训练好后输入图像的尺寸必须是固定值,输出也是固定大小的vector或matrix。如果输入图像大小不定,就比较麻烦。有两种解决办法:
- 从图像中crop一部分传入网络;
- 将图像warp成需要的大小后传入网络。
两种办法都不好,要么crop破坏了图像的完整结构,要么warp破坏了原始形状信息。RPN生成的proposals也是大小形状各不相同,因此存在同样的问题。Faster R-CNN中提出了RoI Pooling解决这个问题。RoI Pooling源自Spatial Pyramid Pooling,它能够将任意大小的特征图区域转换为固定尺寸输出,从而保证后续全连接层的输入维度一致。
3.2 RoI Pooling原理
RoI Pooling Layer的caffe prototxt定义:
layer {
name: "roi_pool5"
type: "ROIPooling"
bottom: "conv5_3"
bottom: "rois"
top: "pool5"
roi_pooling_param {
pooled_w: 7
pooled_h: 7
spatial_scale: 0.0625 # 1/16
}
}
其中参数pooled_w和pooled_h,以及spatial_scale。RoI Pooling layer forward过程:
1. 由于proposal对应M×N尺度,首先使用spatial_scale参数将其映射回(M/16)×(N/16)大小的feature map尺度;
2. 再将每个proposal对应的feature map区域水平分为pooled_w×pooled_h的网格;
3. 对网格的每一份都进行max pooling处理。
这样处理后,即使大小不同的proposal输出结果都是pooled_w×pooled_h固定大小,实现了固定长度输出。这使得网络能够处理任意尺寸的输入图像,同时保持特征的有效性。

图15 proposal示意图
4 Classification
Classification部分利用已经获得的proposal feature maps,通过全连接层与softmax计算每个proposal具体属于哪个类别(如人、车、电视等),输出cls_prob概率向量;同时再次利用bounding box regression获得每个proposal的位置偏移量bbox_pred,用于回归更精确的目标检测框。Classification部分网络结构如图16。

图16 Classification部分网络结构图
从RoI Pooling获取到7×7=49大小的proposal feature maps后,送入后续网络,做了两件事:
- 通过全连接和softmax对proposals进行分类,这属于识别范畴;
- 再次对proposals进行bounding box regression,获取更高精度的rect box。
全连接层InnerProduct layers的示意图如图17:

图17 全连接层示意图
其计算公式为:Y = WX + B,其中W和B都是预先训练好的,大小固定,因此输入X和输出Y也是固定大小。这印证了之前RoI Pooling的必要性。到这里,其他内容已经很容易理解,不再赘述。
5 Faster RCNN训练
Faster R-CNN的训练是在已经训练好的模型(如VGG_CNN_M_1024、VGG、ZF)基础上继续进行。实际训练过程分为6个步骤:
1. 在已经训练好的模型上,训练RPN网络(对应stage1_rpn_train.pt)。
2. 利用步骤1中训练好的RPN网络,收集proposals(对应rpn_test.pt)。
3. 第一次训练Fast RCNN网络(对应stage1_fast_rcnn_train.pt)。
4. 第二次训练RPN网络(对应stage2_rpn_train.pt)。
5. 再次利用步骤4中训练好的RPN网络,收集proposals(对应rpn_test.pt)。
6. 第二次训练Fast RCNN网络(对应stage2_fast_rcnn_train.pt)。
可以看到训练过程类似于一种“迭代”,但只循环了2次。为什么只循环2次?作者提到:“A similar alternating training can be run for more iterations, but we have observed negligible improvements”,即循环更多次没有提升。下面以上述6个步骤讲解训练过程。
训练流程图:
5.1 训练RPN网络
首先读取预训练好的模型(本文使用VGG),开始迭代训练。stage1_rpn_train.pt网络结构如图19:

图19 stage1_rpn_train.pt
与检测网络类似,仍使用Conv Layers提取feature maps。整个网络使用的Loss如下:
Loss = (1/N_cls) * Σ L_cls(p_i, p_i*) + λ * (1/N_reg) * Σ p_i* · L_reg(t_i, t_i*)
其中i表示anchors index,p_i是positive softmax probability,p_i*代表对应GT预测概率(当第i个anchor与GT间IoU>0.7时认为是positive,p_i*=1;IoU<0.3时认为是negative,p_i*=0;0.3~0.7的anchor不参与训练)。t代表predict坐标,t*代表GT坐标。
- cls loss:即rpn_cls_loss层计算的softmax loss,用于分类anchors为positive与negative;
- reg loss:即rpn_loss_bbox层计算的smooth L1 loss,用于bounding box regression。注意该loss中乘了p_i*,即只关心positive anchors的回归(回归中确实没必要关心negative)。
由于实际中N_cls和N_reg差距过大,用参数λ平衡二者(例如N_cls=256, N_reg=2400时设置λ=10),使总Loss计算中能够均匀考虑两种Loss。smooth L1 loss计算公式如下:
smooth_L1(x) = 0.5x^2 if |x| < 1
|x| - 0.5 otherwise
了解数学原理后,反过来看图18:
- 在RPN训练阶段,rpn-data(python AnchorTargetLayer)层会按照与test阶段Proposal层完全一样的方式生成Anchors用于训练;
- 对于rpn_loss_cls,输入的rpn_cls_scores_reshape和rpn_labels分别对应p_i与p_i*,参数隐含在caffe blob的大小中;
- 对于rpn_loss_bbox,输入的rpn_bbox_pred和rpn_bbox_targets分别对应t与t*,rpn_bbox_inside_weights对应p_i*,rpn_bbox_outside_weights未用到(从smooth_L1_Loss layer代码中可以看到),而λ同样隐含在caffe blob大小中。
这样,公式与代码就完全对应了。特别注意,在训练和检测阶段生成和存储anchors的顺序必须完全一致,这样训练结果才能用于检测。
5.2 通过训练好的RPN网络收集proposals
利用之前的RPN网络,获取proposal rois,同时获取positive softmax probability,如图20,然后将信息保存在python pickle文件中。该网络本质上与检测中的RPN网络相同。

图20 rpn_test.pt
5.3 训练Faster RCNN网络
读取之前保存的pickle文件,获取proposals与positive probability。从data层输入网络,然后:
- 将提取的proposals作为rois传入网络,如图21蓝框;
- 计算bbox_inside_weights和bbox_outside_weights,作用与RPN一样,传入smooth_L1_loss layer,如图21绿框。
这样就能训练最后的识别softmax与最终的bounding box regression。

图21 stage1_fast_rcnn_train.pt
之后的stage2训练大同小异,不再赘述。Faster R-CNN还有一种end-to-end的训练方式,可以一次完成训练,有兴趣的读者可自行查阅作者GitHub。这种端到端训练方式在后续的Mask R-CNN等工作中得到了进一步发展。
Q&A
此篇文章初次成文于2016年内部学习分享,后来经多次修正和完善成为现在这个样子。总结常见疑问如下:
为什么Anchor坐标中有负数?
回顾anchor生成步骤:首先生成9个base anchor,然后通过坐标偏移在50×38大小的1/16下采样FeatureMap每个点都放上这9个base anchor,形成50×38×k个anchors。这9个base anchor的坐标是什么并不重要,不同代码实现可能不同。显然,有一部分边缘anchors会超出图像边界,而真实中不会有超出图像的目标,所以会有clip anchor步骤。

图21 clip anchor
Anchor到底与网络输出如何对应?
VGG输出50×38×512的特征,对应设置50×38×k个anchors,而RPN输出50×38×2k的分类特征矩阵和50×38×4k的坐标回归特征矩阵。在实现过程中,每个点的2k个分类特征与4k个回归特征,与k个anchor逐个对应,这是一种“人为设置的逻辑映射”。当然,也可以不这样设置,但无论如何都需要保证
在训练和测试过程中映射方式必须一致。
为何有ROI Pooling还要把输入图片resize到固定大小的M×N?
由于引入ROI Pooling,从原理上说Faster R-CNN确实能够检测任意大小的图片。但在训练时需要使用大batch训练网络,不同大小输入拼batch在实现时代码较为复杂,且当时以Caffe为代表的第一代深度学习框架不如TensorFlow和PyTorch灵活,所以作者选择了把输入图片resize到固定大小800×600。这算是历史遗留问题。另外很多问题属于具体实现,建议读者阅读代码自行理解。