
简介这是一份面向人工智能与智慧交通场景的CNN交通标志分类实践资源适合正在学习深度学习和图像识别的开发者、竞赛参与者或高校学生用于掌握从数据准备、模型训练到单张图片预测的完整流程。该项目围绕交通标志图像分类任务展开资源包共2000个文件包含1994张交通标志PNG图像、3个XML文件及3个Python脚本整体约201.95MBzip目录结构清晰便于按需加载与检索。图片数据按训练集与测试集组织覆盖多种交通标志类别可支持直接开展模型训练与效果验证Python脚本分别对应训练和单张图片测试命令配合数据路径参数即可快速复现整个项目流程。由于数据划分清晰、代码结构简洁读者可以在现有基础上尝试调整网络结构或数据增强策略进一步深入智慧交通标志识别任务也可作为课程设计或毕业设计的基线方案。目前已有78人学习下载适合作为交通标志识别方向的入门实践参考。1. 交通标志分类CNN入门项目里最接近真实工程的“人工智能大作业”交通标志分类是CNN卷积神经网络入门里少有的“能把人工智能大作业做成实战项目”的方向。它不像MNIST那样一跑就99%让人飘也不像自动驾驶检测那样需要一整套多任务流程。这个项目要处理的是一组真实采集的交通标志图像类别固定、数据公开、评价标准清晰只要把CNN的卷积、池化、全连接、训练循环、评估逻辑一路走通就能得到一个能对“限速”“停止”“人行横道”等标志做出判断的分类模型。它适合四种人需要交人工智能作业的在校生刚开始学深度学习想找项目练手的开发者想在智慧交通场景里做算法验证的工程师以及想把“会调参”变成“能落地”的入门者。这篇文章要解决的是怎么从零把这个项目跑通以及怎么把准确率从“能看”调到“能信”。2. 选框架和数据集PyTorch为什么比Keras更适合这个项目2.1 框架选型的三个实际理由先说结论这个项目用PyTorch不用Keras。不是Keras不好而是对这个题目的“调试体验”和“后续拓展”来说PyTorch更顺手。第一个理由是数据集加载PyTorch的Dataset和DataLoader这套接口处理GTSRB这种“每张图大小不一样、文件结构按类别分文件夹”的数据集非常自然几十行就能写一个自己的加载器。第二个理由是训练循环全部自己控制学习率调整、梯度裁剪、早停甚至是“我要中途看一眼中间层的特征图”都不会被封装挡住。第三个理由是部署链路更顺训完的模型既能导出ONNX也能用PyTorch自带的量化接口压到几十兆以内这在智慧交通的边缘设备场景里是硬需求。如果你本来就只会TensorFlow/Keras也不是不能做但后面迁移到部署、量化、或者加一个目标检测头就开始绕路了。Keras适合原型快PyTorch适合项目深。而“项目实践”型的大作业本质上属于后者。次要理由PyTorch在GTSRB这类单卡就能训的数据集上不太挑硬件。一张普通的GTX 1650或者带MPS的Mac也能跑下来。如果是CPU硬跑稍微忍一下训练时间也可以但建议至少有一个能跑PyTorch的GPU环境。2.2 GTSRB数据集的三个隐藏特征尺寸不一、类别失衡、光照干扰用德国交通标志数据集GTSRB做实战是行业里最常见的做法。它包含43类交通标志训练集约3.9万张测试集约1.26万张类别的含义从限速标志到禁令标志、警告标志都有。这个数据集最大的特点不是“大”而是“不怎么规整”。第一原始图像分辨率从十几像素到两百多像素都有。如果直接用全连接层去读特征维度会乱套所以第一步必须统一输入尺寸常见做法是缩放到48×48或32×32。48×48能保留更多的文字细节比如“30”和“80”这种数字在32×32下很容易糊掉。第二类别严重不平衡。部分限速标志样本近两千张而少数类别可能只有一两百张。总体准确率会被样本多的类别拉高但你单独看少数类召回率可能惨不忍睹。因此评估时不能只看总体准确率要看混淆矩阵或者按类别的F1。第三真实场景的光照、遮挡、运动模糊都在图里。这其实对训练是好事——它逼着你做数据增强也逼着你认识到“训练集干净不代表测试集干净”。深度学习在这里不是“玄学”而是数据处理和模型容量匹配的结果。2.3 最小项目目录与运行环境清单这个项目的目录结构我一般这么摆traffic_sign_classifier/ ├── data/ │ ├── GTSRB/ │ │ ├── Train/ # 按类别分文件夹 │ │ └── Test/ # 按类别分文件夹 ├── models/ │ ├── __init__.py │ └── cnn_net.py ├── scripts/ │ ├── train.py │ ├── evaluate.py │ └── predict_video.py ├── checkpoints/ └── requirements.txtdata目录放原始数据models目录放网络结构scripts目录放训练、评估、推理脚本checkpoints目录放训练好的权重。别把所有代码都堆在一个jupyter notebook里——项目实践类的作业既要能演示也要能“拿走再用”。requirements.txt里最底配就这些torch2.0 torchvision0.15 opencv-python4.5 scikit-learn1.0 matplotlib numpy我一般用Python 3.9或3.10。太高版本有些预编译包还没跟上容易在装环境时先“翻车”一次。这里比MNIST那个项目多了一个OpenCV和sklearn前者用于视频帧测试后者用于评估指标计算。参数说明GTSRB数据集的读取方式有两种一种是自己写个Dataset读文件夹一种是直接用torchvision的ImageFolder。ImageFolder要求每个类别一个子文件夹GTSRB的Train目录刚好就是这种结构所以可以直接用。Test目录的格式在不同下载源里不太一样有的版本是平铺的CSV结构。稳妥做法是先只用Train目录做训练和验证单独大版本测试集之后再加。3. 自己写一个能跑的CNN从模型结构到训练闭环3.1 模型结构用7层小网络讲清楚卷积核在干什么很多人一上来就用ResNet、VGG这种大模型结果这个项目里不仅训练慢还容易过拟合。GTSRB的图像内容相对简单一个小型CNN就能在验证集上做到95%以上。你反而能从这个小模型里真正看懂CNN卷积神经网络每一层在干什么这对做人工智能大作业的人来说更重要。我给一个经典的7层结构三层卷积块加两层全连接。每个卷积块都是“卷积–批归一化–ReLU–最大池化”卷积核用3×3通道数从32递增到128。# models/cnn_net.py import torch.nn as nn class TrafficSignCNN(nn.Module): def __init__(self, num_classes43): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 6 * 6, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): return self.classifier(self.features(x))逻辑说明输入是三通道图像假设尺寸已经统一到48×48。经过三个池化层后特征图尺寸从48降到6所以全连接层的输入维度是128×6×6对应代码里的注释位置。如果换用32×32输入这里要改成128×4×4。BatchNorm在这里比较关键能明显加速收敛尤其是GTSRB这种光照差异大的数据。参数说明Dropout放在第一个全连接层后面比例0.5防止小模型在全连接处过拟合。num_classes固定为43和GTSRB的类别数一致。想试其他数据集改这一处就行。3.2 训练脚本损失函数、优化器与学习率调度训练逻辑不要写成一坨“黑匣子”要保证每部分都可以独立修改。常用的组合是交叉熵损失 Adam优化器 步进衰减。# scripts/train.py import torch from torch import nn, optim from torch.utils.data import DataLoader from torchvision import datasets, transforms from models.cnn_net import TrafficSignCNN transform transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.242, 0.236, 0.238]), ]) full_dataset datasets.ImageFolder(rootdata/GTSRB/Train, transformtransform) train_size int(0.85 * len(full_dataset)) val_size len(full_dataset) - train_size train_dataset, val_dataset torch.utils.data.random_split( full_dataset, [train_size, val_size] ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size128, shuffleFalse, num_workers4, pin_memoryTrue) model TrafficSignCNN(num_classes43) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(15): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) scheduler.step() val_acc 0.0 model.eval() with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) preds model(images).argmax(dim1) val_acc (preds labels).sum().item() print(fepoch {epoch1:02d} | loss {running_loss/len(train_dataset):.4f} | fval_acc {val_acc/len(val_dataset):.4f})逻辑说明把85%的训练数据划给训练集15%做验证集。这里用的是random_split对训练流程演示没问题标签对应关系仍从ImageFolder的class_to_idx来实际使用时按索引对齐就行。每个epoch结束后手动调用scheduler.step()让学习率按StepLR的规则在第5个和第10个epoch后缩小10倍。参数说明batch_size 64在显存不足时可以降到32但验证集batch_size可以设大一点因为只做前向推理更省资源。num_workers在Windows上如果报多线程错误就改成0。ImageFolder默认按类名字母排序所以模型的输出索引和类别名之间的映射要用dataset.classes取出来保存后面推理时要用。这段代码跑完15个epoch比较理想的情况是验证集准确率在93%-95%之间。如果只有90%上下先别急着改模型去看看数据预处理是不是resize太小或者有没有忘记Normalize。3.3 评估指标准确率不够还要看清楚错在哪训练结束之后光看一行val_acc不够。你需要一张混淆矩阵至少要知道模型把“限速30”错认成“限速50”的次数是不是很多——这类错误在交通场景里后果很严重。# scripts/evaluate.py import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader, random_split from sklearn.metrics import accuracy_score, classification_report, confusion_matrix import numpy as np transform transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.242, 0.236, 0.238]), ]) dataset datasets.ImageFolder(rootdata/GTSRB/Train, transformtransform) split int(0.85 * len(dataset)) train_set, val_set random_split(dataset, [split, len(dataset) - split]) loader DataLoader(val_set, batch_size128, shuffleFalse) model TrafficSignCNN(num_classes43) model.load_state_dict(torch.load(checkpoints/best.pth, map_locationcpu)) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in loader: preds model(images).argmax(dim1) all_preds.extend(preds.numpy()) all_labels.extend(labels.numpy()) print(accuracy_score(all_labels, all_preds)) print(classification_report(all_labels, all_preds)) cm confusion_matrix(all_labels, all_preds) np.save(confusion_matrix.npy, cm)逻辑说明这段代码先按照训练时的85/15比例切出验证子集再对验证子集做预测生成准确率、每个类别的精确率和召回率以及混淆矩阵保存成npy文件。评估时用的预处理必须和训练时的验证阶段一致只做Resize、ToTensor、Normalize不套数据增强。参数说明混淆矩阵的行是真实类别列是预测类别。对角线越亮越健康。如果某一行的数值分散到了好几列说明这些类别在视觉上高度相似比如限速标志里只有数字不同这时可以考虑在数据增强里加上随机裁剪让模型更关注数字周围的局部纹理而不只是标志的形状。4. 把准确率从93%调到97%数据增强与迁移学习的实战对比4.1 数据增强针对交通标志的三种有效变换要提升准确率不是无脑加层数。GTSRB这类数据集的难点在于场景变化所以数据增强必须贴着“真实世界”来设计。我常用的三种增强组合是随机仿射变换模拟拍摄角度的偏移、颜色抖动模拟不同光照和色温、随机遮挡模拟被树枝或杂物挡住一部分。transform_train transforms.Compose([ transforms.Resize((48, 48)), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), transforms.RandomApply([ transforms.RandomAffine(degrees0, translate(0.1, 0.1), scale(0.9, 1.1)) ], p0.5), transforms.ToTensor(), transforms.RandomErasing(p0.3, scale(0.02, 0.15)), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.242, 0.236, 0.238]), ])逻辑说明RandomRotation(10)只转10度以内因为交通标志在图片里不会大幅旋转ColorJitter改变亮度和对比度RandomAffine加轻微平移和缩放RandomErasing会随机遮住一小块区域强制模型不依赖单一局部特征。训练时用这套增强验证时保持原来的Resize、ToTensor、Normalize即可。参数说明p0.5和p0.3表示概率。数值调太高会导致训练样本失真太严重反而不收敛。RandomErasing的scale是遮挡区域相对于整张图的比例别超过0.15否则标志主体都可能被抹掉。加上这套增强之后同样的模型通常能涨2到3个百分点的验证准确率。4.2 迁移学习ResNet18微调的参数设置如果想进一步涨点不用自己设计复杂网络直接拿ResNet18做微调是更省力的做法。对这种图像内容不算特别复杂但样本规模中等的任务预训练模型收敛更快、泛化也更好。关键参数有三个冻结哪些层、学习率设多大、最后一层怎么换。# scripts/train_resnet.py import torch from torch import nn, optim from torchvision import models, datasets, transforms transform_train transforms.Compose([ transforms.Resize((64, 64)), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.242, 0.236, 0.238]), ]) train_dataset datasets.ImageFolder(rootdata/GTSRB/Train, transformtransform_train) train_loader torch.utils.data.DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.fc nn.Linear(model.fc.in_features, 43) model.to(device) # 冻结前三个残差块只训练最后一个残差块和全连接层 for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(10): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(images), labels) loss.backward() optimizer.step() print(fepoch {epoch1:02d} loss {loss.item():.4f})逻辑说明加载带预训练权重的ResNet18把最后一层的全连接换成输出43类的结构。冻结layer4之前的所有层只更新最后一个残差块和分类头这样显存占用小训练也快。经过10个epoch验证集准确率通常能冲到96%以上。这里只贴了训练循环验证部分与3.2保持一致。参数说明这个方案输入尺寸用64×64因为ResNet自带的池化会压缩特征图输入太小会丢失细节。学习率1e-3是针对新加的全连接层比较安全的取值如果你打算解冻全部层做全量微调学习率要降到1e-4左右否则前期预训练权重会被冲乱。4.3 两个方案怎么选一张表看清成本和收益| 方案 | 训练成本 | 验证集准确率范围 | 适用场景 | | 自定义7层CNN | 单卡10-20分钟 | 93%-96% | 教学演示、快速出结果、理解CNN内部原理 | | ResNet18微调 | 单卡20-40分钟 | 96%-98% | 项目需要更高准确率、参赛、交付给真实场景用 |这不是精确值和你的数据划分、增强强度、随机种子都有关系但大致趋势不会变。我自己的经验是拿一个项目练手先用7层CNN把整个流程跑通把坑都踩一遍等流程稳定了再切ResNet18涨点。直接上大模型出了问题反而不好排查因为你不知道是数据的问题还是模型太大收敛不动的问题。5. 避坑交通标志分类里最常见的五个翻车现场5.1 图像尺寸不一致导致训练时直接报错现象处理GTSRB原始数据时DataLoader在第一个batch就报shape mismatch或者loss变成nan。原因原始图像尺寸从15×15到250×250不等如果加载时没有统一resize同一个batch里的张量形状对不齐跨batch的维度也会乱。解决把transform里的Resize((48, 48))固定下来确保训练、验证、推理三个阶段用一模一样的尺寸。改尺寸时记得同步检查全连接层的输入维度否则会报Linear的in_features不匹配。5.2 验证集划分不当准确率虚高现象用random_split从全部训练数据里随机抽出15%做验证但发现验证准确率比单独测试时高很多。原因随机划分虽然不会让同一张图同时出现在训练和验证里但如果原始数据里包含同一场景的连拍图或者同一批光照条件下拍摄的图像随机划分很容易把场景相似的照片分到两边导致验证集和训练集分布过于接近评估结果虚高。解决先按文件名前缀划分时间戳或场景再切训练验证集。更稳妥的做法是直接用GTSRB官方的Train/Test划分训练时从Train目录里切验证集最终评估放到单独下载的Test目录上。5.3 学习率设置过高训练震荡不收敛现象loss在0.5-1.5之间来回跳验证集准确率一直上不去。原因初始学习率1e-2甚至更高对这个小网络来说太大了。模型参数在最优解附近反复横跳也可能是加了数据增强之后样本分布变了学习率没有跟着调低。解决先把数据增强关掉用1e-3从头训练一次做基线如果还是震荡对lr做一次“从1e-2开始每个epoch乘0.9”的快速扫描。找到一个稳定的初始学习率之后再打开数据增强。5.4 迁移学习时解冻全部层导致预训练权重被破坏现象ResNet18微调的前几个epoch准确率反而比随机初始化还低后面也涨得很慢。原因解冻全部层之后用1e-3的学习率更新底层卷积预训练提取的低级特征被大幅扰动分类头也连带着学不稳。解决先把骨干层冻结只训练fc层跑稳定之后再用1e-4的学习率解冻部分层做全量微调。没跑过基线之前别动全部参数。5.5 显存不够但batch size又不想降现象batch_size64在GPU上直接OOMout of memory但降到32又嫌训练慢。原因输入尺寸设成128×128三个池化层之后特征图还是很大中间激活值占满了显存。模型不大不代表激活值不大。解决先降batch size跑通再优化输入尺寸。对这个项目48×48是性价比很高的选择盲目上大分辨率收益有限显存倒是先撑不住。如果一定要用大分辨率就用梯度累积模拟大batch或者把全连接层的Dropout去掉再试。6. 验证与进阶拿视频帧测一遍模型才算真的做完6.1 用OpenCV读视频帧做分类的最小验证训练完的模型别只停留在“跑测试集”这一步。拿一段路上的行车视频转成视频帧喂给模型才能看出模型在新场景里到底行不行。import cv2 import torch import torchvision.transforms as transforms from PIL import Image from models.cnn_net import TrafficSignCNN model TrafficSignCNN(num_classes43) model.load_state_dict(torch.load(checkpoints/best.pth, map_locationcpu)) model.eval() transform transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.242, 0.236, 0.238]), ]) cap cv2.VideoCapture(test_video.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break img Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) tensor transform(img).unsqueeze(0) with torch.no_grad(): pred model(tensor).argmax(dim1).item() cv2.putText(frame, fclass {pred}, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow(test, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明把视频帧转成RGB的PIL图像再做和训练一致的预处理然后将单张图扩展成batch维度得到预测类别。显示时只在左上角写了类别编号正式展示时你可以把它映射成“限速50”这样的中文名称。注意视频帧的采集场景和训练集很可能不同如果效果明显变差先去检查增强策略是不是太单一。参数说明cv2.VideoCapture读的是BGR必须先转RGB再进模型否则色彩通道错位会让预测结果完全跑偏。waitKey(1)是视频播放的控制频率数值越小播放越快但不会影响模型的推理速度。6.2 从演示到落地模型量化和硬件选型的思路这一步是针对“智慧交通”场景的加分项。如果模型要跑到嵌入式设备或Jetson这类边缘硬件上48×48输入生成的自定义CNN大概只有几MB直接部署问题不大。ResNet18则要先量化或转ONNX。PyTorch里最简单的量化路径是torch.quantization.quantize_dynamic只对全连接层做动态量化代码量很小推理速度能提升一到两倍精度损失通常在一个点以内。更激进的方案是训练后静态量化需要对校准数据集跑一遍并统计激活值范围。我的习惯是先导出ONNX再在硬件上对比浮点模型和量化模型的实际延迟而不是只看理论加速比。精度、速度、显存占用三者只能取两个具体取舍看你要跑的是实时视频流还是单帧抓拍。最后说一个我的习惯。开始做这个项目时我把所有能调的参数都改了一遍结果验证集准确率从95%掉到88%而且完全不知道是哪一步搞坏的。后来把所有实验记录成一张表每次只改一个变量再也没发生过这种问题。做CNN项目控制变量比调参玄学可靠得多。这套流程你照着走一遍应该就能收获一个拿得出手的交通标志分类模型希望帮到你。本文还有配套的精品资源点击获取