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

DeepLabv1

DeepLab v1:空洞卷积与条件随机场

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

在上一节中,我们学习了全卷积网络(FCN),它通过卷积化、反卷积和跳层结构,首次实现了端到端的语义分割。然而,FCN也存在一些不足,例如其输出分辨率较低,边界分割不够精细。本节我们将介绍谷歌提出的DeepLab v1模型,它通过引入空洞卷积(Atrous/Dilated Convolution) 和全连接条件随机场(Fully Connected CRF),在保持感受野的同时获得了更高分辨率的特征图,并显著提升了分割边界的精度。

💡 核心导读

  • 核心问题:FCN因多次池化导致特征图分辨率过低,边界信息丢失严重。
  • 关键创新:DeepLab v1把pool4、pool5的步长改为1,使特征图只下采样8倍;再用空洞卷积在不增加参数和计算量的前提下扩大感受野;最后用全连接条件随机场对分割边界进行精细化优化。
  • 设计思路:将VGG网络的最后两个池化层步长改为1,降低下采样倍数;在后续卷积层中使用空洞卷积恢复感受野;最后用CRF模型迭代优化分割结果。
  • 技术细节:理解空洞卷积中扩张率(rate) 的定义与作用,以及它与传统卷积、反卷积的联系与区别。
  • 历史定位:DeepLab v1(2014年底提出,2015年发表于ICLR)是语义分割领域的重要里程碑,其空洞卷积思想被后续的DeepLab系列及其他分割模型广泛沿用。

从FCN的不足到DeepLab的改进

DeepLab v1的设计初衷源于对FCN缺点的深入思考。回顾FCN,为了获得最终的分类得分图,输入图像需要经过多次池化(例如VGG16有5次步长为2的池化),导致特征图尺寸缩小了32倍。虽然通过反卷积可以上采样回原图尺寸,但丢失的细节信息难以完全恢复,尤其是物体边界变得模糊。FCN-8s虽然融合了pool3、pool4的浅层特征来改善边界,但边界精度仍然有限。

讲师在视频中形象地指出了FCN的一个“不优雅”之处:它在输入端就“补了100圈的0”,以便在32倍下采样后仍保留一定尺度,最后再靠反卷积放大、裁剪回原图尺寸。这虽然是一种实现方式,但本质上仍是信息量被严重压缩后再进行“猜测性”恢复的过程。

DeepLab v1的改进思路非常直接:

  1. 减少信息损失:降低网络中的下采样倍数,让特征图本身的分辨率更高。
  2. 保持宏观视野:在减少下采样的同时,必须保证神经元的感受野足够大,才能理解图像的全局语义。

密集预测与空洞卷积

降低下采样倍数:密集池化

DeepLab v1基于VGG16网络进行改造。其第一个关键操作是将pool4和pool5两个池化层的步长(stride)由2改为1,同时进行适当的填充(padding)以保证空间尺寸不变。

这一改动带来了什么变化?

  • 在传统VGG16中,经过5次步长为2的池化,下采样倍数为 25=322^5 = 32。
  • 在DeepLab v1中,pool4和pool5不再进行下采样,因此总的下采样倍数变为 23=82^3 = 8(仅来自pool1, pool2, pool3)。

这意味着网络最终输出的特征图(得分图)尺寸是输入的1/8,而不是1/32。我们得到了一个更高分辨率、更密集的得分图。这种步长为1的池化操作,可以理解为一种“密集池化”,它在局部窗口内仍然选取最大值(或平均值),但窗口是逐像素滑动的,因此输出的特征图尺寸与输入相同(在适当填充下)。

引入空洞卷积:扩大感受野

然而,仅仅减少下采样会带来一个新的问题:感受野变小。

感受野(Receptive Field) 是指卷积神经网络中,输出特征图上的一个像素点,对应输入图像上的区域大小。它代表了该像素点能“看到”的输入图像范围。网络越深,经过的卷积和池化越多,感受野通常越大,神经元就能看到更宏观的语义信息。

当我们把pool4和pool5的步长改为1后,后续卷积层的输入特征图尺寸变大了,但如果我们继续使用传统的3x3卷积,每个卷积核“看到”的范围(即感受野)相对于原始图像的比例实际上变小了。这就像“管中窥豹”,每个神经元只关注非常局部的细节,缺乏对物体整体和上下文的理解,不利于做出准确的分类判断。

