Python+CNN交通标志识别实战:GTSRB数据集从80%到99%准确率
2026/9/24 23:12:54 网站建设 项目流程

简介:这份资源是一套基于Python与卷积神经网络实现的交通标志识别项目,采用GTSRB数据集,面向计算机、人工智能、通信工程、自动化等专业的在校学生与教师,也适合作为毕业设计、课程设计或项目立项的参考案例。压缩包共9个文件,约311KB,包含5个Python源码文件、2个CSV数据文件、1个XML配置文件和1个Markdown说明文档,分别承担模型构建、训练评估、数据预处理与项目说明等职责。项目代码经过完整测试,运行稳定,答辩评审平均分达到96分,已有224人学习下载。读者可获得从数据预处理、CNN模型搭建到训练与评估的完整流程实现,理解交通标志分类任务的关键环节,并在此基础上修改扩展功能,用于自身课题或作业。下载后建议先阅读README.md了解项目结构,资源仅供学习参考,请勿用于商业用途。

1. 交通标志识别为什么值得用 Python+CNN 啃一遍 GTSRB

路上那块红圈白底的限速牌,人类司机扫一眼就知道踩不踩刹车,但要让车自己认出来,背后是一整套图像分类流水线。基于 Python+CNN 实现的交通标志识别,数据集是 GTSRB,这句话拆开看就是三件事:用 Python 把德国交通标志基准数据集(GTSRB)读进来,用卷积神经网络(CNN)做特征提取和分类,最后跑出一个能对 43 类标志说人话的模型。GTSRB 全称 German Traffic Sign Recognition Benchmark,四万多张实拍图,光照、遮挡、倾斜、模糊全都有,比 MNIST 那种干净手写数字难得多,也真实得多。

为什么不用全连接前馈网络硬怼?图像处理为啥用 CNN 不用前馈神经网络,核心就一句:全连接层把图像拉平后丢掉了空间结构,相邻像素的关系被拍扁,参数还爆炸。CNN 的卷积核在局部感受野上滑动,权值共享,既保留空间信息又把参数量压下来。这个方向适合谁?适合刚学完 Python 基础语法、装过 numpy 和 cv2、想找一个「数据现成、任务明确、能跑出准确率」的深度学习入门项目的人。它不像目标检测那样要标框,也不像分割那样要逐像素,分类任务闭环短,调参反馈快,是练手 CNN 的合适靶子。

2. 把 GTSRB 读进内存:目录结构、尺寸统一与标签映射

2.1 GTSRB 的目录长什么样,为什么不能直接 ImageFolder

GTSRB 官方给的训练集是 43 个文件夹,文件夹名就是类别 ID,从 00000 到 00042,每个文件夹里是一堆 ppm 或 png 图。听起来像 torchvision 的 ImageFolder 能直接吃,但有两个坑:一是图片尺寸不统一,从十几像素到几百像素都有;二是官方还单独给了一个 Test.csv,测试集的标签不在文件夹名里,得靠 csv 里的 ClassId 列去对。所以常见做法是自己写 Dataset,而不是无脑 ImageFolder。

我一般会先把目录结构确认一遍,用一段脚本统计每个类别的样本数和尺寸分布,心里有数再动手。

import os from PIL import Image from collections import defaultdict root = "GTSRB/Training" # 训练集根目录 count = defaultdict(int) sizes = defaultdict(int) for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if not fname.lower().endswith((".png", ".ppm", ".jpg")): continue count[cls] += 1 with Image.open(os.path.join(cls_dir, fname)) as im: sizes[im.size] += 1 print("类别数:", len(count)) print("总样本:", sum(count.values())) print("尺寸种类数:", len(sizes)) print("最常见尺寸:", sorted(sizes.items(), key=lambda x: -x[1])[:5])

这段代码的逻辑很直白:遍历每个类别文件夹,累计样本数,同时用 PIL 打开每张图记录尺寸。参数上root指向训练集根目录,如果你的数据解压后叫GTSRB/Final_Training/Images,就改成那个路径。跑完你会看到尺寸种类可能上百种,这就是为什么必须统一 resize。常见做法是全部缩到 32×32 或 48×48,32×32 够快,48×48 对小标志的细节保留更好,我一般先用 32×32 跑通再往上加。

