☰
DeepLabv3+图像分割实战:Pytorch训练VOC与Cityscapes避坑指南
2026/10/18 7:19:21 网站建设 项目流程

简介:基于PyTorch在VOC与Cityscapes数据集上训练DeepLabv3+图像分割模型的项目源码与教程资源包,面向希望系统掌握语义分割完整流程的计算机视觉学习者与研究者,覆盖数据预处理、模型构建、训练评估到应用部署等核心环节,可迁移至自动驾驶、医学影像分析等场景。压缩包共43个文件,包括23个Python脚本、17个分割效果示意图、2个文本说明和1个Markdown文档,整体约2.13MB;其中py脚本涵盖网络定义、数据加载、训练评估与预测可视化,png图片展示不同数据集的预测对比,便于直观验证模型表现。目前已有493人学习。教程详细拆解DeepLabv3+的关键设计:空洞卷积、ASPP与解码器模块,并结合VOC的20类与Cityscapes的30类标注数据,给出数据增强、学习率策略、IoU评估及微调优化等实战指导;从环境依赖配置到逐模块原理解读,每步配有可运行源码,适合边读边练、快速上手。目录结构按功能拆分,便于快速定位所需模块。

1. 先说结论:DeepLabv3+ 在 VOC 和 Cityscapes 上到底能学到什么

图像分割这几年被问得最多的一个问题是:同样一套分割网络,为什么换一个数据集效果就天差地别?很多初学者拿着一个在 VOC 上跑得不错的模型,直接丢到 Cityscapes 的 19 类城市街景上,结果 mIoU 掉到 20% 都不奇怪。这个落差不是网络结构的问题,而是数据集差异、类别体系、训练策略三件事叠加出来的。基于 Pytorch 在 VOC+Cityscapes 上训练 DeepLabv3+ 图像分割算法,核心价值在于让你亲手经历一次完整的语义分割落地流程——从数据准备、模型搭建、训练调参到评估推理,看清楚一个分割模型从实验室指标到实际场景之间隔着哪些坎。这套源码加流程教程面向的不是想读论文的人,而是想真正把分割模型跑起来、把 mIoU 提上去、把训练脚本改到自己项目里的从业者,新手能循着流程走出第一步,熟手则能在参数和数据策略层面少走弯路。

2. DeepLabv3+ 架构拆解:为什么它至今仍是分割落地的稳妥选择

2.1 从空洞卷积到 ASPP:理解 DeepLabv3+ 的多尺度语义捕获原理

DeepLabv3+ 在分割任务里的地位很特殊。它不是最新的,但它是被工程验证最多的。整套架构建立在两个核心设计上:空洞卷积(Dilated/Atrous Convolution)和 ASPP 模块(Atrous Spatial Pyramid Pooling)。空洞卷积解决的核心问题是:如何在不下采样的情况下扩大感受野。普通卷积要扩大感受野只能加深网络或者加大卷积核,但这两者在分割任务里都会带来分辨率损失。空洞卷积在卷积核内部插入空洞,比如 rate=2 的 3×3 卷积,实际覆盖范围是 7×7,但参数量不变,特征图分辨率也不变。这个特性让 DeepLabv3+ 能在保持较高空间分辨率的前提下,把高层语义特征铺满整个图。

ASPP 模块则是对空洞卷积的进一步组合。它并行使用多个不同空洞率的空洞卷积,常见配置是 rate 分别为 6、12、18,再加上一个 1×1 卷积和全局平均池化分支。其用意在于:不同空洞率捕获不同尺度的上下文信息——小空洞率聚焦近距离细节,大空洞率覆盖远距离上下文,全局池化则提供整图的语义背景。在 Cityscapes 这种大物体多、小物体杂的数据集上,ASPP 的多尺度能力尤其重要,这也是 DeepLabv3+ 在街景分割上长期作为标杆的原因之一。

2.2 Encoder-Decoder 结构:为什么解码器比想象中更重要