为了解决这个矛盾,DeepLab v1引入了第二个关键创新:空洞卷积(Atrous Convolution),也称为膨胀卷积(Dilated Convolution)。

空洞卷积的核心思想是:在标准卷积核的权重之间插入“空洞”(零值),从而在不增加参数数量和计算量的情况下,扩大卷积核的感受野。

我们通过一个一维示例来理解:

import numpy as np

# 假设我们有一个一维输入信号
x = np.array([1, 2, 3, 4, 5, 6, 7], dtype=np.float32)
# 一个标准的一维卷积核(尺寸为3)
kernel = np.array([0.5, 1.0, 0.5])

print("输入信号 x:", x)
print("标准卷积核 kernel:", kernel)

# 1. 标准卷积 (stride=1,这里忽略边界,只计算中心部分)
def standard_conv1d(signal, k):
    # k尺寸为3
    output = []
    for i in range(1, len(signal)-1): # 忽略边界,仅演示
        patch = signal[i-1:i+2]
        output.append(np.sum(patch * k))
    return np.array(output)

result_std = standard_conv1d(x, kernel)
print("\n标准卷积结果(中心部分):", result_std)
print("  感受野:每个输出点看到输入中连续的3个值。")

# 2. 空洞卷积 (rate=2)
# 空洞卷积核可以看作是在标准核元素间插入 (rate-1) 个0
rate = 2
# 构造一个“等效”的空洞卷积核(用于理解感受野)
# 实际计算时,我们是通过修改采样位置来实现的。
print(f"\n--- 空洞卷积 rate={rate} ---")
print("概念上,卷积核变为: [0.5, 0, 1.0, 0, 0.5]")
print("实际计算时,卷积核权重仍是[0.5, 1.0, 0.5],但作用于输入的间隔为rate的像素上。")

# 模拟空洞卷积的计算:卷积核作用于 x[0], x[2], x[4]
def dilated_conv1d(signal, k, rate):
    output = []
    k_size = len(k)
    # 计算输出长度(无填充情况)
    out_len = len(signal) - (rate * (k_size - 1))
    for i in range(out_len):
        # 计算采样位置
        indices = [i + j*rate for j in range(k_size)]
        patch = signal[indices]
        output.append(np.sum(patch * k))
    return np.array(output)

# 为了比较,我们给输入两边补零,模拟padding
x_padded = np.pad(x, (2, 2), 'constant')  # rate=2时卷积核跨度由3格扩到5格,两侧各留2格
result_dil = dilated_conv1d(x_padded, kernel, rate)
# 取中间部分与标准卷积结果长度一致
mid_start = (len(result_dil) - len(result_std)) // 2
result_dil_mid = result_dil[mid_start:mid_start+len(result_std)]

print("空洞卷积结果(中心部分,模拟):", result_dil_mid)
print("  感受野:每个输出点看到输入中间隔为2的3个值,例如覆盖了输入中更广的区域(如位置1,3,5)。")
输入信号 x: [1. 2. 3. 4. 5. 6. 7.]
标准卷积核 kernel: [0.5 1.  0.5]

标准卷积结果(中心部分): [ 4.  6.  8. 10. 12.]
  感受野:每个输出点看到输入中连续的3个值。

--- 空洞卷积 rate=2 ---
概念上,卷积核变为: [0.5, 0, 1.0, 0, 0.5]
实际计算时,卷积核权重仍是[0.5, 1.0, 0.5],但作用于输入的间隔为rate的像素上。
空洞卷积结果(中心部分,模拟): [ 4.  6.  8. 10.  8.]
  感受野:每个输出点看到输入中间隔为2的3个值,例如覆盖了输入中更广的区域(如位置1,3,5)。

对比两组结果时,最右端数值不同是因为该点用到了补零边界;这里只需关注每个输出点的采样间隔,不必逐值对齐。

在二维情况下,对于一个3x3的卷积核,当rate=2时,它作用的像素不再是连续的3x3区域,而是像棋盘格一样,跳过相邻像素,覆盖一个5x5区域中的9个点。这样,感受野就从3x3扩大到了5x5,而参数量依然是9个。

