1. 从零开始:理解DeepLab-V3+与语义分割

如果你对计算机视觉感兴趣,尤其是想让计算机像人一样“看懂”图片里哪个是路、哪个是车、哪个是行人,那么语义分割就是你绕不开的技术。简单来说,语义分割就是给图片里的每一个像素都打上标签,就像我们用不同颜色的画笔把图片里的物体一个个描出来。今天我们要聊的DeepLab-V3+,就是做这件事的“明星模型”之一,它在复杂街景图片上的表现尤其出色。

我最早接触语义分割项目时,被各种论文里的复杂结构图搞得头晕。后来发现,其实可以把DeepLab-V3+想象成一个经验丰富的画家。这位画家作画分两步:第一步是“观察和理解”(编码器Encoder),他会眯起眼睛,从整体到局部仔细端详眼前的风景,理解哪里是天空,哪里是建筑,哪里是道路。第二步是“描绘和上色”(解码器Decoder),他根据刚才的理解,拿起画笔,精确地勾勒出每个物体的轮廓,并涂上对应的颜色。DeepLab-V3+的核心创新,就在于它让这位“画家”的观察方式更聪明(用了ASPP模块扩大视野),并且在上色时不忘参考最初的草图细节(引入了低级特征),从而画得更准、边界更清晰。

我们这次实战选择的“画布”是Cityscapes数据集。这个数据集包含了50多个城市街道场景的精细标注,有34个类别,比如汽车、行人、交通标志、植被等。用它来训练和检验我们的模型再合适不过了。整个项目我会基于Keras和TensorFlow 2.8来实现,这个组合对新手非常友好,能让我们更专注于模型本身,而不是纠结于复杂的框架配置。接下来,我就带你一步步拆解这个“画家”是如何炼成的,并分享我在实现过程中摸索出来的、能让模型效果更好、训练更稳的实战技巧。

2. 核心架构拆解:DeepLab-V3+的“十八般武艺”

要玩转一个模型,死记硬背代码不行,得先弄明白它为什么厉害。DeepLab-V3+的论文图看起来有点复杂,但我们抓住三个关键点,就能把它吃透。

2.1 编码器(Encoder):拥有“多尺度视野”的观察者

编码器的任务是从输入图像中提取越来越抽象的特征。DeepLab-V3+通常会用像Xception或Inception-ResNet V2这样的预训练网络作为“脊柱”(Backbone)来担当此任。但光有这个还不够,模型在顶部祭出了一个“大杀器”——空洞空间金字塔池化(ASPP)模块

你可以把ASPP想象成给模型装上了不同倍率的望远镜。普通的卷积层就像固定焦距的镜头,只能看到固定大小的图案。而ASPP模块同时使用了多个不同膨胀率(dilation rate)的空洞卷积。什么是空洞卷积?就是在卷积核的元素之间“挖洞”,跳过一些像素,这样在不增加参数量的情况下,一下子就能看到更大范围的图像信息。ASPP模块通常会并联使用膨胀率为6、12、18的三个空洞卷积,再加上一个1x1普通卷积和一个全局平均池化层。这相当于让模型同时用上了广角镜、标准镜和长焦镜来观察同一个场景,最后把看到的所有信息融合在一起。这样,无论是近处行人的细节,还是远处大楼的轮廓,模型都能兼顾,从而获得更丰富的场景语义信息。我在代码里把这个模块实现为一个独立的函数 dilated_spatial_pyramid_pooling,它直接作用于Backbone输出的高级特征上。

2.2 解码器(Decoder):融合细节的“精修师”

如果只有编码器,我们得到的特征图尺寸太小,无法还原到原始图像大小进行像素级预测。解码器的任务就是把这些抽象的特征“翻译”回一张密集的预测图。DeepLab-V3+解码器的精妙之处在于它引入了跳跃连接(Skip Connection)