2.2 标签映射与训练/验证划分的写法

GTSRB 的类别 ID 是字符串 "00000" 这种,训练时要转成 0 到 42 的整数。另外测试集的标签在 csv 里,得单独处理。下面这段把训练集按 8:2 划分,并建立类别名到索引的映射。

import os import pandas as pd from sklearn.model_selection import train_test_split root = "GTSRB/Training" samples = [] class_to_idx = {} for idx, cls in enumerate(sorted(os.listdir(root))): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue class_to_idx[cls] = idx for fname in os.listdir(cls_dir): if fname.lower().endswith((".png", ".ppm", ".jpg")): samples.append((os.path.join(cls_dir, fname), idx)) train_s, val_s = train_test_split( samples, test_size=0.2, random_state=42, stratify=[s[1] for s in samples] ) print("训练:", len(train_s), "验证:", len(val_s)) print("类别映射示例:", list(class_to_idx.items())[:3])

逻辑说明:先按文件夹名排序生成稳定的类别索引,保证每次运行映射一致;stratify按标签分层抽样,避免某些类别在验证集里一张都没有。参数test_size=0.2是验证集比例,random_state固定后结果可复现。测试集那边读Test.csv,用ClassId列做标签,Path列拼出图片路径,逻辑类似,只是标签来源从文件夹名换成 csv 列。

注意:GTSRB 的 ppm 格式 PIL 能直接读,但如果你用 cv2.imread,默认读出来是 BGR,送进模型前记得转 RGB,否则颜色通道反了,红色限速牌可能被当成蓝色指示牌,准确率会莫名其妙掉一截。

3. 用 PyTorch 搭一个能打的 CNN:卷积块、池化与分类头

3.1 网络结构怎么定:三层卷积够不够

交通标志识别的 CNN 不需要 ResNet 那么深,GTSRB 图片小、类别少,三层卷积加两层全连接就能到 95% 以上。结构上我一般这样排:Conv(3→32, 3×3) → ReLU → Conv(32→64, 3×3) → ReLU → MaxPool(2×2) → Conv(64→128, 3×3) → ReLU → MaxPool(2×2) → Flatten → FC(128×8×8→256) → ReLU → Dropout(0.5) → FC(256→43)。输入 32×32,两次池化后特征图是 8×8,通道 128,展平后 8192 维接全连接。

import torch import torch.nn as nn class TrafficSignCNN(nn.Module): def __init__(self, num_classes=43): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 32 -> 16 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 16 -> 8 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 8 * 8, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): x = self.features(x) return self.classifier(x) model = TrafficSignCNN() print(sum(p.numel() for p in model.parameters()), "个参数")

逻辑说明:padding=1保证 3×3 卷积后尺寸不变,池化才降采样;inplace=True省一点显存;Dropout 放在全连接之间防过拟合。参数上num_classes=43对应 GTSRB 类别数,如果你只做限速和禁令两类,改成 2 即可。这个网络参数量大概一百多万,CPU 也能跑,GPU 上几分钟一轮。

3.2 数据增强与 DataLoader 的配置

GTSRB 里有些类别样本少,比如某些罕见标志只有两三百张,直接训练会偏。常见做法是加随机旋转、平移、亮度抖动。注意交通标志的旋转不能太夸张,±15 度以内比较安全,转 90 度限速牌就倒过来了,语义变了。

