资讯详情

场景语义分割毕设指南:从模型选型到mIoU调优实践

📅 2026/10/7 18:33:56 | 华诺云谱 👁 阅读
场景语义分割毕设指南:从模型选型到mIoU调优实践
简介一份面向计算机视觉方向毕设与课程作业的深度学习场景语义分割项目资源包。内容聚焦图像像素级分类任务覆盖从数据准备、模型设计、训练优化到评估部署的完整流程适合需要搭建可复现语义分割系统的学生开发者参考。压缩包共二十四个文件以Python脚本为主约十个包含模型训练与测试、数据加载及预处理工具另有若干XML工程配置、TXT说明与PNG效果图便于快速定位项目结构与运行结果整套资源仅820KB轻量易用。已有158人浏览学习。通过该资源可入手一套带工程配置的语义分割代码框架包括数据迭代器、网络模块、训练入口与结果示例帮助理解Encoder-Decoder等架构的落地实现兼顾Python实验与C部署的工程化思路同时为毕设答辩或课程设计报告提供代码与结果支撑。1. 毕设选场景语义分割值得投入的像素级分类硬门槛如果你的毕设和课程作业落在“深度学习场景语义分割”恭喜这是一个下限不低、上限足够高的方向。所谓场景语义分割就是把一张照片里每个像素都归类到某个语义类别里比如道路、行人、车辆、天空核心产出是一张和输入分辨率一致的分割图。相比图像分类只回答“这是什么”语义分割回答的是“每个像素是什么”这也是自动驾驶、遥感解译、医学影像、工业质检里真正能落地的技能点。很多同学拿到这个选题的第一反应是“是不是只要把分类网络改成FCN就能跑”。确实能跑但跑到能写进毕设的水平中间隔着数据集组织、损失函数调整、mIoU指标计算、显存优化这些具体工程问题。这篇笔记我按自己做过几轮分割项目的流程来讲先选型和确定指标再写数据管道然后跑训练并验证最后给出一份避坑清单和答辩前的调优套路。适合把毕设从“能跑通”做到“能讲清楚、能演示、指标拿得出手”的从业者和学生。2. 场景语义分割的模型选型与方法基底从FCN到DeepLabV3的权衡2.1 先搞清楚任务边界像素级分类和mIoU这个指标场景语义分割本质上是一个逐像素分类任务输入是 HxWx3 的彩色图输出是 HxW 的类别索引图每个位置上的数值代表该像素属于哪一类。所以模型结构的最后一层必然是空间维度不变、通道数等于类别数的卷积或上采样输出配合 softmax 或 argmax 得到结果。评价指标上别被 Pixel Accuracy 误导。如果一张图里车辆只占 2% 像素全预测成背景也能拿到 95% 以上的像素准确率这在毕设答辩里一问就塌。要做就盯 mIoU它是每个类别的交集除以并集的平均。公式上对类别 i计算预测为 i 且真实也为 i 的像素数TP预测为 i 但真实不是 i 的像素数FP真实为 i 但没被预测出来的像素数FNIoU_i TP / (TPFPFN)最后对所有类别取平均。每类 IoU 都公平参与平均小目标类和大背景类等权这样你的模型才会真的去学“细小的行人和远处的交通灯”而不是只学一个“大面积天空”。这也是为什么 mIoU 几乎成了语义分割论文和比赛的默认口径。顺带说多类别还有 per-class accuracy、F1 这些二级指标但对毕设来说主表放 mIoU辅表放每类 IoU 看差异就足够撑起分析章节了。指标计算我建议直接用混淆矩阵来求间接又不易出错第四章会给完整代码。2.2 模型选型的四个梯队不追新选能解释的语义分割模型迭代很快但毕设场景我不建议一头扎进最新论文复现。选型标准依次是现有预训练权重好不好找、训练成本能不能压住、网络结构能不能用一两页 PPT 讲清楚、复现别人的分数有多大把握。常见选型梯队大概是这样梯队代表作结构特点毕设适用度全卷积祖辈FCN-32s/16s/8s分类骨干换成卷积输出上采样回原尺寸适合讲原理做 baseline编码器-解码器U-Net、SegNet对称跳连或池化索引恢复细节U-Net 在医学/工业场景好用空洞卷积家族DeepLabV2/V3/V3、PSPNet空洞卷积扩大感受野多尺度池化/ASPPDeepLabV3 最稳妥论文和代码都多新式混合体Swin-UNet、SegFormer、Mask2FormerTransformer 或掩码机制效果好但训练和调参门槛高解释性略难我一般给环境一般、重点是毕业设计要出结果的建议是baseline 用 FCN-8s 或 U-Net主模型用 DeepLabV3backbone 选 ResNet50这种组合在 PASCAL VOC 上能拿到不错的 mIoU且加载 ImageNet 预训练的 ResNet50 后收敛快得多。如果你做的是遥感或者医疗数据这类小数据集U-Net 家族反而更占优势因为它们不需要超大预训练训练所需 epoch 也少。这里有个选型上的关键判断DeepLabV3 的“好”主要来自 ASPP空洞空间金字塔池化它用多个不同膨胀率的空洞卷积并联对同一特征图以不同感受野采样能把多尺度物体的特征都抓一遍。这个模块在答辩时非常好讲你把膨胀率画成几个并列分支老师们一眼就明白。PSPNet 的思路类似用的是金字塔池化做全局上下文融合也很经典。选一个深入吃透比两个都浅尝辄止强得多。2.3 损失函数配置交叉熵打底类别不平衡再上Focal默认损失函数就是交叉熵PyTorch 里一句 CrossEntropyLoss 就够了。它在计算时对每个像素的预测概率求负对数底层已经结合了 softmax所以模型输出的 logits 直接传进去即可不要画蛇添足自己再 softmax 一次数值会过一遍再变差。交叉熵在类别大体均衡时没问题但场景里车辆、行人这类小目标天然像素占比少损失会被天空、道路这类大类主导。如果你看一下训练日志发现小类别 IoU 特别低常见做法是换 Focal Loss。它的核心改动是给每个像素的损失加一个调制因子 (1-p_t)^γ预测置信度越高、损失贡献越低让模型把注意力引向难分类的像素。在 Cityscapes 这种类别极不平衡的数据集上γ 取 2.0 是比较常见的起点。还有一个对症损失的加分项是 dice loss 或 Lovasz-Softmax它们直接近似 IoU 这类指标做优化对小目标和边缘有一定改善。代价是收敛波动更大且对学习率敏感。我的个人建议是课程作业用纯交叉熵毕设如果分析点不够可以加一组“交叉熵 vs 交叉熵Focal”的对比实验这比盲目切换损失函数更安全。# 类别不均衡时给CrossEntropyLoss传入类别权重也非常直观 import torch.nn as nn # 假设有21类用训练集各类像素频次做负对数归一化作为权重 class_counts torch.tensor([1000, 20, 50, 5], dtypetorch.float) # 示意数据 weights 1.0 / (class_counts 1e-6) weights weights / weights.sum() * len(class_counts) # 归一化到数量级一致 criterion nn.CrossEntropyLoss(weightweights, ignore_index255)这段代码里ignore_index255是分割任务的标配参数它表示标注图中数值为 255 的像素不参与损失计算通常是边界、裁切到图像外的区域或者不想监督的难标注像素。这个参数在 Dataloader、损失函数、mIoU 计算里要保持一致否则你会在验证时发现分数不错但可视化里预测把边界全糊了。损失函数和指标计算的一致性是语义分割里最容易翻车也最容易被马虎带过的点。3. 数据集组织与Dataloader实现标注图不能随便resize的细节3.1 数据集选型VOC与Cityscapes怎么选目录怎么摆场景语义分割最容易起步的公开数据集是 PASCAL VOC 2012 增强版21 类含背景分辨率不高但类别典型标注规范网上预训练模型、调参资料最多。想挑战更高难度就上 Cityscapes2975 张训练图19 类街景分辨率大、类别细但训练速度慢很多。如果导师给的是自采数据或者遥感、医疗数据那就按你自己的数据来但格式最好也转化为下面这个目录结构方便直接用现成 Dataloader。常见的数据集根目录组织方式是这样的dataset/ ├── JPEGImages/ # 原始RGB图像统一命名如 000001.jpg ├── SegmentationClass/ # 语义分割标签PNG格式像素值为类别索引 ├── ImageSets/ │ └── Segmentation/ │ ├── train.txt # 训练列表每行一个不带后缀的文件名 │ ├── val.txt │ └── trainval.txt一个容易踩的规矩是原始图可以用 jpg 压缩但标签图必须存成无损的 PNG。因为分割标注是逐像素类别索引如果标签也用 jpg 压缩压缩产生的伪影会在物体边缘产生大量错标像素训练出的模型边缘全是脏的。另外如果自己制作数据用 LabelMe 或 Paddle 标注工具导出的 json 要转成 PNG 掩膜转的时候确认类别索引从 0 开始连续编号别出现空洞的类别序号否则 CrossEntropyLoss 的类别数会多算模型输出维度也对不上。PDF 版数据集协议偶尔会把未标注区域标成 255这就是前面说的 ignore_index 存在的意义。打开标签图后可以先统计像素值分布看看有没有超出类别数的值通常在 0 到num_classes - 1和 255 之间才是合法的。3.2 手写一个同步变换的Dataloaderflip、random crop和resize的坑语义分Dataloader 和分类任务最大的不同在于对输入图像做的大小变换、翻转、旋转必须对标签图做完全一致的变换。很多同学第一次写分割训练代码拿现成分类 Dataloader 改把图像的resize做了标签还是原尺寸于是 CrossEntropyLoss 形状不匹配直接报错或者 torchvision 的某些 transform 随机操作在图像和标签上各随机一次导致分割图和标签错位模型在训练集上就学不出来只涨 loss 不掉。这类问题防不胜防我养成的好习惯是图像和标签永远走同一个随机种子在一个函数里完成同步变换。import cv2 import numpy as np import torch from torch.utils.data import Dataset class SegDataset(Dataset): def __init__(self, img_dir, label_dir, img_list, num_classes, crop_size(512, 512), is_trainTrue): self.img_dir img_dir self.label_dir label_dir self.img_list [line.strip() for line in open(img_list)] self.num_classes num_classes self.crop_size crop_size self.is_train is_train def __len__(self): return len(self.img_list) def sync_transform(self, img, label): # 两个图共用同一个随机种子保证几何变换完全一致 seed np.random.randint(0, 1_000_000) # 随机水平翻转 if self.is_train: np.random.seed(seed) if np.random.random() 0.5: img cv2.flip(img, 1) label cv2.flip(label, 1) # 随机缩放模拟远近变化 np.random.seed(seed 1) scale np.random.uniform(0.5, 2.0) h, w img.shape[:2] new_h, new_w int(h * scale), int(w * scale) img cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) label cv2.resize(label, (new_w, new_h), interpolationcv2.INTER_NEAREST) else: img cv2.resize(img, self.crop_size[::-1], interpolationcv2.INTER_LINEAR) label cv2.resize(label, self.crop_size[::-1], interpolationcv2.INTER_NEAREST) return img, label def __getitem__(self, idx): name self.img_list[idx] img_path f{self.img_dir}/{name}.jpg label_path f{self.label_dir}/{name}.png img cv2.imread(img_path, cv2.IMREAD_COLOR) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) label cv2.imread(label_path, cv2.IMREAD_GRAYSCALE) img, label self.sync_transform(img, label) # 若是训练再做随机裁剪若是验证中心裁剪或直接resize亦可 if self.is_train: seed np.random.randint(0, 1_000_000) np.random.seed(seed) h, w img.shape[:2] crop_h, crop_w self.crop_size if h crop_h and w crop_w: top np.random.randint(0, h - crop_h) left np.random.randint(0, w - crop_w) else: top, left 0, 0 img img[top:topcrop_h, left:leftcrop_w] label label[top:topcrop_h, left:leftcrop_w] img img.astype(np.float32) / 127.5 - 1.0 # 归一化到[-1,1] img torch.from_numpy(img).permute(2, 0, 1).contiguous() label torch.from_numpy(label).long() return img, label这里有几个直接影响效果的参数。resize 标签图时插值必须用cv2.INTER_NEAREST也就是最邻近插值不能贪方便用 LINEAR否则会在类别边界上插出不存在的类别值相当于给标注图注入噪音。随机缩放的范围我设置为 0.5 到 2.0对街景类场景比较合适如果目标特别小、物体密集可以把下限降到 0.3。另外归一化用 ImageNet 统计mean[0.485,0.456,0.406], std[0.229,0.224,0.225]更规范但一般配合预训练 backbone 使用无预训练时可以随意一点。if __name__ __main__: from torch.utils.data import DataLoader ds SegDataset(dataset/JPEGImages, dataset/SegmentationClass, dataset/ImageSets/Segmentation/train.txt, num_classes21, crop_size(512, 512), is_trainTrue) loader DataLoader(ds, batch_size4, shuffleTrue, num_workers2) for img, label in loader: print(img.shape, label.shape, img.dtype, label.dtype) # 打印输出维度确认 break运行这段检查脚本你会看到torch.Size([4, 3, 512, 512])和torch.Size([4, 512, 512])标签不带通道维这一点在把输出 logits 与 label 做损失计算时尤其重要。至于shuffle训练必开验证集改成Falsenum_workers在 Windows 上建议设 0 或 1Linux 上可以给到 4 以上否则有时会莫名卡死。4. 训练配置与mIoU验证代码损失下降不等于指标上涨的完整流程4.1 训练循环从pretrained backbone加载到学习率设置模型部分我用 DeepLabV3 的常见实现思路来说明ResNet50 做 backboneASPP 模块接上采样。训练时有一个重要习惯backbone 用 ImageNet 预训练并且在前几个 epoch 冻结它或者给不同模块分配不同学习率。原因在于分割模型的 decoder 是随机初始化的如果从一开始就全参数一起训练随机初始化的 decoder 会产生很大梯度把 backbone 的预训练特征直接冲乱。import torch import torch.nn as nn from torch.utils.data import DataLoader # 以segmentation_models_pytorch为例这是一个封装好的分割库常见且易跑通 import segmentation_models_pytorch as smp model smp.DeepLabV3Plus( encoder_nameresnet50, encoder_weightsimagenet, in_channels3, classes21, ) # 冻结backbone先只训练decoder几个epoch再解冻 for p in model.encoder.parameters(): p.requires_grad False optimizer torch.optim.AdamW( [{params: model.encoder.parameters(), lr: 1e-5}, {params: model.decoder.parameters(), lr: 3e-4, weight_decay: 1e-4}], ) scheduler torch.optim.lr_scheduler.PolynomialLR(optimizer, total_iters50, power0.9) criterion nn.CrossEntropyLoss(ignore_index255)这段配置里encoder 学习率给 1e-5decoder 给 3e-4这就是常见的差分学习率策略。冻结阶段跑 2 到 3 个 epoch 后把requires_grad全部置回True再完整训练。PolynomialLR是许多分割项目常用的衰减方式learning rate 从初始值按幂函数一路降到 0比 StepLR 的断层式下降更容易把 mIoU 拉到高位。第一次跑分割任务我建议直接用 AdamW 而不是 SGD少操心动量参数后面想刷分再换 SGD Momentum。显存方面512×512 输入、batch_size4、ResNet50 的 DeepLabV3 在 8GB 显存上勉强能跑6GB 就要把 batch 降到 2 或者 crop 改成 480。如果显存还是爆加梯度累积即可optimizer.zero_grad()隔几步做一次等价于变大 batch 的效果。训练主循环本身不复杂但要把验证穿插进来。def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 for i, (imgs, labels) in enumerate(loader): imgs imgs.to(device) labels labels.to(device) logits model(imgs) # 输出形状 [B, 21, H, W] loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() if i % 20 0: batch_norm labels.shape[0] print(f step {i}, loss {loss.item():.4f}) return total_loss / len(loader)每次迭代打印 loss 而不是等整个 epoch 结束再打印这样能尽早发现梯度爆炸或 loss 恒定的问题。如果你的 loss 一开始就在 0.7 附近不动先确认是不是冻结策略生效了全冻结时 decoder 随机初值学得慢loss 下降缓慢是正常的如果解冻后 loss 还是不降多半是学习率太低或数据管道错位了。4.2 验证与mIoU计算混淆矩阵才是最可靠的算法训练损失下降很快不代表 mIoU 就好。原因很简单交叉熵是逐像素求平均的模型学会把大面积道路猜对loss 就能显著下降但小目标仍然错漏。所以验证代码必须自己写把每个类别的 IoU 分开算再求均值。def compute_miou(logits, labels, num_classes, ignore_index255): # logits: [B, C, H, W], labels: [B, H, W] preds logits.argmax(dim1) # 取概率最大的类别索引 mask (labels ! ignore_index) # 过滤掉不参与评价的像素 labels_masked labels[mask] preds_masked preds[mask] # 建立混淆矩阵: 真实行, 预测列 conf_mat torch.zeros(num_classes, num_classes, dtypetorch.int64) idx labels_masked * num_classes preds_masked conf_mat torch.bincount(idx, minlengthnum_classes * num_classes).reshape( num_classes, num_classes ) ious [] for cls in range(num_classes): tp conf_mat[cls, cls].item() fp conf_mat[:, cls].sum().item() - tp fn conf_mat[cls, :].sum().item() - tp union tp fp fn if union 0: ious.append(float(nan)) # 该类别在验证集中没有像素 else: ious.append(tp / union) # 忽略nan类别求所有存在类别的IoU均值 valid [iou for iou in ious if not math.isnan(iou)] return sum(valid) / len(valid), ious这段代码的核心是torch.bincount高效构建混淆矩阵实际项目中几百张验证图、百万级像素这个写法比两层 for 循环快得多。要说明的是nan处理某个类别在验证图里没出现时IoU 的union0直接跳过并参与后续平均。这个细节处理不当会在验证集类别不全时拉低整体分数造成“明明模型还行报告里数值却很难看”的窘境。验证流程建议每个 epoch 结束后完整跑一遍 val 集把 mIoU、每类 IoU、像素准确率一起打印出来存档。如果你发现 mIoU 曲线在某个 epoch 后进入平台期再去调学习率或数据增强不要频繁改网络结构因为一个改动的变量都没固定下来后续分析很难写出有说服力的结论。5. 语义分割五大翻车实录数据、标签、显存与指标的现场排障5.1 标签与图像异步变换训练 loss 反复震荡模型学不透现象训练 20 个 epoch 之后 loss 仍在 0.6 上下波动验证集可视化结果里物体的轮廓与真实标注完全咬合不上仿佛标签“慢半拍”。原因Dataloader 用 two-branch 方式分别处理图像和标签RandomHorizontalFlip对图像随机翻转了但标签没有同步或者对标签做了普通 resize 而非最邻近插值。图像和实际监督信号错位模型在不断学习冲突样本。解决把图像和标签放进同一个函数并用同一个随机种子做同步变换且所有几何变换都在np.ndarray层面上完成最后再转 Tensor。上面第三章的sync_transform设计就是为此而生的别嫌封装麻烦后期少排查好多问题。5.2 显存不足迁移学习在小显存卡上的常见解决方案现象batch_size4、crop512 时模型加载即报 CUDA out of memory或训练中间偶发 OOM 导致进程被杀。原因DeepLabV3 的 ASPP 并行分支带来足够大的中间激活加上反向传播保留的梯度显存峰值比前向计算高不少。解决最直接的降 batch_size 到 2 或 1。如果单卡只有 6GB我会把 crop 缩到 480 或 400这是语义分割训练里最有效的显存调节旋钮。另外在torch.backends.cudnn.benchmark True打开的情况下很多固定输入尺寸的训练会有一定加速还能顺手开启torch.cuda.amp混合精度将梯度精度降到 fp16显存占用能少三分之一左右。注意BN 层在 batch_size1 时统计量不稳定这种情况要么换 GroupNorm要么累加足够多的迭代再同步更新 BN 统计量。5.3 类别不均衡导致小目标类 IoU 为 0但整体 mIoU 看起来还行现象21 类里公交车、自行车的 IoU 常年是 0但模型总体 mIoU 有 0.6 以上曲线图看起来还挺正常。原因这类像素在训练集里占比太少交叉熵的梯度被背景和大类别淹没甚至有些类别模型从未成功预测过一次。解决先统计训练集各类像素频次确认哪些类像素占比低于 1%。处理顺序是给损失函数加类别权重或者换 Focal Loss如果两类目标实在太稀疏考虑在数据增强里做“类别平衡采样”也就是专门裁切那些包含目标小类的区域作为训练样本。在验证统计时保留每类 IoU 单独打印而不要只输出平均 mIoU这样才能定位是哪几类拖后腿。5.4 ignore_index 漏传导致 mIoU 虚高或非法预测现象验证时 mIoU 很高但可视化图中物体边缘像覆盖了一层白噪声或者某些像素预测出了超高索引值在源码中找到空白色块。原因标签里存在 255 的 ignored 区域但计算 IoU 时没有过滤这些像素或者标签里有空洞类序号导致网络输出的类别数与损失函数类别数错位。解决在compute_miou里对标签和预测做掩膜过滤保证混淆矩阵只统计有效类别同时每次数据集加载后打印标签像素值分布确认 min/max 都在[0, num_classes-1] ∪ {255}区间内。这个问题写进毕设的话可能被答辩老师当场指出来所以务必在实验记录里存一张标注意外值统计图作证。5.5 验证集上 mIoU 上升停滞但训练集 IoU 很高现象训练集 mIoU 到 0.75验证集只在 0.68 停滞epoch 增加后验证不再上升甚至下降一点。原因训练过拟合是常见原因另外还有数据增强不足导致模型在验证集新场景上泛化弱。解决先提升数据增强强度比如引入 color jitter、随机遮挡CutMix 对分割不常用来但随机擦除是可行的、多尺度测试。然后适当增加 dropout 或在 decoder 中加入 Spatial Dropout。最后如果验证集是不同分布的拍摄数据这时再考虑换更大 backbone 或加入简单域适应策略但毕设阶段把数据增强和正则做足多数情况能撑住。6. 毕设答辩前的调优三件事可视化、多尺度推理与bad case分析到了最后阶段别再盲目调参了。拿已在验证集上跑好的模型做三件看得见、能写进论文的事情。第一件是把预测结果叠加到原图上生成红绿蓝或随机彩色掩膜以半透明方式与原始图混合。这比单独展示一张分割图更能说明模型“找到物体了”。用cv2.addWeighted(pred_color, 0.6, img, 0.4, 0)一行实现注意要先把类别索引映射成 BGR 色盘再与原图融合。第二件是多尺度推理的验证。把输入缩放到 0.75、1.0、1.5 倍分别送入模型取预测把各尺度预测的 softmax 得分插值回原尺寸后取平均再 argmax。这一操作在 Cityscapes 类高分辨率数据上通常能带来 1 到 3 个点 mIoU 的提升且完全不需要重新训练。代价是推理时间倍增所以毕设报告里可以把它作为“测试时增强”策略单独描述。第三件是 bad case 分析。在验证集预测结果中按每类 IoU 从低到高排序挑选 IoU 最低的两三类把原图、标注图、预测图、错误像素热力图并列放成一张图分析错误是出现在物体边界、小尺寸目标、还是遮挡区域。这类分析是毕设论文里的常见加分项比全文贴一堆结构图更接地气。最后想提醒的是不要把整个项目处处都追求最优。我的习惯是固定一个能跑的配置后就不再频繁改动模型结构把实验记录表、超参记录、对比曲线做好这比反复刷一个指标更能撑住答辩席上的提问。希望这份方案能帮你把毕设的每一步都走扎实也希望你真学到一个能带进工作的技能而不是只留下一个解压后的文件夹。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