DeepLabv3+ 相比前辈 DeepLabv3 的改进,核心不在 encoder 而在 decoder。很多人误以为分割就是把分类网络最后几层换掉,实际远没那么简单。DeepLabv3+ 的 encoder 输出的 stride 通常是 16(也可以设成 8),特征图长宽只剩输入的 1/16,这对分割任务来说空间信息损失过大。Decoder 的作用是把低分辨率的语义特征逐步恢复到原分辨率,但关键技巧是:不能只靠简单上采样,而是要将 encoder 中低层的高分辨率特征(通常是 backbone 第 2 个 stage 的输出,stride=4)与高层上采样结果在通道维度拼接,再经过两层 3×3 卷积融合。

这里有个工程上容易掉坑的点:低层特征虽然空间分辨率高,但语义信息弱,包含大量噪声;高层特征语义强但边界模糊。拼接之后如果不做足够的卷积融合,模型容易学到两类特征的简单叠加,导致输出边界会出现"双重边缘"或毛刺。所以 decoder 里的 3×3 卷积层数、通道数(通常设为 48)以及拼接前的缩放方式,都是直接影响分割精度的细节。

2.3 Backbone 选型:ResNet101 与 MobileNetV2 的取舍

在这套源码流程里,backbone 通常给两个选项:ResNet101 和 MobileNetV2。ResNet101 是精度优先的选择,在 VOC 上 mIoU 可以到 77% 以上,在 Cityscapes 上配合合适的训练策略可以到 78% 上下,但显存占用和推理延迟都不低。MobileNetV2 则把参数量压缩了一个量级,适合在 GPU 资源有限或需要实时推理的场景里使用,代价是精度通常会比 ResNet101 低 3 到 5 个百分点。

我的建议是:如果你只是想先把流程跑通、理解训练机制,先用 MobileNetV2,它训练速度快、显存占用小,能在几小时内完成一轮完整训练;等你确认数据加载、损失计算、评估流程都没问题,再切到 ResNet101 做最终精度验证。不要一上来就用 ResNet101,一旦训练流程或数据代码有 bug,一次跑十几个小时才发现,极浪费时间和精力。

3. 数据准备:VOC 与 Cityscapes 的格式差异和预处理细节

3.1 VOC 数据集的读取与类别映射:从 JPEGImages 到 SegmentationClass

PASCAL VOC 2012 的分割标注是 PNG 格式的单通道索引图,每个像素值对应一个类别 ID,背景是 0,类别从 1 开始(aeroplane=1, bicycle=2 ...)。数据读取的难点在于:VOC 的标注图是从 0 到 20 的索引值,但很多公开代码里会把 255(边界/忽略区域)也标进去,如果直接用 CrossEntropyLoss 计算,255 会被当成一个真实类别参与梯度计算,导致边界区域干扰训练。标准做法是在损失计算时把 ignore_index 设为 255。

VOC 的类别体系只有 20 个前景类别加背景,这套体系在自然图像上表现不错,但如果你要做的是街景分割,就得面对 Cityscapes 的 19 类体系,后者更细,比如车辆会拆分成 car、truck、bus、motorcycle、bicycle,行人单独成类。类别体系不同意味着你不可能拿 VOC 训练的模型直接做 Cityscapes 推理,这不仅仅是数据分布差异,而是语义空间根本对不上。

# 读取 VOC 分割标注的参考实现 from PIL import Image import numpy as np def read_voc_mask(mask_path): mask = Image.open(mask_path) mask = np.array(mask, dtype=np.uint8) # VOC 标注中 255 表示 ignore 区域,需要单独标记 ignore_mask = (mask == 255) # 可选:将 ignore 改成 255,后续 loss 中设置 ignore_index=255 return mask, ignore_mask

