news 2026/10/2 10:04:38

基于YOLOv5的交通标志识别实战:从数据转换到模型训练避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLOv5的交通标志识别实战:从数据转换到模型训练避坑指南

简介:基于YOLOv5的道路交通标志识别项目,以完整源码与配套数据集打包,专为毕业设计、期末大作业及课程设计打造。代码注释详实,从数据准备到模型训练、推理部署均有清晰说明,新手可快速上手;项目曾获导师高度认可的98分,部署门槛低,下载解压即可运行。资源共266个文件,包含YOLOv5模型配置(yaml)、训练权重(pt)、Python脚本(py)、标注图片(jpg/png)及数据集标签等,整体压缩包423.32MB,目录结构分明,便于逐模块学习与二次开发。目前已吸引439人学习下载,适合计算机视觉方向的学生参考其算法实现与工程组织方式,作为高分开题或答辩展示亦具说服力。

1. 交通标志识别为什么是 yolov5 的主场:毕业设计选型先把生态摸清楚

道路交通标志识别是目标检测方向里最常被拿来做毕业设计的真实场景之一,原因很直白:公开数据集好找、场景贴近实际生活、交付时能讲出完整的故事。yolov5 也是我接到这类选题时默认推荐的第一方案。它未必每一项指标都比新模型强,但它把“数据准备—训练—验证—导出部署”整条链路收得很干净,你搭出来的这套流程以后换到任何检测项目都能复用。这篇笔记从零走一遍:先跑通 yolov5 的最小推理,再把交通标志数据集转成训练格式,接着把训练、调参、踩坑、验收讲透。不管手里是 40 系显卡还是老款 1060,这套流程都走得动。

2. 跑通 yolov5 最小系统:环境配置、权重下载与第一次推理

2.1 conda 新建环境:这一步决定后面几天的心态

第一次装 yolov5 最容易翻车的就是环境。torch 装错 CUDA 版本,或者直接往 base 环境里堆依赖,后面 protobuf、dill、matplotlib 各种打架,能把人折腾到想换题。我一般会先按题目建一个干净的 conda 环境,Python 版本按官方仓库要求来,3.8 到 3.10 之间的常见版本都可以,然后按有没有 NVIDIA 显卡决定装 GPU 版还是 CPU 版 torch。CPU 版训练慢,但把流程跑通没问题,后面再租云 GPU 也不迟。

# 创建并激活独立环境,避免污染 base conda create -n yolo5 python=3.8 -y conda activate yolo5 # 按显卡情况安装 torch/torchvision,有 CUDA 就选对应版本,没有就装 CPU 版 pip install torch torchvision # 拉取 yolov5 源码并安装依赖 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt

这里真正要关注的是最后一行 requirements.txt,它会一次性装齐 numpy、opencv、pandas、tensorboard 这些训练和可视化依赖。常见错误是跳过这步,直接用自己的环境跑 detect.py,结果报No module named 'torchvision'或者 opencv 版本对不上。另外一个经验是装完依赖后用一行命令检查 CUDA 是否真的可用,省掉后面训练时才发现没用上 GPU 的时间。

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

如果输出末尾是 True,说明 torch 正确识别到了显卡;如果是 False,要么装成了 CPU 版,要么 CUDA 驱动不对。这一步排查成本很低,但能避免你训练一整晚后才发现全程在用 CPU 硬扛的尴尬。显卡驱动和 torch 的 CUDA 版本对应关系不复杂,但确实有点玄学,我的建议是:驱动别追最新,去 torch 官方安装页看自己驱动支持到哪个 CUDA 版本,再按那个索引装 torch。

2.2 第一次推理:用官方 yolov5s 验证链路通不通

代码和依赖都就位以后,先别急着碰自己的数据集,用官方预训练权重做一次推理,把整条链路验证通。这一步的意义是区分“代码环境问题”和“数据问题”,如果官方权重跑不出框,说明环境还有坑,不值得带着坏环境去训练自己的模型。推理命令极其简单,第一次跑会慢一点,因为要下载 yolov5s.pt 权重。

python detect.py --weights yolov5s.pt --source data/images/street.jpg --conf 0.4

