资讯动态

10个epoch达到0.98 IoU:LR-ASPP与MobileNet V3道路分割实践

发布时间:2026/9/10 8:31:50 来源:尧图企业网站定制
简介面向自动驾驶与智能交通的道路图像语义分割实战资源包以LR-ASPP为主干、MobileNetV3为轻量级特征提取网络采用迁移学习训练10个epoch验证集IoU达到0.98适合有深度学习基础的开发者复现与二次开发。资源内含2000个文件包括1829张PNG与157张JPG道路场景图像涵盖不同光线和路况下的视频帧7个Python脚本覆盖数据预处理、模型训练与评估5个标签或配置TXT文件便于理解类别定义2个训练完成的PTH权重可直接加载推理压缩包整体99.62MB目录结构清晰。已有259人学习下载。通过该资源可获取完整道路分割数据集、训练脚本和预训练模型快速复现IoU 0.98的实验结果也可基于自定义道路场景微调PTH权重支持轻量化部署验证为语义分割算法研究、毕业设计或工程落地提供实用参考。1. 从 10 个 epoch 到 0.98 IoU道路图像语义分割没你想的那么难很多做自动驾驶相关任务的朋友会把语义分割想得很重尤其是看到 DeepLabV3 的大模型之后。我这次拿到一批道路视频帧标注了道路、车道线、车辆、行人这些目标目标是把分割模型跑起来并部署到边缘设备上。最终选了 LR-ASPP 搭配 MobileNet V3 作为骨干网络用迁移学习只训练了 10 个 epoch验证集 IoU 就到了 0.98。这个结果不是靠堆数据堆出来的而是预训练权重在特定道路场景下快速微调的结果。这个组合特别适合需要迅速验证分割效果、或者要把模型塞进实时链路的人看完这一篇可以直接照做。2. LR-ASPP 结构与 MobileNet V3 的取舍为什么道路分割很少用 DeepLabV32.1 ASPP 到 LR-ASPP 的轻量化演进ASPPAtrous Spatial Pyramid Pooling是 DeepLab 系列的核心它把同一个特征图分别用 1×1 卷积和不同膨胀率的 3×3 空洞卷积并行抽取再拼到一起。膨胀率越大感受野越大模型能同时看到小目标和巨大的背景区域。但对于道路分割来说图像尺度相对固定背景结构也规律没必要把五个分支全保留。LR-ASPP 是专门为 MobileNet V3 精简出的版本只保留了 1×1 卷积和两个 3×3 空洞卷积膨胀率分别是 12 和 24同时增加一条 low-level 特征分支用来补边缘细节。这样参数量比原版 ASPP 少一半多在 GPU 和嵌入式设备上都能跑得动。分支配置模块构成膨胀率主要作用完整 ASPP1×1 卷积 3 个 3×3 空洞卷积 Image Pooling6, 12, 18覆盖多尺度全局上下文LR-ASPP1×1 卷积 2 个 3×3 空洞卷积 low-level 1×1 卷积12, 24轻量多尺度 边缘补充这个精简不是随意砍的。道路图像里行人或者小目标可能只占十几个像素路面和背景则占了整张图膨胀率 12 和 24 两层正好覆盖这两个尺度范围。如果换成膨胀率 6对小目标稍有帮助但计算代价上升整体收益不明显。要是你的数据换成了遥感影像图中目标尺度变化很大那 LR-ASPP 不一定够用回退到 DeepLabV3 语义分割那套方案更稳。所以选型的时候先看对象尺度分布不要只看模型名气。2.2 MobileNet V3 的深层特征与低层细节怎么接MobileNet V3 在 ImageNet 上预训练过的分类特征放到语义分割里有两个关键优势深层特征分辨率低但语义信息强低层特征细节多但噪声大。LR-ASPP 的 decoder 不会像 UNet 那样把特征逐层上采样再拼回去而是从 backbone 的低层取一个特征图先经过 1×1 卷积把通道压缩到 128 左右再和 ASPP 上采样后的结果 concat。这个设计极大减少了 decoder 参数量也让模型更适合高分辨率输入。具体到 MobileNet V3 Largebackbone 输出的高层特征是原图 1/32 分辨率低层分支取的是倒数第二层的 1/8 分辨率。torchvision 的实现里已经把这些连接封装好了我们调用模型时不需要手动改 backbone 结构。需要注意一点如果 low-level 特征的通道数太大concat 之后 decoder 计算量会成倍增加这就是官方低层分支用 1×1 卷积压通道的原因。我们自己换类别数时最终分类卷积的输出通道也要对应调整负责 decode 的部分才不会报尺寸不匹配。2.3 用 torchvision 构造模型并替换分类头实际项目里我一般都直接用torchvision.models.segmentation里的lraspp_mobilenet_v3_large不会自己去写空洞卷积拼接。代码量很短import torch from torchvision.models.segmentation import lraspp_mobilenet_v3_large num_classes 8 model lraspp_mobilenet_v3_large(pretrained_backboneTrue) model.classifier[4] torch.nn.Conv2d( in_channelsmodel.classifier[4].in_channels, out_channelsnum_classes, kernel_size1 )pretrained_backboneTrue的意思是只加载 MobileNet V3 在 ImageNet 上的分类预训练权重LR-ASPP 的 decoder 保持随机初始化。如果设置成pretrainedTrue则会加载整个分割模型在 COCO 上的权重。数据量比较小时我更推荐只加载 backbone因为 decoder 在 COCO 上学到的语义和道路场景差别较大直接微调反而可能拖慢收敛。最后一行把 classifier 里最后的 1×1 卷积换掉in_channels从原有输入通道数取out_channels改成自己数据集的类别数其他分支不用动。2.4 为什么不直接上 DeepLabV3DeepLabV3 在自动驾驶数据集上确实精度高但模型体积和推理延迟也高。如果项目对精度要求不是 99% 级别而是要跑在 Jeston Nano 或者手机端LR-ASPP 是更现实的起点。我这次的验证集 IoU 到了 0.98可见轻量模型在小规模道路场景里不会明显吃亏。真正的瓶颈反而转移到数据质量和类别均衡上。如果后面要复现遥感影像语义分割这一类轻量网络也是先验证可行性的首选因为遥感大图的推理速度更关键。3. 道路视频帧如何变成训练数据标注、格式与 Dataset 设计3.1 从原始帧到 Roboflow 标准的目录结构我拿到的文件名长得像frame508_jpg.rf.0616a3ff7fe7efb4052249dcb3e30f3e.jpg这是 Roboflow 导出时的常见命名rf后面跟着哈希用来区分数据版本。如果你从视频里抽帧再标注建议先统一成下面这种目录结构否则训练脚本很快会被文件名搞乱dataset/ images/ train/ 000.jpg 001.jpg valid/ masks/ train/ 000.png 001.png valid/我一般先写脚本把frame*_jpg.rf.*这类文件重命名成顺序数字同时把对应的 mask 文件同步改名。这里有个容易被忽略的坑文件名里的哈希不是按时间顺序生成的直接用sorted()排序可能把frame9排到frame10后面导致图像和 mask 错位。所以一定要按文件名里的帧编号提取整数再排序不能在目录列表上直接取索引。3.2 类别设计与颜色映射道路语义分割数据集的类别定义会影响类别均衡和 IoU 的含义。在这份数据里我定义了 8 类背景、道路、车道线、车辆、行人、交通标志、建筑、其他物体。标注工具导出后mask 应该是单通道灰度图像素值直接用类别编号0~7不能用 RGB 彩色图。如果发现 mask 是三个通道必须先用convert(L)转成灰度否则 CrossEntropyLoss 训练时不会报错但损失会一直不降因为你把三通道当成了一个通道。类别ID类别名称约定颜色 (R,G,B)0背景(0,0,0)1道路(128,0,0)2车道线(255,255,255)3车辆(0,255,0)4行人(255,0,255)5交通标志(0,128,255)6建筑(128,128,0)7其他(128,0,128)这个颜色表不参与训练只在可视化时用来把预测索引映射回彩色图。拿到别人导出的数据集后第一件事就是检查类别顺序和颜色表是否一致。我踩过两次坑一次是类别顺序对不上验证集可视化里所有mask整体错位另一次是背景像素被标成 255这本来是无效区域但我没在损失函数里设置ignore_index255导致损失出现 NaN。正确做法是 Dataset 里统一把 255 映射为 0或者在 loss 里忽略它。3.3 归一化、增强和 Dataset 实现在写 Dataset 前先确定输入尺寸。MobileNet V3 要求输入边长能被 32 整除我统一用(256, 256)这个分辨率对道路分割够用也能让训练快不少。归一化必须使用 ImageNet 统计出的 mean/std因为 backbone 预训练用的是这套参数如果换成了自己的均值预训练特征相当于白加载。数据增强我只用随机水平翻转和随机亮度/对比度调整不用随机裁剪因为道路图像需要保留完整的透视结构。from torch.utils.data import Dataset from PIL import Image class RoadSegDataset(Dataset): def __init__(self, image_dir, mask_dir, transform, target_size(256, 256)): self.images sorted(image_dir.iterdir()) self.masks sorted(mask_dir.iterdir()) self.transform transform self.target_size target_size def __getitem__(self, idx): img Image.open(self.images[idx]).convert(RGB) mask Image.open(self.masks[idx]).convert(L) img img.resize(self.target_size, Image.BILINEAR) mask mask.resize(self.target_size, Image.NEAREST) if self.transform: img, mask self.transform(img, mask) return img, mask这里的关键是 mask 不能用BILINEAR插值因为类别边界会被插成中间值之后argmax会得到错误类别。图片和 mask 如果要一起做翻转必须保证两者使用同一个随机状态我习惯写一个自定义 Compose在内部先torch.set_rng_state让随机状态对齐或者直接在 transform 里传入seed。还有一点Dataset 里不需要对 mask 做 one-hot 编码CrossEntropyLoss会自动处理保留整数索引张量即可。3.4 连续帧的采样与数据分布如果原始素材是视频流连续帧之间背景变化很小直接按 1 帧间隔采样会造成训练集高度相似验证集又出现过拟合的假象。我一般按每秒抽 1~2 帧再用随机偏移把训练和验证分开避免同一段道路出现在两个集合里。这份数据里有些帧是车辆静止的有些是转弯场景类别分布差异很大。不做帧采样直接训练loss 会表现很好但把模型放到新视频上会明显掉点。4. 迁移学习训练冻结、超参、损失函数与 0.98 IoU 的真实含义4.1 冻结骨干网的前期层迁移学习的关键是决定冻结哪些层。这段训练里我冻结了 MobileNet V3 的前 12 层只微调后半部分和 decoder。前几层学的是边缘、颜色、纹理这些通用特征道路图像和 ImageNet 没有本质区别不需要再更新高层的语义特征才更贴近具体类别。如果你的数据只有几十张那可以整个 backbone 都冻结只训练 decoder。如果反过来把所有层都冻结模型效率会非常低精度也会卡在 0.7 附近。数据量越大可微调的层数越多这个比例要自己把握。4.2 为什么用 CrossEntropy 加 Dice Loss道路分割里背景和道路的像素占比经常超过 80%只用 CrossEntropyLoss 会学会把大块区域预测成背景和道路行人、车道线这些小目标全部被忽略。我习惯加一个 Dice Loss它直接优化区域重叠程度对小目标更友好。下面实现只依赖 PyTorch 基础算子import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.softmax(pred, dim1) target F.one_hot(target, num_classespred.shape[1]).permute(0, 3, 1, 2).float() intersection (pred * target).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target.sum(dim(2, 3)) dice (2.0 * intersection smooth) / (union smooth) return 1.0 - dice.mean()pred是模型输出的原始 logitstarget是整数索引 mask。F.one_hot先把它变成[B, H, W, C]再通过permute转成[B, C, H, W]才能和 pred 按通道对齐。smooth是为了防止分子分母都为 0一般取 1.0。训练时总损失用0.7 * CrossEntropyLoss 0.3 * dice_loss这样能保留交叉熵的稳定梯度同时用小物体上的区域重叠约束拉高召回率。4.3 评估指标IOU 计算逻辑验证集的 IoU 不能直接用accuracy_score类别不平衡会让准确率虚高。我按类别分别算交集和并集再取平均def compute_iou(pred, target, num_classes): iou_sum 0.0 for cls in range(num_classes): p (pred cls) t (target cls) inter (p t).sum().float() union (p | t).sum().float() iou_sum (inter 1e-6) / (union 1e-6) return iou_sum / num_classes其中pred和target都是[B, H, W]的整数张量num_classes8。注意这里没有忽略背景类如果背景在数据里占了 80%背景 IoU 很容易接近 1.0把整体均值拉高。所以我除了看 mean IoU还会单独打印每个类别的 IoU。0.98 这个数如果主要来自背景和道路那模型对小目标的学习仍需验证。4.4 10 个 epoch 的训练循环与超参配置训练参数我固定在下面这几个值上方便复现和调参。使用 AdamW 而不是 SGD因为它对学习率更不敏感。初始学习率3e-4batch size 16训练 10 个 epoch。在单张 3090 上大概跑 5 分钟。训练循环代码如下optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) criterion torch.nn.CrossEntropyLoss(ignore_index255) model.train() for epoch in range(10): for images, masks in train_loader: images images.cuda() masks masks.cuda() pred model(images)[out] loss 0.7 * criterion(pred, masks) 0.3 * dice_loss(pred, masks) optimizer.zero_grad() loss.backward() optimizer.step() print(fepoch {epoch1}, loss {loss.item():.4f})model(images)[out]取的是 LR-ASPP 的主输出还有一个[aux]辅助输出这里没用到。ignore_index255让之前提到的无效区域不会产生梯度。10 个 epoch 之后验证集 mean IoU 到了 0.98但这个数字在类别极不平衡时只能参考。我后来检查了每一类道路和背景的 IoU 都在 0.99车道线只有 0.83车辆 0.91行人 0.76。所以 0.98 是均值真正要优化的小目标还有空间。要是你的业务只关心车辆和道路那 0.98 已经足够如果关心行人就得在数据增强和多尺度推理上再下功夫。5. 落地技巧连续帧预测与 ONNX 导出5.1 连续帧推理与 mask 可视化训练完成后最常见的落地方式是把模型接到视频流上。这里有个容易忽略的问题每帧图像输入前必须做和训练一致的归一化并且model.eval()和torch.no_grad()不能省否则显存会随着视频长度慢慢涨。我一般用 OpenCV 读帧然后执行下面这套流程import cv2, torch model.eval() cap cv2.VideoCapture(road.mp4) with torch.no_grad(): for _ in range(int(cap.get(cv2.CAP_PROP_FRAME_COUNT))): ret, frame cap.read() rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img torch.from_numpy(rgb).permute(2, 0, 1).float() img (img / 255 - mean) / std out model(img.unsqueeze(0).cuda())[out] seg out.argmax(dim1).squeeze(0).cpu().numpy() # 用颜色表把 seg 映射回 RGB 并与原帧叠加这里的mean和std必须是训练时用到的常量不能临时改。seg是整数索引图不能直接当灰度图显示要先用前面那张颜色表映射成 RGB再和原帧做 alpha 融合。连续帧之间模型输出互不依赖这也是静态分割模型好落地的地方不需要维护时序状态。5.2 导出 ONNX部署到 C 或 ONNX Runtime 时我会把模型导出成 ONNX。导出前先确保模型处于 eval 状态我用opset_version11而不是更新的版本兼容性更好。动态 batch 维度可以让视频流里一次处理多帧减少调度开销model.eval() x torch.randn(1, 3, 256, 256).cuda() torch.onnx.export(model, x, lraspp.onnx, input_names[input], output_names[output], opset_version11, dynamic_axes{input: {0: batch}, output: {0: batch}})导出后建议用 onnxruntime 对同一张图做一次输出对比误差一般都在 1e-5 以内。如果误差偏大优先检查是不是没加model.eval()或者归一化常量不一致。这个流程直接换成遥感影像语义分割也成立只要把数据切片换成遥感区域类别改成背景、道路、建筑训练和推理流程可以原封不动复用轻量模型跑大范围影像时优势更明显。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价