逻辑说明:PIL 直接读取 PNG 能拿到单通道索引图,不需要做 one-hot 编码,因为 Pytorch 的 CrossEntropyLoss 接受的是类别索引张量。ignore_mask 的作用是让你在可视化或评估时区分真实类别与忽略区域。

3.2 Cityscapes 数据集的准备:gtFine 与 19 类训练体系的转换

Cityscapes 的原始标注是 34 个类别,但标准训练只使用其中的 19 类,其余类别(如城墙、地形变化等)要么合并要么忽略。源码流程里通常会提供一个转换脚本,把原始 GTFine 标注映射到 19 类的 trainIds。这步是 Cityscapes 训练里最常见的出错点,原因在于:很多人直接从官网下载数据,没注意标注文件中存在 255 和 -1 等特殊值,导致训练时 loss 直接 NaN。

Cityscapes 的标注处理要注意三点:第一,原图分辨率是 2048×1024,直接送入网络对显存要求极高,常见做法是在线随机裁剪到 1024×512 或 960×512;第二,类别映射必须精确,例如原始类别 ID 24 对应 motorcycle,要映射到 trainId 17,如果映射表写错,模型会把摩托车学成汽车;第三,忽略区域的像素值统一设为 255,与 VOC 保持一致,方便共用训练代码。

# Cityscapes 19 类映射的关键片段(只列出部分映射) cityscapes_19_map = { 0: 255, # unlabeled -> ignore 1: 255, # ego vehicle -> ignore 2: 255, # rectification border -> ignore 3: 0, # road -> road 4: 1, # sidewalk -> sidewalk 6: 2, # building -> building 7: 3, # wall -> wall 8: 4, # fence -> fence 10: 5, # pole -> pole # ... 其余类别略 } def convert_cityscapes_label(original_label): converted = np.full_like(original_label, 255, dtype=np.uint8) for src_id, dst_id in cityscapes_19_map.items(): converted[original_label == src_id] = dst_id return converted

注意:convert 之后建议做一次像素值分布统计,确认没有遗漏的原始类别 ID 没被映射,否则这些像素会变成 255 被 loss 忽略,但会让可视化的分割结果出现大片黑色空洞,影响你对模型效果的判断。

3.3 数据增强策略:随机缩放、裁剪与翻转的参数设置

分割任务的数据增强和分类完全不同。分类可以随意裁剪缩放,因为物体位置不重要;分割则必须保证标注图与输入图做完全相同的几何变换。源码流程里常见的是对图像和 mask 做同步的随机翻转、随机缩放和随机裁剪。随机缩放的缩放因子通常取 0.5 到 2.0 之间,缩放到一个随机尺寸后再裁剪到固定输入大小。你需要注意的是,如果用 PIL 做缩放,mask 的插值方式必须设置为Image.NEAREST,否则类别边界会被平滑插值产生新的伪类别像素,这是分割任务里非常隐蔽的数据污染来源。

# 同步变换图像和 mask 的核心代码(简化版) import random from PIL import Image def sync_transform(image, mask, crop_size=(512, 512)): # 随机水平翻转 if random.random() > 0.5: image = image.transpose(Image.FLIP_LEFT_RIGHT) mask = mask.transpose(Image.FLIP_LEFT_RIGHT) # 随机缩放 scale = random.uniform(0.5, 2.0) new_w, new_h = int(image.width * scale), int(image.height * scale) image = image.resize((new_w, new_h), Image.BILINEAR) # 关键:mask 必须用 NEAREST 插值 mask = mask.resize((new_w, new_h), Image.NEAREST) # 随机裁剪到固定尺寸 x = random.randint(0, new_w - crop_size[1]) y = random.randint(0, new_h - crop_size[0]) image = image.crop((x, y, x + crop_size[1], y + crop_size[0])) mask = mask.crop((x, y, x + crop_size[1], y + crop_size[0])) return image, mask

