简介:YOLO无人机航拍输电线路绝缘瓷瓶数据集,面向计算机视觉与电力巡检方向的开发者、学生及算法工程师,提供真实场景下高质量绝缘瓷瓶图像与标注,可直接用于YOLO系列目标检测模型训练。压缩包内共1281个文件,包含421张jpg原图、421个xml(VOC格式)、428个txt(YOLO格式)标签,以及1个json(COCO格式)与yaml配置文件,覆盖三种主流标注规范,省去格式转换步骤;另附3个python数据集划分脚本和6个html环境搭建、训练教程,兼顾Linux与Windows平台。压缩包整体约941.93MB,结构清晰,便于按需取用。已有334人学习下载,配套博文还展示了数据集详情,适合需要快速获取电力巡线标注数据并入门YOLO训练的实践者。
1. 绝缘瓷瓶检测数据集:为什么说三种标签格式和划分脚本才是重点
第一次用 YOLO 跑无人机航拍输电线路绝缘瓷瓶检测的时候,我卡在最无聊的一步:标注格式。手里是 VOC 的 XML,模型要 YOLO 的 TXT,中途还想着给 COCO 留一份,三个格式之间的坐标换算和类别编号对齐,全是运行时才暴露的坑。这份数据集一次性把三种标签给齐,再配上划分脚本和训练教程,相当于把「准备数据→划分样本→跑通训练」这条链路直接打通。适合正在做电力巡检、输电线路缺陷检测的工程师,也适合准备拿 YOLO 练手的入门者。这篇文章就按格式、划分、训练、踩坑四个部分往下讲,每个环节都会配上可以直接复现的命令。
2. 读懂 VOC、COCO 和 YOLO 三种格式:坐标系、存储结构和转换边界
2.1 XML、JSON 和 TXT:三种格式各自怎么描述一个瓷瓶
VOC 格式是最「人类可读」的。每张图片对应一个同名 XML 文件,根节点annotation下面,filename写图片名,size里记录 width、height 和 depth,然后每个目标是一个object节点,里面的name是类别名,bndbox给四个值:xmin、ymin、xmax、ymax,都是像素坐标,表示框的左上角和右下角。如果你用 labelImg 标注过,导出 VOC 格式就是这套结构——代码里用 ElementTree 读 XML,拿到object列表就拿到全部检测目标。
COCO 格式则是一个汇总的 JSON 文件,把所有图片和所有标注集中管理。结构分两层:images数组里每个元素记录图片的id、file_name、width、height;annotations数组里每个元素记录这条框属于哪张图(image_id)、类别编号(category_id)、框坐标(bbox)——注意这里的bbox是[x, y, width, height],是左上角坐标加宽高,不是两个点。另外还有area和segmentation字段,segmentation在实例分割里才有值,检测任务里通常只是边框对应的多边形。
YOLO 格式最简单,也最容易出错。每张图片对应一个同名 TXT 文件,每行代表一个目标:class_id、x_center、y_center、width、height,五个数空格隔开。其中中心坐标和宽高都是相对图片宽高的归一化小数,范围 0 到 1。这种设计的好处是模型前处理时不用管输入图片实际是多大,网络输出和标签天然对齐;坏处是你肉眼直接看 TXT 根本看不出框在哪,一旦转换时坐标算错,训练出来的模型会非常「自信地」输出错误位置。
三种格式的本质区别可以归纳成三点:坐标表达方式(两点像素 vs 左上点加宽高像素 vs 中心点加宽高归一化)、类别表达方式(字符串 vs ID vs ID)、文件组织方式(一图一文件 vs 全量聚合 JSON vs 一图一文件)。你只要把这三条主线理清,剩下所有转换都是机械计算。
2.2 格式转换的坐标换算与三个常踩的坑
VOC 转 YOLO 的换算公式如下,分母必须是实际图片的宽高:
x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / heightVOC 转 COCO 则直接换算为左上角坐标加宽高:
x = xmin y = ymin box_w = xmax - xmin box_h = ymax - yminCOCO 转 YOLO 时,要把像素的左上角坐标和宽高换算成归一化的中心点:
x_center = (x + box_w / 2) / width y_center = (y + box_h / 2) / height box_w = box_w / width box_h = box_h / height公式本身不难,难的是转换过程里的三个隐蔽坑。
第一个坑是分辨率不一致。VOC 的 XML 里size字段标了图片宽高,但实际图片可能被预处理缩放、旋转,而 XML 没有跟着更新。归一化时如果直接拿 XML 里的size当分母,而实际图片尺寸已经变了,坐标整体偏移。我在这类数据上见过偏移超过 20 个像素的情况,模型照样能训,但框永远贴不紧瓷瓶边缘。正确做法是转换前用 Pillow 或 OpenCV 读一遍实际图片宽高。
第二个坑是category_id不等于class_id。COCO 的categories列表里id可以是从 1 开始的任意整数,而 YOLO 要求class_id从 0 开始连续递增。如果转换时不建映射表,直接把category_id当class_id,训练时所有类别的标签整体错一位。这种错误最阴险的地方在于 loss 曲线看起来完全正常,只是训练出来所有预测框的类别都是错的。
第三个坑是 VOC 的difficult标志。difficult=1表示难以识别的样本,转换到 YOLO 格式时一般直接丢弃。如果不做过滤,这些困难样本会被当成普通正样本送进训练,模型被迫去拟合那些标注本身就不确定的目标,精度会掉一截。
2.3 拿到数据集后的第一件事:验证标签与图片是否对齐
把 .rar 解压后的第一件事,不是急着写 YAML,而是检查三个格式的覆盖度。我一般用一小段 Python 把文件名集合的差集打出来看:
import os from pathlib import Path root = Path("解压后的目录") images = {p.stem for p in (root / "images").glob("*") if p.suffix.lower() in (".jpg", ".jpeg", ".png")} voc = {p.stem for p in (root / "Annotations").glob("*.xml")} yolo = {p.stem for p in (root / "labels").glob("*.txt")} print("图片数:", len(images)) print("VOC标签数:", len(voc)) print("YOLO标签数:", len(yolo)) print("缺VOC标注的图片:", sorted(images - voc)[:10]) print("缺YOLO标注的图片:", sorted(images - yolo)[:10])这段代码假设目录结构是images/放图片、Annotations/放 VOC 的 XML、labels/放 YOLO 的 TXT。如果两个差值列表不为空,说明部分图片没有对应标注。这类图片在训练时要挑出来,不能直接留在 images 里,否则 YOLO 在数据准备阶段要么报错,要么把没标签的图当纯背景处理,效果很怪。
COCO 不是一图一文件,不能按文件名比对,但要检查 JSON 的完整性:
import json with open("annotations/instances.json", "r", encoding="utf-8") as f: coco = json.load(f) print("images:", len(coco["images"])) print("annotations:", len(coco["annotations"])) print("categories:", coco["categories"]) img_ids = {img["id"] for img in coco["images"]} ann_img_ids = {ann["image_id"] for ann in coco["annotations"]} print("有标注但不在images里的image_id:", sorted(ann_img_ids - img_ids)[:10])categories打印出来就是类别 ID 和类别名的映射,后面写 YOLO 的data.yaml时直接参考。如果有标注但找不到对应图片记录,说明 JSON 和实际图片不一致,划分前要先修掉。我还习惯随机抽三张图做可视化,把标签框直接画在原图上——这一步能发现坐标偏移、类别标反、框超出图片边界这类「数据本身没错但转出来错了」的问题。
3. 用划分脚本切好训练集:随机种子、文件联动与 COCO JSON 重建
3.1 划分的三个核心逻辑:随机种子、文件联动、COCO 的 JSON 重建
划分数据看似简单,写个 random 打乱就行,但对带三种标签的数据集有讲究。
第一,随机种子必须固定。不固定种子,每次跑出来的 train/val 分配都不一样,实验对比就没有意义。你改一行代码重新跑,val 集变了,模型精度的变化就分不清是代码改进带来的,还是数据划分变了带来的。所以脚本里固定random.seed(42),42 换成什么都行,关键是固定下来。同样的种子和同样的数据,跑出来的划分必须完全一致,这是可复现实验的底线。
第二,划分要以图片为单位做文件联动。VOC 和 YOLO 的标签都是「一图一文件」,切分时要把图片和对应的 XML、TXT 一起移动或复制,不能只移图片。常见的错误是只移动了图片,但 labels 目录里还留着全部 1000 个 txt,训练时 YOLO 按图片名去找同名 txt,此时目录对不上,轻则空标签,重则训练集和验证集混进了本不该出现的样本,指标全部失真。
第三,COCO 格式没有独立文件,全量标注都在一个 JSON 里。划分时要先把 train/val/test 各自的图片 id 筛出来,再按 id 过滤annotations,重新生成三个子集的 JSON。手工做这一步不现实,必须脚本化。重建时categories字段要完整保留,否则后面做可视化时类别名全部丢失,只剩数字编号。
3.2 划分脚本:一次搞定三种格式
假设 .rar 解压后目录结构是这样的:
数据根目录/ ├── images/ # 全部图片 jpg/png ├── Annotations/ # VOC 的 xml,与图片同名 ├── labels/ # YOLO 的 txt,与图片同名 └── annotations/ └── instances.json # COCO 全量标注下面这个脚本我一直在用,覆盖图片、VOC 标签、YOLO 标签的同步复制,以及 COCO JSON 按子集重建:
""" 数据划分脚本:按图片名把三种标签同步切到 train/val/test 用法: python split_dataset.py --source 数据根目录 --output ./dataset \ --ratio 0.8 0.1 0.1 --seed 42 """ import os import random import shutil import argparse import json from pathlib import Path def parse_args(): parser = argparse.ArgumentParser(description="划分VOC/COCO/YOLO三种格式数据集") parser.add_argument("--source", type=str, required=True, help="解压后的数据根目录") parser.add_argument("--output", type=str, default="./dataset", help="输出目录") parser.add_argument("--ratio", type=float, nargs=3, default=[0.8, 0.1, 0.1], help="train/val/test比例,按 train, val, test 顺序给") parser.add_argument("--seed", type=int, default=42, help="随机种子") return parser.parse_args() def split_list(items, ratios, seed): random.seed(seed) items = sorted(items) random.shuffle(items) n = len(items) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) return items[:n_train], items[n_train:n_train + n_val], items[n_train + n_val:] def rebuild_coco(coco_file, images, split_name, out_dir): """按图片文件列表重建COCO子集JSON,images 是文件名列表""" with open(coco_file, "r", encoding="utf-8") as f: coco = json.load(f) keep_ids = set() split_images = [] for img in coco["images"]: if img["file_name"] in images: split_images.append(img) keep_ids.add(img["id"]) split_anns = [a for a in coco["annotations"] if a["image_id"] in keep_ids] sub = { "info": coco.get("info", {}), "licenses": coco.get("licenses", []), "categories": coco["categories"], "images": split_images, "annotations": split_anns, } sub_dir = out_dir / split_name / "annotations" sub_dir.mkdir(parents=True, exist_ok=True) with open(sub_dir / f"instances_{split_name}.json", "w", encoding="utf-8") as f: json.dump(sub, f, ensure_ascii=False, indent=1) def main(): args = parse_args() src = Path(args.source) out = Path(args.output) img_dir = src / "images" voc_dir = src / "Annotations" yolo_dir = src / "labels" coco_file = src / "annotations" / "instances.json" all_images = [p.name for p in img_dir.glob("*") if p.suffix.lower() in (".jpg", ".jpeg", ".png")] print(f"共 {len(all_images)} 张图片") train_imgs, val_imgs, test_imgs = split_list(all_images, args.ratio, args.seed) print(f"train={len(train_imgs)} val={len(val_imgs)} test={len(test_imgs)}") for split_name, imgs in [("train", train_imgs), ("val", val_imgs), ("test", test_imgs)]: for img_name in imgs: dst_img_dir = out / split_name / "images" dst_img_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(img_dir / img_name, dst_img_dir / img_name) xml_name = Path(img_name).stem + ".xml" if (voc_dir / xml_name).exists(): dst_voc_dir = out / split_name / "Annotations" dst_voc_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(voc_dir / xml_name, dst_voc_dir / xml_name) txt_name = Path(img_name).stem + ".txt" if (yolo_dir / txt_name).exists(): dst_yolo_dir = out / split_name / "labels" dst_yolo_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(yolo_dir / txt_name, dst_yolo_dir / txt_name) if coco_file.exists(): for split_name, imgs in [("train", train_imgs), ("val", val_imgs), ("test", test_imgs)]: rebuild_coco(coco_file, imgs, split_name, out) for split_name, imgs in [("train", train_imgs), ("val", val_imgs), ("test", test_imgs)]: print(f"{split_name}: {len(imgs)} 张图") if __name__ == "__main__": main()这个脚本的逻辑分四步走:第 1 步收集images/目录下全部图片文件名;第 2 步固定随机种子后打乱切分;第 3 步对每个子集,按图片名去Annotations/和labels/里查询同名文件并复制过去;第 4 步对 COCO 的 JSON 按image_id筛选重建三个子集。用copy2而不是move,是为了保留原始数据——划分之后如果发现比例不合适,重新跑一次就行,不用重新解压原文件,相当于给数据划分留了一份后悔药。脚本对无标签的图片不会报错,但最后汇总打印时你会看到数量差异,这时需要回头处理源数据。
参数方面,--source指向解压出的根目录,脚本自动认images、Annotations、labels、annotations/instances.json这套固定结构;--ratio按 train、val、test 顺序给三个数,数加起来不用等于 1,脚本按比例切;--seed默认 42,复现实验时用同一个 seed 跑出来的划分完全一致。几千张以内的数据量,这个脚本几十秒就结束,慢的不是脚本,而是后面训练。
3.3 划分后的三个自检命令
划分完不能直接开训,先做三个检查。
第一个检查数量比例,确认子集划分符合预期:
for d in train val test; do echo "$d images: $(ls dataset/$d/images | wc -l)" echo "$d yolo_labels: $(ls dataset/$d/labels | wc -l)" echo "$d voc_labels: $(ls dataset/$d/Annotations | wc -l)" done如果某个子集的图片数和标签数对不上,说明源数据里有图无标或有标无图的脏样本,先解决再训练。
第二个检查 COCO 子集的完整性:
import json for split in ["train", "val", "test"]: p = f"dataset/{split}/annotations/instances_{split}.json" with open(p, encoding="utf-8") as f: data = json.load(f) print(split, "images:", len(data["images"]), "anns:", len(data["annotations"]))重点看有没有「有 annotation 但没有对应 image」的悬挂记录。这类记录会导致 COCO 数据加载器在运行时报 KeyError,而且报错位置在框架内部,排错很费劲。
第三个检查类别分布,用 YOLO 的 txt 统计每个类的行数:
for split in train val test; do echo "== $split ==" cat dataset/$split/labels/*.txt 2>/dev/null | awk '{print $1}' | sort | uniq -c done正常情况三个子集里每个类别的样本占比应该接近,如果某个类在 test 里占比特别高、在 train 里几乎没有,模型对这个类别基本是瞎的。这个检查用一条命令就能看清类别分布有没有失衡。
4. 从环境到出图:用 YOLOv8 跑通绝缘瓷瓶训练全流程
4.1 环境配置:conda 创建与 ultralytics 安装
环境配置是很多人第一步就卡住的地方。用 Anaconda 建环境是常见做法,命令就三条:
conda create -n yolo python=3.10 -y conda activate yolo pip install ultralyticsPython 版本 3.8 到 3.12 都能跑 ultralytics,我习惯用 3.10,兼容性最稳。pip install ultralytics会把 torch、torchvision、opencv-python 等一系列依赖自动装上。装完先验证一下环境是否可用:
python -c "import ultralytics; ultralytics.checks()"如果你机器有 NVIDIA 显卡,再跑一句看 GPU 是否被识别:
python -c "import torch; print(torch.cuda.is_available())"输出True说明 CUDA 版 torch 已就位,训练可以用 GPU 加速;输出False说明装的是 CPU 版 torch。CPU 版不是不能训,只是同样的 100 轮,GPU 可能两小时跑完,CPU 要跑到第二天早上。如果你在 PyCharm 里建项目,记得给项目解释器选同一个 conda 环境路径,这样终端里能跑的训练命令,在 IDE 里也能直接跑。
4.2 数据 YAML:路径、类别名和 class_id 的对应关系
YOLOv8 训练前要先写一个数据配置文件,指向前面划分好的目录:
# 绝缘瓷瓶检测 - 数据配置 path: /绝对路径/dataset # 数据根目录,写绝对路径最省事 train: train/images # 相对 path 的训练图片目录 val: val/images # 相对 path 的验证图片目录 test: test/images # 相对 path 的测试图片目录,没有可删掉 nc: 2 # 类别数 names: 0: normal_insulator # 正常瓷瓶,名字要和 VOC/COCO 里一致 1: damaged_insulator # 破损/自爆瓷瓶path我建议写绝对路径。相对路径在换机器、换工作目录训练时经常出现「directory not found」的报错,排查起来玄学感极重。names里的索引必须和 YOLO txt 里的class_id一致,也就是上一章用awk统计出来的那个编号。如果你手里的数据集类别名和这个例子不同,以解压后labels/里实际的文件内容和 COCO 的categories为准,这个名字列表就是训练时标签和 loss 打印的对齐依据。nc可以省略,YOLO 会根据names数量自动推断。
训练前再确认一次类别编号合法性:这批 txt 里class_id的最小值应为 0,最大值应为nc-1。如果出现超出范围的值,训练会在数据加载阶段直接报错,不用等到跑起来才发现。
4.3 训练命令与关键参数:epochs、batch、imgsz 怎么选
数据配置就绪后,跑训练只需要一条命令:
yolo detect train \ data=insulator.yaml \ model=yolov8s.pt \ epochs=100 \ batch=16 \ imgsz=640 \ patience=30 \ project=runs \ name=insulator_exp1model=yolov8s.pt会从官方权重地址自动下载 COCO 预训练权重,第一次跑需要联网。如果你只是想快速验证流程通不通,换成yolov8n.pt,模型更小,训练更快;如果对精度要求高且显存充足,换yolov8m.pt。三个模型大小从 n 到 m 递增,最终 mAP 一般也递增,但训练时间同步翻倍。
epochs=100是个合适的起点。跑完看 mAP 曲线,如果第 100 轮还在缓慢上涨,就继续加大到 150 或 200。batch=16在 8G 显存下跑 yolov8s 大约够用,中途报 OOM 就改成 8 或 4。拿不准显存占用时,训练前看一眼nvidia-smi,别等报错了才去改。
imgsz=640对航拍瓷瓶这类小目标检测优先保持 640。跑通 baseline 之后,可以再试一次imgsz=1280,分辨率翻倍对小目标召回率通常有明显帮助,但训练时间会翻几倍,显存占用也翻倍。patience=30表示验证集精度连续 30 轮没有提升就自动早停,这个参数对不想死守完整 100 轮的人来说很省时间。
训练过程中的输出重点看三块:box_loss、cls_loss、dfl_loss 三条 loss 曲线的整体趋势,每轮结束时打印的 precision、recall、mAP50、mAP50-95,以及最后的权重文件。训练结束在runs/detect/insulator_exp1/weights/下会生成best.pt和last.pt,best.pt按验证集 mAP 择优保存,后面所有推理和部署都用它。训练完后随手跑一次推理,把测试图片过一遍:
yolo detect predict \ model=runs/detect/insulator_exp1/weights/best.pt \ source=dataset/test/images \ conf=0.25 \ save_txt=True这一步能看到模型在没见过图片上的真实表现,框有没有贴住瓷瓶边缘、有没有把杆塔构件当瓷瓶,基本一目了然。
5. 绝缘瓷瓶训练避坑清单:五条真实的翻车记录
5.1 图片有标签,但训练时总报 no labels found
现象:data.yaml 的路径都检查过,图片数量也能正常打印,但训练刚开始就报错,日志里提示 no labels found。这个问题在第一次跑这份数据集的人里出现频率最高。
原因:YOLO 找标签目录有一套默认规则。你用train: train/images指定了图片目录,YOLO 就会自动去同级的train/labels找训练标签,而不是去train/Annotations或随便什么自定义目录。如果你的标签目录命名或层级和默认规则不一致,它就直接判定为没有标签。
解决:确保train/images的同级存在train/labels,且 labels 里的 txt 与 images 里的图片同名。这也正是前面划分脚本把标签复制到split_name/labels而不是别的名字的原因——labels这个目录名是 YOLO 的默认约定,不要为了整齐改叫label或labels_new。
5.2 三种格式类别 ID 对不上,模型把破损瓷瓶当正常瓷瓶
现象:训练 loss 正常,验证集 mAP 也不差,但打开预测图发现破损瓷瓶全被标成正常瓷瓶,正常瓷瓶偶尔又被标成破损。
原因:从 VOC 转 COCO 时category_id可能从 1 开始,而 YOLO 要求class_id从 0 开始。数据集在生成三种格式时,如果某一处转换没有做减一映射,类别标签就会集体错位。这种问题在 loss 曲线上看不出任何异常。
解决:拿一张破损瓷瓶的图片,分别查看它的 XML、COCO JSON 里的category_id和 YOLO txt 里的class_id,三者核对一遍:
import xml.etree.ElementTree as ET import json # 以样例文件 insulator_001 为例 xml_tree = ET.parse("Annotations/insulator_001.xml") for obj in xml_tree.getroot().findall("object"): print("VOC name:", obj.find("name").text) with open("labels/insulator_001.txt") as f: for line in f: print("YOLO line:", line.strip()) with open("annotations/instances.json", encoding="utf-8") as f: coco = json.load(f) for img in coco["images"]: if img["file_name"] == "insulator_001.jpg": for ann in coco["annotations"]: if ann["image_id"] == img["id"]: print("COCO category_id:", ann["category_id"])如果 VOC 的 name 是damaged_insulator,COCO 的category_id是 1,而 YOLO 的class_id是 1,那就错了——YOLO 里 0 应该是normal_insulator,1 应该是damaged_insulator。修正时以 VOC 的name为基准,重建 YOLO 的 txt 和 COCO 的category_id映射,不要只改一个格式。
5.3 小目标瓷瓶漏检严重,mAP50 还可以但 mAP50-95 很难看
现象:瓷瓶在高空航拍图里只有一二十个像素,模型要么直接漏检,要么框显著偏大或偏小。mAP50 看着还行,mAP50-95 却明显偏低。
原因:YOLO 在 640 分辨率输入下,最深特征图的步长是 32,一个 20 像素的小目标在特征图上只有不到一个像素点,信息被压缩得所剩无几。小目标占比高的场景里,默认配置对小目标不友好,这是网络结构本身的约束。
解决:优先把imgsz升到 1280 试一次,效果通常立竿见影。如果显存放不下,用 SAHI 这类切片推理工具,把大图切成 640 的小块分别检测再合并结果。更根本的做法是训练阶段就做切片数据增强,把原始大图随机裁成小块再喂进模型。我一般先跑一个 640 的 baseline,再看验证集里小目标的 AP 表现,决定要不要上 1280。
5.4 训练 loss 不降甚至 NaN
现象:loss 第一轮就是 nan,或者前几轮正常后面突然变成 nan;另一种是 loss 一直挂在某个数值附近不动,怎么调都不降。
原因:标签坐标越界是最常见的元凶——归一化后 w 或 h 大于 1,或中心点坐标落在图片外。其次是学习率过高,超出优化器的稳定区间。batch 过小也会让 BatchNorm 的统计量抖动,导致训练不稳定。
解决:先把所有 txt 做一轮范围校验,过滤越界标签:
import glob for txt in glob.glob("dataset/train/labels/*.txt"): with open(txt) as f: for line in f: parts = line.split() cx, cy, w, h = map(float, parts[1:]) if not (0 < w <= 1 and 0 < h <= 1 and 0 <= cx <= 1 and 0 <= cy <= 1): print(txt, line.strip())如果打印出越界行,说明标签本身有问题,修正后再训,不要硬着头皮跑。如果没有越界且 loss 还是 nan,把学习率从默认的 0.01 降到 0.001 试一次,排除超参问题;batch 尽量提到 16 以上再观察。顺序是:先查标签,再降学习率,最后调 batch。
5.5 测试集指标虚高,部署到新航线上就翻车
现象:本地 val 和 test 的 mAP 都到 90% 以上,模型换到一片新线路、新天气的无人机航拍数据上,效果明显变差,误检漏检都上来了。
原因:划分时没做序列去重。无人机巡检的原始数据是按航线连续拍的,同一基杆塔的相邻十几帧画面高度相似。如果不按拍摄时间或位置分组,这些近乎重复的帧会被同时塞进 train 和 val,模型等于在背题,验证指标自然虚高。
解决:划分前先按拍摄序列分组,一组整体进 train 或进 val,不是按单帧随机分。如果数据集文件名的前缀带有序列信息,比如tower_001_frame_001这样的命名,直接用序列前缀做分组依据再划分。这也是为什么划分脚本里先取图片列表、再按文件联动复制标签,而不是按目录整体移动——分组逻辑可以替换,文件联动逻辑是通用的。
6. 模型训完先别急着部署:用 conf 阈值和混淆矩阵做一轮复盘
6.1 用 yolo val 分阈值复验
训练完的best.pt在 val 上的指标是框架默认阈值下算出来的,部署时不一定是最优工作点。我习惯把不同置信度阈值下的表现都跑一遍:
yolo detect val \ data=insulator.yaml \ model=runs/detect/insulator_exp1/weights/best.pt \ conf=0.25然后把conf改成 0.4 再跑一次,对比两次输出的 precision 和 recall。如果误检多,提高 conf 能压住;如果漏检多,就要降低 conf。具体调到多少,以你实际场景里误检和漏检哪个代价更大来决定。
6.2 混淆矩阵与 edge 部署
每次 val 跑完,runs/detect/insulator_exp1/下会生成confusion_matrix.png。除了正常瓷瓶和破损瓷瓶两类的互相混淆,重点看 background 那一格。如果背景被模型大量预测为目标,说明误检率高,部署时把 conf 往上提到 0.4 到 0.6,误检能压下来,但 recall 也会掉一点。边缘设备上部署检测模型时,很多误检问题都不是模型没训好,而是推理时置信度阈值设得太低,我见过直接把 conf 设成默认 0.25 就上线,结果误检框满天飞的案例。
我最早做绝缘瓷瓶检测项目时,为了让指标好看,训练时把 conf 调得很低,验证集里确实多检出了几个真目标,但部署到现场后误检框多到没法看,演示的时候当场翻车。后来我养成一个习惯:模型训完只做两件事,第一看混淆矩阵的 background 列,第二用测试集把不同 conf 下的表现跑一遍,选好工作点再固化到部署代码里。这两件事做完,模型才敢挂出去。希望帮到你。
本文还有配套的精品资源,点击获取