这条命令里,yolov5s.pt 是官方提供的最小可训练权重,文件会放到 weights 目录;--source可以是一张图片、一个视频文件、一个目录,实战最常用的三种输入都在这;--conf 0.4是置信度阈值,低于 0.4 的预测框被丢掉。跑完后结果默认写在runs/detect/exp/,里面那张带框的图就是我们想要的输出。看到图里准确框出“人、车”这类 COCO 类别,说明模型文件、图像解码、NMS 后处理全部正常,可以进入数据阶段。

如果这一步卡住,绝大多数是 opencv 读取视频帧出错,少部分是 matplotlib 在无显示环境下保存图片异常。我一般会顺手加两个参数再跑一次:--save-txt保存每张图的坐标文本,--save-conf把置信度一起写入文本。这两个参数后面做指标分析时非常有用,现在就养成习惯,后面不慌。

python detect.py --weights yolov5s.pt --source data/images/street.jpg --conf 0.4 --save-txt --save-conf

2.3 交通标志数据集长什么样:VOC xml 与 yolov5 txt 的对应关系

yolov5 训练时读取的不是 xml,而是每个图片对应的同名 txt 标注文件。第 3 章会写完整转换脚本,这里先把格式差异讲清楚,因为后面所有翻车都从这里开始。

项目VOC 格式(xml)yolov5 格式(txt)
坐标表达绝对像素坐标 xmin、ymin、xmax、ymax归一化中心点 x_center、y_center、宽、高
数值范围0 到图片宽/高之间0 到 1 之间的小数
类别表达字符串名称,如 stop、pedestrian从 0 开始的整数编号
存储方式每个对象一个<object>节点每行一个对象:类别 x y w h

举一个实际例子,某个 VOC 标注片段长这样:

<object> <name>stop</name> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>190</xmax> <ymax>150</ymax> </bndbox> </object>

如果这张图宽 640、高 480,且 stop 在类别表里排第 2 位,那么对应的 yolov5 标签行是2 0.242187 0.239583 0.109375 0.145833。四个小数分别对应中心 x、中心 y、宽、高,全部是归一化结果。交通标志数据集的原始标注多半是 VOC 或 COCO 风格,所以转换时最容易错的不是加减乘除,而是忘了归一化,或把 xmax 减 xmin 的宽度算成高度。转换完一定要抽几张图把框画出来肉眼检查,这是所有后续训练的后悔药。

3. 把交通标志数据集转成 yolov5 格式:voc2yolo 脚本与标签校验

3.1 坐标换算逻辑:为什么中心点格式和 VOC 差着一层除法

VOC 的 bndbox 给的是左上角和右下角的绝对像素坐标,yolov5 要的是归一化中心点格式,换算公式并不复杂。设图片宽为 W、高为 H,有:

x_center = (xmin + xmax) / 2 / W
y_center = (ymin + ymax) / 2 / H
box_w = (xmax - xmin) / W
box_h = (ymax - ymin) / H

分母必须是图片的实际宽高,不是标注框的宽高,也不是模型输入尺寸的 640。这个细节看着不起眼,实际踩的人很多:有人把坐标换算成 0 到 1 之后,又拿模型输入尺寸 640 乘了一遍,结果所有框都缩成了极小值;也有人直接把 W 和 H 写反,训练出的框全部偏斜。转换脚本本身很简单,真正值钱的是转换后的自动校验,因为这类错误不会报异常,只会在训练曲线上慢慢折磨你。

3.2 转换脚本:一份能直接跑的 voc2yolo

下面这份脚本面向最常见的数据集组织方式:raw/annotations放 xml,raw/images放同名 jpg,输出写到labels/目录。类名单用固定顺序的列表维护,顺序一旦定下来就不要改。

# voc2yolo.py import glob import os import xml.etree.ElementTree as ET def convert_one(xml_path, txt_path, class_names): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) if img_w <= 0 or img_h <= 0: return False lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: # 遇到未知类别先跳过,但外层要能统计到,不能吞掉错误 continue cls_id = class_names.index(name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) # 裁剪到图像边界,防止坐标越界导致训练增强时报错 xmin = max(0.0, min(img_w - 1, xmin)) ymin = max(0.0, min(img_h - 1, ymin)) xmax = max(0.0, min(img_w - 1, xmax)) ymax = max(0.0, min(img_h - 1, ymax)) if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / 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(txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) return True if __name__ == '__main__': # 顺序必须和后面的 data.yaml 完全一致,这是最容易踩的坑 class_names = ['speedlimit-20', 'speedlimit-30', 'stop', 'pedestrian', 'warning', 'no_entry', 'yield'] os.makedirs('labels', exist_ok=True) total = 0 empty = 0 for xml_path in sorted(glob.glob('raw/annotations/*.xml')): base = os.path.basename(xml_path).replace('.xml', '') img_path = os.path.join('raw/images', base + '.jpg') if not os.path.exists(img_path): print('missing image:', img_path) continue txt_path = os.path.join('labels', base + '.txt') ok = convert_one(xml_path, txt_path, class_names) if not ok: print('bad annotation:', xml_path) if os.path.exists(txt_path) and os.path.getsize(txt_path) == 0: empty += 1 total += 1 print(f'processed {total}, empty {empty}')

