ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于CNN的交通标志识别实战:GTSRB数据集与PyTorch全流程

基于CNN的交通标志识别实战:GTSRB数据集与PyTorch全流程 简介这份资源面向计算机、人工智能、电子信息等专业的学生及企业员工提供基于卷积神经网络识别交通标志的完整Python项目源码与说明文档可用于课程设计、毕业设计、大作业或初期项目立项演示。压缩包共9个文件约310KB包含5个py脚本、2个csv数据文件、1个xml配置和1个md说明文档其中py文件分别承担数据预处理、模型构建、训练与评估等核心环节csv文件对应训练集与测试集数据索引md文档则给出项目说明与运行指引。项目采用GTSRB交通标志数据集覆盖从数据读取、图像预处理到CNN建模、训练与性能评估的完整流程目录结构清晰便于读者理解深度学习项目的工程组织方式。目前已有171人学习下载适合希望快速上手CNN图像分类实战、借鉴完整项目代码与排错思路的学习者参考。1. 从一张模糊的路牌说起CNN 识别交通标志到底在解决什么问题开车经过陌生路口限速牌被树枝挡了一半人眼都要愣一下更别说让机器认。基于 CNN 识别交通标志这件事本质就是给计算机装一双能看懂路牌的眼睛输入一张 32×32 的彩色小图输出它属于哪一类标志——限速 50、禁止超车、注意行人一共 43 类。GTSRBGerman Traffic Sign Recognition Benchmark就是这件事最经典的练手数据集五万多张真实道路抓拍图光照、遮挡、倾斜、模糊全都有比实验室里摆拍的数据难得多。这套方案适合谁刚学完 CNN 卷积神经网络、想找一个能跑通全流程的 Python 项目练手的人也适合需要快速搭一个交通标志识别原型、验证业务可行性的工程师。它不追求 SOTA 精度追求的是从数据读取、预处理、建模、训练到推理部署这条链路你能亲手走一遍知道每一步的参数为什么这么设、哪里容易翻车。下面我按自己实际做过的顺序把这条路拆开讲清楚。2. GTSRB 数据集怎么读、怎么切、怎么增强2.1 先搞清楚 GTSRB 的目录结构和类别分布GTSRB 下载下来通常是一个GTSRB文件夹里面分Final_Training和Final_Test。训练集按类别编号建子文件夹00000到00042每个文件夹里是一堆.ppm图片外加一个GT-00000.csv记录每张图的 ROI 坐标。测试集则是Final_Test/Images下一堆散图配一个GT-final_test.csv标注类别。这里第一个坑就是类别极度不均衡。限速 30、限速 50 这类常见标志每类有两千多张而像「注意冰雪」这种稀有标志可能只有两百来张。直接按原始分布训练模型会严重偏向多数类。常见做法是统计每类数量训练时用加权采样或者对少数类做增强。import os import pandas as pd from collections import Counter def load_train_annotations(root): 读取 GTSRB 训练集所有类别的标注返回 DataFrame records [] train_dir os.path.join(root, Final_Training, Images) for cls_id in sorted(os.listdir(train_dir)): cls_path os.path.join(train_dir, cls_id) csv_path os.path.join(cls_path, fGT-{cls_id}.csv) if not os.path.exists(csv_path): continue df pd.read_csv(csv_path, sep;) for _, row in df.iterrows(): records.append({ path: os.path.join(cls_path, row[Filename]), label: int(cls_id), # 文件夹名就是类别 roi: (row[Roi.X1], row[Roi.Y1], row[Roi.X2], row[Roi.Y2]) }) return pd.DataFrame(records) df load_train_annotations(./GTSRB) print(df[label].value_counts().head()) print(总样本数:, len(df))这段代码做三件事遍历类别文件夹、读每个类别的 CSV、把图片路径和标签拼成一张总表。sep;是 GTSRB 标注文件的分隔符用逗号会读成一整列。roi字段先存着后面裁剪用。跑完你会看到value_counts()输出的分布多数类和少数类差十倍以上这就是后面要处理的隐患。2.2 用 ROI 裁剪 统一尺寸别直接 resize 整图GTSRB 的原始图片尺寸从 15×15 到 250×250 不等而且标志往往只占画面中间一小块四周是道路背景。如果你直接把整图 resize 到 32×32标志会被背景稀释小目标几乎糊成一团。正确做法是先按 CSV 里的 ROI 坐标把标志裁出来再缩放到统一尺寸。import cv2 import numpy as np def crop_and_resize(img_path, roi, size(32, 32)): 按 ROI 裁剪交通标志并缩放到指定尺寸 img cv2.imread(img_path) if img is None: return None x1, y1, x2, y2 roi # 边界保护防止 ROI 越界 h, w img.shape[:2] x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) crop img[y1:y2, x1:x2] if crop.size 0: return None return cv2.resize(crop, size, interpolationcv2.INTER_AREA) sample crop_and_resize(df.iloc[0][path], df.iloc[0][roi]) print(sample.shape) # (32, 32, 3)cv2.INTER_AREA是缩小图片时的推荐插值方式比默认的双线性更少产生摩尔纹。ROI 越界保护不能省GTSRB 里有少量标注坐标贴边甚至超出的样本不保护会直接抛异常。裁剪完再 resize标志在 32×32 里占比明显更大这是后面精度能不能上去的关键一步。2.3 数据增强别用翻转颜色抖动和旋转才是对的交通标志有个特殊性左右翻转会改变语义。比如「禁止左转」翻过来变成「禁止右转」标签就错了。所以水平翻转、垂直翻转这类增强在交通标志上要慎用甚至禁用。真正安全且有效的是小角度旋转、亮度对比度抖动、轻微缩放平移。import albumentations as A train_transform A.Compose([ A.Rotate(limit15, p0.7), # 小角度旋转模拟拍摄倾斜 A.RandomBrightnessContrast( brightness_limit0.3, contrast_limit0.3, p0.7), # 光照变化 A.RandomScale(scale_limit0.15, p0.5), # 轻微缩放 A.ShiftScaleRotate(shift_limit0.1, scale_limit0, rotate_limit0, p0.5), # 平移 A.Resize(32, 32), ]) val_transform A.Compose([A.Resize(32, 32)])limit15是旋转角度上限交通标志在真实拍摄中倾斜一般不超过这个范围再大就失真了。brightness_limit0.3对应阴天、逆光、隧道口这些光照场景。注意增强只在训练集上做验证集和测试集只做 resize否则评估结果不可信。如果你用 PyTorch可以把这套 transform 包进 Dataset 的__getitem__里训练时开、验证时关。3. 用 PyTorch 搭一个能打的 CNN结构、参数与训练循环3.1 网络结构怎么定三层卷积够不够GTSRB 输入只有 32×32不需要 ResNet 那种深网络。我一般用三层卷积加两层全连接参数量控制在百万级以内训练快、不容易过拟合。结构大致是Conv(3→32) → BN → ReLU → MaxPool → Conv(32→64) → BN → ReLU → MaxPool → Conv(64→128) → BN → ReLU → MaxPool → Flatten → FC(128×4×4→256) → Dropout → FC(256→43)。import torch import torch.nn as nn class TrafficSignCNN(nn.Module): def __init__(self, num_classes43): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 32 - 16 nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 16 - 8 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 8 - 4 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x)) model TrafficSignCNN() print(sum(p.numel() for p in model.parameters())) # 约 60 万参数padding1配合kernel_size3保证卷积后尺寸不变尺寸只被 MaxPool 缩小。BatchNorm 放在卷积和 ReLU 之间是标准顺序能明显加速收敛。Dropout 只加在全连接层卷积层后面不加因为卷积本身参数共享、过拟合风险低。这个结构在 GTSRB 上训练几十轮就能到 97% 以上的验证精度再往上堆深度收益很小。3.2 训练循环里那几个必须调对的参数训练脚本看着简单但学习率、优化器、batch size 这几个参数设错结果差很多。我一般用 Adam学习率 1e-3batch size 64配合余弦退火或者 StepLR。损失函数用交叉熵如果类别不均衡严重加weight参数。from torch.utils.data import DataLoader from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model TrafficSignCNN().to(device) # 按类别频率算权重缓解不均衡 class_counts df[label].value_counts().sort_index().values weights torch.tensor(1.0 / class_counts, dtypetorch.float32) weights weights / weights.sum() * len(class_counts) criterion nn.CrossEntropyLoss(weightweights.to(device)) optimizer Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上评估这里省略评估函数weight_decay1e-4是 L2 正则防止权重过大。CosineAnnealingLR让学习率从 1e-3 平滑降到接近 0比固定学习率收敛更稳。T_max30要和总 epoch 数一致设错了学习率曲线就不对。类别权重那段1.0 / class_counts让少数类获得更大权重再归一化到均值为 1避免整体 loss 尺度变化太大。3.3 验证与保存别只看训练精度训练精度高不代表模型好GTSRB 上很容易过拟合到 99% 训练精度但验证只有 90%。每个 epoch 结束必须在验证集上跑一遍记录验证精度和 loss保存验证精度最高的那个权重。def evaluate(model, loader, device): model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) return correct / total best_acc 0.0 for epoch in range(30): # ... 训练代码 ... val_acc evaluate(model, val_loader, device) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch}: val_acc{val_acc:.4f}, best{best_acc:.4f})model.eval()和torch.no_grad()两个都不能少前者关掉 Dropout 和 BatchNorm 的训练行为后者省显存加速。保存state_dict()而不是整个模型加载时更灵活。如果你发现验证精度震荡厉害多半是学习率太大或者 batch size 太小先把学习率降到 5e-4 试试。4. 推理部署与效果验证从单张图到批量测试4.1 加载模型做单张推理训练完的模型要能实际用起来。推理脚本要做的事加载权重、预处理输入图、前向传播、取 top-k 类别。注意推理时的预处理必须和验证集完全一致只做 resize不做增强。import torch.nn.functional as F def predict(image_path, model, device, class_namesNone): model.eval() img cv2.imread(image_path) img cv2.resize(img, (32, 32), interpolationcv2.INTER_AREA) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # OpenCV 是 BGR tensor torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 tensor tensor.unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): logits model(tensor) probs F.softmax(logits, dim1) top_prob, top_idx probs.topk(3, dim1) return top_idx[0].tolist(), top_prob[0].tolist() model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) idxs, probs predict(./test.jpg, model, device) print(list(zip(idxs, probs)))cv2.cvtColor这步极易漏。OpenCV 读进来是 BGR而训练时如果用的是 PIL 或 torchvision 读的 RGB通道顺序不一致推理结果会莫名其妙地差。permute(2, 0, 1)把 HWC 转成 CHW/ 255.0归一化到 0-1这两步必须和训练时一致。unsqueeze(0)补上 batch 维度模型 forward 期望四维输入。4.2 在测试集上跑一遍看混淆矩阵单张推理只能验证流程通不通真正评估要看整个测试集。GTSRB 官方测试集有 12630 张跑完算总体精度和每类精度画混淆矩阵找出哪些类容易混。from sklearn.metrics import confusion_matrix, classification_report def eval_test_set(model, test_loader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) preds model(imgs).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, digits4)) return confusion_matrix(all_labels, all_preds) cm eval_test_set(model, test_loader, device)classification_report会输出每类的 precision、recall、f1-score一眼能看出哪类拖后腿。经验上限速类之间比如限速 80 和限速 100最容易混因为数字区域在 32×32 下差异很小。如果某类 recall 特别低回去看这类样本是不是太少或者增强方式是不是不合适。混淆矩阵里非对角线的热点就是下一步优化的方向。5. 避坑与排查那些让我返工三次的问题5.1 精度卡在 85% 上不去现象训练 loss 一直降训练精度到 99%验证精度死活卡在 85% 左右。原因最常见的是没做 ROI 裁剪整图 resize 后标志太小其次是数据增强用了水平翻转把「禁止左转」和「禁止右转」搞混了。解决先确认预处理里有没有按 ROI 裁剪再把翻转类增强全部去掉只保留旋转和颜色抖动验证精度通常能跳 5 到 8 个点。5.2 推理结果和验证结果对不上现象验证集精度 97%但拿单张图推理结果乱七八糟。原因预处理不一致。训练时用 torchvision 的ToTensor做了 RGB 转换和归一化推理时用 OpenCV 直接读 BGR 图没转通道反了。解决把推理预处理写成和验证 transform 完全一样的函数或者干脆复用同一个 transform 对象别手写两套。5.3 训练到一半 loss 变成 nan现象前几个 epoch 正常突然 loss 变 nan权重全废。原因学习率太大或者某批数据里有损坏图片导致梯度爆炸。解决先把学习率从 1e-3 降到 1e-4 试再在 Dataset 里加一层校验读图返回 None 就跳过或者用零图替代。另外加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)也能兜底。5.4 显存不够batch size 只能设很小现象报 CUDA out of memorybatch size 降到 8 才能跑但小 batch 训练不稳定。原因模型虽然不大但如果你在 Dataset 里把图片 resize 到 64×64 甚至更大显存占用会翻几倍。解决确认输入尺寸就是 32×32别中途改大用pin_memoryTrue和num_workers4加速数据加载实在不够就开混合精度训练torch.cuda.amp显存能省近一半。5.5 测试集精度比验证集低很多现象验证集 97%官方测试集只有 92%。原因验证集是从训练集里随机切的和训练集同分布而官方测试集是单独采集的光照、角度分布不同。解决这是正常现象别慌。想缩小差距可以在训练时加入更强的光照和模糊增强让模型见过更多样的场景。另外确认测试集的标注文件读取没问题类别编号有没有偏移一位。6. 把精度再往上推一推几个我实际用过的技巧模型跑通之后如果想把测试精度从 95% 推到 98% 以上光靠调学习率不够了。我一般会试这几招。第一是测试时增强TTA对同一张测试图做几个不同的小变换比如原图、旋转 10 度、旋转 -10 度分别推理后把 softmax 概率平均取平均后最大的类。这个技巧几乎不增加训练成本测试精度通常能涨 0.5 到 1 个点。def predict_with_tta(image_path, model, device, n_aug5): model.eval() img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (32, 32)) probs_sum torch.zeros(43).to(device) angles [-10, -5, 0, 5, 10] for angle in angles[:n_aug]: M cv2.getRotationMatrix2D((16, 16), angle, 1.0) rotated cv2.warpAffine(img, M, (32, 32)) tensor torch.from_numpy(rotated).permute(2, 0, 1).float() / 255.0 tensor tensor.unsqueeze(0).to(device) with torch.no_grad(): probs_sum F.softmax(model(tensor), dim1)[0] return probs_sum.argmax().item()第二是模型集成训三到五个结构略有差异的模型比如卷积核数量不同、层数差一层推理时投票。代价是训练时间和推理时间成倍增加但精度提升最明显GTSRB 上集成五个模型能到 99% 左右。第三是难例挖掘把验证集里分错的样本挑出来看看它们有什么共性针对性地补数据或调增强。技巧精度提升幅度额外成本适用场景测试时增强 TTA0.5% ~ 1%推理时间 ×5想快速涨点、不想重训模型集成1% ~ 2%训练时间 ×5追求极致精度难例挖掘 补数据1% ~ 3%人工标注成本某几类持续混淆换更强 backbone1% ~ 2%显存和训练时间数据量足够大最后说个我自己的习惯每次改完预处理或者增强策略一定先跑一个 5 epoch 的小实验看验证精度趋势别一上来就训 50 轮。有次我改了 ROI 裁剪逻辑忘了同步改验证集的预处理训了一晚上发现验证精度反而降了回头查才发现验证集还在用旧的整图 resize。这种低级错误靠小实验几分钟就能暴露出来。希望帮到你。本文还有配套的精品资源点击获取
返回列表