ARTICLE DETAIL

资讯详情

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

DeepLabV3+语义分割实战:从VOC数据集到课设验收全流程指南

DeepLabV3+语义分割实战:从VOC数据集到课设验收全流程指南 简介面向模式识别与机器学习课程设计及极市开发者平台打榜需求是以开源DeepLabV3为主干的项目包实现对水体及漂浮物的像素级分割并按面积阈值输出报警。适合具备Python和深度学习基础、需要完成结课项目或参与视觉分割竞赛的开发者。压缩包共258个文件含120张png标注图、100张jpg真实场景图、26个py脚本另有txt说明与sh辅助脚本整体约29.77MB。图像多为河流、海洋等水域场景py脚本覆盖数据预处理、模型训练与推理报警流程。已有223人浏览学习可快速理解打榜项目的整体框架。实际可借助代码与样本复现完整分割流程掌握面积阈值判断和报警输出设计也可基于开源模型迁移至类似检测或分割任务减少从零搭建成本。整体是课程设计与竞赛实践兼具的参考资源。1. 一个开源模型撑起模式识别课设DeepLabV3到底能做什么把DeepLabV3用在模式识别与机器学习的课程结课项目里是很多小组摸索之后发现的最稳路径。它做的是语义分割——不是给整张图一个类别而是对每个像素做分类输出和输入同尺寸的类别掩码。放到课设里它能直观展示特征提取、上下文建模、类别决策这三件事答辩时贴几张预测图老师就能看懂你们做了什么。真正拉开分数差距的其实不在模型结构而在数据准备、类别不平衡处理和mIoU验证这些环节上。这篇笔记就是一条按着顺序做就能走通的小组结课项目路线数据、模型、训练、避坑、验收一段一段说清楚。2. 数据准备决定天花板VOC数据集、Resize与标注对齐语义分割课设和普通图像分类课设最大的差别在于标签不是普通图片而是索引图。很多小组第一行代码就栽在这里用读RGB的方式读分割标签模型训练出来的东西完全没法看。数据这一层没做好后面换什么backbone都救不回来。2.1 为什么先拿PASCAL VOC 2012打底先跑通一个开源数据集再决定要不要换自建数据这是语义分割课设最常见的节奏。PASCAL VOC 2012一共20个物体类别加背景共21类数据规模在一万多张既够训练一个像样的baseline又不会像COCO那样一上来就是十几万张、类别80类把小组的精力全耗在等待上。如果你的课设题目已经限定在某个垂直场景比如“航拍道路分割”“工业零件表面缺陷分割”也建议先用VOC跑通整个流程再加载自建数据做二次迁移。这个“先通用后专用”的路线本身就能在答辩时讲成一个小故事你们用开源数据验证了DeepLabV3的baseline能力再迁移到自己的场景逻辑上是完整的。不要一上来就自己标注几百张图标注质量一旦出问题模型训练全程都在跟脏数据搏斗很难定位是模型的问题还是标签的问题。2.2 目录结构与最小文件清单VOC 2012的标准目录结构是这样的拿到数据集后先按这个结构核对一遍再写代码。路径内容课设里怎么用JPEGImages原始RGB图片jpg格式模型输入SegmentationClass与图片同名的png标签训练目标像素值为类别索引ImageSets/Segmentation/train.txt图片id列表每行一个训练集划分ImageSets/Segmentation/val.txt图片id列表每行一个验证集划分SegmentationClass里的png和普通真彩图不是一回事。它是一张索引图每个像素的数值就是类别ID0表示背景1到20表示物体类别255在VOC里通常作为边缘忽略区。你在看图软件里看到的各种颜色是色板映射出来的视觉效果不是模型要读的RGB通道。理解这一点后面的读图代码才不会写错。2.3 加载数据集VOC格式的正确读法下面是一个可以直接放进项目里的数据集类按VOC标准格式读取图片和标签。import os import torch import numpy as np from torch.utils.data import Dataset from PIL import Image class VOCSegDataset(Dataset): def __init__(self, root, splittrain, img_size512): self.root root self.split split self.img_size img_size self.img_dir os.path.join(root, JPEGImages) self.mask_dir os.path.join(root, SegmentationClass) id_file os.path.join(root, ImageSets, Segmentation, split .txt) with open(id_file, r) as f: self.ids [line.strip() for line in f.readlines()] def __len__(self): return len(self.ids) def __getitem__(self, idx): img_id self.ids[idx] # 图片按RGB读 img Image.open(os.path.join(self.img_dir, img_id .jpg)).convert(RGB) # 标签按P模式读保留像素值即类别索引 mask Image.open(os.path.join(self.mask_dir, img_id .png)) # 训练集做随机水平翻转验证集不做增强 if self.split train: img img.transpose(Image.FLIP_LEFT_RIGHT) mask mask.transpose(Image.FLIP_LEFT_RIGHT) # 图片用双线性缩放到固定尺寸标签用最近邻缩放 img img.resize((self.img_size, self.img_size), Image.BILINEAR) mask mask.resize((self.img_size, self.img_size), Image.NEAREST) # 归一化到[-1, 1]附近的区间使用ImageNet统计量 img np.asarray(img, dtypenp.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img (img - mean) / std img torch.from_numpy(img).permute(2, 0, 1) # 标签直接读成int64像素值本身就是类别号 mask np.asarray(mask, dtypenp.int64) mask torch.from_numpy(mask).long() return img, mask这里有两个参数必须注意。第一标签的resize只能用Image.NEAREST不能用双线性插值。双线性会插出0.5、1.6这类非整数数值交叉熵损失拿到这样的目标值会直接算错。第二mask最终是(H, W)的long型张量不是(C, H, W)的one-hot形式。PyTorch的CrossEntropyLoss接收的target本来就是(N, H, W)的类别索引不需要自己做one-hot转换。这个数据集类把图片缩放到固定大小所以多个样本可以直接用DataLoader默认的collate_fn堆成batch不会因为尺寸不齐报错。如果以后换成不resize的方案就得自己写collate_fn按最大边padding那是另一个复杂度课设阶段没必要碰。2.4 自建小数据集时最容易踩的标注坑如果小组决定自建数据集常见工具是LabelMe画多边形后导出成VOC格式。导出后不要急着训练先写一个检查脚本统计每张标签图里出现的像素值。from PIL import Image import numpy as np def check_mask(path): mask np.array(Image.open(path)) classes np.unique(mask) print(path, 包含类别编号:, classes) # 正常情况0 背景1~N 对应自定义类别 # 如果混入255或预期外的编号说明标注导出色板有问题这个脚本能快速暴露两类问题。一是导出时类别编号错位比如背景变成了1、第一个物体类别变成了2所有类别整体偏移二是混入了255或随机噪声像素这通常来自标注工具保存时的压缩抖动。VOC原本用255表示边缘忽略区但自建数据里如果像素值只是零星噪点把它当ignore_index处理反而会教模型忽略那些区域。更稳的做法是标注时避开边界导出后检查确认只有0到N-1这些连续值。注意背景0是正常类别在交叉熵里要参与训练。self建数据集时不要因为“背景不重要”就把0从损失里排除那样网络会连背景都分不清可视化时整个图都是花的。3. 模型搭建与backbone选型ASPP、Decoder与跑得动的配置数据准备好之后第二个关键决策就是模型怎么搭。DeepLabV3不是只能照抄别人的训练脚本它的结构其实很清晰一个backbone做特征提取然后接ASPP模块做多尺度特征融合最后用一个Decoder把低层特征和高层特征拼起来恢复边界细节。把这个结构讲明白答辩时不虚。3.1 DeepLabV3的两个关键设计ASPP和DecoderASPP的全称是Atrous Spatial Pyramid Pooling空洞空间金字塔池化。它用多个不同空洞率的卷积并行处理同一份特征相当于用不同大小的感受野去看同一个区域。小感受野抓局部纹理大感受野抓全局上下文拼在一起再投影让网络同时拥有“看清细节”和“看懂场景”的能力。decoder部分则是DeepLabV3区别于DeepLabV3的核心。DeepLabV3直接把ASPP输出上采样到原始分辨率边界的锯齿感很强。DeepLabV3的做法是把backbone里靠近输入的低层特征也拿出来经过一层1x1卷积压缩通道后与ASPP输出上采样4倍的结果做拼接再卷积融合。低层特征保留了更多空间位置信息正好弥补空洞卷积和连续下采样丢失的边界细节。这个Decoder对课设这种小数据量任务提升很明显也是老师最喜欢问的点。3.2 backbone选型显存和精度的博弈backbone决定了整个模型的显存占用和训练速度是课设选型里最不能拍脑袋决定的一环。backbone显存压力精度典型表现训练速度课设适配度MobileNetV2低中等快首选普通笔记本也能跑ResNet50中较好中有独立显卡时可选ResNet101高最好慢不推荐课设时间耗不起课设环境大多只有一张显卡甚至有些人用的是学校的云GPU显存8G到12G. 512x512的输入加上ResNet50已经是比较吃紧的组合。我的建议是如果之前没跑过语义分割直接用MobileNetV2先拿到一版完整结果再决定要不要换ResNet50。先保证“有结果”再谈“结果更好”。3.3 最小可跑的ASPP实现课程设计不一定要从零手写整个DeepLabV3最省力的方式是直接用mmsegmentation这类开源训练库选deeplabv3plus系列配置把num_classes改成自己的类别数就能跑。但答辩时如果只知道调库、说不清结构容易丢分所以至少要能把ASPP写明白。下面这个ASPP模块可以直接放在自己的model.py里用于理解结构。import torch import torch.nn as nn import torch.nn.functional as F class ASPP(nn.Module): def __init__(self, in_channels, out_channels256, rates(6, 12, 18)): super().__init__() self.branches nn.ModuleList() # 1x1卷积分支相当于用感受野最小的方式提取特征 self.branches.append(nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) )) # 三个不同空洞率的3x3卷积分支覆盖多尺度上下文 for r in rates: self.branches.append(nn.Sequential( nn.Conv2d(in_channels, out_channels, 3, paddingr, dilationr, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) )) # 全局平均池化分支压缩成全局描述再广播回去 self.branches.append(nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) )) # 所有分支拼起来后做一次投影控制通道数 self.project nn.Sequential( nn.Conv2d(out_channels * (len(rates) 2), out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) ) def forward(self, x): size x.shape[-2:] outs [] for branch in self.branches: out branch(x) # 池化分支输出是1x1要插值回当前特征图尺寸 if out.shape[-2:] ! size: out F.interpolate(out, sizesize, modebilinear, align_cornersTrue) outs.append(out) return self.project(torch.cat(outs, dim1))代码里的rates(6, 12, 18)是DeepLab系列常用的一组空洞率对应的感受野从7到37不等覆盖了从近处细节到远处上下文的范围。in_channels由backbone输出通道决定MobileNetV2最后一层输出是1280ResNet50最后一层输出是2048使用时按实际情况传入。3.4 迁移学习加载预训练权重的关键一步语义分割模型直接随机初始化训练效果通常很差。常见做法是加载在ImageNet上预训练过的backbone权重这属于迁移学习的范畴也是模式识别与机器学习课设里最容易讲出亮点的部分。# 加载预训练权重时如果类别数不一致分类头会被跳过 model DeepLabV3Plus(num_classes6) state_dict torch.load(pretrained_backbone.pth, map_locationcpu) model.load_state_dict(state_dict, strictFalse)这里有个细节经常被忽略strictFalse会让加载器打印一堆“missing key”和“unexpected key”的警告。看到缺失的key集中在最后的分类卷积上是正常的因为你的类别数跟预训练权重不一致。但如果你发现缺失的key出现在backbone或者ASPP里说明预训练权重和你的网络结构对不上要回头检查backbone的输出维度是否匹配而不是硬着头皮往下训。另外要如实说清楚torchvision自带的是deeplabv3_resnet101它跟DeepLabV3不是同一个结构少了一个Decoder。如果课设题目明确要求DeepLabV3别拿torchvision那个模型改名顶替答辩老师一眼就能看穿。要么自己照着mmsegmentation实现Decoder要么就用mmsegmentation的deeplabv3plus配置来训练这两种方式都站得住。4. 训练参数实战损失函数、学习率与类别不平衡怎么配模型结构定了之后训练参数就成了决定整个课设命运的部分。同样是DeepLabV3有人跑出来mIoU能到0.6有人跑出来只有0.1差别主要不在显卡而在损失函数怎么配、学习率怎么降、类别不平衡怎么处理。4.1 损失函数CrossEntropy是基线什么时候加Dice Loss逐像素分类任务的默认损失就是CrossEntropyLossPyTorch里一行代码就能用。VOC有21类直接对每个像素算交叉熵整个batch取平均。但背景像素在大多数图中占比很高普通交叉熵会让网络过度关注背景损失数值降得很漂亮前景却一塌糊涂。常见的做法是对类别做加权让样本少的类别获得更大的损失权重。import torch # class_counts 是每个类别在整个训练集里出现的像素总数 # 用根号倒数软化权重避免背景占比过大导致前景权重爆炸 weights 1.0 / torch.sqrt(class_counts 1e-6) weights weights / weights.sum() * len(class_counts) criterion torch.nn.CrossEntropyLoss(weightweights, ignore_index255)torch.sqrt是一种常用的软化技巧。如果直接用像素占比的倒数背景占90%时前景权重会非常高损失值动不动就上千学习率完全没法调。加了根号之后权重压缩到一个温和的范围内模型训练稳定得多。ignore_index255必须带上否则VOC标签里那些边缘忽略区域会被当成类别255参与反向传播训练过程会非常诡异。Dice Loss在医学分割里很流行它直接优化区域重叠度对类别不平衡更敏感但梯度不平滑多分类场景下一开始就上Dice很容易把训练带崩。课设的合理用法是先跑一版交叉熵作为baseline如果某些小类别IoU确实上不去再考虑把Dice Loss按一定权重加进来对比。4.2 学习率策略poly衰减是DeepLab系列的加分项语义分割模型的学习率策略和普通分类不太一样。DeepLab系列最经典的做法是poly衰减让学习率在整个训练过程中按多项式曲线平滑下降既在前段保持足够的探索能力又能在后段收敛到稳定区域。def poly_lr(epoch, max_epoch, base_lr0.01, power0.9): return base_lr * (1 - epoch / max_epoch) ** power严格按iteration计算更准确但课设阶段按epoch算也够用。如果你只训练30到50个epochpoly衰减和固定学习率差距不大可以直接用固定学习率省事。优化器方面语义分割最稳的组合是SGD加momentum。优化器常用初始学习率适用情况SGD0.01到0.02配合momentum0.9weight_decay1e-4课设最稳AdamW0.001到0.002batch很小或SGD不收敛时更稳学习率要调低实操时有个血泪经验batch size如果只有4BatchNorm的统计量会非常不稳损失曲线来回震荡。这时候不要盲目加学习率先把batch调到8或16。显卡不够就用梯度累计等效放大batch效果比调学习率更直接。4.3 类别不平衡模型把整张图都预测成背景怎么办这件事值得单独拿出来说。VOC数据里背景像素占比经常超过70%如果数据过滤不严格模型很容易收敛到“所有像素都预测背景”的局部最优解。损失数值看着在降可视化出来全图一片黑mIoU趋近于零。除了加权损失还有一个非常管用的前置步骤过滤掉那些完全没有前景物体的训练图片。VOC本身大多数图片都含目标但自建数据里大量“只有背景”的图片非常常见。import numpy as np from PIL import Image def filter_fg_ids(root, split_file): keep [] with open(split_file) as f: for line in f: img_id line.strip() mask np.array(Image.open(f{root}/SegmentationClass/{img_id}.png)) # 像素值种类大于1说明至少含一个前景类别 if len(np.unique(mask)) 1: keep.append(img_id) return keep这个过滤步骤放在数据集切分阶段执行一次不影响DataLoader的加载速度。它的作用不是让模型不学背景而是避免训练batch里出现整批全是背景图的情况让每一次梯度更新都对区分前景有实际帮助。配合加权损失全背景预测的问题基本能解决。5. 避坑与常见问题排查小组课设里最容易翻车的5个环节这部分是几个小组项目里反复出现的问题按“现象、原因、解决”的顺序整理按图索骥就行。5.1 训练loss正常下降但验证集所有输出都是背景现象训练损失从1.0降到0.3曲线很好看验证集mIoU却只有0.01预测图全黑一张物体轮廓都看不到。原因类别不平衡环境下网络发现“全预测背景”就能拿到很低的交叉熵损失。这个局部最优非常强尤其是batch里经常出现全背景图片时。损失下降不代表模型在学前景它只是在把背景像素的置信度越调越高。解决先做上一节提到的前景过滤保证每个batch至少有几张带物体的图然后给CrossEntropyLoss加类别权重最后别用最后一个epoch的权重做验收保存验证集mIoU最高的checkpoint训练后期模型经常在最后几个epoch跑偏。5.2 显存不足ResNet50在普通笔记本上直接OOM现象训练脚本启动后没多久就报CUDA out of memory学生笔记本上的GTX系列显卡尤其常见。原因512x512输入加ResNet50加上ASPP里多分支空洞卷积中间特征占用非常大。batch设成2都可能撑不住而batch太小又会导致BatchNorm不收敛。解决backbone换成MobileNetV2显存占用能降一半以上输入尺寸从512降到384甚至320速度提升明显且精度损失不大batch保持在8以上不够时用梯度累计。另外很多人忽略的是DataLoader的num_workers开太多也会抢显存如果OOM发生在数据加载阶段先把num_workers设成0试一次。5.3 mIoU很高但可视化翻车指标欺骗了你们现象验证集mIoU有0.68看起来很体面随机挑几张训练样本图一看小物体全丢了边缘全是锯齿只有大块类别看得清。原因mIoU是所有类别IoU的平均值。VOC里大类别样本多IoU高小类别如bottle、pottedplant样本少IoU很低。这个低值被平均到整体数值里之后被大类别的高分掩盖了。解决训练结束后输出逐类IoU表格不要只看总mIoU。如果某个小类别IoU只有0.1单独标记出来在报告里说明原因并提出改进方向。这比硬撑着说“我们mIoU达到了0.68”更有说服力。5.4 验证集也做随机翻转指标虚高几个点现象验证集结果比自己预期好很多但拍成视频或跑新场景时表现明显偏差。原因数据加载代码复用了训练分支验证时也执行了随机翻转、随机裁剪。验证评估变成了“开卷考试”同一张图翻过来再预测一次模型相当于在宽松条件下作答指标自然偏高。解决数据增强只加在训练集分支验证集只做resize和归一化。这个判断标准很简单验证流程里出现随机函数不管有没有random关键词都算开卷。把训练和验证的数据处理拆成两个逻辑分开维护。5.5 小组多人环境不一致队友的代码跑不起来现象负责训练的队友交出一个训练好的模型文件其他人加载时发现缺算子、报版本错误重训又复现不出原来的精度。原因PyTorch、mmsegmentation、PIL、numpy四个库只要有一个小版本不同行为就会有差异。小组各跑各的环境最后合代码时全是地狱。解决项目根目录放一份requirements.txt用pip freeze requirements.txt生成精确到小版本。更稳的做法是整个小组共用同一台服务器或同一个conda环境不要每人一台机器各装各的。训练时统一随机种子虽然不能完全消除差异但大部分问题是环境引起的环境统一了问题就少一大半。6. 验收与答辩前做冲刺mIoU怎么算、结果图怎么出彩模型训练完距离提交还有一道验收关卡。不要拿训练日志里的loss去答辩那只能说明模型在训练集上拟合了。老师要看的是验证集上的mIoU和逐类指标。6.1 写一个独立于训练流程的评估脚本训练代码和评估代码混在一起容易在加载权重时出差错。单独写一个eval.py输入模型和验证集输出逐类IoU。import numpy as np import torch def compute_miou(preds, masks, num_classes21, ignore_index255): # preds和masks都是(N, H, W)的long型张量 preds preds.reshape(-1) masks masks.reshape(-1) valid masks ! ignore_index preds, masks preds[valid], masks[valid] ious [] for c in range(num_classes): inter ((preds c) (masks c)).sum().item() union ((preds c) | (masks c)).sum().item() if union 0: ious.append(float(nan)) # 该类别未出现在当前验证集 else: ious.append(inter / union) return ious # 平均时用nanmean跳过验证集里不存在的类别 ious compute_miou(preds, masks) print(mIoU:, np.nanmean(ious))计算时用(preds c) (masks c)求交集用(preds c) | (masks c)求并集。漏掉ignore_index过滤是常见错误VOC的255边界区域一旦混入IoU会略微失真。输出逐类IoU后按类别排序小类别垫底是正常的关键是能解释原因。6.2 可视化输出做三列对比答辩时最直观的武器就是对比图。用matplotlib把原图、真实标签、预测标签横着排成三列每张图下面标注文件名。这个比花哨的交互式demo更能说明问题——老师能一眼看到模型在哪里错、错成什么样。可视化时注意标签图要用色板着色不要直接显示灰度索引图。灰度图在投影仪上几乎看不清。没时间写着色函数的话可以直接用VOC官方提供的voc12_colormap网上到处都是这份标准色板照着转就行。6.3 答辩前至少跑一次小消融准备一张简单的对比表固定住epoch和输入尺寸只改一个变量。表格不需要复杂三到四行就够。配置mIoU备注MobileNetV2 CrossEntropy0.58baselineMobileNetV2 加权CrossEntropy0.64类别不平衡改进ResNet50 加权CrossEntropy0.67换backbone提升这张表回答了两个答辩必问题“你们为什么选这个模型”和“你们的工作比baseline好在哪里”。我一般会把对应类别的IoU明细也打印出来贴在报告附录里被追问时直接翻到那一页比自己现场回忆靠谱得多。这个小习惯救过我很多次也算是在这个课设方向上最值得留给自己的一份“后悔药”。希望帮到你。本文还有配套的精品资源点击获取
返回列表