☰
轻量道路分割实战:MobileNet v3与LR-ASPP边缘优化
2026/10/14 15:10:28 网站建设 项目流程

简介:本资源面向道路场景语义分割的进阶学习者与工程实践者,提供基于LR-ASPP与MobileNet v3骨干网络的完整训练方案,帮助读者掌握轻量级分割模型在自动驾驶、道路巡检等场景中的迁移学习落地方法。压缩包共2000个文件,包含1829张png与157张jpg图像数据、7个Python脚本、5个txt说明文件及2个pth权重文件,整体约99.62MB,覆盖数据、代码与模型权重三类核心内容。目前已有259人学习下载。资源采用迁移学习策略训练10个epoch,验证集IoU达到0.98,读者可据此复现训练流程、理解LR-ASPP结构设计、掌握数据增强与模型评估要点,并借助预训练权重快速验证推理效果,适合作为分割项目入门与调优的参考范例。

1. 道路图像语义分割为什么总在边缘翻车:LR-ASPP 与 MobileNet v3 的实战定位

城市道路场景的语义分割,最让人头疼的从来不是把天空、路面、建筑这些大面积区域分对,而是车道线、路沿、护栏、远处行人这些细长或小目标区域。我最早用一套标准编码器-解码器结构跑道路数据,mIoU 看着还行,但一上路可视化就露馅:车道线断断续续,路沿和路面糊成一片,远处车辆直接消失。问题出在解码器——很多结构靠逐级上采样加跳跃连接恢复分辨率,浅层特征里混着大量纹理噪声,深层语义又丢得差不多了,边界自然糊。

LR-ASPP(Lite Reduced Atrous Spatial Pyramid Pooling)配合 MobileNet v3 这套组合,就是冲着「轻量 + 边界可用」来的。MobileNet v3 负责在低算力下提取多尺度语义,LR-ASPP 负责用极少的参数把不同膨胀率的上下文聚合起来,再通过全局池化补一个图像级分支。它不追求在服务器 GPU 上刷榜,而是让边缘设备或车载低功耗平台也能跑出可用的道路分割结果。适合谁?做自动驾驶感知预研、移动端道路巡检、嵌入式视觉产品的工程师,以及想用单卡甚至 CPU 推理验证语义分割落地可行性的开发者。这篇笔记按「先立住原理、再动手复现、最后讲坑」的顺序走,代码基于 PyTorch,数据用常见的道路语义分割标注格式,不依赖任何特定平台。

2. MobileNet v3 与 LR-ASPP 的选型逻辑:为什么不是 DeepLab 加 ResNet

2.1 道路场景对骨干网络的真实约束

道路图像语义分割和通用分割任务有一个关键差异:输入分辨率往往很高(车载相机常见 1280×720 或更高),但推理必须实时或准实时。ResNet-50 加 DeepLab v3+ 在 Cityscapes 上能刷到 80% 左右的 mIoU,但单帧推理在桌面 GPU 上也要几十毫秒,放到嵌入式设备直接不可用。MobileNet v3 的设计目标就是在 ImageNet 分类上逼近 ResNet 精度,同时把乘加运算量压到十分之一量级。

MobileNet v3 有两个版本:Large 和 Small。道路分割我一般选 Large,因为 Small 在浅层就下采样太狠,细长车道线在 1/16 特征图上只剩几个像素,后面怎么恢复都救不回来。Large 的通道配置和 SE(Squeeze-and-Excitation)模块分布更适合保留中频纹理信息。另一个关键点是 MobileNet v3 的 h-swish 激活函数,它在低精度推理(比如 int8)下比 swish 更稳定,这对后续量化部署很重要。

提示:如果你手头只有 CPU 推理环境,MobileNet v3 Large 加 LR-ASPP 在 512×512 输入下,单帧大约 80~150 毫秒,具体取决于线程数和是否用 MKLDNN 加速。这个量级做离线巡检够用,做实时控制就要再压输入分辨率或换 Small。

2.2 LR-ASPP 到底「减」了什么

标准 ASPP 用 6、12、18 三个膨胀率的 3×3 卷积并行,再加全局池化分支,每个分支都是独立卷积,参数量和计算量都不小。LR-ASPP 做了两处关键精简:第一,把多个膨胀卷积分支合并成一个共享的 3×3 深度可分离卷积,膨胀率只取一个较大值(常见 12 或 24),用深度可分离替代标准卷积;第二,全局池化分支不再用大卷积核,而是先全局平均池化到 1×1,再用 1×1 卷积升维后直接上采样回去做注意力式加权。