参数说明:Image.BILINEAR用于图像缩放能保留一定细节,但对 mask 绝对不能用,必须换成NEAREST。crop_size 的选择取决于你 GPU 显存——8GB 显存跑 ResNet101 建议用 512×512,显存充足可以提高到 768×768 提升精度。

4. Pytorch 实现:模型搭建、损失计算与训练循环的完整串联

4.1 用 Pytorch 搭建 DeepLabv3+ 的步骤分解

使用 Pytorch 搭建 DeepLabv3+ 有很多种方式,既可以从头手写,也可以基于 torchvision 的实现做修改。源码流程里常见的做法是手写关键模块,因为这样更容易在后续替换 backbone 或魔改结构。DeepLabv3+ 的整体结构可以分为四个部分:backbone 特征提取、ASPP 多尺度模块、Decoder 解码模块、最后的分类卷积层。下面给出一组可运行的关键代码片段和对应的理解方式。

import torch import torch.nn as nn import torch.nn.functional as F class ASPP(nn.Module): def __init__(self, in_channels, out_channels=256, rates=(6, 12, 18)): super().__init__() self.branches = nn.ModuleList() # 1x1 卷积分支 self.branches.append(nn.Sequential( nn.Conv2d(in_channels, out_channels, 1), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True) )) # 多个空洞卷积分支 for rate in rates: self.branches.append(nn.Sequential( nn.Conv2d(in_channels, out_channels, 3, padding=rate, dilation=rate), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True) )) # 全局平均池化分支 self.branches.append(nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, out_channels, 1), nn.ReLU(inplace=True) )) def forward(self, x): res = [] for branch in self.branches: out = branch(x) # 全局池化分支需要上采样回原尺寸 if out.shape[-2:] != x.shape[-2:]: out = F.interpolate(out, size=x.shape[-2:], mode='bilinear', align_corners=True) res.append(out) return torch.cat(res, dim=1)

逻辑说明:ASPP 输出通道是 256×5=1280,后续还需要一层 1×1 卷积将通道降下来再送入 decoder。align_corners=True在上采样时能保持坐标对齐,对分割边界一致性有帮助,训练和推理时必须保持相同设置。

class Decoder(nn.Module): def __init__(self, in_channels, low_level_channels, num_classes): super().__init__() # 低层特征先做通道压缩 self.low_level_conv = nn.Sequential( nn.Conv2d(low_level_channels, 48, 1), nn.BatchNorm2d(48), nn.ReLU(inplace=True) ) self.conv1 = nn.Conv2d(in_channels + 48, 256, 3, padding=1) self.bn1 = nn.BatchNorm2d(256) self.conv2 = nn.Conv2d(256, 256, 3, padding=1) self.bn2 = nn.BatchNorm2d(256) self.classifier = nn.Conv2d(256, num_classes, 1) def forward(self, high_level_feat, low_level_feat): low = self.low_level_conv(low_level_feat) high = F.interpolate(high_level_feat, size=low.shape[-2:], mode='bilinear', align_corners=True) x = torch.cat([high, low], dim=1) x = F.relu(self.bn1(self.conv1(x))) x = F.relu(self.bn2(self.conv2(x))) x = self.classifier(x) # 直接输出 logits,不在这里做 softmax return x

这里有一个新手爱犯的错误:把 final softmax 放进模型里输出。训练时 CrossEntropyLoss 自带 softmax,如果你在模型输出层先做了 softmax,等于是双重 softmax,梯度会异常,收敛会变慢。正确做法是模型只输出 logits,softmax 或者 argmax 放到推理阶段再做。

4.2 损失函数选择:CrossEntropyLoss 与 ignore_index 的正确使用

分割任务的标准损失就是 Pixel-wise CrossEntropyLoss,Pytorch 的nn.CrossEntropyLoss原生支持ignore_index参数。对于 VOC 和 Cityscapes,都要把 ignore_index 设为 255,这样才能忽略标注为忽略区域的像素。

