ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeepLabV3+语义分割实战:从课程设计到mIoU评估全流程指南

DeepLabV3+语义分割实战:从课程设计到mIoU评估全流程指南 简介这份资源是一个模式识别与机器学习课程的小组结课项目同时也是极市开发者平台打榜项目。项目以开源DeepLabV3模型为主干对水体及水面漂浮物进行像素级分割并依据面积阈值判断、输出报警消息属于典型的语义分割加规则后处理应用。压缩包共258个文件约29.77MB涵盖120张png、100张jpg图像样本26个Python脚本以及sh、txt、md等配置与说明文件结构上兼顾数据、代码与文档便于对照学习。目前已有223人学习下载。资源对准备课程设计、参加算法比赛或入门工业视觉的读者均有参考价值可从中获取完整的数据读取、模型训练、推理分割与报警判定流程帮助理解DeepLabV3在实际场景中的落地方式也可结合自身需求迁移到水质监测、漂浮物识别等类似任务中。1. 从“结课项目”看DeepLabV3为什么选了它而不是扫一眼就能跑通的分类模型“模式识别与机器学习”这门课的结课项目最尴尬的状态是小组里有人用迁移学习跑了个猫狗分类代码不到50行答辩时老师一问训练细节就冷场。DeepLabV3天然是打破这种局面的选择——它比二分类多一个“像素级建模”难度跨度正好适合小组分工它有大量开源模型权重可以直接下载不需要从零训练它的结构又能牵扯出空洞卷积、ASPP、编码器-解码器这些可以“考一考”的知识点。这个标题想做的事情其实不是“学会部署某个开源模型”而是走通一个标准的模式识别工程流程准备数据、加载预训练模型、定义损失函数、训练调参、评估可视化最后在答辩现场把预测结果展示出来。接下来的内容会按照一个最可靠的小组项目方案把这套流程拆开讲包括可以直接抄的代码和参数表。这次的内容适合正在做课程设计的学生也适合想快速把语义分割落地到团队演示的工程师。2. DeepLabV3原理与选型先讲清空洞卷积和ASPP再谈课程设计适配2.1 从模式识别角度看语义分割任务语义分割在模式识别里属于稠密分类问题它要求模型对图像中的每一个像素输出类别标签而不是像图像分类那样只给整张图一个标签。DeepLabV3在2018年提出是DeepLab系列里同时结合了编码器-解码器结构和空洞卷积的版本。常见的做法是编码器使用ResNet或MobileNet作为骨干网络提取特征并在最后几个阶段把普通卷积替换成空洞卷积这样在不下采样过多的情况下特征图的感受野可以变得更大。ASPP模块再接在骨干网络之后用几个膨胀率不同的空洞卷积并行运算把不同尺度的上下文信息拼在一起。解码器负责把编码器输出的粗糙预测逐步上采样并和骨干网络浅层的高分辨率特征做融合从而恢复物体边界。这一套结构对课程设计很友好因为每个组件都能单独讲解空洞卷积是模型的核心卖点ASPP是多尺度特征的体现而解码器又是典型的“模式识别特征融合”思路。2.2 DeepLabV3与U-Net、SegFormer的取舍在小组项目答辩时老师很可能问为什么不是U-Net为什么不用Transformer这就要求小组至少能把三个方向的差异讲清楚。U-Net的对称编码器-解码器和跳连接结构简单实现成本低但它的原始设计没有使用预训练好的骨干网络后期虽然有很多变体但质量参差。SegFormer这类基于Transformer的模型在语义分割上有更好精度但训练数据要求高而且对于课程设计常见的几万张图片规模显存占用会明显变大。DeepLabV3的好处在于它是把“空洞卷积”这个知识点塞进了模型里同时又有大量在ImageNet上预训练好的开源模型权重可用比如ResNet34、ResNet50小批量数据也能快速收敛。从小组分工的角度讲一个组员负责整理数据集一个组员负责训练脚本一个组员负责推理可视化这套边界在DeepLabV3项目中非常自然。模型核心特点课程设计适配度训练成本U-Net对称编码解码跳连丰富结构容易讲但预训练质量参差低DeepLabV3空洞卷积 ASPP Decoder预训练权重多原理有深度中SegFormerTransformer 多尺度混合精度高但对数据量和显存要求高高2.3 用segmentation_models_pytorch加载一个可训练的最小模型现在的开源生态里PyTorch官方torchvision只内置了DeepLabV3不是原版V3所以最省事的方案是使用第三方库segmentation_models_pytorch它直接暴露了DeepLabV3Plus接口并且支持常用编码器和ImageNet预训练权重。安装和加载代码很短pip install segmentation-models-pytorchimport segmentation_models_pytorch as smp model smp.DeepLabV3Plus( encoder_nameresnet34, encoder_weightsimagenet, classes21, # 数据集的类别数PASCAL VOC 是 20 类 背景 activationNone, # 不接 sigmoid/softmax后面直接用 CrossEntropyLoss ) print(model)这段代码里最重要的三个参数是encoder_name、encoder_weights和classes。encoder_name决定骨干网络resnet34对显存需求较低适合单人单卡如果小组有较好GPU可以换成resnet50。encoder_weightsimagenet表示加载在ImageNet预训练权重这是整个项目能快速收敛的关键如果填写None就是从随机初始化训练训练时间和精度都会明显恶化。classes必须和数据集的实际类别数一致否则输出通道数不对计算损失时直接报错。activationNone意味着模型输出的是未归一化的logits这样在训练时使用交叉熵损失是最标准的做法不要在模型里提前加softmax。提示如果你的训练数据不是PASCAL VOC而是自建的“道路汽车天空”这样的场景只要把classes改成类别数并保证mask像素值为0、1、2这样的连续整数即可。3. 小组课程设计的数据准备与训练流程从标注到跑通一个epoch3.1 课程数据集的来源与预处理在模式识别与机器学习的课程设计里数据准备往往比模型训练更费时间。如果课程允许使用公开数据集最省事的是下载PASCAL VOC 2012的子集它包含20个物体类别和背景而且mask是像素级标注直接就能喂给DeepLabV3。如果小组想要一个更贴近“认识猫”这类趣味场景的主题也可以用LabelMe或Label Studio自建一个小数据集拍50到100张照片用多边形框标注出猫、沙发、地板等3到5个类别。但我建议自建数据前先想清楚像素级标注非常耗时一个类别的边缘复杂时标注一张512x512的图可能要10分钟所以小组内至少要安排两个人专门负责标注另外一个人负责写训练脚本。数据准备好之后要做两件事划分和预处理。划分一般按6:2:2分成训练集、验证集和测试集测试集只在最终评估时用一次。预处理时先把所有图片和mask统一缩放到固定尺寸比如512x512然后用ImageNet的均值和标准差做归一化。这里有一个隐藏坑mask的插值方式必须用最邻近法interpolationcv2.INTER_NEAREST否则会把类别标签插成小数损失函数直接崩溃。加载数据时建议使用PyTorch的Dataset和DataLoader不要手工写for循环读取图片这样后续做数据增强和batch打乱都方便。3.2 DataLoader、损失函数与优化器怎么选直接给一个可用的数据加载和训练循环骨架重点看损失函数和优化器的选法import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset import cv2 import numpy as np class SegDataset(Dataset): def __init__(self, image_paths, mask_paths, size512): self.image_paths image_paths self.mask_paths mask_paths self.size size def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image cv2.imread(self.image_paths[idx]) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) mask cv2.imread(self.mask_paths[idx], cv2.IMREAD_GRAYSCALE) image cv2.resize(image, (self.size, self.size)) mask cv2.resize(mask, (self.size, self.size), interpolationcv2.INTER_NEAREST) image image / 255.0 # 注意归一化必须和预训练权重一致 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) image (image - mean) / std image torch.from_numpy(image).permute(2, 0, 1).float() mask torch.from_numpy(mask).long() return image, mask train_loader DataLoader(train_set, batch_size8, shuffleTrue, num_workers4) model smp.DeepLabV3Plus(encoder_nameresnet34, encoder_weightsimagenet, classes5, activationNone) loss_fn nn.CrossEntropyLoss(ignore_index255) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience2)这段代码的核心点都写在注释里了。loss_fn选择了带ignore_index255的交叉熵这样标注图里用255标记的混淆区域不会参与梯度计算。optimizer用Adam初始学习率1e-3这是课程设计里最容易收敛的组合如果训练轮数超过30个epoch可以把优化器换成SGDmomentum设为0.9初始学习率降到1e-2精度会更稳。scheduler使用ReduceLROnPlateau当验证集loss连续两个epoch不下降时自动把学习率减半这一步常常被小组忽略但恰恰是最终mIoU能进入答辩图表的关键。3.3 训练循环里的监控与保存有了DataLoader和优化器训练循环就不复杂了。我一般会这样组织代码best_iou 0.0 for epoch in range(30): model.train() running_loss 0.0 for images, masks in train_loader: images, masks images.cuda(), masks.cuda() optimizer.zero_grad() outputs model(images) loss loss_fn(outputs, masks) loss.backward() optimizer.step() running_loss loss.item() val_loss, val_iou evaluate(model, val_loader) scheduler.step(val_loss) print(fEpoch {epoch:02d} | train_loss {running_loss/len(train_loader):.4f} | val_iou {val_iou:.2f}) if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), best_model.pth)这个循环里最值得注意的有两点。第一model.train()和model.eval()必须切清楚因为BatchNorm在训练和推理时的行为不一样很多课程设计组的模型在测试时忘记切回eval模式导致分割结果带噪声。第二验证集的mIoU每次都在变化把在验证集上表现最好的权重保存下来而不是把最后一个epoch的权重直接提交这能让答辩演示的结果更稳定。如果训练过程中loss不下降先检查数据归一化再检查是不是忘了给mask做long()转换这两个问题占了语义分割训练失败原因的七成。4. 模式识别课上的评估与演示mIoU怎么算答辩怎么讲4.1 理解mIoU和像素准确率课程报告里必须有这张表课程设计报告里如果只写“准确率98%”很容易被老师追问。语义分割的标准评估指标是mIoU平均交并比它先对每个类别计算预测区域和真实区域的交并比再对所有类取平均。比起整体像素准确率mIoU能更真实地反映模型在各个类别上的表现尤其是物体占比较小的类别。常见做法是在验证集上完整跑一遍推理把每个类别的IoU统计出来最后汇总成一张表格。一个典型的结果表格可能是下面这样类别IoU (%)像素数占比 (%)背景96.178.2猫84.512.4沙发81.76.8地板90.22.6均值88.1-这张表的作用是让答辩老师一眼看出哪个类别是短板。比如沙发的IoU偏低多半是因为训练集中沙发的样本少或者拍摄角度多变。在答辩时主动说出这个观察比被动等老师问效果好得多。4.2 计算mIoU的代码处理好混淆矩阵和255边界计算mIoU的代码不复杂重点是要把所有预测和真值拉平并忽略值为255的像素def compute_miou(pred, gt, num_classes, ignore_index255): pred: 模型输出的类别索引数组shape (H, W) gt: 对应真值数组shape (H, W) pred pred.flatten() gt gt.flatten() mask gt ! ignore_index pred pred[mask] gt gt[mask] ious [] for cls in range(num_classes): p pred cls g gt cls intersection (p g).sum() union p.sum() g.sum() - intersection if union 0: ious.append(intersection / union) return sum(ious) / len(ious)这段代码采用逐类计算的方式而不是直接建立全量混淆矩阵原因是语义分割里像素数可能达到几十万一次性构建num_classes * num_classes的矩阵在类别很多时会有额外内存开销。逐类计算虽然循环慢一点但逻辑清楚更适合课程设计里的调试。注意mask gt ! ignore_index这一行它先把需要忽略的像素全部过滤掉否则那些“未标注”区域会把IoU拉低。4.3 可视化分割结果与小组分工的演示流程评估做完还要有一张“原图-真值-预测”并排对比的图才能放进PPT。常见做法是读取训练好的模型对几张验证集图片推理把预测类别的颜色映射到固定色板再用matplotlib画成三列子图。这里有个容易被忽略的细节在调用模型推理前一定要重新加载图像、做同样的归一化然后把输出用torch.argmax取类别索引不要直接对softmax结果取整。小组内可以按数据、模型、训练、可视化四个角色分工其中训练和可视化由同一个组员负责会让衔接更顺畅。答辩演示时尽量做一个简单的交互脚本启动后读取本地图片显示原图和分割结果这样比现场跑训练集更能体现项目完成度。5. 进阶技巧DeepLabV3在小数据上提升mIoU的3个必调参数5.1 冻结encoder的early layers课程设计的数据量通常不会很大几十张到几百张图很容易让模型在10个epoch后过拟合。一个很实用的技巧是在加载预训练权重后冻结骨干网络的浅层只训练深层和decoder部分。因为预训练模型在ImageNet上学到的底层边缘、纹理特征在小数据集上依然有效重新训练反而会破坏这些特征。代码实现for name, param in model.encoder.named_parameters(): if name.startswith(layer1) or name.startswith(layer2): param.requires_grad False这段代码的意思是让前两层浅层的参数不参与梯度更新保留ImageNet学到的通用特征。深层layer3和layer4仍然可训练去适配你的语义分割标签。这个方法同时还能降低反向传播的显存占用对单卡训练特别友好。5.2 用albumentations做在线增强语义分割的数据增强必须同时作用于图像和mask且mask插值必须最邻近。常见的增强组合是RandomCrop、HorizontalFlip、ShiftScaleRotate和Normalize。注意归一化在增强之后再执行。from albumentations import Compose, HorizontalFlip, RandomCrop, ShiftScaleRotate, Normalize train_transform Compose([ RandomCrop(512, 512), HorizontalFlip(p0.5), ShiftScaleRotate(shift_limit0.1, scale_limit0.1, rotate_limit15, p0.5), Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ])RandomCrop可以避免resize带来的边界模糊ShiftScaleRotate能模拟拍摄角度变化Normalize必须使用和encoder预训练权重相同的均值和标准差否则训练会不稳定。5.3 用多尺度推理抵消边界误差最后一个技巧不是训练阶段而是推理阶段。DeepLabV3对多尺度输入有一定鲁棒性但单尺度推理时物体边缘会出现锯齿。测试时增强TTA最简单的一种做法是把同一张图分别缩放到0.75、1.0、1.5倍分别推理后把logits双线性插值回原尺寸再取平均最后再取argmax。这样通常能提升1到2个mIoU点而且不增加训练时间。要注意的是多尺度推理时的model.eval()和torch.no_grad()必须做好否则显存占用会翻好几倍。在答辩前的最后一晚把5.3的TTA代码加进去重新跑一次验证集你就能得到一张数字更好看的评估表。本文还有配套的精品资源点击获取
返回列表