这段代码值得展开讲几个点。第一,root.iter('object')用的是深层遍历,有些 xml 里嵌套了segmented或part节点,用findall('object')只会取到第一层,容易漏掉目标。第二,裁剪边界那四行不是多余的,公开数据集的框经常比图像宽高多出几个像素,不裁会让 yolov5 在 Mosaic 增强阶段直接报错。第三,empty计数很关键,如果某些类别不在class_names里被静默跳过,就会生成大量空 txt,而空标注文件在训练时等于告诉模型“这张图没有目标”,轻则浪费样本,重则把背景学成负样本。脚本跑完,processed和empty两个数字必须心里有数,empty占比超过 1% 就要回头排查类别命名。

3.3 训练集/验证集划分:随机种子、比例与文件搬运

转换完之后要把数据按比例拆成训练集和验证集。我的习惯是 8:1 到 9:1 之间,交通标志样本数量通常够用,验证集至少留 100 张以上,不然 mAP 曲线波动大得没法判断。划分脚本不难,难在“怎么分得科学”。

# split_data.py import glob import os import random import shutil random.seed(2024) # 固定种子,保证每次划分结果可以复现 imgs = sorted(glob.glob('raw/images/*.jpg')) random.shuffle(imgs) val_ratio = 0.2 val_num = int(len(imgs) * val_ratio) os.makedirs('dataset/images/train', exist_ok=True) os.makedirs('dataset/images/val', exist_ok=True) os.makedirs('dataset/labels/train', exist_ok=True) os.makedirs('dataset/labels/val', exist_ok=True) for idx, img_path in enumerate(imgs): base = os.path.basename(img_path)[:-4] src_label = os.path.join('labels', base + '.txt') if idx < val_num: shutil.copy(img_path, 'dataset/images/val/') if os.path.exists(src_label): shutil.copy(src_label, 'dataset/labels/val/') else: shutil.copy(img_path, 'dataset/images/train/') if os.path.exists(src_label): shutil.copy(src_label, 'dataset/labels/train/')

划分这里有个隐藏问题:如果原始数据是视频抽帧,同一段路的连续几帧高度相似,随机洗牌后可能同一场景的帧同时出现在训练和验证集里,这叫“泄漏”。验证集看起来指标很高,但换一段新视频就崩。交通标志公开数据集大多是不同地点单独拍摄的图片,这个问题不严重;但如果你是自采视频抽帧,必须先把连续帧按视频片段分组,整组划分。举个例子:video01_0001.jpg到video01_0100.jpg是一组,划分时把组作为一个最小单位,而不是把单帧打散。划分完再打印训练和验证各自的图片数、标签数、每个类别的样本数,这一步能提前暴露漏拷 label 的问题。

3.4 data.yaml:类别名顺序决定训练结果能不能对齐

yolov5 训练时的数据配置集中在 data.yaml 里,格式如下:

# dataset/traffic_sign.yaml path: ./ train: images/train val: images/val nc: 7 names: 0: speedlimit-20 1: speedlimit-30 2: stop 3: pedestrian 4: warning 5: no_entry 6: yield

path是配置文件所在目录的基准路径,train 和 val 都相对它写,不要写绝对路径,否则换一台机器训练就要改一遍。nc是类别数,必须和 names 的长度一致。这里的 names 顺序必须和第 3.2 节转换脚本里的class_names完全一致,因为 txt 里只存整数 id,顺序错一个,整个模型的语义就错位了。比如 stop 排到第 0 位训练出来,模型记住的 0 号类别是 stop,而你推理时显示的第 0 位可能写成了 speedlimit-20。这类错误不报任何异常,只能靠推理阶段手动对比,一旦发现就要回炉重排标签。验证 data.yaml 最直接的方法是画图检查,用 opencv 把训练集图片和 label 框画在一起,随机抽 20 张看类别名和框是否对得上。