具体来说,解码器不仅接收来自编码器顶部、经过ASPP处理的、富含语义的高级特征,还额外引入了一个来自编码器较浅层的低级特征。这个低级特征通常来自Backbone的中间层,它虽然语义信息不那么强,但保留了更多的空间细节和边缘信息(比如物体的轮廓)。这就好比画家在画完大致色块后,又回头参考最初画的精细线稿,来修正物体的边界,让边缘更锐利、更准确。

在我的实现中,我特意从Inception-ResNet V2的 activation_4 层提取了这个低级特征。然后通过两次转置卷积(Conv2DTranspose)对其进行上采样和通道数调整,使其在空间尺寸和通道数上与来自高级特征的上采样结果相匹配,最后再将二者在通道维度上拼接(Concatenate)起来。这个融合操作是提升分割边界精度的关键。

2.3 一个我自用的“小改进”:额外的细节注入通道

按照原论文结构实现后,模型效果已经不错。但在实际调试Cityscapes数据时,我发现一些细长物体(如电线杆)的边界还是有点模糊。受跳跃连接思路的启发,我尝试做了一个小改动:额外增加一个从原始输入图像直接引出的细节分支

这个分支很简单,就是在模型最开头,对输入图像直接进行几次卷积,提取一些非常底层的特征(如颜色、纹理、初级边缘)。然后让这个分支“潜伏”起来,直到解码器的最后阶段,再与主解码路径的特征进行融合。这样,模型在做出最终预测前,又能多看一眼最原始的图像细节。具体实现上,我在模型定义函数 inceptionresnetv2_deeplabv3plus 的后半部分,创建了一个名为 down_sampling_1 的分支。经过实验,这个小技巧对于改善某些类别(特别是那些依赖精细轮廓的类别)的分割效果,确实有肉眼可见的提升。当然,这也会轻微增加计算量,算是一个实用的“调参”思路。

3. 实战准备:环境、数据与代码框架

理论懂了,手就痒了。别急,磨刀不误砍柴工,把环境和数据准备好,后面的训练才能一帆风顺。

3.1 搭建你的深度学习工作台

我强烈推荐使用Anaconda来管理Python环境,它能完美解决包依赖冲突的噩梦。下面是我使用的环境配置命令,你可以直接复制粘贴:

# 创建一个新的Python 3.8环境,命名为 tf2
conda create -n tf2 python=3.8
# 激活环境
conda activate tf2
# 安装TensorFlow 2.8 和核心依赖
pip install tensorflow==2.8.0
# 安装图像处理和数据处理的帮手
pip install opencv-python pillow matplotlib scikit-image
# 安装Jupyter Lab,这是我们交互式编程和训练的主界面
pip install jupyterlab

这里有个小坑我踩过:TensorFlow 2.8对CUDA和cuDNN版本有要求。如果你用GPU训练,确保你的CUDA版本是11.2,cuDNN是8.1。去NVIDIA官网下载对应版本安装就好。安装完后,在Python里运行 import tensorflow as tf; print(tf.config.list_physical_devices('GPU')),如果能看到你的GPU信息,就说明环境配置成功了。

3.2 处理Cityscapes数据集:从原始文件到训练样本

Cityscapes数据集官网下载后,文件结构很清晰。我们主要用到两个文件夹:leftImg8bit(包含训练、验证、测试集的RGB图像)和 gtFine(包含对应的精细标注)。标注文件是.png格式,每个像素的灰度值对应一个类别ID。

第一步:制作颜色映射表(Colormap)。 这是关键一步。模型输出的是每个像素的类别ID(0-33),但我们需要可视化时看到的是彩色分割图。因此需要一个JSON文件来定义ID到RGB颜色的映射关系。我已经根据Cityscapes官方标准做好了 colormap_cityscapes.json 文件,你直接拿来用就行。文件内容大概长这样:

{
  "0": [0, 0, 0],
  "1": [70, 70, 70],
  "2": [190, 153, 153],
  ...
  "33": [64, 64, 128]
}

