
最近把几年前做工业质检时写的一套检测代码翻出来重跑主干的思路还是 Fast R-CNN 那一套共享卷积特征图加 RoI Pooling最后挂两个头分别做分类和边框回归。很多人学目标检测是从 YOLO 系列入门的一上来就是 anchor、网格、多尺度预测反而把 Fast R-CNN 跳过去了。可只要你想真正理解“候选框”和“回归”这两个概念是怎么被塞进同一个网络里端到端训练的Fast R-CNN 是绕不开的一站。它没有推倒重来而是把 R-CNN 那套“每个候选框都过一遍卷积”的做法彻底改掉让候选框共享同一张特征图训练一次、前向一次剩下的交给 RoI Pooling 和一个多任务损失。这篇文章我打算按实际推演的思路走从它为什么出现、网络每一层在算什么、损失怎么捏合、参数怎么定一直讲到我自己踩过的坑和现场调参记录尽量让你看完能自己搭一个能跑通的版本而不是只记住几个名词。1. 先搞清楚 Fast R-CNN 到底解决了什么麻烦1.1 R-CNN 的三处硬伤每一处都在烧钱要理解 Fast R-CNN得先知道它前面那哥们儿有多笨重。R-CNN 的流程很直白用 Selective Search 在每张图上生成大约 2000 个候选区域然后把每个候选区域单独裁剪出来缩放成固定尺寸逐个送进卷积网络提取特征再用 SVM 做分类最后用一组线性回归器修正边框。流程听起来没毛病问题全在“逐个”这两个字上。一张图要跑 2000 次卷积网络前向而这 2000 个框里有大量重叠区域同一块像素被反复计算了几十遍算力几乎全浪费在重复劳动上。第二个问题是训练被切成了好几段。卷积特征提取、SVM 分类器、边框回归器是三套独立训练的东西中间还夹着一个“把特征缓存到磁盘”的步骤。VGG16 版本下2000 个框的特征向量存下来动辄几百 GBIO 直接成为瓶颈训练一轮的时间被磁盘读写拖得很难受。第三个问题是精度上的妥协因为卷积特征是离线缓存的SVM 和回归器在训练时无法反向影响卷积层整个网络没法端到端微调。你想调卷积核的参数得把前面的流程全部重跑一遍迭代成本高到不现实。把这三条摆在一起看测试慢、训练碎、不能端到端。这就是 Fast R-CNN 要一次性解决的目标而且它的解法不是小修小补而是把“每个框单独过网络”这条根给拔掉。1.2 SPP-Net 给的那把钥匙在 Fast R-CNN 之前SPP-Net 已经证明了一件事卷积特征图不必对每个候选框重算。它的做法是把整张图送进卷积网络得到一张共享的特征图然后对每个候选框在特征图上找到对应的区域用空间金字塔池化把它压成固定长度的向量。这解决了“重复计算”这一半问题测试速度提升非常明显。但 SPP-Net 留了尾巴。它的池化层是空间金字塔结构多尺度池化的设计在反向传播上比较别扭导致它没能把卷积层也一起微调起来——实际上 SPP-Net 只微调了全连接层卷积部分依旧是冻结的。结果就是精度没有比 R-CNN 提升太多训练流程也依然是多阶段的。Fast R-CNN 的聪明之处在于它把金字塔池化简化成一个固定尺度的 RoI Pooling 层输出的尺寸写死成 7×7反向传播路径清晰于是整个网络第一次真正做到了从前到后一起训练。这里有个取舍值得说清楚简化成单一尺度是会损失一点多尺度信息的但论文里的实验显示在深度网络上多尺度金字塔带来的增益远小于端到端微调带来的增益。换句话说与其保留一个更难训练的多尺度结构不如换成简单结构把反向传播打通。这个判断在当时是反直觉的但工程上极其正确。1.3 Fast R-CNN 的取舍留着候选框改掉整条流水线Fast R-CNN 的输入是“一张完整图片 一组候选框”输出是每个候选框的类别和精修后的坐标。它保留了 Selective Search 这类外部候选框生成算法把重活全放在后面的网络里。很多人会问为什么不干脆把候选框生成也做进网络里答案很实在那一步的工程改动量和当时的算力条件都撑不住更务实的做法是先把网络内部打通把端到端的收益拿到手。这个遗留问题后来被 Faster R-CNN 用 RPN 解决但 Fast R-CNN 的价值就在这一步——它把“候选框”从一个外部流程变成了网络内部可以消费的数据格式。我个人觉得 Fast R-CNN 最值得学的不是某个模块而是它的拆分逻辑把整个检测任务拆成“特征提取、区域特征对齐、多任务输出”三段段与段之间用可微的操作连接。这个结构后来被几乎所有两阶段检测器沿用包括你现在看到的 Mask R-CNN、Cascade R-CNN骨架都还在这个框架上。2. 网络结构逐层拆解与一张图的完整旅程2.1 从原图到共享特征图卷积只跑一遍假设输入是一张 600×800 的图经过 VGG16 的卷积部分得到一张大约 38×50、通道数 512 的特征图。注意这里的关键参数是下采样倍数strideVGG16 的卷积部分一共做了四次池化每次步长 2所以整体下采样 16 倍600/16≈37.5实际取整后是 38 左右。这个 16 倍的关系后面要用到很多次因为候选框是定义在原图坐标系上的要投影到特征图上必须除以这个倍数。这一层是整条链路里唯一一次对整张图做卷积运算也是速度提升的根本来源。R-CNN 要对 2000 个框各跑一遍这里只跑一遍理论上的计算量差距就在这个倍数上。当然实际加速比不会完全等于 2000因为候选框大小不一整图卷积的输入分辨率也更高但量级上的差别已经足够明显。注意不同主干的 stride 不一样。VGG16 是 16ResNet-50 在 conv4 阶段也是 16但如果你用的是更浅的网或者做过修改的网一定要把这个数字算准。它直接决定了 RoI Pooling 的 spatial_scale 参数填错了框会整体偏移而且是按比例偏移很难一眼看出来。2.2 RoI Pooling 到底做了什么RoI Pooling 是整个模型里最需要动手算一遍的模块。它的任务是把特征图上任意大小的一块区域压成固定尺寸比如 7×7 的输出。过程分两步先把候选框从原图坐标映射到特征图坐标方法是四个坐标全部除以 stride然后把映射后的区域均匀划分成 7×7 个格子每个格子内做最大池化。举个具体的数字。假设某个候选框在原图上是 (x1100, y1180, x2340, y2500)stride 是 16映射到特征图上变成 (6.25, 11.25, 21.25, 31.25)实际计算时会对齐到整数边界一般取整得到 (6, 11, 21, 31)。那么这个区域在特征图上的高度是 20宽度是 10。划分成 7×7 的格子后每个格子高度方向约 20/7≈2.86宽度方向约 10/7≈1.43因为不能划分出小数像素实际会用 ceil 取整比如高度方向每格取 3。取整之后会出现格子之间重叠或者区域略微超出边界的情况这就是所谓的量化误差。这个量化误差在目标比较大的时候影响有限但对于小目标就很要命几像素的偏移在高分辨率原图上可能对应几十个像素框的位置就对不准了。后来 Mask R-CNN 提出的 RoI Align 用双线性插值取代取整操作本质上就是在修这个问题。你在实现 Fast R-CNN 的时候如果数据集里小目标多值得直接换成 RoI Align 试试接口是兼容的。输出的 7×7×512 张量被展平成 25088 维向量接两个 4096 维的全连接层。注意展平这个操作意味着 RoI Pooling 的输出尺寸必须固定这也是为什么池化尺寸要写死——它后面跟着的是全连接层而全连接层对输入维度是敏感的。2.3 两条输出分支分类头与回归头穿过两个 4096 维全连接层之后网络分成两支。分类分支输出 K1 维向量K 是真实类别数多出来的那一维是背景类经过 softmax 得到每个类别的概率。回归分支输出 4×(K1) 维理论上每个类别都有自己的边框修正参数但实际训练时只有正样本对应的真实类别那一组参数会参与计算背景类那一组直接被忽略。为什么要给每个类别单独准备一套回归参数因为不同的类别在形状和尺度上的分布差异可能很大人脸的框修正逻辑和汽车的框修正逻辑并不通用。用共享的回归头也能跑但精度会掉一些尤其是在类别形态差异大的数据集上。代价是参数量变成 K1 倍如果你的类别数上百这一层的参数会占据相当可观的显存。这里还有个容易搞混的点R-CNN 时代用的是 SVM 做分类Fast R-CNN 换成了 softmax。论文里的实验数据显示softmax 的精度不但没掉反而略好于 SVM同时还省掉了单独训练 SVM 的麻烦。所以你现在看所有现代检测器分类头基本都是 softmax 或者 sigmoid 交叉熵没人再用 SVM 了。2.4 完整数据流的形状变化表把上面几段拼起来一张图的完整数据流可以整理成下面这样。这张表我在自己复现代码的时候贴在显示器边上每加一层就对照一下形状能省掉大量调试时间。阶段输入形状操作输出形状输入图3×600×800卷积主干512×38×50候选框映射2000×4除以 stride 并取整2000×4RoI Pooling2000 个变长区域7×7 均匀划分加最大池化2000×512×7×7展平2000×512×7×7reshape2000×25088全连接2000×25088两次 4096 维映射2000×4096分类头2000×4096线性层2000×(K1)回归头2000×4096线性层2000×4×(K1)这张表里的 2000 是候选框数量实际训练时不会把 2000 个框全部送进去算损失会做采样这部分放到第 3 章讲。推理阶段倒是可以把所有候选框都过一遍反正只做前向。3. 损失函数与训练策略多任务是怎么捏到一起的3.1 分类损失与定位损失的加权组合Fast R-CNN 的损失函数是整篇论文里最精妙的设计它把两个原本独立的训练目标合成了一个。形式很简洁总损失等于分类损失加上定位损失其中定位损失前面带一个指示函数只有当这个候选框属于正样本也就是和某个真实框的 IoU 超过阈值时才计入。用公式表达就是对每个 RoI分类部分用交叉熵定位部分用平滑 L1两者相加。关键在于那个指示函数它保证了背景框只贡献分类损失不会污染回归头的训练。如果不加这个约束让背景框也去回归一个目标位置网络会学到一堆毫无意义的坐标输出训练直接崩掉。权重系数我一般设成 1也就是两个损失等权。论文里也是 1。如果你在某个自定义数据集上发现定位精度明显偏低、分类精度过剩可以试着把定位损失的权重提到 1.5 或 2反之亦然。这算是个经验性的调参点没有理论最优解只能靠验证集试。3.2 Smooth L1为什么不用 L2定位损失用的是 Smooth L1而不是大家更熟悉的 L2 或者 L1。原因得从两者的缺点说起。L2 损失对误差是平方关系误差大的时候梯度也大遇到离群样本容易把训练带飞而且在检测任务里预测框和真实框差得远是常态前期几乎全是离群点。L1 损失倒是线性增长、梯度稳定但它在零点不可导且梯度恒定为常数误差很小的时候收敛速度慢精度上容易在最优解附近来回晃。Smooth L1 把两者拼了起来误差绝对值小于 1 的时候用平方保证小误差区间的平滑和快速收敛大于 1 的时候用线性抑制离群点带来的梯度爆炸。切换点那个 1 就是它的超参数一般不用改。实现上就是两个分支按条件选择代码就几行我在第 4 章会给出来。这个设计思路其实值得迁移到别的回归任务上。只要你做的是回归而且数据里存在明显的离群标注Smooth L1 或者它的近亲 Huber Loss 都是比 MSE 更稳的选择。3.3 边框回归的参数化与反解回归头直接预测四个绝对坐标很难收敛因为坐标值的分布随图像尺寸变化太大。Fast R-CNN 的做法是预测相对偏移相对于候选框的位置偏移和尺度缩放比都取对数。具体是四个量中心点 x 方向的偏移量等于真实框中心减候选框中心再除以候选框宽度y 方向同理宽度的缩放比是真实框宽除以候选框宽再取对数高度同理。取对数是为了让缩放关系对称——框放大两倍和缩小两倍在对数空间里是正负对称的网络学起来更均衡。推理的时候反过来解预测的中心点等于候选框中心加上预测偏移乘以候选框宽预测的宽等于候选框宽乘以偏移量的指数。这一步反解很容易写错尤其是坐标和宽高的对应关系我在第一次实现的时候把宽和高写反了结果框全部横竖颠倒画出来一眼就能看出来。建议写完立刻拿两三组数据手工验算一遍比对输入输出比盲目跑训练快得多。3.4 样本采样、超参数与微调范围训练时不会真的把 2000 个候选框全部拿来算损失而是每张图抽 64 个。抽取规则是和任意真实框 IoU 大于等于 0.5 的算正样本随机抽 25%也就是 16 个剩下的 75% 从 IoU 落在 0.1 到 0.5 之间的框里抽取。注意下限是 0.1 而不是 R-CNN 用的 0.3这么设是为了保证负样本足够多同时保留一些“难例”负样本。IoU 低于 0.1 的框基本和真实目标没交集对训练没有信息量通常直接排除。论文里的 batch 是两张图合起来每个 batch 处理 128 个 RoI。这个 batch size 小得可怜因为 VGG16 显存占用高。这也带来一个问题批量归一化在小 batch 下统计量不稳所以原论文用的 VGG16 根本没加 BN 层。你自己复现时如果换了带 BN 的主干要么冻结 BN 的统计量要么把 batch 调大否则训练会明显不稳。学习率策略上初始 0.001跑完 3 万次迭代后降到 0.0001再跑 1 万次。动量 0.9权重衰减 0.0005。数据增强只做了水平翻转。微调范围是 VGG16 从 conv3_1 开始的后半部分前面的层冻结。冻结前面的层是因为浅层学的是边缘、纹理这类通用特征数据量不够的时候强行微调反而会破坏预训练权重带来的收益。注意正负样本比例 1:3 是个经验值不是铁律。如果你的数据集里目标特别密集正样本本来就多可以适当提高正样本比例如果是那种一张图只有一两个小目标的场景1:3 可能都不够得往下压负样本比例否则网络容易退化成“全预测背景”。3.5 用 SVD 砍掉全连接层的计算量Fast R-CNN 在推理速度上的一个附加优化是截断 SVD。全连接层 fc6 的参数量是 25088×4096接近一个亿在推理时这一层耗时占比很高。做法是对这一层的权重矩阵做奇异值分解保留前 1024 个奇异值把一层大矩阵乘法拆成两层小矩阵乘法参数量降到原来的一个零头实测几乎不掉精度速度有可观提升。这个技巧放到今天其实依然有用。如果你在部署边缘设备模型尾部的大全连接层往往是可以压缩的重点区域。不过要注意如果你后面改成了全卷积结构这个优化点就不存在了所以它算是一个跟特定结构绑定的优化。4. 动手跑一遍从数据准备到推理可视化4.1 数据准备与格式对齐数据这块最容易出问题的不是算法是格式。我用的是 VOC 格式的标注每张图对应一个 xml里面记录若干目标框的类别和坐标。实际喂给模型前要转成两个东西一个是图片路径列表一个是每张图对应的 N×5 张量五列分别是类别索引和四个坐标。坐标顺序我统一用左上右下也就是 x1, y1, x2, y2别混用中心点格式混了之后报错会很难查。这里有个细节值得强调图像的预处理要和预训练权重匹配。VGG16 在 ImageNet 上训练时用了均值减除和特定的归一化参数如果你在微调时换了一套归一化浅层特征分布对不上收敛会慢很多。我的做法是直接把 torchvision 里对应权重的预处理参数抄过来写成一个 transforms 流水线避免手写错。数据集划分上我习惯留 10% 做验证集并且保证验证集里的类别分布和训练集接近。小数据集上尤其要注意这点否则验证 mAP 抖动会非常大你根本判断不了模型是在进步还是在随机波动。4.2 骨架搭建与 RoI Pooling 实现骨架部分我直接用 torchvision 的 VGG16取它的 features 部分作为卷积主干把后面的 avgpool 和 classifier 丢掉。然后自己接 RoI Pooling、全连接和两个头。代码结构大致如下。import torch import torch.nn as nn import torchvision from torchvision.ops import roi_pool class FastRCNN(nn.Module): def __init__(self, num_classes21, pooled_size7, spatial_scale1/16): super().__init__() backbone torchvision.models.vgg16( weightstorchvision.models.VGG16_Weights.IMAGENET1K_V1 ) self.features backbone.features self.pooled_size pooled_size self.spatial_scale spatial_scale self.fc6 nn.Linear(512 * pooled_size * pooled_size, 4096) self.fc7 nn.Linear(4096, 4096) self.cls_score nn.Linear(4096, num_classes) self.bbox_pred nn.Linear(4096, num_classes * 4) self.dropout nn.Dropout(0.5) self.relu nn.ReLU(inplaceTrue) def forward(self, images, rois): # images: (N, 3, H, W) rois: (K, 5) 第一列是 batch 索引 feat self.features(images) pooled roi_pool(feat, rois, output_size(self.pooled_size, self.pooled_size), spatial_scaleself.spatial_scale) x pooled.flatten(1) x self.relu(self.fc6(x)) x self.dropout(x) x self.relu(self.fc7(x)) x self.dropout(x) cls_logits self.cls_score(x) bbox_deltas self.bbox_pred(x) return cls_logits, bbox_deltas有几处值得展开说。roi_pool 的输入要求是 (N, C, H, W) 的特征图和 (K, 5) 的框第一列必须是 batch 索引很多人第一次用的时候忘了这一列直接丢四列坐标进去形状校验会直接报错。spatial_scale 就是 stride 的倒数这里 VGG16 是 1/16。另外注意 roi_pool 这个接口在新版 torchvision 里被标记为推荐改用 roi_align但依然可用。如果你追求更好的小目标表现直接把函数名换成 roi_align参数里去掉 spatial_scale 换成 sampling_ratio其余不用动。4.3 训练循环与关键参数记录训练循环里最绕的是样本采样和损失计算。我拆成两步先算每个候选框和真实框的 IoU然后按 0.5 和 0.1 两个阈值分类正负正样本按 25% 比例采样。采样时如果正样本不够要用负样本补齐保证每个 batch 的样本数固定否则不同 batch 的损失尺度不一致。def smooth_l1_loss(pred, target, beta1.0): diff (pred - target).abs() return torch.where(diff beta, 0.5 * diff ** 2 / beta, diff - 0.5 * beta)上面这个实现里 beta 取 1和论文一致。注意它返回的是逐元素损失最后要做一个归约归约的对象一般是有正样本的维度数不是全部元素这一点论文里特别强调过用错误的归约方式会导致损失数值量级不对学习率就得跟着重调。训练记录方面我建议同时盯三个量总损失、正样本数量、平均 IoU 最大的候选框分数。第一个判断训练是否在推进第二个判断采样是否正常第三个判断分类头有没有在学到东西。我碰到过一次正样本数量长期为零的诡异情况原因是我把标注框的坐标写成了中心点格式IoU 算出来全是错的表面上损失还在缓慢下降实际上网络只是在学背景。超参数我沿用了论文的配置学习率 0.001 起步3 万次迭代后降到 0.0001再 1 万次收尾动量 0.9权重衰减 0.0005。数据增强只做水平翻转。在 VOC07 这种规模的数据集上单卡跑完一轮大概几个小时。4.4 推理后处理NMS 与阈值调优推理阶段输出的是每个候选框在每个类别上的得分和偏移量。后处理分三步走。第一步按类别分别取分只保留得分超过阈值的框背景类直接丢掉。第二步用预测的偏移量把候选框还原成检测框同时裁剪到图像边界内防止框超出画面。第三步做非极大值抑制把同一个目标上的重复框合并掉。NMS 的阈值我用 0.3和论文一致。这个值偏保守意味着只要有 30% 重叠就认为是同一个目标会合并掉。如果你的场景里有大量密集重叠的小目标这个阈值得往上调比如 0.5 甚至 0.6否则挨得近的目标会被误合并成一个。反过来如果是稀疏场景0.3 能有效压掉重复框。得分阈值我在不同数据集上试过 0.05 到 0.5 的范围差异挺大。小目标多的时候阈值要压低因为小目标的分类得分普遍偏低阈值一高就全被滤掉大目标为主的时候可以提到 0.3 以上能省掉大量无效框后处理速度也会快一些。这个参数没有通用最优值只能拿验证集画 PR 曲线来选。4.5 实测数据与速度对比我在 VOC07 上跑的结果用 VGG16 主干单尺度 600 像素mAP 大概落在 66 到 67 这个区间和论文报的 66.9 基本吻合。如果换成 VOC0712 的联合训练集能到 70 左右。同条件下换成 CaffeNet 这种轻量主干mAP 会掉到 58 上下但速度会快好几倍适合对精度要求不那么苛刻的场景。速度方面完整流程里最耗时的其实是候选框生成那一步因为 Selective Search 是纯 CPU 算法和 GPU 上的网络推理没法并行。网络本身的前向在 GPU 上只要零点几秒但加上候选框生成单张图的整体耗时会被拉长不少。这也是为什么后面 Faster R-CNN 要把候选框生成也做成 GPU 上的网络——那一步才是真正的瓶颈。5. 踩坑记录常见故障与排查思路5.1 训练侧loss 不动、震荡、假收敛损失完全不下降我遇到过三种原因。第一种是学习率太大前期直接发散损失先是暴涨然后变成 nan解决方案是把初始学习率降到 0.0001 试一轮看损失是否下降。第二种是标签格式不对比如类别索引从 1 开始而背景类占了 0导致所有样本的标签整体错位表现就是损失能降但降到一个很高的平台期就不动了。第三种是数据预处理和预训练权重不匹配浅层特征分布偏移表现为损失缓慢下降但验证集精度上不去。损失震荡的情况多半出在采样上。如果某个 batch 恰好正样本特别多损失会突然抬高下一个 batch 全是负样本损失又掉下去。解决办法是在采样时做强约束保证每个 batch 的正样本数量恒定采样不到就用负样本补。我改完之后损失曲线明显平滑了很多。还有一种比较隐蔽的“假收敛”损失在下降但验证集 mAP 一动不动。这通常是模型学会了走捷径比如所有框都预测成同一类。查的办法是统计预测结果的类别分布如果某个类别的占比超过 80%基本可以确认。原因往往是类别不均衡少数类样本太少需要加类别权重或者做过采样。5.2 检测侧漏检、重复框、框偏移漏检最常见的原因不是模型不行是后处理阈值设太高。尤其是小目标得分普遍在 0.1 到 0.3 之间你把阈值卡在 0.5等于把这些目标全部筛掉了。排查方法是把阈值降到 0.02画一下所有框看看有没有被漏掉的目标其实模型已经检出来了只是分数低。重复框的问题有两个来源。一是 NMS 阈值太高相邻的两个真实目标被当成同一类或者同一个目标的两个框重叠度不够被保留下来这时候要调低 NMS 阈值。二是回归头输出的偏移量异常导致同一个目标被修正出两个距离很远的框这种情况要看回归损失的收敛情况如果回归损失一直很高说明回归头没学好。框整体偏移是比较典型的坐标映射问题。如果所有框都往一个方向平移了固定的距离八成是 stride 算错了或者映射时忘记减掉特征图的偏移量。如果框的大小系统性偏小可能是回归反解时把宽高的指数运算写错了。我建议写一个可视化脚本把候选框、修正后的框、真实框用不同颜色画在一张图上一眼就能看出是哪个环节的问题比看指标快得多。5.3 工程侧显存、速度与部署显存不够是最常见的工程问题。Fast R-CNN 的显存大头在全连接层的激活值上每个 RoI 经过两个 4096 维全连接层激活值非常占空间。减小显存的办法有几个减少每张图的采样 RoI 数量比如从 64 降到 32用截断 SVD 压缩全连接层或者直接把主干的浅层冻结减少反向传播时的激活保存量。我用第二个办法把显存占用压下来大约三成。速度优化上除了 SVD 之外还可以把 RoI Pooling 的输出尺寸从 7×7 降到 6×6 或者 5×5全连接层的输入维度会平方级下降速度快很多代价是精度会掉一点点。另外把图像短边从 600 降到 480速度提升也很明显但小目标的检测精度损失比较大得权衡。部署时要注意的一点是候选框生成这一步。如果整个流程都在服务端问题不大如果要做端侧部署Selective Search 这种 CPU 密集算法会很拖后腿通常的做法是用一个轻量的候选框生成网络替代它或者干脆换成单阶段检测器的思路。5.4 常见问题速查表现象可能原因排查方法处理方式损失变 nan学习率过大或标签越界打印每个 batch 的损失和标签范围降低学习率检查标签索引损失降但验证不涨类别不均衡或走捷径统计预测类别分布加类别权重重采样所有框整体偏移stride 参数错误对比映射前后坐标修正 spatial_scale框大小系统性偏差回归反解公式写错手工验算两三组坐标检查指数与乘除关系小目标大量漏检得分阈值过高把阈值降到 0.02 复看降低阈值或换 RoI Align密集目标被合并NMS 阈值过低可视化 NMS 前后框数量调高 NMS 阈值显存溢出RoI 数量过多打印各层激活形状减少采样数或加 SVD训练速度极慢候选框生成在 CPU分别计时各阶段考虑端到端候选框生成这张表基本覆盖了我这两年调这类模型时遇到的大部分情况留着当速查用还是比较省事的。6. 这套框架的边界在哪里6.1 两个没有解决的问题Fast R-CNN 留下的第一个坑是候选框生成依然是外部的、CPU 上的、不可学习的。这导致两个后果一是推理时延被这一步卡住二是候选框的质量直接决定检测上限网络再强也救不回漏掉的候选框。第二个坑是 RoI Pooling 的量化误差对小目标和需要像素级对齐的任务不友好这个问题直到 RoI Align 出现才算彻底解决。这两个问题不是实现瑕疵而是架构层面的边界。你如果要做的是高精度、小目标密集的检测任务光靠 Fast R-CNN 的原始结构是不够的得在候选框生成和对齐方式上动刀。6.2 后续演进路线的对照从 Fast R-CNN 往后看几条主线都很清楚。候选框生成被 RPN 收进网络成了 Faster R-CNN特征对齐被 RoI Align 修正配合分割分支成了 Mask R-CNN检测头的结构被反复堆叠成级联处理 IoU 阈值不匹配的问题再往后单阶段检测器把候选框这一步整个绕开用密集预测加回归的方式做检测速度上去了小目标精度靠特征金字塔补回来。近几年 Transformer 结构也被引入检测任务把候选框和特征对齐都变成了注意力计算思路完全不同但你要理解那些新结构里“查询”和“键值”在做什么回头看看 RoI Pooling 在把空间区域压成向量这件事上扮演的角色会很有帮助。我个人的体会是Fast R-CNN 这套东西最大的价值不在于它现在的精度而在于它把检测任务的几个核心矛盾——共享计算、区域特征对齐、多任务联合训练——第一次用一套相对干净的工程方案表达出来了。你把这些矛盾理解了再去看后来的任何检测器都会觉得只是在不同的权衡点上做选择。实际动手复现一遍哪怕只是跑通一个简化版本对理解后续模型的帮助比读十篇综述都大。