扩张率(rate):定义了卷积核权重之间的间隔。rate=1即为标准卷积;rate=2表示在水平和垂直方向上都间隔1个像素进行采样(等价于在权重之间插入1个零);rate=4则间隔3个像素,也就是插入3个零。

空洞卷积与反卷积还有一个对称关系:反卷积(转置卷积)可以看作先在输入特征图的元素之间插零,再用卷积核去乘;空洞卷积则保持输入不动,改在卷积核内部插零。两者插零的对象不同,但都在拉大“间隔”——前者放大空间尺寸,后者扩大感受野。理解这一点,就能明白DeepLab为什么可以用“密集池化 + 空洞卷积”替代“先降采样32倍、再用反卷积上采样”。

在DeepLab v1中,空洞卷积被应用在改造后的VGG网络的后几层:

  • 在conv5层使用rate=2的空洞卷积。
  • 在conv6层(即原VGG的fc6卷积化后的层)使用rate=4的空洞卷积。

通过结合密集池化(减少下采样) 和空洞卷积(扩大感受野),DeepLab v1成功获得了高分辨率(仅下采样8倍)且具有足够大感受野的特征图。这比FCN先下采样32倍再进行上采样的方式“优雅”得多,因为它保留了更多原始空间信息。

稀疏特征提取与稠密特征提取。 把两条路线放在一起对比会更清楚。传统做法是“降采样→卷积→上采样”:降采样把特征图缩小,卷积层的感受野虽然够大,但特征点变得稀疏,很多相邻位置的特征组合(例如连续四个位置的特征)已经不存在了;最后的上采样本质上只是用少数离散特征做双线性插值,放大后的图并不会凭空增加有效信息。DeepLab则是“密集池化 + 空洞卷积”:池化步长为1,信息量基本不减少;空洞卷积每次只平移一格,但每个卷积核覆盖的范围很大。这样得到的得分图既稠密(相邻输出点之间没有跳过输入),又有足够大的感受野,这才是空洞卷积想要达到的效果。

全连接条件随机场(Fully Connected CRF)优化边界

尽管空洞卷积提供了更好的特征图,但CNN本身的性质决定了它在深度传递过程中会逐步丢失细节的位置信息,导致分割边界不够锐利、存在模糊区域。讲师提到:“图像在CNN中逐层传递,是一个逐步抽象的过程,原来的位置信息会随着深度而减少甚至消失。”

为了恢复精确的边界,DeepLab v1引入了后处理步骤:全连接条件随机场(Fully Connected Conditional Random Field, FC-CRF)。

条件随机场(CRF) 是一种概率图模型,常用于考虑上下文信息的结构化预测任务。在分割中,它可以建模像素间标签的依赖关系。“全连接”意味着CRF模型考虑图像中每一对像素之间的关系,而不仅仅是相邻像素。这使得模型能够利用全局上下文信息来优化每个像素的标签。

DeepLab v1中FC-CRF的能量函数通常包含两部分(这里只给出形式,具体的推导与求解留到下一节):

  1. 一元势函数(Unary Potential):来源于CNN的预测得分图,表示每个像素独立属于某个类别的概率。即 ψu(xi=l)=−log⁡P(xi=l)\psi_u(x_i=l) = -\log P(x_i=l),其中 P(xi=l)P(x_i=l) 是CNN把像素 ii 判为类别 ll 的概率。
  2. 二元势函数(Pairwise Potential):鼓励相似像素(颜色接近、位置邻近)分配相同的标签,反之则鼓励分配不同标签。DeepLab采用高斯核函数来定义像素间的相似性: ψp(xi,xj)=μ(xi,xj)∑m=1Kw(m)k(m)(fi,fj)\psi_p(x_i, x_j) = \mu(x_i, x_j) \sum_{m=1}^{K} w^{(m)} k^{(m)}(\mathbf{f}_i, \mathbf{f}_j) 其中 fi,fj\mathbf{f}_i, \mathbf{f}_j 是像素的特征(如RGB颜色和坐标位置),k(m)k^{(m)} 是高斯核,μ\mu 是标签兼容性函数。

通过最小化这个总能量函数,CRF可以对CNN输出的粗糙分割图进行迭代优化,使边界与图像中物体的真实边缘对齐,消除内部的不一致性。