如果你未来想用在自己的数据集上,照着这个格式,为你的每个类别分配一个独特的RGB颜色即可。

第二步:编写数据生成器(Data Generator)。 这是用Keras训练大规模图像数据的标准做法。我们不能一次性把所有图片读进内存,而是写一个生成器,在训练时动态地加载和预处理一批批数据。核心任务包括:

  1. 读取图像和标签:用 tf.io.read_filetf.image.decode_png
  2. 统一尺寸:将图像和标签缩放到我们设定的模型输入大小(如512x1024)。这里有个超级重要的细节:对图像(RGB)进行缩放时,可以使用双线性插值(bilinear);但对标签(Label)进行缩放时,必须使用最近邻插值(nearest_neighbor。原因我在开头也提过,标签值是离散的类别ID,如果用双线性插值,可能会产生不存在的类别ID(比如在类别1和类别3之间插值出类别2),这会向模型注入错误信息。我实测过,用错插值方法,模型精度会直接掉2个百分点。
  3. 数据增强:为了提升模型泛化能力,可以在生成器里加入随机翻转、随机亮度/对比度调整等。对于街景数据,水平翻转是非常安全且有效的增强手段。
  4. 归一化:将图像像素值从[0, 255]范围归一化。对于Inception-ResNet V2这个Backbone,需要归一化到[-1, 1]之间。

3.3 项目代码结构:清晰明了才好维护

我把代码分成了两个主要文件,这样逻辑更清晰:

  • deeplab_v3_plus.py:这里是所有“零件”和“组装车间”。里面定义了模型构建的核心函数,比如 convolution_block(一个标准的卷积+BN+ReLU模块)、dilated_spatial_pyramid_pooling(ASPP模块)、以及最重要的 inceptionresnetv2_deeplabv3plus 模型主函数。所有模型架构相关的代码都集中在这里。
  • cityscapes_deeplab_v3plus.ipynb:这是一个Jupyter Notebook文件,是我们的“主控台”和“实验记录本”。所有训练流程、参数设置、可视化代码都写在这里。为什么用Notebook?因为训练模型是个反复调试的过程,你需要随时查看数据、查看中间输出、绘制损失曲线、评估模型效果。Notebook的交互式特性完美契合这个需求。你只需要打开这个文件,按顺序运行里面的代码块即可。

4. 模型训练全流程:从配置到调优

一切就绪,让我们启动训练。这个过程就像教一个新手画家,需要耐心地调整教学方法(超参数)。

4.1 初始化模型与损失函数

首先,在Notebook里调用 deeplab_v3_plus.py 中的函数创建模型。记得指定输入图片大小和类别数(Cityscapes是34类,包括背景)。

from deeplab_v3_plus import inceptionresnetv2_deeplabv3plus
model = inceptionresnetv2_deeplabv3plus(model_image_size=(512, 1024), num_classes=34)
model.summary() # 打印模型结构,检查是否构建正确

接下来是选择损失函数。语义分割最常用的是分类交叉熵损失(Categorical Crossentropy)。但这里有个关键:因为Cityscapes的标注图片里,有些像素是“忽略”类别(如车辆轮廓外的模糊区域),我们需要在计算损失时屏蔽掉这些像素。这可以通过 tf.keras.losses.SparseCategoricalCrossentropy 配合一个样本权重掩码(mask)来实现,或者使用 tf.keras.losses.CategoricalCrossentropy 并提前将忽略类对应的标签设为0权重。

我个人的选择是使用 Sparse Categorical Crossentropy,并在数据生成器里,将标签中的忽略类ID(通常是255)替换为一个有效的背景类ID(如0),同时在计算损失时忽略这些像素。这样实现起来相对直观。

4.2 优化器与学习率策略

优化器我推荐使用 Adam,它的自适应学习率特性让新手也能比较容易地训练出不错的结果。初始学习率可以设为1e-4。

但固定学习率不是最优的。当模型训练到后期,损失下降变慢时,如果学习率还是那么大,可能会在最优解附近震荡。因此,配置一个学习率衰减策略是很好的做法。TensorFlow 2.8的 tf.keras.callbacks.ReduceLROnPlateau 回调函数就非常方便。你可以设置当验证集损失连续3个epoch(patience=3)不再下降时,将学习率乘以一个因子(如0.5,factor=0.5)进行衰减。同时,再设置一个 EarlyStopping 回调,当验证损失长时间不下降时自动停止训练,防止过拟合。

我的经验是,在训练初期(前10个epoch),损失下降很快,之后会进入一个平台期。这时学习率衰减被触发,模型又能继续“挖掘”潜力,精度往往会再提升一点。

4.3 应对显存不足的实用技巧

用深度学习,显存(GPU Memory)永远是个紧张资源。DeepLab-V3+输入大尺寸图像时尤其如此。如果你在训练时遇到“OOM”(内存不足)错误,别慌,可以尝试下面两种方法:

  1. 减小批次大小(Batch Size):这是最直接有效的方法。在Notebook里找到 BATCH_SIZE 这个参数,把它调小,比如从8调到4或2。缺点是可能会让训练过程更不稳定,收敛慢一点。为了补偿,你可能需要更谨慎地调整学习率。
  2. 减小输入图像尺寸:在 deeplab_v3_plus.py 中修改 MODEL_IMAGE_SIZE。Cityscapes原始图像是2048x1024,我们可以按比例缩放,比如设为 (512, 1024) 或更小的 (320, 640)。图像变小,模型每一层需要处理的数据量就平方级地减少,显存占用会大幅下降。这是以牺牲一些细节信息为代价换取的可训练性。通常,对于街景分割, (512, 1024) 是一个在效果和效率之间不错的平衡点。

在我的单张RTX 3090(24GB显存)上,使用 (512, 1024) 的输入尺寸和 BATCH_SIZE=8 是可以顺利训练的。如果你的显卡是RTX 3080(10GB)或更小,可能需要将批次大小降到4,或者将图像尺寸降到 (384, 768)

4.4 训练过程监控与可视化

训练开始后,别干等着。在Notebook里,我们可以实时绘制损失和精度曲线。Keras的 model.fit() 方法返回的 history 对象就包含了每个epoch的训练和验证损失/精度。

更直观的是,定期在验证集上运行模型,并可视化分割结果。我通常会写一个函数,随机选取几张验证集图片,让模型预测,然后将原图、真实标签(用colormap上色后)和预测结果(同样上色后)并排显示。这样一眼就能看出模型在哪里分得好,哪里分得差。比如,模型可能把阴影区域错误地分成了道路,或者把远处的行人漏掉了。这些直观的观察,是后续进行针对性调优(比如增加相关数据增强)的重要依据。

5. 进阶优化与问题排查指南

模型能跑起来只是第一步,让它跑得更好、更稳才是进阶目标。下面分享几个我踩过坑后总结的优化技巧和常见问题解决方法。

5.1 数据增强的“艺术”

对于Cityscapes这样的街景数据,恰当的数据增强是提升模型泛化能力的廉价法宝。除了之前提到的随机水平翻转,还可以考虑:

  • 随机亮度/对比度调整:模拟不同天气和光照条件。
  • 随机缩放(小幅)和裁剪:让模型适应物体不同的大小。
  • 添加随机噪声或模糊:增强模型对图像质量下降的鲁棒性。

但要注意,不能对标签图进行除了几何变换(翻转、缩放、裁剪)之外的增强。颜色抖动、噪声等操作只应用于输入图像。同时,所有增强都应该是随机的、概率性的,而不是每张图都应用所有变换,否则可能会引入不自然的偏差。

5.2 类别不平衡与损失函数改进

Cityscapes数据集中,“道路”、“天空”、“建筑”这些大类别的像素数量,远远多于“交通标志”、“行人”等小类别。如果直接用标准交叉熵损失,模型会倾向于把所有像素都预测成大类,因为这样总损失最小。

为了解决这个问题,可以尝试 加权交叉熵损失(Weighted Cross-Entropy Loss)。思路很简单:为每个类别计算一个权重,稀有类别的权重大,常见类别的权重小。权重可以根据每个类别在训练集中出现的像素频率的倒数来计算。在TensorFlow中,你可以先计算好每个类别的权重,然后在调用损失函数时传入 class_weight 参数。

另一个更强大的方法是使用 Dice Loss 或 Focal Loss。Dice Loss直接优化分割任务中常用的Dice系数,对小目标更友好。Focal Loss则通过降低易分类样本的权重,让模型更专注于难分的样本(通常是那些稀有类别或边界像素)。你可以将交叉熵损失和Dice Loss结合起来,形成一个混合损失函数,我试过效果不错。

5.3 训练不收敛或效果差的排查清单

如果训练时损失居高不下,或者验证集精度很低,可以按以下清单检查:

  1. 数据预处理是否正确? 再次确认图像和标签的读取、缩放(特别是标签的最近邻插值)、归一化流程无误。可视化几对训练样本,看看标签是否对齐。
  2. 学习率是否合适? 学习率太大可能导致损失震荡甚至爆炸,太小则下降缓慢。尝试使用学习率查找器(Learning Rate Finder)策略,从一个很小的值(如1e-6)开始,逐步增大,观察损失下降最快的区间,将其作为初始学习率。
  3. 模型初始化有问题? 确保没有错误地冻结了Backbone的层。在训练初期,我们通常希望Backbone的权重能进行微调。检查 conv_base.trainable 是否为True。
  4. 过拟合了? 如果训练损失持续下降但验证损失不降反升,就是过拟合。可以尝试:增加Dropout比率(我代码中的 rate_dropout 参数)、添加更多的数据增强、使用权重正则化(如L2)、或者简单地收集更多数据。
  5. 评估指标是否合理? 对于语义分割,不要只看整体的像素精度(Accuracy),因为背景像素占比太高会拉高这个值。更应该关注平均交并比(Mean Intersection over Union, mIoU),它计算每个类别的预测区域与真实区域的重合度,然后取平均,更能反映模型对各个类别的分割能力。在Keras中,需要自己实现mIoU作为回调函数或评估指标。

5.4 推理部署与性能优化

模型训练好后,我们可能想把它用到实际项目中,比如处理视频流。这时需要考虑推理速度。

首先,使用 model.save() 保存整个模型(H5格式或SavedModel格式)。然后,在加载模型进行单张图片预测时,注意预处理(归一化)和后处理(将预测的类别ID矩阵通过colormap转换为彩色图)的流程要与训练时完全一致。

如果想提升推理速度,可以考虑以下方法:

  • 模型量化:使用TensorFlow Lite将模型从FP32转换为INT8精度,模型体积会缩小,推理速度大幅提升,对精度影响通常很小。
  • 更换轻量级Backbone:Inception-ResNet V2精度高但计算量大。可以考虑换成MobileNetV2或MobileNetV3作为Backbone,它们在速度和精度之间取得了很好的平衡。DeepLab-V3+官方论文也提供了基于Xception和MobileNetV2的变体。
  • 减小输入分辨率:在可接受的精度损失范围内,降低推理时的输入图片尺寸。

最后,分享一个我调试时的小习惯:在模型的关键位置(如ASPP模块输出后、解码器融合后)添加一些临时代码,打印出特征图的尺寸和取值范围。这能帮你确认数据流是否如你预期的那样流动,避免因为维度不对齐或数值异常导致的隐蔽错误。模型构建和训练是一个系统工程,耐心和细致的调试是成功的关键。希望这些从实战中得来的经验,能帮你少走弯路,更快地训练出属于你自己的高性能语义分割模型。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