
简介这份YOLO系列算法目标检测数据集面向计算机视觉初学者与工业安全检测开发者专门用于安全防护服与护目镜佩戴识别数据集已划分训练集/验证集包含data.yaml配置文件可直接适配YOLOv5/v7/v8/v9/v10/v11等主流版本训练与验证。包内共计421个文件包括140张jpg原图、140个txt标注文件与140个xml标注文件同时提供两种标签格式YOLO格式的txt文件以类别和归一化中心坐标、宽高描述目标框VOC格式的xml文件便于在标注工具或其他框架中使用整个压缩包仅3.81MB轻量易下载。目前已有71人学习下载。该数据集标注完整、划分清晰适合作为安全防护/护目镜检测模型的入门练习数据也可用于算法对比测试或教学演示通过这套数据可快速完成环境配置、模型训练和指标验证省去自行采集与标注的时间。1. 用 YOLO 训练安全防护服与护目镜检测140 张带标签图够干什么建筑工地和高危车间的安全着装检查落到视觉算法上就是两个类别防护服safety vest和护目镜goggles。一份 140 张、已经带 YOLO 标签的 zip 压缩包听上去不够训一个能上线的模型但在迁移学习的框架下这个量级恰好能跑通完整流程并得到一个可用的预研级权重。你不需要从零训练YOLOv8 的 COCO 预训练权重已经认得“人”和“衣服”的底层特征140 张图要做的是把高层语义掰向“防护服”和“护目镜”这两个具体类别。这篇文章会从数据集核验开始依次覆盖 YOLO 格式转换、小样本训练参数设置、评估指标解读最后落到导出和部署推理整条链路都是可以直接照做的命令和代码。2. 数据集核验与 YOLO 格式转换先看清 zip 里有什么再谈训练拿到 zip 包之后直接解压训练是最容易踩坑的动作。140 张图里很可能混着不同分辨率、不同标注格式VOC XML、COCO JSON、YOLO txt甚至有一小部分图根本没有对应标签文件。YOLO 训练时缺失标签的图会被跳过但你不会立刻发现直到 val 结果的类别数不齐全才意识到问题。2.1 解压后的目录结构与完整性检查先看压缩包内部结构再解压避免直接炸到当前目录。unzip -l safety_dataset.zip | head -40这个命令只列出压缩包内容不释放文件。head -40看前 40 行能判断压缩包顶层目录是否规范。常见的问题是顶层直接散落 jpg 和 txt 文件没有 images/labels 的二级结构这种情况先手工归拢再继续。解压后做的第一件事是核对图像和标签的命名对应关系。mkdir -p safety_data unzip safety_dataset.zip -d safety_data cd safety_data # 统计 jpg 与 txt 数量 find . -name *.jpg | wc -l find . -name *.txt | wc -l如果图片 140 张txt 也是 140 个只能说明“标签文件数量对得上”不代表每个 txt 里的标注内容合法。逐行校验才是重点。2.2 标签合法性校验类别 ID、归一化坐标和边界检查YOLO 格式每一行的结构是class_id x_center y_center width height其中中心坐标和宽高都是相对图片宽高的归一化值范围应在 0 到 1 之间。如果标注工具导出的是像素坐标或者类别 ID 超出了类别总数YOLO 训练时会出现“标签加载失败但不中断”的诡异行为。下面的脚本做一次全量扫描import os from pathlib import Path img_dir Path(safety_data/images) label_dir Path(safety_data/labels) IMG_EXTS {.jpg, .jpeg, .png, .bmp} num_classes 2 # 0: 防护服, 1: 护目镜 for img_path in img_dir.iterdir(): if img_path.suffix.lower() not in IMG_EXTS: continue label_path label_dir / (img_path.stem .txt) if not label_path.exists(): print(f[MISSING LABEL] {img_path.name}) continue lines label_path.read_text().strip().splitlines() if not lines: print(f[EMPTY LABEL] {img_path.name}) continue for i, line in enumerate(lines): parts line.split() if len(parts) ! 5: print(f[WRONG FORMAT] {img_path.name}#{i}: {line}) continue cls, cx, cy, w, h int(parts[0]), *map(float, parts[1:]) if cls num_classes: print(f[BAD CLASS] {img_path.name}#{i}: class{cls}) for v, name in [(cx, cx), (cy, cy), (w, w), (h, h)]: if not (0.0 v 1.0): print(f[OUT OF RANGE] {img_path.name}#{i}: {name}{v})这段脚本里值得留意的两个判断类别 ID 超界数据集中混入其他项目的类别和坐标越界常见于手工标注或格式转换脚本写错。都属于“不致命但会拖低收敛速度”的脏数据。坐标值超过 1 的情况YOLO 训练时会将目标直接丢弃轻微超出比如 1.02时某些框架会裁剪但结果不具可复现性。2.3 数据集划分小样本下按目录划分而非随机切分140 张图的标准划分比例是 8:1:1即 train 112 张、val 14 张、test 14 张。YOLO 推荐的数据集目录结构不是把所有图片放在一个目录然后靠 train.txt 索引而是物理上分开放置safety_data/ ├── images/ │ ├── train/ # 112 jpg │ ├── val/ # 14 jpg │ └── test/ # 14 jpg └── labels/ ├── train/ # 112 txt ├── val/ # 14 txt └── test/ # 14 txt随机分割时注意一个细节同一个场景的连拍帧要分到同一个集合里。140 张图中大概率包含一个人物的多张连续帧如果 train 和 val 出现同一人不同角度的相似图像会虚高 val mAP且不反映真实泛化能力。建议按文件名前缀聚类后再切分前缀相同视为同一对象。import random from pathlib import Path imgs sorted(Path(safety_data/images).glob(*.jpg)) random.seed(42) random.shuffle(imgs) train_n int(len(imgs) * 0.8) val_n int(len(imgs) * 0.1) split_map {train: imgs[:train_n], val: imgs[train_n:train_n val_n], test: imgs[train_n val_n:]} for split, paths in split_map.items(): (Path(safety_data/images) / split).mkdir(parentsTrue, exist_okTrue) (Path(safety_data/labels) / split).mkdir(parentsTrue, exist_okTrue) for p in paths: p.rename(Path(safety_data/images) / split / p.name) label_src Path(safety_data/labels) / (p.stem .txt) if label_src.exists(): label_src.rename(Path(safety_data/labels) / split / label_src.name)random.seed(42)保证每次执行划分结果一致这在复现实验结果时很重要。划分逻辑说明先确定 80% 的 train 量再在剩余样本里取一半做 val、一半做 test得到的正是 8:1:1 比例。全部用rename移动而不是复制避免磁盘出现两份副本导致后续误用。2.4 检查类别平衡与标注框尺寸分布140 张图、2 个类别还可能出现“防护服 500 个框护目镜只有 80 个框”的极端不平衡。小数据集上类别不平衡的直接后果是模型对少数类偏向保守val 阶段护目镜的 recall 会明显偏低。统计框数量用一段简短的脚本即可from collections import Counter from pathlib import Path label_dir Path(safety_data/labels/train) counter Counter() area_bins Counter() for label_file in label_dir.glob(*.txt): for line in label_file.read_text().strip().splitlines(): cls, _, _, w, h map(float, line.split()) counter[int(cls)] 1 area_ratio w * h if area_ratio 0.01: area_bins[1% (小目标)] 1 elif area_ratio 0.05: area_bins[1%~5% (中目标)] 1 else: area_bins[5% (大目标)] 1 print(类别框数:, dict(counter)) print(面积分布:, dict(area_bins))area_ratio w * h计算的是归一化面积占比。如果护目镜的框大都落在1%区间而防护服在5%区间那么训练时的imgsz参数就要向高分辨率方向调640 或 800否则下采样后护目镜连 10 个像素都不到特征根本学不出来。这是一个在训练前就该做的决策点不要等 mAP 崩了才回头查。3. 小数据集迁移学习从 yolov8n 预训练权重起步配置 data.yaml 与关键超参数140 张图最大的风险不是学不会特征而是过拟合——模型把训练集背下来val 集上一塌糊涂。应对手段有两个迁移学习冻结底层以及激进的数据增强。这一章从 data.yaml 配置开始逐步把训练命令讲透。3.1 编写 data.yaml路径、类别名与验证集的角色在 safety_data 目录下创建一个名为safety.yaml的文件path: /absolute/path/to/safety_data train: images/train val: images/val test: images/test names: 0: safety_vest 1: gogglespath建议写绝对路径。Ultralytics 在处理相对路径时基于当前工作目录解析如果你从项目根目录切换到 safety_data 再切回会莫名报错 “dataset not found”。names中的类别顺序必须和标签文件里的class_id完全一致0 对应防护服、1 对应护目镜顺序错了 val mAP 会因类别错位而变成随机水平。这里 class_id 为 0 的类别名不要写成hard_hat这类相近但不相同的标签——类别名不仅写入日志还会写进导出模型的输出节点。后续做推理解析时名称错一个字CSV 报告里就会混入未知类别。3.2 训练命令与超参语义freeze、epochs 和 batch 怎么配合pip install ultralytics # 确保主环境有可用的 YOLOv8 实现 yolo detect train \ modelyolov8n.pt \ datasafety.yaml \ epochs100 \ imgsz640 \ batch16 \ freeze10 \ patience20 \ cacheFalse参数说明modelyolov8n.ptnano 版本约 320 万参数。140 张图撑不起 large 或 xlarge 的容量网络越大越容易在训练后期把小样本噪声当成真实分布学进去。nano 在这个数据量上最容易收敛且推理速度最快。若 val mAP 收敛后仍偏低再换yolov8s.pt做横向对比。freeze10冻结模型前 10 层这些层捕获的是边缘、纹理、色彩等通用视觉特征在 COCO 上已经学过不需要在小数据集上重新拟合还能大幅减少回传梯度时的显存和内存压力。epochs100小数据集 100 epoch 足够YOLOv8 默认的学习率调度会在训练末期把 lr 降到接近 0 来稳定权重。从 50 epoch 到 100 epoch 的收益增量通常比从 100 到 200 明显因为模型在 30~50 epoch 之后就开始进入震荡期。batch16显存不够时优先降到 8不要动imgsz。batch 减少只是梯度估计的方差变大而 imgsz 降低会直接牺牲小目标召回。patience20连续 20 个 epoch 验证 mAP 无提升则早停。防止后期过拟合且白耗算力。cacheFalse140 张图没必要缓存进内存如果机器内存紧张这个参数保持 False。训练启动后把注意力放在终端输出的三个数值上box_loss、cls_loss、dfl_loss。box_loss 是回归框的损失cls_loss 是分类损失。小数据集下这两者如果在前 20 个 epoch 内没有明显下降第一嫌疑是学习率太大导致梯度来回震荡第二嫌疑是标签有格式错误——回到第 2 章的校验脚本排查。3.3 数据增强小数据集上最值得动手的超参数YOLOv8 的数据增强参数在训练时不需要单独开启只要不显式置 0 就会默认生效。下面这份配置针对“防护服、护目镜”场景做了定制yolo detect train \ modelyolov8n.pt \ datasafety.yaml \ epochs100 \ imgsz640 \ batch16 \ freeze10 \ patience20 \ hsv_h0.015 \ hsv_s0.7 \ hsv_v0.4 \ fliplr0.5 \ mosaic0.5 \ mixup0.2增强参数的调整逻辑hsv_s0.7和hsv_v0.4饱和度与明度扰动适度调大。工地上不同品牌防护服颜色差异明显荧光黄绿和荧光橙在同一批数据里很常见色彩扰动帮助模型对这些颜色变化更鲁棒。注意hsv_h色调只给到 0.015防止把荧光黄变成普通黄从而学错颜色特征。mosaic0.5把 4 张图拼成一张训练输入显著扩大每个 batch 中的目标数量和背景多样性。140 张图的数据量下 mosaic 是收益最高的增强手段但概率设为 0.5 而不是默认的 1.0因为小目标在图块尺寸被压缩后辨识度骤降完全不裁剪的原始图像仍需要被模型看到。mixup0.2两张图按比例混合像素和标签。对护目镜这种纹理细节不多的小目标mixup 帮助有限但能改善背景混淆。小数据集的误区是把 mixup 开到 0.5 以上营造假数据效果通常是过拟合变成欠拟合。fliplr0.5水平翻转防护服和护目镜左右翻转后仍是合法样本保持 0.5 即可垂直翻转不建议开启。3.4 训练中的实时观察点与常见信号训练过程每 10 个 epoch 会打印一次 val 指标重点看50PPrecision和50mmAP50。在 140 张图的小样本上train loss 降得非常快前 10 个 epoch 可能就下降 60%但 val mAP 往往是锯齿状上升不用为单次波动焦虑早停会在持续不涨时自动结束。训练完成后的产物在runs/detect/train/目录weights/best.pt和weights/last.pt是两类关键输出。best.pt 基于 val mAP 保存last.pt 是最后一个 epoch 的权重。判断该用哪个时跑一次 val 对比两者如果 best.pt 与 last.pt 的指标差距小于 0.5%说明最后 20 个 epoch 处于稳态选 best.pt 即可这也符合常规项目交付习惯。现象原因排查动作train loss 下降但 val mAP 震荡不涨过拟合前兆模型开始背训练样本降低 epochs增大 mixup或增强泛化增强项护目镜 recall 低于防护服 20% 以上小目标 样本数量偏少增大 imgsz 到 800或在 val 中增加近距离帧训练到 20 epoch 所有 loss 均为 nan学习率过大或不稳定的增强参数检查是否有异常标签坐标降低增强系数的上限值4. 模型评估与漏检定位mj指标、混淆矩阵与类别不平衡的补救训练完成后不要只看打印出来的最后一个 mAP 数字就收工。小数据集的评估要回答的不是“整体好不到哪里去”而是“漏检到底集中在哪个类别、哪类尺寸”。4.1 用 val 模式跑出混淆矩阵和 PR 曲线yolo detect val \ modelruns/detect/train/weights/best.pt \ datasafety.yaml \ splitval \ conf0.25 \ iou0.5conf0.25表示置信度阈值低于此值的目标会被当作背景丢弃。iou0.5是 NMS 的 IoU 阈值同一物体被多个框覆盖时IoU 超过 0.5 的相邻框会被合并。命令执行完成后runs/detect/val/下会生成confusion_matrix.png、PR_curve.png、F1_curve.png以及带标注框的可视化图片。查看混淆矩阵时重点看矩阵中“护目镜预测成了背景”的格子数值——这是漏检信号比误检更危险因为工地上漏检一个护目镜可能意味着安全巡检系统整列放行。针对漏检目标的具体可视化可以用 val 模式另一个参数yolo detect val \ modelruns/detect/train/weights/best.pt \ datasafety.yaml \ splitval \ conf0.05把conf降到 0.05模型会输出置信度极低的检测框从中找到那些应该是护目镜但得分只有 0.2~0.4 的样本。这些就是“模型其实认出来了但不敢说”的困难样本是后续做针对性增强的核心素材。4.2 类别不均衡如何补救不多收集数据时的增强策略如果 val 显示护目镜的 recall 只有 60%而防护服有 90%在无法快速补充数据的约束下优先做两类操作。第一类是复制粘贴增强Copy-Paste。把护目镜实例裁剪出来随机粘贴到不包含护目镜的训练图上同时同步写入对应标签。OpenCV 实现版如下import cv2 import numpy as np from pathlib import Path src_imgs list(Path(safety_data/images/train).glob(*.jpg)) def paste_instances(src_mask, dst_img, dst_label, instance_count20): # 从数据集中收集护目镜 ROI 的坐标 # 在无护目镜图的目标区域贴图并追加 YOLO 格式标签行 for _ in range(instance_count): x_range, y_range random_safe_region(dst_img.shape, roi_w, roi_h) dst_img[y_range, x_range] src_mask dst_label.append(f1 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f})这段代码的可行性依赖一个前提训练集中有足够多“没有护目镜但有防护服”的图像作为背景板。粘贴时护目镜的尺寸要和目标区域匹配不要硬塞进 300×300 的像素区。注意复制粘贴增强不能直接作用到 val 和 test 上——评估集必须保持原始分布否则指标会虚高。第二类是调整损失权重。YOLOv8 没有直接暴露class_weight参数但可以在 dataset 配置里修改或通过loss_scale对少数类的分类损失加权。实际操作时更稳妥的做法是通过复制少数类样本来改变类别分布——把护目镜样本在训练集内重复 2 倍等价于类别重加权同时不需要改代码。4.3 可视化误差分析把失误框画在图上逐张看val 结束后runs/detect/val/里检出的标注图要真正打开看而不是只盯指标。逐张扫图的技巧是按置信度排序从低置信度看起。最低置信度的框是模型在边界况下做出的判断它们往往代表了最典型的缺陷比如护目镜反光导致纹理丢失、防护服被工具腰带遮挡导致中间断裂。如果看完整轮发现误检集中在人形上部区域可以检查训练集的标签是否把“护目镜”和“防护服”两个类别混在了同一个框里。标注不规范时YOLO 会在两类之间来回徘徊表现为 val 图上防护服的框不断跳动。此时需要回到标签文件修正那些同时包裹两个类别的框。5. 模型导出与部署推理ONNX/TensorRT 格式转换与批处理检测脚本训练评估完成后项目交付的关键一步是把模型从 PyTorch 权重导出为可部署格式。140 张图训练出来的模型文件 best.pt 约 10MB 左右但这个格式依赖 PyTorch 环境不适合直接嵌入生产服务。通常的做法是导出为 ONNX 做跨平台推理或进一步转为 TensorRT 在 NVIDIA GPU 上加速以适应实时视频流或边缘设备部署。5.1 导出 ONNX 并验证输出尺寸yolo export \ modelruns/detect/train/weights/best.pt \ formatonnx \ opset12 \ dynamicTrue \ imgsz640导出完成后目录下出现 best.onnx 文件。dynamicTrue允许推理时输入尺寸不固定为 640×640这在使用单张不同分辨率图像检测时非常有用。ONNX 模型的输出是形状为(1, 84, 8400)的张量其中 84 4 个边框坐标 80 个 COCO 类别概率但这个尺寸是 COCO 的 80 类设定。在本例中由于类别数只有 2最终输出维度不完全是 84需要用一个简单的脚本查看实际导出模型的输出形状netron工具也可以做可视验证。5.2 用 ONNX Runtime 写批处理推理脚本ONNX 格式的好处是不依赖训练框架直接使用 onnxruntime CPU 推理即可完成检测任务非常适合批量处理文件夹中的历史图片。以下脚本从 test 集逐张读取图片输出检测结果到本地目录import cv2 import numpy as np import onnxruntime as ort from pathlib import Path sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name input_shape sess.get_inputs()[0].shape # [1, 3, 640, 640] def detect(image_path): img cv2.imread(str(image_path)) ih, iw img.shape[:2] resized cv2.resize(img, (640, 640)) blob resized.astype(np.float32) / 255.0 # 归一化到 [0,1] blob np.transpose(blob, (2, 0, 1))[None] # HWC - NCHW outputs sess.run(None, {input_name: blob})[0] # [N, 6num_cls, 8400] for det in outputs[0]: cx, cy, w, h, score, cls_id det[:6] if score 0.3: continue x1 int((cx - w / 2) * iw) y1 int((cy - h / 2) * ih) x2 int((cx w / 2) * iw) y2 int((cy h / 2) * ih) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{int(cls_id)}:{score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return img out_dir Path(outputs) out_dir.mkdir(exist_okTrue) for p in Path(safety_data/images/test).glob(*.jpg): result detect(p) cv2.imwrite(str(out_dir / p.name), result)脚本里需要注意blob np.transpose(...)[None]这一步ONNX 模型期望 NCHW 布局而 OpenCV 读入是 HWC。score在输出中的位置因模型结构而异实际运行时建议打印outputs.shape确认维度分布不要直接照搬索引。置信度阈值 0.3 是 CPU 推理时的常用值现场部署在 GPU 且要求高召回时可下调到 0.15。5.3 TensorRT 加速与 AMD GPU 推理的现实选择低端边缘设备上ONNX Runtime 的 CPU 推理速度通常在 20 至 40 ms/帧。需要更极致的性能时用 TensorRT 将 ONNX 转换为 engine 格式可以将推理时间压到 50% 左右。trtexec --onnxbest.onnx \ --saveEnginebest.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:4x3x640x640--fp16使用半精度计算显存占用减半、速度提升但某些 GPU 上数值精度降低会对护目镜这类小目标召回产生轻微影响部署时要在真实视频流上比对确认。做GPU推理时需注意一个现实问题AMD 显卡不支持标准 TensorRT 加速做实验阶段可以顺畅训练但部署到 AMD GPU 上的官方支持仍然受限。常见做法是直接用 PyTorch 推理或等待 ONNX Runtime 对特定厂商后端的兼容更新工程上更稳妥的方案还是在 NVIDIA 环境做 TensorRT 部署。6. 使用 TTA 与伪标签半监督技巧把 140 张图的泛化能力再挤出 10%最后一个模型优化技巧是利用测试时增强Test Time Augmentation, TTA和伪标签Pseudo-Labeling迭代让有限的数据集发挥更大价值。YOLOv8 内置了简单的 TTA 推理参数yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest/ \ saveTrue \ augmentTrueaugmentTrue会让模型对输入图片做翻转、缩放等多尺度推理再融合输出结果。对于小目标的护目镜检测TTA 通常能将 recall 提升 3 到 8 个百分点代价是推理时间成倍增加。这一技巧只适合预研验证和离线分析实时检测场景下收益不值当。伪标签的玩法是这样的在未标注的工地监控视频帧上跑一遍 best.pt收集置信度超过 0.6 的检测框人工抽查确认没有明显错漏之后把它们当成真标签并入训练集再做一次 30 个 epoch 的微调。伪标签第 1 轮的价值在于把模型已经稳定的判断转为更多训练样本而不是解决模型没学会的类别。如果护目镜本来 recall 就低伪标签只会把低 recall 的分布扩大方向反了。一个小技巧做伪标签前先用第 4 章的 val 可视化确认模型在“近景、背光、逆光、遮挡”几类场景的能力边界。140 张图大概率没覆盖背光场景这时去采集一段包含逆光明暗变化的视频做伪标签比在相似亮度帧里做复制更有增量。伪标签生成的推理结果需要把输出格式转成 YOLO txt用第 5 章的 ONNX 推理脚本即可做到保留原图路径和归一化坐标直接写入 labels 目录。伪标签数量控制在原数据集的一半以内即不超过 70 张。超过这个比例人工复核成本上升且模型可能把早期固化错误的分布放大。合格的标准是伪标签样本加入后原 val 的 mAP50 不降再观察混淆矩阵里漏检格子是否变小。本文还有配套的精品资源点击获取