4. 训练自己的交通标志识别模型:预训练权重、超参数与曲线判读

4.1 预训练权重选 s 还是 m:精度、显存和训练时间的三角

yolov5 官方权重从 n 到 x 一溜排开,参数量和精度依次上涨。做交通标志识别这种不算太难的检测任务,我一般不推荐一上来就选最大的,实践中最常用的是 s 和 m 两档。yolov5s 显存占用小,训练速度快,6G 显存跑 batch 16 没有压力,适合先通流程;yolov5m 精度比 s 高一截,但显存和时间成本上涨明显,适合在 s 已经把流程跑通、准备冲最终指标时换上去。交通标志本身是小目标居多,模型容量稍微大一点确实有帮助,但前提是数据没有大问题,否则就是拿显卡电费买教训。

预训练权重还有个容易被忽略的用法:当你自己标注的数据量不够时,先从公开交通标志数据集训一版,保留这个权重,再用自己的数据继续微调。这比每次从 COCO 预训练权重开始效果更直接,因为 COCO 的 80 类里没有专门的交通标志语义,模型要花更多时间去理解什么是“限速牌”。迁移学习不是玄学,它解决的是“数据量不够时模型先从大模型偷学特征”的问题,交通标志识别这种任务很适合这个套路。

4.2 训练命令与必调超参数:batch、epoch、imgsz 和 hyp 文件

一切就绪后,训练命令长这样:

python train.py \ --data dataset/traffic_sign.yaml \ --weights yolov5s.pt \ --epochs 120 \ --batch-size 16 \ --imgsz 640 \ --workers 4 \ --device 0

逐个拆开说。--data指向第 3 章写的 data.yaml;--weights是预训练权重,最好先下载到本地再填路径,避免训练到一半联网拉权重失败;--epochs交通标志这类数据集 100 到 150 轮足够,再多就有过拟合倾向;--batch-size越大 loss 曲线越平滑,但受显存限制,6G 显存配 batch 16 比较中庸;--imgsz是训练输入分辨率,交通标志小目标多,条件允许就上 640 甚至 1280,显存不够再往下退;--workers是数据加载进程数,Windows 上设成 0 反而更稳,Linux 下 4 或 8 都没问题。

yolov5 的超参数默认值在data/hyps/hyp.scratch-low.yaml里,做交通标志识别我一般会单独建一个 hyp 文件调三处:学习率、色域增强、mosaic 开关。一个常用的修改版长这样:

# data/hyps/hyp.traffic.yaml lr0: 0.005 # 初始学习率,默认 0.01,数据规模小时调低更可靠 lrf: 0.01 # 最终学习率 = lr0 * lrf,余弦退火的终点 momentum: 0.937 weight_decay: 0.0005 hsv_h: 0.015 # 色调扰动,标志颜色本身是语义,不能扰得太狠 hsv_s: 0.7 # 饱和度扰动,提高对褪色标志的适应 hsv_v: 0.4 # 亮度扰动,模拟逆光和阴影 degrees: 0.0 # 旋转设为 0,交通标志不会倒着出现 translate: 0.1 # 平移扰动,让目标出现在不同位置 scale: 0.5 # 缩放扰动,模拟远近变化 mosaic: 1.0 # mosaic 增强默认开,对小目标训练帮助明显 mixup: 0.1 # mixup 比例,太大会让标志边缘变糊

把文件传给训练命令:python train.py ... --hyp data/hyps/hyp.traffic.yaml。

这里要强调两个方向。一是交通标志的颜色是关键特征,hsv_h不能设太大,否则红色禁令牌被扰动成蓝色,模型会学歪;二是旋转扰动对标志这类刚性物体应该关掉,现实中标志不会横着长。很多人拿默认 hyp 直接训,效果也能看,但这类任务相关的细节是自己调优时真正值钱的地方。还有一个参数值得单独说:--multi-scale,它让模型在训练中随机使用不同输入尺寸,对交通标志这种尺度变化大的任务很友好,代价是训练时间变长、曲线更抖,适合数据量大的时候用。

4.3 训练曲线怎么读:loss、mAP 和过拟合信号

