简介:本资源面向计算机视觉学习者与道路场景语义分割开发者,提供基于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-ASPP(Lite Reduced Atrous Spatial Pyramid Pooling)配 MobileNet v3 就是冲着这个矛盾来的——它把 ASPP 里那些大膨胀率的空洞卷积砍掉,只保留全局池化和一个低膨胀率分支,参数量和计算量大幅下降,同时在道路、人行道、车辆这些大目标上精度损失很小。这个组合适合谁?适合需要在 Jetson、瑞芯微、地平线这类边缘设备上做实时道路分割的团队,也适合想入门语义分割但不想一上来就被 ResNet 压垮的开发者。它不追求刷榜,追求的是「能跑起来、跑得稳、边界够用」。
2. LR-ASPP 与 MobileNet v3 的配合逻辑:从骨干到分割头的选型拆解
2.1 MobileNet v3 作为骨干:为什么不是 v2 也不是 EfficientNet
MobileNet 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.0,0.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 深度可分离卷积分支。砍掉大膨胀率空洞卷积的代价是感受野变小,但道路分割里大部分像素属于大目标(路面、天空、建筑),全局池化已经能提供足够的全局上下文,大膨胀率空洞卷积带来的收益在这个任务上并不明显。
| 对比项 | 标准 ASPP | LR-ASPP |
|---|---|---|
| 分支数 | 4(1x1 + 3个空洞卷积 + 全局池化) | 3(1x1 + 深度可分离3x3 + 全局池化) |
| 最大膨胀率 | 18 | 1 |
| 参数量(分割头) | 约 2.1M | 约 0.3M |
| 计算量(512x512输入) | 约 4.8 GFLOPs | 约 0.9 GFLOPs |
| 道路类 mIoU 影响 | 基准 | 下降约 0.8% |
这个表里的数据是我在 Cityscapes 上跑出来的,不同数据集会有浮动,但量级关系稳定。LR-ASPP 的另一个改动是低层特征融合方式:标准 DeepLab v3+ 会把骨干网络的浅层特征拿过来和分割头输出做 concat,LR-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 默认是 stride=2 的下采样,你需要把最后两个 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, pretrained=True): super().__init__() # 加载预训练权重,去掉分类头 base = mobilenet_v3_large(pretrained=pretrained) 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_channels=960, low_channels=112, num_classes=19): super().__init__() # 全局池化分支 self.global_pool = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, 128, 1, bias=False), nn.BatchNorm2d(128), nn.ReLU(inplace=True) ) # 1x1卷积分支 self.conv1x1 = nn.Sequential( nn.Conv2d(in_channels, 128, 1, bias=False), nn.BatchNorm2d(128), nn.ReLU(inplace=True) ) # 深度可分离3x3分支 self.dw_conv = nn.Sequential( nn.Conv2d(in_channels, in_channels, 3, padding=1, 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) ) # 低层特征融合 self.low_conv = nn.Sequential( nn.Conv2d(low_channels, 128, 1, bias=False), nn.BatchNorm2d(128), nn.ReLU(inplace=True) ) # 最终分类头 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]), mode='bilinear', align_corners=False) c1 = self.conv1x1(high_feat) dw = self.dw_conv(high_feat) # 融合 x = gp + c1 + dw x = nn.functional.interpolate(x, size=(h, w), mode='bilinear', align_corners=False) low = self.low_conv(low_feat) x = x + low return self.classifier(x)in_channels=960是 MobileNet v3 Large 最后输出的通道数,low_channels=112是步长 16 那层的通道数。三个分支的输出通道统一到 128,相加后上采样到低层特征尺寸,再和低层特征相加,最后 1x1 卷积输出类别数。num_classes=19是 Cityscapes 的类别数,换成自己的数据集要改这个值。全局池化分支里interpolate的align_corners=False和训练时保持一致,否则推理结果会有半个像素的偏移,道路边界上看得出来。
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), dtype=np.uint8) for obj in data['objects']: if obj['label'] == 'ignore': continue mask = np.zeros((h, w), dtype=np.uint8) polygon = np.array(obj['polygon'], dtype=np.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], outline=1, fill=1) 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_ok=True) 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_classes=19, ignore_index=255, dice_weight=0.5): super().__init__() self.ce = nn.CrossEntropyLoss(ignore_index=ignore_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, dim=1) 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_dice=True): 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_index=255让交叉熵跳过忽略区域。Dice 损失里用mask把忽略区域置零,避免它们影响交集和并集的计算。dice_weight=0.5是个经验值,调到 1.0 的话小类别召回会高一点,但大类别 mIoU 可能掉 0.5 个点。训练脚本里用一个use_dice标志控制前 5 个 epoch 不启用 Dice。
3.3 训练参数:学习率、批大小和迭代次数的实操设置
MobileNet v3 + LR-ASPP 的参数量小,训练时可以用大一点的批大小。单卡 11GB 显存,512x512 输入,批大小 8 没问题,用梯度累积可以等效到 16。学习率用多项式衰减,初始 0.01,power 0.9。优化器用 SGD 加 momentum 0.9,weight decay 1e-4。Adam 也能用,但 SGD 在分割任务上泛化通常好一点。
| 参数 | 值 | 说明 |
|---|---|---|
| 输入尺寸 | 512x512 | 再大显存吃紧,再小边界糊 |
| 批大小 | 8 | 单卡11GB,梯度累积到16 |
| 初始学习率 | 0.01 | SGD,多项式衰减 |
| 迭代次数 | 40000 | Cityscapes 约 120 个 epoch |
| 权重衰减 | 1e-4 | 只对卷积和全连接,BN 不加 |
| 动量 | 0.9 | SGD 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, size=labels.shape[1:], mode='bilinear', align_corners=False) loss = criterion(pred, labels, use_dice=use_dice) optimizer.zero_grad() loss.backward() optimizer.step() # 每50步打印一次 if i % 50 == 0: print(f'Epoch {epoch}, Iter {i}, Loss {loss.item():.4f}, Dice {use_dice}') # 每个epoch验证一次 miou = evaluate(model, val_loader) print(f'Epoch {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_version=11, do_constant_folding=True )opset_version=11对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,不够再上 INT8,INT8 校准集用 500 张训练图,覆盖不同光照和天气。
# TensorRT 转换命令,用 trtexec trtexec --onnx=lraspp_mobilenetv3.onnx \ --saveEngine=lraspp_fp16.engine \ --fp16 \ --workspace=2048 \ --minShapes=input:1x3x512x512 \ --optShapes=input:1x3x512x512 \ --maxShapes=input:1x3x512x512workspace=2048是 2GB 显存给 TensorRT 做优化,Jetson Xavier NX 上够用。minShapes、optShapes、maxShapes都设成一样,因为输入尺寸固定。INT8 的话加--int8 --calib=calibration.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], axis=0).astype(np.uint8) # [512,512] # 颜色映射 color = np.zeros((512, 512, 3), dtype=np.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在 axis=0 上做,因为输出是 [C,H,W]。颜色映射表按 Cityscapes 官方颜色来,可视化时和标注图对比能快速看出哪里分错了。如果推理结果整体偏一个类别,先检查 ONNX 导出时输出节点是不是接在classifier后面,有时候会误接到中间层。
4.4 帧率实测:Jetson Xavier NX 上的性能数据
| 配置 | 精度 | 推理耗时 | 帧率 |
|---|---|---|---|
| PyTorch FP32 | 72.1 mIoU | 85 ms | 11.8 FPS |
| TensorRT FP16 | 71.9 mIoU | 38 ms | 26.3 FPS |
| TensorRT INT8 | 70.4 mIoU | 22 ms | 45.5 FPS |
| TensorRT FP16 + 0.75宽度 | 70.2 mIoU | 28 ms | 35.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的对应关系是固定的,比如labelId=7对应trainId=0(道路)。转换时查这个表,别直接用原始 ID。
5.2 现象:推理结果整体偏移半个像素,道路边界有锯齿
原因是上采样时align_corners设置不一致。训练时用align_corners=False,推理时如果用了True,特征图会偏移半个像素,边界上看得出来。另一个可能是 ONNX 导出时interpolate的align_corners属性没保留,TensorRT 解析时默认成了True。
解决:训练、导出、推理三处统一用align_corners=False。导出 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 或 32,BN 的动量从 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_location='cpu') 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, strict=False) # 重新初始化classifier nn.init.kaiming_normal_(model.classifier.weight, mode='fan_out', nonlinearity='relu') nn.init.constant_(model.classifier.bias, 0) return modelstrict=False让缺失的classifier键不报错。kaiming_normal的fan_out模式适合后面接 ReLU 的情况,虽然classifier后面没有激活函数,但用这个初始化能让初始输出值在合理范围。微调时学习率用 0.001,比从头训练的 0.01 小一个量级,避免把预训练特征打乱。
验证迁移效果的时候,我习惯先冻结骨干只训分割头 5 个 epoch,再解冻全部微调。冻结阶段学习率可以到 0.005,解冻后降到 0.001。如果自己的数据集和 Cityscapes 分布差异大(比如全是乡村道路),冻结阶段可以跳过,直接全网络微调,但学习率要再降一半。
有个习惯我保持了三年:每次训完一个模型,先把推理结果按类别拆开看,道路、车辆、行人各挑 10 张可视化,肉眼过一遍。mIoU 是平均值,小类别的翻车在平均值上看不出来,但上路之后就是血泪教训。希望帮到你。
本文还有配套的精品资源,点击获取