RCNN到Fast-RCNN:目标检测演进与工程实现要点

发布时间:2026/10/13 17:43:25
RCNN到Fast-RCNN:目标检测演进与工程实现要点
1. 对象识别不是“分类加个框”四件事必须同时完成如果你跟我一样是从图像分类入门深度学习的那第一次听到“对象识别”这个任务时多半会有个朴素的想法分类我会了就是让卷积网络输出“这是猫还是狗”再给它加一个边界框回归让网络顺便输出“猫在图片的哪个位置”这不就完事了这个想法方向没错但真正做起来会发现一个很现实的问题一张图片里到底有多少个对象、每个对象是什么、它们分别在哪儿这三个问题在推理之前是完全未知的。分类任务输入是一张干净的、尺寸固定的图片输出是一组类别概率而对象识别输入还是一张图输出却是一组长度不定的检测结果每个结果包含类别、置信度、边界框四个坐标。输入输出结构都变了光靠把分类网络改一改是解决不了的。我当年第一次写对象识别程序时最直观的感受就是网络结构还是那个网络结构但整个数据流和训练流程的复杂度高了一个量级。你需要同时处理好四个子问题定位对象在哪里用边界框表示通常是一个四元组 (x, y, w, h) 或 (x1, y1, x2, y2)。分类框里的东西是什么这是分类网络的老本行。多目标图里可能同时有 3 个人、2 辆车、1 只狗你的算法必须能输出多个框而不是只给一个答案。重叠与冗余同一个对象可能会被许多候选框覆盖你得有一套去重机制把最终输出收敛到每个对象只留一个框。这套东西听起来复杂但 2014 年 RCNN 的提出把问题拆成了几个“用旧工具拼装起来的新方案”。这种拆法非常符合程序员的思维习惯把一个大问题分解成若干可独立处理、可独立验证的小模块每个模块用当时已经很成熟的技术搞定。想要真正理解后续的 Fast-RCNN、Faster-RCNN 是怎么演进的必须先理解 RCNN 是怎么把一个“看似无解”的问题拆成三块的。这篇就顺着 RCNN 的拆解思路讲起再讲 Fast-RCNN 是怎么解决 RCNN 的性能瓶颈最后聊一些训练和部署时容易踩的坑。适合有一定 CNN 基础、想从“会分类”跨到“会检测”的开发者。2. RCNN 的破局思路把检测拆成候选区域、CNN 特征、SVM 三件旧事RCNNRegion-based CNN的核心思想用一句话说就是先用传统的计算机视觉方法生成一堆可能包含对象的区域然后让 CNN 逐个区域做特征提取最后用分类器判断每个区域是什么。这看起来不像一个“深度学习方案”倒更像一个“传统视觉 CNN 特征”的混合管线。但 2014 年它确实把对象识别的精度提升了一大截。我拆开讲。2.1 第一步选择性搜索生成候选区域在 RCNN 之前主流的窗口滑动法是在整张图片上以不同尺寸、不同长宽比密集滑动窗口每一个窗口都进分类器。这样做的计算量极其惊人即使步长设得很大一张 500x500 的图也需要跑几万个窗口。RCNN 换了个思路先用一个叫“选择性搜索”selective search的传统算法从图里挑出“最可能包含对象”的约 2000 个区域而不是穷举所有位置。选择性搜索的想法是先按像素颜色、纹理把图切成很多小块然后按相似度颜色相似、纹理相似、大小相近、空间上相邻不断合并小块每合并一次就生成一个候选区域。整个过程有点像“从碎片拼出物体”因为真实物体内部颜色和纹理往往是一致的所以这些合并出来的区域大概率就是对象所在的地方。这一步虽然属于传统算法但它的意义很大把检测的空间从“全图所有位置”缩小到了“大约 2000 个值得看的位置”。我在理解 RCNN 时是先把这一步当作一个可替换的黑盒模块来看的。它不参与 CNN 的训练也不参与梯度传播只是负责输出一组坐标。这种模块化解耦让调试变得很方便候选区域质量不对我可以单独可视化检查它有没有把对象覆盖住。2.2 第二步CNN 只负责当特征提取器选出候选区域后每个区域都被缩放成统一尺寸比如 227x227然后送入一个卷积网络提取特征。这里的 CNN 并不是为检测专门训练的它最初做的是图像分类任务在大型通用视觉数据集上预训练完成。RCNN 的做法是把 CNN 当作一个“万能特征提取器”直接取它的全连接层输出比如 4096 维向量作为这个区域的语义特征。这个设计在当时是有争议的。因为预训练模型的分类知识来自 ImageNet 这种纯分类数据集直接拿来做检测特征直觉上有点“专业不对口”。但后来实验证明这些特征已经足够通用用来描述“区域里有什么”完全可行。作为程序员你可以把它理解成“迁移学习”在一个大任务上学到的表示拿到相关的小任务上依然好使。不过 RCNN 做了两个针对性微调把 CNN 最后一个 1000 类分类层换成 21 类20 个对象类 1 个背景类用候选区域数据对 CNN 做一次微调微调时的正样本是“与真实框 IoU 大于 0.5 的候选区域”负样本是“IoU 小于 0.3 的区域”。这一步叫“pre-training fine-tuning”的组合目的是让 CNN 输出的特征更贴检测任务。但它还不是最终分类器只是让特征变得“更适合被后续的分类器使用”。2.3 第三步SVM 分类 边界框回归每个候选区域经过 CNN 得到 4096 维特征后RCNN 并不是直接接 Softmax而是为每个类别单独训练了一个 SVM 二分类器。比如“猫”类别所有猫类的正样本区域标为正所有非猫区域标为负训练一个判别“这个区域是不是猫”的 SVM。为什么不用 SoftmaxRCNN 作者当时的实验发现用 SVM 做最终分类比直接 Softmax 精度高。原因比较复杂其中一个重要因素是 SVM 对负样本的使用方式不同SVM 训练时会用一种“难负样本挖掘”策略从所有错误分类的负样本中挑出最难的、最有区分度的那些继续训练相当于把分类器“针对性磨利”。而 Softmax 是用微调 CNN 时的随机负样本一次性训练的。这是 RCNN 在工程结构上非常重要的一个细节CNN 微调和 SVM 训练用的正负样本定义不一样是两套独立流程。我最初被这一点绕了很久后面会对这个做对比说明。最后还要训练一个边界框回归器。因为选择性搜索给出的框往往不够准可能框住了对象但留有大量背景或者切掉了对象一部分。边界框回归做的事情是输入 CNN 提取的特征输出四个修正量 (dx, dy, dw, dh)分别表示中心点平移量和宽高校正量把候选框修正到更贴近真实框。我写一段伪代码来概括 RCNN 的完整评估流程方便你理解它的执行顺序def rcnn_predict(image, svm_models, bbox_regressors, cnn): proposals selective_search(image, max_regions2000) boxes [] for region in proposals: cropped crop_resize(image, region, size(227, 227)) feature cnn(cropped) # 输出 4096 维向量 for cls_id in range(1, 21): score svm_models[cls_id].predict(feature) if score threshold: box bbox_regressors[cls_id].predict(feature) boxes.append((cls_id, score, adjust_region(region, box))) return nms(boxes) # 非极大值抑制去重2.4 训练 pipeline 里那三组容易混淆的概念RCNN 的代码不难真正难的是它组织训练数据的方式。我第一次上手时花了不少时间才理清楚三个完全不同的“正负样本”定义环节正样本定义负样本定义作用CNN 微调IoU 0.5 的候选区域IoU 0.3 的候选区域让特征适配备检测任务SVM 训练真实框ground truth或者与真实框 IoU 最高的框IoU 0.3 的区域 难负样本获得高精度最终分类边界BBox 回归训练与真实框 IoU 0.6 的区域不使用负样本只对正样本拟合修正候选框位置同一个“区域”在三个环节里的“身份”可能完全不同。一个 IoU 为 0.4 的候选区域在 CNN 微调里是正样本到 SVM 里可能就变成负样本了因为没到 0.5 的阈值到回归器里完全不用。这就是 RCNN 让人“一看就懂、一写就懵”的地方。我建议所有初学 RCNN 的人先在表格里把三套样本标定义清楚再动代码不然训练出来精度莫名其妙地差。3. 为什么 RCNN 单张图片要跑 47 秒慢是慢在重复计算RCNN 虽然效果好但性能差到几乎无法用于实际产品。在当时的公开评测集上它检测单张图片需要大约 47 秒。我算过一笔账这笔账对理解后面的 Fast-RCNN 至关重要。RCNN 的时间主要花在两部分一是每张图生成约 2000 个候选区域二是每个候选区域都要单独过一次 CNN。假设你的 CNN 单张推理耗时 20ms那么 2000 个区域就是 2000 x 20ms 40 秒。这还不算候选区域生成和 SVM 分类的时间。问题的本质在于这 2000 个候选区域互相之间严重重叠但我们的做法是把它们当作一张张全新的图片来处理。你裁剪出来的区域 A 和区域 B 可能只差十几个像素它们经过 CNN 时前几层卷积计算的特征图几乎完全相同但这些重复计算没有被利用起来。用程序员的说法就是“没有做子图级别的共享计算优化”。RCNN 论文里其实也提到CNN 的卷积部分占据绝大多数计算量而后面的全连接层只占很小一部分。如果一个候选区域在图上只偏移了 10 个像素它产生的特征图里大部分位置的值和偏移前是一样的。如果能设计一套机制让整张图只算一次卷积然后直接从卷积特征图里“抠出”各个候选区域对应的部分就能省掉几乎所有重复计算。这就是 Fast-RCNN 的核心动机。除了推理慢RCNN 在训练上也很折磨人。因为每个区域的 CNN 特征都要单独提取训练数据可能是几万个区域提取出的特征文件动辄几十 GB。我当时做的第一版教程实验特征是分批次提取然后存到磁盘上的跑一次完整训练流程要一天多。而且 RCNN 的精度上限还受制于候选区域数量——你想多生成几个候选区域提升召回率推理时间就线性往上飙完全没办法在精度和速度之间做灵活取舍。RCNN 的另一个工程槽点是训练流程太长。CNN 微调、SVM 训练、边界框回归器训练是三个独立阶段每个阶段都要单独准备数据、单独调参。中间任何一步的样本定义搞错后面整条线都废了。这类模型在当时被称为“多阶段检测器”后来的 Fast-RCNN 之所以让大家兴奋很大一部分原因就是它把这个多阶段流程变成了一次端到端的训练。4. Fast-RCNN 的关键一跃ROI Pooling 把重复卷积变成一次共享Fast-RCNN 这篇论文的标题基本就说清楚了它快在哪不再让每个候选区域单独过 CNN而是整张图只过一遍卷积网络拿到整图特征图后再通过一个叫 ROI Pooling 的操作从特征图里取出对应各个候选区域的特征。我最初理解 ROI Pooling 时觉得它像是编程里的“大数组切片”。整张图经过卷积之后得到的是一个 C x H x W 的特征图这个特征图把整张图的空间信息和语义信息都压缩在里面。此时候选区域是从原图坐标系里给出的一个矩形框比如 (x1, y1, x2, y2)我们需要做的就是把这个坐标映射到特征图坐标系上然后把那部分特征“抠”出来。但“抠”出来之后还有一个问题CNN 后面的全连接层要求输入尺寸固定而各个候选区域的大小参差不齐。ROI Pooling 解决这个问题的办法非常简单粗暴不管候选区域多大一律在特征图上分成固定的块数比如 7x7对每一块做 max pooling。这样无论原始候选区域是 50x50 还是 200x300最后输出的都是 7x7xC 的定长特征可以直接送进全连接层。这个分块操作里有个值得注意的细节候选区域的边界在映射到特征图时经常不是整数。比如原图坐标除以卷积步长后得到 10.7怎么办Fast-RCNN 当时的做法是直接量化取整比如向下取整到 10再把候选区域的高宽也按比例量化到整数。这意味着每个块的边界也不是完全精确的可能会有最多一个像素的偏差。作为入门者你可以先不纠结这个量化误差。因为到了下一代的 Faster-RCNN 和后来的 Mask-RCNN作者引入了 ROI Align 来解决这个问题用双线性插值替代取整精度进一步提高。但在理解 Fast-RCNN 时只要记住它的量化取整实现非常简单、速度很快但会有轻微定位精度损失就够了。Fast-RCNN 的推理流程变成这样def fast_rcnn_predict(image, net, rpn_proposals): feature_map net.conv_layers(image) # 整图一次前向 results [] for region in rpn_proposals: roi_feature roi_pool(feature_map, region, output_size(7, 7)) cls_scores, bbox_deltas net.head(roi_feature) results.append((cls_scores, bbox_deltas, region)) return nms(results)注意这里的候选区域生成和 RCNN 一样仍然是用选择性搜索等外部算法。这个“外部候选区域”的依赖要到 Faster-RCNN 才解除。在计算量上Fast-RCNN 的四阶段变化非常直观阶段工作计算量占比约整图卷积每张图 1 次大部分候选区域生成选择性搜索CPU 算法较少ROI Pooling约 2000 次小规模池化极少全连接 分类每区域过一次 head 网络较少原来的瓶颈“每个区域单独过完整 CNN”被彻底移除了整图只需要一次卷积。虽然 ROI Pooling 之后每个候选区域仍然要过一次全连接层但全连接层计算量远小于卷积层整体速度提升了约 20 倍。在当年的公开评测集上Fast-RCNN 把单张图片检测时间从 RCNN 的 47 秒降到了约 2 秒训练时间也从几十小时降到了不到一天。5. 端到端训练Softmax 替换 SVM 之后损失函数怎么设计Fast-RCNN 不只是推理变快了它还把训练流程从“多阶段独立”变成了“一步到位”这是它更值得学习的地方。RCNN 的思路是“CNN 只提取特征最终分类交给外部 SVM”。但 Fast-RCNN 直接把最终分类器换成了 Softmax并让整个网络一起训练。模型结构是卷积层 - ROI Pooling - 两个并行的输出头分类头 回归头。损失函数由两部分的加权和组成分类损失对每个候选区域预测它属于哪一类包括背景类用 Softmax 交叉熵。回归损失只对非背景的候选区域计算度量预测的边界框修正量与真实修正量之间的差异用的是 smooth L1 损失。smooth L1 的特别之处在于误差小的时候按平方函数增长误差大的时候按线性函数增长。相比传统的 L2 损失它对离群点没那么敏感。检测任务里候选区域的情况千差万别偶尔会出现一个特别难修正的框如果用 L2这个框产生的巨大梯度会带偏整个训练用 smooth L1 则可以限制它的影响。这是从工程经验里总结出来的损失设计后来被几乎所有的检测模型沿用。除了损失函数Fast-RCNN 在训练时对 mini-batch 的组织方式和普通分类任务完全不同这一点对写代码的人来讲是最大的坎。在普通分类任务里你随机从所有训练样本里抽 32 张图片每张图片一个标签。但在 Fast-RCNN 里样本的单位是“候选区域”而不是“图片”。如果直接从所有图片的所有候选区域里随机抽 128 个区域来训练梯度那么这 128 个区域很可能来自 128 张不同的图片。每张图片的卷积层都要做一次完整的前向传播计算量极大。Fast-RCNN 的做法是随机抽 2 张图片然后从每张图片的候选区域里各抽 64 个区域组成一个 batch128 的训练单元。这样一次迭代只需要对 2 张图做卷积但梯度更新却基于 128 个候选区域的信息。在 PyTorch 里你只需要保证这两个图片的 ROI 特征可以拼成一个 batch 送进全连接层而卷积层是逐图前向的最后把梯度回传时按图累计即可。这里有一点很容易被新手忽略在接受 ROI Pooling 后的全连接层时来自不同图片、不同尺寸区域的 ROI 特征其 batch 维度可以拼在一起因为每个 ROI 的输出尺寸都是固定的 7x7xC。所以你可以把所有候选区域不管来自哪张图作为一个大 batch 输入全连接头训练代码实现起来非常简洁。Fast-RCNN 的训练还有个重要细节正负样本的比例要控制。每张图的 64 个候选区域中论文里建议最多 25% 是正样本与真实框 IoU 大于 0.5其余都是负样本。为什么要限制正样本比例因为一张给定的图片里真正包含对象的区域很少而背景区域极多。如果按自然比例采样负样本会淹没正样本模型会直接学会“所有区域都是背景”而且收敛极慢。把正样本比例强行拉高是为了让模型在训练时每步都能看到足够的实事例。我在写第一个检测模型的 dataloader 时最大的困惑就在这个采样逻辑——分类任务的 dataloader 是“按图片采”检测任务的 dataloader 是“按图片采再按区域采”两者的设计思路差了一整个维度。6. 两个时代的真实差距RCNN 与 Fast-RCNN 的选择依据我见过很多人问既然 Fast-RCNN 更快为什么还要学 RCNN直接上 Fast-RCNN 不就行了这个问题的答案很简单Fast-RCNN 是在 RCNN 的思想框架上做的优化不理解前者你很难理解后者为什么要做那些看似反常的设计。比如你不理解“CNN 特征提取可以共享”就理解不了 ROI Pooling 的意义你不理解“三阶段样本定义不一致”的问题就理解不了“端到端多任务训练”真正解决了什么。但作为实践者你大概率不会真的去训练一个 RCNN而是直接使用 Fast-RCNN 或 Faster-RCNN。这时候对比一下两个模型各自的特点能帮你在自己的项目里做选择对比维度RCNNFast-RCNN候选区域生成选择性搜索约 2000 个选择性搜索约 2000 个CNN 计算方式每个区域独立过网络整图过网络 ROI Pooling最终分类器每个类别一个 SVM单个 Softmax 头训练方式三阶段独立训练端到端联合训练边界框回归独立训练回归器与分类共享特征的回归头推理耗时当时评测集约 47 秒/张约 2 秒/张实现复杂度中等但流程繁琐中高需要处理 ROI 采样精度当时评测集 mAP约 58~66约 66~70对于自己的项目我一般这样判断如果只是一个快速验证想法的小实验比如检查“用预训练特征 线性分类器做检测的效果基线”RCNN 思路反而是最快出结果的——你甚至不需要修改 CNN 结构直接提取特征存下来就能调 SVM。很多传统视觉工程师转型初期喜欢先用 RCNN 思路做个基线再换 Fast-RCNN 提升性能。如果目标是做一个真正可用的检测服务那至少从 Fast-RCNN 起步因为它已经是统一训练的模型后续想换骨干网络、加数据增强、调候选区域数量都更加顺滑。更进阶的 Faster-RCNN 只是把“选择性搜索”换成了一个可训练的区域提议网络主干和 Fast-RCNN 几乎一样。从这个角度看Fast-RCNN 是整个系列里最核心的“分水岭模型”它比 RCNN 快得多又不具备 Faster-RCNN 那种“候选区域 检测联合训练”的复杂性最适合用来理解检测模型的整体骨架。7. 程序员训练这套模型时容易踩的五个坑最后这部分我集中写一些亲身踩过的坑。这些坑在论文里不会写在教程里容易被篇幅省略但每一个都足以让你的实验多耗两天。坑一候选区域生成方式决定了你的性能上限。如果你用选择性搜索生成候选区域一定检查它的召回率所有真实框里至少有 90% 以上被候选区域覆盖IoU 0.5后面模型的精度才有保障。候选区域的 IoU 阈值要和训练保持一致。如果候选区域本身就没框住对象后面 CNN 学得再好也白搭。我做过一个实验把候选区域数量从 2000 减到 500mAP 掉了 8 个点——不是模型变笨了而是漏检了。在代码里把候选区域可视化叠加在原图上检查一次比看指标曲线更有用。坑二调整候选区域数量时重新训练比改参数更重要。有些教程会把“候选区域数量”当作一个可以随便调的推理超参。其实不能这么理解。候选区域数量的变化会改变训练时正负样本的分布。比如你从 2000 个候选区域变成 100 个模型见过的负样本复杂度完全不同原来训练好的分类边界就不再适用。所以凡是改了候选区域生成逻辑无论是数量、算法还是阈值都要重新训练模型而不是只改推理代码。坑三ROI 坐标对齐不能被忽略。Fast-RCNN 的 ROI Pooling 里有量化和取整操作如果原图的坐标映射有问题边界框会偏移。常见错误是候选区域坐标是在缩放后的图片上生成的但你在 ROI Pooling 时用了原图坐标的尺度。一定要记住所有候选区域坐标都必须在同一坐标系下——要么都在原图坐标系、要么都在网络输入分辨率坐标系统一后再换算到特征图尺度。坐标单位/尺度不一致导致的检测框偏移是排查起来最隐蔽的 bug。坑四正负样本采样比例要显式控制。前文说了 Fast-RCNN 一个 batch 里正样本只占 25% 左右。但实际实现时还要注意一个细节每张图片的候选区域里正样本数量不足 25% 的情况很常见。比如图中只有 1 个对象只有 2 个候选区域与它 IoU 超过 0.5这时候你就不能严格凑够“16 个正样本”。处理办法是有多少用多少剩余的位置直接用负样本补齐。也就是说构造 batch 时要支持“正样本不足时自动降级”的逻辑否则程序会一直报错或者训练不收敛。坑五pretrain 模型和任务数据差异很大时要调整微调策略。Fast-RCNN 系列几乎都依赖预训练 CNN 做初始化。如果你的检测任务和预训练任务差异很大比如预训练是通用物体分类你的任务是识别工业零件直接对整个网络做微调前期 loss 会下降得极慢。一种稳妥的套路是先冻结卷积层只训练 ROI Pooling 之后的全连接头跑几个 epoch 之后再解冻卷积层用小学习率整体微调。这么做相当于先让检测头学会“利用已有特征”再让卷积层“配合检测头”。我跑工业零件检测时这个策略把训练时间省了将近一半最终的精度也比从零开始训高不少。RCNN 和 Fast-RCNN 在今天看来已经是“老古董”了但它们的很多设计细节在现在的 YOLO、Faster-RCNN 变体里依然能看到影子共享卷积特征、多任务损失、正负样本采样、非极大值抑制、边界框回归。我个人的体会是检测模型虽然有各种新版本但只要你把这一篇里这些核心概念的来龙去脉想清楚了后面看任何检测论文都会轻松很多。至少我在真正调通第一个 Fast-RCNN 模型之后再去看 Faster-RCNN明显感觉不再是“看天书”而是在看同一个故事的不同章节。