查看全课大纲(第 62 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

R-FCN的实现

小象实战讲义 · 基于深度学习的计算机视觉

在上一节中,我们探讨了R-FCN的设计思路,其核心目标是解决Faster R-CNN中ROI-wise子网络(如全连接层)无法共享计算的问题。本节我们将深入R-FCN的具体实现细节,理解其如何通过“位置敏感得分图”和“位置敏感ROI池化”这两个关键设计,构建一个几乎完全由卷积层组成的、计算高度共享的目标检测网络。学完本节,你将能清晰地阐述R-FCN的完整架构、训练流程,并理解其性能提升背后的原理。

💡 核心导读

  • 网络架构:基于 ResNet-101,在共享卷积上并行接出 RPN 与位置敏感得分图(RPN 接 conv4,得分图接 res5 经 1×1 降维后的 1024 通道特征)。
  • 位置敏感得分图:使用 k²(C+1) 个通道的卷积层,显式编码目标的类别及其在ROI网格中的相对位置信息。
  • 位置敏感ROI池化:将每个ROI划分为 k×k 个网格(bin),每个bin只从对应位置和类别的得分图中提取特征并进行均值池化,最后通过投票确定类别。
  • 损失函数与训练:采用分类与回归的多任务损失,并引入在线难例挖掘(OHEM)和类似Faster R-CNN的四步交替训练法提升性能。

网络整体结构

R-FCN的整体结构设计旨在最大化卷积计算的共享。其主干网络采用了在ImageNet上预训练好的ResNet-101。

这里有一个重要的预处理步骤:ResNet-101 原本在卷积层之后会接全局平均池化(GAP)和 1000 维全连接层用于 ImageNet 分类,R-FCN 则要保留空间信息。具体做法是:保留 res5(conv5)阶段的全部卷积,但把该阶段首层的 stride 由 2 改为 1,并配合空洞卷积,使总步长由 32 降到 16(即 1/16 分辨率的特征图);res5 输出为 2048 通道,再追加一个随机初始化的 1×1 卷积将其降为 1024 通道。RPN 则接在与检测分支共享的 conv4 特征上。这样既保留了空间信息,也控制了后续分支的计算量。

降维后的 1024 维特征图(来自 res5/conv5,步长 16)同时输入两个位置敏感得分图分支;RPN 不在这条支路上,它直接接在与检测分支共享的 conv4 特征(同样 1024 通道、1/16)上:

  1. 位置敏感得分图(用于分类):通过一个 k²(C+1) 通道的卷积层生成。其中 C 是目标类别数,k 是划分的网格大小(例如3)。这部分是R-FCN的核心创新。
  2. 位置敏感得分图(用于回归):通过一个 4k² 通道的卷积层生成,用于边界框的精修(回归)。

整个结构是端到端的,所有需要学习的参数(权重)对于整张图像而言都是共享的。

位置敏感得分图与ROI池化

这是R-FCN最核心、最巧妙的设计,它解决了“如何让ROI-wise的操作也共享计算”的难题。

位置敏感得分图

首先,我们通过一个特殊的卷积层,从1024维的特征图上生成 k²(C+1) 个通道的“位置敏感得分图”。

  • C+1:代表 C 个物体类别加上1个背景类。
  • k²:代表我们将每个候选区域(ROI)在概念上划分成一个 k×k 的均匀网格。通常 k=3 或 7。

那么,这 k²(C+1) 个通道具体代表什么呢?每个通道都编码了“特定类别”在“ROI网格中特定位置”的响应强度。

举例来说,假设我们有 k=3,类别为“人”(person)。那么对于“人”这个类别,就会生成9个得分图:

  • 第1个图:专门对“人”的左上部位敏感。
  • 第2个图:专门对“人”的中上部位敏感。
  • …
  • 第9个图:专门对“人”的右下部位敏感。

这9张图与主干输出的特征图尺寸相同(约为输入图像的1/16)。网络训练的目标就是:当一张人像图片输入时,在“人-左上”得分图上,人的头部区域会呈现高响应值;在“人-右下”得分图上,人的脚部区域会呈现高响应值。

位置敏感ROI池化

当RPN给出一个候选区域(ROI)后,我们如何利用上面那一大堆得分图来判断这个ROI里是什么物体呢?这就需要“位置敏感ROI池化”。

  1. 网格划分:将当前的ROI区域同样划分成 k×k 个网格(称为bin),与得分图的设计相对应。
  2. 池化操作:对于每个bin (i, j)(例如左上角的bin是 (1,1)),我们只从与之对应的那个得分图通道里提取特征。
    • 具体来说,对于类别 c,我们找到第 c 组得分图中的第 (i, j) 个图。
    • 在这个bin覆盖的区域内,对该得分图进行均值池化,得到一个标量值 r_c(i, j)。这个值可以理解为“当前ROI在 (i, j) 这个位置,看起来像类别 c 的对应部位”的置信度。
  3. 投票与分类:对每个类别 c,我们都得到了 k×k 个值 {r_c(i, j)}。论文将这 k×k 个值求和,得到该类别的总体得分 s_c。
    s_c = \sum_{i=1}^{k} \sum_{j=1}^{k} r_c(i, j)
    
    然后,对所有 C+1 个类别的得分 {s_c} 进行softmax归一化,得到概率分布。概率最高的类别即为该ROI的预测类别。(下面演示代码为便于报告数值取平均,与求和只差常数因子 1/k²:各类别得分的相对大小与argmax预测类别不变,但softmax概率数值与损失值会随之改变。)

这个过程为什么有效?

  • 如果一个ROI框得准,并且里面确实是一个人,那么“人-左上”图在ROI的左上bin区域响应会高,“人-右下”图在ROI的右下bin区域响应也会高。所有bin的得分都高,平均后 s_person 自然就高。
  • 如果一个ROI框歪了,或者里面是别的物体,那么某些部位对不上,对应的得分图在相应bin内响应就会低,从而拉低平均分,使得该类别的最终得分降低。

用于边界框回归的分支原理类似。它生成 4k² 个通道的得分图,分别编码了边界框中心点 (x, y) 和高宽 (w, h) 在 k×k 个位置上的偏移量。回归时,同样采用位置敏感的ROI池化,在每个bin内池化后,再对 k×k 个结果求平均,得到最终的4个回归参数。

下面我们用一个小规模的模拟代码来直观感受一下位置敏感ROI池化的计算过程。假设我们有一个极其简化的场景。

import numpy as np

# 模拟参数
k = 3  # 网格大小 3x3
C = 1  # 假设只有1个类别(不含背景)
score_maps_channels = k * k * (C + 1)  # 实际应为 3*3*2=18 个通道,这里为简化,我们只模拟对一个类别的9个图

# 1. 模拟生成9张位置敏感得分图 (每张图尺寸 6x6)
# 为了演示,我们手工构造:每张图只在“该出现”的位置有一个高亮块。
np.random.seed(42)
score_maps = {}
# 假设高亮块是值为10,背景为0
base_map = np.zeros((6, 6))
for i in range(k):
    for j in range(k):
        # 创建一张新图
        map_ij = base_map.copy()
        # 假设这个类别物体的第(i,j)部分应该出现在整张图的特定区域(例如,左上部分对应图的左上角)
        # 这里我们简单地让第(i,j)张图在坐标(i*2, j*2)附近有一个2x2的高亮块
        start_x, start_y = i * 2, j * 2
        map_ij[start_x:start_x+2, start_y:start_y+2] = 10
        score_maps[(i, j)] = map_ij
        print(f"得分图({i},{j}) - 高亮块位置 ({start_x}:{start_x+2}, {start_y}:{start_y+2}):")
        print(map_ij.astype(int))
        print()

# 2. 模拟一个ROI (左上角坐标(x0,y0), 高h, 宽w)
# 情况A: ROI框得很准,覆盖了所有高亮块
roi_A = {'x0': 0, 'y0': 0, 'w': 6, 'h': 6}
# 情况B: ROI框歪了,只覆盖部分高亮块
roi_B = {'x0': 1, 'y0': 1, 'w': 4, 'h': 4}

def position_sensitive_roi_pooling(score_maps_dict, roi, k=3):
    """执行简化的位置敏感ROI池化(均值池化)"""
    pooled_values = np.zeros((k, k))
    roi_x0, roi_y0, roi_w, roi_h = roi['x0'], roi['y0'], roi['w'], roi['h']
    bin_w, bin_h = roi_w / k, roi_h / k
    
    for i in range(k):
        for j in range(k):
            # 当前bin在输入图像上的坐标范围
            bin_x_start = roi_x0 + j * bin_w
            bin_y_start = roi_y0 + i * bin_h
            bin_x_end = bin_x_start + bin_w
            bin_y_end = bin_y_start + bin_h
            # 简化处理:论文中 bin 边界按特征图尺度取整后在离散位置上做均值池化;
            # 亚像素精度的双线性插值是后续 RoIAlign(Mask R-CNN)的改进
            bin_x_start, bin_y_start = int(bin_x_start), int(bin_y_start)
            bin_x_end, bin_y_end = int(bin_x_end), int(bin_y_end)
            
            # 取出对应的得分图
            map_ij = score_maps_dict[(i, j)]
            # 取出bin区域
            bin_region = map_ij[bin_y_start:bin_y_end, bin_x_start:bin_x_end]
            # 均值池化
            if bin_region.size > 0:
                pooled_values[i, j] = np.mean(bin_region)
            else:
                pooled_values[i, j] = 0
    return pooled_values

print("="*50)
print("情况A: ROI准确覆盖(0,0,6,6)")
pooled_A = position_sensitive_roi_pooling(score_maps, roi_A, k)
print("池化后的 k x k 网格值:")
print(pooled_A)
print(f"类别得分(9个值的平均): {np.mean(pooled_A):.2f}")

print("\n" + "="*50)
print("情况B: ROI偏移(1,1,4,4)")
pooled_B = position_sensitive_roi_pooling(score_maps, roi_B, k)
print("池化后的 k x k 网格值:")
print(pooled_B)
print(f"类别得分(9个值的平均): {np.mean(pooled_B):.2f}")
得分图(0,0) - 高亮块位置 (0:2, 0:2):
[[10 10  0  0  0  0]
 [10 10  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]]

得分图(0,1) - 高亮块位置 (0:2, 2:4):
[[ 0  0 10 10  0  0]
 [ 0  0 10 10  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]]

得分图(0,2) - 高亮块位置 (0:2, 4:6):
[[ 0  0  0  0 10 10]
 [ 0  0  0  0 10 10]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]]

