☰
3200张YOLO交通信号灯数据集实战:从数据校验到部署调优全链路指南
2026/10/15 8:51:45 网站建设 项目流程

交通信号灯检测这个方向,我在过去两年里断断续续做过四五个项目,从最早的自己拿手机拍视频标数据,到后来直接采购现成数据集做微调,踩过的坑不算少。这次拿到一份3200张规模的YOLO格式交通信号灯数据集,正好借这个机会把整个使用链路完整梳理一遍。3200张这个量级在目标检测里属于"够用但不宽裕"的档位,用好了能训出一个在常规城市场景下相当能打的模型,用不好就是过拟合、漏检、误检三件套齐活。下面我会从数据集本身的结构讲起,一路说到训练配置、评估指标解读、以及实际部署时那些文档里不会写的细节。不管你是刚接触YOLO的新手,还是已经训过几个模型想找找优化思路的老手,应该都能从里面捞到点东西。

1. 先搞清楚这3200张到底装了什么

拿到一个数据集,最忌讳的就是直接yolo train一把梭。我见过太多人连类别分布都没看就开训,最后模型对某一类几乎完全无响应,回头排查半天才发现那一类的样本只有几十张。所以第一步永远是"验货"。

1.1 目录结构与标注格式的确认

标准的YOLO格式数据集通常是这样的组织方式:

dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

images和labels下的文件名必须一一对应,只是扩展名不同(图片是.jpg或.png,标签是.txt)。这一点看起来是废话,但我确实遇到过有人把标签文件命名成img_001_label.txt,结果训练时一个标签都读不到,loss直接不下降,还以为是学习率的问题。

每个.txt标签文件里,每一行代表一个目标框,格式是:

<class_id> <x_center> <y_center> <width> <height>

后面四个值都是归一化到0到1之间的浮点数,相对于图片的宽高。这里有个新手极容易搞错的点:x_center和y_center是框中心点的坐标,不是左上角。我早期自己写标注脚本时就犯过这个错,把左上角坐标当中心点写进去,训出来的框整体偏移了半个框的距离,可视化一看全是歪的。

交通信号灯这个场景,类别定义通常不会太复杂,常见的是按颜色分:红灯、绿灯、黄灯,有的数据集还会细分箭头灯、圆形灯、倒计时数字等。3200张的规模,如果只分3类,平均每类上千个实例,算是比较健康的分布;如果分了七八类,那就要警惕长尾问题了。

1.2 用几行代码把数据分布摸清楚

在动手训练前,我习惯先跑一段统计脚本,把类别分布、框的尺寸分布、每张图的平均目标数都算出来。这一步花不了十分钟,但能帮你避开后面几天的无效调参。

import os from collections import Counter import matplotlib.pyplot as plt label_dir = "dataset/labels/train" class_counter = Counter() boxes_per_image = [] widths, heights = [], [] for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname)) as f: lines = [l.strip() for l in f if l.strip()] boxes_per_image.append(len(lines)) for line in lines: parts = line.split() cls = int(parts[0]) w, h = float(parts[3]), float(parts[4]) class_counter[cls] += 1 widths.append(w) heights.append(h) print("类别分布:", class_counter) print("平均每图目标数:", sum(boxes_per_image) / len(boxes_per_image)) print("最大目标数:", max(boxes_per_image))

跑完之后重点看三个数:类别是否均衡、平均每图目标数、框的宽高分布。交通信号灯数据集有个典型特征——框普遍偏小。因为信号灯在整张图里占的面积本来就不大,归一化后的宽高经常在0.02到0.1之间。这个特征直接决定了后面训练时的输入分辨率选择和anchor设置策略,后面会细说。

提示:如果发现某个类别实例数少于总数的5%,基本可以判定为长尾类别,训练时需要针对性做重采样或者用focal loss这类对难样本加权的损失函数。

1.3 可视化抽查:别跳过这一步

统计数字只能告诉你"有多少",不能告诉你"标得对不对"。我强烈建议随机抽20到30张图,把标注框画上去看一眼。用matplotlib或者opencv都行,代码很短:

