首页 / 资讯中心 / 文章详情

从昆虫数据集到YOLO训练:txt/xml双格式标注与混淆矩阵修复实战

从昆虫数据集到YOLO训练:txt/xml双格式标注与混淆矩阵修复实战 ★ FEATURED ARTICLE
简介面向YOLO系列目标检测模型训练与验证的完整昆虫检测数据集覆盖Leconte、Boerner、linnaeus、armandi、coleoptera、acuminatus、Linnaeus等七类常见害虫适合农业植保监测、昆虫识别研究以及目标检测算法对比。数据包共2000个文件其中JPEG原始图片1693张、txt格式YOLO标签1694个、xml格式VOC标签1693个txt可直接用于YOLOv5/v8等框架xml兼容VOC系模型压缩包约906MB目录结构清晰导入即可训练。已有1938人学习下载资源自带标准化标签体系且文件按数字编号便于批量处理省去数据收集与格式转换的繁琐环节可帮助读者快速构建昆虫检测基线模型也可作为论文实验的公开对比基准。1. 昆虫检测数据集入手7 类学名、txt 与 xml 双格式给 YOLO 训练铺路做农林害虫检测的人多半跟我一样卡住的不是 YOLO 模型本身而是标注。这套 insects-dataset.rar 是昆虫检测数据集打包了 7 个类别的标注Leconte、Boerner、linnaeus、armandi、coleoptera、acuminatus、Linnaeus标签同时给了 txt 和 xml 两份。txt 直接喂 YOLOxml 拿来校验和转 VOC 系适合刚配好 YOLO 环境、想找一份多类别检测数据练手的同学也适合要把自己数据整理成同一格式的工程人。资源以编号 txt 形式给出1630、1636、1627 这一串命名就是图片名。拿到后先花十分钟把格式读透再训练后面会少踩不少坑。2. 先啃数据格式YOLO 的 txt 与 VOC 的 xml 分别装了什么解压后先看文件清单一共 10 个标注文件1622.txt、1624.txt、1625.txt、1627.txt、1628.txt、1630.txt、1633.txt、1634.txt、1636.txt、1639.txt。文件名就是图片编号每个 txt 对应一张同名图片。这类“一图一标”格式是 YOLO 数据集最通用的形态labelImg 导出和很多标注平台的默认选项都是它。好处是边界清晰坏处是你不知道哪些采集照片没被标注得自己数一遍文件数量。如果完整包里只有这 10 个 txt那就是样例级规模不够训练只能当格式参照如果完整资源里还有成百上千个同类文件按下面流程整理即可。2.1 从 1630.txt 说起YOLO 归一化坐标的五列数字打开 1630.txt内容长这样3 0.621094 0.472656 0.168750 0.125000 6 0.823437 0.551562 0.093750 0.087500第一列是 class_id从 0 开始计数代表这个框属于哪一类后面四列是 x_center、y_center、width、height全部做了归一化取值范围 0~1。比如第一行第二个值 0.621094意思是框中心的横坐标在图片宽度的 62.1% 处0.168750 是框宽占图片宽度的 16.875%。归一化的最大好处是换图片尺寸不用改标注640x480 和 1920x1080 通用。用下面的脚本可以把 txt 还原成像素坐标顺手检查有没有非法值from pathlib import Path import cv2 img cv2.imread(1630.jpg) h, w img.shape[:2] bad 0 for line in Path(1630.txt).read_text(encodingutf-8).strip().splitlines(): parts line.split() if len(parts) ! 5: bad 1 continue cid, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) print(fclass_id{int(cid)}, box({x1},{y1},{x2},{y2})) print(f异常行数: {bad})这里先读取图片真实宽高再把归一化数值换算成像素框坐标。注意 YOLO 训练时读的仍是归一化值像素坐标只是给人看的异常行判断是必备动作我见过不少数据集里混入空行或坐标越界的坏标注不查的话训练时 loss 会莫名奇妙爆炸。参数xc - bw / 2是把中心点表示转成左上角表示int()只用于显示不影响原始 txt。2.2 xml 是 VOC 的完整注解name 与 bndbox 字段怎么转成 YOLO同源图片的 xml 长这样节选annotation filename1630.jpg/filename size width640/width height480/height /size object namearmandi/name bndbox xmin176/xmin ymin132/ymin xmax284/xmax ymax192/ymax /bndbox /object /annotationVOC 系标注用绝对像素坐标类别字段 name 直接写学名而 YOLO 的 txt 只有 class_id你不看映射表根本不知道 3 代表谁。所以双格式的真正价值是互相校验txt 负责训练xml 负责确认 id 有没有串位。把 xml 转成 txt 的脚本我一般这样写import xml.etree.ElementTree as ET name_to_id { Leconte: 0, Boerner: 1, linnaeus: 2, armandi: 3, coleoptera: 4, acuminatus: 5, Linnaeus: 6, } tree ET.parse(1630.xml) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) for obj in root.findall(object): name obj.find(name).text cid name_to_id[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) xc (xmin xmax) / 2 / w yc (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h print(f{cid} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f})关键点是把 name 到 id 的映射表放在循环外面一次性查表不要在循环里写 if-elif类别多的时候既慢又容易漏。保留 6 位小数是 YOLO 惯例精度足够且文本体积小。转完记得抽样对比随机找三张图用 2.1 的脚本画框跟 xml 里的 bndbox 数值对一遍。2.3 类别映射表7 类学名合并大小写坑把两套标注对起来得到这张映射表class_idtxt 数值对应xml 里的 name备注0LeconteLeconte种级学名1BoernerBoerner种级学名2linnaeuslinnaeus小写开头3armandiarmandi常见于松柏类害虫命名4coleopteracoleoptera鞘翅目目级名称5acuminatusacuminatus种加词6LinnaeusLinnaeus与 2 同为 Linnaeus仅大小写不同直接按 7 类训练会踩一个大坑linnaeus 和 Linnaeus 其实是同一个拉丁名摘要里写成两个类训练时等于把同一种昆虫拆成两类两个类互相抢正样本表现就是损失函数下不去、混淆矩阵里这两个格子特别亮。我拿到这种数据集的第一件事是把所有 name 转成小写再统计唯一值确认到底有几个有效类。另一个细节是 coleoptera 是“目”级单位其余几个是“种”级单位粒度并不一致。这通常说明采集者是按现场形态快速标注的没有做严格的分类学整理。训练角度可以保留原样跑一轮然后用混淆矩阵看哪两类互相串再决定要不要合并但如果你用这个名字做导出要注意类别名称大小写和粒度问题。提示做任何合并或改名之前先把 labels 目录整体备份一份cp -r labels labels_bak改错了能回滚不用从头解压。补充一句文件编号里有 1622 到 1639 的跨度中间缺了 1623、1626、1629 等序号说明原始采集图有筛选。文件名断号不用在意YOLO 只认主名一致不要求编号连续。3. 把标注整理成 YOLO 训练目录目录结构、划分脚本与 data.yamlYOLOv5 和 YOLOv8 训练时并不直接读散落的编号 txt它要求一个固定目录图片归图片标签归标签train/val 分开。目录放不对训练时会报 label not found或者把 val 当 train 用。这一步写成脚本以后换数据集能直接复用。3.1 标准目录结构images 与 labels 分别放什么标准结构如下datasets/insects/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── insects.yaml图片放在 imagestxt 放在 labels两边文件名主名一致、扩展名不同。先确认资源包里有没有同名图片如果只有这 10 个 txt 没有图说明你拿到的是标注子集先补图再继续。建目录和拷贝# 建目录train/val 两组images 放图labels 放 txt mkdir -p datasets/insects/images/train datasets/insects/images/val mkdir -p datasets/insects/labels/train datasets/insects/labels/val # 拷贝当前目录下的图片和标签后续再按比例拆分 cp *.jpg datasets/insects/images/train/ cp *.txt datasets/insects/labels/train/注意这里的*.jpg只拷贝当前目录下的图子目录里的要用find . -name *.jpg -exec cp {} \;。拷贝前先ls *.jpg | wc -l和ls *.txt | wc -l对一下数量图片数比 txt 少就说明有图没有被标注这类图不能进训练集。3.2 train/val 划分脚本数据量够的话按 8:2 随机划分import glob import random import os import shutil label_files glob.glob(datasets/insects/labels/train/*.txt) random.seed(2025) random.shuffle(label_files) val_count int(len(label_files) * 0.2) val_files label_files[:val_count] train_files label_files[val_count:] for f in val_files: name os.path.splitext(os.path.basename(f))[0] shutil.move(f, datasets/insects/labels/val/) for ext in (jpg, jpeg, png): img fdatasets/insects/images/train/{name}.{ext} if os.path.exists(img): shutil.move(img, datasets/insects/images/val/) break print(ftrain{len(train_files)}, val{len(val_files)})random.seed(2025)固定随机种子保证每次跑出来的划分一致便于复现如果项目要多次对比实验这一步会很关键。划分顺序是先挪 txt 再挪对应图移动失败时能根据残留文件对账。小样本下 20% 的 val 可能只有几张图这种规模不适合划分直接全量训练、用交叉验证思路评估。划分前我还会做一个类别统计防止某个类全落在 val 里from collections import Counter counts Counter() for f in label_files: for line in open(f, encodingutf-8): cid line.split()[0] counts[cid] 1 print(counts)如果某个 class_id 只出现一两次先把这类样本单独挑出来按比例同时分到 train 和 val而不是全随机。这是昆虫数据集最容易踩的点背景类特别多目标类极少随机划分容易把稀有类全分到 train。3.3 data.yaml 与类别文件class 顺序必须和 txt 中的 class_id 对齐在 datasets/insects 下写 insects.yamlpath: datasets/insects train: images/train val: images/val nc: 7 names: 0: Leconte 1: Boerner 2: linnaeus 3: armandi 4: coleoptera 5: acuminatus 6: Linnaeus这里names的顺序就是 txt 里 class_id 的顺序YOLO 不看字段名只看位置。常见翻车是有人把 names 按首字母排序结果 id0 变成 Boerner模型训了 100 轮全错。如果你决定合并大小写需要同步做两件事改 yaml 里的 nc 和 names以及重写所有 txt 的第一列。重映射脚本import glob import xml.etree.ElementTree as ET # 第一步以 xml 为准统计真实出现过的类名统一小写 seen set() for xmlf in glob.glob(datasets/insects/**/*.xml, recursiveTrue): for obj in ET.parse(xmlf).getroot().findall(object): seen.add(obj.find(name).text.lower()) new_names sorted(seen) new_id {name: idx for idx, name in enumerate(new_names)} print(new_names) # 第二步重写 txt 第一列 old_names [Leconte, Boerner, linnaeus, armandi, coleoptera, acuminatus, Linnaeus] for txtf in glob.glob(datasets/insects/labels/**/*.txt, recursiveTrue): out_lines [] for line in open(txtf, encodingutf-8): parts line.split() if len(parts) ! 5: continue old_name old_names[int(parts[0])] new_name old_name.lower() out_lines.append(f{new_id[new_name]} { .join(parts[1:])}) with open(txtf, w, encodingutf-8) as f: f.write(\n.join(out_lines))用 xml 作为标注真实来源是因为 txt 只有 id看不出学名如果原 txt 的 id 映射就是错的只有 xml 能兜底。recursiveTrue 保证 train/val 里的文件都被处理。脚本执行前记得跑 2.1 的解析脚本做一次全量扫描别让异常行混进重写结果。4. 训练与排查超参数、损失函数、BN 崩溃与类别不平衡目录和 yaml 就绪后可以开始训练。这一章把命令、日志怎么读、以及我实际踩过的四个典型坑写在一起省得你反复试错。4.1 训练命令与关键超参数YOLOv5 训练命令python train.py \ --data insects.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --name insects_run--weights yolov5s.pt需要先把 YOLO 预训练模型放到当前目录没有预训练权重从零训的话小数据集很难收敛。--img 640是输入分辨率昆虫目标小640 起步显存足够时用 960 对 mAP 提升明显。--batch 16看显存16G 卡别超过 16V100 上可以开到 64batch 太大会让 BN 层统计不稳定尤其在数据量小的时候。YOLOv8 的写法差异不大yolo detect train datainsects.yaml modelyolov8s.pt epochs100 imgsz640 batch16v5 和 v8 对目录结构、data.yaml 的要求基本一致这个数据集两套代码都能直接吃。新手建议先跑 s 模型把流程走通别一上来就 YOLOv8x样本量小时大模型反而更容易过拟合。类别只有 7 个但样本极少的情况模型容量不是瓶颈数据质量才是。4.2 损失函数与训练日志怎么看训练过程中盯三个 lossbox_loss预测框和真实框的 IoU 偏差、cls_loss类别预测错多少、obj_loss有没有把背景当目标或漏掉目标。我一般看 results.csv 或训练终端的曲线图重点不是 train loss 降得多快而是 val mAP0.5 的斜率。常见的三种情况train loss 降、val mAP 也在涨正常train loss 降、val mAP 不动过拟合加早停或数据增强cls_loss 比 box_loss 高一倍以上先查类别标注而不是调参。调参优先动 img 和 epochs其次动 lr。loss 曲线本身带毛刺一个 epoch 反弹就改参数是最费时间的做法玄学点说给模型多一点时间。还有一点类别少时不要盲目加分类权重YOLOv5 默认 cls 系数是 0.5如果 cls_loss 掉不下去先把注意力放在标注上而不是调这个系数。4.3 高频踩坑记录现象训练到一半 loss 变成 NaN或者权重导出后推理全黑。 原因学习率过大导致 BN 层统计量崩溃在小目标、小样本的昆虫数据集上尤其容易触发也可能是 txt 里有 x_center 或 width 为 0 的非法标注。 解决把默认 lr 乘 0.1例如--lr 0.001并保证 warmup 开启同时用 2.1 的脚本全量扫描 labels删掉坐标越界行。这两步做完NaN 基本消失。现象mAP0.5 还行但 mAP0.5:0.95 很差。 原因昆虫在整图里占比太小默认锚框和 640 分辨率下特征太弱。 解决imgsz 提到 960batch 相应减半再用--evolve跑一次锚框自适应或者换成 YOLOv8 的 anchor-free 分支。经验判断当目标宽度小于图宽 5% 时640 基本不够用。现象混淆矩阵里 linnaeus 和 Linnaeus 两个格子一定互亮如果没合并或者合并后某个目级类把其他类吞掉。 原因前者是大小写重复导致同类被拆两类后者是 coleoptera 粒度太宽把多个种级目标框在一起了。 解决统一小写重映射再看混淆矩阵决定是否把 coleoptera 拆开。这种问题靠调参解决不了只能回数据侧改。现象训练时 val 正常部署到边缘设备后误检率变高。 原因部署时输入尺寸和训练不一致或者 conf 阈值没重调另一个隐蔽原因是训练时的负样本和真实场景差异大。 解决导出模型时固定相同的 imgsz推理时把 conf 从 0.5 提到 0.7 观察同时看原始帧里“像昆虫但没标注”的区域标注漏掉的部分在部署端全变成误检。5. 验证与部署从权重到推理跑通完整检测链路训练结束不是终点。把 best.pt 拿出来做推理、导出、回验这步做到位才敢说这个数据集真的被你用起来了。5.1 用训练好的权重做图片推理YOLOv5 的推理命令python detect.py \ --weights runs/train/insects_run/weights/best.pt \ --source test/ \ --conf 0.5 \ --save-txtbest.pt是验证集 mAP 最高的权重不是最后一个 epoch 的权重--conf 0.5低于该置信度的框会被丢--save-txt把预测结果按 YOLO 格式输出。昆虫检测我一般会跑两次conf0.3 看召回conf0.7 看精确率再根据业务偏好在 0.5 附近定阈值。快速验证单张图可以用 Python APIimport torch model torch.hub.load(ultralytics/yolov5, custom, pathbest.pt, force_reloadTrue) results model(test.jpg, size640) results.print() results.save(output/)torch.hub 加载适合写脚本验证但它依赖完整的 PyTorch 环境真正部署时不这么用下一节导出成 onnx 更实际。5.2 导出 onnx 做边缘部署的注意点python export.py --weights best.pt --img 640 --batch 1 --include onnx导出时最容易被忽略的是输入尺寸导出的 onnx 在部署端必须用同一个 640 送图如果推理时 resize 成 1280 再送框坐标会整体偏移误检率看着像模型问题其实是尺寸不匹配。YOLOv5 导出的 onnx 默认不包含 NMS后处理里要自己写部署时 conf 和 NMS 的 IoU 阈值最好和训练配置保持一致不然 val 结果和线上表现对不上。用 onnxruntime 推理的骨架import onnxruntime as ort import cv2 import numpy as np sess ort.InferenceSession(best.onnx) input_name sess.get_inputs()[0].name img cv2.resize(img, (640, 640)) / 255.0 img img.transpose(2, 0, 1)[None].astype(np.float32) out sess.run(None, {input_name: img})[0] # 输出 shape 按导出实际为准常见是 [1, 8400, nc4] 或 [1, nc4, 8400] # 先做 transpose 统一成 [1, N, nc4]再过滤置信度、按类 NMS不同版本的输出张量排列不同v5 和 v8 也不一样写后处理前先打印out.shape确认不要照抄网上的坐标索引。边缘部署监控误检率高的项目我通常会把 conf 阈值从 0.5 上调一档先压误检再谈召回。5.3 用混淆矩阵回验数据集质量python val.py --data insects.yaml --weights best.pt --conf 0.5 --save-jsonval.py 跑完会在 runs/val 下生成 confusion_matrix.png。主对角线越亮越好某一列明显比同行亮说明大量真实目标被分到那一类。这个图不只是工程指标它直接反映标注质量如果某两类在矩阵里互混严重先怀疑标注框边界贴太紧再怀疑类别定义本身。把错分样本导出来逐张看往往比调模型参数更有用。这也正好是下一章的具体技巧用混淆矩阵反向修复标注数据侧修正后再训一轮就能看出 mAP 的确定性提升。6. 用混淆矩阵反向修复标注一个把精度再抬 3 个点的技巧训练一版 best.pt 后我会做一次“质量回注”利用混淆矩阵反向扫描错分样本把数据里的标注问题揪出来改掉再重训。对昆虫这种密集小目标数据集这个操作常常能把 mAP0.5 抬 3 个点左右而且它是数据侧的确定性修正不是碰运气调参。具体四步val.py 加--save-json导出全部预测写脚本过滤出“预测类别与真实类别不一致且置信度高于 0.25”的检测把这些图带框导出到同一个文件夹逐张人工核对。典型的发现有三种一是 linnaeus 和 Linnaeus 这种同类不同大小写没合并二是标注框把一个区域里的多只昆虫框成一个类别对但边界错模型学到的形状是坏的三是背景的叶子、土块被标成了昆虫部署时全变成误检来源。这三种问题看 val 指标看不出看错分图一眼就明白。筛选脚本的骨架import json preds json.load(open(runs/val/exp/predictions.json)) for item in preds: img_id item[image_id] for det in item[predictions]: if det[conf] 0.25: continue # 对比 det[category_id] 与真实标注不一致就记录 # 简化版打印高置信检测人工回看 print(img_id, det[category_id], det[conf])实际做的时候把 predictions.json 里的 category_id 和 xml 的 name 对照导出错分样本目录每天花半小时过一遍。修正完 xml重新生成 txt再训一轮。从那以后我每次拿到新数据集都会强制走一遍“训练→混淆矩阵→错样本回看”的闭环不在调参里硬耗。这套 insects-dataset.rar 虽然只有 10 个 txt 样本但 txt/xml 双格式、7 类学名大小写重复、目级与种级混排这些典型坑都在里面拿来当 YOLO 练手和标注格式参照都很合适。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站