ARTICLE DETAIL

资讯详情

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

CNN交通标志分类实战:从数据预处理到模型部署全解析

CNN交通标志分类实战:从数据预处理到模型部署全解析 简介面向智慧交通与交通物流场景的交通标志识别实战项目聚焦CNN在图像分类任务中的落地应用适合具备一定Python基础、正在学习深度学习的算法工程师、高校学生及竞赛参与者。压缩包共2000个文件包含1994张PNG格式的交通标志图像作为训练与测试数据、3个XML文件以及3个Python脚本整体大小约201.95MB。已有78人学习浏览资源以项目实践为导向可直接通过train.py --data_train、--data_test等参数启动训练也可用predict.py对单张图片进行推理覆盖数据组织、模型构建、训练验证、单图测试等关键环节。目录内图片文件按类别分目录存放便于扩展或迁移到其他分类任务同时保留脚本入口与模型输出参数适合作为课程设计、毕业设计或交通标志分类方向的项目基线帮助读者快速跑通流程并深入理解CNN识别原理。1. 用 cNN 给交通标志分类这个包为什么值得拆开看一遍交通标志分类是把“智慧交通”落地的第一站。别把它当入门级玩具任务——真动手的时候30km/h 和 50km/h 两块限速牌远看几乎一样红圈白底黑字像素一压缩连边缘都糊在一起模型结构或者预处理稍微差一点准确率就卡在 85% 上下不动改哪都像在碰玄学。这个包走的是一条完整的工程闭环train.py做训练predict.py做单张预测数据集按 train/test 拆好关键路径全部通过命令行参数暴露出来不写死、不藏着。特别适合正在做人工智能课设、想找 CNN 实践抓手、或者想用最短路径理解“从图片到模型再回到单张图片”的从业者。它解决的不是“神经网络能不能认出路标”而是“这一整套链路怎么搭才不翻车”。2. 数据布局与命令行传参先把路径和文件名的逻辑吃透后面才不玄学我拆项目有个习惯先看文件和目录不看代码。因为数据集的组织方式直接决定代码怎么写而这个包的数据组织方式藏在 predict 命令里不在 README 里。先把这块吃透后面训练和推理的每一步才有依据。2.1 文件名拆开看标签藏在父目录里不在文件名里打开压缩包第一眼看到的是01639_00000.png、01146_00000.png、01600_00000.png这类“数字_数字”的图片名。如果只看文件名很容易以为前面那串数字就是类别01639就是第 1639 类不是的别这么干。真正决定标签的是父目录。predict 命令里写得很清楚python predict.py --model traffic_sign.model -i ./test/00000/00017_00000.png.png -s这一行的结构是./test测试集根目录→00000类别子目录→00017_00000.png图片文件。也就是说这张图属于第 0 类。类别信息写在父目录名上文件名后半段00000只是这张图在原始采集流程里的编号或帧号和分类任务无关。所以训练时读数据的标准做法是把父目录名转成 int 当标签import os import cv2 import numpy as np def load_images_from_class_dirs(base_dir, target_size(32, 32)): X, y [], [] for cls_name in sorted(os.listdir(base_dir)): cls_path os.path.join(base_dir, cls_name) if not os.path.isdir(cls_path): continue for img_name in sorted(os.listdir(cls_path)): if not img_name.lower().endswith(.png): continue img_path os.path.join(cls_path, img_name) img cv2.imread(img_path) if img is None: continue img cv2.resize(img, target_size) X.append(img) y.append(int(cls_name)) # 类别ID来自父目录名 return np.array(X), np.array(y)这段代码的逻辑很直白os.listdir拿到的是字符00000转成int就是标签 0图片全部 resize 到统一尺寸。关键点在于if img is None: continue——cv2.imread遇到损坏文件不会报错只会返回None不跳过的话下一步cv2.resize直接崩溃。参数说明base_dir传入./train或./testtarget_size取(32, 32)是交通标志这类小目标的常见选择再大就浪费算力再小边缘细节就丢了。如果目录里混入非数字文件夹建议顺手加一句if not cls_name.isdigit(): continue防止把别的目录当类别扫进来。2.2 命令行参数暴露路径为什么 --data_train、--data_test 不写死更好train.py 和 predict.py 都用了命令行参数而不是硬编码路径这对复现非常友好。你在笔记本上调通以后换台机器只需要改命令不用翻开源码改路径。train.py 的参数按这个套路组织import argparse ap argparse.ArgumentParser(descriptionTraffic Sign Train Script) ap.add_argument(--data_train, requiredTrue, help训练集根目录按类别分子目录存放) ap.add_argument(--data_test, requiredTrue, help验证集根目录结构与训练集一致) ap.add_argument(--model, defaulttraffic_sign.model, help模型保存路径) args vars(ap.parse_args()) print(train dir:, args[data_train]) print(test dir:, args[data_test]) print(model :, args[model])--model给了默认值traffic_sign.model所以只传数据集也能跑--data_train和--data_test设成requiredTrue防止忘传以后在代码深处报一个莫名其妙的错。predict.py 的参数设计也值得抄ap.add_argument(--model, requiredTrue, help模型文件路径) ap.add_argument(-i, --image, requiredTrue, help单张图片路径) ap.add_argument(-s, --show, actionstore_true, help是否弹窗显示结果图片)-s是布尔开关命令行里加了就是 True不加就是 False。这种“开关型参数”在推理脚本里比传字符串--show True干净得多。2.3 预处理三件套尺寸、归一化、通道顺序三处必须一致训练和推理的预处理如果对不上前面所有努力全部白费。我见过的项目里最容易在这三个地方翻车第一resize 尺寸必须一致。训练时缩到 32×32推理时哪怕缩到 33×33模型输入维度就报错。第二归一化必须一致。训练时除以 255 缩放到 0-1推理时忘了除像素值就是 0-255预测结果基本是乱的。第三通道顺序必须想清楚。cv2.imread读进来是 BGRmatplotlib显示期望 RGB颜色错乱不一定影响准确率但中间一旦做了通道相关的数据增强误差就会滚雪球。一个典型预处理函数长这样def preprocess_image(image, target_size(32, 32)): # cv2.imread 读出来的原始图BGR 通道0-255 image cv2.resize(image, target_size) image image.astype(float32) / 255.0 return image逻辑说明先astype(float32)再除 255这两个顺序不能换。如果直接用 uint8 除以 255Python 会做整数除法所有结果不是 0 就是 1等于把图片二值化模型永远收敛不了。这个错很隐蔽因为代码不报异常只是 loss 掉得奇慢。2.4 训练前必做的检查类别分布和图片尺寸统计很多人在训练脚本跑到一半才想起来数据集有问题不如上来就打印一段统计信息。我一般在主流程前加三行from collections import Counter print(训练集类别分布:, Counter(y_train)) print(训练集图片尺寸:, X_train.shape, 像素范围:, X_train.min(), X_train.max())Counter(y_train)直接告诉你每个类别多少张图一眼看出有没有类别不平衡像素范围打印出来能确认前期预处理是否已经归一化。这一步只要 10 秒钟能省掉后面至少半小时的玄学排查。3. 训练链路全拆解数据划分、CNN 结构选型与模型保存格式train.py 的核心链路是读数据 → 切验证集 → 搭模型 → 训练 → 保存。这五步每一步都有讲究尤其最后一步模型保存格式稍不注意就会变成坑。3.1 数据划分train_test_split 和 stratifylabels 的含义--data_train 指向的是完整训练集但训练时不能把所有图都拿去拟合必须留出一部分当验证集否则你无法判断模型是“真学会了”还是“背下来了”。这里建议直接用 scikit-learn 的切分函数from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( images, labels, test_size0.2, random_state42, stratifylabels )参数说明test_size0.2表示留 20% 当验证集random_state42固定随机种子保证每次跑出来的划分一致方便对比实验stratifylabels是最容易被忽略的一个参数它让划分后的训练集和验证集里每个类别的比例和原始数据集保持一致。如果不加类别少的标志可能在验证集里一张都不出现准确率指标就失真了。3.2 CNN 结构选型小数据集别迷恋深网络两层卷积加批归一化优先这个场景下数据集不大图片又是标志这类结构清晰的目标我一般不推荐直接抄 ResNet 或者 VGG参数多、训练慢、还容易过拟合。更稳妥的是一套轻量 CNN两层卷积 池化 批归一化 全连接。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import ( Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization ) def build_model(input_shape(32, 32, 3), num_classes43): model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), BatchNormalization(), MaxPooling2D(pool_size(2, 2)), Conv2D(64, (3, 3), activationrelu), BatchNormalization(), MaxPooling2D(pool_size(2, 2)), Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(num_classes, activationsoftmax) ]) return model选型理由第一层卷积 32 个核提取边缘、颜色、纹理这类低级特征第二层卷积升到 64 个核在中级特征层面做组合。两层足够因为交通标志的判别信息集中在外圈形状和中心符号上不需要深度的语义抽象。BatchNormalization放在卷积后面激活函数前面是稳定训练的关键——我见过太多不加 BN 的项目loss 曲线像心电图。Dropout(0.5)在全连接层防过拟合小数据集尤其需要。如果你不确定类别数量可以用len(set(y_train))动态算num_classes别写死。否则类别数不对最后一层 softmax 输出维度会和标签对不上。3.3 训练参数epoch、batch_size、学习率和早停模型编译和训练的常见写法如下model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs15, batch_size32, verbose1 )参数说明sparse_categorical_crossentropy配合整数标签用省去 one-hot 编码的步骤adam的默认学习率是 0.001对这个小数据集基本够用。epochs15看起来不多但对几千张图、两层卷积的规模15 个 epoch 足够收敛batch_size32是内存和梯度稳定性的平衡点。如果训练到后面发现验证准确率停滞常见做法是把 epochs 加到 30 并加上早停from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_accuracy, patience3, restore_best_weightsTrue ) model.fit(X_train, y_train, validation_data(X_val, y_val), epochs30, batch_size32, callbacks[early_stop])patience3表示验证准确率连续 3 个 epoch 不涨就停restore_best_weightsTrue会把权重回滚到最优的那一个 epoch避免保存一个过拟合的中后段模型。3.4 模型保存格式traffic_sign.model 里到底存了什么训练结束后的保存命令是model.save(args[model])这个.model后缀是自定义的。Keras 模型默认保存格式可能是 HDF5 或 SavedModel但换成.model后缀不会改变内部结构。也就是说traffic_sign.model本质上就是 Keras 保存的模型文件加载时依然要用load_model而不是pickle.load。从 3.3 的代码来理解save 保存的是三样东西模型结构各层配置、训练好的权重、以及编译信息优化器状态、loss 配置。这也是为什么加载后可以直接model.predict不用重新 compile。这里隐含着第一个坑跨机器复现时Keras 版本不一致最容易让.model文件加载失败。后面避坑章节我会专门展开讲。4. 单张预测链路predict.py 从读图到输出置信度的四个环节推理脚本看起来比训练简单但它的容错设计更考验经验。predict.py 的几个关键环节每个都不能跳过。4.1 先加载模型再读图片顺序和错误处理都有讲究import cv2 import numpy as np from tensorflow.keras.models import load_model model load_model(args[model]) image_path args[image] if not os.path.exists(image_path): print(f[错误] 图片不存在: {image_path}) exit(1) image cv2.imread(image_path) if image is None: print(f[错误] 图片读取失败可能是文件损坏: {image_path}) exit(1)这里先加载模型是为了让模型加载报错提前暴露。如果图片路径错了模型加载成功但图片读取失败错误信息会清楚很多。cv2.imread对不存在的路径不会抛异常只会静默返回None所以os.path.exists和is None两道检查都值得写。真实项目里用户最容易错的就是路径少写一个斜杠、或者把类别目录写错。4.2 推理预处理必须和训练严格对齐模型加载好了图片也读进来了接下来这一步是整个 predict.py 的灵魂——预处理必须和训练时完全一致image cv2.resize(image, (32, 32)) image image.astype(float32) / 255.0 image np.expand_dims(image, axis0) # 增加 batch 维度 preds model.predict(image)[0] idx int(np.argmax(preds)) confidence float(preds[idx]) print(预测类别:, idx, 置信度:, round(confidence, 4))代码逻辑拆开看resize 成 32×32和训练时保持一致astype(float32) / 255.0做归一化np.expand_dims(image, axis0)把形状从(32, 32, 3)变成(1, 32, 32, 3)因为模型接受的是“一批图片”的输入单张图也要凑一个 batch 维度。model.predict(image)返回的是二维数组[0]取第一张图的结果np.argmax拿到概率最高的类别下标preds[idx]就是模型对该类的置信度。这是新手最容易漏的一步忘了加 batch 维度Keras 会报维度错误但报错信息比较绕不像 NumPy 那样直白。如果你看到 “expected 4 dimensions” 之类的提示第一反应就应该是expand_dims没加。4.3 -s 可视化开关显示图片和标注别把通道顺序搞乱predict.py 的-s参数表示显示图片。常见的实现是在预测完成后把图片放大、写上预测结果再弹窗if args[show]: show_img cv2.resize(image[0], (200, 200)) cv2.putText(show_img, fclass: {idx}, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imshow(prediction, show_img) cv2.waitKey(0) cv2.destroyAllWindows()这里有个小细节image[0]在 resize 之后是float32且归一化到 0-1直接cv2.imshow显示出来会是一片黑或者泛白。正确做法是先乘回 255 并转回uint8再显示。代码里这一步容易漏我习惯这样处理show_img cv2.resize(image[0], (200, 200)) show_img np.clip(show_img * 255.0, 0, 255).astype(uint8) # 还原显示范围clip是为了防止浮点误差把像素值顶到 255 以上显示出现白斑。4.4 批量推理predict 天然支持多张图不用写循环单张测试没问题后如果你想验证整个 test 目录的准确率不需要一张张调用 predict.py。Keras 的predict本身支持批量输入直接把多张图堆成一个 batchX_batch np.array([preprocess_image(cv2.imread(p)) for p in test_paths]) preds model.predict(X_batch, batch_size32) labels np.argmax(preds, axis1)一行代码跑完整批测试。注意batch_size32只是控制内存占用不改变结果。这个批量用法在后面做混淆矩阵时也会用到。5. 避坑地图五个常见翻车点照着排查省一下午这一章是血泪经验的集中区。以下五个坑每一个我都见过不止一次也是这个项目复现过程中最常把人气到摔键盘的地方。5.1 图片读取返回 None崩在 resize 上现象训练或预测脚本跑到cv2.resize时突然抛出异常提示cv2.error或者NoneType对象没有大小属性。原因cv2.imread读取失败的图片路径返回的不是异常而是None。路径写错、图片文件损坏、文件名大小写对不上.PNG和.png都会引发这个结果。更隐蔽的是有些解压工具在解压时把文件路径里的中文转成了乱码导致程序找不到文件。解决在imread之后立刻判空。我在 2.1 和 4.1 里都写了if img is None: continue或exit(1)这不是防御性编程的洁癖是真能救命。另外读取之前用os.path.exists先做一次路径确认能明显缩小排查范围。5.2 类别不平衡少数类怎么训练都认不出来现象训练集整体准确率到了 90%但某个类别比如某种不常见的警告标志在验证集里一张都没认对看训练日志loss 还在下降但验证集准确率波动很大。原因不同类别的样本数量差异悬殊。这个包的数据集本身不是完全均衡的有的类别只有几十张有的类别几百张模型把大多数参数都用来拟合大数据类了小数据类的梯度信号被淹没。解决第一步先把 2.4 的Counter(y_train)打出来看看每类样本量。如果差异超过 5 倍常见处理办法有两个一是对少数类做数据增强旋转、平移、亮度扰动补足数量二是在model.fit里传class_weight让少数类的loss惩罚更大from sklearn.utils.class_weight import compute_class_weight weights compute_class_weight(balanced, classesnp.unique(y_train), yy_train) class_weight dict(enumerate(weights)) model.fit(X_train, y_train, epochs15, batch_size32, class_weightclass_weight)compute_class_weight(balanced, ...)会自动根据样本量反比计算权重多数类权重小于 1少数类权重大于 1这样模型会更认真地对待小类别。5.3 traffic_sign.model 加载失败版本不一致和后缀名迷惑现象在自己电脑上训练好的traffic_sign.model换台机器加载时报错常见的有Unknown layer、Unrecognized data type或者直接提示不是有效的 HDF5 文件。原因.model后缀是自定义的Keras 或者 TensorFlow 根本不认识这个后缀它内部仍然是标准格式。报错真正的原因多数是两台机器的 TensorFlow/Keras 版本不一致旧版本保存的模型权重在新版本里需要做兼容转换。解决第一加载时统一用tf.keras.models.load_model不要混用原生 Keras 和 tf.keras——两个库的内部实现不兼容。第二如果版本差异过大就用h5py先打开文件看内部结构确认它到底是 HDF5 还是 SavedModel 目录再选择对应加载方式。第三以后保存模型时可以明确后缀model.save(traffic_sign.keras) # Keras 3.x 新格式 # 或者 model.save(traffic_sign.h5) # HDF5 格式用 Keras 认识的后缀比自定义.model少一层迷惑。这算是这个项目本身留给使用者的一个隐形坑。5.4 BGR/RGB 通道顺序混用显示全蓝但准确率没事现象用cv2.imshow显示预测图正常但用matplotlib.pyplot.imshow显示同一张图颜色明显偏蓝如果训练阶段用了色彩扰动测试集上准确率突然崩掉。原因cv2.imread读进来是 BGR 顺序matplotlib默认按 RGB 显示两者不转换就会出现“红蓝互换”。如果训练时用的是 OpenCV 读图推理时也用 OpenCV模型本身不受影响但一旦中间混用了 PILPillow 读图是 RGB两张图在模型眼里就是完全不同的内容准确率直接跳水。解决统一入口。整个项目从预处理到训练再到预测只用cv2.imread读图需要显示的时候再转 RGBrgb_img cv2.cvtColor(image, cv2.COLOR_BGR2RGB) plt.imshow(rgb_img)这个转换只影响显示不影响模型输入。如果你打算用 PIL 读图那就要保证训练和推理全部换成 PIL绝不能混用。5.5 训练 loss 在降验证准确率却纹丝不动现象训练日志里loss从 1.2 降到 0.4但val_accuracy一直卡在 60% 左右像被焊死了。原因这是三个问题叠加时的典型症状。最常见的是学习率太大模型在最优解附近来回震荡loss 整体在降但精度上不去其次是验证集和训练集分布不一致验证集里有些类别的样本长得和训练集差异太大最后是过拟合信号——训练集 loss 降得很快验证集 loss 却开始反弹准确率自然停滞。解决先看history.history[val_loss]如果它先降后升立即把epochs调小或启用早停3.3 里写的EarlyStopping。如果 val_loss 还在降那就是学习率和模型容量的问题把 Adam 学习率从 0.001 降到 0.0003同时给卷积层增加一个 3×3 的过滤器数量from tensorflow.keras.optimizers import Adam model.compile( optimizerAdam(learning_rate0.0003), losssparse_categorical_crossentropy, metrics[accuracy] )再把Conv2D(32, ...)改成Conv2D(64, ...)给模型多一点拟合能力。这两步改完大多数“loss 降但 accuracy 不动”的情况都能缓解。6. 把准确率再往上顶混淆矩阵、真实场景测试与迁移学习路线训练完、单张预测也通了准确率到了 90% 上下很多人就收工了。但实际部署前还有三道检查值得做它们决定这个模型是“作业能交”还是“真能用”。第一道检查混淆矩阵。准确率只告诉你整体比例不告诉你哪个类别在互相混淆。交通标志里最典型的是限速标志之间30km/h 和 50km/h、70km/h 和 80km/h红圈里数字不同其余全部一样模型很容易认差。用 scikit-learn 打印分类报告from sklearn.metrics import classification_report y_pred_labels np.argmax(model.predict(X_val), axis1) print(classification_report(y_val, y_pred_labels))classification_report每一类一行精确率、召回率、F1 分数一目了然。我看到这个报表才知道原来项目里最烂的不是样本最少的类别而是长得最像的两个限速牌——召回率 0.55 的那个类就是模型最该重点补强的地方。第二道检查真实场景测试。测试集里的图是经过裁剪、对齐、亮度和训练集一致的。真实场景里手机随手拍一张角度歪的、反光的、树荫挡住半边的模型能不能认出来是另一回事。我建议训练完用手机在屏幕前拍几张或者拍真的路牌丢给 predict.py 跑一遍。如果真实图效果差别慌这是正常现象试着在训练的数据增强里加一点随机旋转和亮度扰动能明显提升鲁棒性。第三道检查迁移学习。如果准确率卡在 92% 上不去而你又想把项目做得再深一点可以尝试用预训练模型替换前面的卷积部分。注意预训练模型比如 ResNet50要求输入分辨率至少 48×48甚至 224×224所以需要先把数据改成更大的尺寸import tensorflow as tf base_model tf.keras.applications.MobileNetV2( include_topFalse, weightsimagenet, input_shape(64, 64, 3) ) base_model.trainable False model tf.keras.Sequential([ base_model, tf.keras.layers.GlobalAveragePooling2D(), tf.keras.layers.Dense(num_classes, activationsoftmax) ])include_topFalse丢掉原来的分类头trainableFalse冻结预训练权重只训练最后新增的全连接层。这样训练速度快而且前面几层用的是在百万级 ImageNet 上学到的通用特征比从零训练的两层卷积表达能力更强。从那以后我每次拿到这类分类资源都会强制走一遍固定流程先统计类别分布确认预处理函数是同一个入口训练完立刻跑 predict.py 做单张测试最后打印 classification_report 看类别级的混淆情况。这套流程看着不起眼但帮我避掉了三分之二的翻车现场。希望帮到你。本文还有配套的精品资源点击获取
返回列表