Faster R-CNN 通过与 RPN 共享特征图提高了整个检测过程的速度。然而,其第2阶段仍保留 Fast R-CNN 的处理手法,将数百区域逐一送入子网络。R-FCN 在 RoI 间亦共享特征,减少了区域处理的计算量。在采用 ResNet-101 作为基础网络时,测试速度为每张图像170毫秒,比 Faster R-CNN 对照模型快2.5~20倍。
图1. 共享卷积层对比
R-FCN 的网络结构
为了实现这一目标,R-FCN 提出了位置敏感的得分图,以解决图像分类中的平移不变性和物体检测中的平移变化之间的冲突问题。因而,R-FCN 可以自然地采用全卷积图像分类器骨干,例如最新的残差网络(ResNets),用于目标检测。
R-FCN 检测框架像 FCN 一样由共享、全卷积结构组成。为将平移变化引入 FCN 中,其使用一组特殊卷积层作为 FCN 的输出,构造出位置敏感得分图。每一个得分图编码了相对空间位置信息(例如,“目标的左侧”)。在这个 FCN 之上,附加一个位置敏感的 RoI 汇集层,它从这些得分图中输出信息,其后接全局池化层,没有权重(卷积/fc)层。整个架构是端到端学习的。所有可学习的层都是卷积并且在整个图像上共享,然后编码目标检测所需的空间信息。
图2:R-FCN 目标检测的核心思想。
ResNet-101 中的最后一个卷积块是2048-d,R-FCN 附加一个随机初始化的1024-d 1 × 1 1\times 1 1×1卷积层以减小维度。然后应用卷积生成 k 2 ( C + 1 ) k^2(C+1) k2(C+1) 通道的得分图。
Faster R-CNN 与 R-FCN 的网络结构对比如下图所示:
图3. 网络结构对比
假设基础网络输出的特征图维度为 C o u t × H × W C_{out}\times H\times W Cout×H×W,RoI Pooling 的输出尺寸为 k × k k\times k k×k。 则在第2阶段,Fast R-CNN 的输入为 N × C o u t × k × k N\times C_{out}\times k\times k N×Cout×k×k;而 R-FCN 为 1 × C o u t × H × W 1\times C_{out}\times H\times W 1×Cout×H×W。
基于区域的目标检测与图像分类网络
目标检测的常见深度网络系列(Fast R-CNN, SPPNet, Faster R-CNN)可以通过感兴趣区域(RoI)池化层分为两个子网络:
- 独立于 RoI 的共享、“全卷积”子网络;
- 不共享计算的 RoI-wise 子网络。
这种分解历史上来源于开创性的分类体系结构,例如 AlexNet 和 VGG 网络,它们由两个子网络组成—— 一个以空间池化层结束的卷积子网,后面是几个全连接(fc)层。因此,图像分类网络中的(最后)空间池化层自然地变成目标检测网络(Fast R-CNN, SPPNet, Faster R-CNN)中的 RoI 池化层。
但近期最先进的图像分类网络,如残差网络(ResNets)和 GoogLeNets(GoogLeNet, Inception-V3)是全卷积设计(只有最后一层是全连接,在目标检测微调时将其移除并替换)。通过类比,我们自然而然地使用所有卷积层在目标检测体系结构中构建共享的卷积子网,这使得 RoI-wise 子网没有隐藏层。然而,该领域的经验性研究表明,这种简单的解决方案所产生的较差的检测准确度与网络的较高分类准确度不匹配。为了解决这个问题,在 ResNet 论文中,Faster R-CNN 检测器将 RoI 汇集层以不太自然的方式插入到两组卷积层之间——这会创建一个更深的 RoI-wise 子网,由于每个 RoI 的计算非共享,以较低的速度为代价提高了准确性。
R-FCN 认为上述不自然的设计是由于图像分类增加平移不变性而物体检测关注平移变化的两难选择造成的:
- 一方面,图像级分类任务促成平移不变性——图像内部对象的移位应该是不加区分的。因此,深度(全)卷积体系结构尽可能是平移不变的,这可以通过ImageNet分类中的主要结果来证明 (ResNet, GoogLeNet, Inception-V3)。
- 另一方面,目标检测任务需要在一定程度上平移变化的定位表示。例如,候选框内目标的平移应该产生有意义的响应,用于描述候选框与对象重叠的程度。我们假设图像分类网络中较深的卷积层对平移不敏感。
为了解决这个难题,ResNet 论文的检测方案将 RoI 汇集层插入卷积中——这个特定区域操作打破了平移不变性,在评估不同区域时 RoI 后的卷积层不再是平移不变的。然而,这种设计牺牲了训练和测试效率,因为它引入了相当多的区域层(如下表所示)。
位置敏感的得分图与位置敏感的 RoI 池化
为了将位置信息显式编码到每个 RoI 中,R-FCN 通过规则网格将每个 RoI 矩形划分为 k × k k \times k k×k 个 区间。对于大小为 w × h w \times h w×h 的 RoI 矩形,区间的大小 ≈ w k × h k \approx \frac{w}{k} \times \frac{h}{k} ≈kw×kh (Fast R-CNN, SPPNet)。R-FCN 的最后一个卷积层为每个类别生成 k 2 k^2 k2 通道的得分图。在第 ( i , j ) (i,j) (i,j) 个区间( 0 ≤ i , j ≤ k − 1 0 \leq i,j \leq k-1 0≤i,j≤k−1),定义一个位置敏感的 RoI 池化操作,它只汇集第 ( i , j ) (i,j) (i,j) 个得分图:
r c ( i , j ∣ Θ ) = ∑ ( x , y ) ∈ bin ( i , j ) z i , j , c ( x + x 0 , y + y 0 ∣ Θ ) / n r_c(i,j ~|~ \Theta) = \sum_{(x,y)\in \text{bin}(i,j)} z_{i,j,c}(x+x_0, y+y_0 ~|~ \Theta)/n rc(i,j ∣ Θ)=(x,y)∈bin(i,j)∑zi,j,c(x+x0,y+y0 ∣ Θ)/n
这里 r c ( i , j ) r_c(i,j) rc(i,j) 是第 ( i , j ) (i,j) (i,j) 个区间对第 c c c 个类别的汇集响应, z i , j , c z_{i,j,c} zi,j,c 是 k 2 ( C + 1 ) k^2(C+1) k2(C+1) 个得分图中的一个, ( x 0 , y 0 ) (x_0, y_0) (x0,y0) 表示 RoI 的左上角, n n n 是区间中的像素数, Θ \Theta Θ 表示网络全部可学习的参数。第 ( i , j ) (i,j) (i,j) 个区间的跨度为 ⌊ i w k ⌋ ≤ x < ⌈ ( i + 1 ) w k ⌉ \lfloor i\frac{w}{k} \rfloor \leq x < \lceil (i+1)\frac{w}{k} \rceil ⌊ikw⌋≤x<⌈(i+1)kw⌉ 和 ⌊ j h k ⌋ ≤ y < ⌈ ( j + 1 ) h k ⌉ \lfloor j\frac{h}{k} \rfloor \leq y < \lceil (j+1)\frac{h}{k} \rceil ⌊jkh⌋≤y<⌈(j+1)kh⌉。相比于 Faster R-CNN 中的 RoI Pooling,这里每个 bin 在不同通道上池化,像一组不同深度的钻井平台,攫取不同通道的信息将其映射到空间位置上。汇集操作如图2.所示,其中一种颜色代表一对 ( i , j ) (i,j) (i,j)。公式执行平均池化,但也可以进行最大池化。
然后 k 2 k^2 k2 个位置敏感的分数在 RoI 上投票。在本文中,我们简单地通过平均得分进行投票,为每个 RoI 生成 ( C + 1 ) (C+1) (C+1) 维向量: r c ( Θ ) = ∑ i , j r c ( i , j ∣ Θ ) r_c(\Theta)=\sum_{i,j}r_c(i,j ~|~ \Theta) rc(Θ)=∑i,jrc(i,j ∣ Θ)。然后我们跨类别计算 softmax 响应: s c ( Θ ) = e r c ( Θ ) / ∑ c ′ = 0 C e r c ′ ( Θ ) s_c(\Theta)=e^{r_c(\Theta)} / \sum_{c'=0}^C e^{r_{c'}(\Theta)} sc(Θ)=erc(Θ)/∑c′=0Cerc′(Θ)。它们用于评估训练期间的交叉熵损失以及在推理期间对 RoI 进行排序。
R-FCN 以类似的方式进一步解决了边界框回归(R-CNN, Fast R-CNN)。除了上面的 k 2 ( C + 1 ) k^2(C+1) k2(C+1) 维卷积层,其为边界框回归追加一个并蒂的 4 k 2 4 k^2 4k2 维卷积层。位置敏感的 RoI 池化在这一 4 k 2 4 k^2 4k2 特征图集上执行,为每个 RoI 产生 4 k 2 4 k^2 4k2 维向量。然后通过平均投票将其汇总到 4 4 4 维向量中。这个 4 4 4 维向量根据 Fast R-CNN 中的参数化方法将边界框参数化为 t = ( t x , t y , t w , t h ) t=(t_x, t_y, t_w, t_h) t=(tx,ty,tw,th)。为简单起见,实现中执行未知类别的边界框回归,但同样适用于特定于类的对应物(即,具有 4 k 2 C 4 k^2 C 4k2C 维输出层)。
位置敏感得分图的概念部分受到 FCIS 的启发,它开发了用于实例级语义分割的 FCN。位置敏感的 RoI 池化层用于学习目标检测的得分图。在 RoI 层之后没有可学习的层,几乎可以无开销实现区域计算并加速训练和推理。
图4. R-FCN 的总体架构
区域提案网络(RPN)提出候选 RoI,然后将其应用于得分图。所有可学习的权重层都是卷积的,并在整个图像上计算;逐 RoI 的计算成本可以忽略不计。
可视化
在图5 和图6 中,当
k
×
k
=
3
×
3
k \times k = 3 \times 3
k×k=3×3 时,我们可视化由 R-FCN 学习的位置敏感得分图。期望在目标的特定相对位置强烈激活这些专用映射。 例如,“top-center-sensitive”分数图显示大致靠近目标顶部中心位置的高分。
如果候选框与真实目标精确重叠(图5.),则 RoI 中的大部分
k
2
k^2
k2 bin 被强烈激活,并且他们的投票产生高分。相反,如果候选框与真实目标没有正确重叠(图6.),则 RoI 中的某些
k
2
k^2
k2 bin 不会被激活,并且投票得分较低。
图5. R-FCN
k
×
k
=
3
×
3
k \times k = 3 \times 3
k×k=3×3 时预测人类别的可视化
图6. 当 RoI 未正确重叠目标时的可视化
Position Sensitive ROI Pooling 在 Caffe2 中的实现
PSRoIPoolOp
spatial_scale_
指输入特征图 X X X 相对于输入图像的空间比例。例如,如果 X X X 的步幅为16,则为0.0625。group_size_
为池化输出 Y Y Y 的高度(宽度)。output_dim_
为池化输出的通道数,可能是用于分类的类数,如果用于类不可知边界框回归,则为4。
没有CPU实现。
template <typename T, class Context>
class PSRoIPoolOp final : public Operator<Context> {
public:
PSRoIPoolOp(const OperatorDef& operator_def, Workspace* ws)
: Operator<Context>(operator_def, ws),
spatial_scale_(OperatorBase::GetSingleArgument<float>(
"spatial_scale", 1.)),
group_size_(OperatorBase::GetSingleArgument<int>("group_size", 1)),
output_dim_(OperatorBase::GetSingleArgument<int>("output_dim", 1)) {
DCHECK_GT(spatial_scale_, 0);
DCHECK_GT(group_size_, 0);
pooled_height_ = group_size_;
pooled_width_ = group_size_;
}
USE_OPERATOR_CONTEXT_FUNCTIONS;
bool RunOnDevice() override {
// No CPU implementation for now
CAFFE_NOT_IMPLEMENTED;
}
protected:
float spatial_scale_;
int group_size_;
int output_dim_;
int pooled_height_;
int pooled_width_;
int channels_;
int height_;
int width_;
};
PSRoIPoolGradientOp
spatial_scale_
和output_dim_
两个参数。
template <typename T, class Context>
class PSRoIPoolGradientOp final : public Operator<Context> {
public:
PSRoIPoolGradientOp(const OperatorDef& def, Workspace* ws)
: Operator<Context>(def, ws),
spatial_scale_(OperatorBase::GetSingleArgument<float>(
"spatial_scale", 1.)),
group_size_(OperatorBase::GetSingleArgument<int>("group_size", 1)),
output_dim_(OperatorBase::GetSingleArgument<int>("output_dim", 1)) {
DCHECK_GT(spatial_scale_, 0);
DCHECK_GT(group_size_, 0);
pooled_height_ = group_size_;
pooled_width_ = group_size_;
}
USE_OPERATOR_CONTEXT_FUNCTIONS;
bool RunOnDevice() override {
// No CPU implementation for now
CAFFE_NOT_IMPLEMENTED;
}
protected:
float spatial_scale_;
int group_size_;
int output_dim_;
int pooled_height_;
int pooled_width_;
int channels_;
int height_;
int width_;
};
PSRoIPoolOp<float, CUDAContext>::RunOnDevice()
输入:X,RoIs。
输出:Y,mapping_channel。
auto& X = Input(0); // Input data to pool
auto& R = Input(1); // RoIs
auto* Y = Output(0); // PSRoI pooled data
auto* A = Output(1); // mapping_channel
Y->Resize(R.dim32(0), output_dim_, pooled_height_, pooled_width_);
A->Resize(Y->dims());
调用 PSRoIPoolForward。
int output_size = Y->size();
PSRoIPoolForward<float><<<CAFFE_GET_BLOCKS(output_size),
CAFFE_CUDA_NUM_THREADS,
0, context_.cuda_stream()>>>(
output_size, X.data<float>(), spatial_scale_, X.dim32(1), X.dim32(2),
X.dim32(3), pooled_height_, pooled_width_, R.data<float>(), output_dim_,
group_size_, Y->mutable_data<float>(), A->mutable_data<int>());
PSRoIPoolForward
每个线程计算池化输出的一个点。
由索引获得其在输出中的位置。
CUDA_1D_KERNEL_LOOP(index, nthreads) {
// The output is in order (n, ctop, ph, pw)
int pw = index % pooled_width;
int ph = (index / pooled_width) % pooled_height;
int ctop = (index / pooled_width / pooled_height) % output_dim;
int n = index / pooled_width / pooled_height / output_dim;
RoI 边界取整后乘以空间尺度,获得其在特征图上的大小。
// [start, end) interval for spatial sampling
const T* offset_bottom_rois = bottom_rois + n * 5;
int roi_batch_ind = offset_bottom_rois[0];
T roi_start_w = static_cast<T>(
roundf(offset_bottom_rois[1])) * spatial_scale;
T roi_start_h = static_cast<T>(
roundf(offset_bottom_rois[2])) * spatial_scale;
T roi_end_w = static_cast<T>(
roundf(offset_bottom_rois[3]) + 1.) * spatial_scale;
T roi_end_h = static_cast<T>(
roundf(offset_bottom_rois[4]) + 1.) * spatial_scale;
强制太小的 ROI 为1x1。
// Force too small ROIs to be 1x1
T roi_width = max(roi_end_w - roi_start_w, 0.1); // avoid 0
T roi_height = max(roi_end_h - roi_start_h, 0.1);
计算每个 bin 桶的尺寸。
// Compute w and h at bottom
T bin_size_h = roi_height / static_cast<T>(pooled_height);
T bin_size_w = roi_width / static_cast<T>(pooled_width);
根据输出索引计算出在输入特征图上的位置并向外取整,修剪到输入边界。
第
(
i
,
j
)
(i,j)
(i,j) 个区间的跨度为
⌊
i
w
k
⌋
≤
x
<
⌈
(
i
+
1
)
w
k
⌉
\lfloor i\frac{w}{k} \rfloor \leq x < \lceil (i+1)\frac{w}{k} \rceil
⌊ikw⌋≤x<⌈(i+1)kw⌉ 和
⌊
j
h
k
⌋
≤
y
<
⌈
(
j
+
1
)
h
k
⌉
\lfloor j\frac{h}{k} \rfloor \leq y < \lceil (j+1)\frac{h}{k} \rceil
⌊jkh⌋≤y<⌈(j+1)kh⌉。
int hstart = floor(
static_cast<T>(ph) * bin_size_h + roi_start_h);
int wstart = floor(
static_cast<T>(pw)* bin_size_w + roi_start_w);
int hend = ceil(
static_cast<T>(ph + 1) * bin_size_h + roi_start_h);
int wend = ceil(
static_cast<T>(pw + 1) * bin_size_w + roi_start_w);
// Add roi offsets and clip to input boundaries
hstart = min(max(hstart, 0), height);
hend = min(max(hend, 0), height);
wstart = min(max(wstart, 0),width);
wend = min(max(wend, 0), width);
以下操作和 RoIPool 不同。
获得集合的大小及当前索引所对应输入的通道。
int gw = pw;
int gh = ph;
int c = (ctop * group_size + gh) * group_size + gw;
求 bin 中元素的均值。
r
c
(
i
,
j
∣
Θ
)
=
∑
(
x
,
y
)
∈
bin
(
i
,
j
)
z
i
,
j
,
c
(
x
+
x
0
,
y
+
y
0
∣
Θ
)
/
n
r_c(i,j ~|~ \Theta) = \sum_{(x,y)\in \text{bin}(i,j)} z_{i,j,c}(x+x_0, y+y_0 ~|~ \Theta)/n
rc(i,j ∣ Θ)=(x,y)∈bin(i,j)∑zi,j,c(x+x0,y+y0 ∣ Θ)/n
const T* offset_bottom_data =
bottom_data + (roi_batch_ind * channels + c) * height * width;
T out_sum = 0;
for (int h = hstart; h < hend; ++h){
for (int w = wstart; w < wend; ++w){
int bottom_index = h*width + w;
out_sum += offset_bottom_data[bottom_index];
}
}
T bin_area = (hend - hstart) * (wend - wstart);
top_data[index] = is_empty ? 0. : out_sum / bin_area;
mapping_channel[index] = c;
PSRoIPoolGradientOp<float, CUDAContext>::RunOnDevice()
输入:X,RoIs,Argmaxes,dY
输出:dX
auto& X = Input(0); // Input data to pool
auto& R = Input(1); // RoIs
auto& A = Input(2); // mapping channels
auto& dY = Input(3); // Gradient of net w.r.t. output of "forward" op
// (aka "gradOutput")
auto* dX = Output(0); // Gradient of net w.r.t. input to "forward" op
// (aka "gradInput")
在累积梯度之前必须将dX
清零。
调用 PSRoIPoolBackward kernel 函数。
dX->ResizeLike(X);
// Must zero-out dX before accumulating gradients
math::Set<float, CUDAContext>(
dX->size(), 0.f, dX->mutable_data<float>(), &context_);
PSRoIPoolBackward<float><<<CAFFE_GET_BLOCKS(dY.size()),
CAFFE_CUDA_NUM_THREADS,
0, context_.cuda_stream()>>>(
dY.size(), dY.data<float>(), A.data<int>(), R.dim32(0), spatial_scale_,
X.dim32(1), X.dim32(2), X.dim32(3), pooled_height_, pooled_width_,
output_dim_, dX->mutable_data<float>(), R.data<float>());
return true;
PSRoIPoolBackward
获得元素在输出中的索引。
CUDA_1D_KERNEL_LOOP(index, nthreads) {
// The output is in order (n, ctop, ph, pw)
int pw = index % pooled_width;
int ph = (index / pooled_width) % pooled_height;
int n = index / pooled_width / pooled_height / output_dim;
计算 RoI 在输入特征图上的起止范围。
// [start, end) interval for spatial sampling
const T* offset_bottom_rois = bottom_rois + n * 5;
int roi_batch_ind = offset_bottom_rois[0];
T roi_start_w = static_cast<T>(
roundf(offset_bottom_rois[1])) * spatial_scale;
T roi_start_h = static_cast<T>(
roundf(offset_bottom_rois[2])) * spatial_scale;
T roi_end_w = static_cast<T>(
roundf(offset_bottom_rois[3]) + 1.) * spatial_scale;
T roi_end_h = static_cast<T>(
roundf(offset_bottom_rois[4]) + 1.) * spatial_scale;
强制太小的 RoI 为1x1。
// Force too small ROIs to be 1x1
T roi_width = max(roi_end_w - roi_start_w, 0.1); //avoid 0
T roi_height = max(roi_end_h - roi_start_h, 0.1);
计算每个 bin 桶的大小。
// Compute w and h at bottom
T bin_size_h = roi_height / static_cast<T>(pooled_height);
T bin_size_w = roi_width / static_cast<T>(pooled_width);
根据输出索引计算出在输入特征图上的位置并向外取整,修剪到输入边界。
int hstart = floor(
static_cast<T>(ph)* bin_size_h + roi_start_h);
int wstart = floor(
static_cast<T>(pw)* bin_size_w + roi_start_w);
int hend = ceil(
static_cast<T>(ph + 1) * bin_size_h + roi_start_h);
int wend = ceil(
static_cast<T>(pw + 1) * bin_size_w + roi_start_w);
// Add roi offsets and clip to input boundaries
hstart = min(max(hstart, 0), height);
hend = min(max(hend, 0), height);
wstart = min(max(wstart, 0), width);
wend = min(max(wend, 0), width);
找到对应输入通道上的区域,使用 gpu_atomic_add 累加梯度。
bool is_empty = (hend <= hstart) || (wend <= wstart);
// Compute c at bottom
int c = mapping_channel[index];
T* offset_bottom_diff =
bottom_diff + (roi_batch_ind * channels + c) * height * width;
T bin_area = (hend - hstart) * (wend - wstart);
T diff_val = is_empty ? 0. : top_diff[index] / bin_area;
for (int h = hstart; h < hend; ++h){
for (int w = wstart; w < wend; ++w){
int bottom_index = h * width + w;
gpu_atomic_add(diff_val, offset_bottom_diff + bottom_index);
}
}
}
参考资料:
- R-FCN解读
- R-FCN:基于区域的全卷积网络来检测物体
- R-FCN源代码解读
- [译] 基于R-FCN的物体检测
- 目标检测论文笔记:R-FCN
- ROI Align 在 R-FCN 中的推广:PSROI-Align(附代码)
- 详解R-FCN
- 论文笔记 | R-FCN: Object Detection via Region-based Fully Convolutional Networks
- 基于R-FCN的物体检测
- R-FCN论文翻译——中文版
- R-FCN论文阅读(R-FCN: Object Detection via Region-based Fully Convolutional Networks )
- R-FCN:基于区域的全卷积网络来检测物体
- 解答关于R-FCN的所有疑惑(原创)