查看全课大纲(第 57 / 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

SPP-Net

SPP-Net:共享卷积计算与空间金字塔池化

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

在上一节,我们深入分析了R-CNN模型,其核心问题在于速度“刻骨铭心的慢”。本节我们将介绍其重要的改进模型——SPP-Net。SPP-Net通过两项关键创新,极大地提升了目标检测的效率:一是将卷积计算提前并共享,避免了对数千个候选区域的重复计算;二是引入了空间金字塔池化(Spatial Pyramid Pooling)技术,使得网络能够接受任意尺寸的输入,并生成固定长度的特征向量。理解SPP-Net,是掌握从R-CNN到Fast R-CNN这一技术演进的关键一步。

💡 核心导读

  • 核心问题:R-CNN为何慢?根源在于对每个候选区域都独立进行卷积计算,导致大量重复计算。
  • 核心改进一(共享计算):SPP-Net将卷积层前移,对整张图片只做一次卷积,然后在得到的特征图上直接提取所有候选区域的特征。
  • 核心改进二(SPP层):引入空间金字塔池化层,将任意尺寸的特征图通过“划格子”和池化的方式,转换为固定长度的特征向量,从而解决了全连接层要求固定输入尺寸的限制。
  • 关键洞察:卷积层输出的特征图保留了输入图像的空间位置关系,这使得在特征图上定位候选区域成为可能。
  • 遗留问题:SPP-Net仍然是一个多阶段训练流程,且SPP层之前的卷积层无法进行微调(fine-tune)。

从R-CNN的瓶颈到SPP-Net的改进思路

R-CNN最致命的缺点是速度慢。回顾其流程:首先用Selective Search从原图中提取约2000个候选区域,然后每个区域都需要经过裁剪或拉伸(warp)预处理,再各自独立送入CNN(如AlexNet)进行卷积特征提取。

这意味着,同一张图片的卷积特征被重复计算了约2000次。计算量巨大,是导致训练和测试过程异常缓慢的根本原因。

那么,一个直观的改进思路是:能否只对整张图片做一次卷积计算,然后让所有候选区域共享这个卷积结果?

SPP-Net正是基于这一思路提出的。它的核心改进有两点:

  1. 共享卷积计算:直接输入整张图片,进行一次完整的卷积计算。所有候选区域的特征都从这同一份卷积特征图上提取。
  2. 适应任意尺寸输入:通过空间金字塔池化(SPP) 技术,将不同尺寸的候选区域特征图,统一转换为固定长度的特征向量,从而能够输入到后续的全连接层。

SPP-Net的网络架构与工作原理

整体架构:卷积共享与区域级计算分离

SPP-Net的网络结构清晰地体现了“共享计算”的思想:

输入图片
    ↓
[共享卷积计算] Conv1 -> Conv2 -> Conv3 -> Conv4 -> Conv5 (生成特征图)
    |                                    |
    |                                    | (在特征图上定位)
    |                                    ↓
    |                    Selective Search (原图产生候选框 → 映射到特征图坐标)
    |                                    |
    |                                    ↓
    |                            [SPP层] (将不同尺寸区域特征转换为固定长度向量)
    |                                    |
    |                                    ↓
    |                            [全连接层] Fc6, Fc7, Fc8 (可微调)
    |                                    |
    |                                    ↓
    |                            [输出头] SVMs分类器 / Bounding Box回归
    |
(整图计算,不微调)               (区域级计算,可微调)

与R-CNN的关键区别在于:

  • 卷积层位置:R-CNN是先提取区域,再对每个区域做卷积。SPP-Net是先对整图做卷积,再在特征图上提取区域。
  • 微调范围:在SPP-Net中,SPP层之前的所有卷积层(Conv1-5)在微调阶段参数是冻结的,不再更新。只能微调SPP层之后的全连接层(Fc6、Fc7、Fc8)。注意原因不是“SPP层不可导”——划格子和最大值池化本身都是可导的,梯度有明确定义。原论文只说明了“只微调全连接层”这一做法,并未展开分析;Fast R-CNN 论文对此给出的解释是效率:当RoI的感受野较大时,误差穿过SPP层向Conv1-5反传的效率很低,因此实践中选择冻结其下方的卷积层。

关键技术一:为何能在特征图上定位区域?

一个关键问题是:在原始图像上通过Selective Search得到的候选框(Bounding Box),如何对应到卷积层输出的特征图上?

这依赖于卷积神经网络的一个重要性质:卷积层输出的特征图,其空间位置关系与输入图像基本保持一致。

考虑一个卷积核,它可能负责检测“车窗玻璃”这种特征。当它在图像上滑动时,只有在有车窗玻璃的位置,对应的特征图位置才会被激活(呈现高亮)。这意味着,输入图像中某个物体所在的位置,会反映在特征图上相同(或按步长缩放后)的相对位置。

因此,我们只需要将原始图像上的候选框坐标,按照网络前5层卷积的累积步长(例如取16,即特征图边长约为原图的1/16)进行缩放,就能得到该候选框在Conv5输出特征图上的对应区域。这个区域称为感兴趣区域(Region of Interest, RoI)。后续的特征提取(SPP层)就在这个RoI上进行。

# 模拟原始图像和特征图的空间映射关系
# 假设原始图像尺寸为 (224, 224)
orig_h, orig_w = 224, 224
# 假设经过Conv1-5后,特征图尺寸缩小为原来的1/16(步长累积效应)
feat_h, feat_w = orig_h // 16, orig_w // 16 # (14, 14)

# 假设Selective Search在原图上得到一个候选框 [x, y, width, height]
# 坐标是绝对像素值
orig_bbox = [50, 30, 80, 120]  # x, y, w, h

# 将其映射到特征图上的坐标(取整)
feat_bbox = [
    int(orig_bbox[0] / 16), # x_feat
    int(orig_bbox[1] / 16), # y_feat
    int(orig_bbox[2] / 16), # w_feat
    int(orig_bbox[3] / 16)  # h_feat
]

print(f"原始图像候选框: {orig_bbox}")
print(f"映射到特征图上的区域: {feat_bbox}")
原始图像候选框: [50, 30, 80, 120]
映射到特征图上的区域: [3, 1, 5, 7]

关键技术二:空间金字塔池化(SPP)层

解决了区域定位问题,下一个挑战是:从特征图上提取出的RoI特征图,尺寸各不相同(因为原始候选框大小不一),如何输入到需要固定尺寸输入的全连接层?

R-CNN采用的方法是裁剪(crop)或暴力拉伸(warp):裁剪会把目标直接截断(例如只框住大半个车身),拉伸则会改变长宽比(瘦高的灯塔、扁平的飞机都被压成正方形),两者都会损害区域内容。SPP-Net提出了一个更优雅的解决方案:空间金字塔池化。

从结构上看,SPP层取代了Conv5之后原来的Pool5池化层(因此文献里也常说SPP特征对应“Pool5特征”)。它的操作非常巧妙,其灵感来源于传统的图像特征描述子(如PHOG,即金字塔梯度方向直方图)。它的工作流程如下:

  1. 确定金字塔层级:对于输入的任意尺寸的RoI特征图(例如尺寸为 h x w x c,其中c是通道数),我们预先定义几个池化网格。
    • 常见设置是三个层级:1x1, 2x2, 4x4。
    • 1x1网格意味着对整个特征图进行池化,得到1个值。
    • 2x2网格意味着将特征图均匀划分为2行2列,共4个子区域,每个子区域分别池化。
    • 4x4网格同理,划分成16个子区域。
  2. 对每个网格进行池化:在每个划分好的子区域(称为“bin”)内,进行最大池化(Max Pooling) 操作。这样,每个bin会输出该区域所有通道上的最大值(如果是多通道,则每个通道独立池化)。
  3. 拼接成固定向量:将所有层级、所有bin的池化结果(每个bin输出c个值,代表c个通道)按顺序拼接起来,形成一个长的特征向量。
    • 对于 1x1, 2x2, 4x4 三个层级,总共的bin数量为 1 + 4 + 16 = 21。
    • 如果特征图有256个通道(c=256),那么最终SPP输出的特征向量长度就是 21 * 256 = 5376。

这个向量的长度是固定的(由预设的金字塔层级和通道数决定),与输入RoI特征图的具体尺寸(h, w)完全无关。这样就完美解决了全连接层需要固定长度输入的问题。

import numpy as np

def spatial_pyramid_pooling(feature_map, levels=[1, 2, 4]):
    """
    简化的空间金字塔池化演示。
    feature_map 形状为 (H, W, C):每个 bin 在每个通道上独立做最大池化,
    再把所有 bin 的结果按“层级 → 行 → 列 → 通道”的顺序依次拼接。
    """
    h, w, c = feature_map.shape
    output = []
    
    for level in levels:
        # 计算每个bin的尺寸
        bin_h = h / level
        bin_w = w / level
        
        for i in range(level):
            for j in range(level):
                # 计算当前bin的边界(取整)
                h_start = int(i * bin_h)
                h_end = int((i + 1) * bin_h)
                w_start = int(j * bin_w)
                w_end = int((j + 1) * bin_w)
                
                # 提取当前bin的区域
                bin_region = feature_map[h_start:h_end, w_start:w_end, :]
                # 进行最大池化:只在空间维度上取最大,通道维保留 → 每个bin输出c个值
                pooled_val = np.max(bin_region, axis=(0, 1))
                output.extend(pooled_val)
                
    return np.array(output)

# 构造一个模拟的RoI特征图 (6, 6, 1),尺寸是任意的
np.random.seed(42)
roi_feature_map = np.random.randn(6, 6, 1)
print(f"输入特征图形状: {roi_feature_map.shape}")

# 应用SPP,层级为 [1, 2, 4]
spp_output = spatial_pyramid_pooling(roi_feature_map)
print(f"SPP输出向量的长度: {len(spp_output)}")
print(f"输出向量前10个值:\n {spp_output[:10]}")
输入特征图形状: (6, 6, 1)
SPP输出向量的长度: 21
输出向量前10个值:
 [ 1.85227818  1.57921282  1.52302986  1.85227818  0.82254491  0.49671415
  0.64768854  1.52302986 -0.23413696  1.57921282]

SPP-Net的训练流程与遗留问题

SPP-Net的训练流程与R-CNN类似,也是一个多阶段过程,但特征来源和微调对象发生了变化:

  1. 预训练(Pre-training):使用ImageNet数据集预训练一个CNN模型(如AlexNet),得到模型 M。
  2. 提取SPP特征:将训练图片输入 M,进行前向传播至 Conv5 层,得到整图的特征图。然后对于每张图片的所有Selective Search候选区域,在特征图上找到对应RoI,并通过SPP层计算出固定长度的特征向量。这一步得到所有区域的SPP特征 F。
  3. 微调全连接层:使用上一步得到的SPP特征 F 作为输入,对应的区域类别标签作为输出,只微调网络最后的部分(即SPP层之后的全连接层,如Fc6, Fc7, Fc8),得到微调后的模型 M'。注意,Conv1-5的参数在此阶段被冻结。
  4. 训练SVM分类器:使用微调后模型 M' 的某一层全连接层(如Fc7)的输出特征 F',训练多个SVM分类器(每类一个),用于最终的类别判断。
  5. 训练Bounding Box回归器:使用第2步得到的SPP特征 F(来自 Conv5 后),训练Bounding Box回归模型,用于对候选框的位置进行微调。

SPP-Net带来的性能提升与遗留问题:

  • 速度提升:由于共享了卷积计算,特征提取不再需要对约2000个区域各做一遍卷积。整体训练时间约为25.5小时,其中微调全连接层16小时、特征提取5.5小时、SVM与Bounding Box训练4小时;作为对照,R-CNN的总训练时间约84小时。
  • 精度并未同步提升:SPP-Net 的主要收益在速度。按本课程 Fast R-CNN 一节对比表的口径(VGG16、VOC 2007),SPP-Net 的 mAP 为 63.1%,反而低于 R-CNN 的 66.0%,因此不能说“精度保持甚至略有提升”。
  • 遗留问题:
    1. 多阶段训练:仍然需要独立训练SVM和Bounding Box回归器,流程复杂。
    2. 特征存储:仍需将中间特征(SPP特征和Fc7特征)写入磁盘,占用大量空间。
    3. 无法端到端微调:这是SPP-Net最受诟病的一点。注意不能简单归因为“SPP层不可导”——划格子和最大值池化都是可导的,梯度有定义。真正的原因是效率:Fast R-CNN 论文指出(SPP-Net 原论文并未展开这一分析),当RoI的感受野较大时,误差穿过SPP层向Conv1-5反传的效率很低。因此,网络底部的卷积层无法根据检测任务进行优化,限制了模型的性能上限。

📝 动手练一练

  1. 概念辨析:SPP-Net中的“共享卷积计算”具体是如何实现的?它与R-CNN的处理方式有何本质区别?为什么这种共享能大幅提升速度?
👉 点击查看参考答案

实现方式:SPP-Net将卷积层提到最前面,整张图片只进行一次前向传播,通过Conv1-5层得到一张特征图。所有Selective Search提取的候选区域,不再从原图截取并单独卷积,而是通过坐标映射,直接在这张共享的特征图上截取对应的区域(RoI)进行后续处理。

本质区别:R-CNN是 “先区域,后卷积”,2000个区域要算2000次卷积。SPP-Net是 “先卷积,后区域”,2000个区域共享1次卷积结果。

提速原因:卷积运算是CNN中计算量最大的部分。SPP-Net避免了将这最耗时的部分重复2000次,因此特征提取阶段的速度得到数量级的提升。

  1. 手动计算:假设一个RoI特征图尺寸为 8 x 6 x 256(高8,宽6,通道256)。如果SPP层采用 [1x1, 2x2, 4x4] 的金字塔层级,请问经过SPP层后,输出的特征向量长度是多少?请简述计算过程。
👉 点击查看参考答案

计算过程:

  1. 确定总bin数:金字塔层级 [1, 2, 4] 对应的网格数分别为 1x1=1, 2x2=4, 4x4=16。总bin数 = 1 + 4 + 16 = 21。
  2. 确定每个bin的输出:对每个bin进行最大池化,由于特征图有256个通道,池化操作在每个通道上独立进行。因此,每个bin会输出一个256维的向量(每个通道一个最大值)。
  3. 计算总长度:将21个bin的256维向量拼接起来。总特征长度 = 21 (bins) * 256 (channels) = 5376。

答案:输出的特征向量长度为 5376。

本章小结

本节我们深入剖析了SPP-Net,它是目标检测从R-CNN迈向高效化、集成化道路上的关键里程碑。其两大核心创新——共享卷积计算与空间金字塔池化(SPP)——直指R-CNN的效能痛点。共享计算的思想深刻影响了后续所有目标检测模型的设计;而SPP层则巧妙地将传统计算机视觉中的多尺度思想融入深度学习,解决了输入尺寸固定的限制。尽管SPP-Net自身因无法端到端微调等问题未能成为最终解决方案,但它为Fast R-CNN和Faster R-CNN的诞生铺平了道路。

需要如实定位的是:SPP-Net发表于2014年(ECCV),它和随后的Fast R-CNN、Faster R-CNN都属于两阶段检测器的历史阶段。2019年前后起,目标检测的主流逐步转向以YOLO系列为代表的单阶段实时模型,以及以DETR为代表的Transformer检测器,本节这些模型更适合作为理解检测思想演进的基础来学。不过“多尺度池化”的思路并未过时——今天CNN检测器中的SPP/SPPF模块就是它的直系后代。

📋 行动清单

  • 理解共享计算:在脑海中清晰复现SPP-Net“先整图卷积,再特征图提区域”的流程,并与R-CNN的流程进行对比。
  • 掌握SPP原理:能够不借助资料,解释清楚SPP层如何将任意尺寸的输入转换为固定长度的输出,并说出其与金字塔概念(如PHOG)的联系。
  • 分析优劣:列出SPP-Net相对于R-CNN的主要改进点,并指出它仍然存在的至少两个关键问题。

—— 小象教研组