训练过程中,yolov5 会实时把指标写进runs/train/exp/results.csv和results.png。第一次训练的人最容易犯的错是只盯训练集 loss,看到 train/box_loss 一路下降就以为万事大吉,其实更要看的是 val 侧指标,以及最终训练输出目录里weights/best.pt和last.pt的区别。

正常曲线一般长这样:前 10 轮 loss 下降又快又陡,20 轮后逐渐平缓,mAP@0.5 上升后出现平台。如果 validation loss 先降后升,而 train loss 还在降,这是过拟合信号,解决办法不是继续加数据增强,而是提前停止、减小学习率或者减少 epoch。如果 mAP 从第一轮开始就在 0 附近抖动,先别调参,回头检查标签:十有八九是第 3 章的归一化或类别顺序问题。训练结束时会自动选 mAP 最高的权重存为 best.pt,后面所有推理、验证、导出都用它,不要顺手用了 last.pt,后者通常是最后一轮的权重,过拟合程度更高。看曲线时我还习惯把results.csv用 pandas 读出来,按轮次画精度的局部放大图,这样比直接看 results.png 更细。

import pandas as pd df = pd.read_csv('runs/train/exp/results.csv') print(df.columns) # 列出所有指标列 print(df[['metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', 'val/box_loss']].tail())

这样做的好处是能精确看到 mAP 从第几轮开始不再上升,方便你决定训练轮数,而不是盲目填一个 300 轮跑三天。

4.4 显存不够怎么办:降分辨率、调 nbs、用梯度累积

显存不够是这台课设最常遇到的事,尤其老显卡上跑交通标志的高分辨率大图。最直接的办法是把--imgsz从 640 降到 512 或 416,代价是小目标更难学,所以这只适合数据里目标本身不太小的场景。第二个办法是把--batch-size调小到 8 甚至 4,同时配合--nbs 64。

yolov5 的 nbs 参数是“名义 batch size”,它的机制是用梯度累积模拟大 batch:实际 batch 小,但每 N 步累加一次梯度再更新权重,效果上等价于一个大 batch。我这个例子把 nbs 设成 64,实际 batch 16 的话,模型相当于每 4 步做一次更新,既省显存又不牺牲太多训练稳定性。注意 loss 的放缩也因此变了,不要只看表面数值,对比 mAP 曲线才是判断依据。

5. 避坑手册:交通标志识别里 5 个典型的翻车现场与解法

5.1 坐标全部错乱的坑:xml 看起来没问题,txt 却全乱

现象:转换脚本跑完,抽查某一两张图,框的位置明显对不上物体,有的框跑到图外,有的把所有物体堆在左上角。训练时 loss 一开始就很大,验证集 mAP 始终在个位数徘徊。

原因:常见两类。一类是宽度写成(xmax - xmin)但忘了除以图像宽,导致框宽是归一化后的几百倍;另一类是图像本身存在 EXIF 旋转,VOC xml 里的宽高是按旋转前算的,而训练时读图和转换脚本读图用到了不同方向的尺寸信息。交通标志数据集很多从行车记录仪截取,EXIF 里的 Orientation 字段经常暗藏问题,转出来的框错位一点都看不出来,直到训练阶段 loss 爆炸。

解决:先用一个 40 行的小脚本把“原图 + 预测框”画出来,随机抽 20 张肉眼检查。一旦发现错位,回到原始图片用 PIL 读 EXIF 方向,统一按旋转后的宽高重算。我的习惯是转换前先把所有图片统一转成标准方向并覆盖写入,再跑转换脚本,从根上避免这个黑匣子。

5.2 小目标漏检:路牌明明在画面上,模型就是不报

现象:训练完成后 mAP 看着不小,但验证集中距离较远的限速牌、小型警告牌一个都检不出来,画面上 30×30 像素的小标志直接穿过。

原因:交通标志数据集里源图往往是 2048×2048 甚至更大的行车记录仪截图,直接 resize 到 640 之后,一个 30 像素的标志只剩 9 个像素,特征基本消失。模型不是不会检,而是这个目标在输入图上已经不存在了。这类小目标漏检是交通标志识别的标配问题,跟模型大小关系不大。