DeepLab v1 整体架构与流程

现在,我们可以勾勒出DeepLab v1的整体架构:

  1. 输入图像:送入改造后的CNN。
  2. 改造的CNN骨干网络(基于VGG16):
    • 将pool4和pool5的步长设为1(密集池化)。
    • 在conv5与conv6(conv6由VGG的fc6卷积化而来)使用空洞卷积(rate分别为2和4)。
    • 输出一个下采样8倍的得分图(Score Map)。
  3. 双线性插值上采样:将得分图上采样8倍至原始图像尺寸,得到初步的分割结果。
  4. 全连接CRF优化:将上采样后的分割结果(以及原始图像的RGB信息)输入FC-CRF模型,进行迭代推理,优化边界,得到最终精细的分割图。

这个过程清晰地将问题分解为两步:CNN负责识别和粗略定位(是什么物体,大概在哪),CRF负责精修边界(物体的精确轮廓在哪)。

本章小结

DeepLab v1于2014年底提出、2015年发表在ICLR上,是语义分割发展史上的一个重要模型,它巧妙地解决了FCN存在的分辨率与感受野之间的矛盾。

  • 核心贡献:提出了空洞卷积,这是一种高效扩大感受野的工具,使得网络能够在保持高分辨率特征图的同时,捕获足够的上下文信息。
  • 系统设计:采用“CNN + CRF”的两阶段范式,CNN负责语义理解,CRF负责细节优化,显著提升了分割边界的质量。
  • 历史意义:DeepLab v1的成功验证了空洞卷积在密集预测任务中的有效性,为后续的DeepLab系列(v2, v3, v3+)以及众多其他分割模型奠定了基础。

站在今天回看,DeepLab v1属于深度学习分割的早期范式:后续的DeepLabv2用空洞空间金字塔池化(ASPP)做多尺度特征提取,DeepLabv3起不再使用全连接CRF后处理,DeepLabv3+又引入轻量解码器;近年主流的Transformer/掩码分类式分割模型(如Mask2Former)与提示式分割模型(如SAM)进一步成为研究与工程的主流,这套“CNN + 全连接CRF”的两阶段流程已基本退出实际使用。不过空洞卷积本身并未过时,它仍是现代分割网络和实时分割模型中扩大感受野的常用组件。

📋 行动清单

  • 理解空洞卷积中rate参数的含义,并能够计算给定rate和卷积核大小下的实际感受野。
  • 对比FCN与DeepLab v1在“下采样—上采样”流程上的区别,说出各自优劣。
  • 阐述全连接CRF在DeepLab v1中扮演的角色,以及为什么它比只考虑局部邻居的CRF更有效。

📝 动手练一练

  1. 概念辨析:假设我们有一个3x3的标准卷积核,当设置空洞卷积的rate=3时,这个卷积核在输入特征图上实际覆盖的感受野尺寸是多少?(假设输入足够大,不考虑边界效应)
👉 点击查看参考答案

对于一个尺寸为 k×kk \times k 的标准卷积核,当使用扩张率 rr 时,其等效的感受野尺寸 FF 可以通过以下公式计算: F=k+(k−1)×(r−1)F = k + (k - 1) \times (r - 1) 代入 k=3,r=3k=3, r=3: F=3+(3−1)×(3−1)=3+2×2=7F = 3 + (3 - 1) \times (3 - 1) = 3 + 2 \times 2 = 7 因此,一个3x3卷积核在rate=3时,其感受野尺寸为7x7。你可以想象这个3x3的权重,被“拉伸”到了一个7x7的网格上(中间有空洞),覆盖了更广的输入区域。

  1. 代码理解:以下代码模拟了标准卷积和空洞卷积在小型矩阵上的操作,请观察输出并思考感受野的变化。
import numpy as np

# 定义一个小的输入特征图 (6x6)
input_map = np.arange(36).reshape(6, 6).astype(np.float32)
print("输入特征图 (6x6):")
print(input_map)

# 定义一个简单的3x3卷积核(边缘检测类似)
kernel = np.array([[1, 0, -1],
                   [1, 0, -1],
                   [1, 0, -1]], dtype=np.float32)

print("\n卷积核 (3x3):")
print(kernel)