from torch import nn def build_loss(num_classes, ignore_index=255): criterion = nn.CrossEntropyLoss( ignore_index=ignore_index, reduction='mean' ) return criterion

有个值得注意的细节:当你的数据集中存在大量忽略区域时(比如 Cityscapes 的边界),reduction='mean'会把 loss 在有效像素数上做平均,这会导致有效像素少的样本对整个 batch 的 loss 贡献变小,模型可能偏向忽略区域较多的样本。更稳的做法是统计每个 batch 中有效像素的比例,如果低于某个阈值,就把这个 batch 的 loss 单独分析甚至跳过的策略,但常见流程里直接使用ignore_index=255一般够用。

4.3 训练循环的完整代码:学习率策略、权重初始化与 checkpoint

分割模型训练耗时较长,一套完善的训练循环必须有学习率调整策略、模型保存机制和中间评估逻辑。这里重点说两个容易被低估的点:poly 学习率策略和 checkpoint 保存方式。所谓 poly 学习率,是指基础学习率乘以(1 - iter / total_iters) ** 0.9,相比 step decay,它能在训练后期更平缓地微调权重,对分割模型 mIoU 的提升有明显帮助。

def poly_lr(base_lr, curr_iter, total_iters, power=0.9): return base_lr * (1 - curr_iter / total_iters) ** power # 训练循环核心骨架 def train_one_epoch(model, loader, optimizer, criterion, epoch, total_epochs): model.train() total_loss = 0.0 for i, (images, masks) in enumerate(loader): images, masks = images.cuda(), masks.cuda() # 计算 poly 学习率并更新到 optimizer cur_iter = epoch * len(loader) + i total_iters = total_epochs * len(loader) lr = poly_lr(0.01, cur_iter, total_iters) for param_group in optimizer.param_groups: param_group['lr'] = lr optimizer.zero_grad() logits = model(images) loss = criterion(logits, masks) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(loader)

注意上面学习率更新方式是按 iteration 更新的。如果你按 epoch 更新,后期学习率下降太慢,精度会卡在某个瓶颈附近上不去。

关于权重初始化:backbone 部分通常使用 ImageNet 预训练权重,这是必不可少的,ASPP 和 decoder 部分是随机初始化。很多教程只字不提预训练权重,导致一个只有数百轮训练的小模型在 VOC 上 mIoU 只能到 30%。下载预训练权重的时间比你自己从头训练节省至少一半以上,这个成本必须花。

5. 踩坑避坑指南:VOC 和 Cityscapes 训练里最常见的 5 个翻车现场

5.1 翻车点一:训练 loss 正常下降,但 mIoU 一直是 0 或 1%

现象:训练过程 loss 从 2.5 慢慢降到 1.0 左右,看起来在收敛,但验证集上 mIoU 却只有 0.x,几乎什么类别都没预测出来。

原因:最常见的是 label 与 model 输出的类别对应关系错位。比如 Cityscapes 的原始 label 里,背景像素值是 255,你把它转成 tensor 时忘了减 1 或做映射,导致模型把几乎所有像素都预测成了背景类,而真实类别像素只有极少数,mIoU 趋近于 0。

解决:第一步先做数据可视化。把一批训练图像的 mask 用 matshow 或保存成伪彩色图直接看,确认像素值分布合理。第二步检查 dataloader 返回的 mask 张量 min 和 max,跟 num_classes 做比较,确认没有超出范围的索引值。

5.2 翻车点二:模型在 VOC 上表现好,换到 Cityscapes 直接崩

现象:用同一套代码把数据集从 VOC 换成 Cityscapes,训练后效果远差于预期,mIoU 连 40% 都到不了。

原因:Cityscapes 图像分辨率太高,直接 resize 到 512×512 会导致小物体(比如交通灯、摩托车)被压缩到几个像素,几乎不可分辨。另一个原因是 Cityscapes 类别不平衡严重——road 类别占大量像素,pole、traffic light 等类别像素占比极小,默认的 CrossEntropyLoss 对高频类别有天然偏向。