解决:最有效的手段是分块训练和分块推理。常见做法是维护一个滑窗裁切步骤,把大图切成 640×640 且带重叠的若干块,只保留中央区域的标注,最后汇总各块检测结果做去重。如果嫌麻烦,就把--imgsz提到 1280 直接训练,显存不够就配合第 4.4 节降 batch。这两个方向是交通标志小目标问题的两条主线,光靠调 conf 阈值解决不了问题。另外,训练时保持默认的 Mosaic 增强对小目标有增益,不要因为曲线波动就关掉,先跑完再判断。

5.3 逆光与夜间场景:数据增强让灯牌从背景里显出来

现象:白天测试效果挺好,换成逆光路段或傍晚场景,召回率直线下滑,标志牌上反光、背光,模型要么漏检要么把路灯当目标框出来。

原因:数据集大多在白天晴天采集,光照分布单一,模型学到的其实是“白日光照下的标志长什么样”,而不是“标志的语义形状”。逆光时标志牌的边缘对比度下降,YOLO 的深层特征提取不出来;夜间标志靠反光材质与背景区分,白天训练数据里没有这种对比模式。

解决:在数据增强上对症下药。一是调大 hyp 文件里的hsv_v和translate,并额外加对比度扰动,让模型见过更暗、更偏色的输入;二是如果数据集里完全没有夜间样本,值得单独采一小批夜间图混进训练集,人工标注几百张就够。增强不是越多越好,特别是把颜色扰得太狠会影响标志类别判断,这个度要在验证集上实测。实战中我会跑两组实验:一组默认增强,一组加了逆光扰动,对比逆光测试集上的 recall,数值说话。

5.4 类别极度不均:少样本类别 mAP 被拖到脚面

现象:val 输出的每个类别指标里,stop、限速牌这类常见类别 mAP 很高,少数类别如“禁止拖拉机”一类的 mAP 是 0,整个项目的平均指标被拖低一大截。

原因:公开交通标志数据集天然存在长尾分布。常见标志出现几千次,少数标志只有几十次,yolov5 的 loss 是按 batch 平均计算的,多数类别学得多、少数类别没机会学。这不是模型坏了,是统计规律。

解决:最可靠的是在数据层面做类别平衡,不要只在网络层面加损失权重。常见做法是把少数类别样本重复复制进训练集,或者对包含少数类别的图片做额外增强后扩充;如果自建数据,就注意采集时主动补采少数类别。另一个技巧是先在完整公开数据上预训练,再微调自己的少数类别。判断依据仍然是验证集上逐类打印的 P、R、mAP,不要只看总平均。训练完第一件事就是打印 per-class 指标,哪类最低就去查那类的训练样本数,是 30 张还是 300 张,心里要有数。

5.5 导出 onnx 后指标掉一截:预处理不一致的坑

现象:PyTorch 下推理 mAP 0.93,导出 onnx 后用 onnxruntime 重新跑同一张图,框的位置偏了几个像素,低置信度目标消失,指标掉了 5 个点以上。

原因:yolov5 推理用 letterbox 保持长宽比并把图填充到 640×640,导出 onnx 后用 opencv 或 onnxruntime 加载时,预处理做了两套,但填充值和归一化方式不一致。常见三种错位:PyTorch 侧用 BGR 而 onnx 侧用 RGB;归一化一个用 0~255 另一个用 0~1;letterbox 的填充位置左右算偏。每一步差一点点,叠加起来精度就掉得明显。

解决:把预处理统一成同一套函数,最实用的是在 onnx 推理侧复刻 letterbox 的完整逻辑,包括填充值、位置、比例,并把 imgsz 固定成训练时的值,不要用动态输入。导出后第一件事是画一张“PyTorch 结果 vs onnx 结果”的对比图,两个框完全重合再谈后续优化。我见过不少同学在量化上折腾半天,最后发现只是 BGR 和 RGB 的顺序没对齐。

6. 验证与交付:把交通标志项目从能跑变成能答辩

6.1 val.py 结果表怎么翻译:P、R、mAP 三类指标的解读

训练结束后的验证不是跑一遍 detect.py 看几张图就完事,答辩时老师一定会问“你这个模型到底好在哪里”。标准做法是用验证脚本在测试集上出指标表。

python val.py \ --data dataset/traffic_sign.yaml \ --weights runs/train/exp/weights/best.pt \ --task test \ --conf 0.4 \ --iou 0.5