# 1. 标准卷积 (stride=1, padding='valid',即无填充)
def standard_conv2d(input_img, k):
    k_h, k_w = k.shape
    i_h, i_w = input_img.shape
    o_h, o_w = i_h - k_h + 1, i_w - k_w + 1
    output = np.zeros((o_h, o_w))
    for i in range(o_h):
        for j in range(o_w):
            patch = input_img[i:i+k_h, j:j+k_w]
            output[i, j] = np.sum(patch * k)
    return output

result_std = standard_conv2d(input_map, kernel)
print("\n标准卷积结果 (4x4,无填充):")
print(result_std)
print("说明:每个输出值由输入中一个连续的3x3区域计算得出。")

# 2. 空洞卷积 (rate=2)
# 我们通过手动构建一个“膨胀后”的等效核视图来理解,实际实现是间隔采样。
rate = 2
print(f"\n--- 空洞卷积 rate={rate} ---")
print("概念上,卷积核被‘拉伸’到一个更大的网格上,权重间有空洞。")
print("对于3x3核,rate=2时,其作用的输入区域相当于一个5x5区域中的9个特定点。")

# 为了简化演示,我们计算一个输出点,展示其采样的输入位置
# 计算输出点(0,0)的值:它应该对应输入位置 (0,0), (0,2), (0,4), (2,0), (2,2), (2,4), (4,0), (4,2), (4,4)
sample_positions = [(i*rate, j*rate) for i in range(3) for j in range(3)]
print(f"输出点(0,0)采样的输入位置: {sample_positions}")
# 提取这些位置的值(6x6的输入足以覆盖这9个位置,这里只打印前6个)
sample_values = [float(input_map[pos]) for pos in sample_positions if pos[0] < 6 and pos[1] < 6]
print(f"对应的输入值(部分): {sample_values[:6]}...")
print("可以看到,它看到了更广的输入区域(从第0行到第4行)。")
输入特征图 (6x6):
[[ 0.  1.  2.  3.  4.  5.]
 [ 6.  7.  8.  9. 10. 11.]
 [12. 13. 14. 15. 16. 17.]
 [18. 19. 20. 21. 22. 23.]
 [24. 25. 26. 27. 28. 29.]
 [30. 31. 32. 33. 34. 35.]]

卷积核 (3x3):
[[ 1.  0. -1.]
 [ 1.  0. -1.]
 [ 1.  0. -1.]]

标准卷积结果 (4x4,无填充):
[[-6. -6. -6. -6.]
 [-6. -6. -6. -6.]
 [-6. -6. -6. -6.]
 [-6. -6. -6. -6.]]
说明:每个输出值由输入中一个连续的3x3区域计算得出。

--- 空洞卷积 rate=2 ---
概念上,卷积核被‘拉伸’到一个更大的网格上,权重间有空洞。
对于3x3核,rate=2时,其作用的输入区域相当于一个5x5区域中的9个特定点。
输出点(0,0)采样的输入位置: [(0, 0), (0, 2), (0, 4), (2, 0), (2, 2), (2, 4), (4, 0), (4, 2), (4, 4)]
对应的输入值(部分): [0.0, 2.0, 4.0, 12.0, 14.0, 16.0]...
可以看到,它看到了更广的输入区域(从第0行到第4行)。
👉 点击查看参考答案与思考

从输出可以看出:

  • 标准卷积:每个输出点(如result_std[0,0])由输入中一个连续的3x3区块(input_map[0:3, 0:3])计算得到。
  • 空洞卷积(概念):对于rate=2,计算输出点(0,0)时,卷积核作用的输入位置是(0,0), (0,2), (0,4), (2,0), (2,2), (2,4), (4,0), (4,2), (4,4)。这些点散布在一个5x5的区域内(行和列索引从0到4),而不是连续的3x3区域。因此,感受野扩大到了5x5,但参与计算的权重数量仍然是9个(3x3)。

思考:如果输入图像尺寸很小(例如14×14),而rate设置得非常大(例如15),3×3卷积核的采样跨度会达到31个像素,绝大部分采样点都落在图像之外,只剩下中心一个权重真正参与计算,于是退化成类似1×1卷积的效果,失去了捕获空间上下文的能力。因此,rate的设置需要与输入特征图的尺寸相匹配。

—— 小象教研组