这样做的代价是:对极端多尺度目标的覆盖不如完整 ASPP。但在道路场景里,目标尺度分布相对集中——路面、天空、建筑是大尺度,车道线、路沿是细长结构,行人车辆是中尺度。LR-ASPP 的单一膨胀率加全局分支,恰好能覆盖「局部上下文 + 全局上下文」两个层次,中间尺度靠骨干网络的多级下采样特征本身来补。实测下来,在道路数据上 LR-ASPP 和完整 ASPP 的 mIoU 差距通常在 1~2 个百分点以内,但参数量能少一半以上。

2.3 输出步长与膨胀率的配合关系

这里有一个容易翻车的参数配合问题。MobileNet v3 作为骨干,通常取到 1/16 输出步长(即下采样 16 倍)就停止,不再往 1/32 走。原因是道路分割对空间分辨率敏感,1/32 特征图上的车道线已经基本不可辨认。在 1/16 特征图上做 LR-ASPP,膨胀率不能设太大,否则有效感受野会超出特征图边界,边缘区域出现零填充伪影。我一般把膨胀率设在 12 左右,配合 1/16 步长,感受野大约覆盖 100~150 像素的原图范围,对车道线连续性和路沿定位比较平衡。

如果你非要用 1/8 输出步长,膨胀率可以降到 6~8,但计算量会上去。如果为了省算力用 1/32,那 LR-ASPP 的膨胀率要相应调大,但细长目标分割质量会明显下降。这个配合关系没有绝对公式,建议在验证集上做一组消融:固定骨干,只改输出步长和膨胀率,看边界区域的 IoU 变化。

3. 用 PyTorch 搭出可训练的道路分割网络:骨干加载与 LR-ASPP 头部实现

3.1 骨干网络的特征提取层改造

MobileNet v3 在 torchvision 里有预训练权重,但直接拿来当骨干需要改两处:一是把最后的分类头和全局池化去掉,只保留卷积特征提取部分;二是确认输出步长。torchvision 的mobilenet_v3_large默认在features里依次下采样,索引到第 16 个 block 时输出步长是 1/16,通道数是 960。我一般取到features[16]为止,后面不再走。

import torch import torch.nn as nn from torchvision.models import mobilenet_v3_large, MobileNet_V3_Large_Weights class MobileNetV3Backbone(nn.Module): def __init__(self, pretrained=True): super().__init__() weights = MobileNet_V3_Large_Weights.DEFAULT if pretrained else None base = mobilenet_v3_large(weights=weights) # 取到第 16 个 block,输出步长 1/16,通道 960 self.features = base.features[:17] # 记录输出通道,供头部使用 self.out_channels = 960 def forward(self, x): return self.features(x)

这段代码的关键在features[:17]。MobileNet v3 Large 的features是一个Sequential,索引 0 到 16 对应从 1/2 到 1/16 的下采样过程。取到 17(不含)意味着输出特征图空间尺寸是输入的 1/16,通道 960。预训练权重加载后,浅层卷积已经具备通用纹理提取能力,道路数据量不大时能明显加快收敛。

注意:不同版本的 torchvision 里mobilenet_v3_large的features索引可能略有差异,建议打印len(base.features)和每层输出尺寸确认。如果索引取错,输出步长变成 1/32,后面 LR-ASPP 的膨胀率就要重新调。

3.2 LR-ASPP 头部的模块拆解与代码

LR-ASPP 头部可以拆成三个分支:局部分支(深度可分离膨胀卷积)、全局分支(全局池化加 1×1 卷积)、融合分支(拼接后 1×1 卷积降维到类别数)。局部分支的深度可分离卷积用groups=in_channels实现,膨胀率设 12。全局分支先自适应平均池化到 1×1,再 1×1 卷积把通道压到 128,然后双线性上采样回原特征图尺寸,用 sigmoid 做注意力加权。