输出表里最重要的三列是 precision、recall 和 mAP@0.5。precision 是检出的框里有多少是对的,recall 是所有真实标志里被检出多少,mAP@0.5 是不同置信度下 P-R 曲线的面积。对交通标志识别,我更看重 recall,因为漏检一个路牌比多画一个框后果严重得多。关键指标不要只看平均数,逐类打印后把最差的类别单独存图观察。

6.2 视频 demo 与结果导出:答辩现场演示的固定动作

答辩可能不允许现场训练,但可以现场跑推理。把测试视频放在项目根目录下,用最佳权重跑一次,同时导出坐标文本,这是演示的固定动作。

python detect.py \ --source test_road.mp4 \ --weights runs/train/exp/weights/best.pt \ --conf 0.35 \ --save-txt \ --save-conf

跑完之后runs/detect/exp/里会有一份带框的视频和每帧的 txt 坐标文件。如果时间充裕,我一般会把其中连续十几帧的检测结果落成一张大图,标注出真值和预测框对比,放进论文的实验分析章节里。

6.3 答辩前必查的三个检查点

第一个检查点:把随机种子固定,把测试集路径固定,连续跑两遍 val.py 结果完全一致,防止答辩现场复现时指标对不上。第二个检查点:把最低置信度设为 0.25 重新跑一遍 detect,确认视频 demo 不会因为阈值调太高经常漏检。第三个检查点:把每种失败样例单独截图,想清楚“为什么漏检”和改进方向,哪怕答辩时回答不上来,也能展示你做过验证。

我带课设时吃过最大的亏就是一阵猛训,对手里模型哪一类最差、为什么差完全没有印象,答辩时被老师问住只能支支吾吾。所以后来我养成一个习惯:训练结束先打印 per-class 指标表贴在代码旁边,再开始写论文,所有的改进方向都从表格里的最低项入手。这个习惯帮你把项目从“能跑”变成“能讲”,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 10:02:44

多目标麋鹿群优化算法MOEHO:ZDT测试与盘式制动器设计Matlab实战

搞过多目标优化的人&#xff0c;一定都有过这种经历&#xff1a;两个目标函数一个想让它越小越好&#xff0c;另一个也想让它越小越好&#xff0c;可它俩偏偏互相拉扯。我之前做一组工艺参数优化时&#xff0c;先是用线性加权把两个目标揉成一个&#xff0c;权重从0.1试到0.9&a…

作者头像 李华
网站建设 2026/10/2 10:01:51

大数据ROI怎么算?一套数据资产价值评估的实操方法论

"这套数据平台上线快一年了&#xff0c;投入了好几个人力、几台服务器&#xff0c;老板昨天开会问了一句&#xff1a;大数据ROI到底是多少&#xff1f;赚回本了吗&#xff1f;"这个问题&#xff0c;十个人里九个答不上来。不是因为数据不好&#xff0c;而是因为压根没…

作者头像 李华
网站建设 2026/10/2 10:01:51

用Laya微调7B模型:构建毫秒级System 1决策网关的实践

从去年年底开始&#xff0c;我一直在折腾一个实时决策网关&#xff0c;核心场景是&#xff1a;请求进来之后&#xff0c;系统需要在几十毫秒内完成意图判断、风险拦截、会话路由这类“低延迟但必须准确”的决策。最初我用的方案是串一个通用大模型API上去&#xff0c;效果虽好&…

作者头像 李华
网站建设 2026/10/2 10:00:41

Laplacian Loss:图像重建中高频结构保真的可微损失函数

1. Laplacian Loss不是“拉普拉斯滤波器”&#xff0c;而是图像重建里的隐式高频保真契约 你可能在论文里见过它&#xff0c;也可能在超分模型的损失函数配置里瞥过一眼——Laplacian Loss&#xff0c;名字带着数学家的冷峻气质&#xff0c;但实际作用远比字面更务实&#xff1…

作者头像 李华
网站建设 2026/10/2 9:59:43

UE5性能优化实战:不依赖超分,渲染预算分配实现3倍帧率

这次我们来看一个指向性非常明确的 UE5 优化挑战&#xff1a;不用超分辨率&#xff0c;把帧率提到接近 3 倍&#xff0c;并且用可复现的测试数据&#xff0c;正面回应社区里“不开超分就救不了 UE5 性能”的论调。项目标题里提到的“恶意开发者攻击”&#xff0c;在技术社区里通…

作者头像 李华