ARTICLE DETAIL

资讯详情

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

基于YOLOv5的道路交通标识识别:从数据集到训练避坑全指南

基于YOLOv5的道路交通标识识别:从数据集到训练避坑全指南 简介一套基于YOLOv5算法的道路交通标识识别系统面向毕业设计、期末大作业与课程设计场景提供完整Python源码与配套数据集。从模型训练、推理验证到图形化界面覆盖全流程代码逐段注释新手也能理解核心逻辑便于二次开发和功能扩展。压缩包共266个文件约423.33MB主要文件类型包括53个py源码、59个yaml模型配置、10个pt权重、jpg/jpeg/png图像数据集、6个csv训练指标、5个sh脚本及dockerfile等源码、配置与图像数据分区存放检索方便。项目已通过严格调试可快速部署运行内置预训练权重和整理好的数据集能直接用于交通标志识别演示或模型训练同时提供训练日志与评估结果方便复现实验和撰写论文。代码中还包含数据预处理、模型定义、训练测试脚本以及界面交互逻辑模块划分清晰便于按需修改。目前已有50人浏览学习这份高分毕设项目对希望快速搭建深度学习应用或研究YOLOv5识别原理的读者很有参考价值。1. 别急着写代码先想清楚这套毕设到底要交付什么每年这个时节能看到一大批“道路交通标识识别”的毕设题目绝大多数人第一反应是去找源码、下数据集、跑通一个detect.py然后截图写报告。做过一次你就会发现真正决定毕设能不能落到“高分”的是你能不能把 YOLOv5 从“能跑”变成“在你的数据集上稳定收敛、推理结果经得起问辩”。这篇笔记不打算复述 YOLOv5 的论文而是按我实际做完一版“基于 YOLOv5 的道路交通标识识别系统”的顺序把数据集怎么整理、模型怎么调、训练踩了什么坑、最后怎么导出推理一条条讲清楚。适合正在做毕设、或者想拿这套方案做课程设计的同学如果你已经跑通过 YOLOv5可以直接跳到第 5 章看避坑记录那几段是我重复训练十几轮之后才总结出来的。先说结论这套系统的核心难点不在 YOLOv5 本身而在于交通标识这种“小目标 类别多 真实场景遮挡严重”的数据分布。你选的每个参数——输入尺寸、anchor、置信度阈值、NMS 的 IoU——都会被这个分布放大成肉眼可见的漏检或误检。所以接下来的每一章我都会先讲参数为什么这么定再给可以直接抄的配置和命令。2. 交通标识识别为什么绕不开 YOLOv5选型、原理与后处理里真正卡人的环节2.1 小目标密集场景下YOLOv5 的 Backbone 和 Neck 各自扛了什么活道路交通标识的检测任务有一个很明显的特征目标尺寸跨度极大。同一张 1920×1080 的街景图里远处的限速牌可能只有 20×20 像素近处的标志牌能占到 200×300。传统的两阶段检测器Faster R-CNN 系列对小目标确实更稳但推理速度压不下来毕设答辩现场如果用 CPU 跑实时视频流基本就卡成幻灯片了。YOLOv5 能在精度和速度之间取得平衡靠的是它这套结构组合Backbone 用 CSPDarknet 提特征Neck 用 PANet 做多尺度融合Head 在三个尺度上分别输出大、中、小目标的预测结果。我一般会给同学画一张简化的网络结构图帮助他们理解Backbone 对输入图做 5 次下采样越往后的特征图语义越强、空间分辨率越差Neck 里的 FPN 自顶向下传递语义信息PANet 再自底向上传回空间位置信息。交通标识这种既需要“认出这是限速 40”又需要“精确定位它在画面左上角”的任务恰好同时依赖语义和位置两个维度所以 PANet 的融合对最终 mAP 的贡献非常明显。不建议上来就换成更重的 Backbone比如 CSPResNeXt参数量翻倍但交通标识本身纹理简单、类别差异主要靠颜色和形状轻量网络足够。2.2 从预测到输出YOLOv5 后处理的三个参数漏检误检全看这里很多人调试 YOLOv5 只看训练 loss忽略了推理阶段的后处理——这是典型的黑匣子思维。实际上detect.py里那几行 NMS 代码决定了一个检测框能不能出现在最终画面上。YOLOv5 的 Head 在每个网格上预测出边界框坐标、目标置信度obj score和类别概率三个值相乘得到最终的置信度分数。然后 NMS 按类别逐个抑制重叠框用的是 IoU 阈值。我通常会直接改data/hyps/hyp.scratch-low.yaml里的conf_thres和iou_thres但这两个值在训练和推理阶段的表现差异很大。训练时我习惯把conf_thres设到 0.001这样能保证所有候选框都参与 loss 计算不会因为阈值太高把难样本直接滤掉推理时再提到 0.25 左右。如果发现某个类别比如“禁止停车”总是被旁边的“禁止长时间停车”挤掉不要急着调iou_thres先看一眼两个类别的标注框是不是在标注阶段就画歪了。后处理本身不产生信息它只是把模型已经学到的信息按你给的规则筛一遍——所以规则给得对不对直接决定输出质量。2.3 Python 侧的训练管线为什么我坚持用官方仓库而不是魔改版YOLOv5 的官方仓库在 GitHub 上长期维护这本身就是它适合毕设的最大理由。用pip install -r requirements.txt就能装齐依赖Python 版本建议固定在 3.8~3.10torch 版本选和你的显卡驱动匹配的稳定版。许多同学喜欢去找“加了注意力机制”“优化了损失函数”的魔改源码我的经验是先跑通原版再谈魔改。原版仓库的训练、验证、导出三个流程是解耦的train.py产出的best.pt可以直接喂给val.py和export.py这个链路非常干净。魔改版往往改坏了某个配置文件你连报错都分不清是代码问题还是自己环境问题。这里有一个很实在的点YOLOv5 的detect.py --weights best.pt --source xxx.jpg这条命令能让你在拿到模型后五分钟内看到可视化结果这在写毕设进度报告时非常有用。每调一轮参数就把验证集里典型的十几张图跑一遍看检测框的覆盖情况而不是只盯着一张 mAP 曲线图。后面第 6 章我会讲我怎么把这套流程固化成一个简单的验证脚本。3. 把数据准备做扎实TT100K 与自建数据集的取舍、目录结构与标注转换脚本3.1 公开数据集怎么选TT100K 和 CCTSDB 各自的边界在哪里交通标识识别领域常用的公开数据集主要有 TT100K腾讯交通标志数据集和 CCTSDB长沙理工交通标志数据集。TT100K 包含 1 万张左右的真实街景图标注了上百个类别但它有一个很坑的地方类别数量多且分布极不均衡。限速标志有上万实例“禁止鸣喇叭”可能只有几十个样本。直接用全量 100 多类训练一是小类目根本学不出来二是类别之间外形相似比如限速 40、60、80模型容易在相似类上反复震荡。我的做法是降类。把 TT100K 整理成毕设要求的 8~12 个主类例如“限速”“禁止停车”“禁止通行”“注意行人”“路口优先”“施工”“公交专用”等。这样既保留了真实场景的复杂度又让模型在小样本类别上有足够的数据支撑。CCTSDB 的优点是标注格式统一、类别只有三大类警告、禁令、指示适合做快速基线但它的图片多为行车记录仪截图分辨率偏低直接拿来训练小目标检测效果不够好。我的建议是用 CCTSDB 做通线验证用整理后的 TT100K 做最终训练再加上一部分自采数据补足场景多样性。3.2 自建数据集的完整流程从爬图、清洗到标注的每一步怎么做自建数据集的流程没有捷径但可以按部就班做得很扎实。第一步是收集图片。你可以用手机在城市道路拍摄一段时间也可以从开源街景数据里截取。注意两点一是要覆盖不同光照条件黄昏和逆光场景对交通标识识别影响很大二是要保证同一类标识在画面里有不同的尺寸和角度避免模型只是记住了某个固定位置和大小。第二步是清洗。把模糊、过曝、目标占比过小的图片删除。第三步是标注推荐用 LabelImg它能直接输出 PASCAL VOC 格式的 XML 文件后续转 YOLO 格式很方便。标注时我建议遵循统一规则目标完全被遮挡超过三分之一就不要标两个同类标识挨在一起时各自独立标注标识在画面边缘且一半出画时按可见部分标注完整的框。这套规则能有效减少标注噪声——噪声是训练里最隐蔽的翻车原因模型学了错误的边界mAP 会低得莫名其妙。3.3 VOC XML 转 YOLO txt一个半小时就能写完的转换脚本与四个边界坑YOLOv5 需要的标签格式是每个图片对应一个 txt 文件每行是class x_center y_center width height坐标都归一化到 0~1。转换脚本的核心逻辑不复杂解析 XML 里的bndbox坐标除以图片宽高然后写入对应的 txt。我每次写这类脚本都会特别注意四个边界问题这里直接列成代码内的注释。import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, out_txt_path, class_mapping): tree ET.parse(xml_path) root tree.getroot() # 注意: 图片尺寸必须从 XML 的 size 节点读取, 不要用 PIL 现量 # 因为部分数据集图片带 EXIF 旋转信息, 直接读宽高会和实际标注对不上 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_mapping: # 不在映射表里的类别直接跳过, 而不是抛异常 # 毕设里常会遇到数据集标注了其他这类无效类别 continue cls_id class_mapping[name] xmlbox obj.find(bndbox) xmin float(xmlbox.find(xmin).text) ymin float(xmlbox.find(ymin).text) xmax float(xmlbox.find(xmax).text) ymax float(xmlbox.find(ymax).text) # 归一化前做边界裁剪: 有些标注框会超出图像边界 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) # 防止标注退化成一个点: 宽高小于 1 像素的框直接丢弃 if xmax - xmin 1 or ymax - ymin 1: continue x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) # class_mapping 示例: {speed_limit: 0, no_parking: 1, no_entry: 2 ...}这个脚本里四个边界坑分别是图片尺寸从 XML 读取而不是用 PIL 现量、类别映射表里缺失的类别直接跳过而非中断、标注框越界要裁剪、退化框要丢弃。每一个都对应我在实践中碰到过的真实事故——最惨的一次是某个数据集的 XML 里xmax比图片宽度还大转出来的 txt 里有一半框的坐标超过 1训练时 loss 直接发散。你在写自己的转换脚本时把这四条规则加进去能省掉后面排查数据的一大半时间。3.4 数据集目录结构与 train/val 划分一次到位避免训练中反复返工YOLOv5 对数据集的目录结构有约定建议直接按官方要求来后面data.yaml写路径时最省事。标准结构是images/train、images/val、labels/train、labels/val四个目录图片和标签文件名一一对应不含扩展名。划分比例我习惯用 8:2但有一个原则同一个场景的连拍帧必须全部放进同一个集合不能一部分在 train、一部分在 val。否则模型在训练时已经见过几乎相同的画面验证指标虚高答辩时被问“你怎么证明模型泛化能力”会很被动。划分完成后用下面这段代码快速自检一遍检查每个 txt 是否有对应的图片、坐标是否都在 [0,1] 区间、类别 ID 是否越界。这个自检步骤跑一遍只要几秒钟但它能避免后面训练到一半才发现标签错乱的尴尬。import os def validate_dataset(img_dir, label_dir, num_classes): img_files {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} label_files {os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith(.txt)} # 检查图片与标签是否能一一对应 orphan_labels label_files - img_files missing_labels img_files - label_files print(f缺失标签的图片: {len(missing_labels)}, 没有对应图片的标签: {len(orphan_labels)}) for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f), r) as fp: for line in fp: parts line.strip().split() cls_id int(parts[0]) coords [float(x) for x in parts[1:]] if cls_id 0 or cls_id num_classes: print(f类别越界: {f}) if any(c 0 or c 1 for c in coords): print(f坐标越界: {f})4. 训练自己的数据集yaml 配置、超参数含义与 loss 曲线判读方法4.1 从零改到能跑通的最小配置文件data.yaml 和模型 yaml 该怎么写用 YOLOv5 训练自己的数据集第一步是改两个 yaml 文件。第一个是数据配置文件告诉训练脚本你的图片和标签在哪个目录、有几类、类别名字叫什么。第二个是模型配置文件决定网络宽度、深度和 anchor 尺寸。很多毕设翻车就是 anchor 没改COCO 的默认 anchor 是给 80 类通用目标设计的应用到交通标识这种长宽比接近 1:1、偏小尺寸的目标上匹配率低训练前期收敛极慢。先看data.yaml的写法以data/tt100k_custom.yaml为例# 训练集和验证集的图片路径, 注意用绝对路径最稳 # 相对路径在 export 模型或换机器推理时容易失效 train: /home/user/datasets/tt100k/images/train val: /home/user/datasets/tt100k/images/val # 类别数量必须与标签文件中的最大索引1一致 nc: 10 # 类别名称顺序必须与转换脚本里的 class_mapping 顺序一致 names: [speed_limit, no_parking, no_entry, pedestrian, yield, stop, construction, bus_lane, warning, guide]nc和names是最容易出错的点标签文件里class_id是从 0 开始编号的如果你的类别有 10 类nc: 10对应的就是 0~9如果不小心把nc写成了最大类 ID 的数值比如 9训练时类别数少了loss 里的分类分支就会越界报错或者静默学习错误映射。names的顺序则直接影响可视化时框上显示的类别名和推理代码里的CLASSES列表必须一致。再看模型 yaml。以models/yolov5s.yaml为例官方给的基础配置文件里有一组可调参数depth_multiple控制网络深度width_multiple控制通道数。表格里列出来我常用的三组配置对显存和速度的影响模型配置depth_multiplewidth_multipleBatch16 时显存占用约推理速度V100, 约yolov5s0.330.506 GB2.5 msyolov5m0.670.7511 GB4.0 msyolov5l1.01.021 GB6.5 ms交通标识识别不是细粒度分类任务目标纹理简单yolov5s通常已经足够。如果检测小目标如 20×20 像素的远距离限速牌效果不佳优先增大输入分辨率而不是换更大的模型——分辨率提升对 mAP 的收益往往比参数量翻倍更明显。我一般会把yolov5s.yaml复制一份命名为yolov5s_custom.yaml只改nc为 10其他参数不动先跑一个基线版本再逐步调。4.2 训练命令与超参数拆解epochs、batch-size、img-size、lr 到底影响什么YOLOv5 的训练入口是train.py参数很多但真正需要理解透的也就五六个。我在命令行里跑过几十轮实验把最常改的参数列成一张表后面针对你的数据集可以直接抄参数我的默认值什么时候改改错会怎样--img640小目标多时改 768 或 896显存翻倍, 耗时变长--batch16根据显存调整, 显存不足改 8太大 OOM, 太小收敛慢--epochs100数据集小且简单可降到 60太少欠拟合, 太多易过拟合--datadata/tt100k_custom.yaml固定不说了, 写错直接报错--weightsyolov5s.pt想更快收敛用官方预训练权重不加载预训练权重收敛慢很多--hypdata/hyps/hyp.scratch-low.yaml调 lr 和增强参数乱改易发散常用的一条训练命令长这样python train.py \ --data data/tt100k_custom.yaml \ --cfg models/yolov5s_custom.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --img 640 \ --epochs 100 \ --workers 4 \ --project runs/train \ --name tt100k_v1这条命令里的几个关键点--workers 4是数据加载的进程数Windows 环境下如果报多进程相关错误改成--workers 0最稳Linux 下建议保持至少 2否则数据读取会成为训练瓶颈。--epochs 100配上早停策略YOLOv5 的patience参数默认 100 太大我一般显式设--patience 20意思是验证集指标连续 20 轮不提升就自动停止能省掉大量无效训练时间。损失函数的细节不需要死磕但你要知道它是三部分加权求和box 回归损失CIoU、目标置信度损失BCEWithLogitsLoss、分类损失BCEWithLogitsLoss。YOLOv5 的hyp.scratch-low.yaml里box: 0.05、cls: 0.5、obj: 1.0是默认权重我的经验是交通标识场景下cls可以适当提高到 0.7。因为这个数据集类别间差异较小限速 40 和限速 60 的图案只有数字不同分类分支需要更强的监督信号才能区分相似类别。4.3 训练时的三张图怎么看loss、mAP 和混淆矩阵训练结束后runs/train/{name}/目录下会生成一组曲线图和指标文件。很多同学只会看results.png里 loss 曲线有没有下降但这远远不够。我的习惯是按三步看训练是否健康。第一步看train/box_loss和val/box_loss如果 val loss 在某个 epoch 后持续上升而 train loss 还在降说明过拟合已经开始了应该考虑加数据增强或减小模型容量。第二步看mAP0.5和mAP0.5:0.95的差距如果前者很高比如 0.9但后者很低比如 0.4说明检测框位置不够精准锚框回归欠佳需要调高--img分辨率或者增加训练轮数。第三步看混淆矩阵confusion_matrix.png。这是最容易暴露数据问题的图。如果两个类别比如“禁止停车”和“禁止长时间停车”在混淆矩阵里交叉误检第一反应应该是去看这两个类别的标注质量而不是加训练时间。我在做 TT100K 时遇到过同一个标识在两张图片里一个标成“禁止停车”一个标成“禁止通行”这种标注冲突不改模型永远学不会。关于超参数搜索YOLOv5 提供了train.py --evolve做遗传算法调参但我建议毕设阶段不要碰。它需要多组并行训练单卡上跑一轮要十几个小时最后收益可能只有 1~2 个点的 mAP。更实际的调参路径是先跑基线 → 看混淆矩阵排查数据 → 改img和cls权重 → 再跑一轮这个循环两三轮就足够拿到体面的结果。5. 训练避坑指南5 个我反复踩过的坑按“现象 → 原因 → 解决”逐一给你排查方法5.1 损失值训到一半变 NaN先查标签和数据增强再查学习率现象训练进行到 20~30 轮box_loss突然变成nan曲线直接断崖之后所有指标归零。原因最常见的是标注数据里有“脏数据”——坐标全为 0、边界框宽高为负数或者转换脚本没做边界裁剪导致坐标异常。其次是数据增强开启后hyp里的hsv_h、degrees等某些极端增强组合让输入变成纯黑或纯白图像模型输出梯度爆炸。解决排查分两步。第一步跑我第 3 章给的数据校验脚本检查labels目录所有 txt 坐标是否在 [0,1] 区间、宽高是否为 0第二步把hyp.scratch-low.yaml里的hsv_h: 0.015、hsv_s: 0.7、hsv_v: 0.4先调低一半跑一个短实验看是否恢复正常。如果短实验仍然nan再把--lr从默认 0.01 降到 0.001。这个顺序是我排查时的固定套路每次都奏效。5.2 mAP 看着很高但实际预测一个小目标都找不到现象val.py报的mAP0.5有 0.85但拿一张真实街景图去detect.py预测远处的限速牌完全没框出来只检出近处的大标识。原因训练和推理的输入尺寸不一致以及后处理阈值设置不合理。train.py的--img 640会在训练时做随机缩放和裁剪模型见过不同尺度的目标但推理时detect.py默认--img 640如果你的测试原图是 1920×1080缩放到 640 后远处的小目标已经缩小到几个像素信息丢失严重。解决推理时提高缩放尺寸python detect.py --img 1280 --conf 0.25。另外确认train.py训练时是否开了--multi-scale如果开了模型对尺度变化适应力更强但训练时间明显变长。部署到边缘设备时比如 RK3588 上的 NPU 推理热门前沿里经常聊的 yolov5 量化小目标丢失会更严重量化前要先在整数化校准集里多放一些小目标图不然输出检测框会少到没法用。5.3 同一张图反复训练导致验证集指标虚高现象训练过程正常但val指标一路飙升比测试时手动目测的效果好得多。原因数据集划分时没有按“场景隔离”。比如你自采的视频序列每隔 5 帧保存一张图随机划分后训练集和验证集里出现了连续帧模型等于已经看过验证集的内容指标自然不真实。解决按时间或地点分组划分。视频抽帧数据按视频 ID 分组同一视频的帧全部进入训练集或验证集街景数据按街道或拍摄段划分。划分完成后用os.path校验 train 和 val 里是否有重复文件名的图片。这一步做扎实答辩时才有底气说模型泛化能力强。5.4 类别不均衡导致少数类压根没框现象混淆矩阵里样本数多的类别比如限速表现很好样本数少的类别比如施工recall 接近 0。原因YOLOv5 默认的类别损失是加权 BCE但权重和样本数量成反比的逻辑在这种高度不均衡的数据集上不够。TT100K 里施工类可能只占 1% 的实例模型为了降低整体 loss直接把它当背景忽略。解决先做类目合并如把不同限速值合成一个“限速”类减少类别数量再去hyp.scratch-low.yaml里加一个类别权重列表或者用最简单的办法——复制少数类样本在datasets.py里按类别做上采样。如果毕设答辩不需要区分限速具体数值合并类别是最省力且最有效的手段。5.5 Windows 上训练中途卡死GPU 利用率掉到 0%现象训练跑了十几个 epoch 后突然卡住不报错GPU 利用率归零只能强杀进程。原因YOLOv5 的数据加载用了torch.utils.data.DataLoader默认num_workers在 Windows 上可能触发多进程死锁。另外如果开了--cache把图片缓存进内存数据集太大时内存可能被吃满系统开始疯狂交换。解决Windows 下把--workers设为 0或者 2 但不要更高--cache建议只在图片总量小于 2GB 时使用。训练前的磁盘 IO 瓶颈也常见把数据集放到 SSD 上不要放在机械硬盘里——这个看着很不技术但真的能显著缩短训练时间。6. 让模型在答辩现场说清楚话推理可视化、置信度校准与边缘部署验证模型训练完不等于系统做完。毕设答辩或项目演示时你真正需要的是一个能让老师看懂“系统在哪里工作、哪里不工作”的推理脚本。我的习惯是绕过detect.py的默认输出方式单独跑一个可控的 Python 脚本把每张测试图的检测数量、各类别置信度分布、检漏情况全部打印出来。这个脚本的核心逻辑很简单加载best.pt对图片做letterbox预处理前向推理然后 NMS 后处理。import torch import cv2 from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords # 加载模型, 注意 device 要和导出格式匹配 model attempt_load(runs/train/tt100k_v1/weights/best.pt, map_locationcpu) model.eval() img0 cv2.imread(test_road.jpg) # letterbox: 保持宽高比缩放到 640, 不足部分填充灰边 img cv2.resize(img0, (640, 640)) # 简化的缩放逻辑 img_tensor torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 img_tensor img_tensor.unsqueeze(0) with torch.no_grad(): pred model(img_tensor)[0] # conf_thres0.25, iou_thres0.45 是推理默认值 det non_max_suppression(pred, conf_thres0.25, iou_thres0.45, classesNone) # 输出每个检测框的坐标、置信度和类别 for *xyxy, conf, cls in det[0]: print(f类别: {int(cls)}, 置信度: {conf:.3f}, 框: {xyxy})这个脚本可以作为毕设系统里的“推理核心模块”后面接 GUI、接视频流都是在这个结果上做文章。至于导出成 ONNX 或者 RKNN 做边缘部署那是另一条路我自己的实践是先确保 PyTorch 推理的 mAP 达标再做导出和量化。量化之后 mAP 掉 2~3 个点是正常现象不用慌关键是确认小目标类别掉得不算多。最后说一句这些年做检测项目的习惯每改一次参数就把训练结果目录名带上一两个关键参数比如tt100k_v1_img640_cls0.7留着以后回看。别都堆在默认的exp、exp2里。这个习惯让我少烧了很多时间重新对比实验希望也能帮到你。本文还有配套的精品资源点击获取
返回列表