class LRASPPHead(nn.Module): def __init__(self, in_channels=960, num_classes=19, dilate_rate=12): super().__init__() # 局部分支:深度可分离膨胀卷积 self.local_conv = nn.Sequential( nn.Conv2d(in_channels, in_channels, 3, padding=dilate_rate, dilation=dilate_rate, groups=in_channels, bias=False), nn.BatchNorm2d(in_channels), nn.ReLU(inplace=True), nn.Conv2d(in_channels, 128, 1, bias=False), nn.BatchNorm2d(128), nn.ReLU(inplace=True) ) # 全局分支:池化 + 1x1 卷积 + 上采样 self.global_pool = nn.AdaptiveAvgPool2d(1) self.global_conv = nn.Sequential( nn.Conv2d(in_channels, 128, 1, bias=False), nn.BatchNorm2d(128), nn.ReLU(inplace=True) ) # 融合后分类 self.classifier = nn.Conv2d(128, num_classes, 1) def forward(self, x): local_feat = self.local_conv(x) global_feat = self.global_conv(self.global_pool(x)) global_feat = nn.functional.interpolate( global_feat, size=local_feat.shape[2:], mode='bilinear', align_corners=False) # 全局分支做注意力加权,再与局部分支相加 out = local_feat * torch.sigmoid(global_feat) + local_feat return self.classifier(out)

局部分支里groups=in_channels是深度可分离的核心,它让每个通道独立做空间卷积,参数量从标准卷积的in_channels × in_channels × 9降到in_channels × 9。膨胀率 12 配合 3×3 核,有效感受野约 25×25,在 1/16 特征图上对应原图约 400 像素范围,对道路场景的中等尺度上下文够用。全局分支的 sigmoid 加权是一种轻量注意力,让网络根据整图内容调整局部特征的响应强度,比如夜间场景整体偏暗时,全局分支会抑制过强的局部响应,减少噪声误判。

3.3 完整网络组装与输入尺寸约束

把骨干和头部拼起来,再加一个上采样到原图尺寸的输出。这里有一个实操细节:训练时输入尺寸最好固定为 32 的倍数,因为 MobileNet v3 有 5 次下采样(1/2 到 1/16 是 4 次,但浅层还有一次 stride 2 的卷积),不对齐会导致特征图尺寸出现奇数,上采样后和标签对不齐。我一般用 512×512 或 768×768 训练,推理时可以用任意尺寸,但建议也是 32 的倍数。

class RoadSegNet(nn.Module): def __init__(self, num_classes=19, pretrained=True): super().__init__() self.backbone = MobileNetV3Backbone(pretrained=pretrained) self.head = LRASPPHead(in_channels=self.backbone.out_channels, num_classes=num_classes) def forward(self, x): input_size = x.shape[2:] feat = self.backbone(x) out = self.head(feat) # 上采样回输入尺寸 out = nn.functional.interpolate(out, size=input_size, mode='bilinear', align_corners=False) return out

上采样用双线性插值而不是转置卷积,原因是转置卷积在轻量网络里容易产生棋盘格伪影,道路分割的边界区域对这类伪影特别敏感。双线性插值虽然平滑,但配合前面的膨胀卷积,边界定位已经够用。如果对边界要求更高,可以在上采样后加一个轻量的边界细化模块,但那是另一个话题了。

4. 道路数据训练全流程:从标签处理到损失函数与学习率调度

4.1 道路标注数据的读取与忽略区域处理

道路语义分割数据集通常有两种格式:一种是每张图对应一张单通道标签图,像素值就是类别 ID;另一种是彩色标签图,需要按调色板映射回类别 ID。我一般统一转成单通道 PNG,类别 ID 从 0 开始连续编号。关键点是忽略区域(ignore index),道路数据里常见的是 ego vehicle 遮挡区域、未标注区域,这些像素在损失里要排除,否则网络会学到无意义的模式。

import numpy as np from PIL import Image def load_label(label_path, ignore_index=255): label = np.array(Image.open(label_path), dtype=np.uint8) # 如果标签是彩色图,需要按调色板映射,这里假设已是单通道 # 将 ignore_index 之外的类别保留,ignore 区域设为 255 label = np.where(label == ignore_index, 255, label) return label.astype(np.int64)

忽略区域的处理直接影响 mIoU 计算。如果训练时把 ignore 像素也算进损失,网络会花精力去拟合这些无意义区域,导致有效类别精度下降。验证时计算 mIoU 也要排除 ignore 像素,否则指标虚高。常见做法是在 DataLoader 里就把 ignore 区域标成 255,损失函数用CrossEntropyLoss(ignore_index=255)。

4.2 损失函数选择:交叉熵加 Dice 的组合策略

