简介这份资源面向计算机、人工智能、电子信息等相关专业的学生与开发者提供一套基于卷积神经网络识别交通标志的完整Python项目可用于课程设计、毕业设计、大作业或初期项目立项演示。项目以GTSRB交通标志数据集为训练与测试基础代码经过实际运行验证功能正常适合作为入门深度学习的实战练习。压缩包共9个文件包含5个Python源码文件、2个CSV数据文件、1个XML配置文件及1份Markdown项目说明整体约310KB体积轻便便于快速下载与本地部署。源码按数据预处理、模型构建、训练与评估等环节拆分配合说明文档可帮助读者理解CNN在图像分类任务中的完整流程掌握数据读取、网络搭建、模型训练与性能评估等关键步骤。目前已有170人学习适合希望积累计算机视觉项目经验、完善简历或完成学业任务的读者参考借鉴。1. 从一张模糊的路牌说起CNN 识别交通标志到底在解决什么问题开车经过路口限速 60 的圆牌被树影遮了一半人眼都要愣一下更别说让机器认。基于 CNN 识别交通标志这件事本质就是让卷积神经网络在一堆 32×32 的小图里把「限速 60」「禁止掉头」「前方施工」这些类别分清楚。它解决的不是炫技问题而是自动驾驶和辅助驾驶里最基础的一环环境感知。GTSRBGerman Traffic Sign Recognition Benchmark是这个方向最经典的公开数据集五万多张实拍图、43 个类别光照、遮挡、倾斜、模糊全都有拿它练手最接近真实路况。这篇笔记面向两类人刚学完 cnn卷积神经网络 想找个完整项目练手的以及手里有 python源码 但跑不通、不知道参数怎么调的。我会把数据怎么读、模型怎么搭、训练怎么不翻车、结果怎么验证一条条讲清楚。2. GTSRB 数据集拆解与 python 环境准备别急着跑代码2.1 GTSRB 的目录结构和类别分布GTSRB 原始压缩包解压后通常长这样GTSRB/Final_Training/Images/下面按类别编号分成 43 个文件夹从00000到00042每个文件夹里是若干张.ppm格式的图片外加一个GT-00000.csv记录每张图的 ROI 区域和尺寸。测试集在GTSRB/Final_Test/Images/图片混在一起标签在GT-final_test.csv里。这里第一个坑就来了训练集图片尺寸不统一从 15×15 到 250×250 都有而 CNN 需要固定输入。常见做法是统一缩放到 32×32这也是原始论文的基准尺寸。类别分布极度不均衡。限速 30类别 1有 2000 多张而限速 120类别 8只有 1000 出头某些稀有类别甚至不到 200 张。如果你直接按顺序喂数据模型会偏向多数类。我一般会先统计一遍分布再决定要不要做重采样或类别加权。import os import pandas as pd from collections import Counter # 统计训练集每个类别的图片数量 train_dir GTSRB/Final_Training/Images class_counts {} for cls in sorted(os.listdir(train_dir)): cls_path os.path.join(train_dir, cls) if os.path.isdir(cls_path): imgs [f for f in os.listdir(cls_path) if f.endswith(.ppm)] class_counts[cls] len(imgs) # 打印分布看看有没有严重不均衡 for k, v in class_counts.items(): print(f类别 {k}: {v} 张) print(最多:, max(class_counts.values()), 最少:, min(class_counts.values()))这段代码只做一件事把每个类别的图片数量数出来。os.listdir遍历目录endswith(.ppm)过滤掉 CSV 文件。跑完你会看到最多和最少差十倍以上这就是后面要处理不均衡的依据。参数上没什么可调的但注意路径里的Final_Training别写成Final_Test两个目录结构一样很容易搞混。2.2 python 环境与依赖安装vscode 和 pycharm 都适用环境这块python下载安装教程 网上一搜一大把我不重复。重点说版本和依赖。这个项目用 Python 3.8 到 3.10 都行3.11 以上某些老版本 TensorFlow 会报错。深度学习框架二选一TensorFlow/Keras 或者 PyTorch。原始 python源码 大多基于 Keras因为写起来短。我建议先用 Keras 跑通再考虑换 PyTorch。依赖清单如下直接 pip 装# 创建虚拟环境避免污染全局 python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/Mac 激活 source venv/bin/activate # 安装核心依赖 pip install numpy pandas matplotlib opencv-python pip install tensorflow2.10.0 pip install scikit-learnopencv-python用来读.ppm图片虽然 PIL 也能读但 cv2 在批量处理时更快。scikit-learn用来做混淆矩阵和分类报告。TensorFlow 版本我锁在 2.10是因为再往上tf.keras.preprocessing.image.ImageDataGenerator的一些参数行为有变化老代码容易出玄学问题。如果你用 vscode配置python 环境记得在设置里把解释器指向 venv 里的 python否则终端装完了编辑器里还是找不到包。pycharm配置python环境 同理在 Project Interpreter 里选 venv。提示装完 TensorFlow 后跑一句python -c import tensorflow as tf; print(tf.__version__)能打印出版本号才算成功。如果报 DLL 错误多半是 Visual C 运行库没装。2.3 把 ppm 读进来并转成 numpy 数组.ppm格式 cv2 直接支持但要注意颜色通道顺序。cv2 默认读进来是 BGR而 matplotlib 显示和大多数预训练模型期望的是 RGB。训练时如果通道顺序搞反模型也能收敛但准确率会莫名其妙低几个点这种问题最难查。import cv2 import numpy as np def load_image(path, size(32, 32)): # cv2 读取得到 BGR img cv2.imread(path) if img is None: raise ValueError(f读不到图片: {path}) # 缩放到固定尺寸 img cv2.resize(img, size, interpolationcv2.INTER_AREA) # BGR 转 RGB img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) return img # 测试一张 sample load_image(GTSRB/Final_Training/Images/00000/00000_00000.ppm) print(sample.shape, sample.dtype) # 期望 (32, 32, 3) uint8cv2.resize的interpolation参数在小图缩放时建议用INTER_AREA它比默认的INTER_LINEAR更少产生锯齿。cvtColor那一步千万别省。返回的 dtype 是 uint8后面送进模型前要除以 255 归一化到 0 到 1这个在数据生成器里做。3. 用 Keras 搭一个能打的 CNN结构、参数与训练策略3.1 卷积基与分类头的分层设计cnn基本结构 就三块卷积层提特征、池化层降维、全连接层做分类。但具体堆几层、每层多少卷积核直接决定模型能不能在 GTSRB 上跑到 95% 以上。我一般用三个卷积块每个块里两个 3×3 卷积加一个 2×2 最大池化卷积核数量从 32 翻到 64 再翻到 128。这个配置在 32×32 输入上参数量适中不会过拟合得太厉害。from tensorflow.keras import layers, models def build_cnn(num_classes43): model models.Sequential([ # 第一块32 个卷积核提取边缘和颜色 layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shape(32, 32, 3)), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第二块64 个卷积核提取形状和纹理 layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第三块128 个卷积核提取高层语义 layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 分类头 layers.Flatten(), layers.Dense(256, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return model model build_cnn() model.summary()paddingsame保证卷积后尺寸不变这样池化前特征图不会缩得太快。每个卷积块后加Dropout(0.25)全连接层后加Dropout(0.5)这是防过拟合的常规操作。Flatten把 4×4×128 的特征图拉成 2048 维向量再进 256 维全连接。最后一层 43 个神经元对应 43 类softmax 输出概率。model.summary()跑一下参数量大概在 100 万出头不算大普通显卡甚至 CPU 都能训。3.2 数据增强与类别加权让模型别偏科前面说了类别不均衡解决办法有两个一是用ImageDataGenerator做在线增强旋转、平移、缩放、亮度调整让少数类在每轮训练里被「造」出更多样本二是算类别权重让损失函数对少数类更敏感。两个一起用效果最好。from tensorflow.keras.preprocessing.image import ImageDataGenerator from sklearn.utils.class_weight import compute_class_weight import numpy as np # 训练集增强配置 train_datagen ImageDataGenerator( rescale1./255, # 归一化到 0-1 rotation_range15, # 随机旋转 ±15 度 width_shift_range0.1, # 水平平移 10% height_shift_range0.1, # 垂直平移 10% zoom_range0.1, # 缩放 10% brightness_range(0.8, 1.2), # 亮度扰动 validation_split0.2 # 划出 20% 做验证 ) # 假设 X_train, y_train 已经加载好 # 这里用 flow 从内存数组生成 train_gen train_datagen.flow( X_train, y_train, batch_size64, subsettraining ) val_gen train_datagen.flow( X_train, y_train, batch_size64, subsetvalidation ) # 计算类别权重 class_weights compute_class_weight( class_weightbalanced, classesnp.unique(y_train), yy_train ) class_weight_dict dict(enumerate(class_weights))rescale1./255把像素值压到 0 到 1这是 CNN 训练的标配不做的话收敛极慢。rotation_range15别设太大交通标志旋转 30 度以上就不像真实场景了。validation_split0.2从训练集里切验证集注意flow的subset参数要和生成器里的validation_split配合。compute_class_weight的balanced模式会自动按类别频率反比给权重少数类权重高多数类权重低。这个字典在model.fit里通过class_weight传进去。3.3 编译、训练与回调早停和降学习率编译时优化器选 Adam学习率从 0.001 开始。损失函数用分类交叉熵因为标签是整数不是 one-hot所以用sparse_categorical_crossentropy。回调里加两个EarlyStopping防止过拟合ReduceLROnPlateau在验证损失不降时降学习率。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) callbacks [ EarlyStopping(monitorval_loss, patience8, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience4, min_lr1e-6), ModelCheckpoint(best_model.h5, monitorval_accuracy, save_best_onlyTrue) ] history model.fit( train_gen, epochs50, validation_dataval_gen, class_weightclass_weight_dict, callbackscallbacks )patience8意思是验证损失连续 8 轮不降就停restore_best_weightsTrue保证停的时候回到最好的那轮权重不用后悔药。ReduceLROnPlateau的factor0.5每次把学习率砍半patience4比早停的耐心值小这样先降学习率再考虑停。ModelCheckpoint把验证准确率最高的模型存成best_model.h5后面测试直接加载这个文件。训练轮数设 50 是上限实际早停可能二三十轮就结束了。4. 训练完不算完评估、可视化与常见翻车排查4.1 在测试集上算准确率、混淆矩阵和分类报告训练时看到的验证准确率是切出来的真正要报告的是在Final_Test上的结果。测试集标签在 CSV 里读进来和预测结果对齐。import pandas as pd from sklearn.metrics import classification_report, confusion_matrix import numpy as np # 读测试集标签 test_csv pd.read_csv(GTSRB/GT-final_test.csv, sep;) test_labels test_csv[ClassId].values test_paths [GTSRB/Final_Test/Images/ f for f in test_csv[Filename].values] # 批量加载测试图 X_test np.array([load_image(p) for p in test_paths]) / 255.0 # 加载最好的模型并预测 model.load_weights(best_model.h5) preds model.predict(X_test, batch_size64) pred_labels np.argmax(preds, axis1) # 输出报告 print(classification_report(test_labels, pred_labels, digits4)) print(confusion_matrix(test_labels, pred_labels))sep;是因为 GTSRB 的 CSV 用分号分隔不是逗号这个细节不注意会读成一整列。load_image返回 uint8除以 255 归一化和训练时一致。classification_report会给出每个类别的精确率、召回率、F1重点看召回率低的类别往往是那些样本少或者长得像的。混淆矩阵能看出哪两类互相误判比如限速 60 和限速 80 经常混。4.2 训练曲线怎么读过拟合和欠拟合的信号把history里的准确率和损失画出来比只看最终数字有用得多。import matplotlib.pyplot as plt fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(history.history[accuracy], labeltrain) axes[0].plot(history.history[val_accuracy], labelval) axes[0].set_title(Accuracy) axes[0].legend() axes[1].plot(history.history[loss], labeltrain) axes[1].plot(history.history[val_loss], labelval) axes[1].set_title(Loss) axes[1].legend() plt.show()如果训练准确率一直涨、验证准确率早早平了甚至往下掉两条线开口越来越大就是过拟合该加 Dropout 或加数据增强。如果两条线都低还贴在一起那是欠拟合模型容量不够得加层或加卷积核。如果验证损失震荡得厉害多半是 batch size 太小或学习率太高。4.3 避坑与排查五个血泪教训现象一训练准确率卡在 10% 以下不动。原因通常是标签没对齐比如图片路径和标签顺序错位或者归一化忘了做。解决先拿 100 张图过一遍模型看预测分布是不是均匀随机再检查X_train和y_train的索引是否一一对应。现象二验证准确率比训练准确率高很多。这听起来是好事但往往是验证集太小或者和训练集有重叠。GTSRB 的validation_split是从训练集尾部切的如果数据按类别排序过切出来的验证集可能只包含某几类。解决切之前先shuffle或者手动用train_test_split分层抽样。现象三模型在测试集上准确率比验证集低一大截。常见原因是测试集图片的预处理和训练不一致比如训练用了 RGB测试忘了转或者缩放插值方式不同。解决把训练和测试的预处理写成一个函数两边都调它别复制粘贴。现象四显存爆了报 OOM。batch size 设太大或者图片没缩放直接送进网络。解决把 batch size 从 64 降到 32 甚至 16确认输入是 32×32 而不是原始尺寸。如果还爆用tf.keras.mixed_precision开混合精度。现象五训练到一半 loss 变成 NaN。学习率太高或者数据里有脏图全黑、全白、损坏。解决把学习率降到 1e-4加clipnorm1.0梯度裁剪再写个脚本扫一遍图片把标准差为 0 的图剔掉。注意.ppm文件用 PIL 读有时候会报UnidentifiedImageError换 cv2 基本都能解决。如果 cv2 也读不了用file命令看下文件头是不是真的 ppm。5. 从 95% 到 98%几个我反复验证过的提分技巧第一个技巧是在卷积块之间加 BatchNormalization。原始结构里卷积后直接 ReLU如果输入分布偏移训练会不稳。在Conv2D和activation之间插一层BatchNormalization再把activation显式写成layers.Activation(relu)收敛更快最终准确率通常能涨 1 到 2 个点。代价是训练稍慢但值得。# 改造后的卷积块示例 layers.Conv2D(64, (3, 3), paddingsame, use_biasFalse), layers.BatchNormalization(), layers.Activation(relu),use_biasFalse是因为 BatchNorm 自带偏置再加卷积偏置是冗余的。第二个技巧是测试时增强TTA。对每张测试图做几次小变换水平翻转、轻微平移分别预测再平均概率。交通标志左右翻转后语义可能变比如左转和右转所以翻转要慎用但平移和缩放是安全的。def predict_with_tta(model, img, n5): preds [] for _ in range(n): # 随机平移 ±2 像素 dx, dy np.random.randint(-2, 3, size2) M np.float32([[1, 0, dx], [0, 1, dy]]) shifted cv2.warpAffine(img, M, (img.shape[1], img.shape[0])) shifted shifted / 255.0 preds.append(model.predict(shifted[np.newaxis, ...], verbose0)) return np.mean(preds, axis0)n5是次数太多没必要5 次已经能平滑掉随机误差。warpAffine做平移边界默认填充黑色对 32×32 小图影响可控。第三个技巧是用学习率预热。前 3 轮把学习率从 1e-5 线性升到 1e-3再正常训练。这个在 Keras 里用LearningRateScheduler回调实现能避免训练初期 loss 震荡。我试过在 GTSRB 上预热能让最终准确率稳定提升 0.5 个点左右尤其是 batch size 较大的时候。最后一个习惯每次改结构或参数只改一个变量跑完记录结果。我见过太多人一次改三四个地方结果好了不知道哪个起作用坏了也不知道哪个背锅。用一个简单的 CSV 记下实验编号、改动内容、验证准确率、测试准确率几轮下来你就知道哪些技巧对这个数据集真正有效。这个习惯比任何单个技巧都值钱。希望帮到你。本文还有配套的精品资源点击获取