得分图(1,0) - 高亮块位置 (2:4, 0:2):
[[ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [10 10  0  0  0  0]
 [10 10  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]]

得分图(1,1) - 高亮块位置 (2:4, 2:4):
[[ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0 10 10  0  0]
 [ 0  0 10 10  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]]

得分图(1,2) - 高亮块位置 (2:4, 4:6):
[[ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0 10 10]
 [ 0  0  0  0 10 10]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]]

得分图(2,0) - 高亮块位置 (4:6, 0:2):
[[ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [10 10  0  0  0  0]
 [10 10  0  0  0  0]]

得分图(2,1) - 高亮块位置 (4:6, 2:4):
[[ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0 10 10  0  0]
 [ 0  0 10 10  0  0]]

得分图(2,2) - 高亮块位置 (4:6, 4:6):
[[ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0  0  0]
 [ 0  0  0  0 10 10]
 [ 0  0  0  0 10 10]]

==================================================
情况A: ROI准确覆盖(0,0,6,6)
池化后的 k x k 网格值:
[[10. 10. 10.]
 [10. 10. 10.]
 [10. 10. 10.]]
类别得分(9个值的平均): 10.00

==================================================
情况B: ROI偏移(1,1,4,4)
池化后的 k x k 网格值:
[[10.  10.   5. ]
 [10.  10.   5. ]
 [ 5.   5.   2.5]]
类别得分(9个值的平均): 6.94

从模拟结果可以清晰看到:

  • 当ROI(roi_A)完美覆盖所有高亮部位时,每个bin都能从对应的得分图中提取到完整的响应值(10),平均得分很高(10.0)。
  • 当ROI(roi_B)发生偏移时,每个bin覆盖的区域与得分图高亮区域只有部分重叠,导致池化后的值降低,平均得分也随之下降(从 10.00 降到 6.94)。这直观地展示了位置敏感机制如何使网络对目标的定位准确性变得敏感。

损失函数与训练细节

多任务损失函数

R-FCN的损失函数与Faster R-CNN一脉相承,由分类损失和回归损失两部分组成,并通过一个指示函数 [u >= 1] 来控制回归损失是否生效(仅对前景目标计算回归损失)。

  1. 分类损失: 对于每个ROI,我们通过前述的位置敏感ROI池化和投票机制,得到了每个类别的得分 s_c,并经过softmax得到概率 p_c。分类损失就是真实类别 u 的负对数概率:

    L_{cls}(s, u) = -\log p_u, \quad \text{其中} \quad p_u = \frac{\exp(s_u)}{\sum_{c=0}^{C} \exp(s_c)}
    
  2. 回归损失: 回归分支输出 4k² 个通道。经过位置敏感ROI池化和 k×k 网格上的平均投票后,得到4个回归参数 t = (t_x, t_y, t_w, t_h)。回归损失采用平滑L1损失:

    L_{reg}(t, v) = \sum_{i \in \{x, y, w, h\}} \text{smooth}_{L1}(t_i - v_i)
    

    其中 v = (v_x, v_y, v_w, v_h) 是真实的边界框回归目标。

总损失为两者加权和:

L = L_{cls} + \lambda [u >= 1] L_{reg}

在线难例挖掘

R-FCN在训练中采用了在线难例挖掘策略。具体做法是:

  1. 前向传播时,计算所有ROI的损失。
  2. 将ROI按照损失值从高到低排序(正负样本分开排序)。
  3. 在反向传播时,只对损失最高的那一部分ROI进行梯度回传。同时,为了保证正样本不被忽略,会确保正负样本的比例(例如1:3)。

这种方法迫使网络更专注于学习那些难以分类的样本,从而提升模型的判别能力。

训练流程

R-FCN采用了与Faster R-CNN相似的四步交替训练法:

  1. 单独训练RPN网络。
  2. 使用第一步RPN产生的提议,单独训练R-FCN网络。
  3. 用第二步训练好的R-FCN网络重新初始化RPN,但固定共享的卷积层,只微调RPN独有的层。
  4. 保持共享卷积层固定,利用第三步更新的RPN产生的提议,微调R-FCN独有的层(即位置敏感得分图对应的卷积层)。

这种交替训练使得RPN和R-FCN能够相互促进,最终得到一个协同良好的检测系统。

性能分析

R-FCN在保持高精度的同时,显著提升了检测速度。

  • 精度提升:在PASCAL VOC数据集上,基于ResNet-101的R-FCN(k=7)的mAP达到了83.6%,超越了同背景下的Faster R-CNN。这主要归功于位置敏感机制更好地适应了全卷积网络结构,并编码了至关重要的位置信息。
  • 速度提升:由于几乎完全由卷积层构成,计算共享度极高,R-FCN的测试速度比Faster R-CNN快了约2.5倍。训练速度也有显著提升。

关于 k 的选择:k 代表了位置划分的精细程度。实验表明,k=1(即不划分位置)会严重损害性能,因为这退化为对整个ROI做全局池化,失去了平移敏感性。k=3 已有不错效果,k=7 通常能获得最佳精度,但也会略微增加计算量。

📝 动手练一练

  1. 概念辨析:R-FCN中的“位置敏感得分图”和Faster R-CNN中的“特征图”在根本用途上有何不同?位置敏感机制是如何解决ROI-wise子网络计算共享问题的?
👉 点击查看参考答案
  • 根本用途不同:
    • Faster R-CNN的特征图是通用的、类别无关的视觉特征表示。ROI池化从中提取固定大小的特征向量后,交给后续的全连接网络去判断类别和位置。
    • R-FCN的位置敏感得分图是类别和位置特定的。每个通道直接编码了“某类物体某部位”存在的置信度。分类决策通过简单的池化和投票在得分图层面完成,无需复杂的全连接网络。
  • 解决计算共享问题:
    • Faster R-CNN中,每个ROI经过池化后都需要独立地通过一系列全连接层进行计算,这部分无法共享。
    • R-FCN将原本全连接网络完成的“分类决策”功能,提前并分散到了卷积层生成的位置敏感得分图中。对于整张图像,所有得分图只需计算一次。对于每个ROI,只需要在对应的通道上进行一次轻量的、无参数的位置敏感ROI池化操作,即可完成分类。这使得所有计算(包括ROI-wise的部分)都变成了共享的卷积计算,极大提升了效率。
  1. 流程理解:假设一个 k=3 的R-FCN网络,正在处理一个“狗”的候选框。请描述从“位置敏感得分图”到最终输出“狗”这个类别标签,中间经历了哪些关键步骤?
👉 点击查看参考答案

关键步骤如下:

  1. 获取得分图:网络已生成 k²(C+1)=9*(C+1) 张位置敏感得分图。其中,对应“狗”这个类别有9张图,分别对狗身体的左上、中上、右上、左中、中心、右中、左下、中下、右下部位敏感。
  2. ROI网格划分:将当前狗的候选框区域同样划分为3×3的网格。
  3. 位置敏感池化:
    • 对于左上角的bin,从“狗-左上”得分图中,截取该bin覆盖的区域,进行均值池化,得到值 r_{dog}(1,1)。
    • 对于中上角的bin,从“狗-中上”得分图中池化,得到 r_{dog}(1,2)。
    • 以此类推,完成9个bin的池化,得到9个值 {r_{dog}(i,j)}。
  4. 类别投票:将9个值求平均(论文的公式用求和,二者只差常数因子 1/k²,各类别排序不变),得到“狗”类别的总体得分 s_{dog}。
  5. Softmax分类:同时对其他所有类别(包括背景)重复步骤3和4,得到每个类别的得分 {s_c}。对这些得分进行softmax归一化,得到概率分布。
  6. 输出结果:选择概率最高的类别作为最终预测。如果“狗”类别的概率最高,则输出标签“狗”。

本章小结

本节我们深入剖析了R-FCN的实现细节。作为R-CNN系列中致力于“全卷积化”的代表,R-FCN通过引入位置敏感得分图和位置敏感ROI池化这两个核心组件,巧妙地将目标检测中必须的“位置敏感性”重新注入到深度卷积网络中,同时实现了近乎100%的卷积计算共享。这不仅在理论上优雅地解决了Faster R-CNN的瓶颈,也在实践中带来了精度和速度的双重提升。

R-FCN标志着两阶段目标检测器在架构设计上的一次重要演进,它展示了如何通过精心设计卷积层的输出语义,来替代传统的全连接网络,为后续更多全卷积的检测模型(如后来的Mask R-CNN的掩码头分支)提供了思路。

今天,R-FCN 已被 Mask R-CNN、Cascade R-CNN 等两阶段后续以及 YOLO 系列、DETR 等检测器取代;但“位置敏感得分图 + 位置敏感池化”仍是全卷积检测器的代表性设计,也是理解“把位置信息编码进通道”的经典案例。

📋 行动清单

  • 在脑海中复述R-FCN的整体结构,明确 RPN(接 conv4)与分类/回归得分图(接 conv5 降维后)的输入、输出和作用。
  • 用自己的话解释“位置敏感”的含义:为什么需要它?它是如何通过得分图和池化操作实现的?
  • 对比R-FCN和Faster R-CNN的训练流程,理解四步交替训练法的目的和步骤。

—— 小象教研组