ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

X光安检数据集实战:VOC/COCO/YOLO格式转换与YOLO训练调参指南

X光安检数据集实战:VOC/COCO/YOLO格式转换与YOLO训练调参指南 简介面向目标检测学习者和安检场景开发者这份资源汇集1000张真实X光安检图片画面场景丰富标注框质量高同时给出VOC、COCO、YOLO三种常见格式标签标签按格式分目录存放便于切换训练框架可直接用于YOLO系列模型训练。压缩包共2000个文件约105.65MB主要包含xml标注、txt标签、yaml配置、py脚本和html教程覆盖从环境搭建、数据划分到模型训练的完整流程。目前已有254人学习下载。配套教程分别覆盖Windows和Linux环境讲解YOLO环境搭建及按案例修改训练自己的数据集附带的三个Python脚本可灵活生成训练集、验证集、测试集并自动写入对应文件夹或生成索引文件省去手动处理数据集的繁琐操作。对需要快速上手安检目标检测或入门YOLO的开发者来说是一份能直接落地、也适合课程设计与毕业设计参考的实用资料。1. 拿到一份 X 光安检数据集后先别急着开训做目标检测的都知道数据集的坑往往比模型还多。这份标题里写着“1000张X光安检图VOC/COCO/YOLO三格式划分脚本训练教程”的压缩包看起来是一份别人整理好、开箱即用的东西但实际上真正让它“开箱即用”的不是你解压完就能直接训而是你要能说清楚三个问题三种格式里的标签和图片对不对得上、划分脚本按什么规则切数据、训练参数在安检场景下怎么调才不翻车。这篇笔记就按这个顺序展开。我会把 X 光安检图像本身的特点先说明白然后把 VOC、COCO、YOLO 三种格式之间的差异和转换逻辑讲透接着拆解划分脚本的常见写法最后落到 YOLO 训练的关键配置和踩坑记录。读者不管是学生做课题、工程师做原型验证还是想把这个方向积累成自己的数据集资产这篇都能当一份可照着做的操作手册。2. X 光安检图像为什么让检测模型“水土不服”2.1 安检图与你平时训练的自然图像到底差在哪很多人拿到这个数据集第一反应是直接用 YOLO 的默认参数开训结果 mAP 低得怀疑人生。问题很少出在代码上而是出在图像本身。X 光安检图不是自然光下的照片它有四个天然属性直接影响你怎么配参数、怎么做预处理。第一透视叠加。X 光穿过物体包里的手机、充电宝、钥匙在投影面上是彼此重叠的。检测框之间大量存在高 IoU 的遮挡关系这也是为什么文档里推荐使用较高 NMS 阈值或做分阶段的检测策略。YOLO 默认的 NMS 是 0.45在自然图上没问题到了安检图上会让很多本来被遮挡的目标直接被抑制掉。第二灰度单通道。安检机输出的原始图像大多是单通道灰度伪彩色只是后处理做出来的显示效果。YOLO 在输入时通常期望三通道 RGB这里有一个很多人没注意到的点如果直接把灰度图复制成三通道模型理论上也能学但你等于在特征提取的第一层浪费了通道注意力如果做伪彩色映射不同映射函数会直接影响模型的收敛行为。第三近距离大目标与多尺度。安检机成像距离固定物体在视野里的尺度分布和 COCO 那种“包含小目标”的设定完全不同。比如一个行李箱里一瓶水可能占图像面积的 10%而一把钥匙只有 0.5%。YOLOv8 的 Anchor-Free 设计对这种尺度分布还算友好但输入分辨率不能直接用默认的 640要结合你的实际图像尺寸去调。第四材质成像差异。金属、液体、有机物、陶瓷在 X 光下的灰度响应各不相同。有些物品比如液体边缘模糊没有明显的纹理特征模型很容易把注意力放到背景噪声上。我在实际训练时发现如果不过度做随机裁剪和颜色扰动mAP 会有显著提升——因为这个场景下“不变性”的假设比其他场景弱得多。2.2 紫线、橙线、绿线别忽略 X 光图里的“材质语义”做过安检机图像处理的人都知道安检机图片里往往叠加了材质分类的伪彩色提示——有机物显示为橙色无机物显示为蓝色金属物品边缘常带有紫色高亮。这类信息本质上是一种“半监督语义”它已经帮我们把一部分分类问题提前做了预解答。所以用 YOLO 训练安检数据集时一个常规操作是不要直接丢弃原始灰度信息也不要直接拿伪彩色图训练。更好的做法是把原始灰度和材质信号用通道拼接的方式合在一起形成一个 4 通道或 6 通道输入然后在网络第一层做一次卷积适配。不过我一般不对刚接触这个领域的人推荐这种改网络结构的方案第一版先用灰度图复制成三通道去训练拿到一个基准结果再说。这符合“先跑通、再调优”的原则也避免一上来就陷入数据预处理和网络结构同时引入变量的泥潭。2.3 数据规模只有 1000 张你首先要做的是“数据可信度检查”1000 张图在目标检测任务里属于“小数据”。除非你的类别非常少且目标都非常显著否则这个数据量不足以让模型学到一个泛化能力很强的特征表达。所以在任何训练之前第一步不是写训练脚本而是做数据可信度检查。我按以下顺序过一遍图片能正常打开吗有没有损坏的 JPEG 或 PNG 文件标注框是否都在图像边界内——特别是坐标是负数或超出宽高的类别标签是否连续、从 0 开始且没有空标签文件每张图的标注框数量分布有没有大量图片是空标注不同类别的实例数量分布有没有某些类别只有十几二十个框。前三项是格式问题有脚本能批量查。第四项和第五项直接决定你后面要不要做类别重采样或合成增强。比如测试集里如果某个类别的实例数太少评测出来的 AP 方差会非常大一次训练好与坏根本说明不了问题。我通常要求测试集里每个类别至少要有 30 个以上的实例否则就重新切分。3. 三种格式的标签到底在说什么VOC、COCO 与 YOLO 的坐标哲学差异3.1 VOC 的 XML 与 YOLO 的 TXT一个像素坐标系一个归一化坐标系标题里写了同时提供 VOC、COCO、YOLO 三种格式这是这份数据集最值钱的地方也是最容易出问题的地方。很多人以为三种格式只是文件后缀不同实际上是坐标系的根本差异这个理解不到位后面所有脚本都会写错。VOC 格式每个标注文件是一个 XML里面最核心的是bndbox节点给出目标的左上角和右下角像素坐标即xmin, ymin, xmax, ymax。这个坐标是绝对像素值看图说话人眼可以直接对应。其优点是直观缺点是不同尺寸的图片之间没法直接混用——你训练时如果做 resize坐标得跟着改不然就全偏了。COCO 格式把所有标注集中到一个 JSON 文件里用 categories、images、annotations 三个数组组织。每个 annotation 里的 bbox 是[x, y, width, height]同样是像素坐标系只是从“两个点”换成了“起点宽高”。另外 COCO 还强制要求每一张图都有唯一的 id所有标注通过 image_id 关联这个关系如果不一致转换脚本必然崩。YOLO 格式的训练标签是每个图片对应一个同名 TXT 文件每一行对应一个目标格式是class_id x_center y_center width height全部是归一化到 [0,1] 的浮点数。注意YOLO 里没有直接给两个角点坐标而是给中心点坐标和宽高且全部除以图像宽高做了归一化。有个很隐蔽但致命的点COCO 的 bbox 坐标是“左上角 宽高”YOLO 的 bbox 坐标是“中心点 宽高”VOC 是“左上角 右下角”。三种格式看起来相似但相互转换时不能只是改文件格式一定要做坐标系换算。网上很多“convert”脚本翻车就是只改了文件后缀没做坐标换算。3.2 用 Python 把 VOC XML 转成 YOLO TXT一份可直接抄的脚本下面这份脚本假设数据集文件结构是标准的 VOC 风格Annotations文件夹里全是 XMLJPEGImages里全是和 XML 同名的图片。脚本做的事是遍历所有 XML解析出每个目标的类别和 bbox转成 YOLO 格式写入 TXT 文件。import os import xml.etree.ElementTree as ET def voc_xml_to_yolo_txt(xml_path, out_dir, class_names): os.makedirs(out_dir, exist_okTrue) tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) out_lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in class_names: continue cls_id class_names.index(cls_name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 坐标裁剪防止越界 xmin max(0, min(xmin, img_w)) xmax max(0, min(xmax, img_w)) ymin max(0, min(ymin, img_h)) ymax max(0, min(ymax, img_h)) # 转成 YOLO 格式中心点 宽高归一化 box_w xmax - xmin box_h ymax - ymin x_center xmin box_w / 2.0 y_center ymin box_h / 2.0 x_center_norm x_center / img_w y_center_norm y_center / img_h box_w_norm box_w / img_w box_h_norm box_h / img_h out_lines.append(f{cls_id} {x_center_norm:.6f} {y_center_norm:.6f} {box_w_norm:.6f} {box_h_norm:.6f}) if out_lines: xml_name os.path.splitext(os.path.basename(xml_path))[0] out_path os.path.join(out_dir, xml_name .txt) with open(out_path, w) as f: f.write(\n.join(out_lines)) # 使用示例 class_names [gun, knife, liquid, lighter] # 按你的类别顺序填 xml_folder Annotations out_folder labels for xml_file in os.listdir(xml_folder): if xml_file.endswith(.xml): voc_xml_to_yolo_txt(os.path.join(xml_folder, xml_file), out_folder, class_names)逻辑说明脚本先从 XML 的size节点拿到图像的宽高这是坐标换算的前提。然后遍历每个object节点取出类别名和 bbox 坐标把 VOC 的“左上角右下角”换算成 YOLO 的“中心点宽高”最后除以图像宽高做归一化。注意我加了坐标裁剪这一步避免某些标注工具的越界误差直接进入训练。有几个参数你要重点确认class_names的顺序就是模型训练时的类别 ID 顺序必须和之后训练的 data.yaml 保持一致。如果你在标注时用了不在列表里的类别名会被直接跳过输出文件中就会少掉某些目标——这一步静默发生最坑。我的习惯是转换完以后统计一遍所有 TXT 里的类别 ID 分布和 VOC 里的类别实例数做交叉核对。3.3 从 YOLO 到 COCO为什么 JSON 格式总是对不上反过来把 YOLO 格式转成 COCO JSON 也是常见操作。很多人做模型对比实验时需要在一个统一的 COCO 数据格式下评测多个模型这时候手里只有 YOLO 格式的标签就需要转换。import json import os from PIL import Image def yolo_txt_to_coco_json(img_folder, label_folder, class_names, output_json): images, annotations [], [] ann_id 1 for img_name in os.listdir(img_folder): if not img_name.lower().endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(img_folder, img_name) with Image.open(img_path) as img: img_w, img_h img.size img_id len(images) 1 images.append({ id: img_id, file_name: img_name, width: img_w, height: img_h }) label_path os.path.join(label_folder, os.path.splitext(img_name)[0] .txt) if not os.path.exists(label_path): print(f警告: {img_name} 没有对应标签文件) continue with open(label_path, r) as f: for line in f.readlines(): parts line.strip().split() if len(parts) ! 5: print(f跳过非法行: {img_path} - {line}) continue cls_id int(parts[0]) x_center float(parts[1]) * img_w y_center float(parts[2]) * img_h box_w float(parts[3]) * img_w box_h float(parts[4]) * img_h xmin x_center - box_w / 2.0 ymin y_center - box_h / 2.0 annotations.append({ id: ann_id, image_id: img_id, category_id: cls_id 1, # COCO 类别从 1 开始 bbox: [xmin, ymin, box_w, box_h], area: box_w * box_h, iscrowd: 0 }) ann_id 1 coco_json { images: images, annotations: annotations, categories: [{id: i 1, name: name} for i, name in enumerate(class_names)] } with open(output_json, w) as f: json.dump(coco_json, f, indent2) # 使用示例 yolo_txt_to_coco_json(images/train, labels/train, class_names, train.json)注意看脚本里的category_id: YOLO 的类别 ID 从 0 开始但 COCO 的类别 ID 从 1 开始。这个 1 的偏差是绝大多数转换脚本出 bug 的地方——漏掉它模型能跑但评测时类别全对不上mAP 直接废掉。另外 COCO 要求每个 annotation 必须有area字段它是 bbox 宽高的乘积不是多边形面积别拿分割标注的逻辑来算。和上一份脚本一样转换完要做验证。这里有一个我一直保留的习惯把 JSON 里的标注数量和三份原始 YOLO TXT 里的行数做对比如果数量不一致说明文件组织有错位比如图片和标签文件名没对齐。这种问题在数据集制作时非常常见不多跑一步校验后面训练时的损失曲线会莫名其妙地降不下去。4. 划分脚本为什么 8:1:1 的随机切分不靠谱4.1 按类别分布做分层采样才是正道拿到数据集后第一件事是划分 train/val/test。最省事的写法是random.shuffle以后按 8:1:1 切但如果你真的这么干在 1000 张这种小数据集上很有可能出现某个类别在验证集里只有几个框的情况。评测时那个类别的 AP 波动极大严格来说这个实验就没有统计意义了。正确做法是按类别分布做分层采样。也就是说先统计每个类别在每张图里出现的次数然后让划分后的三个子集在类别分布上尽量接近原始数据的比例。sklearn 里没有直接支持多标签分层采样的函数一个简化但有效的方案是用“图像中包含的类别组合”作为分层键。比如一张图里同时有“枪”和“刀”另一张只有“液体”它们的分层键不同。下面给出一份实用的划分脚本核心逻辑是把图片按“包含的类别组合”分组再在每个分组内按比例抽到 train、val、test。import os import random from collections import defaultdict def stratified_split(label_folder, img_folder, train_ratio0.8, val_ratio0.1, seed42): random.seed(seed) # 1. 读取每张图片的类别组合 img_label_map {} for txt_name in os.listdir(label_folder): if not txt_name.endswith(.txt): continue img_stem os.path.splitext(txt_name)[0] txt_path os.path.join(label_folder, txt_name) cls_set set() with open(txt_path, r) as f: for line in f: parts line.strip().split() if len(parts) 5: cls_set.add(int(parts[0])) img_label_map[img_stem] cls_set # 2. 按类别组合分组 group_dict defaultdict(list) for img_stem, cls_set in img_label_map.items(): group_key tuple(sorted(cls_set)) # 组合作为键 group_dict[group_key].append(img_stem) train_list, val_list, test_list [], [], [] # 3. 在每个组内按比例抽取 for group_key, img_stems in group_dict.items(): random.shuffle(img_stems) n len(img_stems) n_train int(n * train_ratio) n_val int(n * val_ratio) train_list.extend(img_stems[:n_train]) val_list.extend(img_stems[n_train:n_train n_val]) test_list.extend(img_stems[n_train n_val:]) # 4. 写文件列表 for split_name, split_list in [(train, train_list), (val, val_list), (test, test_list)]: out_path f{split_name}.txt with open(out_path, w) as f: for img_stem in split_list: img_path os.path.join(img_folder, img_stem .jpg) f.write(img_path \n) print(f{split_name}: {len(split_list)} 张) return train_list, val_list, test_list # 使用示例 stratified_split(labels, images)逻辑说明脚本第一步把每张图片的类别组合读出来比如图片 A 包含类别 {0, 2}图片 B 包含类别 {1}。第二步把类别组合相同的图片归到同一组。第三步在每组内独立做随机划分这样切分后每个子集的类别分布天然保持了原始数据的比例。第四步输出图片路径列表这份列表可以直接给 YOLO 的训练脚本用。4.2 划分完之后还要做两件事检查一致性、记录版本划分完之后不要急着训练还有两个常规动作。第一重新统计 train/val/test 里的类别框数量打印成一张矩阵表——行是类别列是三个子集肉眼确认没有哪一列出现“0 实例”的情况。第二把划分结果存一份带时间戳的数据清单和模型权重文件放同一个目录。这样每次实验你都能追溯到“这个模型是用哪一批图片训出来的”。很多人忽略版本记录这件事直到某天发现实验对不上了不知道是数据变了还是代码变了。我的习惯是在每个实验目录下放一个data_manifest.yaml内容就是三份列表的文件名、哈希值、类别数、总框数。一句话数据划分不是一次性的准备工作而是整个实验流程里的一等公民。5. 训练教程的实操细节YOLOv8 在安检数据上的配置与参数调整5.1 数据目录与 data.yaml 的正确写法当你手里已经是 YOLO 格式的标签时训练的工作量主要在配置上。以 YOLOv8 为例AC 自定义版、v8 或后续版本思路一致第一步是组织数据目录。有一个常见误区是直接把标签放进images文件夹里在同一个目录下放图片和标签。YOLO 官方的数据加载器不做报错它只是安静地找不到标签文件然后训练出的模型就是背景检测器——损失在降但 mAP 永远为零。标准目录结构长这样dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ ├── val/ │ │ └── ... │ └── test/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── ... │ ├── val/ │ │ └── ... │ └── test/ │ └── ... └── data.yamldata.yaml的内容如下path: /absolute/path/to/dataset train: images/train val: images/val test: images/test nc: 4 names: [gun, knife, liquid, lighter]有一个细节值得留意path字段建议写绝对路径。Ultralytics 的 YOLO 在处理相对路径时会相对于当前工作目录去解析一旦你换了个路径运行脚本就会报 FileNotFoundError。绝对路径虽然不方便移动到别处但至少保证你自己不会在环境切换时被坑。我用 YOLO 训练这么多次最烦的就是这种“换个登录目录就崩”的路径玄学。5.2 必调的训练参数与推荐值YOLOv8 开箱即用的默认参数是基于 COCO 数据集调出来的。COCO 有 33 万张图片、80 个类别你的安检数据只有 1000 张、4 到 8 个类别直接套默认参数意味着你在用一个为大数据设计的配方去训一个小数据任务效果不可能理想。下面是我的经验参数表按重要性排序列出参数默认值安检场景推荐理由imgsz6401280 或原图等比例安检图像细节都在中小编码区域640 会丢失纹理batch168 或 4看显存图像分辨率上去了显存紧张宁可小 batch 保精度epochs100200 起步数据量小需要更多轮次收敛patience5080小数据 loss 震荡大太早早停会错过最佳点mosaic1.00.5 或关闭安检图目标重叠严重mosaic 拼图会加剧重叠复杂度close_mosaic1030让最后 30 轮用真实分布微调不再做拼接增强fliplr0.50.0X 光安检物品有左右语义比如枪的朝向不建议水平翻转optimizerautoSGD 或 AdamW小数据自动选择器不稳定手选更可控特别要解释mosaic这一项。YOLOv8 默认开启马赛克增强把四张图拼在一起训练。在自然图像上这是非常有效的正则化手段但在 X 光安检图上目标本身互相遮挡严重拼图以后模型会看到更多“半遮挡半截目标”学出来的特征容易偏向局部纹理而不是整体形状。我的实际体感是把 mosaic 从 1.0 降到 0.5 之后验证集 AP 大约有 2 到 3 个点的提升。另外fliplr建议关掉因为 X 光图像里有相当一部分物品存在明显的“朝向”语义对半折叠的刀和枪左右翻转等于在教模型忽略方向信息。训练命令如下yolo detect train datadataset/data.yaml modelyolov8s.pt \ imgsz1280 batch8 epochs200 patience80 \ mosaic0.5 close_mosaic30 fliplr0.0 \ projectruns/xray_security nametrain_v15.3 训练起来以后你要盯的不是 loss而是这三样很多人训练时一直盯终端里打印的 loss这是除了不调参之外最大的时间浪费。loss 是训练集的拟合程度指标和你最终关心的“测试集上的检测效果”不是一回事。尤其在安检场景类别不均衡的情况下总 loss 平滑下降但某个稀有类别的 AP 可能一直在零附近。你要盯的是以下三样东西第一results.png里的验证集 mAP50 和 mAP50-95 曲线。如果训练了 100 轮 mAP50 还在 0.1 以下大概率是标签和图片没对上先别调参回去检查数据。第二confusion_matrix.png。看哪些类别之间混得最多。比如刀和钥匙经常混说明检测框是准的但 ROI 区分度不够这时需要回去检查标签里有没有把小刀标成钥匙或者反向标注。第三train_batch*.jpg和val_batch*.jpg的增强预览图。不要只看最终结果要看训练时喂给模型的图长什么样。如果 mosaic 拼图后目标被裁得只剩一个角那说明这个增强强度在这个场景下不合适回去调 mosaic 比值。我还遇到过一种训练不起来的情况前 20 轮云里雾里loss 在降但 mAP 完全不动。后来把label_smoothing从默认 0.0 调到 0.1 后有了明显改善。对小数据集来说标签平滑能让模型对标注噪声不过拟合特指安检数据里目标边缘标注本身就有主观性的问题。5.4 验证预测效果时用什么方式最直观训练完成后验证脚本是yolo detect predict modelruns/xray_security/train_v1/weights/best.pt \ sourcedataset/images/test/ save_confTrue save_txtTrue跑完之后用随机抽样的方式挑 20 张预测结果图人工看一遍。这一步不要用程序算完 mAP 就走人一定要做。我见过 mAP50 到了 0.85 但实际预测图上有很多漏检的情况原因是评测指标是框级别的匹配一些小目标漏检不会显著拉低 mAP但在真实场景里漏掉一把水果刀就是完全不可接受的。如果你有真值标签还可以做一次“按类别分组的可视化对比”把每个类别的预测置信度分布直方图画出来观察哪些类别的置信度普遍偏低。这个做法比单一的数字指标更能指导下一步优化方向——置信度偏低说明特征学得不够好置信度偏高但误检说明样本分布有问题。6. 避坑与常见问题排查五个高频警情6.1 现象训练时 loss 降了但 mAP 一直是 0原因有几种可能性最常见的是标签文件和图片文件名对不上。YOLO 的标签检索规则是把图片文件名的主干部分拿出来去labels目录里找同名.txt文件。如果你的图片叫做IMG_20230101_123456.jpg标签却叫IMG_20230101_123456.txt和图片名完全一致这才对得上。一旦你对图片做了重命名但没同步改标签名训练就变成了“标注全部为空”。排查方法很简单在训练脚本里加一段检查代码统计有多少图片文件对应不上标签文件。我这里给出一段 shell 命令find images/train -name *.jpg | sed s/\.jpg$// | sort /tmp/img_list.txt find labels/train -name *.txt | sed s/\.txt$// | sort /tmp/label_list.txt diff /tmp/img_list.txt /tmp/label_list.txt | head -20如果 diff 有输出立即停训把文件名对齐再回来。6.2 现象训练到一半报错CUDA out of memory这个在安检场景下尤其常见因为你把imgsz调到 1280 后显存占用是按输入面积平方增长的。解决思路依次是先把batch减半从 8 减到 4如果还爆就减到 2再把workers从默认 8 减到 4减少数据加载的内存峰值最后还是不行就把imgsz从 1280 降到 960。降输入尺寸对 mAP 的影响远小于训练中断带来的时间损失先用能跑起来的配置拿到 baseline 才是正道。另外检查一下你的 PyTorch 是不是 CUDA 版本对应的版本。很多人环境里 pip 装的 torch 是 CPU 版本报错会显示torch.cuda.is_available()返回 False和显存溢出不是一回事别搞混。6.3 现象验证集 mAP 高但新图片上检测效果极差这是泛化问题根源几乎都是数据划分泄漏——训练集、验证集、测试集里出现了同一来源的相似图片。安检场景的特殊之处在于同一台安检机、同一个箱包在短时间内连续拍照得到的图像之间相似度极高。如果你按文件名顺序切分前 80% 做训练、后 20% 做测试等于让模型记住了这批箱包的外观而不是学会了“结构上像刀的东西是刀”。正确做法是按“采集批次”或“箱包编号”做分组切分确保同一个来源的图像全部落在一个子集里。如果数据集里没有分组标签至少做一次图片哈希去重把完全相同的图片从训练集和测试集两边同时剔除再重新划分。6.4 现象标注框全是 [0, 0, 0, 0] 或宽高为 0这个问题的根源在转换脚本。VOC 的 XML 里如果某个bndbox节点的xmax和xmin相等或者某个标签里 yolo 格式的 width 填了 0后续训练时 YOLO 会直接跳过这个标注或者报错all bbox points are the same。我的处理方式是在转换脚本里加一道过滤宽高小于 1 像素的目标直接剔除并打印警告。别想着“先留着也许模型能扛”——这种坏标注只会把训练损失搞成 NaN。清理要果断删掉的比例记录下来以后写数据报告时说明即可。6.5 现象训练后各类别 AP 差距悬殊假如你的数据集里 “gun” 有 800 个实例“lighter” 只有 40 个实例训练后 lighter 的 AP 大概率非常难看。这时候优先做的事情不是调模型而是去数据层面做文章。第一个选择是类别重采样让每个 batch 里稀有类别的图片占比更高。YOLO 的数据加载器没有直接暴露这个参数但你可以通过重复采样稀有类别的图片来实现第二个选择是做针对性的裁剪增强把包含稀有类别目标的区域裁剪出来放大后再训练。第三个选择是干脆接受现状明确告诉你自己“这个类别在当前数据规模下就是测不准”把它从定量评测改成定性观察。这不是躺平是研究边界——知道什么能测、什么测不了比盲目刷一个虚高的指标有价值得多。7. 把这份资源用到极致的三个进阶技巧7.1 类别不均衡时用“两阶段训练”代替单次训练如果你已经跑通了上面所有流程想进一步提升安检场景下的稀有类别检测效果我建议试一下两阶段训练。第一阶段用全部数据以常规配置训练到收敛第二阶段冻结 backbone 层把学习率调低到原本的十分之一同时用稀有类别占比更高的重采样数据集微调整个检测头。这个做法背后的直觉在于backbone 负责提取通用视觉特征第一阶段已经学到位了检测头负责把特征映射到具体类别第二阶段用更多稀有类别样本去校准决策边界。在 X 光安检这类类别不平衡严重的小数据集上它的提升幅度往往比任何结构改进都直接。YOLOv8 中冻结 backbone 可以用freeze10参数表示冻结前 10 层。yolo detect train datadataset/data.yaml modelruns/xray_security/train_v1/weights/best.pt \ imgsz1280 batch8 epochs100 lr00.0001 freeze10 \ mosaic0.5 close_mosaic30 fliplr0.0 \ projectruns/xray_security nametrain_v2_finetune7.2 切分完成后用“交叉验证”代替单次划分1000 张图片做单次 8:1:1 划分验证集只有 100 张每个类别的 AP 方差会大到让你的实验结论不可靠。更稳妥的做法是用 5 折交叉验证把数据切成 5 份轮流拿 1 份做验证、其余 4 份训练最终报告 5 次的平均 mAP 和标准差。这在 YOLO 里没有内置支持需要自己写循环。代价是训练时间变成 5 倍所以不是每次实验都做。我的建议是在最终验证“这个数据集方案到底行不行”的时候跑一次完整的 5 折在日常调参阶段用单次 8:1:1 就行只做相对比较。这个习惯帮我避开过好几次“调参调出幻觉”的坑——单次划分的波动经常比调参带来的提升还大。7.3 用“人工抽检清单”守住质量底线每次训练完我都要求自己按固定格式输出一份人工抽检记录。清单格式非常简单随机抽 20 张测试图逐张记录“真实目标数”、“正确检测数”、“误检数”、“漏检数”最后算出每张图的检出率和误检率再取平均。这份记录的价值在于它能让你在未来某天回头审视“这个模型到底能不能用”时有一个不是 mAP 数字的、更接近真实体验的判断依据。尤其对安检这类对漏检零容忍的场景误检可以靠置信度阈值调节——把检测阈值从 0.25 调到 0.5误检会成倍下降但漏检是硬伤阈值调再高也救不回来。所以我的习惯是调阈值时只看漏检率误检率作为辅助参考。这个排序逻辑在安检方向里非常重要它决定了你做模型部署时怎么选置信度门限。以上是这份数据资源在技术上能走到的几个层面先确认数据可信再做格式转换和分层划分接着把训练参数按场景特征调整最后用交叉验证和人工抽检守质量。每一步都有可复现的脚本和明确的检查点照着走一遍拿到一个可靠的基准结果不是难事。希望这些经验能帮你在自己的项目里少走一趟弯路多留一点时间给真正需要动脑的地方。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进