最近手头有个设备检测类的小项目,需要从零构建一套图像识别能力。数据是现场拍的设备仪表画面,场景杂、角度乱、光照不稳定,没有现成的公开数据集可用。评估一圈下来,我决定用YOLOv12作为检测主干,自己标注数据、自己定训练策略,最后把模型接入到实际检测流程里。
这篇文章就把整个链路掰开来讲:为什么选YOLOv12、自建数据集怎么做标注和划分、训练参数怎么配、中途踩了哪些坑,以及部署推理时要注意什么。不管你是第一次碰目标检测,还是已经跑过YOLO系列但没试过自建数据集,应该都能在这里面找到能直接抄作业的部分。
1. 为什么在这个项目里选YOLOv12
1.1 YOLOv12的升级点落到实际场景意味着什么
YOLOv12最核心的变化是把注意力机制引入了原本以卷积为主的检测架构里。相比之前的版本,它在不显著增加计算量的前提下,让模型能够同时关注局部纹理和更大范围的上下文信息。这在我们这种复杂背景下的小目标检测场景里特别有用:仪表、开关、指示灯这类目标往往只占画面很小一部分,周边还有管线、设备框架、阴影等干扰,纯卷积结构很容易把注意力浪费在大片背景上。
从实测来看,YOLOv12在中小目标上的召回率提升是最明显的。这不是说它的精度一定碾压所有老版本,而是它在“算力成本”和“精度收益”之间的平衡做得更好。我自己的判断是,如果项目对实时性有要求,同时检测目标又偏小、背景又偏乱,YOLOv12值得认真试一轮。
1.2 自建数据集和公开数据集完全是两条路线
用COCO这类公开数据集的时候,数据是现成的,标签也相对规整,你需要操心的大多是训练参数。自建数据集则是另一套玩法:图片采集、清洗、标注、校验、划分、增强,每一环都可能直接决定最终精度的上限。
我这次项目里的待检测目标有三类:仪表读数区域、开关状态、指示灯亮灭。现场环境存在大量逆光、遮挡、角度倾斜,这导致前期的数据策略必须围绕“还原真实场景”来设计,而不是简单堆图片数量。很多项目模型效果上不去,回头一查不是算法问题,而是数据没整理明白。
1.3 模型选型上的关键取舍
做选型时我对比了YOLOv8n、YOLOv11n和YOLOv12n三个小体量版本。从架构复杂度上看,YOLOv12引入注意力结构后,对小目标的语义信息提取能力确实更强,尤其适合我们需要检测的目标周围有大量相似背景的情况。
我最终选的是YOLOv12n作为基线模型,原因很直接:一是训练成本可控,普通消费级显卡也能跑;二是n系列参数量小,后期部署时对设备性能的要求不会太高;三是如果精度不够,可以随时切换到s或m版本,升级路径平滑,不太需要改数据侧的东西。
2. 自建数据集:从采集到标注的关键步骤
2.1 采集环节决定了模型能学到的上限
这一步最容易被低估。你希望模型在真实场景中稳定工作,采集数据时就必须尽量覆盖真实场景中的各种变化。我总结了一份采集检查表,每次现场采集都按这个标准过一遍:
| 检查项 | 具体要求 | 经验解读 |
|---|---|---|
| 角度覆盖 | 每个目标至少包含正面、侧面、俯视、斜视四类角度 | 模型无法理解没见过的视角 |
| 光照覆盖 | 包含强光、逆光、阴影、暗光、夜间补光场景 | 光照变化会显著影响检测稳定性 |
| 距离覆盖 | 目标在画面中呈现大、中、小三种尺寸 | 小尺寸样本不足会导致漏检 |
| 遮挡覆盖 | 部分目标被手、工具、管线等遮挡 | 遮挡样本不用太多,但必须有 |
| 状态覆盖 | 开关的开/关、灯亮/灭成对采集 | 类别不均衡往往从这里开始 |
采集数量上,我的经验是每个类别至少准备500张有效图片,三类目标总计1500张以上。如果你场景复杂,宁可增加到每个类别800张,也不要把希望寄托在训练技巧上。数据层面的先天不足,后面所有调参努力都是补丁。
2.2 清洗环节不要省略
很多人采集完图片直接标注,这是一个常见的坑。原始图片里往往有大量模糊、重复、误拍内容,例如快速手持拍摄造成的运动模糊、长时间无人值守堆积的相似帧、拍到了无关区域。这些图片如果不处理,标注时浪费人工,训练时还会干扰模型学习。
我的做法是分两步清洗:第一步看缩略图,快速删除完全不可用的图片;第二步逐张检查,重点看目标是否清晰可辨、是否存在过度曝光或欠曝。对光照异常但目标尚可辨认的图片,我会保留,因为这类样本对提升模型鲁棒性有帮助。完全模糊、目标都无法区分的图片,直接删掉就好。
清洗后统一图片尺寸。这次项目里拍摄图片原始分辨率是1920x1080,为了兼顾训练速度和显存占用,我统一缩放到1280x720,保持宽高比不变,不做裁切。缩放这一步必须在标注之前完成,否则标注框坐标会和后续训练尺寸对不上。
2.3 标注工具的选型与规范制定
标注工具我用过不少,最终留下两个主力选择:LabelImg和Roboflow。LabelImg是本地运行,界面简洁,适合单机小批量标注;Roboflow在浏览器里协作方便,适合多人同时标注,还能顺带做数据增强。这次项目是单人标注,我用了LabelImg,流程简单直接。
| 工具 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| LabelImg | 单人本地标注 | 上手快,格式直接输出YOLO格式 | 批量管理能力弱 |
| Roboflow | 多人协作/云端数据集管理 | 内置增强,版本管理方便 | 免费额度有限,网络依赖 |
| X-AnyLabeling | 半自动辅助标注 | 支持模型预标注,效率高 | 需要一定环境配置能力 |
标注规范上,固定三条硬规则:
- 标注框必须紧贴目标外轮廓,宁可紧一点点,不要外扩一大圈。外扩框会把大量背景学进特征里。
- 目标被遮挡超过一半时不标注,小于一半遮挡时正常标注,但要在备注里记录。
- 所有类别统一使用矩形框,不用多边形。后续跑YOLO时矩形框是原生格式,省去转换麻烦。
标注完成之后,我还会用脚本检查一遍标注文件:图片是否都能对应到标签文件、标签中的类别编号是否都在配置范围内、是否有空标签文件。这些检查会写成一个简单的脚本批量执行,几百个文件几秒钟就能扫完。
2.4 数据划分与增强策略
划分数据集的原则是“按场景分组,而不是按图片随机切”。什么意思?如果你把同一个场景、连续几帧的图片一部分分到训练集,一部分分到验证集,验证集和训练集的相似度太高,测出来的精度会虚高。我用一个简单策略:先按拍摄场景分组,再按组整体划分到训练集、验证集和测试集,比例大致是8:1:1。
增强我主要依赖YOLO训练框架自带的在线增强,包括马赛克、随机翻转、随机色域变换、轻微旋转。离线增强只做了两类:亮度抖动和轻度噪声添加,目的是模拟现场光照波动。这里必须提醒一句:离线增强只对训练集做,验证集和测试集保持原始图像。否则验证结果会因为数据泄露而失真,我早期吃过这个亏,后面详细展开。
3. 训练配置与完整实施流程
3.1 环境准备与验证
训练环境我用的是Python 3.10 + CUDA 11.8,显卡是12GB显存。如果你显存只有8GB左右,下面的方案也能跑,只要把batch调小一点。
conda create -n yolo12 python=3.10 -y conda activate yolo12 pip install ultralytics安装完成后,先确认PyTorch能正常识别GPU:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))我会额外跑一个简单的推理测试,加载预训练权重对一张样例图片做检测,确保整个环境闭环没问题。这个步骤虽然简单,但能提前暴露很多环境问题,不用等到正式训练时再排查。
3.2 数据集目录结构与配置文件
我按照YOLO系列最通用的目录结构组织数据:
datasets/device_panel/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/images目录下放图片,labels目录下放对应的txt标注文件,文件名和图片名一一对应。这个结构不需要额外写转换脚本,直接能被训练框架识别。
对应配置文件custom.yaml如下:
path: /home/user/datasets/device_panel train: images/train val: images/val test: images/test nc: 3 names: 0: meter 1: switch 2: indicator这里nc是类别数,names是类别名称。yaml文件一旦配置好,后边基本不用动。我习惯把类别名称固定为英文小写单词,这样日志和导出模型里都能看得清楚,避免中文路径或中文字符的编码问题。
3.3 训练命令与参数选择
准备好数据集后,我用下面的命令启动训练:
yolo detect train data=custom.yaml model=yolo12n.pt epochs=120 imgsz=640 batch=16 device=0 patience=20 project=runs/exp_name这里每个参数都值得说清楚:
| 参数 | 本次取值 | 含义与选择理由 |
|---|---|---|
| model | yolo12n.pt | 加载COCO预训练权重做迁移学习,大幅缩短收敛时间 |
| epochs | 120 | 基础训练轮数,配合早停机制,实际跑到90轮左右停止 |
| imgsz | 640 | 输入尺寸,兼顾小目标检测能力和显存占用 |
| batch | 16 | 单卡12GB显存下的安全值,太大容易OOM |
| patience | 20 | 连续20轮验证集指标无提升则提前终止训练 |
关于预训练权重,我这里多说一句。自建数据集和COCO数据集类别完全不同,但底层特征仍然通用。通过预训练权重初始化,模型对边缘、纹理、形状等基础特征的提取能力可以直接继承,训练时只需适配新类别的语义信息。这是自建数据集训练里最有效的提速方式,没有之一。如果你是从零初始化训练,300轮可能都达不到预训练权重100轮的效果。
3.4 训练过程的监控与判断
训练启动后,我关注的核心指标不是训练集loss,而是验证集mAP50和mAP50-95。训练集loss只要是在下降,说明模型在拟合已有数据,但真正有意义的是在未见过的验证集上的表现。每次训练跑到一半,我会把训练日志里的指标曲线拉出来看,主要看两个信号:
第一个信号是mAP50曲线是否还在持续上升。如果曲线在中期就进入平台期,说明数据容量或模型容量可能到头了,这时我会考虑增加数据量或者升级模型版本。第二个信号是训练loss和验证loss的差距是否越拉越大,如果验证loss连续多轮上升而训练loss持续下降,这是过拟合信号,早停机制会帮忙截断,但我也需要检查增强策略是否太激进。
训练结束后,project目录下会生成weights文件夹,里面包含best.pt和last.pt。best.pt是验证集表现最好的权重,last.pt是最后一轮权重。后续推理和部署,一律使用best.pt,这个选择不需要犹豫。
4. 训练过程中的常见问题与排查笔记
4.1 验证集精度虚高:数据泄露的典型表现
我第一次训练时,验证集mAP50到了0.95,当时还挺高兴,结果一到新拍的现场照片上,效果惨不忍睹。排查完之后发现问题出在增强环节:我把亮度抖动和噪声添加做成了离线增强,并且作用到了所有图片上,包括验证集和测试集。由于验证集也经过了相同的增强变换,模型看到的“验证分布”和增强后的训练分布高度一致,验证指标完全失去了参考意义。
正确的做法是让增强只作用于训练阶段,验证和测试时使用原始图像。YOLO框架自带的在线增强本来就只在训练时生效,不需要你额外干预。自定义的离线增强一定要严格限定在训练集目录内,并且处理完毕后检查一遍目录结构,避免文件被覆盖到验证集里。这个坑属于看不见但影响巨大的类型,建议大家在训练前就养成检查数据划分的习惯。
4.2 小目标检测效果差:提升输入尺寸是立竿见影的手段
训练中期,我测试了一组现场图片,发现远处的小尺寸仪表有大量漏检。检查标注质量没发现明显问题,目标本身尺寸太小成为主要矛盾。YOLOv12虽然强化了小目标特征提取,但输入尺寸直接决定了目标映射到特征图上的像素数。
我把imgsz从640提升到960,然后重新训练同样的数据,小目标的召回率提升非常明显。代价是显存占用上升,batch被迫从16降到了8。对于自建数据集里目标偏小的场景,优先拉高输入尺寸通常比堆训练技巧更有效。如果尺寸拉高后显存不够,我会先用640尺寸训练一个基线,再用960尺寸在基线上继续微调,这样能降低训练压力。
4.3 类别不均衡:少数类几乎学不到特征
项目里指示灯目标数量明显少于仪表和开关,初期训练结果显示指示灯类别的AP明显偏低。解决不均衡有两条路线:数据侧和算法侧。数据侧我做了过采样,把少类别的训练样本重复纳入训练过程,相当于间接提高了少类别在batch中的出现频率。算法侧我调整了cls_loss权重,让模型更关注分类损失,但实际操作下来数据侧的过采样更稳定,效果也更直接。
还要注意一个细节:不均衡不仅体现在图片数量上,还体现在单张图片中的目标实例数量上。有些图里只有1个指示灯,有些图里有5个,这会影响训练时正负样本的分布。我最后用脚本统计了各类别实例总数,把少类别实例数量过采样到了多数类别的0.8倍左右,训练效果有明显改善。
4.4 显存不足与训练中断
训练到中途遇到OOM是最常见的事故。输入尺寸、batch、模型版本三个因素叠加决定峰值显存占用。遇到OOM时,我最先做的是降batch,比如从16降到8,这通常能直接解决。如果还是OOM,再把imgsz从960降到640,或者把模型从s换回n。
实际操作中,我还会同时调整一下数据加载的worker数量。worker太少会导致数据加载变慢,GPU空转;worker太多会额外占用内存。我的经验是8到12个worker比较合适,具体取决于CPU核数和内存大小。也可以开启混合精度训练,让显存占用进一步下降,代价是训练过程可能有轻微的精度波动,但在YOLO系列上一般影响不大。
4.5 常用问题速查表
| 问题现象 | 可能原因 | 排查与处理 |
|---|---|---|
| 训练loss不降 | 样本标签混乱、学习率过高 | 抽查标签文件,降低学习率重训 |
| 验证mAP很低但训练loss正常 | 验证集分布和训练集差异过大 | 按场景划分,而非随机切片 |
| 小目标频繁漏检 | 输入尺寸偏小 | 提升imgsz,或使用切片推理 |
| 少类别AP忽高忽低 | 类别样本量太少 | 过采样、复制少类别样本 |
| 显存不足中断 | batch或imgsz过大 | 减小batch,开启混合精度 |
5. 推理部署与效果验证
5.1 模型导出与推理代码
训练完成后,我先用best.pt跑一遍测试集,确认指标稳定之后,再导出为ONNX格式用于部署。导出命令很简单:
yolo export model=runs/exp_name/weights/best.pt format=onnx imgsz=640ONNX格式的好处是跨平台兼容性好,后续转成其他推理后端都很方便。推理时我写了一个标准的单张图片检测脚本:
from ultralytics import YOLO model = YOLO("runs/exp_name/weights/best.pt") results = model.predict( source="test_img.jpg", conf=0.25, iou=0.45, imgsz=640, device="cuda" ) for box in results[0].boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f"类别: {results[0].names[cls_id]}, 置信度: {conf:.2f}, 坐标: {xyxy}")conf和iou是两个直接决定检测效果后处理参数。conf阈值设太低,会出现大量误检框;设太高,小目标容易被滤掉。我一般先用0.25起步,根据实际误检率和漏检率微调。iou阈值控制NMS时两个重叠框的合并力度,0.45是YOLO系列常用的保守值,如果目标密集可以适当降到0.35。
5.2 实际场景效果观察
模型接入真实检测流程后,我统计了一轮运行数据。在720p输入下,单帧推理速度大约稳定在15到20毫秒,足以满足实时检测需求。精度方面,仪表和开关的检测效果比较稳定,指示灯在光线正常的条件下表现也不错,但暗光环境下存在一定比例的漏检。这和数据分布强相关,因为现场采集阶段暗光样本比例偏低,后续补了一批模拟暗光数据重新训练,效果才明显好转。
这里还要提一个部署细节:摄像头采集的画面和训练时的画面如果存在色域差异,会直接影响检测效果。我的做法是上线前采集一段现场视频,跑一轮中间帧检查,重点关注颜色偏移和图像清晰度两个维度。如果发现问题,优先调整采集端的色彩参数,而不是重新训练模型,这条路径的修复成本低得多。
5.3 推理性能优化方向
如果项目后期有更大的并发或更严格的时延要求,可以优先考虑把ONNX模型再转换到更高性能的推理格式,同时在模型不变的前提下,使用半精度推理替代单精度推理。这样处理之后,单帧推理速度通常还能再提升30%以上,精度损失可以控制在很小范围内。
我在这个项目迭代过程中,有个很深的体会:模型结构升级带来的收益往往是有限的,真正拉开差距的还是数据质量和数据规范。同样的YOLOv12,数据整理做扎实了,小样本场景也能有不错的精度;数据乱糟糟,换再新的模型也救不回来。
如果让我只留一条建议给正在自建数据集的人,那一定是先固化数据规范,再开始批量标注。标注规范不确定就动手,后面返工的成本远高于换模型结构的成本。我下一步计划把同一套数据分别跑一遍YOLOv8和YOLOv12的对比实验,看看注意力机制的增益在不同数据规模下到底有多大差异,到时候再整理成一篇实测记录分享出来。