import cv2 import random img_dir = "dataset/images/train" samples = random.sample(os.listdir(img_dir), 20) for name in samples: img = cv2.imread(os.path.join(img_dir, name)) h, w = img.shape[:2] label_path = os.path.join("dataset/labels/train", name.rsplit(".", 1)[0] + ".txt") if os.path.exists(label_path): with open(label_path) as f: for line in f: cls, xc, yc, bw, bh = map(float, line.split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow("check", img) cv2.waitKey(0)

这一步能抓出很多隐蔽问题:框是不是偏了、有没有漏标、有没有把路灯误标成信号灯、夜间图片的框是不是画在了错误位置。我在一个项目里就发现过标注员把一整批夜间图片的框全部标到了图片左上角,原因是他们的标注工具在处理暗光图片时坐标映射出了bug。这种问题不抽查根本发现不了,等模型训出来效果差再回头查,浪费的时间是抽查的几十倍。

2. 为什么交通信号灯检测比想象中难

很多人觉得信号灯检测是个"简单任务"——不就是三个圆嘛。真做过就知道,这个场景的难点相当集中,而且每一个都直接影响你的训练策略选择。

2.1 小目标问题是绕不过去的坎

前面统计框尺寸时已经看到了,信号灯的归一化宽高经常在0.05以下。在YOLO里,如果输入分辨率是640×640,一个0.05宽度的框换算成像素只有32个像素。而YOLO的下采样倍率通常是32倍,也就是说在最小的特征图上,这个目标可能只占1个格子甚至不到。这就是典型的小目标检测困境。

解决思路有几条,我按实际效果排序:

  • 提高输入分辨率:把imgsz从640提到1280,小目标的像素面积直接翻四倍。代价是显存占用和训练时间大幅上升,但效果提升通常最明显。3200张的规模,用1280分辨率训练在单张24G显存的卡上是完全可行的。
  • 调整特征金字塔:YOLOv8及之后的版本默认用了P3、P4、P5三层特征图,P3是8倍下采样,对小目标相对友好。如果目标特别小,可以考虑增加P2层(4倍下采样),但这会显著增加计算量。
  • 切图训练:把大图切成小块分别训练,等效于提高了目标在单张输入中的占比。缺点是会破坏目标的完整性,信号灯可能被切一半,需要小心处理边界。

我个人的经验是,对于3200张这个规模,优先用1280分辨率,性价比最高。如果显存不够,退而求其次用960,再配合mosaic增强,效果也能接受。

2.2 光照和天气带来的域偏移

交通信号灯是户外目标,光照条件变化极其剧烈。白天强光下信号灯可能过曝成一个白点,夜间则是高亮的灯珠配上漆黑的背景,黄昏和黎明时段又有复杂的散射光。如果数据集中这几种场景的分布不均衡,模型很容易在某一类光照下失效。

我建议在数据统计阶段就按光照条件给图片分个组,粗略分成白天、夜间、黄昏/黎明三类,看看比例。理想情况下三类都应该有足够的样本。如果夜间样本特别少,训练时就要针对性地对白天图片做亮度、对比度的增强,模拟夜间效果,或者干脆在推理阶段对夜间场景单独调阈值。

2.3 遮挡与截断的普遍性

城市道路上的信号灯经常被树枝、广告牌、前车遮挡,或者只露出一半。这类样本在标注时怎么处理,直接决定了模型的行为。常见做法有两种:

  • 遮挡超过50%的不标
  • 只要人眼能辨认出是信号灯就标,哪怕只露出一角

两种做法没有绝对对错,但必须全数据集统一。我见过一个数据集里两种标准混用,结果模型对遮挡目标的置信度忽高忽低,完全没法用。拿到数据集后,如果发现标注标准不统一,要么统一重标,要么把有歧义的样本直接剔除。

3. 训练配置:从data.yaml到超参数

数据集验完货,接下来就是配置训练。这一部分我会给出具体的配置文件和参数解释,你可以直接抄,但更重要的是理解每个参数为什么这么设。

3.1 data.yaml的写法与路径陷阱

path: /home/user/datasets/traffic_light train: images/train val: images/val test: images/test names: 0: red 1: green 2: yellow

这里有几个坑:

  • path用绝对路径最稳妥,相对路径在不同工作目录下启动训练时容易出问题。
  • train和val是相对于path的路径,不要写成绝对路径,否则path就失去意义了。
  • names的索引必须从0开始连续,不能跳号。如果写成0、1、3,训练时类别3的样本会被当成无效标签。

另外,val和test的区别要搞清楚。val是训练过程中用来监控指标、做早停和调参的,test是最后评估用的,训练全程不应该碰。我见过有人把val和test设成同一个目录,然后拿val上的指标当最终结果汇报,这本质上是一种数据泄露,指标会虚高。

3.2 输入分辨率与batch size的权衡

前面说了小目标要用高分辨率,但分辨率和batch size是此消彼长的关系。以YOLOv8为例,在24G显存的卡上:

分辨率最大batch size(近似)训练速度小目标效果
64064快一般
96032中较好
128016慢好

batch size太小会导致训练不稳定,BN层的统计量不准。如果显存实在不够,可以用梯度累积来模拟大batch:设batch=8,accumulate=2,等效于batch=16。YOLO的训练脚本一般支持nbs(nominal batch size)参数,会自动处理梯度累积。

我的建议是:优先保证分辨率,batch size用梯度累积补。因为分辨率直接决定小目标能不能被看到,而batch size的影响可以通过其他手段缓解。

3.3 数据增强的取舍

YOLO默认开启的增强包括mosaic、mixup、随机缩放、随机翻转等。对于交通信号灯,我的经验是:

  • mosaic:保留,但把mosaic的概率从默认的1.0降到0.5左右。mosaic会把四张图拼成一张,对小目标检测有帮助,但交通信号灯场景下拼接后可能出现不合理的信号灯组合(比如一张图里同时出现红灯和绿灯在同一方向),可能引入噪声。
  • mixup:建议关闭或调低。mixup把两张图按透明度叠加,对信号灯这种颜色敏感的目标不太友好,叠加后颜色会失真。
  • HSV增强:保留,而且可以适当加大色调(hue)扰动的范围。因为信号灯的核心特征是颜色,让模型对色调变化更鲁棒是有益的。但要注意,如果色调扰动过大,红灯可能被调成橙色,反而有害。一般hsv_h=0.015是默认值,可以试到0.02。
  • 随机翻转:水平翻转可以保留,垂直翻转要谨慎。现实中的信号灯不会上下颠倒,垂直翻转会引入不真实的样本。
  • 随机缩放:保留,这对小目标检测很重要,能让模型适应不同距离的信号灯。

3.4 损失函数与类别不平衡

YOLOv8用的是TaskAlignedAssigner做正负样本分配,分类损失是BCE,回归损失是CIoU加DFL。对于交通信号灯这种类别数少、但可能存在长尾的场景,可以考虑:

  • 如果某一类样本特别少,在data.yaml里没法直接设类别权重,但可以通过过采样来平衡。具体做法是把少样本类别的图片在训练集中重复若干次,或者用WeightedRandomSampler。
  • 如果误检特别多(把路灯、车尾灯误判成信号灯),可以适当提高分类损失的权重,或者在推理时提高置信度阈值。

我做过一个对比实验,同样是3200张数据,不做任何类别平衡处理的模型,对黄灯的召回率只有0.62,做了过采样后提升到0.81。黄灯样本少是交通信号灯数据集的通病,因为黄灯持续时间短,采集到的样本天然就少。

4. 训练过程中的监控与调参

配置好之后开训,但开训不等于放手不管。训练过程中的指标变化能告诉你很多信息,关键是要会看。

4.1 看懂YOLO输出的那些指标

YOLO训练时会输出一长串指标,新手容易看花眼。重点盯这几个:

  • box_loss / cls_loss / dfl_loss:三个损失分量。box_loss管框的位置回归,cls_loss管分类,dfl_loss是分布焦点损失,也是回归的一部分。正常情况下三个都应该稳步下降。如果cls_loss下降很慢甚至不降,说明分类有困难,可能是类别不平衡或者特征区分度不够。
  • precision / recall:精确率和召回率。交通信号灯场景下,recall通常比precision更重要,因为漏检一个红灯的后果比误检一个要严重得多。如果recall偏低,优先排查是不是小目标漏检。
  • mAP50 / mAP50-95:mAP50是IoU阈值0.5时的平均精度,mAP50-95是0.5到0.95多个阈值下的平均。后者更严格,更能反映框的定位精度。交通信号灯的框通常比较规整,mAP50-95和mAP50的差距不应该太大,如果差距超过0.2,说明框的定位不够准。

4.2 过拟合的识别与应对

3200张数据说多不多,说少不少,过拟合是常见问题。判断过拟合的标志是:训练集loss持续下降,但验证集loss开始上升,或者验证集mAP在某个epoch后不再提升甚至下降。

应对手段按优先级:

  1. 增加数据增强强度:这是最直接有效的。调高mosaic概率、增加缩放范围、加入随机裁剪。
  2. 早停:设patience=20左右,验证集指标连续20个epoch不提升就停。
  3. 减小模型规模:如果用的是YOLOv8l或x,可以换成m或s。交通信号灯这个任务的特征相对简单,大模型未必比小模型好,反而更容易过拟合。
  4. 权重衰减:适当提高weight_decay,从默认的0.0005提到0.001。
  5. Dropout:YOLO的分类头部分可以加dropout,但效果因任务而异。

我自己的经验是,交通信号灯任务用YOLOv8m或YOLOv8s就足够了,用l以上的模型在3200张数据上几乎必然过拟合。模型容量要和数据量匹配,这是铁律。

4.3 学习率调度的小技巧

YOLO默认用余弦退火加warmup。对于3200张的小数据集,我建议:

  • lr0(初始学习率)设0.01,不要用默认的0.01以上。小数据集上大学习率容易震荡。
  • warmup_epochs设3到5,让模型先稳定下来再进入正常训练。
  • lrf(最终学习率因子)设0.01,让学习率在训练末期降到初始值的1%。

如果训练过程中发现loss剧烈震荡,先把学习率降一半试试。如果loss下降太慢,再考虑适当提高。调学习率是最有效的调参手段之一,但也是最容易调过头的,建议一次只动一个参数,观察几个epoch再决定下一步。

5. 评估与部署:训练完之后的事

模型训完,mAP看着不错,但这只是开始。真正决定项目成败的是部署后的表现。

5.1 在测试集上做一次诚实的评估

训练时用的val集和最终评估用的test集必须分开。在test集上跑一次评估,看每个类别的AP:

yolo val model=runs/detect/train/weights/best.pt data=data.yaml split=test

重点关注每个类别单独的AP,而不是只看总体mAP。总体mAP高但某一类AP特别低的情况很常见,比如红灯和绿灯都很好,黄灯拉胯。这时候要针对性地补充黄灯样本,或者对黄灯单独调阈值。

另外,建议按光照条件分组评估。把test集分成白天、夜间两组,分别算mAP。如果夜间mAP明显低于白天,说明模型对夜间场景的泛化不够,需要补充夜间数据或做针对性增强。

5.2 导出ONNX与推理速度优化

训练完的.pt文件在部署时通常要转成ONNX或其他格式。YOLO的导出命令很简单:

yolo export model=best.pt format=onnx opset=12 simplify=True dynamic=False

几个关键点:

  • opset建议用12或以上,兼容性好。
  • simplify=True会调用onnx-simplifier做图优化,能减小模型体积、提升推理速度。
  • dynamic=False表示固定输入尺寸。如果部署场景中输入尺寸固定,设False能让推理引擎做更好的优化;如果需要动态尺寸,设True。

导出后建议用onnxruntime跑一遍,对比ONNX和PyTorch的输出是否一致。我遇到过一次导出后精度下降的问题,排查发现是某个算子在不同opset下的行为差异,换opset后解决。

推理速度方面,交通信号灯检测通常要求实时性。在GPU上,YOLOv8s在640分辨率下能跑到几百FPS,1280下会降到一百多,仍然够用。如果在边缘设备上部署,可能需要量化到INT8,但量化会带来精度损失,需要重新评估。

5.3 实际部署中的阈值调整

训练时的默认置信度阈值是0.25,IoU阈值是0.45。但实际部署时这两个值需要根据场景调。

  • 置信度阈值:如果漏检多,降低到0.15或0.2;如果误检多,提高到0.4或0.5。交通信号灯场景我一般设0.3左右,兼顾两者。
  • IoU阈值(NMS):信号灯之间通常不会重叠,所以NMS的IoU阈值可以设低一点,0.3到0.4,避免把相邻的信号灯误删。

还有一个实用技巧:按类别设不同阈值。红灯的阈值可以设低一点(比如0.2),宁可误检不可漏检;绿灯的阈值可以设高一点(比如0.4),因为绿灯误检的后果相对轻。这个策略在实际项目中很有效,但需要根据具体业务需求来定。

6. 几个我踩过的坑和对应的解法

最后这部分是我个人在交通信号灯检测项目里踩过的真实坑,写出来希望能帮你省点时间。

坑一:标签文件里有空行或空格行。YOLO读取标签时对空行比较敏感,有的版本会直接报错,有的版本会静默跳过但导致图片和目标对不上。建议在训练前用脚本清洗一遍,去掉所有空行和首尾空格。

坑二:图片和标签文件名大小写不一致。在Linux下这是两个不同的文件,在Windows下不区分。如果数据集是在Windows上整理的,拿到Linux上训练可能一半的标签找不到。统一转成小写最保险。

坑三:验证集里混入了训练集的图片。这种情况在手动划分数据集时很容易发生,尤其是图片文件名相似的时候。建议用哈希值去重,确保train和val没有重叠。数据泄露会让验证指标虚高,掩盖真实的过拟合问题。

坑四:忽略了图片的EXIF方向信息。手机拍的图片经常带有EXIF旋转信息,用PIL读取时如果不做处理,图片可能是横着的,但标注是按正常方向标的,导致框全部错位。用ImageOps.exif_transpose处理一下就好。

坑五:训练时用了默认的cache=True,但内存不够。YOLO默认会把图片缓存到内存或磁盘来加速训练。3200张1280分辨率的图片,缓存到内存可能需要十几G。如果内存不够,训练会莫名其妙地卡住或崩溃。设cache=False或者用cache='disk'。

坑六:模型在雨天或雾天场景下几乎失效。这是域偏移的典型表现。如果部署环境有这类天气,训练数据里必须包含相应样本,否则再好的模型也没用。数据决定了模型的上限,这句话在交通信号灯检测里体现得淋漓尽致。

坑七:忘了固定随机种子。做对比实验时,如果每次训练的随机种子不同,指标波动可能比你的改动带来的影响还大。设seed=42,保证实验可复现。

坑八:导出ONNX后忘了同步更新预处理。训练时的预处理(归一化、resize方式)必须和推理时完全一致,否则精度会掉。特别是resize,训练时用的是letterbox(保持长宽比补边),推理时如果直接用普通resize,框的位置会偏。

这些坑里,前四个是数据层面的,中间两个是训练配置层面的,最后两个是部署层面的。数据层面的坑最隐蔽也最致命,建议在训练前花足够时间做数据清洗和校验。训练配置的坑相对好排查,因为报错信息通常比较明确。部署层面的坑最容易被忽略,因为训练时一切正常,上线后才暴露。

交通信号灯检测这个任务,说难不难,说简单也不简单。3200张的YOLO数据集是一个很好的起点,但数据集只是原料,最终模型的效果取决于你怎么用它。从数据校验、增强策略、分辨率选择到部署调优,每一个环节都有优化的空间。我个人的体会是,与其花大量时间调模型结构,不如把精力放在数据质量和训练配置的合理性上,前者的收益往往更大。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询