解决:第一,输入分辨率从 512 提高到 768 或 1024,配合 batch size 减小来匹配显存;第二,使用类别加权损失或者至少统计类别频率,确认高频类别不会淹没低频类别;第三,如果你跑的是 ResNet101,务必使用 Cityscapes 预训练权重或先在 CoCo 上预训练过的权重,否则模型对街景的底层特征认知是缺失的。

5.3 翻车点三:显存溢出(CUDA out of memory)

现象:训练跑不到 10 个 iteration 就报 CUDA out of memory,尤其在切换 ResNet101 后。

原因:ResNet101 的 ASPP 和 Decoder 中间特征图占显存极大,输入 768×768 时中间特征图尺寸接近 96×96×1024,一个 batch 里 8 张图叠加显存直接爆掉。

解决:第一原则不是减小 batch size,而是检查输入分辨率是否过高,Cityscapes 建议起始用 512×512 逐步提升;第二是把 batch size 调成 2 或 4,用梯度累积模拟大 batch;第三是打开 Pytorch 的torch.backends.cudnn.benchmark = True,并确保输入尺寸固定,让 cuDNN 选择最优卷积算法,能有效降低显存峰值。

import torch # 训练开头加上这两行,有奇效 torch.backends.cudnn.benchmark = True torch.backends.cudnn.deterministic = False

注意:deterministic=False会让卷积结果有轻微随机性,但对训练影响极小。如果你需要完全可复现,可以考虑设为 True,但某些操作会变慢。

5.4 翻车点四:验证集 mIoU 忽高忽低,同一个 epoch 结果不稳定

现象:每次跑验证集,mIoU 波动 3 到 5 个百分点,看起来像模型没收敛。

原因:大概率是验证时开了 BatchNorm 的 training 模式,或者验证数据集太小导致统计波动过大。分割模型在验证时必须切换到 eval 模式,否则 BatchNorm 层会用当前 batch 的统计量而非全局统计量,对单张图像推理影响尤其大。

解决:在验证代码前明确调用model.eval(),并且把torch.no_grad()包住整个验证循环。另外,验证集如果只有几十张图,mIoU 的置信区间很宽,建议使用完整验证集评估,或记录多次评估的平均值再对比。

5.5 翻车点五:损失下降到后期出现周期性尖刺然后恢复

现象:训练 20 个 epoch 后 loss 在某个 batch 突然飙高,然后又迅速回落到正常水平。

原因:检查你的 dataloader 里是否有损坏或异常图像/标注。常见情况是某张 Cityscapes 标注图在转换时出现了未映射的特殊像素值,比如 33 或 -1,导致该 batch 的 loss 异常。

解决:写一段验证代码,遍历整个训练集的每张 mask,打印其中的类别 ID 集合,并与你期望的类别 ID 列表做对比,找出所有不匹配的样本,要么修正要么剔除。这个步骤只跑一次,但能省下你排查一整天的时间。

6. 提升 mIoU 的实战技巧:从 70% 到 75% 的四个关键优化

6.1 使用 OHEM(在线难例挖掘)策略聚焦难样本

当 mIoU 卡在 70% 左右时,继续训练和调学习率都很难再突破。一个最直接有效的优化是采用 Online Hard Example Mining。OHEM 的做法是:对每个输入 batch,用模型计算所有像素的 loss,然后只取 loss 最大的一部分像素(比如前 25%)参与梯度回传,抑制大量易分类像素对梯度的主导作用。这对 Cityscapes 尤其有效,因为 road 类别容易学,低频类别难学,OHEM 强制模型在这些困难像素上多花精力,mIoU 提升在 2 到 3 个点。

