资讯详情

资讯详情

Swin Transformer+CBAM+多尺度融合:自动驾驶路面分类改进实践

简介面向自动驾驶路面信息分类任务这份代码以Swin-Transformer为骨干融入CBAM注意力机制与多尺度特征融合并采用Focal loss应对类别不平衡同时提供Adam、SGD、AdamW三种优化器及余弦退火学习率策略便于进行消融对比实验。资源包含完整图像数据集共2000个文件以1991张路面jpg图像为主辅以3个json标签文件、3个Python训练/推理脚本、2个txt配置与1个readme说明整体压缩包约388MB既可直接用于训练验证也可快速理解网络改进思路。除常规loss与accuracy曲线外还额外生成混淆矩阵、recall、precision、F1 score、特异度以及ROC、PR曲线等评估图表并支持运行QT脚本进行图形化推理帮助读者完整掌握改进分类模型的训练、评估与部署流程。该资源已有109人学习适合计算机视觉方向的研究者、竞赛选手及自动驾驶感知相关项目开发者参考。1. 自动驾驶路面分类为什么需要“Swin注意力多尺度难例挖掘”路面信息分类在自动驾驶里是一个非常“接地气”的感知任务车要判断前方是干燥沥青、湿泥、冰面还是积水直接决定制动策略和路径规划。之前用过 ResNet 这类 CNN 做主干晴天没问题但碰到夜间反光、泥水飞溅、冰雪纹理模糊时小目标细节丢失严重误判率一下就上去了。后来我发现问题不在分类头而在特征提取CNN 的感受野有限对全局上下文和局部细纹理的平衡天生吃力。Swin-Transformer 的分层结构与移位窗口机制正好能保留局部细节的同时建模长距离依赖再在颈部加入 CBAM 注意力机制强化关键通道和空间位置配合多尺度特征融合把浅层纹理与深层语义拼起来最后用 Focal loss 压制训练集中“干泥”“冰面”这类样本数量悬殊带来的梯度倾斜。这篇文章就围绕这一整套改进方案展开从数据组织、损失函数、网络搭建、训练评估到 QT 推理每个环节都给出可直接复用的代码和调参思路适合做自动驾驶感知、路面病害分类或细粒度图像识别的工程师参考。2. 训练数据组织与 Focal Loss 的调参逻辑2.1 数据怎么按文件名归类和划分这个项目的路面图片命名规律非常直接比如20220303104905547-dry-mud.jpg和20220702185258992-ice.jpg文件名里的dry-mud、ice就是类别标签。如果后续想扩类别命名部分可以改成snow、wet-asphalt之类代码里通过解析文件名就能自动生成标签不用额外维护 CSV 标注文件。比如dry-mud这一类在 3 月采集的图片多7 月就只有一张说明季节分布不均这会导致类别不平衡。另外一个隐藏问题是拍摄时间跨度大光照色温不一样模型容易学到“色调”而不是“路面材质”。常见做法是用glob扫描全部图片通过正则从文件名提取标签再按 8:1:1 切分训练集、验证集、测试集。分类任务的输入分辨率我一般会统一到 224×224 或 256×256但 Swin-Transformer 的 patch size 是 4224 能被 4、8、16、32 整除后续层级特征图尺寸不会出现小数这个细节很多人会忽略。代码组织方式如下import glob import re from sklearn.model_selection import train_test_split def parse_label(filename): match re.search(r-(dry-mud|ice|wet-asphalt|snow)\.jpg$, filename) return match.group(1) if match else unknown all_files glob.glob(data/*.jpg) labels [parse_label(f) for f in all_files] train_files, val_files train_test_split( all_files, test_size0.2, stratifylabels, random_state42 ) val_files, test_files train_test_split( val_files, test_size0.5, stratify[parse_label(f) for f in val_files], random_state42 )分段分层抽样有个好处单独在验证集上再做一次划分确保测试集完全没有参与过调参过程。stratify参数按原始类别比例抽样避免切分后某些类别集中到单一集合。参数random_state42固定随机种子保证多次实验结果可比这对后面做优化器消融对比非常重要。2.2 Focal Loss 的 gamma 和 alpha 怎么设路面数据里“干泥”和“正常沥青”在白天场景太像负样本容易主导梯度而“冰面”样本少但事故代价高。Focal Loss 的核心改动是在交叉熵上乘一个(1 - p_t)^γ的调制因子让高置信度的易分类样本损失权重下降把模型注意力逼向难样本。公式上是FL -α_t (1 - p_t)^γ log(p_t)当γ0时退化为带权重的交叉熵。实际调参过程中我发现γ太大容易导致训练初期损失震荡尤其是 Swin-Transformer 这种大模型收敛本来就慢初期预测概率都在 0.5 附近(1-p)^γ对所有样本都在压梯度等于变相降低学习率。比较稳妥的起步参数是γ2.0α按类别频率反比设置。比如统计结果里dry-mud占 60%ice占 5%那α分别为 0.4 和 0.95 左右让少数类的损失贡献被抬高。等模型 loss 稳定后再逐步把γ加到 2.5观察验证集上recall是否上升。类别不平衡特别严重时可以配合 WeightedRandomSampler 给少数类增加采样概率但注意这会增加每个 epoch 的训练时间。实现代码如下import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alphaNone, gamma2.0, num_classes4): super().__init__() self.gamma gamma if alpha is None: self.alpha torch.ones(num_classes) else: self.alpha torch.tensor(alpha) def forward(self, logits, targets): ce F.cross_entropy(logits, targets, reductionnone) p torch.exp(-ce) alpha_t self.alpha[targets].to(logits.device) focal alpha_t * (1 - p) ** self.gamma * ce return focal.mean()logits是模型输出的未归一化分数targets是类别索引。torch.exp(-ce)算出的p就是 softmax 后对应正确类别的概率。发现 loss 长期不降时优先检查p是否集中在 0 附近如果是说明模型在摆烂需要调低gamma或增大alpha。此外测试阶段不要用 Focal Loss它只是训练期的辅助函数推理时直接argmax(logits)就行。3. Swin-Transformer 与 CBAM 注意力机制的组合方式3.1 Swin-Transformer 主干网络的选择理由Swin-Transformer 采用分层特征金字塔结构stage 2 到 stage 4 输出的特征图分辨率逐渐减半通道数翻倍这和 FPN 类多尺度融合结构天然兼容。它的移位窗口自注意力shifted window在窗口内部做自注意力相邻层之间窗口移位相当于用局部注意力模拟卷积的局部归纳偏置同时保持全局建模能力。相比 ViT 直接对整张图做全局注意力Swin 的计算复杂度从平方级降为线性在自动驾驶这种需要批处理大量路况图片的场景下更实用。主干网络选择swin_tiny_patch4_window7_224参数量小推理帧率更高。如果换成swin_base或swin_large精度能涨但显存占用会翻几倍路面分类任务用 tiny 和 small 足够。pretrainedTrue时加载 ImageNet-22K 预训练权重这是官方提供的 backbone 参数初始化在自动驾驶数据集上做迁移学习时预训练权重能显著加速收敛这也是消融实验里第一个要对比的变量不加载预训练权重直接从头训练。注意 Swin 和 CNN 不一样它的 patch embedding 层对输入分辨率更敏感做数据增强时尽量别用随机缩放过头最好统一先 resize 到 224 再裁剪。3.2 CBAM 注意力模块插入哪个位置CBAM 是轻量级注意力模块包含通道注意力Channel Attention和空间注意力Spatial Attention两个分支。通道注意力用全局平均池化和全局最大池化分别压缩空间信息再经过共享 MLP 得到两个通道权重向量相加后接 sigmoid 生成最终通道权重。空间注意力则沿通道维度做平均池化和最大池化拼成一个 2 通道的特征图卷成 1 通道再接 sigmoid。它能以极小的参数量提升特征表达力但前提是插入位置不能破坏 Swin 的层级结构。通用做法是把 CBAM 放在 Swin 的每个 stage 输出之后也就是stage1、stage2、stage3、stage4的特征图都过一遍 CBAM再做多尺度融合。如果只在最后一个 stage 加改进效果很有限浅层纹理细节没有被注意力强化路面裂缝和小石子这类目标依然会丢。实现时用一个nn.ModuleDict把 CBAM 分别连接到四个 stage 输出。这里给出完整的骨干网络封装代码import torch import torch.nn as nn from timm.models.swin_transformer import swin_tiny_patch4_window7_224 class ChannelAttention(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.mlp nn.Sequential( nn.Linear(channels, channels // reduction, biasFalse), nn.ReLU(inplaceTrue), nn.Linear(channels // reduction, channels, biasFalse), ) def forward(self, x): avg_pool x.mean(dim[2, 3]) max_pool x.amax(dim[2, 3]) attention self.mlp(avg_pool) self.mlp(max_pool) return x * torch.sigmoid(attention).unsqueeze(2).unsqueeze(3) class SpatialAttention(nn.Module): def __init__(self, kernel_size7): super().__init__() self.conv nn.Conv2d(2, 1, kernel_size, paddingkernel_size // 2, biasFalse) def forward(self, x): avg_out x.mean(dim1, keepdimTrue) max_out x.amax(dim1, keepdimTrue) out torch.cat([avg_out, max_out], dim1) return x * torch.sigmoid(self.conv(out)) class CBAM(nn.Module): def __init__(self, channels): super().__init__() self.channel_attention ChannelAttention(channels) self.spatial_attention SpatialAttention() def forward(self, x): x self.channel_attention(x) x self.spatial_attention(x) return x class SwinCBAMBackbone(nn.Module): def __init__(self, num_classes4, pretrainedTrue): super().__init__() backbone swin_tiny_patch4_window7_224(pretrainedpretrained) # 提取 Swin 四个 stage 输出的特征而不是最终分类头 self.stage1 backbone.layers[0] self.stage2 backbone.layers[1] self.stage3 backbone.layers[2] self.stage4 backbone.layers[3] self.norm1 backbone.norm[0] self.norm2 backbone.norm[1] self.norm3 backbone.norm[2] self.norm4 backbone.norm[3] c1, c2, c3, c4 96, 192, 384, 768 self.cbam1 CBAM(c1) self.cbam2 CBAM(c2) self.cbam3 CBAM(c3) self.cbam4 CBAM(c4) # 后面接分类头 self.classifier nn.Linear(c4, num_classes) def forward_features(self, x): x self.stage1(x) f1 self.cbam1(x.permute(0, 3, 1, 2).float()) x self.stage2(x) f2 self.cbam2(x.permute(0, 3, 1, 2).float()) x self.stage3(x) f3 self.cbam3(x.permute(0, 3, 1, 2).float()) x self.stage4(x) f4 self.cbam4(x.permute(0, 3, 1, 2).float()) return f1, f2, f3, f4这里有个关键细节Swin 的中间特征图维度是(B, H/p, W/p, C)而 CBAM 期望输入是(B, C, H, W)所以必须在送入 CBAM 前做permute否则通道和空间维度的统计结果完全是错的。timm库的 Swin 模型里backbone.norm是 layer norm 列表需要分别取出来对每个 stage 的特征做归一化。stage1到stage4的通道数分别是 96、192、384、768这个通道数配置直接影响后续多尺度融合时各层 1×1 卷积的输出维度后面做融合时会把 4 层统一到 256 维。3.3 多尺度特征融合的具体实现多尺度特征融合的思想来自 FPN浅层特征分辨率高、语义弱适合定位精细纹理深层特征分辨率低、语义强适合分类。路面分类里既有“大范围冰面”这种全局目标也有“裂缝”“石子”这种局部纹理只取最后一层特征做分类会让小目标信息流失。融合方式我用的是双向特征金字塔的简化版从深层到浅层逐级上采样相加再从浅层到深层逐级下采样相加最后把 4 个尺度合并成一个向量。第一步用 1×1 卷积把各层的 96、192、384、768 通道统一到 256然后深层特征双线性插值到上一层尺寸做相加。上采样推荐双线性插值而不是反卷积反卷积容易产生棋盘伪影。融合完成后接全局平均池化把不同尺度的特征压缩成固定长度向量作为分类头的输入。注意融合后分类头的输入维度是 256 还是 512 或者 1024取决于怎么拼接。如果四层池化向量直接cat得到 1024 维如果按 FPN 方式逐层融合后再池化得到 256 维。实践里后者效果更好因为 FPN 结构已经完成了跨尺度信息交换cat反而会让分类头过度依赖于某一个尺度的统计量。相关代码class MultiScaleFusion(nn.Module): def __init__(self, in_channels_list, out_channels256): super().__init__() # 每个尺度都做一个 1x1 卷积统一通道数 self.lateral_convs nn.ModuleList([ nn.Conv2d(c, out_channels, 1) for c in in_channels_list ]) # 融合特征最终的分类头 self.head nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(out_channels, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(128, 4), ) def forward(self, features): f1, f2, f3, f4 features # 1. 统一通道 p1 self.lateral_convs[0](f1) p2 self.lateral_convs[1](f2) p3 self.lateral_convs[2](f3) p4 self.lateral_convs[3](f4) # 2. 自顶向下融合p4 上采样加到 p3 p3 p3 F.interpolate(p4, sizep3.shape[2:], modebilinear, align_cornersFalse) p2 p2 F.interpolate(p3, sizep2.shape[2:], modebilinear, align_cornersFalse) p1 p1 F.interpolate(p2, sizep1.shape[2:], modebilinear, align_cornersFalse) # 3. 输出融合后的最强尺度 (p1 有最丰富的空间信息) return self.head(p1)sizep3.shape[2:]这种写法比写死目标尺寸更稳妥因为输入图片如果不固定尺寸不同 batch 的特征图大小可能变化。align_cornersFalse是双线性插值的默认推荐值像素中心对齐方式更符合卷积特征图的几何定义。融合时先做加法再做interpolate这样每一层都携带了全局语义信息浅层不会只在边缘纹理上打转。最后分类头的Dropout(0.3)是应对融合后维度增加带来的过拟合风险尤其是当训练数据量只有几千张图时Transformer 模型很容易在验证集上出现 loss 上升但准确率停滞的情况。4. 训练策略三种优化器对比与余弦退火调度4.1 Adam、SGD、AdamW 的消融对比设计这个项目把优化器作为消融实验的重要维度同时提供 Adam、SGD、AdamW 三种选择方便确认到底哪种优化器最适合改进后的网络。SGD 加动量是传统 CNN 训练的标配对超参数敏感度低但收敛慢Adam 自适应学习率收敛快但容易收敛到尖锐极小值AdamW 是 Adam 的修正版把权重衰减从梯度里剥离出来直接作用在参数更新上有效抑制过拟合尤其是 Transformer 大类模型的权重衰减不能直接套用 L2 正则的公式一定要用解耦的 AdamW。实际操作中三种优化器的初始学习率必须分开设置SGD 建议 0.01 到 0.05Adam 建议 1e-4 到 3e-4AdamW 建议 1e-4 到 2e-4。如果直接统一用 1e-3SGD 会很早 loss 爆炸Adam 则可能前期微调过猛导致后期学不动。消融对比时固定模型结构、数据集、数据增强和损失函数仅切换优化器类型并用相同的余弦退火策略训练结束后对比验证集的最高 accuracy 和 loss 收敛速度。代码实现时用build_optimizer函数统一创建def build_optimizer(model, optimizer_typeAdamW, lr1e-4, weight_decay5e-2): if optimizer_type Adam: optimizer torch.optim.Adam(model.parameters(), lrlr, weight_decayweight_decay) elif optimizer_type AdamW: optimizer torch.optim.AdamW(model.parameters(), lrlr, weight_decayweight_decay) elif optimizer_type SGD: optimizer torch.optim.SGD(model.parameters(), lrlr * 10, momentum0.9, weight_decayweight_decay) return optimizerSGD 的lr要单独放大到 Adam 的 10 倍左右才能保证训练步长接近。weight_decay5e-2适合 AdamW如果换回 SGD 时可以适当降到 1e-4因为 SGD 本身泛化能力已经更强过大的权重衰减会把模型参数压得太小导致特征表达能力下降。4.2 余弦退火学习率调度器与训练主循环余弦退火Cosine Annealing Warm Restarts会在每个周期内把学习率从初始值平滑降到接近 0然后瞬间回升。这种波动能帮助模型跳出局部极小值尤其是 Transformer 这类训练后期容易卡在平坦区域的情况。路面分类里我会设T_010表示首个周期 10 个 epochT_mult2表示下一周期长度翻倍。如果硬件时间充足多周期重启的精度通常比单周期线性下降高 1 到 2 个百分点。训练过程里必须同步记录训练集和验证集的 loss 与 accuracy每个 epoch 结束都要算一次。Swin-Transformer 收敛速度比 ResNet 慢前 5 个 epoch 准确率可能只有 50% 左右不要因此提前终止。训练主循环里还需要用scaler.scale(loss)做混合精度训练否则 Swin 加 CBAM 的显存占用轻松超过 16G。下面是完整训练流程代码片段scaler torch.cuda.amp.GradScaler() scheduler torch.optim.lr_scheduler.CosineAnnealingWarmRestarts( optimizer, T_010, T_mult2, eta_min1e-6 ) for epoch in range(epochs): model.train() train_loss, train_acc 0.0, 0.0 for images, targets in train_loader: images, targets images.cuda(), targets.cuda() optimizer.zero_grad() with torch.cuda.amp.autocast(): logits model(images) loss criterion(logits, targets) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() train_loss loss.item() * images.size(0) preds logits.argmax(dim1) train_acc (preds targets).sum().item() validate(model, val_loader, criterion) scheduler.step()scaler.scale(loss).backward()是关键点混合精度下反向传播必须用 scale 后的 loss否则梯度会下溢为零。scheduler.step()在 CosineAnnealingWarmRestarts 里应该放在每个 epoch 结束后而不是每个 batch 结束后否则学习率会在一个 epoch 内部就剧烈波动。验证阶段的model.eval()配合torch.no_grad()必须在函数内部显式声明。4.3 评估指标的计算与可视化除了准确率这个项目还要求输出混淆矩阵、precision、recall、F1 score、特异度和 ROC/PR 曲线。这些指标能从不同角度暴露模型问题accuracy 在类别不平衡下会虚高尤其当“干泥”类别占 60% 时全猜干泥也有 60% 准确率。所以每轮验证要单独计算每个类别的 recall特别是ice类它代表的是行车安全隐患宁可误报也不能漏报。sklearn提供完整计算函数但要注意recall_score的average参数macro对每个类别算完再平均weighted按样本比例加权两个值差异大说明模型严重偏科。特异度的计算方式是对每个类别TN / (TN FP)表示“不是这个类别时模型判断正确的概率”在路面分类里特异度太低会导致频繁刹停影响乘坐体验。生成 ROC 曲线时predict_proba输出的概率矩阵需要做 One-vs-Rest 处理二分类和多分类的曲线画法完全不一样。相关评估代码如下from sklearn.metrics import (confusion_matrix, precision_score, recall_score, f1_score, roc_curve, auc) def full_eval(model, val_loader): y_true, y_prob [], [] model.eval() with torch.no_grad(): for images, targets in val_loader: logits model(images.cuda()) y_true.extend(targets.cpu().numpy()) y_prob.extend(torch.softmax(logits, dim1).cpu().numpy()) y_pred np.argmax(y_prob, axis1) cm confusion_matrix(y_true, y_pred) print(fPrecision: {precision_score(y_true, y_pred, averagemacro):.4f}) print(fRecall: {recall_score(y_true, y_pred, averagemacro):.4f}) print(fF1: {f1_score(y_true, y_pred, averagemacro):.4f}) # specificity: TN / (TN FP) tn cm.sum(axis1) - cm.diagonal() fp cm.sum(axis0) - cm.diagonal() specificity tn / (tn fp) print(Specificity per class:, specificity) return cm在阅读混淆矩阵时有一个多次踩坑的经验对角线数值高不代表分类正确要观察非对角线上的错分模式。比如dry-mud和ice频繁互错说明模型主要依赖颜色信息而不是纹理如果wet-asphalt被错分成ice大概率是反光区域被空间注意力过度放大此时调整 CBAM 里空间注意力的卷积核大小或减少gamma都能改善。5. QT 推理脚本的集成与部署边界5.1 推理脚本的功能设计与加载流程训练完成后运行qt_interface.py即可启动基于 PyQt 的推理界面。脚本的核心逻辑包括加载训练好的权重文件、加载一张或多张图片、前向计算类别与置信度。加载模型时一定要复现训练时的预处理流程包括Resize((224, 224))、ToTensor()、标准化均值和标准差。如果训练和推理的预处理不一致模型的输入分布偏移直接导致预测错乱。界面布局上左侧显示原始图像右侧显示预测结果和置信度条。有一个容易被忽略的隐患PyQt 里QFileDialog.getOpenFileName返回的文件路径在 Windows 下是反斜杠传给PIL.Image.open时没问题但如果路径中包含了中文目录PIL 会报错需要在打开前做一次Path(path).resolve()转换。推理前用model.eval()关闭 dropout再用torch.no_grad()包裹前向计算这两个操作会显著影响最终预测的稳定性。5.2 将模型导出为 TorchScript 用于车载部署PyQt 脚本适合实验室演示和数据集验证真正部署到自动驾驶车辆上时要转成 TorchScript 或 ONNX 格式。TorchScript 的优势是不依赖原始 Python 环境能在车载 Linux 系统上通过 libtorch 直接加载推理延迟比 Python 的 PyTorch 低 30% 以上。导出的过程很简单但有一个致命坑如果模型内部使用了torch.amax、torch.cat这类动态算子需要确保这些算子在 TorchScript 的 JIT 编译器支持列表里。CBAM 里的amax(dim[2,3])属于常用算子支持良好。导出命令如下model.eval() example_input torch.randn(1, 3, 224, 224).cuda() traced_model torch.jit.trace(model, example_input) traced_model.save(swin_cbam_road.pt)torch.jit.trace需要给一个固定尺寸的示例输入模型内部所有张量形状都会按这个输入固化。如果后续要支持动态分辨率输入必须改用torch.jit.script模式但 script 模式对代码里 Python 语法兼容性要求更高本项目里 CBAM 的permute和view操作需要手动标注形状。实际工程里直接固定 224×224 输入是更稳妥的方案因为车载相机采集的图像都会先经过裁剪器统一缩放。推理完成后推荐在测试集上计算每个类别的预测置信度分布如果某个类别输出的 softmax 最大概率普遍低于 0.6说明模型对这类样本的判别力不足需要回到数据层面补充更多角度和光照条件的数据而不是继续调网络结构。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →