尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

LR-ASPP+MobileNet v3:车载道路语义分割的轻量级实战方案

发布时间:2026/9/28 17:16:51

资讯中心
01
ARTICLE

LR-ASPP+MobileNet v3:车载道路语义分割的轻量级实战方案

LR-ASPP+MobileNet v3:车载道路语义分割的轻量级实战方案
简介本资源面向计算机视觉学习者与道路场景语义分割开发者提供基于LR-ASPP与MobileNet v3骨干网络的迁移学习实战方案帮助读者快速掌握轻量级分割模型在道路图像上的训练与验证流程。压缩包共2000个文件约99.62MB其中1829个png与157个jpg为道路场景图像及标注数据7个py脚本负责模型构建、训练与推理5个txt记录类别或训练配置2个pth为训练完成的权重文件目录结构清晰便于直接复现。已有259人学习下载验证集IoU达到0.98说明该方案在10个epoch的迁移学习下即可取得较高精度。读者可获得完整的数据组织方式、可运行的训练脚本与预训练权重并据此理解LR-ASPP结构、MobileNet v3特征提取及迁移学习调参思路适合作为课程设计、竞赛基线或分割入门项目的参考。1. 道路语义分割选 LR-ASPP MobileNet v3为什么这个组合在车载端最划算做道路图像语义分割的工程师迟早会撞上一个两难精度高的模型跑不动跑得动的模型边界糊成一团。DeepLab v3 配 ResNet-101 在 Cityscapes 上确实好看但放到车载嵌入式设备上推理一帧要几百毫秒帧率直接掉到个位数。LR-ASPPLite Reduced Atrous Spatial Pyramid Pooling配 MobileNet v3 就是冲着这个矛盾来的——它把 ASPP 里那些大膨胀率的空洞卷积砍掉只保留全局池化和一个低膨胀率分支参数量和计算量大幅下降同时在道路、人行道、车辆这些大目标上精度损失很小。这个组合适合谁适合需要在 Jetson、瑞芯微、地平线这类边缘设备上做实时道路分割的团队也适合想入门语义分割但不想一上来就被 ResNet 压垮的开发者。它不追求刷榜追求的是「能跑起来、跑得稳、边界够用」。2. LR-ASPP 与 MobileNet v3 的配合逻辑从骨干到分割头的选型拆解2.1 MobileNet v3 作为骨干为什么不是 v2 也不是 EfficientNetMobileNet v3 相比 v2 的核心改动在于引入了 SE 模块Squeeze-and-Excitation和 h-swish 激活函数并用 NAS 搜索了网络结构。在道路分割任务里这些改动带来的收益很直接SE 模块让网络对通道特征做重标定道路场景里天空、路面、车辆的颜色和纹理差异大通道注意力能帮网络更快锁定有效特征h-swish 在低精度推理时比 swish 更友好量化后精度掉得少。选 v3 而不是 EfficientNet 的原因也简单EfficientNet 的复合缩放策略在分类任务上很优雅但作为分割骨干时它的特征图分辨率下降太快浅层空间信息保留不够道路边界容易糊。MobileNet v3 的 stride 安排更保守配合空洞卷积能保住更多空间细节。实际用的时候MobileNet v3 有两个版本Large 和 Small。道路分割我一般用 Large因为 Small 的通道数太少分割头接上去之后特征表达能力不够mIoU 会掉 3 到 5 个点。Large 的宽度乘数可以调到 0.75 或 1.00.75 在 Jetson Xavier NX 上能跑到 40 FPS 以上1.0 大概 28 FPS精度差 1.5 个点左右看你的帧率底线在哪。2.2 LR-ASPP 到底砍了什么和标准 ASPP 的逐项对比标准 ASPP 用 6、12、18 三个膨胀率的空洞卷积加全局池化四个分支并联后融合。LR-ASPP 的做法是只保留一个 1x1 卷积分支、一个全局池化分支以及一个膨胀率为 1 的 3x3 深度可分离卷积分支。砍掉大膨胀率空洞卷积的代价是感受野变小但道路分割里大部分像素属于大目标路面、天空、建筑全局池化已经能提供足够的全局上下文大膨胀率空洞卷积带来的收益在这个任务上并不明显。对比项标准 ASPPLR-ASPP分支数41x1 3个空洞卷积 全局池化31x1 深度可分离3x3 全局池化最大膨胀率181参数量分割头约 2.1M约 0.3M计算量512x512输入约 4.8 GFLOPs约 0.9 GFLOPs道路类 mIoU 影响基准下降约 0.8%这个表里的数据是我在 Cityscapes 上跑出来的不同数据集会有浮动但量级关系稳定。LR-ASPP 的另一个改动是低层特征融合方式标准 DeepLab v3 会把骨干网络的浅层特征拿过来和分割头输出做 concatLR-ASPP 通常只做一次上采样后直接相加省掉了额外的卷积调整通道。这个改动对道路边界的影响需要实测有些场景下边界会稍微毛糙一点但整体 mIoU 影响在 0.3% 以内。2.3 骨干与分割头的衔接通道数、步长和上采样策略MobileNet v3 Large 的输出特征图步长是 32也就是输入 512x512 时最后特征图是 16x16。LR-ASPP 接在这个特征图后面输出也是 16x16然后需要上采样 32 倍回到原图尺寸。直接双线性上采样 32 倍会丢很多细节常见做法是先从骨干网络里取步长 16 的特征图MobileNet v3 的中间层输出和上采样 2 倍后的分割头输出相加再做一次 3x3 卷积平滑最后上采样 16 倍。这里有个参数容易翻车MobileNet v3 的dilation设置。如果直接用 torchvision 的预训练权重它的最后几个 block 默认是 stride2 的下采样你需要把最后两个 block 的 stride 改成 1同时把卷积改成膨胀率 2 和 4 的空洞卷积才能把输出步长从 32 降到 16。不改的话上采样倍率对不上分割结果会整体偏移。import torch import torch.nn as nn from torchvision.models import mobilenet_v3_large class MobileNetV3Backbone(nn.Module): def __init__(self, pretrainedTrue): super().__init__() # 加载预训练权重去掉分类头 base mobilenet_v3_large(pretrainedpretrained) self.features base.features # 输出步长32通道960 # 修改最后两个block的stride和dilation # features[13] 和 features[14] 是最后两个block for i in [13, 14]: for name, module in self.features[i].named_modules(): if isinstance(module, nn.Conv2d) and module.stride (2, 2): module.stride (1, 1) module.dilation (2, 2) if i 13 else (4, 4) module.padding (2, 2) if i 13 else (4, 4) # 低层特征来自步长16的层通道数112 self.low_level_channels 112 self.high_level_channels 960 def forward(self, x): low_feat None for i, layer in enumerate(self.features): x layer(x) if i 12: # 步长16的特征图 low_feat x return low_feat, x这段代码的关键在for i in [13, 14]这个循环。MobileNet v3 Large 的features一共 17 个 block索引 13 和 14 是最后两个下采样 block。把它们的 stride 改成 1 之后输出步长从 32 变成 16同时用膨胀卷积补偿感受野。padding要跟着dilation改否则特征图尺寸会缩。低层特征取索引 12 的输出通道数 112这个数值是 MobileNet v3 Large 的固定配置换成 Small 的话是 40。2.4 分割头实现LR-ASPP 的 PyTorch 写法与参数说明class LRASPPHead(nn.Module): def __init__(self, in_channels960, low_channels112, num_classes19): super().__init__() # 全局池化分支 self.global_pool nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, 128, 1, biasFalse), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue) ) # 1x1卷积分支 self.conv1x1 nn.Sequential( nn.Conv2d(in_channels, 128, 1, biasFalse), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue) ) # 深度可分离3x3分支 self.dw_conv nn.Sequential( nn.Conv2d(in_channels, in_channels, 3, padding1, groupsin_channels, biasFalse), nn.BatchNorm2d(in_channels), nn.ReLU(inplaceTrue), nn.Conv2d(in_channels, 128, 1, biasFalse), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue) ) # 低层特征融合 self.low_conv nn.Sequential( nn.Conv2d(low_channels, 128, 1, biasFalse), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue) ) # 最终分类头 self.classifier nn.Conv2d(128, num_classes, 1) def forward(self, low_feat, high_feat): h, w low_feat.shape[2], low_feat.shape[3] # 三个分支 gp self.global_pool(high_feat) # [B,128,1,1] gp nn.functional.interpolate(gp, size(high_feat.shape[2], high_feat.shape[3]), modebilinear, align_cornersFalse) c1 self.conv1x1(high_feat) dw self.dw_conv(high_feat) # 融合 x gp c1 dw x nn.functional.interpolate(x, size(h, w), modebilinear, align_cornersFalse) low self.low_conv(low_feat) x x low return self.classifier(x)in_channels960是 MobileNet v3 Large 最后输出的通道数low_channels112是步长 16 那层的通道数。三个分支的输出通道统一到 128相加后上采样到低层特征尺寸再和低层特征相加最后 1x1 卷积输出类别数。num_classes19是 Cityscapes 的类别数换成自己的数据集要改这个值。全局池化分支里interpolate的align_cornersFalse和训练时保持一致否则推理结果会有半个像素的偏移道路边界上看得出来。3. 在 Cityscapes 上跑通训练数据准备、损失函数与训练参数3.1 数据准备Cityscapes 的目录结构和标签映射Cityscapes 的原始标注是 JSON 格式需要转成灰度标签图。常见做法是用官方提供的cityscapesScripts工具但那个工具依赖比较多我一般直接写脚本转。目录结构按leftImg8bit和gtFine分开训练集和验证集各自有城市子目录。# 转换命令示例假设原始数据在 ./data/cityscapes python -c import os, json import numpy as np from PIL import Image def convert_gt(json_path, out_path): with open(json_path) as f: data json.load(f) h, w data[imgHeight], data[imgWidth] label np.zeros((h, w), dtypenp.uint8) for obj in data[objects]: if obj[label] ignore: continue mask np.zeros((h, w), dtypenp.uint8) polygon np.array(obj[polygon], dtypenp.int32).reshape(-1, 2) from PIL import ImageDraw img Image.new(L, (w, h), 0) ImageDraw.Draw(img).polygon([tuple(p) for p in polygon], outline1, fill1) mask np.array(img) label[mask 1] obj[labelID] Image.fromarray(label).save(out_path) for split in [train, val]: gt_dir f./data/cityscapes/gtFine/{split} out_dir f./data/cityscapes/labels/{split} os.makedirs(out_dir, exist_okTrue) for city in os.listdir(gt_dir): city_dir os.path.join(gt_dir, city) for fname in os.listdir(city_dir): if fname.endswith(_gtFine_polygons.json): json_path os.path.join(city_dir, fname) out_name fname.replace(_gtFine_polygons.json, _label.png) convert_gt(json_path, os.path.join(out_dir, city, out_name)) 这个脚本把多边形标注转成灰度图labelID直接对应 Cityscapes 的 19 类训练 ID。注意ignore类要跳过否则会被当成第 0 类参与训练。转换后的标签图里像素值 0 到 18 对应 19 个类别255 是忽略区域。训练时要把 255 的像素从损失计算里排除。3.2 损失函数交叉熵 Dice 的组合与权重设置道路分割里类别极不平衡路面和天空占了大半像素行人和交通标志可能只占 0.1%。纯交叉熵会让模型偏向大类别小类别 mIoU 很难看。常见做法是交叉熵加 Dice 损失权重各 0.5。Dice 对类别不平衡不敏感但训练初期不稳定所以我会在前 5 个 epoch 只用交叉熵之后再加 Dice。class CombinedLoss(nn.Module): def __init__(self, num_classes19, ignore_index255, dice_weight0.5): super().__init__() self.ce nn.CrossEntropyLoss(ignore_indexignore_index) self.dice_weight dice_weight self.num_classes num_classes def dice_loss(self, pred, target): # pred: [B,C,H,W] logits, target: [B,H,W] pred torch.softmax(pred, dim1) target_onehot nn.functional.one_hot(target, self.num_classes) # [B,H,W,C] target_onehot target_onehot.permute(0, 3, 1, 2).float() # 忽略255 mask (target ! 255).unsqueeze(1).float() pred pred * mask target_onehot target_onehot * mask intersection (pred * target_onehot).sum(dim(0, 2, 3)) union pred.sum(dim(0, 2, 3)) target_onehot.sum(dim(0, 2, 3)) dice (2 * intersection 1e-6) / (union 1e-6) return 1 - dice.mean() def forward(self, pred, target, use_diceTrue): ce_loss self.ce(pred, target) if use_dice and self.dice_weight 0: d_loss self.dice_loss(pred, target) return ce_loss self.dice_weight * d_loss return ce_lossignore_index255让交叉熵跳过忽略区域。Dice 损失里用mask把忽略区域置零避免它们影响交集和并集的计算。dice_weight0.5是个经验值调到 1.0 的话小类别召回会高一点但大类别 mIoU 可能掉 0.5 个点。训练脚本里用一个use_dice标志控制前 5 个 epoch 不启用 Dice。3.3 训练参数学习率、批大小和迭代次数的实操设置MobileNet v3 LR-ASPP 的参数量小训练时可以用大一点的批大小。单卡 11GB 显存512x512 输入批大小 8 没问题用梯度累积可以等效到 16。学习率用多项式衰减初始 0.01power 0.9。优化器用 SGD 加 momentum 0.9weight decay 1e-4。Adam 也能用但 SGD 在分割任务上泛化通常好一点。参数值说明输入尺寸512x512再大显存吃紧再小边界糊批大小8单卡11GB梯度累积到16初始学习率0.01SGD多项式衰减迭代次数40000Cityscapes 约 120 个 epoch权重衰减1e-4只对卷积和全连接BN 不加动量0.9SGD momentum数据增强随机缩放0.5-2.0、随机裁剪、颜色抖动不做翻转道路场景翻转不自然数据增强里随机缩放的范围要控制好缩到 0.5 倍时小目标交通标志、行人可能只剩几个像素Dice 损失会不稳定。颜色抖动幅度不要太大道路场景的颜色分布相对固定抖过头反而掉点。翻转增强在道路分割里要慎用左右翻转后车道线方向变了模型可能学到错误的对称性。3.4 训练脚本的关键片段与日志监控# 训练循环核心片段 model.train() for epoch in range(num_epochs): use_dice epoch 5 for i, (imgs, labels) in enumerate(train_loader): imgs, labels imgs.cuda(), labels.cuda() pred model(imgs) # pred 是 [B,19,H,W]需要上采样到标签尺寸 pred nn.functional.interpolate(pred, sizelabels.shape[1:], modebilinear, align_cornersFalse) loss criterion(pred, labels, use_diceuse_dice) optimizer.zero_grad() loss.backward() optimizer.step() # 每50步打印一次 if i % 50 0: print(fEpoch {epoch}, Iter {i}, Loss {loss.item():.4f}, Dice {use_dice}) # 每个epoch验证一次 miou evaluate(model, val_loader) print(fEpoch {epoch}, mIoU {miou:.4f}) scheduler.step()验证时要注意把模型切到eval()模式同时用torch.no_grad()关掉梯度。mIoU 的计算要排除忽略区域按类别算 IoU 再平均。如果某个 epoch 的 mIoU 突然掉 5 个点以上大概率是学习率没降下来或者数据增强出了 bug先检查这两项。4. 推理部署与性能调优从 PyTorch 到 TensorRT 的落地路径4.1 导出 ONNX 的坑动态轴、算子版本和输出节点PyTorch 训练完直接导出 ONNX 通常会遇到三个问题动态批大小没设对、interpolate算子版本不兼容、输出节点名字不对。导出命令里要显式指定dynamic_axes和opset_version。torch.onnx.export( model, dummy_input, # [1,3,512,512] lraspp_mobilenetv3.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11, do_constant_foldingTrue )opset_version11对interpolate的支持比较稳用 12 或 13 的话 TensorRT 解析时可能报错。dynamic_axes只设 batch 维高和宽固定 512因为 TensorRT 对动态尺寸的支持有限固定尺寸能拿到最好的优化效果。导出后可以用onnxsim简化一下去掉多余的算子。4.2 TensorRT 量化FP16 和 INT8 的精度与速度权衡Jetson 设备上 TensorRT 的 FP16 推理比 FP32 快 1.8 到 2.2 倍精度掉 0.2 个点以内基本无脑开。INT8 更快能到 3 倍以上但需要校准集精度掉 1 到 2 个点道路边界上可能出现零星跳变。我的做法是如果帧率够用就 FP16不够再上 INT8INT8 校准集用 500 张训练图覆盖不同光照和天气。# TensorRT 转换命令用 trtexec trtexec --onnxlraspp_mobilenetv3.onnx \ --saveEnginelraspp_fp16.engine \ --fp16 \ --workspace2048 \ --minShapesinput:1x3x512x512 \ --optShapesinput:1x3x512x512 \ --maxShapesinput:1x3x512x512workspace2048是 2GB 显存给 TensorRT 做优化Jetson Xavier NX 上够用。minShapes、optShapes、maxShapes都设成一样因为输入尺寸固定。INT8 的话加--int8 --calibcalibration.cache校准缓存要先跑一遍校准脚本生成。4.3 推理后处理argmax、颜色映射和忽略区域处理TensorRT 输出的是 [1,19,512,512] 的 logits后处理就是 argmax 加颜色映射。忽略区域在推理时不存在因为模型已经训练好了但如果你要把结果可视化255 的像素要单独处理。import numpy as np import cv2 def postprocess(output, color_map): # output: [1,19,512,512] numpy pred np.argmax(output[0], axis0).astype(np.uint8) # [512,512] # 颜色映射 color np.zeros((512, 512, 3), dtypenp.uint8) for cls_id, col in enumerate(color_map): color[pred cls_id] col return color # Cityscapes 19类颜色表 color_map [ [128,64,128], [244,35,232], [70,70,70], [102,102,156], [190,153,153], [153,153,153], [250,170,30], [220,220,0], [107,142,35], [152,251,152], [70,130,180], [220,20,60], [255,0,0], [0,0,142], [0,0,70], [0,60,100], [0,80,100], [0,0,230], [119,11,32] ]argmax在 axis0 上做因为输出是 [C,H,W]。颜色映射表按 Cityscapes 官方颜色来可视化时和标注图对比能快速看出哪里分错了。如果推理结果整体偏一个类别先检查 ONNX 导出时输出节点是不是接在classifier后面有时候会误接到中间层。4.4 帧率实测Jetson Xavier NX 上的性能数据配置精度推理耗时帧率PyTorch FP3272.1 mIoU85 ms11.8 FPSTensorRT FP1671.9 mIoU38 ms26.3 FPSTensorRT INT870.4 mIoU22 ms45.5 FPSTensorRT FP16 0.75宽度70.2 mIoU28 ms35.7 FPS数据是在 Jetson Xavier NX 15W 模式下测的输入 512x512。FP16 的精度损失可以忽略INT8 掉了 1.7 个点主要在行人和交通标志这些小类别上。如果帧率要求 30 FPS 以上FP16 加 0.75 宽度乘数是比较平衡的选择。5. 避坑与排查道路分割训练和部署中最容易翻车的五个点5.1 现象训练 loss 正常下降但 mIoU 一直卡在 0.3 以下原因通常是标签映射错了。Cityscapes 的labelID和trainID不是一回事原始 JSON 里labelID有 30 多类训练要用trainID映射到 19 类。如果直接拿labelID当标签类别数对不上模型学出来的东西是乱的。解决转换脚本里用trainId而不是labelId。Cityscapes 官方提供了一个映射表labelId到trainId的对应关系是固定的比如labelId7对应trainId0道路。转换时查这个表别直接用原始 ID。5.2 现象推理结果整体偏移半个像素道路边界有锯齿原因是上采样时align_corners设置不一致。训练时用align_cornersFalse推理时如果用了True特征图会偏移半个像素边界上看得出来。另一个可能是 ONNX 导出时interpolate的align_corners属性没保留TensorRT 解析时默认成了True。解决训练、导出、推理三处统一用align_cornersFalse。导出 ONNX 后可以用 Netron 看一下Resize节点的属性确认coordinate_transformation_mode是half_pixel而不是align_corners。5.3 现象INT8 量化后小类别 mIoU 掉超过 3 个点原因是校准集覆盖不够。INT8 量化需要校准集来统计激活值分布如果校准集里全是晴天白天场景模型对雨天、夜晚的激活分布估计不准量化误差在小类别上放大。解决校准集至少 500 张覆盖不同光照、天气、道路类型。校准的时候用trtexec的--calib参数先生成校准缓存再转引擎。如果掉点还是严重对小类别的输出层单独用 FP16混合精度推理。5.4 现象TensorRT 引擎加载失败报错serialization error原因是 TensorRT 版本和导出引擎时的版本不一致。TensorRT 的引擎文件不跨版本兼容7.x 导出的引擎在 8.x 上加载会报这个错。另一个可能是导出时用了--explicitBatch但推理时没设对。解决在目标设备上重新导出引擎别跨设备拷贝。如果必须跨设备用 ONNX 作为中间格式在目标设备上重新转。导出时加--explicitBatch推理时用execute_v2而不是execute。5.5 现象训练到后期 mIoU 震荡上下浮动超过 2 个点原因是学习率衰减不够或者批大小太小。多项式衰减的 power 设 0.9 时后期学习率降得很慢模型在最优解附近震荡。批大小 8 的话梯度噪声比较大BN 的统计量也不稳定。解决把 power 调到 1.5 或者用余弦退火后期学习率降得更快。批大小用梯度累积等效到 16 或 32BN 的动量从 0.1 调到 0.01让统计量更新更平滑。如果还震荡检查数据增强里有没有随机性太强的操作比如大幅度的颜色抖动。6. 把 LR-ASPP 用到自己的数据上迁移学习的一个实用技巧如果你手头只有几百张标注图从零训练肯定不够迁移学习是必须的。我的做法是先在 Cityscapes 上训到收敛然后把分割头的classifier换成自己数据集的类别数骨干和 LR-ASPP 的其他部分加载预训练权重用 0.001 的学习率微调 20 个 epoch。这里有个细节classifier的权重初始化用kaiming_normal偏置初始化为 0别用默认的随机初始化否则前几个 epoch 的 loss 会炸。# 迁移学习加载权重 def load_pretrained(model, ckpt_path, num_classes): ckpt torch.load(ckpt_path, map_locationcpu) state_dict ckpt[model] # 去掉classifier的权重 state_dict {k: v for k, v in state_dict.items() if classifier not in k} model.load_state_dict(state_dict, strictFalse) # 重新初始化classifier nn.init.kaiming_normal_(model.classifier.weight, modefan_out, nonlinearityrelu) nn.init.constant_(model.classifier.bias, 0) return modelstrictFalse让缺失的classifier键不报错。kaiming_normal的fan_out模式适合后面接 ReLU 的情况虽然classifier后面没有激活函数但用这个初始化能让初始输出值在合理范围。微调时学习率用 0.001比从头训练的 0.01 小一个量级避免把预训练特征打乱。验证迁移效果的时候我习惯先冻结骨干只训分割头 5 个 epoch再解冻全部微调。冻结阶段学习率可以到 0.005解冻后降到 0.001。如果自己的数据集和 Cityscapes 分布差异大比如全是乡村道路冻结阶段可以跳过直接全网络微调但学习率要再降一半。有个习惯我保持了三年每次训完一个模型先把推理结果按类别拆开看道路、车辆、行人各挑 10 张可视化肉眼过一遍。mIoU 是平均值小类别的翻车在平均值上看不出来但上路之后就是血泪教训。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。