道路分割里类别极度不平衡:路面和天空占大部分像素,车道线、行人、交通标志只占几个百分点。纯交叉熵会让网络偏向大类别,小类别 IoU 很低。我一般用交叉熵加 Dice 损失的组合,交叉熵负责像素级分类,Dice 负责类别级重叠度,两者加权求和。权重上,交叉熵占 0.5,Dice 占 0.5,或者根据小类别占比动态调。

class DiceLoss(nn.Module): def __init__(self, ignore_index=255, smooth=1.0): super().__init__() self.ignore_index = ignore_index self.smooth = smooth def forward(self, logits, targets): num_classes = logits.shape[1] probs = torch.softmax(logits, dim=1) valid_mask = (targets != self.ignore_index) targets = targets.clone() targets[~valid_mask] = 0 one_hot = nn.functional.one_hot(targets, num_classes).permute(0, 3, 1, 2).float() one_hot = one_hot * valid_mask.unsqueeze(1).float() probs = probs * valid_mask.unsqueeze(1).float() dims = (0, 2, 3) intersection = torch.sum(probs * one_hot, dims) cardinality = torch.sum(probs + one_hot, dims) dice = (2. * intersection + self.smooth) / (cardinality + self.smooth) return 1 - dice.mean()

Dice 损失里smooth设 1.0 是防止分母为零,尤其当某个类别在当前 batch 里完全没出现时。valid_mask把 ignore 区域排除在 Dice 计算之外,避免无效像素干扰。组合损失时,交叉熵用nn.CrossEntropyLoss(ignore_index=255),总损失是0.5 * ce_loss + 0.5 * dice_loss。如果发现小类别还是学不好,可以把 Dice 权重提到 0.7,或者对交叉熵做类别加权。

4.3 学习率调度与 warmup 的实操参数

MobileNet v3 预训练权重加载后,浅层特征已经很好,深层和 LR-ASPP 头部是随机初始化。如果一开始就用大学习率,深层参数更新太猛,会把预训练浅层特征带偏。我一般用 warmup 加余弦退火:前 500 到 1000 步线性从 1e-6 升到初始学习率,然后余弦退火到 1e-6。初始学习率设 0.01(batch size 16 时),如果 batch 更小就按比例降。

from torch.optim.lr_scheduler import LambdaLR import math def get_scheduler(optimizer, warmup_steps=800, total_steps=40000, base_lr=0.01): def lr_lambda(step): if step < warmup_steps: return step / warmup_steps progress = (step - warmup_steps) / (total_steps - warmup_steps) return 0.5 * (1 + math.cos(math.pi * progress)) return LambdaLR(optimizer, lr_lambda)

warmup 步数取决于数据量和 batch size。如果一轮迭代只有几百步,warmup 设 200 到 300 就够;如果一轮有几千步,可以设 800 到 1000。余弦退火的周期设总步数,让学习率在训练结束时刚好降到最低。优化器用 SGD 加 momentum 0.9 和 weight decay 1e-4,或者 AdamW 加 weight decay 1e-2。SGD 在分割任务上泛化通常更好,但收敛慢一些;AdamW 收敛快,适合快速验证。

5. 训练和推理中容易翻车的五个坑:从显存爆炸到边界锯齿

5.1 显存不够:输入尺寸和 batch size 的取舍

现象:512×512 输入、batch size 8 时,显存占用超过 11GB,训练直接 OOM。原因:MobileNet v3 虽然轻量,但 LR-ASPP 的局部分支在 1/16 特征图上做膨胀卷积,特征图通道 960,中间激活值占用不小;加上双线性上采样到原图尺寸后,输出张量是batch × num_classes × 512 × 512,num_classes 为 19 时也不小。解决:优先降 batch size 到 4 或 2,用梯度累积模拟大 batch;如果还不够,把输入降到 384×384,但要注意车道线在低分辨率下更细,mIoU 会掉 2 到 3 个点。另一个办法是用混合精度训练,显存能省 30% 到 40%。

5.2 损失不下降:忽略区域和标签对齐问题

现象:训练几个 epoch 后损失在 2.0 附近震荡,mIoU 不涨。原因:标签图在数据增强(比如随机裁剪、翻转)时没有同步变换,或者忽略区域在增强后错位。道路数据里常见的是裁剪后标签和图像不对应,网络学到的是随机噪声。解决:用 albumentations 或 torchvision 的联合变换,确保图像和标签同步;增强后可视化几张标签叠加图,确认忽略区域和实际遮挡区域一致。另一个常见原因是标签类别 ID 不连续,比如从 1 开始编号,而损失函数默认从 0 开始,导致类别错位。

