简介:这份资源是面向高校学生与深度学习入门者的交通标志识别项目源码,基于Python与CNN卷积神经网络实现,可直接用于毕业设计、课程设计或期末大作业。项目已通过导师指导并取得高分,下载后无需修改即可运行,适合需要完整可交付方案、又希望理解CNN图像分类流程的读者。压缩包共19个文件,约18.06MB,包含4个Python脚本分别负责模型训练、图像裁剪与GUI交互,2个h5权重文件保存训练好的分类模型,另有11张png示例图片、1个txt依赖清单及gitignore配置,结构清晰、模块分工明确。目前已有479人学习下载。读者可从中获得一套完整的交通标志识别实现方案,涵盖数据预处理、模型构建、权重加载与图形界面演示,便于快速复现结果、撰写论文或在此基础上替换数据集开展二次开发。
1. 从一份交通标志识别源码说起:CNN 到底在识别什么
路上那块红圈白底的限速牌,人眼扫过去不到半秒就能认出来,但要让程序稳定认出来,难点从来不在“看见”,而在“看懂”。这份 python 实现的基于 CNN 深度学习网络的交通标志识别源码,本质上就是解决“看懂”这件事:把一张摄像头拍到的、可能歪斜、过曝、被树影遮掉一角的交通标志图片,映射到它所属的类别编号上。它适合三类人:正在做毕业设计、需要一份能跑通、能改、能写进论文的完整工程的同学;刚学完 CNN 卷积神经网络理论、想找一个真实数据集练手的深度学习入门者;以及需要快速搭一个交通标志识别 baseline、再往上叠自己想法的人。源码这个词听起来像终点,其实它只是起点——真正决定你能不能毕业、能不能复现的,是数据怎么进、网络怎么搭、训练怎么调这三件事。
2. 交通标志识别源码拆开看:数据、网络、训练三段式
拿到一份交通标志识别源码,第一反应不该是直接python train.py,而是先搞清楚它的骨架。绝大多数这类工程都是三段式:数据加载与预处理、CNN 网络定义、训练与评估循环。这三段里,数据决定了上限,网络决定了下限,训练决定你能不能摸到那个上限。下面按这个顺序拆。
2.1 数据集长什么样:GTSRB 的目录结构与类别分布
交通标志识别最常用的公开数据集是 GTSRB(German Traffic Sign Recognition Benchmark),德国交通标志识别基准。它有两个版本:训练集是分好类的文件夹,测试集是一堆图片加一个 CSV 标注文件。典型目录结构是这样:
GTSRB/ ├── Train/ │ ├── 00000/ │ │ ├── 00000_00000.ppm │ │ └── ... │ ├── 00001/ │ └── ... 共 43 个类别文件夹 ├── Test/ │ ├── 00000.ppm │ └── ... └── Test.csv43 个类别,训练集约 3.9 万张,测试集约 1.2 万张。这里第一个坑就来了:类别极度不均衡。限速 30 的样本可能有两千张,而某些稀有标志只有一两百张。如果你直接按文件夹顺序喂给模型,模型会偏向多数类。常见做法是在 DataLoader 里用WeightedRandomSampler按类别频率倒数加权采样,或者对少数类做数据增强。
图片尺寸也不统一,从 15×15 到 250×250 都有。CNN 需要固定输入尺寸,所以预处理里必须 resize。我一般统一到 32×32 或 48×48,前者训练快、适合毕业设计演示,后者精度更高、适合写论文刷指标。
2.2 用 PyTorch 搭一个能跑的 CNN:三层卷积的最小实现
网络部分不用一上来就 ResNet,交通标志这种 43 分类、图片又小的任务,三层卷积加两层全连接就够跑出 95% 以上的准确率。下面是一个可以直接抄的最小实现:
import torch import torch.nn as nn class TrafficSignCNN(nn.Module): def __init__(self, num_classes=43): super().__init__() # 卷积块1:输入3通道,输出32通道,3x3卷积 self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(32) # 卷积块2:32->64 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(64) # 卷积块3:64->128 self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.bn3 = nn.BatchNorm2d(128) self.pool = nn.MaxPool2d(2, 2) # 每次下采样一半 self.relu = nn.ReLU(inplace=True) self.dropout = nn.Dropout(0.5) # 防过拟合 # 输入32x32,经过3次pool变成4x4,通道128 self.fc1 = nn.Linear(128 * 4 * 4, 256) self.fc2 = nn.Linear(256, num_classes) def forward(self, x): x = self.pool(self.relu(self.bn1(self.conv1(x)))) # 32->16 x = self.pool(self.relu(self.bn2(self.conv2(x)))) # 16->8 x = self.pool(self.relu(self.bn3(self.conv3(x)))) # 8->4 x = x.view(x.size(0), -1) # 展平 x = self.dropout(self.relu(self.fc1(x))) return self.fc2(x)逻辑说明:每个卷积块是Conv2d → BatchNorm → ReLU → MaxPool的标准组合。BatchNorm 放在卷积后、激活前,能让训练更稳、收敛更快,这是血泪经验——不加 BN 的时候学习率稍微大一点就梯度爆炸。三次 pool 把 32×32 降到 4×4,展平后是 128×4×4=2048 维,接全连接。
参数说明:kernel_size=3, padding=1保证卷积后尺寸不变,尺寸缩小只靠 pool,这样计算量可控。Dropout(0.5)只在全连接前用,卷积层不加,因为卷积参数共享本身就有正则效果。num_classes=43对应 GTSRB 类别数,换成自己的数据集要改这里。
2.3 训练循环与关键超参:学习率、batch size、epoch 怎么定
训练循环是源码里最容易被忽略、又最影响结果的部分。下面这段是核心:
import torch.optim as optim from torch.utils.data import DataLoader device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = TrafficSignCNN(num_classes=43).to(device) # 交叉熵损失,分类任务标配 criterion = nn.CrossEntropyLoss() # Adam 学习率 1e-3,毕业设计够用 optimizer = optim.Adam(model.parameters(), lr=1e-3) # 每 10 个 epoch 学习率降为原来的 0.1 scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上评估 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f"Epoch {epoch}, Val Acc: {correct/total:.4f}")逻辑说明:optimizer.zero_grad()必须在反向传播前调用,否则梯度会累加,这是新手最常见的翻车点。scheduler.step()放在 epoch 末尾,按步降学习率。评估时切model.eval()并包torch.no_grad(),省显存也避免 BN 用错统计量。
参数说明:学习率 1e-3 是 Adam 的常用起点,如果 loss 震荡就降到 1e-4。batch size 一般 64 或 128,显存不够就往下调,但别低于 16,太小 BN 统计不稳。epoch 30 对这个小数据集足够,看验证集准确率不再涨就可以停。这里有个玄学:同样的代码,换个随机种子结果能差一两个点,所以论文里最好固定种子并跑三次取平均。
3. 把源码跑起来:环境、数据、训练命令的完整链路
上一章讲的是骨架,这一章讲怎么让它真正在你机器上动起来。很多人卡在环境上,不是代码问题,是 python 安装、依赖版本这些琐事。按下面顺序走,能少走弯路。
3.1 python 环境与依赖安装:避开版本冲突
先确认 python 版本,建议 3.8 到 3.10,太新的版本有些库还没跟上。用 conda 建独立环境最省心:
conda create -n tsr python=3.9 conda activate tsr # 安装 PyTorch,去官网选对应 CUDA 版本的命令 pip install torch torchvision # 其余依赖 pip install numpy pandas matplotlib pillow tqdm逻辑说明:独立环境避免和你系统里其他项目的包打架。PyTorch 的安装命令一定要去官网按你的 CUDA 版本生成,直接pip install torch可能装到 CPU 版,训练慢十倍。
参数说明:python=3.9是兼容性最好的版本之一。如果你没有 GPU,CPU 版也能跑,只是 30 个 epoch 可能要一两个小时。验证 GPU 是否可用:
import torch print(torch.cuda.is_available()) # 输出 True 才说明 GPU 可用 print(torch.cuda.get_device_name(0))3.2 数据加载与增强:transforms 里每个参数的作用
数据增强是提升泛化最便宜的手段。交通标志在真实场景里会有旋转、模糊、光照变化,训练时就要模拟这些:
from torchvision import transforms, datasets train_transform = transforms.Compose([ transforms.Resize((32, 32)), # 统一尺寸 transforms.RandomRotation(15), # 随机旋转±15度 transforms.ColorJitter(brightness=0.3, # 亮度扰动 contrast=0.3), # 对比度扰动 transforms.RandomAffine(degrees=0, # 随机平移 translate=(0.1, 0.1)), transforms.ToTensor(), # 转成张量,归一化到0-1 transforms.Normalize(mean=[0.5, 0.5, 0.5], # 标准化 std=[0.5, 0.5, 0.5]) ]) val_transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) train_set = datasets.ImageFolder("GTSRB/Train", transform=train_transform) val_set = datasets.ImageFolder("GTSRB/Test", transform=val_transform) train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=4) val_loader = DataLoader(val_set, batch_size=64, shuffle=False, num_workers=4)逻辑说明:ImageFolder要求数据按类别分文件夹,这也是为什么 GTSRB 训练集是那种结构。训练集用增强,验证集和测试集绝对不能用增强,否则评估结果不可信,这是很多人写论文时被导师挑出来的问题。
参数说明:RandomRotation(15)的 15 度是经验值,交通标志一般不会倒过来,旋转太大反而引入噪声。ColorJitter的 0.3 是扰动幅度,太大图片会失真。Normalize的 mean 和 std 用 0.5 是简化写法,严格应该用数据集统计值,但 0.5 在实践中够用。num_workers=4在 Windows 上有时会报错,改成 0 即可。
3.3 训练、验证、保存模型:一条命令跑通全流程
把前面几段拼起来,写成一个train.py,直接python train.py就能跑。训练完保存模型权重:
# 训练结束后保存 torch.save(model.state_dict(), "traffic_sign_cnn.pth") print("模型已保存") # 加载时 model = TrafficSignCNN(num_classes=43) model.load_state_dict(torch.load("traffic_sign_cnn.pth")) model.eval()逻辑说明:只保存state_dict()而不是整个模型,这样加载时需要有相同的类定义,但文件更小、更灵活。加载后一定要model.eval(),否则 Dropout 和 BN 还是训练模式,预测结果会飘。
参数说明:保存路径自己定,.pth是 PyTorch 惯例后缀。如果要在别的机器上加载,确保类定义一致,否则会报 key 不匹配。
4. 交通标志识别训练避坑:那些让准确率上不去的坑
这一章是我自己踩过、也看别人踩过的坑。每一条都按现象、原因、解决来写,照着排查能省很多时间。
4.1 准确率卡在 60% 上不去:先查标签对齐
现象:训练 loss 在降,但验证准确率一直在 60% 左右晃,怎么调学习率都没用。
原因:最常见的是标签和图片没对齐。用ImageFolder时,类别是按文件夹名排序映射成 0 到 42 的,如果你自己另外维护了一份标签映射,顺序对不上,模型学的就是错的对应关系。
解决:打印train_set.class_to_idx确认映射,然后检查你的评估代码是不是用了同一套映射。另一个可能是测试集 CSV 里的文件名和实际图片对不上,读 CSV 时用os.path.exists校验一遍。
4.2 loss 变成 nan:学习率太大或数据没归一化
现象:训练几个 batch 后 loss 直接变 nan,模型输出全是 nan。
原因:学习率太大导致梯度爆炸,或者输入图片没做归一化,像素值 0 到 255 直接进网络,数值太大。
解决:先把学习率降到 1e-4 试试;确认ToTensor()和Normalize()都在 transform 里;如果还不行,加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)。
4.3 验证集准确率远高于测试集:数据泄漏
现象:验证集 98%,测试集只有 80%。
原因:验证集和训练集有重叠,或者验证集用了训练时的增强,又或者你在验证集上调了太多超参,过拟合了验证集。
解决:确保训练集和验证集完全分开,验证集不做随机增强,超参调整看的是验证集趋势而不是死磕某一个点。GTSRB 官方给了独立的测试集,最终指标应该报测试集。
4.4 GPU 显存不够:batch size 和 num_workers 的取舍
现象:报CUDA out of memory。
原因:batch size 太大,或者 num_workers 开太多导致每个 worker 都占显存。
解决:先把 batch size 减半,从 64 降到 32 甚至 16;num_workers 在 Windows 上设 0,Linux 上设 2 到 4 就够。还可以用torch.cuda.empty_cache()清理缓存,但治本还是减 batch。
4.5 训练慢得离谱:确认真的在用 GPU
现象:一个 epoch 要十几分钟。
原因:模型或数据没搬到 GPU 上,或者装的是 CPU 版 PyTorch。
解决:检查next(model.parameters()).is_cuda是否为 True,检查torch.cuda.is_available()。如果 False,重装 GPU 版 PyTorch。另外imgs.to(device)和labels.to(device)别漏,漏了会在 CPU 和 GPU 之间来回拷,更慢。
5. 从能跑到好用:提升交通标志识别精度的几个实操技巧
跑通只是及格线,毕业设计要拿得出手,或者你想把这个方案用到实际项目里,还得往上推一把。这一章讲几个我实际用过、性价比高的技巧。
5.1 用迁移学习把准确率推到 99%
自己从零训三层 CNN,GTSRB 上大概能到 96% 到 97%。想再往上,最省力的是迁移学习:拿一个在 ImageNet 上预训练好的 ResNet18 或 MobileNetV2,把最后一层换成 43 类,微调。代码改动很小:
import torchvision.models as models import torch.nn as nn # 加载预训练 ResNet18 model = models.resnet18(pretrained=True) # 替换最后的全连接层 model.fc = nn.Linear(model.fc.in_features, 43) model = model.to(device) # 微调时学习率调小,1e-4 甚至 1e-5 optimizer = optim.Adam(model.parameters(), lr=1e-4)逻辑说明:预训练模型已经学到了边缘、纹理这些底层特征,交通标志识别正好用得上。只换最后分类层,前面的卷积层要么冻结、要么用很小的学习率微调。
参数说明:pretrained=True会下载权重,第一次跑需要联网。学习率一定要比从零训练小,因为预训练权重已经很好了,大步长会破坏它。输入尺寸要改成 224×224 以匹配 ResNet 的预期,或者改模型第一层卷积的 stride。
5.2 混淆矩阵告诉你模型到底错在哪
准确率是个笼统指标,想知道模型把哪些标志认混了,画混淆矩阵:
from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) preds = model(imgs).argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) plt.figure(figsize=(16, 14)) sns.heatmap(cm, annot=False, cmap="Blues") plt.savefig("confusion_matrix.png")逻辑说明:混淆矩阵对角线是正确分类,非对角线是错分。交通标志里最容易混的是各种限速牌(30、50、80 长得像)和不同方向的指示牌。
参数说明:43 类矩阵比较大,annot=False不然数字挤成一团。看的时候重点看哪些类错分多,针对性补数据或加增强。
5.3 模型部署前必须做的两件事
第一件是量化。训练完的模型是 float32,部署到嵌入式设备上太大太慢,用动态量化能压到四分之一:
model.eval() quantized_model = torch.quantization.quantize_dynamic( model, {nn.Linear}, dtype=torch.qint8 ) torch.save(quantized_model.state_dict(), "traffic_sign_quantized.pth")第二件是用真实场景图片测一遍。GTSRB 是裁剪好的标志图,实际摄像头拍到的是整张路况图,标志只占一小块。你得先用检测模型把标志框出来,再送进识别模型。这一步不做,模型在实验室 99%,上路就废。
我自己的习惯是:每改一次网络结构或数据增强,都固定随机种子跑三次,取平均和标准差,只报最好那一次的结果是自欺欺人。另外,源码里的默认参数不一定适合你的数据,学习率、batch size、增强幅度这三个一定要自己动手试。希望帮到你。
本文还有配套的精品资源,点击获取