from torchvision import transforms from torch.utils.data import Dataset, DataLoader from PIL import Image train_tf = transforms.Compose([ transforms.Resize((32, 32)), transforms.RandomRotation(15), transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)), transforms.ColorJitter(brightness=0.3, contrast=0.3), transforms.ToTensor(), transforms.Normalize(mean=[0.34, 0.31, 0.32], std=[0.27, 0.26, 0.27]), ]) val_tf = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean=[0.34, 0.31, 0.32], std=[0.27, 0.26, 0.27]), ]) class GTSRBDataset(Dataset): def __init__(self, samples, transform): self.samples = samples self.transform = transform def __len__(self): return len(self.samples) def __getitem__(self, i): path, label = self.samples[i] img = Image.open(path).convert("RGB") return self.transform(img), label train_loader = DataLoader(GTSRBDataset(train_s, train_tf), batch_size=64, shuffle=True, num_workers=2) val_loader = DataLoader(GTSRBDataset(val_s, val_tf), batch_size=64, shuffle=False, num_workers=2)

逻辑说明:训练集用增强,验证集只做 resize 和归一化,保证评估稳定。Normalize的均值和标准差是 GTSRB 训练集的统计值,用 0.34/0.27 这组是常见经验值,你也可以自己算一遍。batch_size=64在 8G 显存上比较稳,显存小就降到 32。num_workers在 Windows 上如果报错就设 0,这是血泪经验,多进程在 Windows 下容易卡死。

4. 训练、评估与调参:让准确率从 80% 爬到 97%

4.1 训练循环与学习率调度

训练循环本身不复杂,关键是损失函数、优化器和学习率调度。分类任务用交叉熵,优化器用 Adam 起步,学习率 1e-3,跑十几个 epoch 后如果验证准确率不涨了,用 StepLR 或 CosineAnnealing 降学习率。

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = TrafficSignCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=20) for epoch in range(20): model.train() running_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() running_loss += loss.item() * imgs.size(0) scheduler.step() model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) pred = model(imgs).argmax(1) correct += (pred == labels).sum().item() total += labels.size(0) print(f"epoch {epoch+1} loss {running_loss/total:.4f} val_acc {correct/total:.4f}")

逻辑说明:weight_decay=1e-4是 L2 正则,配合 Dropout 一起压过拟合;CosineAnnealingLR让学习率按余弦曲线从 1e-3 降到接近 0,比固定学习率收敛更稳。参数T_max=20要和总 epoch 数一致,否则调度节奏对不上。每轮打印验证准确率,如果连续几轮不涨,就该考虑加数据增强或换更大输入尺寸了。

4.2 混淆矩阵看哪些类别在互相认错

准确率是个总数,看不出问题在哪。GTSRB 里有些标志长得像,比如限速 30 和限速 80 在低分辨率下容易混,圆形禁令标志之间也容易串。跑一个混淆矩阵,找出错得最多的类别对,再针对性处理。

from sklearn.metrics import confusion_matrix import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) pred = model(imgs).argmax(1).cpu().numpy() all_preds.extend(pred) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) # 找出非对角线最大的几个混淆对 off = cm.copy() np.fill_diagonal(off, 0) top = np.dstack(np.unravel_index(np.argsort(off.ravel())[::-1][:5], off.shape))[0] for i, j in top: print(f"真实 {i} 被预测成 {j}: {off[i, j]} 次")

逻辑说明:confusion_matrix的行是真实标签,列是预测标签,对角线是正确数。把对角线清零后排序,就能看到最严重的混淆对。参数上val_loader必须shuffle=False,否则预测和标签对不上。如果发现某两类互相错得特别多,常见做法是单独给这两类加样本,或者把输入尺寸从 32 提到 48,让细节更清楚。

注意:别一上来就堆很深网络或加注意力模块。GTSRB 这个任务,三层卷积加好的增强就能到 97% 以上,加太复杂反而容易过拟合,训练还慢。先跑通基线,再谈优化。

5. 避坑与排查:那些让准确率卡在 80% 的常见问题

5.1 现象:训练准确率很高,验证准确率死活上不去

原因:典型过拟合。GTSRB 训练集四万多张,但分布不均,某些类别样本多,模型记住了这些类别的纹理而不是通用特征。解决:先加数据增强,尤其是随机旋转和亮度抖动;再把 Dropout 从 0.5 提到 0.6;如果还不行,检查是不是验证集划分有问题,比如同一段连续视频的帧被分到了训练和验证两边,导致验证集泄漏。用stratify分层抽样能缓解,但如果是按视频分帧的数据,得按视频 ID 划分而不是按图片。

