ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于CNN的GTSRB交通标志识别实战:数据预处理到模型训练

基于CNN的GTSRB交通标志识别实战:数据预处理到模型训练 简介基于CNN的交通标志识别Python项目完整包含源码与项目说明主要面向计算机、人工智能等相关专业学生的课程设计、毕业设计及实战练习。方案选用公开的GTSRB德国交通标志数据集覆盖多类别标志图像围绕数据预处理、CNN模型构建、训练、评估与推理展开。资源共9个文件含5个Python脚本分别承担输入处理、CNN定义、训练、评估等职责2个CSV文件提供数据划分与标签映射辅以XML配置和Markdown说明文档整体压缩包仅310KB结构紧凑、易读易用。目前已有169人学习/下载适合具备一定Python基础、希望入门深度学习图像识别的学习者。通过该资源可快速跑通交通标志识别全流程理解数据增强、卷积层设计、训练与验证等关键环节也可作为后续改进、扩展或项目立项演示的起点。1. 用 CNN 啃下 GTSRB交通标志识别到底在解决什么问题交通标志识别是自动驾驶感知链里最“短平快”的一环:目标小、类别多、颜色对比强,但又容易被光照和运动模糊干扰。GTSRB 数据集的 43 类标志、5 万 张样本,恰好把“小数据集 高类别数”的典型矛盾摆在了桌面上——用传统 HOG SVM 勉强能到 90% 出头,再往上就非常吃力。而 CNN 通过局部感受野和权值共享,把特征提取和分类融合进同一个网络,在 GTSRB 上轻松做到 97% 以上的测试精度。这份资源里包含完整的 Python 源码、预处理脚本、训练与评估模块,适合做课程设计或毕业设计的同学直接复现。看完这篇,你能独立跑通整个流程,并知道每个参数为什么这么设。2. GTSRB 数据集的目录结构与预处理链路2.1 数据集在项目里是怎么组织的解压资源后会发现根目录下除了.py文件,还有data3文件夹和两个 CSV 文件。data3的结构是典型的 GTSRB 原始分布:每个子文件夹名对应一个类别 ID(0 到 42),里面是该类别的所有 PPM 格式图片。train_data.csv 记录的是训练集图片的相对路径和标签,test_data.csv 则是测试集的标注。注意 GTSRB 官方划分里测试集图片尺寸不统一,从 15×15 到 250×250 都有,这跟很多“已经 resize 好”的数据集不一样,所以预处理脚本的存在就非常关键。data3/ ├── 0/ # 类别 0:限速 20 │ ├── 00000_00000.ppm │ ├── 00000_00001.ppm │ └── ... ├── 1/ # 类别 1:限速 30 └── ...csv 文件里每一行是两张图片拼起来的路径格式,形如data3/0/00000_00000.ppm,0,前面的逗号分隔路径和标签。读取时用pandas.read_csv之后按逗号拆开即可。这个设计让数据加载不依赖os.walk去遍历目录,训练和测试集划分完全由 csv 控制,方便做交叉验证时只改 csv 不改代码。2.2 Preprocessing.py 里的关键操作Preprocessing.py干的事情主要有三件:统一尺寸、灰度化/归一化、数据增强。统一尺寸是必须的,因为 CNN 的全连接层要求输入张量尺寸固定。GTSRB 官方推荐把图片 resize 到 32×32,这个尺寸是精度和计算量的折中——再大比如 48×48精度提升不到 0.5%,但训练时间增加一倍以上;再小的话小标志的纹理细节就丢了。from PIL import Image import numpy as np def preprocess_image(image_path, target_size(32, 32)): # 打开图片并转为 RGB,防止 PPM 格式的通道顺序问题 img Image.open(image_path).convert(RGB) # 保持宽高比的 resize,不足部分用灰色填充 img.thumbnail(target_size, Image.Resampling.LANCZOS) canvas Image.new(RGB, target_size, (128, 128, 128)) canvas.paste(img, ((target_size[0] - img.width) // 2, (target_size[1] - img.height) // 2)) # 归一化到 [0, 1],加速收敛 arr np.asarray(canvas, dtypenp.float32) / 255.0 return arr这段代码里用了thumbnail paste的组合而不是直接resize,原因在于 GTSRB 很多图片本身不是正方形,直接拉伸会让圆形标志变椭圆,影响形状特征的提取。先缩放再居中粘贴到灰色画布上,既保留了原始宽高比,又让全连接层拿到固定尺寸的输入。LANCZOS重采样在高缩放比场景下比双线性插值保留更多边缘信息,这是处理小目标时容易被忽略的细节。2.3 数据增强的具体做法原始训练集虽然 5 万多张,分摊到 43 类也就是每类一千多张,对 CNN 来说仍然偏少。项目里采用了几种轻量增强:随机旋转 ±15 度、水平/垂直平移 2 像素、亮度抖动 ±20%。这些操作在 PIL 里用ImageEnhance和transform就能做,不需要引入 OpenCV。增强方式参数范围作用随机旋转±15°模拟车辆晃动造成的角度偏移平移±2 px模拟检测框定位误差亮度抖动0.8 ~ 1.2 倍模拟不同时段光照变化增强操作必须在训练时动态做,而不是预先存盘。原因是动态增强相当于每轮训练都看到略有不同的样本,变相扩大了数据集规模;如果提前增强并保存,数据量膨胀 10 倍以上,磁盘占用和 IO 都会成为瓶颈。项目里是在TSRInput.py的数据生成器里逐 batch 调用增强函数,这个设计是合理的。3. CNN 网络结构设计:从 Input 到分类器的每一层安排3.1 网络骨架为什么这样搭TSRCnn.py里定义的核心网络是一个三层卷积结构。第一层卷积核 5×5、输出 32 个特征图;第二层 3×3、输出 64 个特征图;第三层 3×3、输出 128 个特征图。每层卷积后面接 ReLU 激活和 2×2 最大池化,池化步长也是 2。第三层卷积之后不是直接接全连接,而是先经过一个全局平均池化,把 128 个 4×4 的特征图压成 128 维向量,再接两个全连接层,最后 Softmax 输出 43 类的概率分布。import torch.nn as nn class TSRCNN(nn.Module): def __init__(self, num_classes43): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size5, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), ) self.classifier nn.Sequential( nn.Linear(128 * 4 * 4, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)输入是 3×32×32 的张量,经过三次 2×2 池化后尺寸变为 32/2³ 4,所以view之后的特征维度是 128×4×4。这里有个容易被忽略的点:第一层卷积padding2是必要——如果 padding0,5×5 卷积会把 32×32 缩成 28×28,经过三层下采样后到全连接层的维度就不是整数,代码会报维度不匹配错误。所有 padding 值都按(kernel_size - 1) // 2设置,保证卷积不改变空间尺寸,只有池化在做下采样。3.2 全连接层的 Dropout 到底该放哪Dropout 放在最后一个全连接层之前,比率设为 0.5。这个位置是有讲究的:前面的卷积层参数量小,即使过拟合也主要过拟合在特征提取的平移不变性上,对泛化影响有限;而全连接层参数量占整个网络的 80% 以上,是过拟合的重灾区。所以 dropout 只加在全连接之前,卷积层不加——这也是 VGG 系列网络的惯用做法。如果把 dropout 加到卷积层后面,反而会破坏相邻像素间的空间关联,降低特征提取质量。3.3 为什么不用更深的 ResNet 或 VGGGTSRB 的图片尺寸只有 32×32,深度网络在这里施展不开。ResNet-50 第一层 7×7 卷积步长为 2,直接就把 32×32 下采样到 16×16,前面几层的特征提取根本没有足够空间展开;而且深度网络的感受野在 32×32 输入下已经覆盖全图,继续加层带来的非线性增益极其有限,反而容易过拟合。当前这个三层结构在 5 万张训练集上,验证集精度能到 98% 左右,已经逼近人类水平GTSRB 论文里的基线是 98.8%。也就是说,在 GTSRB 这个场景下,这个网络规模就是性价比最高的选择。4. 训练循环与超参数调优实战4.1 TSRTrain.py 里的训练主流程训练入口在TSRTrain.py,数据加载在TSRInput.py里通过自定义Dataset类实现。核心训练循环使用 PyTorch 标准流程:每个 epoch 遍历全部训练数据,前向传播计算损失,反向传播更新梯度,每 100 个 batch 打印一次当前损失和精度。训练超参数如下:batch size 64、初始学习率 0.001、Adam 优化器、交叉熵损失函数,默认训练 30 个 epoch。optimizer torch.optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) criterion nn.CrossEntropyLoss() for epoch in range(30): model.train() for batch_idx, (images, labels) in enumerate(train_loader): images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() if batch_idx % 100 0: print(fEpoch {epoch} Batch {batch_idx} Loss {loss.item():.4f}) scheduler.step()梯度清零用optimizer.zero_grad()而不是model.zero_grad(),两者效果一样但前者语义更明确:只重置优化器管理的参数梯度,避免误清其他张量的梯度。学习率调度用StepLR,每 10 个 epoch 缩小 10 倍,目的是在训练后期用更小的步长在损失曲面底部精细搜索,防止越过最优点。weight_decay1e-4是 L2 正则项,它给大权重施加惩罚,进一步抑制过拟合——在 43 类分类任务里,这个值过大1e-2会欠拟合,过小0则验证损失会先降后升。4.2 训练过程中怎么判断模型状态只看 loss 曲线是不够的,关键要看训练集和验证集 loss 的分叉趋势。每轮结束后在验证集上跑一次完整评估,记录 top-1 精度。如果训练 loss 持续下降但验证 loss 在第 15 轮左右开始回升,说明模型开始过拟合,此时应该提前终止,或者把 dropout 从 0.5 提高到 0.6。反之,如果训练 loss 从一开始就降不动,说明学习率过大或网络初始化有问题,优先把 lr 降到 0.0003 再试。现象原因调整方向训练 loss 下降慢学习率过小lr 从 0.001 调到 0.003loss 震荡剧烈学习率过大 / batch 太小lr 减半或 batch 调大到 128验证精度远低于训练过拟合增大 dropout 或加 weight_decay前几个 epoch 精度接近 1/43网络没收敛检查数据标签是否对齐4.3 数据加载的细节与坑TSRInput.py里自定义 Dataset 返回的是增强后的图片张量和标签。有个容易踩的坑:csv 里的路径是相对路径,而图片实际在data3目录下,运行时的工作目录必须是项目根目录,否则PIL.Image.open会抛FileNotFoundError。建议在main函数开头加一个路径断言:import os assert os.path.exists(data3/0), 请确认当前工作目录是项目根目录另外,transforms.ToTensor()会把 PIL Image 从 HWC 转成 CHW 并且自动归一化到 [0,1],但如果你已经像 2.3 节那样手动把值域缩放到 [0,1],再套ToTensor()就会得到 [0, 1/255] 的“额外缩放”,输入分布偏离预期。项目的做法是:预处理脚本只做图像变换,归一化交给ToTensor()统一处理,避免双重缩放。如果需要做复杂的图像增强比如颜色空间变换,建议在__getitem__里用torchvision.transforms的Compose组合操作,而不是混合手写和库函数。4.4 GPU 显存不足的应对策略32×32 的输入已经很小,三层卷积网络占用显存也不会超过 2GB,但如果你在本机额外加载了预训练权重或者改大了 batch size,还是可能遇到 OOM。最直接的解决办法是把 batch size 从 64 减到 32,同时把pin_memory参数设成True,让 DataLoader 的 GPU 传输走更快的锁页内存。如果还是不够,检查是不是把验证集图片也加载到了 GPU——验证模式不用算梯度,可以临时用with torch.no_grad():包裹并且把模型切到eval()模式,中间特征图不保留梯度,显存占用能下降 60% 以上。5. 评估结果与三种常见训练事故的排查手法5.1 模板匹配式评估:用测试集算分类报告项目里的TSREval.py负责最终评估,核心输出三样东西:整体准确率、每类别的 precision/recall/F1、以及混淆矩阵。GTSRB 的类别分布不均匀比如限速 50 的样本量是限速 120 的 3 倍,所以只看准确率会掩盖少样本类别的失败。评估时用sklearn.metrics.classification_report一次性输出所有指标,留意哪些类别的 recall 明显低于整体均值——这些通常就是增强不足或类内差异过大的类别。from sklearn.metrics import classification_report, confusion_matrix import numpy as np # y_true 和 y_pred 分别是测试集真实标签和模型预测结果 print(classification_report(y_true, y_pred, digits4)) # 找出最容易混淆的类别对 cm confusion_matrix(y_true, y_pred) for i in range(43): for j in range(43): if i ! j and cm[i][j] / max(cm[i].sum(), 1) 0.05: print(f类别 {i} 有 {cm[i][j]} 个样本被误判为类别 {j})这段代码把混淆矩阵中占比超过 5% 的误判对列出来,能直接定位问题样本。在 GTSRB 中最常见的误判是限速 30 和限速 50,因为两个标志的外圈都是红色圆形,内圈数字只有细微差异;另一类高发误判是“禁止驶入”和“禁止机动车”,原因同理。发现这种问题后,应对策略是单独给这两类多做一些随机裁剪和旋转增强,而不是盲目增加所有类别的数据量。5.2 训练 loss 变成 NaN 的完整排查顺序loss 变成 NaN 是所有 CNN 训练里最让人头疼的问题,排查顺序应该固化下来。第一步看学习率:如果 lr 超过 0.01,梯度更新幅度过大可能直接越过数值表示范围,先把 lr 降到 0.0001 验证是否复原。第二步看输入数据:ToTensor之后的值必须在 [0,1] 区间,如果发现图片里有 NaN 像素常见于损坏的 PPM 文件,在__getitem__里做一次torch.isnan(img).any()检查。第三步看标签:CrossEntropyLoss要求标签是 LongTensor 并且取值在 [0, 42],如果有标签等于 43 或者负值,loss 计算会自动失败。第四步看模型输出:如果最后一层输出出现了 inf,说明前面卷积的权重爆炸,可以在每个 epoch 开始前打印model.features[0].weight.abs().max()监测权重幅值。5.3 导出模型并做单张图片推理验证训练完成后把模型导出为 TorchScript 格式,这样不依赖原始的类定义也能在其他环境里加载。导出方式和直接torch.save的区别在于:TorchScript 会把网络结构固化成静态图,部署时不需要重新定义类。下面是导出和单张推理的完整流程:model.eval() example_input torch.randn(1, 3, 32, 32) traced_model torch.jit.trace(model, example_input) traced_model.save(tsr_model.pt) # 单张图片推理 from PIL import Image import torchvision.transforms as T def predict(image_path, model): img Image.open(image_path).convert(RGB) img T.Resize((32, 32))(img) img T.ToTensor()(img).unsqueeze(0) with torch.no_grad(): output model(img) pred output.argmax(dim1).item() return predtorch.jit.trace走的是实际运算路径,所以模型里不能有依赖数据内容的分支比如if x.size(0) 10这种动态逻辑,否则 trace 出来的图在推理时可能出现偏差。当前这个网络的 forward 里只有线性变换,没有动态分支,所以直接 trace 是安全的。单张推理时顺手打印一下预测概率的前三高值,如果最高值不到 0.5 而第二第三名非常接近,这个样本大概率是需要人工复核的边界情况。调低torch.resize的antialiasTrue可以略微改善小尺寸缩放的锯齿问题,对推理精度的帮助通常在 0.2% 以内,但值得加到预处理里。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表