import torch
import torchvision.ops as ops
print('PyTorch version:', torch.__version__)PyTorch version: 2.11.0+xpu
jshn9515
2026-03-19
2026-04-04
在上一节中,我们介绍了 R-CNN。它的核心思想很直观:先通过 Selective Search 找到候选区域,再把每个候选区域分别送进 CNN 做特征提取,最后再进行分类和边界框回归。这个方法第一次成功地把深度学习引入目标检测,也显著提升了检测精度。
但是,R-CNN 有一个非常致命的问题:它太慢了。原因并不复杂,因为一张图像往往会生成大约 2000 个候选区域,而 R-CNN 需要对这 2000 个区域分别运行 2000 次 CNN 前向传播。虽然这些候选框大多来自同一张图像,彼此之间还存在大量重叠区域,但 R-CNN 并没有利用这种重叠带来的共享信息,而是重复地对相似区域做卷积计算。这就造成了极大的计算浪费。
所以,Fast R-CNN (Girshick 2015) 要解决的核心问题其实非常明确:既然这些候选区域都来自同一张图像,我们能不能只对整张图像做一次卷积,然后让所有候选区域共享这次卷积得到的特征?这正是 Fast R-CNN 的基本出发点。它不再对每个 proposal 单独运行 CNN,而是先对整张图像提取特征图,再从特征图中取出每个候选区域对应的部分进行分类和回归。这样一来,原来 R-CNN 中最昂贵的重复卷积计算就被大幅消除了,检测速度也因此显著提升。
更重要的是,Fast R-CNN 不仅提高了速度,还把分类和边界框回归统一进了同一个网络中,使目标检测开始朝着 端到端训练(end-to-end training) 的方向迈进。也就是说,它不仅比 R-CNN 更快,而且在训练方式上也更加自然、更加现代。这也就是为什么 Fast R-CNN 能够在当时引起如此大的关注,并成为目标检测领域的一个重要里程碑。
在接下来的内容中,我们就来详细看看,Fast R-CNN 是如何通过 共享卷积特征 和 RoI Pooling 来解决 R-CNN 的效率问题的。
PyTorch version: 2.11.0+xpu
我们知道,R-CNN 最大的问题在于它对每个 proposal 都要单独跑一次 CNN,导致大量重复计算。Fast R-CNN 的做法是把这个顺序反过来:先对整张图像做一次卷积,得到共享的特征图,再在特征图上处理各个 proposal。这样一来,最昂贵的卷积计算只做一次,所有候选区域都共享同一张特征图。
于是,Fast R-CNN 的主线就变成:
这里要注意,Fast R-CNN 并没有取消 proposal。它仍然依赖外部的候选区域方法,只是把“先裁剪 proposal,再分别跑 CNN”改成了“先整图卷积,再共享特征”。所以,它相对于 R-CNN 的关键改进有两点:
在这条主线里,最关键的一步是:候选区域大小各不相同,但后续网络希望看到固定大小的输入。那么,怎么把不同大小的 proposal 统一起来?这就要靠下面的 兴趣区域池化(Region of Interest Pooling, RoI Pooling)。
总的来说,RoI Pooling 的目标可以概括成一句话:
把任意大小的区域特征,转换成固定大小的区域特征。
对于特征图上的一个候选区域,RoI Pooling 不关心它原来有多大,而是先把它划分成固定数量的网格,再对每个网格做一次池化操作,通常是 max pooling。这样,不管输入区域是大还是小,最后都会得到同样大小的输出。
例如,如果我们规定输出始终是 7x7,那么:
最后,无论输入尺寸如何变化,输出都是固定形状的特征图。这样后续的全连接层就可以统一处理所有 proposal。
不过,RoI Pooling 也并不是完美的。它在实际操作中会引入一个问题:量化(quantization)误差。
原因在于,proposal 从原图映射到特征图时,坐标往往不是整数,需要取整。把 RoI 划分成固定网格时,每个网格的边界也可能不是整数,也需要取整。这样一来,原本连续的候选区域边界,在经过多次离散化之后,位置会出现偏差。对于分类任务来说,这种误差有时影响不大;但对于目标检测中的定位任务,特别是当我们希望边界框更加精确时,这种对齐误差就可能带来明显问题。
这也是为什么后来的 Mask R-CNN 中,会进一步引入更精确的 RoI Align (He et al. 2018)。不过在 Fast R-CNN 这一阶段,RoI Pooling 已经是一个非常关键、也非常成功的设计了。
x = torch.randint(0, 15, (1, 1, 4, 6), dtype=torch.float32)
# RoIs are (batch_idx, x1, y1, x2, y2). Here we use the whole image.
rois = torch.tensor([[0, 0, 0, 5, 3]], dtype=torch.float32)
# We want the output size to be (1, 1, 2, 3).
y = ops.roi_pool(x, rois, (2, 3)) # type: ignore
print('Input feature map:')
print(x, end='\n\n')
print('RoI Pooling output:')
print(y)Input feature map:
tensor([[[[11., 11., 11., 5., 0., 5.],
[12., 12., 7., 6., 7., 10.],
[ 1., 14., 8., 0., 1., 0.],
[ 1., 13., 11., 9., 10., 8.]]]])
RoI Pooling output:
tensor([[[[12., 11., 10.],
[14., 11., 10.]]]])
这里,我们输入的特征图大小是 4x6,假设我们的 RoI 覆盖整张图,我们希望输出的特征图大小是 2x3。那么,RoI Pooling 就会把输入的 4x6 区域划分成 2x3 个网格,每个网格大约是 2x2 的区域。然后在每个网格内取最大值,最终得到一个 2x3 的输出特征图。例如,RoI Pooling 的第一个数对应特征图左上角的那个 2x2 的区域,以此类推。
经过 RoI Pooling 之后,每个 proposal 都被转换成了固定大小的特征表示。接下来,网络需要回答两个问题:
Fast R-CNN 的一个重要改进,就是把这两个任务放进同一个网络里一起完成。
1. 分类输出:判断这个 RoI 属于什么类别
对于一个输入的 RoI,网络首先要判断它属于哪个类别。这里的类别并不只是我们真正关心的目标类别,比如“人”“猫”“汽车”,还要额外加上一个特殊类别,那就是背景。为什么要加背景类?因为并不是每个 proposal 都真的包含目标。Selective Search 生成的大量候选框中,有些确实覆盖了物体,有些则只是背景区域,或者只覆盖了物体的一部分。因此,网络必须先学会判断这个 proposal 是某个目标还是只是背景。
所以,如果数据集中一共有 \(K\) 个前景类别,那么 Fast R-CNN 的分类分支通常会输出 \(K + 1\) 个类别分数,其中多出来的就是背景类。随后,通过 softmax,就可以把这些分数转换成概率分布。网络最终会选择概率最大的类别,作为该 proposal 的预测类别。如果背景类概率最高,就说明这个 proposal 应该被当作无效候选框丢弃;如果某个前景类概率最高,就说明网络认为这里包含该类别的目标。
2. 边界框回归输出:进一步修正候选框位置
仅仅知道“这个区域里是什么”还不够,因为 proposal 通常只是一个粗略候选框,它和真实目标框之间往往还存在偏差。例如,一个候选框可能稍微偏左,稍微偏大,高度不够,或者宽度多了一点。所以,除了分类之外,网络还需要学习如何对 proposal 进行微调,让它更接近真实边界框。这就是 边界框回归(Bounding Box Regression) 的任务。
通常,我们会把一个候选框表示为 \((x, y, w, h)\) 的形式,其中 \((x, y)\) 是框的中心坐标,\(w\) 是宽度,\(h\) 是高度。对于每个 proposal,网络会输出一个 4 维的回归向量 \((t_x, t_y, t_w, t_h)\),表示对原始 proposal 的调整。它们不是直接预测真实框坐标,而是预测“应该怎样从 proposal 变换到更准确的框”。这种做法更稳定,也更容易学习。
一个常见的参数化方式是:
\[ t_x = \frac{x^* - x}{w}, \quad t_y = \frac{y^* - y}{h}, \quad t_w = \log\left(\frac{w^*}{w}\right), \quad t_h = \log\left(\frac{h^*}{h}\right) \]
其中,\((x^*, y^*, w^*, h^*)\) 是真实边界框的坐标。\(t_x\) 和 \(t_y\) 表示中心坐标的偏移,\(t_w\) 和 \(t_h\) 表示宽高的缩放。通过学习这个回归向量,网络就可以在推理阶段对 proposal 进行微调,使得最终的检测框更准确地覆盖目标。
此外,在 Fast R-CNN 中,边界框回归通常还是类别相关(class-specific)的。对于每个前景类别,网络都会预测一组单独的边界框修正参数。例如,如果有 20 个前景类别,那么回归分支就输出 80 个值。为什么这样设计?因为不同类别的物体,边界框修正模式可能并不完全一样。比如“人”“瓶子”“汽车”的几何形状差异较大,让每个类别学习自己的回归方式,往往会更灵活。当然,这也意味着参数量会增加一些。但在 Fast R-CNN 中,这样的设计通常是值得的。
不过,Fast R-CNN 虽然比 R-CNN 好很多,却并没有真正解决目标检测中的所有瓶颈。最主要的问题就是,它仍然依赖外部的 Region Proposal 方法。也就是说,Fast R-CNN 虽然把“对每个 proposal 重复做卷积”这个问题解决了,但 proposal 本身还是要靠 Selective Search 之类的传统方法提前生成。而 Selective Search 它本身就比较慢,而且它不属于神经网络的一部分,不能和后面的检测网络一起训练。因此,即使 Fast R-CNN 的网络前向传播已经快了很多,整个检测系统依然会被 proposal 生成这一步拖慢。
除了 proposal 仍然依赖外部方法,Fast R-CNN 还有一个更细节的问题,那就是 RoI Pooling 的量化误差。在前一小节中我们提到过,RoI Pooling 需要做两次离散化:一次是把原图中的 proposal 映射到特征图上,一次是把特征图上的区域划分成固定网格。在这两个过程中,连续坐标通常都要取整。这样一来,proposal 的边界位置就可能发生偏移,导致提取出来的特征和原始候选框并不能完全精确对齐。这个问题在 Fast R-CNN 阶段还没有被彻底解决,后来才由 RoI Align 进一步改进。
因此,Fast R-CNN 处在一个过渡但关键的位置,它证明了目标检测不应该由许多彼此分离的模块拼接而成,而应该尽量共享计算、统一训练。这其实也是后来很多检测框架共同遵循的思路。
在下一节中,我们将继续沿着这个思路,看看 Faster R-CNN 是如何进一步把 Region Proposal 也整合进网络中,让候选框生成也能跟随网络一起训练,在提高速度的同时进一步提升精度。