ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于CNN的交通标志分类实战:从GTSRB数据到模型量化部署

2026/10/1 10:53:51 拓冰建站 浏览量
基于CNN的交通标志分类实战:从GTSRB数据到模型量化部署 简介这份资源面向人工智能与智慧交通方向的初学者及项目实践者聚焦使用卷积神经网络完成交通标志图像分类任务适合作为课程设计、竞赛入门或深度学习练手项目。压缩包共2000个文件以1994张png交通标志图片构成训练与测试样本另含3个xml标注文件和3个py脚本整体约201.95MB目录按类别组织便于直接读取与批量训练。训练脚本支持指定训练集、测试集与模型保存路径预测脚本可对单张图片输出分类结果并展示置信度覆盖从数据加载、模型训练到推理验证的完整流程。已有78人学习下载说明该实践方案具备一定参考价值。读者可借此理解CNN在图像分类中的网络搭建、数据预处理与模型评估思路并基于现有脚本快速复现实验、调整超参数或迁移到其他标志识别场景。1. 从一张模糊的路牌说起CNN 交通标志分类到底在解决什么城市道路上的限速牌、禁止掉头、注意行人这些标志在车载摄像头里往往只有几十个像素还经常被逆光、雨雾、树影切掉一半。智慧交通系统要在这种条件下实时判断「前方 200 米是什么标志」靠传统模板匹配或者 HOGSVM 基本撑不住——换个角度、换种光照准确率就断崖式下跌。卷积神经网络CNN之所以成为交通标志分类的主流方案核心在于它能把「形状颜色纹理」这些低层特征自动逐层抽象成高层语义不需要人工设计特征算子。这个项目标题指向的是一套完整的落地链路用 CNN 对交通标志做多分类覆盖数据准备、模型搭建、训练调参、推理部署四个环节。它适合三类人正在做人工智能大作业或毕业设计的学生、想入门深度学习项目实战的工程师、以及需要给智慧交通产品做算法原型的开发者。数据集通常用德国交通标志识别基准GTSRB共 43 类、5 万多张图片是入门 CNN 图像分类最经典的练手场。下面我按自己实际跑通这套流程的顺序把每一步的参数、坑和验证方法讲清楚。2. 数据准备与预处理把 5 万张图变成 CNN 能吃的张量2.1 GTSRB 数据集的目录结构与读取方式GTSRB 的原始格式是每个类别一个文件夹图片是 PPM 格式尺寸从 15×15 到 250×250 不等。直接丢给 CNN 会出问题尺寸不统一、类别极度不均衡限速 30 有 2000 多张某些稀有标志只有 200 多张。我一般先把数据整理成统一的目录树再用ImageFolder或自定义 Dataset 读取。import os import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class GTSRBDataset(Dataset): def __init__(self, root_dir, transformNone): self.samples [] self.transform transform # 遍历每个类别文件夹收集 (图片路径, 类别索引) for label in sorted(os.listdir(root_dir)): class_dir os.path.join(root_dir, label) if not os.path.isdir(class_dir): continue for fname in os.listdir(class_dir): if fname.lower().endswith((.ppm, .png, .jpg)): self.samples.append((os.path.join(class_dir, fname), int(label))) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label这段代码的关键点是sorted(os.listdir())保证类别索引稳定不会因为文件系统顺序不同导致标签错位。convert(RGB)必须加GTSRB 里有灰度图不转成三通道后面网络输入维度会对不上。2.2 尺寸归一化与数据增强的参数选择所有图片统一缩放到 32×32 还是 48×48我的经验是如果算力有限、追求训练速度32×32 够用GTSRB 上能到 97% 左右如果要冲 99% 以上用 48×48 配合更深的网络。归一化用 ImageNet 的均值和标准差就行虽然 GTSRB 不是 ImageNet但迁移过来效果稳定。train_transform transforms.Compose([ transforms.Resize((32, 32)), # 统一尺寸 transforms.RandomRotation(15), # 随机旋转 ±15 度模拟拍摄角度偏差 transforms.ColorJitter(brightness0.3, contrast0.3), # 模拟光照变化 transforms.RandomAffine(degrees0, translate(0.1, 0.1)), # 随机平移 transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.272, 0.260, 0.266]) # GTSRB 统计值 ]) val_transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.272, 0.260, 0.266]) ])数据增强只加在训练集上验证集和测试集绝对不能加随机旋转和平移否则评估结果没有意义。RandomRotation(15)这个 15 度是试出来的再大一些部分标志的箭头方向会变得歧义反而拉低准确率。ColorJitter的 brightness 和 contrast 都设 0.3再高会让红色禁令标志褪色成粉色模型学不到颜色特征。2.3 类别不均衡的处理加权采样还是数据补全GTSRB 的类别分布从 200 多到 2000 多不等比例接近 1:10。两种处理方式我都试过过采样少数类容易过拟合加权损失函数更稳。具体做法是给CrossEntropyLoss传weight参数权重取该类样本数的倒数再归一化。from collections import Counter import numpy as np # 统计每个类别的样本数 labels [s[1] for s in train_dataset.samples] counts Counter(labels) num_classes 43 total sum(counts.values()) # 计算权重样本越少权重越高 weights torch.tensor( [total / (num_classes * counts[i]) for i in range(num_classes)], dtypetorch.float32 ) criterion torch.nn.CrossEntropyLoss(weightweights)total / (num_classes * counts[i])这个公式的含义是如果某个类别的样本数低于平均值它的权重就大于 1损失函数会更关注这些样本。实测下来稀有类别的召回率能从 0.82 提到 0.91整体准确率不掉。3. CNN 模型搭建从三层基线到 ResNet 迁移的选型对比3.1 一个能跑通的四层 CNN 基线结构新手最容易犯的错是一上来就堆很深结果训练不收敛还找不到原因。我建议先用一个四层卷积的基线把流程跑通确认数据管道没问题再换更复杂的模型。import torch.nn as nn import torch.nn.functional as F class TrafficSignCNN(nn.Module): def __init__(self, num_classes43): super().__init__() # 输入 3×32×32 self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(64) self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) self.bn3 nn.BatchNorm2d(128) self.conv4 nn.Conv2d(128, 128, kernel_size3, padding1) self.bn4 nn.BatchNorm2d(128) self.pool nn.MaxPool2d(2, 2) self.dropout nn.Dropout(0.5) self.fc1 nn.Linear(128 * 2 * 2, 256) self.fc2 nn.Linear(256, num_classes) def forward(self, x): x self.pool(F.relu(self.bn1(self.conv1(x)))) # 32→16 x self.pool(F.relu(self.bn2(self.conv2(x)))) # 16→8 x self.pool(F.relu(self.bn3(self.conv3(x)))) # 8→4 x self.pool(F.relu(self.bn4(self.conv4(x)))) # 4→2 x x.view(x.size(0), -1) # 展平 128*2*2 x self.dropout(F.relu(self.fc1(x))) return self.fc2(x)每个卷积层后面跟 BatchNorm 再跟 ReLU这个顺序不能反。BatchNorm 放在 ReLU 前面能让激活值分布更稳定训练初期 loss 下降更平滑。padding1配合kernel_size3保证特征图尺寸不变只有池化层在降采样。Dropout 设 0.5 是分类任务的经验值再高会欠拟合。3.2 迁移学习用 ResNet18 替换基线网络的收益与代价基线模型在 GTSRB 上大概能到 97.5%想再往上走迁移学习是最省力的路径。ResNet18 在 ImageNet 上预训练过底层卷积核已经能识别边缘、纹理、色块拿过来微调就行。import torchvision.models as models def build_resnet18(num_classes43, freeze_backboneTrue): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) if freeze_backbone: # 冻结除 fc 层外的所有参数 for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(512, num_classes) ) return modelfreeze_backboneTrue时只训练最后的 fc 层训练速度快适合数据量小的情况。如果数据量够GTSRB 5 万张算够可以解冻后面几个 layer4 的卷积块一起微调准确率能再提 0.5 到 1 个百分点。代价是显存占用翻倍训练时间从 10 分钟变成 40 分钟单卡 1080Ti。3.3 输入尺寸与网络深度的匹配关系32×32 输入配四层卷积刚好再深就会因为特征图太小而丢失空间信息。如果要用 ResNet18 这种级别的网络输入至少 64×64否则最后的 7×7 池化会变成 2×2信息压缩太狠。我一般这样搭配输入尺寸推荐网络预期准确率单 epoch 耗时32×32四层 CNN 基线97.5%约 25 秒48×48六层 CNN98.6%约 50 秒64×64ResNet18 微调99.2%约 90 秒这张表是在单卡 1080Ti、batch_size128 条件下测的不同硬件会有浮动但相对关系稳定。选型时先看你的部署端算力如果是嵌入式设备32×32 加四层 CNN 是最优解如果是服务器端推理直接上 ResNet18。4. 训练调参与评估让模型真正收敛的五个关键设置4.1 学习率、优化器与 batch_size 的联动学习率是最玄学的参数没有之一。我的习惯是先用 Adam 配 1e-3 跑一轮看 loss 曲线如果前 100 个 step 就震荡说明太大如果 10 个 epoch 还平着不动说明太小。确定量级后再换 SGD 配 momentum 做精细收敛。import torch.optim as optim # 阶段一Adam 快速下降 optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) # 阶段二SGD 精细收敛通常在 Adam 跑 20 epoch 后切换 # optimizer optim.SGD(model.parameters(), lr1e-2, momentum0.9, weight_decay5e-4) # 学习率调度每 15 个 epoch 降为原来的 0.1 scheduler optim.lr_scheduler.StepLR(optimizer, step_size15, gamma0.1)weight_decay1e-4是 L2 正则化系数防止过拟合。batch_size 设 128 是显存和梯度稳定性的折中再大梯度方向更准但泛化可能变差再小训练抖动明显。如果显存不够用梯度累积模拟大 batch。4.2 训练循环与验证集监控训练循环里最容易漏的是model.eval()和torch.no_grad()不加这两个验证结果会偏乐观因为 Dropout 和 BatchNorm 在训练模式下的行为不同。def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss, correct, total 0.0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * imgs.size(0) _, preds outputs.max(1) correct (preds labels).sum().item() total labels.size(0) return running_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() running_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) running_loss loss.item() * imgs.size(0) _, preds outputs.max(1) correct (preds labels).sum().item() total labels.size(0) return running_loss / total, correct / total验证集准确率连续 5 个 epoch 不提升就可以停再训下去只会过拟合。我一般把最好的模型权重存下来用torch.save(model.state_dict(), best.pth)后面推理直接加载这个文件。4.3 混淆矩阵与分类报告找出模型到底错在哪整体准确率 99% 不代表没问题可能某个类别全军覆没。用 sklearn 的classification_report和confusion_matrix逐类看。from sklearn.metrics import classification_report, confusion_matrix import numpy as np def get_all_preds(model, loader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in loader: imgs imgs.to(device) outputs model(imgs) _, preds outputs.max(1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) return np.array(all_preds), np.array(all_labels) preds, labels get_all_preds(model, test_loader, device) print(classification_report(labels, preds, digits4)) cm confusion_matrix(labels, preds)重点看 recall 低于 0.95 的类别。GTSRB 里最容易混的是「限速 80」和「限速 100」两者都是红圈白底黑字数字形状在低分辨率下很接近。如果这两个类别的混淆矩阵非对角线值高说明输入分辨率不够得升到 48×48。5. 避坑与排查训练交通标志分类模型时最常见的五个翻车现场5.1 现象loss 从第一个 epoch 就是 nan原因学习率太大或者输入数据没有归一化像素值在 0 到 255 之间直接进网络梯度爆炸。解决先检查ToTensor()后面有没有跟Normalize()再把学习率降到 1e-4 试一轮。如果还是 nan检查标签里有没有 -1 或超出类别数的值。5.2 现象训练准确率 99%测试准确率只有 70%原因数据泄漏。常见于把同一张图的增强版本同时分到了训练集和验证集。解决划分数据集时按原始图片 ID 分不要按增强后的样本分。另外检查验证集的 transform 里有没有混入随机增强。5.3 现象模型对某些类别完全不预测原因类别权重没设或者设反了。CrossEntropyLoss的 weight 是「样本越少权重越大」如果写成样本数本身稀有类会被彻底忽略。解决用total / (num_classes * counts[i])重新算权重打印出来确认稀有类的权重值大于 1。5.4 现象推理时单张图片预测结果和验证集不一致原因推理时的预处理和验证集不一致。比如验证集用了Resize((32,32))推理时忘了加或者 Normalize 的均值标准差写错了。解决把验证集的 transform 单独封装成一个函数推理时直接调用同一个函数不要手写。5.5 现象GPU 显存够但训练速度异常慢原因num_workers设成了 0数据加载在主进程里串行执行GPU 一直在等数据。解决DataLoader里设num_workers4或 8pin_memoryTrue。如果用的是 Windowsnum_workers大于 0 可能报错改成 0 但把batch_size调大一些补偿。6. 从 99% 到落地模型量化与单张图片推理的完整链路训练出 99% 准确率的模型只是第一步真正部署到车载设备或者边缘盒子上还得过量化这一关。我一般用 PyTorch 的动态量化把模型从 FP32 压到 INT8体积缩小 4 倍推理速度提升 2 到 3 倍准确率掉不到 0.3 个百分点。import torch.quantization # 加载训练好的模型 model TrafficSignCNN(num_classes43) model.load_state_dict(torch.load(best.pth, map_locationcpu)) model.eval() # 动态量化只量化 Linear 和 Conv 层 quantized_model torch.quantization.quantize_dynamic( model, {nn.Linear, nn.Conv2d}, dtypetorch.qint8 ) # 保存量化后的模型 torch.save(quantized_model.state_dict(), best_quantized.pth) # 对比体积 import os print(fFP32 模型大小: {os.path.getsize(best.pth) / 1024:.1f} KB) print(fINT8 模型大小: {os.path.getsize(best_quantized.pth) / 1024:.1f} KB)quantize_dynamic的第二个参数指定要量化的层类型交通标志分类网络里主要是 Conv2d 和 Linear全量化就行。量化后的模型在 CPU 上推理单张 32×32 图片大约 3 毫秒完全满足实时性要求。单张图片推理的完整流程封装成一个函数方便集成到上位机或者服务里def predict_single(image_path, model, devicecpu): model.eval() transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.272, 0.260, 0.266]) ]) img Image.open(image_path).convert(RGB) tensor transform(img).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): output model(tensor) prob torch.softmax(output, dim1) conf, pred prob.max(1) return pred.item(), conf.item() # 使用示例 label, confidence predict_single(test_stop.ppm, quantized_model) print(f预测类别: {label}, 置信度: {confidence:.4f})unsqueeze(0)这步经常被漏掉导致输入维度是 3×32×32 而不是 1×3×32×32网络会报维度错误。torch.softmax把 logits 转成概率置信度低于 0.7 的样本建议人工复核或者拒绝预测这在智慧交通场景里比强行给一个错误答案更安全。最后说一个我踩过的坑量化后的模型在 x86 上跑得好好的部署到 ARM 板子上精度掉了 5 个百分点。原因是 ARM 的量化指令集对某些卷积核的支持不完整解决办法是用 ONNX Runtime 做跨平台推理或者针对目标硬件重新做量化校准。这个教训让我养成了一个习惯——任何模型在目标硬件上跑通之前都不算真正完成。希望帮到你。本文还有配套的精品资源点击获取