尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于CNN的GTSRB交通标志识别:从数据预处理到模型训练全解析

发布时间:2026/9/24 19:48:24

资讯中心
01
ARTICLE

基于CNN的GTSRB交通标志识别:从数据预处理到模型训练全解析

基于CNN的GTSRB交通标志识别:从数据预处理到模型训练全解析
简介这是一份面向智慧交通场景的交通标志识别项目资源基于卷积神经网络CNN对GTSRB德国交通标志数据集进行分类适合深度学习初学者或计算机视觉方向学习者动手实践。压缩包共8个文件以Python脚本为主辅以CSV数据文件和XML配置Python脚本覆盖数据预处理、CNN网络搭建、模型训练与评估等关键环节CSV文件提供标注好的训练/测试数据整体大小仅310KB轻量但流程完整。已有623人学习。通过该项目可系统掌握图像分类任务从数据加载、模型构建到训练调参的完整链路并学会在真实基准数据集上优化准确率为后续智慧交通项目落地打下基础。GTSRB数据集覆盖43类交通标志约5万张图像能有效检验模型的泛化能力代码结构简洁适合作为课程设计或毕业设计的参考基线。1. 交通标志识别为什么拿CNN做这份GTSRB工程能直接给你什么把交通标志识别跑通是智慧交通方向里最典型的深度学习落地练习。原因很简单数据公开、任务封闭、效果立即可见。这个项目正是围绕CNN卷积神经网络和GTSRB数据集搭起来的一套完整Python工程目录里TSR-master下同时放了数据预处理、数据加载、模型定义、训练和评估五个脚本你要做的不是从零写算法而是把这条链路完整跑一遍并理解每个脚本在干什么。GTSRB是德国交通标志识别基准数据集包含43类、约5万张真实道路场景图片里面光照不均、遮挡、模糊、类别样本不均衡全都有跟你在网上随手下的玩具数据集完全是两个量级。这份资源特别适合人工智能课程需要交大作业、或者毕业设计想快速出一个视觉项目的学生也适合想从CNN理论跳到实际工程的从业者。下文我按照项目里脚本的实际顺序把每个环节的参数、逻辑和踩过的坑拆开讲。2. 数据准备与预处理GTSRB目录、CSV格式和Preprocessing.py的四个关键操作2.1 先认清GTSRB的目录结构和CSV字段打开data3项目里存放数据的根目录你会看到内部按 Train 和 Test 组织。Train 下面有43个子文件夹文件夹名从0到42每个编号对应一类交通标志比如0是限速201是限速30直到42类结束。Test 目录则是所有测试图片混在一起没有按子文件夹分类。这也是GTSRB最早的坑测试集图片全靠CSV文件里的标注来区分类别如果你直接把Test图片塞进模型训练那就等于提前偷看了答案。项目里的 train_data.csv 和 test_data.csv 就是帮你干这个事的。我建议你先用文本编辑器打开看一眼不要直接上代码。里面每行是一条样本核心字段就三个字段示例说明FilenameTrain/1/00005_00000_00025.png相对于data3根目录的图片路径ClassId1类别编号0到42之间的整数SignNameSpeed limit (30km/h)类别文字描述方便人看这个CSV相当于项目的后悔药。原始GTSRB自带CSV字段很乱包含ROI坐标、是否遮挡、是否模糊等一堆附加列实际训练根本用不上。项目里已经抽成了最直白的三列你只需要关心Filename和ClassId。我自己处理这类数据时的习惯是拿到CSV先打印行列数、再随机抽5行看看路径是否真实存在因为路径错了后面所有代码都会白跑。2.2 预处理管线灰度、直方图均衡、统一尺寸、归一化GTSRB的图片不是整齐划一的原始分辨率从15x15到250x250都有而且很多是在行车记录仪视角下拍的逆光、暗光很常见。如果直接拿原始图丢给CNN模型一半的容量都要浪费在适应光照变化上。Preprocessing.py 就是解决这个问题的我把核心逻辑拆出来import cv2 import numpy as np IMG_SIZE 32 # 统一缩放到 32x32 def load_and_preprocess(row, data_rootdata3): # row 是 CSV 里的一行data_root 是数据根目录 path f{data_root}/{row[Filename].replace(chr(92), /)} img cv2.imread(path) # 转灰度交通标志的颜色信息受光照影响太大先降维 img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 直方图均衡化把对比度拉开逆光图也能看清边缘 img cv2.equalizeHist(img) # 统一尺寸缩小用 INTER_AREA避免出现摩尔纹 img cv2.resize(img, (IMG_SIZE, IMG_SIZE), interpolationcv2.INTER_AREA) # 归一化到 [0,1]让梯度更新更平稳 img img.astype(np.float32) / 255.0 # 增加 channel 维度变为 (1, H, W)对应 PyTorch 的 NCHW 输入 return img.reshape(1, IMG_SIZE, IMG_SIZE)这段代码里有三个容易被忽略的地方。第一路径分隔符用row[Filename].replace(chr(92), /)处理是因为原始CSV在Windows下生成路径是反斜杠Linux下直接读会报文件不存在这个细节我后面还会专门讲。第二转灰度不是拍脑袋的决定GTSRB里很多标志的内外圈颜色在弱光下会糊成一片但形状和图案仍然可辨灰度图能把模型注意力逼到结构特征上。第三INTER_AREA是缩小图片时的正确选择如果用默认的线性插值小目标上的文字和数字会被插值糊掉。2.3 数据加载器TSRInput.py 如何把CSV和图片配对预处理只是单张图片的函数真正喂给模型的是TSRInput.py里的数据加载器。这个脚本做的事情说白了就是三件读CSV、调预处理、按批次打包。常见做法是用PyTorch的Dataset和DataLoader封装import torch from torch.utils.data import Dataset, DataLoader class TSRDataset(Dataset): def __init__(self, csv_path, data_rootdata3, labelsTrue): self.df pd.read_csv(csv_path) self.data_root data_root self.labels labels def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img load_and_preprocess(row, self.data_root) if self.labels: label int(row[ClassId]) return torch.tensor(img), torch.tensor(label) return torch.tensor(img) # 使用时这样创建迭代器 train_loader DataLoader(TSRDataset(train_data.csv), batch_size32, shuffleTrue)这里batch_size是训练时的关键参数。GTSRB训练集约39000张batch设为32时一个epoch大概1220步跑起来不会等太久如果你显存够大调到64会更稳。shuffleTrue只对训练集打开测试集和验证集必须保持顺序否则评估结果会乱。另外注意__getitem__里每次重新读图再预处理速度偏慢但胜在省内存GTSRB全量图如果一次性读入内存会吃掉好几个GB学生机不一定扛得住。3. CNN模型设计TSRCnn.py的网络结构、参数量与输入输出形状3.1 为什么全连接网络在这里必翻车把一张32x32的灰度图拉直是1024个像素值。如果直接用全连接层第一层到第二层的权重矩阵就是1024x1024约100万个参数这还只是第一层。到了原图分辨率250x250单层参数量直接爆炸。更本质的问题是全连接层没有局部性——它把每个像素孤立对待而交通标志的识别依赖的是边缘、角点、圆环组合这些局部结构。CNN卷积层通过滑动窗口提取局部特征参数在不同位置共享一个3x3卷积核只有9个权重却能在整张图上滑动。这也是cnn卷积神经网络能成为图像分类默认选择的核心原因。3.2 TSRCnn.py里的网络结构解析项目里的TSRCnn.py定义的就是一个经典的小型CNN结构是卷积块池化卷积块池化全连接。我见过不少项目直接堆VGG16但对GTSRB这种43类、单通道、32x32输入的任务来说深度网络是杀鸡用牛刀训练慢且容易过拟合。这份工程里的结构反而更合理import torch.nn as nn class TSRNet(nn.Module): def __init__(self, num_classes43): super().__init__() self.features nn.Sequential( # 第一卷积块1通道 - 32通道 nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(), nn.Conv2d(32, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 32x32 - 16x16 # 第二卷积块32通道 - 64通道 nn.Conv2d(64, 64, kernel_size3, padding1) if False else nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(), nn.Conv2d(64, 64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 16x16 - 8x8 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))其中第二卷积块第一层我修正为nn.Conv2d(32, 64, ...)保证通道数衔接正确原始工程里如果有笔误训练时也会报尺寸不匹配错误。整个网络用参数表看更清楚层级输出形状参数量作用Conv2d(1, 32, 3)32x32x32320提取低级边缘特征Conv2d(32, 32, 3)32x32x329248叠加感受野提取局部纹理MaxPool2d(2)16x16x320下采样扩大感受野Conv2d(32, 64, 3)16x16x6418496提取形状组合特征Conv2d(64, 64, 3)16x16x6436928加深抽象层次MaxPool2d(2)8x8x640下采样Linear(4096, 256)2561048832全连接分类Linear(256, 43)4311051输出43类得分总参数量约112万绝大多数集中在第一个全连接层。这也是为什么不能在中间层把特征图做得太大否则全连接层参数量会失控。Dropout放在最后一个全连接前概率0.5这是防止过拟合的关键。3.3 输出层与损失函数的配套关系TSRCnn.py最后输出的是43个实数每个数字代表模型认为该图属于某个类别的得分。这里有一个新手常踩的坑不要在模型最后一层手动加Softmax。PyTorch的nn.CrossEntropyLoss内部已经包含了Softmax运算你只要把原始得分logits直接传给损失函数就行。如果你在模型里先Softmax再丢给CrossEntropyLoss等于做了两次软化模型训练会变慢且收敛不稳。项目里TSRTrain.py如果没改这个逻辑就不要在TSRCnn.py里自作聪明。4. 训练与调参TSRTrain.py里的学习率、批次和防过拟合配置4.1 训练主循环与超参选择TSRTrain.py是整个工程里最长的脚本本质就做三件事加载数据、定义优化器、循环训练。核心代码可以缩成这样import torch import torch.nn as nn from torch.utils.data import DataLoader, random_split model TSRNet(num_classes43) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 数据集切分训练集里再划 10% 当验证集 full_ds TSRDataset(train_data.csv) train_ds, val_ds random_split(full_ds, [int(len(full_ds)*0.9), len(full_ds) - int(len(full_ds)*0.9)]) train_loader DataLoader(train_ds, batch_size32, shuffleTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience2, factor0.5) for epoch in range(30): model.train() running_loss 0.0 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() running_loss loss.item() # 每个epoch结束在验证集上评估一次 model.eval() val_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) val_loss criterion(outputs, labels).item() _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) print(fEpoch {epoch1}: train_loss{running_loss/len(train_loader):.4f}, val_acc{correct/total:.4f}) scheduler.step(val_loss)这里lr0.001是Adam优化器最常用的起步值一般不需要再调大。random_split按9:1划分训练集和验证集这里有个玄学问题GTSRB的Train目录是按类别分文件夹的如果你的数据集类内样本顺序集中直接随机切可能让某个类在验证集里消失或变少。稳妥做法是在切分前按ClassId做分层抽样保证每个类别在验证集里都有代表。ReduceLROnPlateau在验证损失两个epoch不降时自动把学习率减半比固定学习率跑到底要稳得多。4.2 数据增强必须克制交通标志不能乱翻转很多图像分类项目习惯性地加随机水平翻转这在交通标志识别上是个危险的默认操作。原因很直接交通标志有方向性语义。禁止左转的标志翻转后变成了禁止右转靠右行驶翻转变成了靠左行驶。如果训练集里水平翻转后的图片还是原来的标签模型等于在学习错误映射最终准确率会被拉低好几个点。我见过有人把验证集准确率从91%调到88%最后查出来就是多了一行transforms.RandomHorizontalFlip()。GTSRB适合的增强是亮度扰动、小角度旋转、小范围平移和缩放。这些操作不会改变标志的方向语义还能增强模型对拍摄角度和光线变化的鲁棒性# 安全的增强方式用 torchvision 的 transforms 组合 from torchvision import transforms train_transform transforms.Compose([ transforms.RandomRotation(degrees10), # 小角度旋转最多10度 transforms.ColorJitter(brightness0.2, contrast0.2), # 亮度对比度扰动 transforms.RandomAffine(degrees0, translate(0.1, 0.1)), # 最多10%平移 ])旋转角度10度以内是安全的超过15度后限速标志里的数字会变形到难以辨认。ColorJitter只调亮度和对比度不要加饱和度扰动因为标志颜色本身就是类别信息的一部分。我在自己项目里还试过加高斯噪声但效果一般GTSRB的图片本身已经有行车记录仪的真实噪声人工噪声反而干扰训练。4.3 过拟合的预警信号与早停训练GTSRB这个规模的网络最常出现的现象是训练准确率一路飙到99%以上验证准确率在92%左右卡住然后开始波动甚至下降。这就是过拟合的典型信号。判断标准很简单训练loss持续下降验证loss开始上升两者之间的gap越来越大。处理手段优先级如下先加Dropout当前0.5已经够用、再做数据增强、然后减小网络容量。更强的做法是直接上早停Early Stopping。在TSRTrain.py里记录验证集准确率最高的那次epoch把权重单独存成best_model.pth而不是等30个epoch跑完再保存最后一次的权重。因为最后一个epoch往往已经不是最优的了。我实践里常用的做法是best_acc 0.0 for epoch in range(30): # ... 训练和验证代码 ... if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fSave best model at epoch {epoch1}, val_acc{val_acc:.4f})这样不管后续多少个epoch过拟合你手里始终握有验证集上最好的那一版权重。很多新手训练完直接torch.save(model.state_dict(), last_model.pth)结果过拟合阶段的烂权重被保存下来这是完全错误的使用习惯。5. 评估与避坑TSREval.py的指标解读和五个常见坑5.1 TSREval.py 到底该看什么指标TSREval.py在测试集上做最终评估逻辑与训练循环里的验证部分一致但有三处不同不计算梯度、不更新权重、单独加载best_model.pth。评估代码骨架如下import torch from torch.utils.data import DataLoader model TSRNet(num_classes43) model.load_state_dict(torch.load(best_model.pth)) model.eval() test_ds TSRDataset(test_data.csv) test_loader DataLoader(test_ds, batch_size64, shuffleFalse) correct 0 total 0 class_correct [0] * 43 class_total [0] * 43 with torch.no_grad(): for imgs, labels in test_loader: outputs model(imgs) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) # 按类别统计方便看哪些类拖后腿 for i in range(labels.size(0)): cls labels[i].item() class_total[cls] 1 if preds[i].item() cls: class_correct[cls] 1 print(fOverall Accuracy: {correct / total:.4f}) for cls in range(43): if class_total[cls] 0: rec class_correct[cls] / class_total[cls] if rec 0.7: # 打印准确率低于70%的类别 print(fClass {cls}: recall{rec:.2f}, samples{class_total[cls]})整体准确率之外我强烈建议看一眼逐类召回率。GTSRB本身类别不均衡限速类的样本量远大于某些稀少标志整体准确率可能挺好看但某个冷门类别可能只有60%召回率。这类问题在整体准确率上看不出来只有分拆到每个类才能发现。另外要注意shuffleFalse和model.eval()两个细节缺一不可前者保证评估结果可复现后者关掉Dropout让前向传播结果确定。5.2 避坑记录五条血泪经验坑一训练集准确率99%测试集准确率却只有85%现象训练过程中loss降到0.05以下训练准确率接近100%但TSREval.py跑出来的整体准确率明显偏低。原因模型严重过拟合把训练集里的光照条件、拍摄角度甚至背景都背下来了。GTSRB的训练图和测试图来自不同时间、不同地点的拍摄分布本身有差异。解决确认保存的是best_model.pth而非last_model.pth检查是否开了数据增强把Dropout从0.3提到0.5如果还不行减少中间层特征通道数。核心原则是模型容量要让位于泛化能力。坑二Linux下运行报文件不存在明明CSV里路径是对的现象cv2.imread返回None或者直接抛出FileNotFoundError但用眼睛看CSV里的路径确实存在。原因train_data.csv是在Windows环境下生成的路径分隔符是反斜杠\Linux下不识别。解决在我给出的load_and_preprocess函数里已经做了replace(chr(92), /)处理。如果你用的不是这个函数在读取路径前统一执行一次字符串替换或者用pathlib.Path处理跨平台路径。坑三训练时报错expected input to have 3 channels现象模型输入定义的Conv2d(1, 32, 3)但实际数据进来报通道数不匹配。原因预处理里转灰度后忘记了reshape(1, H, W)这步读进来的图还是(H, W)二维数组没有通道维度。PyTorch的Conv2d强制要求输入是(N, C, H, W)四维。解决在归一化之后加上img img.reshape(1, IMG_SIZE, IMG_SIZE)并在__getitem__里用torch.tensor(img)保证维度完整。这个报错出现的频率极高几乎每个从零写GTSRB的人都会遇到一次。坑四随机翻转把方向性标志学反了现象加了随机水平翻转后训练准确率不升反降尤其禁止左转/禁止右转这类成对标志互相混淆。原因水平翻转改变了标志的方向语义但标签没有跟着变。模型在训练时同时看到左转和翻转后的右转图都标为左转内部特征被搞混乱。解决去掉RandomHorizontalFlip全部依赖只保留旋转、平移、亮度扰动。旋转角度控制在正负10度内超过15度同样会破坏数字类标志的可读性。坑五验证集划分不合理导致评估结果忽高忽低现象每次跑训练验证集准确率波动超过5个百分点而且同一个best_model.pth在不同次评估里表现不一致。原因训练集的随机划分没有按类别分层某些冷门类别可能在验证集里只有一两个样本偶然预测错一个就把准确率拉低很多。解决用StratifiedShuffleSplit或train_test_split(stratifylabels)做分层切分保证每个类别在训练集和验证集中的分布比例与原数据一致。这个改动的效果不一定每次都明显但能保证评估过程的稳定性。6. 进阶用法把模型用到单张新图片上的推理套路训练好模型、评估完准确率之后这个项目还不算真正落地。因为实际使用场景是单张图片送进来立刻输出类别而不是批量跑测试集。我建议你单独写一个推理脚本把预处理和模型加载串起来import cv2 import torch def predict_single(image_path, model, class_namesNone): # 复用训练时的预处理函数保证一致 img load_and_preprocess({Filename: image_path}, data_root) img_tensor torch.tensor(img).unsqueeze(0) # 变成 (1, 1, 32, 32) model.eval() with torch.no_grad(): outputs model(img_tensor) _, pred torch.max(outputs, 1) prob torch.softmax(outputs, dim1).max().item() if class_names: return class_names[pred.item()], prob return pred.item(), prob # 使用示例 model TSRNet(num_classes43) model.load_state_dict(torch.load(best_model.pth)) class_id, confidence predict_single(test_sample.png, model) print(fPredicted class: {class_id}, confidence: {confidence:.2f})这里最关键的是load_and_preprocess必须与训练时的预处理完全一致。我见过有人评估脚本里用了灰度推理脚本里却忘了转灰度导致同样的图片在训练和推理阶段走的是两条不同的数据管线模型效果凭空掉一截。还有一个容易被忽略的点如果你在训练时做了RandomRotation这类数据增强推理时绝对不能带上这些随机变换。model.eval()只是关闭了Dropout和BatchNorm的batch统计但数据增强是在数据集类里做的模型本身管不到。从那以后我每个项目都会把预处理函数单独抽到一个文件里训练和推理共同引用绝不复制粘贴两份代码也不在推理时临时改参数。这条习惯帮我挡掉了至少三四次模型怎么突然不准了的排查。希望这篇拆解能帮你把GTSRB这条链路跑通也少踩几个我已经替你踩过的坑。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。