
简介面向目标检测学习者和零售补货场景开发者这份超市货架空置/缺货检测数据集共4470张图片标注了Empty-Space空位与Reduced商品减少两类目标。其中Empty-Space框数7570个、Reduced框数16205个矩形框总数达23775个为补货判断提供细粒度监督信号。压缩包共2000个文件以1999个xml标注文件为主另含1个说明txt整体大小约251.17MB图片与标注采用Pascal VOC和YOLO双格式既适合YOLO系列模型快速训练也便于通过labelImg查看或二次修正。数据使用labelImg按矩形框规则标注类别边界清晰覆盖常见空置与商品减少场景可直接用于训练货架状态识别模型或作为迁移学习、模型评测的基准数据。已有380人浏览学习适合需要真实场景标注样本的中高级目标检测学习者与零售行业算法工程师。1. 货架空置缺货检测数据集零售巡检里最难标注的那一类数据零售巡检场景里货架空置检测一直是个“看着简单、落地玄学”的视觉任务。表面上是判断货架上哪里空了可模型必须分得清层板边缘、商品间距、阴影反光和真实缺口的区别。这份数据集给出了 4470 张真实货架图用目标检测领域最常见的 VOC 与 YOLO 两套格式组织成 2 类标签把数据准备这一步替你补齐了。对于想用 YOLO 做超市、便利店缺货巡检的工程师这份数据是标准的启动原料。它解决的是“没有可控训练数据”的冷启动问题既能用来验证模型结构选型也可以作为迁移学习的基底再用自己门店的少量图片做微调。需要留个心眼的是这份数据的类别口径和标注规范动手前要先和你的业务场景对齐否则容易出现“模型学的是货架结构业务要的却是补货工单”这种落差。2. 数据集拆解VOC 和 YOLO 格式分别装了什么2 类标签怎么分工2.1 VOC 格式的目录结构与 XML 标注字段先把压缩包解压到本地VOC 数据集的目录组织比较固定。我一般先看顶层目录结构确认 Annotations、JPEGImages 这些核心目录是否存在再决定后面的脚本往哪写。unzip supermarket_empty_shelf_dataset.zip -d ./dataset tree ./dataset -L 2这一步不是走过场。有些数据集会顺手带上 ImageSets/Main 下的 train.txt、val.txt有些则不带。如果带了训练前划分可以直接引用如果没带就需要我们自己在后面用脚本生成。另外注意图片统一是 jpg 还是 png这会影响后续读取路径的拼接方式。解压后花几分钟把 Annotations 里任意一个 XML 读出来确认标注字段是否完整。一个标准的 Pascal VOC 标注文件包含图片尺寸、文件名以及每个目标的类别名和矩形框坐标格式长这样annotation folderJPEGImages/folder filenameIMG_20240312_103456.jpg/filename size width1920/width height1080/height depth3/depth /size object nameempty/name bndbox xmin732/xmin ymin205/ymin xmax986/xmax ymax452/ymax /bndbox /object object namenormal/name bndbox xmin112/xmin ymin204/ymin xmax688/xmax ymax451/ymax /bndbox /object /annotation字段含义并不复杂。filename 对应当前图片文件名size 里的 width 和 height 是原图宽高后面转 YOLO 格式时归一化要依赖这两个值。object 下嵌套的 name 是类别名bndbox 里四个值是矩形框的左、上、右、下像素坐标。字段含义注意事项filename对应图片名需与 JPEGImages 下实际文件名一致size/width、height原图宽高转归一化坐标时必用错了全盘错object/name类别名全数据集应该只有两个固定取值bndbox像素绝对坐标原点在左上角x 向右、y 向下这类数据集一般把两个类别定为 normal 和 empty具体命名以压缩包里的 labelmap 或 names.txt 为准。normal 表示当前货架区域陈列正常empty 表示出现了空置、缺货或明显的陈列缺口。理解到这层后面训练配置里 names 列表的填写顺序就清楚了。VOC 格式的好处是可视化方便。用 OpenCV 或 PIL 读取 XML 里的坐标直接在原图上画框能快速检查标注质量。但 YOLO 系模型训练不吃 XML它要的是每张图对应一个 txt 的归一化坐标格式所以下一步必须做格式转换。2.2 YOLO 格式的归一化坐标与 2 类标签的分工逻辑YOLO 格式是“一图一 txt”每行描述一个目标五个数字分别是 class_id、center_x、center_y、width、height。所有坐标值都归一化到 01 区间与原始分辨率解耦。打开任意一个标注文件你会看到这样的内容1 0.4474 0.3039 0.1323 0.2287 0 0.2083 0.3032 0.3000 0.2287第一行含义是类别 id 为 1假设 empty目标中心点位于图像宽度的 44.74%、高度的 30.39%框宽占全图宽度的 13.23%框高占全图高度的 22.87%。这个格式的好处是训练时不管输入图片被缩放到 640 还是 960标注都不需要跟着变。VOC 的像素坐标转成 YOLO 归一化坐标核心就四个公式。center_x 等于 xmin 加 xmax 除以 2 再除以图片宽度center_y 同理用 ymin 和 ymax 计算width 等于 xmax 减 xmin 除以图片宽度height 同理。这一步看起来简单但边界情况特别多比如标注框超出图像边界、坐标是字符串没转 float、图片宽高填错转出来全是负数或大于 1 的异常值。关于 2 类标签的分工很多人一开始会误以为 normal 类是多余的只留 empty 一类也能训。实际上在货架场景里normal 框提供了“正常陈列长什么样”的上下文。超市货架有层板、支撑柱、价签条这些结构和商品混在一起如果训练集里只有空置框模型没有见过“陈列整齐”的正样本长什么样推断时很容易把层板间隙、商品间距误判成缺口。带上 normal 类相当于给了模型一个对比学习的参照系在背景复杂时能明显缓解误报。另外一个容易忽略的点是有些 XML 文件里可能一个 object 都没有这种图是纯背景图也会作为负样本参与训练。转换脚本里千万不要跳过这类图片txt 文件为空是可以的它帮助模型学会在正常货架图上不输出任何框。3. 把 VOC 转成 YOLO 格式并用脚本划分训练集3.1 自己动手写一个 VOC 转 YOLO 的转换脚本虽然网上有现成的 convert 工具但我建议至少自己手写一遍转换脚本。原因有两个一是货架数据集里偶尔会出现目标框贴边、超出图像边界的脏标注现成工具往往直接报错或跳过二是如果你后面要自己继续标注门店数据脚本就是你的数据流水线后面接增强、接统计都方便。我一般会用 Python 写一个函数来处理核心逻辑如下import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue cls_id class_map[name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 坐标裁剪到图像边界内 xmin max(0, xmin) ymin max(0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) cx ((xmin xmax) / 2) / img_w cy ((ymin ymax) / 2) / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h # 框宽高小于1个像素的脏数据直接丢弃 if bw 0 or bh 0: continue lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if lines: txt_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) class_map {normal: 0, empty: 1} xml_dir dataset/Annotations out_dir dataset/labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(xml_dir, xml_file), out_dir, class_map)代码逻辑分三块。第一块读取 XML 里的 size 节点这是归一化的分母如果图片尺寸读错后面所有坐标都会错位。第二块遍历每个 object从 bndbox 里取四个边界值用一个类名字典把类别名映射成数字 id。第三块做坐标换算先裁剪到图像边界内再按公式计算中心点和宽高的归一化值。参数说明class_map 决定类别名到数字 id 的映射顺序必须和后面 data.yaml 里的 names 列表一一对应。缩放到 6 位小数是为了减少浮点误差ultralytics 读取时不会因为位数问题丢框。裁剪这一步不是可有可无货架图片常有标注框稍微超出图像边缘的情况不裁剪算出来的 center_x 或 width 可能大于 1模型训练时会出现 loss 异常。转换完成后做一次统计检查。对于 xml 里没有 object 的图片我们期望它对应一个 0 字节的空 txt 文件这是正常的。可以用下面这段脚本快速排查确实有些图在标注时就被漏掉了那就从训练集里剔除而不是让它带着错标签进模型。import os label_dir dataset/labels empty_files [] for f in os.listdir(label_dir): path os.path.join(label_dir, f) if os.path.getsize(path) 0: empty_files.append(f) print(f空标注文件数量: {len(empty_files)}) for name in empty_files[:20]: print(name)打印出来的空标注文件要对照原图人工确认。货架上确实没有空置也没有商品遮挡的纯背景图可以保留如果图片上明明有很明显的空置区域XML 里却没标说明这个样本有问题放进训练集只会给模型传递错误信号。3.2 按门店或批次划分训练、验证、测试集而不是随机打散转换出标签之后下一步是划分数据集。很多人在这一步直接 random shuffle按 7:2:1 把图片随机分成 train、val、test。这种做法对一般目标检测任务勉强能用但对货架空置检测是隐患。超市货架图片通常是连拍序列同一排货架在几分钟内连续拍好几张相邻帧的差异只有顾客遮挡和角度微变。如果随机划分同一个货架的连续帧会同时出现在训练集和验证集验证集的指标虚高模型看起来 mAP 能有 0.85上了真实新门店却翻车严重。这就是典型的数据泄漏。我一般会按图片命名的前缀分组比如 IMG_20240312_103456 和 IMG_20240312_103458 属于同一次采集应当划到同一个集合里。import os import random from collections import defaultdict from shutil import copyfile random.seed(42) img_dir dataset/JPEGImages label_dir dataset/labels imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] # 按采集批次前缀分组 groups defaultdict(list) for img in imgs: prefix img.rsplit(_, 1)[0] groups[prefix].append(img) group_names list(groups.keys()) random.shuffle(group_names) total len(group_names) train_groups group_names[:int(total * 0.7)] val_groups group_names[int(total * 0.7):int(total * 0.85)] test_groups group_names[int(total * 0.85):] for split_name, split_groups in [ (train, train_groups), (val, val_groups), (test, test_groups) ]: split_img_dir fdataset/{split_name}/images split_label_dir fdataset/{split_name}/labels os.makedirs(split_img_dir, exist_okTrue) os.makedirs(split_label_dir, exist_okTrue) for g in split_groups: for img in groups[g]: copyfile( os.path.join(img_dir, img), os.path.join(split_img_dir, img) ) txt img.replace(.jpg, .txt) if os.path.exists(os.path.join(label_dir, txt)): copyfile( os.path.join(label_dir, txt), os.path.join(split_label_dir, txt) )这段脚本的关键是按前缀分组而不是按单张图片分组。我在这踩过坑一开始图省事直接用随机打散训练时 loss 很漂亮验证集 mAP 也高但把模型拿到另一个门店的监控画面上一测误报多到没法看。后来把训练曲线和验证集图片调出来对比才发现验证集里全是训练集里同一批货架的近邻帧模型实际上是“背”下了那个货架的样子。参数说明seed 固定成 42 保证每次运行划分结果一致。分组前缀之所以用 rsplit(_, 1)[0]是因为图片命名里时间戳含多个下划线只用最后一个下划线切分前面的部分作为批次前缀。实际处理时你要看一眼自己的图片命名规律原则是“同一时间、同一货架、同一摄像头角度”的图片必须分到同一组。3.3 生成 YOLO 训练用的 data.yaml 并检查每个类别框数划分完成后写一个 data.yaml 给 ultralytics 用。这个文件路径写错了或者类别顺序对不上训练直接报错所以每一步都要检查。path: dataset train: train/images val: val/images test: test/images names: 0: normal 1: emptypath 是数据集根目录train 和 val 是相对 path 的图片目录路径。names 的顺序必须和转换脚本里 class_map 的 id 对应id 为 0 的类别写在第一行。这里 normal 对应 id 0empty 对应 id 1。启动训练之前再做一个基础统计看每个类别有多少个框、平均每张图多少个目标。这个数字决定你对类别不平衡的判断。python - EOF import os from collections import Counter cnt Counter() img_cnt 0 for split in [train, val, test]: label_dir fdataset/{split}/labels for f in os.listdir(label_dir): path os.path.join(label_dir, f) if os.path.getsize(path) 0: continue with open(path) as fp: for line in fp: cls_id line.split()[0] cnt[split _cls cls_id] 1 for k, v in cnt.items(): print(k, v) EOF这一段统计脚本会输出 train_cls0、train_cls1、val_cls0 这些键值对。如果发现 empty 类的框数只有 normal 的一半甚至更少后面训练时就要考虑类别权重或者过采样这个我们在第四章和第五章都会讲到。还能顺带发现另一个问题如果 train 里有某个类别一张都没有那说明划分脚本有 bug 或者原始标注就不均衡。这类问题越早发现越好省得训练跑到一半才报错。4. 用 YOLO 训练空置检测模型命令、超参与指标解读4.1 训练命令与模型选型标签准备好、data.yaml 写好之后用 ultralytics 的命令行就能直接开训。模型我先建议用 s 规格起步yolo11s.pt 或 yolov8s.pt 都行先用小模型把数据链路和训练流程跑通再换大模型刷精度。在纯小白上手这一块ultralytics 的 yolo 命令已经比当年用 darknet 写 cfg 和权重文件省心太多了。yolo train \ datasupermarket.yaml \ modelyolo11s.pt \ epochs150 \ imgsz640 \ batch16 \ device0 \ workers4 \ patience20参数含义逐一说清楚。data 指向刚才写的 yaml 文件model 可以传预训练权重路径也可以用 yolo11s.yaml 从头训练但货架数据只有 4470 张用预训练权重做迁移学习收敛更快、精度更高。epochs 设 150配合 patience20 做早停也就是连续 20 个 epoch 验证集指标没有提升就提前结束。batch16 在 24G 显存下跑 640 分辨率没问题显存小的改成 8。这里有一个实际的权衡点如果你最终部署在 200 万像素摄像头画面里目标框很小640 输入容易把空置细节模糊掉。显存允许的情况下可以试试 imgsz960尤其是货架较远、缺口较小的场景。代价是训练和推理速度都变慢需要自己平衡。4.2 三个必调参数imgsz、cls 损失权重、增强开关训练通过之后真正影响缺货检测效果的参数有三个不是学习率也不是优化器。第一个是 imgsz。前面提过 640 是起步值但空置检测的典型目标框在整图中的占比可能只有 5% 到 15%缩到 640 以后细节丢得很厉害。我建议直接做一组对比实验imgsz640 和 imgsz960 各训一次看验证集 mAP50 的差距。如果差距超过 3 个点部署时就算用 TensorRT 加速也要保持这个输入分辨率否则精度损失是模型结构救不回来的。第二个是 cls 损失权重。ultralytics 的默认参数里 cls0.5这是分类损失的系数。对缺货检测这种正负样本严重不均衡的任务我一般会把 cls 提到 1.0 到 1.5。这样模型在训练时会花更多注意力在“这个区域到底是不是空置”的分类判断上配合置信度阈值调整能减少把层板阴影误判成缺口的概率。调太高也不行我试过 cls3.0模型变得极度保守recall 掉得厉害空置区域漏检一片。第三个是增强开关。YOLO 默认开启 Mosaic、随机平移、缩放等增强对通用检测是好东西但货架场景要挑着用。mosaic: 0.8 degrees: 0.0 fliplr: 0.0 scale: 0.3 translate: 0.1这段增强配置的意思是mosaic 概率 0.8旋转角度关闭水平翻转关闭缩放 0.3平移 0.1。我给货架场景的推荐是把 degrees 和 fliplr 都关掉。原因是货架层板线是水平结构旋转增强会把层板线转成斜的模型被迫学习各种奇怪的角度分布推理时反而对正常的水平层板不敏感。水平翻转的问题在于它会颠倒货架上的商品排列方向标签内容本身不随翻转改变语义但模型会在翻转后的图上看到“右边空、左边满”的虚假模式影响真实场景下的判断。4.3 指标看什么mAP50 之外更要看 precision/recall 的取舍货架空置检测的业务场景决定了它和通用目标检测的评价方式不一样。通用检测看 mAP但巡检系统最终要生成补货工单误报一次就得让店员白跑一趟货架漏报一次就可能错过补货时机。所以在模型验证阶段不要把 mAP50 当成唯一追求precision 和 recall 的取舍比 mAP 更重要。我一般会在训练结束后单独做一次验证集评估输出 precision、recall 和不同置信度阈值下的表现。yolo val \ modelruns/detect/train/weights/best.pt \ datasupermarket.yaml \ imgsz640 \ conf0.25 \ iou0.5默认 conf0.25 的意思是置信度低于 0.25 的预测框直接丢掉。输出里最关键的两个数字是 precision 和 recall。如果你发现 precision 明显低于 recall说明模型很多误报把阈值往上提到 0.35 或 0.4 能压掉一部分反过来 recall 太低说明漏检严重先检查是不是 empty 类样本太少而不是急着调阈值。评估输出里还有一个 per-class 的 AP 表格一定分开看 normal 和 empty 各自的 AP。很多项目整体 mAP 看着不错其实 normal 类贡献了大头empty 类的 AP 低得离谱这种模型上不了线。per-class 指标才能反映空置检测的真实水平。5. 货架空置检测的 5 个踩坑记录标注口径、数据泄漏与误检5.1 半空货架算不算空标注口径不统一导致模型判空尺度漂移现象训练时 loss 正常下降验证集 mAP 也不低但模型对新门店的检测结果非常不稳定。同一个货架上午检出一个大缺口下午同一位置什么都没检出来。原因数据集的标注口径不统一。“只剩两三件商品”的货架有的标注者标成 empty有的标成 normal露出大面积层板但还留着一排商品的也被不同人用不同标准处理。模型在训练时反复看到相互矛盾的正负标签学会的判断边界是模糊的。解决标注规范里必须把“空”的定义量化。我的做法是把空置分成两档完全露底层板可见区域超过 50%标为 empty还有商品但排列稀疏不标。如果业务上想检测“补货预警”级别可以把数据标注改造成 empty_full 和 empty_partial 两类但这属于原始标签的重新定义需要自己重新梳理数据。先用现有数据集跑通流程心里清楚这个口径问题会在迁移到新门店时暴露就好。5.2 验证集泄漏同一连拍序列被随机划分成 train 和 val现象训练集和验证集的 loss 曲线几乎贴在一起验证集 mAP 高达 0.9但模型在真实门店摄像头上一测漏检和误报一起出现。原因随机划分数据时同一个货架的连拍相邻帧被分到了训练集和验证集。验证集和训练集在内容上高度重叠模型在训练时见过几乎一模一样的画面验证自然“高分低能”。这属于数据泄漏。解决用 3.2 节的按前缀分组脚本重新划分数据。关键是理解分组粒度——同一摄像头、同一时间段、同一货架的图片必须进同一个集合。我自己的经验是宁可组数少一点也不能让验证集“偷看”训练内容。如果采集场景复杂可以用拍摄时间戳按小时分桶再把桶作为划分的最小单位。5.3 模型把货架层板边缘当成空置区域现象推理结果里很多框压在货架层板的下边缘、支撑柱旁边或者带价签条的白色背板区域。这些位置的置信度还不低0.3 到 0.5 之间。原因空置区域的视觉特征和层板阴影、货架结构边缘其实很接近都是长条状、颜色偏暗或偏白。如果训练集里 normal 类的多样性不够模型没有充分见过“有层次板但不是缺货”的样本就会学出一个过宽的判断边界。解决第一优先级是回到标注数据确认 normal 类有没有覆盖各种货架结构样本第二优先级是推理侧加先验过滤货架空置区域通常不会出现在画面顶部边界或紧贴层板下缘的位置这类框对应的纵坐标区间可以单独调整。我在部署时还习惯把检测框的宽高比做一个统计空置缺口的框一般是偏横的长条如果出现大量接近正方形的预测框大概率是误检可以直接过滤。5.4 类别不平衡normal 框远多于 empty 框时的训练策略现象训练结束后empty 类的 recall 明显低于 normal 类验证集里漏掉的大多是那些缺口小、不明显的空置区域。原因这是典型的小样本类问题。一个货架图上可能只有 1 个 empty 框但有 3 到 4 个 normal 框算下来整个数据集里 empty 框总数可能只有 normal 的三分之一。模型天然偏向预测多数类。解决分两步走。第一步在数据层面做 empty 类的过采样把 empty 类较多的图片复制到同目录下并随机改名多算一份但保持和原图同名的 txt 标签。第二步在训练时调高 cls 损失权重到 1.0 左右。如果后面你要自己继续标数据建议每张图里至少有一个 normal 和一个 empty 的搭配这样模型学到的对比信息更均衡。5.5 旋转增强把货架结构学歪了哪些增强要关掉现象模型在训练集上表现很好但在真实门店图上货架的倾斜角度稍有变化检测结果就乱掉到处是斜着或者变形的预测框。原因默认增强配置里开启了旋转 augmentation货架层板线在旋转后变成斜线模型被迫适应“斜着的货架”这种在真实场景中极少出现的情况。训练数据被增强成偏态分布模型学会了不存在的特征模式。解决在 ultralytics 的配置里显式关掉旋转和水平翻转degrees0.0、fliplr0.0。保留下 scale 和 translate这两个增强模拟摄像头远近和视角平移对货架场景更友好。这里也要注意如果你用的是第三方训练框架增强参数的名称可能不同但思路一样先识别哪些增强会破坏货架的几何结构再逐个关闭验证。6. 进阶用置信度 面积过滤让检测结果更符合门店巡检习惯6.1 写一个带面积先验的推理过滤脚本模型训好之后直接输出全部检测框是不能上线的。货架空置检测在业务侧有两个硬约束第一误报会让店员白跑腿第二太小的框通常是噪声不是需要处理的缺口。我通常在推理脚本里加上置信度阈值和面积先验双重过滤。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) def filter_detections(results, img_w, img_h, conf0.35, min_ratio0.005): filtered [] img_area img_w * img_h for r in results: for box in r.boxes: cls_id int(box.cls[0]) conf_val float(box.conf[0]) x1, y1, x2, y2 box.xyxy[0].tolist() w x2 - x1 h y2 - y1 box_area_ratio (w * h) / img_area if conf_val conf: continue if box_area_ratio min_ratio: continue filtered.append({ class: cls_id, conf: conf_val, bbox: (x1, y1, x2, y2), area_ratio: box_area_ratio }) return filtered这段脚本在模型输出的基础上做了两层过滤。第一层置信度过滤把低于 0.35 的框直接丢掉这个数字不是随便拍的我一般会先在验证集上画出置信度阈值和误报数量的关系曲线选择一个误报增长率开始变陡的位置。第二层面积过滤min_ratio0.005 表示检测框面积必须占到整图面积的 0.5% 以上才保留。货架场景里一个商品位缺口对应的框面积通常远大于这个比例能把散落的小噪声框清掉一大半。参数说明min_ratio 要根据自己的摄像头安装高度和货架距离来定。摄像头装得越高同样一个真实缺口在画面里占的比例越小这个值就要往低调到 0.002 或 0.003。建议在部署现场先跑一晚监控视频把模型输出的框面积分布存下来用分位数决定阈值而不是拍脑袋。6.2 用混淆矩阵和误报截图迭代阈值调优阈值不能只看验证集里的 mAP 数字因为验证集本身的分布和你真实门店场景可能差得远。我自己的迭代习惯是从验证集里抽 100 张模型预测错误的图按误报和漏检分别归档然后去查这些样本集中在什么位置、什么条件下。ultralytics 在训练结束时会自动在 runs/detect/train 目录下生成混淆矩阵和验证集预测图confusion_matrix.png 和 val_batch 开头的几张图特别有用。看混淆矩阵时重点关注 empty 类被预测成 normal 的那一格这一格代表漏检如果占比高说明 empty 类的特征还没学好优先补数据而不是调阈值。反过来看背景被预测成 empty 的那一格如果背景误报多把 min_ratio 调大一些或者检查是不是层板边缘样本在训练集里太少。还有一个容易被忽略的做法用测试集里的失败样本反推数据分布缺口。比如我发现模型在蓝色促销堆头附近的误报特别多去翻训练集发现大部分货架图都是普通白色层板几乎没有促销区样本。这时候不是调参能解决的得自己补拍补标一批促销区的图加进训练集重新微调。迭代到后期你会发现阈值调整只是最后一道阀门真正决定上线效果的是训练数据的覆盖度。做这个项目最深的体会是目标检测数据集从来不只是“解压、转换、开训”三步曲数据口径、划分方式、类别平衡、部署侧的先验约束每一步都要和业务场景反复对齐。我现在拿到一份新数据集第一件事永远是翻标注文件第二件事是统计类别分布最后才轮到模型训练。数据集的坑大多藏在你看不见的标注一致性里越早花时间确认后面越省心。希望这篇笔记能帮你少踩几个我已经踩过的坑。本文还有配套的精品资源点击获取