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

YOLO数据集清洗工具:从体检脚本到训练前校验的完整方案

YOLO数据集清洗工具:从体检脚本到训练前校验的完整方案 ★ FEATURED ARTICLE
简介面向计算机、电子信息及数学等专业学生的YOLO数据集清洗工具包是课程设计、期末大作业与毕业设计的实用参考。资源由资深算法工程师整理包含源代码与配套文档说明代码采用参数化编程关键参数可方便更改注释明细、思路清晰并附带运行结果下载后可直接对照验证适合快速上手YOLO数据预处理与项目复现。压缩包共7个文件以C源码、Qt界面文件、工程配置pro、说明文档md等类型为主zip包仅12KB体量轻但结构完整。文件分工明确C源文件与头文件构成核心逻辑界面文件提供可视化操作界面工程配置文件用于项目构建说明文档提供使用指引便于移植与二次开发。目前已有379人学习下载适合课程设计、期末大作业及毕业设计阶段的同学们参考借鉴。1. yolo数据集的清洗工具先洗数据再谈调参第一次拿 YOLO 训练自采数据集时我在锚框和超参上折腾了很久mAP 始终卡在 0.4 上下。后来把标注文件导出用脚本扫了一遍问题根本不在模型——两成左右的 bbox 越界两百多张重复图像还有一批图只有标签没有对应文件。数据源不干净后面所有调参都是在有噪声的输入上白费劲。yolo数据集的清洗工具源代码文档说明要解决的就是这一层在训练前用可复现的脚本把图像、标注文件、类别分布和重复样本全部查一遍输出清洗报告和一版干净、可以追溯的数据集。这篇文章会从清洗原理讲到源码拆分、配置参数和真实踩坑。新手可以照着命令跑通全流程老手可以直接拿走体检脚本和训练前校验逻辑。无论你用的是 COCO、BDD100K 这类公开数据集还是自采的矿泉水瓶、受电弓、车辆巡检图像这套思路都适用。清洗不是数据增强的替代品它做的是把错误样本拿掉、把分布暴露出来让后续的增强和调参真正作用在有效信息上。2. 清洗工具在洗什么从越界框到类别失衡的六项必查项清洗工具有一个很容易被误解的点它不是替你做数据决策而是把影响训练收敛的问题找出来分成“可自动修”“必须删”“需要人工确认”三类。我一般把必查项收敛成六项越界坏框、图像不可读、文件名不匹配、重复与近似重复、类别分布失衡与空标注、格式转换合法性。六个里面只有前三个适合全自动处理后三个需要先出报告再决定否则清洗本身就会变成新的错误来源。因为清洗工具的核心是“只读体检 受控修改”我建议不管最终用什么工具都要先保证第一份输出是问题清单而不是直接删改后的数据集。2.1 坏框与越界框先跑一段只读体检脚本YOLO 的损失函数里有坐标回归项bbox 越界会让这部分梯度指向无意义的方向。更隐蔽的问题是宽或高为 0 的框、左下角大于右上角的框这些在 VOC 格式里不会报错进入训练后却会导致 loss 震荡甚至 NaN。公开数据集也不一定干净COCO2017 的 JSON 结构虽然经过多轮校对个别边角样本仍存在框尺寸与图像尺寸不一致的情况BDD100K 这类自动驾驶数据集里夜间样本的 bbox 也偏粗糙。所以不要假设下载来的数据可以直接训练。我一般会先用一段只读脚本做统计不删任何文件只输出报告import os, glob import xml.etree.ElementTree as ET from PIL import Image def inspect_voc(anno_dir, img_dir): bad 0 total 0 for xml_path in glob.glob(os.path.join(anno_dir, *.xml)): root ET.parse(xml_path).getroot() img_name root.find(filename).text img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): print(f[missing img] {img_name}) continue W, H Image.open(img_path).size # 损坏图片在这里会抛异常 for obj in root.findall(object): b obj.find(bndbox) xmin float(b.find(xmin).text) ymin float(b.find(ymin).text) xmax float(b.find(xmax).text) ymax float(b.find(ymax).text) total 1 if xmin 0 or ymin 0 or xmax xmin or ymax ymin or xmax W or ymax H: print(f[bad box] {img_name}: ({xmin},{ymin},{xmax},{ymax}) img({W},{H})) bad 1 print(ftotal_boxes{total}, bad_boxes{bad}, bad_ratio{bad/max(total,1):.4f})这段代码的判定逻辑有几处容易忽略。xmax 用而不是是因为图像宽高按像素数算时右下角等于宽高并不越界等于宽高说明框恰好贴边这是合法的。宽和高的判定用xmax xmin而非xmax xmin这样能把宽度为 0 的框也揪出来。读取图像尺寸这里用 PIL 而不是 cv2因为cv2.imread遇到损坏文件会静默返回 None后面取.shape会再抛一个更难定位的异常而 PIL 会直接报一个带文件名的 OSError方便排查哪个文件坏了。体检之后要决定“修”还是“删”。我的经验是越界比例低于 5% 时可以直接 clip 修正高于 15% 就要回去查标注流程正确定位是哪一步把坐标写歪了。清洗报告里要单独列出 “bad_ratio” 这个数字它比总数更能反映数据质量。2.2 重复与近似重复感知哈希做候选标注重叠做确认自采数据里重复图的重灾区来自连续抽帧。视频里同一辆车在相邻帧之间往往只移动了几个像素抽出来的几十张图在模型眼里几乎一样。这些近似重复样本会被模型反复学习相当于人为放大了某一个场景在损失函数里的权重造成验证集 mAP 在个别类别上虚高、换个场景就崩。这类问题在公开数据集里相对少见但在行车记录仪、无人机巡线、监控视频抽帧里非常普遍。近重复检测我习惯分两步走。第一步用感知哈希生成指纹第二步看标注框的重叠情况两步都命中才进候选删除列表。哈希距离这块没有统一阈值属于“玄学”区域我一般先设 12跑完看 quarantine 目录里的样本再微调。from PIL import Image def dhash(img, size8): img img.convert(L).resize((size 1, size), Image.BILINEAR) diff [] for row in range(size): for col in range(size): left img.getpixel((col, row)) right img.getpixel((col 1, row)) diff.append(left right) return sum(v i for i, v in enumerate(diff))这里size8控制指纹精度生成的指纹长度是 64 位两张图的指纹按位比较汉明距离越小越相似。只用 dhash 很容易误删比如同一条路白天和傍晚各拍一张像素分布接近但内容语义不同。所以我会在 dhash 相似的基础上追加一条约束两图的 bbox 列表也要高度重合至少一组框的 IoU 大于 0.8才会把这一对标记为重复候选。这样就能把“同一场景不同光照”和“同一视频连续帧”区别开前者通常保留后者删除。2.3 类别分布与空标注清洗报告要暴露长尾但不能替你决策类别分布是清洗报告里最容易看出问题的一项。极端情况是某一类有 5000 个框、另一类只有 50 个框这种长尾如果不暴露出来训练时模型会倾向于把所有预测都推向高频类。CCPD 车牌数据集类别少、样本量大主要清洗点是坏框和模糊样本CrowdHuman 这种密集人群数据集类别只有 person 但遮挡严重清洗重点变成低质量密集框而自采的小样本数据如矿泉水瓶检测往往还要面对某些类别只有二三十个框的情况。清洗工具能做的是输出类别分布表同时统计空标注图的数量。空标注处理没有一个通用解我一般保留 10% 到 20% 的空样本进训练集纯背景负样本对压制误检很有帮助但要不要留、留多少应该由人来定工具只负责把 empty 数量报出来。from collections import Counter import glob, os import xml.etree.ElementTree as ET def class_stats(anno_dir): counter Counter() empty 0 total 0 for xml_path in glob.glob(os.path.join(anno_dir, *.xml)): root ET.parse(xml_path).getroot() objs root.findall(object) if not objs: empty 1 for obj in objs: counter[obj.find(name).text] 1 total 1 print(fimages{total}, empty{empty}) print(counter.most_common(10))上面这段脚本在 VOC 格式上做统计。如果标注是 COCO 的 JSON遍历方式会不一样但统计逻辑相同。注意 empty 的统计口径是“没有对象的图像”而不是“没有标签文件的图像”两者不同后者可能只是漏标。2.4 格式互转合法性VOC、COCO、YOLO 三种坐标系的四个校验点YOLO 训练消费的标注绝大多数是 txt 格式也就是 cx, cy, w, h 且全部归一化到 [0,1]。而标注平台导出的往往是 VOC 格式的 xyxy 绝对像素或者 COCO 格式的 xywh 绝对像素。格式转换是清洗流程里最容易出脏数据的环节也是最值得放断言的地方。格式坐标系框表达最常见的坑VOC绝对像素xmin, ymin, xmax, ymax忘记限制 xmax W, ymax HCOCO绝对像素x, y, width, height把 width 当成了右下角坐标YOLO归一化cx, cy, width, height忘记除以图像宽高转换时我会在函数里加断言宁可抛异常也不把一个坏坐标写进训练集目录def voc_to_yolo(xmin, ymin, xmax, ymax, W, H): cx (xmin xmax) / 2.0 / W cy (ymin ymax) / 2.0 / H bw (xmax - xmin) / W bh (ymax - ymin) / H assert 0 cx 1, fcenter x out of range: {cx} assert 0 cy 1, fcenter y out of range: {cy} assert bw 0 and bh 0, zero width/height after convert return cx, cy, bw, bh很多人在转换时只检查 bbox 四角是否越界忽略中心点。实际上中心点越界比角点越界更致命因为 YOLO 的回归目标天然以中心点为核心中心点在图像外意味着目标主体根本不在画面内模型永远学不到有效特征。VOC 转 YOLO 时还有一个常见翻车点是把xmax - xmin写成xmax - x这类混用变量脚本不报错但结果全错。这就是为什么清洗工具要在转换后对结果再跑一遍合法性断言而不是信任转换函数本身。3. 源码怎么拆inspect、clean、split 三段式与输出物清洗工具的源码结构直接决定它能不能被团队长期使用。我见过不少一把梭的单文件工具一个脚本里既做体检又做删除还做划分改一个阈值要通读 500 行代码。合理的做法是把源码拆成三个入口加一个公共库inspect 管只读体检clean 管受控清洗split 管数据集划分common 放公共读写函数。三个入口的生命周期不同体检要反复跑清洗是破坏性操作划分依赖清洗结果混在一起会让误操作成本变大。这一章讲的就是这套拆分方式代码量不大但边界清楚后面接训练流程会很顺。3.1 目录结构与三个入口脚本的职责边界我一般用下面这个目录布局. ├── config.yaml # 所有路径与阈值都在这一个文件里 ├── src/ │ ├── inspect.py # 体检只读不改输出 report/report.json │ ├── clean.py # 清洗先把原数据备份再生成干净集 │ ├── split.py # 按种子切 train/val保证可复现 │ └── common.py # 图像与标注的公共读写函数 ├── report/ # 体检输出、清洗日志 └── README.md # 快速开始、参数表、FAQ 三件事common.py 里放的是 load_annotations、save_json、compute_dhash 这类各脚本共用函数。inspect.py 只依赖 common 的读函数不写任何文件clean.py 负责移动文件到 quarantinesplit.py 只读清洗后的目录再按比例划分。这样三条命令可以独立执行也可以串成一条流水线。要注意 clean.py 不直接调用 inspect 的统计逻辑而是读 inspect 生成的 report.json这样如果清洗参数调错了可以只重跑 clean 而不必重新体检。3.2 体检脚本 inspect.py只读不写把问题落成 JSON体检脚本的核心约束是“只读不写”它要能反复运行并且输出结构化的报告而不是一堆散落的 print。这里给一个针对 YOLO txt 格式的最小可运行版本# inspect.py —— 只读检查不修改任何文件 import argparse, json, os, glob def check_txt(txt_path, img_root): txt_name os.path.splitext(os.path.basename(txt_path))[0] img_candidates glob.glob(os.path.join(img_root, txt_name .*)) img_path img_candidates[0] if img_candidates else None if img_path is None: return [missing_image] problems [] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: problems.append(fbad_line:{line.strip()}) continue x, y, w, h map(float, parts[1:]) if w 0 or h 0 or x 0 or x 1 or y 0 or y 1: problems.append(fbad_box:{parts}) return problems def main(): p argparse.ArgumentParser() p.add_argument(--txt-root, requiredTrue) p.add_argument(--img-root, requiredTrue) p.add_argument(--report, defaultreport.json) a p.parse_args() agg {} for txt in glob.glob(os.path.join(a.txt_root, *.txt)): probs check_txt(txt, a.img_root) if probs: agg[os.path.basename(txt)] probs with open(a.report, w) as f: json.dump(agg, f, ensure_asciiFalse, indent2) if __name__ __main__: main()这段脚本里最关键的一行是txt_name .*。图片可能是 jpg、png、bmp 甚至 JPEG 大写后缀用文件名主干去匹配任意扩展名比写死.jpg稳妥得多这个习惯在后面避坑章节里还会再提到。参数方面--txt-root指向标签目录--img-root指向图像目录--report是输出 JSON 的路径。脚本不会修改任何文件只会把问题样本按文件名汇总到 JSON 里。真实项目里 inspect 还会扩展更多检查项比如 bbox 面积小于min_box_area的疑似误标、类别 id 是否落在class_map范围内、空标注图数量的统计。但结构不变每个检查项返回一个问题列表最后统一聚合写盘。输出 JSON 而不是 print 的另一个好处是可以直接喂给后面的清洗执行器也方便在训练后回溯数据问题。3.3 清洗执行器 clean.py先备份再移动保留后悔药清洗是破坏性操作所以 clean.py 的第一原则是“先备份再移动最后才考虑删除”。我习惯把有问题的样本移到一个带时间戳的 quarantine 目录而不是直接 os.remove。这样训练结果如果变差了还能从 quarantine 里把样本找回来这个习惯在项目初期无比重要。# clean.py —— 破坏性操作前先备份 import shutil, os, json, datetime def move_to_quarantine(item, src_roots, dst_quarantine): # item 是文件名主干src_roots 是 [图像目录, 标签目录] moved [] for src_root in src_roots: for ext in [.jpg, .jpeg, .png, .bmp, .txt, .xml, .json]: src os.path.join(src_root, item ext) if os.path.exists(src): dst os.path.join(dst_quarantine, os.path.basename(src)) shutil.move(src, dst) moved.append(src) return moved def main(): report json.load(open(report/report.json)) bad_items list(report.keys()) quarantine fquarantine/{datetime.date.today().isoformat()} os.makedirs(quarantine, exist_okTrue) for item in bad_items: move_to_quarantine(item, [./images, ./labels], quarantine)这里要注意一个细节move_to_quarantine遍历扩展名时会把图像和标签文件一起迁移不允许只删一侧。很多清洗脚本只删了出问题的图片对应的标签文件还留在原目录训练初始化时 label 与 image 对不上报一个更难定位的错。移动完成后再改 config 里的data_root指向清洗后的目录而不是在原始目录上打补丁。如果磁盘空间允许我还会在移动前给原始目录打一个 tar 包虽然很少用到但它是真正的后悔药。3.4 split.py 与 report.json固定随机种子让数据集版本可追溯第三个入口是数据集划分。清洗完成之后要把数据按固定随机种子划分成 train 和 val保证同一份数据跑两次实验得到的是同一份划分否则实验结果没法对比。split.py 的核心逻辑不复杂把清洗后的图像文件名读进来按比例 shuffle 后写入两个清单文件。# split.py —— 固定种子划分输出 train.txt / val.txt import random, os def split_files(image_dir, train_list, val_list, ratio0.8, seed42): names [os.path.splitext(f)[0] for f in os.listdir(image_dir)] names.sort() rng random.Random(seed) rng.shuffle(names) split_idx int(len(names) * ratio) with open(train_list, w) as f: f.write(\n.join(names[:split_idx])) with open(val_list, w) as f: f.write(\n.join(names[split_idx:]))seed42是超参数里最容易被忽略的一个。不固定随机种子的话每次划分结果不同模型效果差异可能被误归因于清洗或模型改动。划分结果建议同时写入 report.json 的 manifest 字段记录“这版数据集包含哪些文件、从哪个原始版本清洗而来”。数据版本可追溯这件事等到你要向别人解释 mAP 为什么提升了 2 个点时会省掉一整天的嘴仗。4. 跑通完整清洗流程config.yaml、命令顺序与文档说明的读法工具源码有了接下来是落地。这一章讲的是怎么把上一步的脚本接成一条可重复执行的流水线先改配置再跑体检看报告没问题后再预演清洗最后才动真格。整套流程的核心是“每一步都有输出物”体检输出 report.json清洗输出 quarantine 目录和 manifest划分输出 train/val 清单后面训练脚本只消费这些输出物不直接碰原始数据。另外还要回答一个实际问题拿到一份清洗项目源码时文档说明应该怎么看、怎么用。文档不是说明书它是你判断这个工具是否可信的依据。4.1 一份最小 config.yaml 和它背后八个参数把路径和阈值全部收敛到一个 config 文件里是清洗工具能复用的前提。下面这份配置覆盖了大部分 YOLO 检测数据的场景data_root: ./raw_dataset output_root: ./clean_dataset quarantine_root: ./quarantine format: voc # voc / coco / yolo img_exts: [.jpg, .jpeg, .png] checks: bad_box: true dedup: true missing_image: true empty: keep # reserve / keep / drop dedup_threshold: 12 # 感知哈希汉明距离小于该值视为重复候选 min_box_area: 16 # 小于 16 像素的框会提示 split_ratio: [0.8, 0.2] seed: 42参数取值作用与影响data_root目录路径指向原始数据集根目录清洗脚本不会修改它formatvoc / coco / yolo决定解析标注文件的方式选错会直接导致解析失败checks.emptyreserve / keep / drop空标注图的处理策略keep 是保留为负样本dedup_threshold整数去重灵敏度设置过大会误删有效样本过小则漏掉重复min_box_area像素数小于该值的 bbox 会在报告中提示为疑似错误标注split_ratio[train比例, val比例]划分比例测试集建议单独留出不要从 val 里再切seed整数固定划分和清洗顺序的随机种子必须固定img_exts列表识别图像扩展名用于文件名主干匹配checks.empty的三个取值值得展开。keep 表示保留空标注图进入训练集适合绝大多数检测任务reserve 表示把空标注图单独移到一个 negative 目录需要训练配置里再挂一次drop 则是直接删除只适合目标几乎必然出现、背景无意义的任务。我一般不会用 drop哪怕训练集的负样本比例不高也宁愿用 reserve。4.2 三条命令的标准执行顺序先体检、再预演、最后清洗清洗工具的典型调用是三条命令依序执行python src/inspect.py --config config.yaml python src/clean.py --config config.yaml --dry-run python src/clean.py --config config.yaml第一次先跑 inspect输出 report.json。这个阶段最重要的不是直接清而是看问题分布坏框多不多、重复多不多、空标注占多少。如果 bad_ratio 超过 15%先回去查标注流程而不是急着清洗。第二次跑 clean 时带上--dry-run参数脚本只打印将要移动的文件列表不实际移动这一步我强烈建议保留因为误删后在训练阶段才会暴露时间成本高得多。在 PyCharm 里配置好解释器后也可以用同样的命令跑通这三步注意脚本里依赖的第三方库只有 PyYAML 和 Pillow装这两个就够了。不要在这一步引入 torch 或 ultralytics清洗工具应保持轻量跑得快才能让人愿意每次都跑。4.3 文档说明看四件事依赖、样例命令、输出定义、已知限制一份文档说明值不值得信我拿到手会先找四个部分缺一个都要警惕环境依赖清单、样例命令、输出物定义、已知限制。环境依赖用来判断工具能不能在当前环境跑起来样例命令用来验证工具的作者自己是否真的用过它输出物定义用来判断清洗报告字段是否清晰已知限制用来判断它能不能处理你的数据格式。如果文档只有接口说明没有一条可以复制的命令这个工具多半是半成品如果文档里明确写了“当前不支持 COCO 的 instance segmentation 标注”说明作者清楚自己的边界反而更可信。文档说明在落地时扮演三个角色让新手五分钟跑通、让老手知道参数边界、让接手的人敢在三个月后改代码。后面这一点最容易被忽略清洗工具一旦进入团队它的维护者大概率不是你。4.4 清洗结果怎么喂给训练Ultralytics 与 Darknet 两种格式清洗完成后需要把干净数据集转换成训练框架认识的配置。Ultralytics 的 YOLO 系列用的是 dataset.yaml 指向 images 和 labels 目录Darknet 风格则要求 train.txt 和 val.txt 里每行一张图片的绝对路径。建议清洗工具里带一个生成 dataset.yaml 的小脚本# make_dataset_yaml.py import yaml def write_dataset_yaml(path, train_dir, val_dir, class_map): cfg { path: /absolute/path/to/clean_dataset, train: train_dir, val: val_dir, names: class_map, } with open(path, w) as f: yaml.safe_dump(cfg, f, allow_unicodeTrue)一个容易踩的坑是 dataset.yaml 里的 path 写成相对路径导致在别的目录执行训练命令时找不到数据。另一个坑是 names 字典里的 id 必须与清洗时输出的 class_map.json 一致比如自采数据里“person”排第几这个顺序不能有第二种写法。如果项目用到 YOLO 实例分割清洗工具的图片级操作仍然适用删除一张图时要连带删掉对应的 segmentation JSON但坏框检查对多边形标注不适用需要单独检查 polygon 点是否落在图像范围外这是分割标注特有的清洗项。5. 清洗工具落地的五个翻车点现象、原因与排查步骤前面几章把工具搭建和流水线讲完了这一章专门写真实数据上会翻车的地方。这些坑有一个共同特点单看每一项都不难但它们往往要等到训练启动甚至训练结束后才暴露排查成本极高。下面五条按“现象、原因、解决”三段式写清楚碰到类似问题可以直接照着排查。5.1 体检通过但训练报坐标越界检查对象错了现象inspect 报告显示没有任何坏框但 YOLO 训练一启动就报坐标越界或者 loss 出现 NaN进程直接退出。原因清洗工具检查的是 VOC XML 的绝对坐标而训练读的是转换后的 txt。转换脚本里宽高参数传反了或者某个文件用的是上一张图的宽高导致转换结果超出 [0,1] 范围。体检和训练消费的不是同一份标注检查自然对不上。解决清洗流程里一定要加“转换后复检”这一步。在 voc_to_yolo 之类的转换函数里加断言或者 clean 完成后对输出目录里的所有 txt 再跑一遍 inspect。我通常的做法是在 clean.py 末尾自动调用一次针对目标格式的检查函数如果不通过就返回非零退出码后续训练命令不会执行。这样能把错误挡在训练之前。5.2 图像扩展名混用导致漏删文件名比较只看主干现象清洗报告已经把问题样本列出来了但清洗完再看原目录某些标了问题的图片还在训练时 label 与 image 对不上。原因删除脚本里写死了glob(*.jpg)而实际目录里同一文件名的图片是 .png 或 .jpeg 后缀。文件名匹配用的是“完整文件名”而不是“文件名主干”。解决所有清洗工具的文件操作统一走os.path.splitext取主干来匹配。前面 inspect 脚本里已经用了txt_name .*的写法clean 脚本也要保持同样的逻辑。另外注意 .jpeg 和 .jpg 这类同后缀扩展名差异写扩展名列表时最好包含全部可能的变体。5.3 类别 id 错位让所有标签整体漂移class_map 必须单一来源现象训练后 mAP 极低随机抽查图片发现“人”类别的位置输出的是“车”所有预测结果整体错位。原因classes.txt 的顺序来过一次变更。第一个人标注时顺序是 person, car, tree第二个人标注时导出顺序变成 car, person, treeYOLO txt 里类别都是整数 id肉眼完全看不出错。清洗工具如果只关心坐标合法性不检查类别 id 的语义映射这个问题会被直接放行。解决清洗工具必须输出一份 class_map.json并且训练、标注导出、清洗三个环节都引用同一份文件。只要 class_map 顺序变化所有 txt 都要同步重写。训练前再加一个交叉校验统计每个类别 id 的样本数如果某个类别出现 0 样本说明映射可能出问题了。5.4 去重误删不同光照下的有效样本阈值不能只看哈希距离现象清洗后验证集 mAP 反而掉了 1 到 2 个点查 quarantine 目录发现大量“同一地点、不同时间”的照片被当成重复删掉了。原因去重只看感知哈希的汉明距离没有结合标注内容。白天和傍晚的同一辆车像素整体分布接近dhash 距离可能很小但语义上是两个有效样本模型可以从中学到光照鲁棒性。解决去重判定改成双重条件哈希距离是候选条件还必须有一组 bbox 的 IoU 大于 0.8 才确认删除。阈值也要按数据来源调整视频抽帧产生的数据帧间隔小阈值设低一点比如 6行车记录仪间隔较大的数据阈值可以到 16。这个数字没有标准答案每次调完阈值都抽查一下 quarantine 目录看删的是不是真的重复样本。5.5 空标注全删导致背景误检负样本要有保留策略现象清洗后模型在纯背景、无目标画面上频繁输出假目标误检率飙升。原因清洗配置把空标注图当成“数据质量问题”全部删掉了模型在训练里完全没有见过“这里没有目标”的负例推理时自然会把背景纹理当成物体。空标注图不是脏数据它是负样本。解决config.yaml 里的checks.empty不要设成 drop。先设 keep 保留 10% 到 20% 的空标注图或者设 reserve 单独放一个 negative 目录在训练配置里独立挂载。清洗报告里要把 empty 数量单独列出来方便后续调整保留比例。这里没有任何一个工具能替你决定必须结合实际场景判断一个纯背景画面对模型是助力还是噪声。6. 验证清洗效果的最小实验mAP 对比与三分钟体检习惯清洗做完了怎么知道它真的有效最直接的做法是拿同一份原始数据跑两次训练一次用清洗前一次用清洗后网络结构、超参数、训练轮数全部保持一致只改数据入口。我自己会用同一个固定种子训练 50 到 60 个 epoch对比清洗前后的 loss 曲线和验证集 mAP。如果 mAP 提升不足 0.5 个点说明数据源本来就不脏这时候不应该继续在清洗上花时间应该回到模型结构或者数据增强上。如果 loss 曲线从震荡变成平稳下降清洗的作用就体现出来了。6.1 用同一份超参对比清洗前后的 loss 曲线与 mAP对比实验要记录三个指标训练 loss 是否下降更平稳、验证集 mAP 的变化、以及每个类别 AP 的分布。清洗最典型的收益不是 mAP 整体暴涨而是低频类别的 AP 明显回升、误检率下降。看指标时要注意一点清洗后总样本数变少训练轮数可能需要相应增加否则对比的是“更少数据”而不是“更干净数据”。6.2 把体检脚本焊在训练流程前面省掉一个星期的重训时间最后说一个我自己的习惯也不算什么高级技巧就是把 inspect 脚本放在每次训练命令之前让数据体检成为默认前置步骤。新数据进目录的那一刻就自动跑一遍体检再决定是否进入训练队列。养成这个习惯之后我几乎没有再遇到过训练三天后才发现类别顺序标错的局面。清洗工具的价值不在于它有多智能而在于它把数据质量从不可见变成可见。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站