def ohem_loss(logits, masks, ignore_index=255, threshold_ratio=0.25): num_classes = logits.shape[1] # 计算逐像素 loss loss_map = F.cross_entropy( logits, masks, ignore_index=ignore_index, reduction='none' ) # 找出有效像素 mask valid_mask = masks != ignore_index if valid_mask.sum() == 0: return loss_map.mean() # 将忽略区域的 loss 设为无穷,便于后续选取最小值 loss_map_inf = loss_map.clone() loss_map_inf[~valid_mask] = float('inf') # 展平后取 loss 最小的前 25% 像素 k = int(valid_mask.sum().item() * threshold_ratio) topk_loss, _ = torch.topk(loss_map_inf.flatten(), k, largest=False) return topk_loss.mean()

参数说明:threshold_ratio是选取难样本的比例,工程经验值在 0.2 到 0.3 之间。比例太低会让梯度大部分来自边界噪声,比例太高又退化成普通 CE loss,建议从 0.25 起步调试。

6.2 多尺度推理与翻转融合:让验证集 mIoU 再上一个台阶

训练阶段的增强是随机的,推理阶段同样可以做增强融合。常见做法是:对每张验证图像分别以 [0.75, 1.0, 1.25] 三个尺度缩放输入,同时将每张图做原图和水平翻转共两张,总共得到 6 个预测结果,取平均后再 argmax。这个技巧通常可以稳定提升 0.5 到 1.5 个 mIoU,而且不需要重新训练。

def ms_inference(model, image, scales=(0.75, 1.0, 1.25), flip=True): model.eval() original_h, original_w = image.shape[-2:] preds = [] with torch.no_grad(): for scale in scales: new_h, new_w = int(original_h * scale), int(original_w * scale) scaled_img = F.interpolate(image, size=(new_h, new_w), mode='bilinear', align_corners=True) logits = model(scaled_img) logits = F.interpolate(logits, size=(original_h, original_w), mode='bilinear', align_corners=True) preds.append(torch.softmax(logits, dim=1)) if flip: flip_img = torch.flip(scaled_img, dims=[3]) flip_logits = model(flip_img) flip_logits = torch.flip(flip_logits, dims=[3]) flip_logits = F.interpolate(flip_logits, size=(original_h, original_w), mode='bilinear', align_corners=True) preds.append(torch.softmax(flip_logits, dim=1)) final_prob = torch.stack(preds, dim=0).mean(dim=0) return final_prob.argmax(dim=1)

多尺度推理对显存要求较高,批量评估时建议固定 batch size 为 1 或者 2,确保每次只处理少量图像。

6.3 使用边界感知损失优化分割边界质量

如果你发现 mIoU 已经不错,但分割结果的边界粗糙、存在粘连,可以考虑在 CrossEntropyLoss 基础上加一个边界损失项。做法是:从真实 mask 中提取边界像素,给这些位置的 loss 加权。这个技巧对 Cityscapes 的 road 和 sidewalk 的分界线改善比较明显。不过要注意边界像素占比很小,对应的边界权重不能设得过高,否则会干扰整体语义收敛。

6.4 模型蒸馏:用小模型逼近大模型精度

如果你的部署场景只能跑 MobileNetV2,但想要 ResNet101 的精度,知识蒸馏是一个实际工程中最常用的妥协方案。用已经训练好的 ResNet101 权重在训练集上生成软标签(即 logits 经过温度缩放的概率分布),然后用 MobileNetV2 同时拟合真实 mask 和教师模型的软标签。教师模型的学生模型损失通常设为0.7 * CE(软标签) + 0.3 * CE(真实标签),这个比例需要实验调整。

这条路径的落地门槛比前面几个都要高,但对于真正想投入分割模型落地的团队来说,收益最大。我自己在实际项目里用蒸馏把 MobileNetV2 的 mIoU 从 68% 提到了 72%,推理速度几乎没有下降。这个方向值得你认真尝试——即使只做到一次完整的蒸馏实验,你对分割模型的 capacity 边界和数据信息量的理解都会深入一个层次。希望这些调试技巧对你有实际帮助。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询