5.3 边界锯齿:上采样方式和膨胀率的配合

现象:推理结果里路沿和车道线边缘呈锯齿状,或者出现周期性条纹。原因:上采样用了最近邻插值,或者转置卷积的 stride 和 kernel 不匹配产生棋盘格。解决:上采样统一用双线性插值,align_corners=False;如果还有锯齿,检查 LR-ASPP 的膨胀率是否过大,导致边缘区域出现零填充伪影。把膨胀率从 12 降到 8 试试,边界会平滑一些,但上下文覆盖会减弱。另一个技巧是在上采样后加一个 3×3 深度可分离卷积做边界平滑,参数量增加很少。

5.4 小类别 IoU 极低:类别权重和 Dice 的调节

现象:路面和天空 IoU 都在 0.9 以上,但车道线、行人、交通标志 IoU 不到 0.3。原因:类别不平衡加上小目标在 1/16 特征图上像素太少。解决:在交叉熵里给小类别更高权重,权重可以按类别频率的倒数开方来设;Dice 损失对小类别更敏感,可以把 Dice 权重从 0.5 提到 0.7。另外,训练时不要过度依赖随机裁剪,因为裁剪可能把小目标裁掉,可以用缩放增强让目标在特征图上占更多像素。如果还是不行,考虑在 1/8 特征图上加一个辅助分割头,专门监督小类别。

5.5 推理速度不达标:算子融合和输入分辨率

现象:模型在桌面 GPU 上单帧 30 毫秒,但部署到目标设备后变成 200 毫秒。原因:目标设备没有针对深度可分离卷积做优化,或者推理框架没有做算子融合。解决:导出 ONNX 后用推理引擎的图优化,把 Conv+BN+ReLU 融合成一个算子;深度可分离卷积在部分设备上要拆成 depthwise 和 pointwise 两步,确认推理引擎支持合并。输入分辨率从 512 降到 384,速度能提升约 40%,但 mIoU 会掉。如果目标设备支持 int8 量化,用训练后量化把模型压到 int8,速度通常能翻倍,但要注意 h-swish 在 int8 下的精度损失,建议量化后在验证集上重新评估。

6. 把 LR-ASPP 推到可用边界:验证方法、量化部署与一个收尾习惯

训练完一个道路分割模型,最怕的是「指标好看、上路翻车」。我一般会做三层验证:第一层是标准 mIoU 和各类别 IoU,确认没有类别被完全忽略;第二层是边界区域单独评估,把标签做形态学膨胀得到边界带,只算边界带内的 IoU,这个指标比整体 mIoU 更能反映边缘质量;第三层是时序一致性检查,如果数据来自视频,连续帧的分割结果不应该出现剧烈跳变,可以算相邻帧预测的 IoU 波动。

量化部署是 LR-ASPP 加 MobileNet v3 的强项,但有几个参数要盯。训练后静态量化需要校准集,校准集最好覆盖白天、傍晚、夜间、雨天等不同光照条件,每类 50 到 100 张。校准过程中观察每层激活值的分布,如果某一层出现极端离群值,量化后精度会崩。我一般把第一层和最后一层保持 float32,中间层量化到 int8,这样精度损失通常控制在 1 到 2 个 mIoU 点以内。如果目标设备支持混合精度,LR-ASPP 的全局分支可以保持 float16,局部分支用 int8,速度和精度平衡更好。

还有一个容易被忽略的点:输入归一化参数要和训练时完全一致。训练用 ImageNet 均值方差,推理时如果忘了减均值,或者均值顺序搞错(RGB 和 BGR),分割结果会整体偏移。我习惯把归一化参数写进模型导出配置里,推理时直接读配置,不手动传。

最后说一个我自己的习惯:每次改完网络结构或损失函数,先在一个小批量(比如 20 张图)上过拟合,确认模型能把这 20 张图的分割误差降到接近零。如果连过拟合都做不到,说明结构或损失有 bug,不用浪费时间去跑完整训练。这个习惯帮我省过很多次「训练三天发现标签读错」的后悔药。道路分割这个方向,模型结构只是起点,数据质量、标签对齐、边界验证才是决定能不能上车的硬功夫。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询