5.2 现象:loss 变成 nan,训练直接崩

原因:学习率太大,或者输入没归一化。GTSRB 原图是 0 到 255 的整数,直接送进网络,梯度爆炸是迟早的事。解决:确认ToTensor()之后有没有接NormalizeToTensor会把像素缩到 0 到 1,但还不够,得再减均值除标准差。学习率从 1e-3 降到 1e-4 试试,Adam 对学习率比 SGD 敏感,1e-3 有时就偏大。

5.3 现象:某些类别准确率极低,其他类别接近满分

原因:类别不平衡。GTSRB 里最多的类别有两千多张,最少的只有两百多张,差十倍。解决:用加权交叉熵,给样本少的类别更大权重;或者用 WeightedRandomSampler 让每个 batch 里各类别比例均衡。加权交叉熵的写法是给CrossEntropyLossweight参数,权重取类别频率的倒数归一化。

5.4 现象:GPU 利用率低,训练慢得离谱

原因:数据加载成了瓶颈。num_workers设太小,或者图片是 ppm 格式,PIL 解码慢。解决:把num_workers提到 4 或 8,pin_memory=True加速 CPU 到 GPU 的拷贝。如果还是慢,可以先把所有图片预处理成 32×32 的 npy 数组存下来,训练时直接读数组,省掉每次解码的开销。这个预处理换来的加速非常明显,尤其在小图上。

5.5 现象:测试集提交上去分数比验证集低一截

原因:测试集的标签在 csv 里,路径拼接时可能漏了子目录,或者测试集图片的尺寸分布和训练集不同。解决:先检查测试集读进来的图片数量和 csv 行数是否一致,再抽查几张图确认标签对得上。另外测试集不能用训练集的增强,只能 resize 和归一化,这点容易写错。

6. 把模型推到 99%:TTA、集成与部署前的最后几手

到 97% 左右,单模型单次推理基本到顶了,再想往上走,得用测试时增强(TTA)和模型集成。TTA 的思路是:推理时对同一张图做多种变换,比如原图、水平翻转、轻微旋转,分别预测后把概率平均。交通标志水平翻转要小心,左转和右转标志翻转后语义就反了,所以只对不涉及方向的类别做翻转,或者干脆只用旋转和缩放。

def predict_tta(model, img_tensor, n_aug=5): model.eval() probs = torch.zeros(1, 43).to(img_tensor.device) with torch.no_grad(): probs += torch.softmax(model(img_tensor), dim=1) for _ in range(n_aug - 1): aug = transforms.RandomAffine(degrees=10, translate=(0.05, 0.05))(img_tensor) probs += torch.softmax(model(aug), dim=1) return probs.argmax(1)

逻辑说明:对同一张图做多次随机仿射变换,每次取 softmax 概率累加,最后取 argmax。参数n_aug是增强次数,5 次通常够,再多收益递减还费时间。注意这里用的是RandomAffine而不是翻转,避开方向敏感的标志。

集成则是训练三到五个结构相同但初始化不同的模型,推理时投票或平均概率。GTSRB 上三模型集成通常能再涨 0.5 到 1 个百分点。代价是推理时间翻三倍,如果部署在边缘设备上要权衡。

部署前还有一件事:把 PyTorch 模型转成 ONNX 或 TorchScript,脱离 Python 环境跑。转 ONNX 用torch.onnx.export,注意输入尺寸固定成 1×3×32×32,动态轴设 batch 维。转完之后用 onnxruntime 加载,推理速度通常比原生 PyTorch 快,尤其在 CPU 上。

我自己踩过最深的坑是归一化参数。有次换了个数据集,均值标准差没改,模型在验证集上看着还行,实际路测时阴天和逆光下疯狂翻车。后来养成习惯,每换一批数据先重新统计均值和标准差,再训练。这个习惯比任何调参技巧都值钱。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询