简介:面向电力场景电气设备红外检测需求,这份数据集资料围绕变压器及相关设备的目标检测任务整理,适合电力巡检、缺陷识别方向的研究人员与工程师用于模型训练和算法验证。配套文档约1MB,仅含1个docx文件,集中说明数据集的构成、标注规范与使用边界,便于在正式取数前快速判断其适用性。数据覆盖4271张红外图像,采用Pascal VOC与YOLO双格式标注,共14个类别、11896个矩形框,涉及空气断路器、电流互感器、连接器、避雷器、负荷开关、塑壳断路器、电压互感器、真空断路器和柜身等对象;其中Connection类框数最多,PF类较少,类别分布差异可供采样与损失设计参考。资料同时注明标注工具为labelImg、标注规则为逐类画框,并提示不对训练模型精度作保证。已有101人学习,适合需要了解红外电力设备数据规格、评估数据质量与规划检测实验的读者查阅。
1. 电力红外变压器检测数据集:4271 张图与 14 类标签先看清
电力场景电气设备红外图像变压器检测数据集,4271 张红外图配 VOC 与 YOLO 双格式标注,14 类设备框共 11896 个。它不是拿来就涨点的万能包,最大价值是把 ACB、CT、MCCB、VCB、PT 等一次凑齐,让你能把“设备定位”这件事先跑通。适合做电力巡检算法验证的工程师、做课程设计的学生,也适合想练类别不均衡和小目标检测的熟手;如果你的任务只是识别单一变压器,直接上这份数据集反而会把问题复杂化。拿到压缩包后先别急着训练,先看标签拼写、空框和类别分布,这三件事比选模型更影响最终结果。
2. VOC 与 YOLO 双格式拆解:14 类标签的读法、分布与合并决策
2.1 VOC xml 与 YOLO txt 各自管什么
这份数据集给的是 jpg 图片、VOC 格式 xml、YOLO 格式 txt,数量都是 4271,没有分割路径的 txt,也没有 mask 图。VOC 的 xml 里,一张图对应一个<annotation>,里面包含<size>的宽高,以及若干个<object>,每个 object 有<name>和<bndbox>的 xmin、ymin、xmax、ymax。YOLO 的 txt 每行是class_id cx cy w h,坐标已经归一化到 0 到 1,class_id 从 0 开始,具体类别名不在 txt 里,而是由训练配置里的 names 列表决定。两边唯一的硬约束是:xml 的 size 必须和 jpg 实际宽高一致,否则转换出来的归一化坐标会整体跑偏。
常见翻车点是把 VOC 的 xmax、ymax 当成宽高,或者把 YOLO 的 cx、cy 当成左上角。VOC 是绝对像素坐标,左上角加右下角;YOLO 是中心点加宽高,再除以图片宽高。用 labelImg 标注时,框的边界偶尔会贴到 0 或图片最大宽高,转换时要做一次裁剪,不然会出现负数或大于 1 的坐标。训练器一般会报 warning,但不会帮你修数据,最后表现成框偏移、mAP 虚低。
2.2 14 类标签分布:先看框数再谈模型
原始类别名是["ACB","CT","Conection","Connection","LA","LBS","MCCB","MOF","PF","PT","VCB","body","connection","core"]。注意这里有三个高度相似的写法:Conection、Connection、connection。输入里明确给出中文含义的只有 ACB 空气断路器、CT 电流互感器、Connection 连接器、LA 避雷器、LBS 负荷开关、MCCB 塑壳断路器、PT 电压互感器、VCB 真空断路器、body 柜身;MOF、PF、core 以及拼写变体没有给出中文,先按标签字面保留,不要自己猜。
| 标签 | 中文含义(按给定) | 框数 | 观察 |
|---|---|---|---|
| ACB | 空气断路器 | 94 | 小类,容易漏 |
| CT | 电流互感器 | 790 | 与 PT 外形接近 |
| Conection | 标签字面,疑似拼写变体 | 471 | 与 Connection 混淆 |
| Connection | 连接器 | 3961 | 最大类,主导损失 |
| LA | 避雷器 | 267 | 小类 |
| LBS | 负荷开关 | 548 | 中等 |
| MCCB | 塑壳断路器 | 1600 | 第二大类 |
| MOF | 标签字面 | 273 | 小类 |
| PF | 标签字面 | 56 | 极少,先查是否有漏标 |
| PT | 电压互感器 | 661 | 与 CT 易混 |
| VCB | 真空断路器 | 144 | 小类 |
| body | 柜身 | 380 | 大框,可能包住其他类 |
| connection | 标签字面,小写变体 | 1952 | 与 Connection 混淆 |
| core | 标签字面 | 699 | 中等 |
总框数 11896。Connection一类就占 3961,加上Conection的 471 和connection的 1952,连接器相关框合计 6384,超过一半。PF 只有 56 个框,ACB 94 个,VCB 144 个,这三个类在训练集里很可能被大类压住。先做一次统计,不要凭感觉调参。
from pathlib import Path import xml.etree.ElementTree as ET from collections import Counter raw = Path("dataset/raw") xmls = sorted(raw.rglob("*.xml")) counter = Counter() empty_xml = [] bad_boxes = [] for xml_path in xmls: root = ET.parse(xml_path).getroot() objects = root.findall("object") if not objects: empty_xml.append(xml_path.name) continue for obj in objects: name = (obj.findtext("name") or "").strip() counter[name] += 1 box = obj.find("bndbox") if box is None: bad_boxes.append((xml_path.name, name, "missing bndbox")) continue try: x1 = float(box.findtext("xmin")) y1 = float(box.findtext("ymin")) x2 = float(box.findtext("xmax")) y2 = float(box.findtext("ymax")) except (TypeError, ValueError): bad_boxes.append((xml_path.name, name, "non-numeric")) continue if not (0 <= x1 < x2 and 0 <= y1 < y2): bad_boxes.append((xml_path.name, name, (x1, y1, x2, y2))) print("类别框数:", counter) print("空 xml 数:", len(empty_xml), empty_xml[:5]) print("异常框数:", len(bad_boxes), bad_boxes[:5])这段脚本做三件事:统计每个<name>的框数、找没有 object 的空 xml、找坐标非法或缺失 bndbox 的框。raw换成你解压后的图片和 xml 根目录;strip()不能省,标签名前后有空格会让类别对不上。跑出来的 Counter 应该能和上表对上,如果对不上,先检查是不是重复解压了目录,或者 xml 和 jpg 不在同一层。
2.3 合并重复类还是保留原样:一张映射表定决策
我一般会做两组实验,而不是一上来改标签。第一组是原始 14 类,完全按 txt 里的 class_id 走,作为 baseline;第二组把Conection和connection映射到Connection,类别数从 14 变成 12,总框数仍是 11896。合并后连接器类 6384 个框,MCCB 1600,CT 790,body 380,core 699,PT 661,小类仍然小。这个对照能回答一个问题:模型到底是被“连接器样本多”带偏,还是被“同一类拆成三个名字”带偏。
不建议把 CT 和 PT 合并,也不建议把 body 和 core 合并。CT 电流互感器、PT 电压互感器在红外图里可能外形接近,但语义不同;body 是柜身,core 是标签字面核心部件,框的层级可能不一样,合并会把大框和小框混成一类。合并只处理明显拼写和大小写变体,别扩散。
RAW_CLASSES = [ "ACB", "CT", "Conection", "Connection", "LA", "LBS", "MCCB", "MOF", "PF", "PT", "VCB", "body", "connection", "core" ] MERGE_MAP = { "Conection": "Connection", "connection": "Connection", } MERGED_CLASSES = [] for name in RAW_CLASSES: target = MERGE_MAP.get(name, name) if target not in MERGED_CLASSES: MERGED_CLASSES.append(target) print(MERGED_CLASSES) # ['ACB', 'CT', 'Connection', 'LA', 'LBS', 'MCCB', 'MOF', 'PF', 'PT', 'VCB', 'body', 'core']MERGE_MAP为空时就是原始 14 类;启用后类别数变成 12。关键点:映射之后必须重新生成 YOLO txt 的 class_id,不能只改 names 列表。训练配置里的 names 顺序和 txt 的编号是一一对应的,顺序错一位,所有框都会学到错误类别。原始 14 类 baseline 和 12 类合并实验各留一份 data.yaml,别覆盖。
2.4 用脚本把标签健康度扫一遍
除了类别统计,还要看每张图的框数分布。红外图里经常出现一张图只有柜身大框,或者只有一个小连接器,这种样本对定位有帮助,但对分类帮助有限。统计每张图框数,可以快速找空 txt、单框图和异常多框图。空 txt 不是一定有问题,它可能被当成背景图使用;但如果空 txt 数量很大,说明标注覆盖不完整,训练时背景样本会压低召回。
from pathlib import Path from collections import Counter label_dir = Path("dataset/raw/labels") box_per_image = Counter() empty_txt = [] for txt in label_dir.glob("*.txt"): lines = [ln for ln in txt.read_text(encoding="utf-8").splitlines() if ln.strip()] box_per_image[len(lines)] += 1 if not lines: empty_txt.append(txt.name) print("每图框数分布:", sorted(box_per_image.items())[:10]) print("空 txt 数:", len(empty_txt), empty_txt[:10])box_per_image的键是框数,值是该框数对应的图片数。如果 0 框图片占比超过 10%,先确认这些图是不是纯背景;如果 1 框图片大量集中在 body,说明很多图只标了柜身,没有标内部设备,训练时模型会倾向于只输出大框。空 txt 可以保留少量作背景,但不要大量保留,否则小类召回会被拉低。
3. 从原始压缩包到可训练目录:划分、转换与加载的三个脚本
3.1 目录怎么摆:images 与 labels 严格对应
解压后先把原始文件放成dataset/raw/images和dataset/raw/labels,图片和 xml 可以同目录,但转换后的 YOLO txt 单独放labels。最终训练目录用dataset/transformer_ir,下面分train、val、test,每个 split 里各有images和labels。文件名 stem 必须完全一致,abc.jpg对应abc.txt,不要出现abc.JPG和abc.txt大小写不一致,Linux 上会直接找不到标签。
常见做法是先转换再划分,这样划分脚本只复制 jpg 和 txt,不碰 xml。转换脚本读 xml 和 jpg,输出 txt 到dataset/transformer_ir/labels,图片复制到dataset/transformer_ir/images。不要在原始目录上直接改,留一份 raw 方便回滚。
3.2 VOC 转 YOLO:归一化、裁剪与类别映射
下面这段转换脚本同时处理类别映射、坐标裁剪和未知类别报错。未知类别直接抛异常,不要用continue悄悄跳过,否则你会在训练后发现少了一个类,回头查很费时间。
from pathlib import Path from PIL import Image import xml.etree.ElementTree as ET import shutil RAW = Path("dataset/raw") OUT = Path("dataset/transformer_ir") RAW_CLASSES = [ "ACB", "CT", "Conection", "Connection", "LA", "LBS", "MCCB", "MOF", "PF", "PT", "VCB", "body", "connection", "core" ] MERGE_MAP = {"Conection": "Connection", "connection": "Connection"} CLASSES = ["ACB", "CT", "Connection", "LA", "LBS", "MCCB", "MOF", "PF", "PT", "VCB", "body", "core"] def clip(v, lo, hi): return max(lo, min(v, hi)) def convert_one(xml_path, img_path): w, h = Image.open(img_path).size root = ET.parse(xml_path).getroot() lines = [] for obj in root.findall("object"): name = (obj.findtext("name") or "").strip() name = MERGE_MAP.get(name, name) if name not in CLASSES: raise ValueError(f"未知类别 {name} in {xml_path}") box = obj.find("bndbox") if box is None: continue x1 = float(box.findtext("xmin")) y1 = float(box.findtext("ymin")) x2 = float(box.findtext("xmax")) y2 = float(box.findtext("ymax")) x1 = clip(x1, 0, w - 1) y1 = clip(y1, 0, h - 1) x2 = clip(x2, 0, w) y2 = clip(y2, 0, h) if x2 <= x1 or y2 <= y1: continue cx = (x1 + x2) / 2.0 / w cy = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h cls_id = CLASSES.index(name) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") return "\n".join(lines) (OUT / "images").mkdir(parents=True, exist_ok=True) (OUT / "labels").mkdir(parents=True, exist_ok=True) for img_path in sorted(RAW.rglob("*.jpg")): xml_path = img_path.with_suffix(".xml") if not xml_path.exists(): print("缺 xml:", img_path) continue shutil.copy2(img_path, OUT / "images" / img_path.name) txt = convert_one(xml_path, img_path) (OUT / "labels" / f"{img_path.stem}.txt").write_text(txt, encoding="utf-8")RAW是解压后的根目录,OUT是转换输出;MERGE_MAP为空字典时保留 14 类,启用时是 12 类;CLASSES的顺序就是 YOLO 的 class_id 顺序,必须和 data.yaml 的 names 完全一致。坐标裁剪到图片范围内,防止 labelImg 的边界框贴边产生负值。:.6f保留六位小数,足够训练用。转换完先看OUT/images和OUT/labels的文件数是否都是 4271 左右,少一张都要查。
3.3 按图像划分训练/验证/测试并防泄漏
划分按图片级别做,不要按框级别做。同一张图的多个框必须落在同一个 split,否则验证集会出现训练时见过的设备,mAP 虚高。常见比例是 8:1:1,小数据集可以 7:2:1,验证集至少留 300 张以上,测试集至少 300 张,不然单类指标波动很大。随机种子固定,划分结果存一份 csv,后续换模型才能公平比较。
import random import shutil import hashlib from pathlib import Path OUT = Path("dataset/transformer_ir") random.seed(202405) images = sorted((OUT / "images").glob("*.jpg")) random.shuffle(images) n = len(images) n_val = int(n * 0.1) n_test = int(n * 0.1) splits = { "val": images[:n_val], "test": images[n_val:n_val + n_test], "train": images[n_val + n_test:], } seen = {} for p in images: h = hashlib.md5(p.read_bytes()).hexdigest() if h in seen: print("重复图片:", p.name, "与", seen[h]) seen[h] = p for split, files in splits.items(): img_dir = OUT / split / "images" lbl_dir = OUT / split / "labels" img_dir.mkdir(parents=True, exist_ok=True) lbl_dir.mkdir(parents=True, exist_ok=True) for img in files: shutil.copy2(img, img_dir / img.name) shutil.copy2(OUT / "labels" / f"{img.stem}.txt", lbl_dir / f"{img.stem}.txt") print({k: len(v) for k, v in splits.items()})seed=202405可以换成你们团队固定的种子;n_val和n_test各取 10%,剩下 80% 训练;md5 检查重复图片,重复图跨 split 是最隐蔽的泄漏源。复制而不是移动,原始转换目录保留一份,后面做 12 类合并实验可以重新划分,不用重跑转换。划分后检查每个 split 的 jpg 数和 txt 数是否一致。
3.4 自定义 Dataset 检查一圈再交给训练器
训练前用一个小 Dataset 或检查脚本把路径、标签、类别范围过一遍。重点看三件事:图片能否打开、txt 每行是否 5 个值、class_id 是否小于类别数。红外 jpg 偶尔有损坏文件,PIL 打开时报UnidentifiedImageError,这种图直接剔除或重新解压。
from pathlib import Path from PIL import Image OUT = Path("dataset/transformer_ir") num_classes = 12 # 14 类 baseline 改成 14 for split in ["train", "val", "test"]: imgs = {p.stem for p in (OUT / split / "images").glob("*.jpg")} lbls = {p.stem for p in (OUT / split / "labels").glob("*.txt")} print(split, "图片", len(imgs), "标签", len(lbls), "缺标签", len(imgs - lbls), "多标签", len(lbls - imgs)) for stem in list(imgs)[:200]: Image.open(OUT / split / "images" / f"{stem}.jpg").verify() for line in (OUT / split / "labels" / f"{stem}.txt").read_text().splitlines(): parts = line.split() if len(parts) != 5: print("坏行", stem, line) continue cls_id = int(parts[0]) if not (0 <= cls_id < num_classes): print("越界类别", stem, cls_id)num_classes要和 data.yaml 的 names 数量一致;verify()只检查文件完整性,不保证像素能解码,真正训练时还会再读一次。坏行和越界类别必须清零,不要带着 warning 开跑。data.yaml 写成下面这样,路径用相对路径,换机器时只改path。
path: dataset/transformer_ir train: train/images val: val/images test: test/images names: 0: ACB 1: CT 2: Connection 3: LA 4: LBS 5: MCCB 6: MOF 7: PF 8: PT 9: VCB 10: body 11: core如果保留 14 类,names 要按原始顺序补回Conection和connection,id 不能错位。这份数据集不保证训练精度,标签质量要自己抽检:每个类随机抽 20 张图,把框画回图上,看框是否贴住设备、类别是否标错。抽检比盲目加 epoch 有用。
4. YOLO 训练参数落地:红外小目标、类别不均衡与增强边界
4.1 基线训练配置:先让 14 类都跑起来
第一轮不要追求高 mAP,先把 14 类原始标签跑通,确认数据管道、类别数、评估流程没有错。模型选团队已有的检测权重,不要拿分类权重硬改头;输入尺寸先 640,显存不够降到 512,小目标多再上 960。batch 根据显存调,8 或 16 都行,workers 在 Linux 上 4 到 8,Windows 上先 0 或 2,避免多进程卡死。
from ultralytics import YOLO model = YOLO("baseline.pt") # 替换成本地已有的检测权重 model.train( data="dataset/transformer_ir/data.yaml", imgsz=640, epochs=100, batch=8, workers=4, device=0, optimizer="AdamW", lr0=0.001, lrf=0.01, warmup_epochs=3, mosaic=0.4, close_mosaic=10, hsv_h=0.0, hsv_s=0.1, hsv_v=0.2, degrees=0.0, translate=0.1, scale=0.3, fliplr=0.5, flipud=0.0, project="runs_ir", name="baseline_14cls", seed=202405, )baseline.pt是占位,换成本地权重文件;device=0是单卡,CPU 改成"cpu";imgsz=640是基线,PF、ACB 这种小类如果框很小,可以再做一组 960 的对照;epochs=100配close_mosaic=10,最后 10 轮关掉 mosaic 让模型收一收;optimizer用 AdamW 对学习率不敏感,SGD 需要更大的 lr0。project和name用来分目录,别把 14 类和 12 类实验混在一起。
4.2 红外图像增强边界:HSV、Mosaic 与旋转怎么设
红外图有两种常见形态:灰度图和伪彩图。灰度图没有色相语义,hsv_h设 0,hsv_s也可以设 0,hsv_v小幅度调,模拟曝光和温度对比变化。伪彩图的颜色对应温度带,hsv_h大了会把高温色改成低温色,模型学到的颜色线索直接乱掉,所以hsv_h=0.0是保守起点,最多给到 0.01。hsv_s=0.1、hsv_v=0.2属于轻增强,不要照搬自然图像的 0.7。
Mosaic 会把四张图拼成一张,对密集小目标有帮助,但电力柜体有明确结构,拼得太狠会把柜身、连接器、断路器的上下文切碎。mosaic=0.4比默认 1.0 温和,小目标多可以 0.5,PF 只有 56 个框时不要开到 1.0。degrees=0.0是因为设备安装方向固定,旋转 10 度以上会出现不存在的姿态;flipud=0.0是上下翻转在电力场景不真实,fliplr=0.5左右翻转一般可以接受,但涉及固定柜门方向时先抽检。scale=0.3控制缩放范围,translate=0.1控制平移,别让框被裁掉太多。
4.3 类别不均衡处理:重采样、损失与阈值
14 类里 PF 56 框、ACB 94 框、VCB 144 框,Connection 相关 6384 框。直接训练时,大类会主导分类损失,小类容易变成背景。第一步不是复制小类,而是看 per-class AP:如果 PF 的 AP 为 0,先查测试集里 PF 有多少框,测试集本身太少的话指标没有统计意义;如果测试集够但 AP 低,再考虑训练集过采样。过采样可以复制含小类的图片,但要在训练集内部复制,不能跨到验证集,复制倍数 2 到 3 倍就够,太多会过拟合。
损失层面,YOLO 的分类损失对类别频率有一定敏感性,但不要指望换损失解决所有问题。更实际的做法是:先跑 14 类 baseline,再跑 12 类合并版,比较 PF、ACB、VCB 的单类 AP;如果合并后小类 AP 明显上升,说明拼写重复类在分散学习能力;如果没变化,问题在样本本身。推理阶段可以按类设阈值,PF 这种小类降低置信度阈值,MCCB 这种大类提高阈值,减少误检。
4.4 训练日志看什么:分项损失与单类 AP
训练日志里重点看box_loss、cls_loss、dfl_loss和验证集的 mAP50、mAP50-95。box_loss降不下去,先查框坐标和图片尺寸是否匹配;cls_loss降但 mAP 不升,查标签类别是否错位;dfl_loss波动大,可能是小目标框太小或增强太强。验证集 mAP50 高、mAP50-95 低,说明框的位置不够准,红外图边缘模糊时很常见,可以把imgsz提上去,或者减少 mosaic。
单类 AP 比总 mAP 更有诊断价值。PF、ACB、VCB 如果一直是 0,不要继续加 epoch,先抽 20 张测试图看模型输出;如果模型根本不输出这些小类,说明训练集里小类被压制,做训练集过采样或切图训练;如果输出了但框错,查标注框是否把设备局部当成了整体。训练日志和验证图一起看,别只看一个总指标。
5. 避坑与排查:拼写重复类、空框和误检的 5 个现场记录
5.1 拼写重复类把模型学散
现象:训练完看混淆矩阵,Conection、Connection、connection三个类互相串,单类 AP 都不高,但总 mAP 看起来还能接受。原因:三个名字在数据里是三个 class_id,模型把它们当成三个不同类别学,连接器样本被拆成 3961、1952、471 三份,每一份都不够纯;标注时同一个人可能今天写Connection,明天写connection,标签本身就不一致。
解决:先做映射实验,把Conection和connection归到Connection,类别数从 14 变 12,重新生成 txt 和 data.yaml。不要只改 names,必须改 txt 的 class_id。映射后跑一组对照,重点看连接器类的召回和误检是否下降。如果业务上确实要区分大小写变体,那得先确认它们代表不同设备;从这份数据的命名看,更像是拼写不统一,不像两个独立类别。
提示:映射前备份原始 labels,合并实验和原始实验各留一份目录,否则回头做 14 类 baseline 又要重新解压。
5.2 空标注与坐标越界让框跑偏
现象:训练时出现大量corrupt label或坐标越界 warning,验证图上的框整体偏移,甚至跑到图片外面。原因:labelImg 保存时框贴边,xmin 小于 0 或 xmax 大于图片宽度;也有 xml 的<size>和 jpg 实际尺寸不一致,转换脚本用 xml 宽高归一化,坐标就全错;空 xml 被转成空 txt,训练器把整张图当背景。
解决:转换时用 PIL 读 jpg 实际宽高,不要信 xml 的 size;坐标裁剪到[0, w]和[0, h];空 txt 单独列出,少量保留作背景,大量空标注先回查标注流程。训练前跑一遍坏行检查,len(parts) != 5和cls_id越界必须清零。坐标问题不要靠增强掩盖,增强只会让偏移更随机。
5.3 图像级划分泄漏把验证集刷虚高
现象:验证集 mAP 很高,测试集一落千丈;或者同一张图在训练集和验证集都出现,只是文件名不同。原因:按框随机划分,同一张图的多个框被分到不同 split;或者数据集中有重复图片,md5 相同但文件名不同,划分时没去重。红外巡检图经常连续拍摄,同一设备多角度,文件名不同但画面几乎一样,随机划分很容易泄漏。
解决:按图片 stem 划分,同一张图的全部框跟着图走;划分前算 md5,重复图只保留一张或强制分到同一侧;连续帧按时间或设备分组划分。验证集和测试集不要参与任何过采样。划分结果存 csv,换模型时复用同一份,别每次重新随机。
5.4 小类 PF、ACB、VCB 被大类淹没
现象:总 mAP 看着还行,但 PF 的 AP 是 0,ACB 和 VCB 也很低;推理时小类几乎不输出,或者输出框很大,把整个柜体框住。原因:PF 只有 56 个框,ACB 94 个,VCB 144 个,训练时被 Connection 和 MCCB 的大类损失压住;小目标在 640 输入下像素太少,下采样后特征消失;body 大框和 core 中框又抢了定位能力。
解决:先确认测试集里这些小类有足够框,测试集太少就只看训练集召回,不强行报 AP;训练集对含小类的图做 2 到 3 倍过采样;输入尺寸提到 960 做对照;小目标密集时用切块训练或切块推理,把大图切成重叠子图再检测。阈值按类调,PF 用低阈值保召回,MCCB 用高阈值压误检,别一套 0.25 走到底。
5.5 推理阈值照搬默认值导致漏检和误检
现象:训练指标不错,部署后要么漏掉小设备,要么背景热斑被当成设备。原因:验证时用的是低置信度算 PR 曲线,部署时直接套 0.25,小类分数普遍低,全被过滤;红外图背景有热源,模型把热斑学成连接器或断路器;不同类别的分数分布不一样,统一阈值不适合。
解决:在测试集上按类扫阈值,画 precision-recall 曲线,选业务可接受的平衡点;PF、ACB、VCB 适当降低阈值,MCCB、Connection 适当提高;部署前用负样本集跑一遍,统计误检来源。阈值表写进配置文件,不要写死在代码里,换场景时只改配置。
6. 验证与进阶技巧:用混淆矩阵和热区回看模型到底学了什么
6.1 混淆矩阵先看 CT/PT 与连接器互串
训练完不要只看 mAP,先导混淆矩阵。CT 和 PT 外形接近,红外图上都可能是圆柱或方块,互串很常见;Connection合并后样本多,容易把body上的局部结构也吸过去;core和body的框可能有包含关系,混淆矩阵里会表现为大框套小框。把混淆矩阵按行归一化,看每一类的召回,比看总准确率有用。
from ultralytics import YOLO model = YOLO("runs_ir/baseline_14cls/weights/best.pt") metrics = model.val( data="dataset/transformer_ir/data.yaml", split="test", conf=0.001, iou=0.6, ) print("mAP50-95:", metrics.box.map) print("mAP50:", metrics.box.map50) print("每类 AP:", metrics.box.ap_class_index, metrics.box.ap)conf=0.001是为了让 PR 曲线覆盖低分框,部署阈值另算;iou=0.6是评估匹配阈值,和训练时的 NMS 不是一回事。跑完看ap_class_index对应的类别,PF、ACB、VCB 如果还是 0,回到第 5 章查小类,不要直接换更大的模型。
6.2 按类阈值与热区回看
我一般会把测试集误检按置信度排序,取前 50 张画框回看。重点看两类:背景热斑被误检成设备,以及小设备被大框包住。背景热斑的典型特征是框内没有明显设备边缘,只有一块亮区;小设备被包住时,模型输出的是 body 大框,里面没有独立的小框。前者加负样本或提高阈值,后者切图训练或过采样。
| 检查项 | 看什么 | 处理 |
|---|---|---|
| 混淆矩阵 | CT/PT、Connection/body 互串 | 补难例,别急着合并语义类 |
| 单类 AP | PF、ACB、VCB 是否为 0 | 过采样、提输入尺寸、切块 |
| 误检 Top50 | 背景热斑、柜体局部 | 加负样本,按类调阈值 |
| 漏检 Top50 | 小目标、遮挡设备 | 降阈值,切块推理 |
从那以后我每次拿到类似红外数据集,都强制先做一遍类别拼写合并审计和空框扫描,再谈训练;模型选型放在数据健康度后面,能少走很多回头路。希望帮到你。
本文还有配套的精品资源,点击获取