VOC数据集与YOLOv5的水果目标检测:小样本训练全流程

发布时间:2026/9/13 23:10:45
VOC数据集与YOLOv5的水果目标检测:小样本训练全流程 简介一份面向苹果、香蕉、橙子三种水果的定制VOC格式数据集用于深度学习中图像分类与目标检测的入门实践和算法对比。资源为zip压缩包共609个文件包含300张jpg图像、300个xml标注文件以及6个txt类别或数据划分列表、2个py辅助脚本、1个cfg配置文件整体仅23.8MB数据量适中且下载迅速。目前已有1371人学习浏览。数据集附带了VOCdevkit标准工具包可直接完成数据解析、指标评估与可视化大部分图片背景为白色少量带有背景干扰能够检验模型对背景抑制和泛化能力。借助xml标注信息既可训练Faster R-CNN、YOLO、SSD等目标检测网络也能改造为图像分类实验虽然样本规模有限但标注规范、目录清晰适合初学者在真实项目中走通数据准备、模型训练、效果评估的完整链路也便于在课堂或竞赛中快速搭建基线模型。1. 300 张图的水果 VOC 数据集为什么值得按完整流程走一遍这批数据的文件列表里躺着一个orange_11.xml.baiduyun.uploading.cfg说明它是从网盘直接转手出来的真实项目数据不是教程里精心清理过的样例。数据本身是 PASCAL VOC 结构JPEG 图片配同名 XML 标注三分类 apple、banana、orange整体 300 张。这个规模放在今天的深度学习实践里属于典型的「小样本 部分背景干扰」场景大部分图片是白底方便模型聚焦目标本身但仍有相当一部分带真实背景正好用来检验模型有没有偷懒走捷径。对想跑通「数据集 → 标注解析 → 目标检测模型训练 → 指标验证」全流程的人这套数据比动辄几千类的公开数据集友好得多。VOC 格式是 YOLOv5、Faster R-CNN、SSD 都能直接消费的通用结构300 张图则迫使你把迁移学习、数据增强、过拟合控制这些绕不开的问题提前面对。接下来先从目录结构开始把这批数据拆开再拼回去。2. VOC 标注格式与 VOCdevkit 目录组织先把数据读懂2.1 从文件名反推数据集构成文件命名规律很直观类别_序号.jpg比如apple_84.jpg、banana_89.jpg、orange_34.jpg。与之对应每张图片应该有一个同名 XML 标注文件比如apple_84.xml里面记录这张图里有哪些水果、每个水果的框坐标。这种「图片 同名 XML」的配对方式就是 PASCAL VOC 最核心的约定。需要注意那个orange_11.xml.baiduyun.uploading.cfg。这个后缀说明原始上传是百度网盘客户端产生的临时残留正主是orange_11.xml但 cfg 文件的存在暗示当时上传可能被中断过。你拿到压缩包解压后先搜一下还有没有其他.uploading或者.cfg文件再用ls -l对比 XML 和 JPEG 的文件大小size 0的标注文件直接删掉重下不然后面解析会抛异常。另外一个细节XML 里的filename标签不一定和实际文件名一致有些标注工具会写成全路径或者带后缀。训练前要做一次一致性校验把filename和 JPEGImages 目录里的实际文件做交叉比对。常见做法是用脚本读取每个 XML 的 filename 字段再去 JPEGImages 里查这个文件是否存在缺了就记录下来。这一步虽然不产生收益但能省下后面 debug 的半天时间。2.2 XML 标注文件逐字段拆解打开一个标注文件典型内容长这样annotation folderVOC2007/folder filenameapple_84.jpg/filename source databaseFruit VOC/database /source size width640/width height480/height depth3/depth /size segmented0/segmented object nameapple/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin120/xmin ymin80/ymin xmax420/xmax ymax360/ymax /bndbox /object /annotationsize里存的是原始图像的宽、高、通道数这是后面做坐标归一化时唯一的尺度依据千万不能拿模型输入尺寸去换算否则框全偏。object可以重复出现一张图里有多类或多个同类别目标时就有多个object块。bndbox里的 xmin、ymin、xmax、ymax 是左上角和右下角的像素坐标PASCAL VOC 习惯上以左上角为 (1, 1)这一点在转 YOLO 格式时会有影响后面会专门说。truncated表示目标是否被图像边界截断difficult表示目标是否难以辨认。对这个小数据集来说difficult1的框在训练时一般直接跳过——难例在样本充足时能提升鲁棒性但 300 张图里引入难例只会放大噪声。各字段在生产环境中的用途可以对照下面这张表字段含义训练时的处理方式folder / filename文件溯源校验图片与标注是否配对size原始图像宽高坐标归一化的尺度基准必须用name目标类别映射到类 ID构建 one-hot 标签bndbox框坐标转 YOLO 格式或送入检测头的损失函数truncated / difficult目标质量标记通常跳过或降低损失权重2.3 VOCdevkit 工具包的标准目录树压缩包里的VOCdevkit是 VOC 官方的标准工具包目录名解压后应该是这样的结构VOCdevkit/ └── VOC2007/ ├── JPEGImages/ # 所有原图jpg ├── Annotations/ # 所有 XML 标注与 JPEGImages 同名 ├── ImageSets/ │ └── Main/ # train.txt / val.txt / trainval.txt └── labels/ # 转 YOLO 格式后生成非 VOC 原生ImageSets/Main下是纯文本文件每行一个图片 ID不带扩展名。train.txt是训练集列表val.txt是验证集列表trainval.txt是两者合并。原版 VOC 还会为每个类别单独生成类似apple_train.txt的类别级列表用于分类任务或困难样本挖掘定制数据集一般没有不影响检测流程。这个目录组织是 VOC 能被十几年来的检测框架无缝接住的关键数据加载器只需要读ImageSets/Main里的 ID 列表再去JPEGImages和Annotations按 ID 取值即可。YOLOv5 虽然有自己的一套标签格式但很多开源项目依然先组织成 VOC 结构再转成训练格式因为 labelImg、roLabelImg 这类标注工具原生读写 VOC XML团队协作时这个格式是通用语言。后期labels/目录是转出来的 YOLO 格式标签放在这里方便归档但它不属于 VOC 原生约定。3. 训练前必做统计类别分布、过滤坏数据、划分验证集3.1 用 ElementTree 统计三类水果的标注框拿到数据先别急着训练第一步是统计标注分布。很多人按图片数量估算类别这不对——一张图里可能同时有苹果和橙子必须按object节点数统计。用 Python 标准库里的xml.etree.ElementTree就行不需要装额外依赖import os import xml.etree.ElementTree as ET ann_dir VOCdevkit/VOC2007/Annotations stats {} for f in sorted(os.listdir(ann_dir)): if not f.endswith(.xml): continue path os.path.join(ann_dir, f) try: tree ET.parse(path) except ET.ParseError as e: print(f[skip] {f}: XML 解析失败 - {e}) continue root tree.getroot() for obj in root.iter(object): name obj.findtext(name, ).strip() stats[name] stats.get(name, 0) 1 total sum(stats.values()) for k, v in sorted(stats.items(), keylambda x: -x[1]): print(f{k:8s}: {v:4d} 个目标 ({v/total:.1%}))这段代码先遍历Annotations目录只处理.xml结尾的文件orange_11.xml.baiduyun.uploading.cfg以.cfg结尾不会混进来。ET.parse包在try里遇到半截 XML 直接跳过并打印文件名这就是当时百度网盘上传中断留下的隐患。root.iter(object)会递归找出所有object节点统计时按标注框数量计算而不是按图片数。输出大致长这样apple : 118 个目标 (39.3%) banana : 97 个目标 (32.3%) orange : 85 个目标 (28.3%)具体数字以你解压后的实际标注为准但大概率不是均衡的。看分布的目的不是追求三分类样本数一样而是要预判少数类会不会拖垮 mAP——如果 orange 明显偏少训练时就要给橙子类别更高的损失权重或者做针对性的数据增强。3.2 背景干扰识别白底图与复杂背景图的快速分桶摘要里提到一个关键特点大部分图片是白底一部分有背景干扰。白底图看似简单但占比过高会诱导模型学「白色即背景」的捷径。为了量化这个风险我一般会先按背景类型给图片分桶。不需要训练分类器用四角像素的亮度方差就能快速估算from PIL import Image import numpy as np def clutter_score(path): im np.array(Image.open(path).convert(RGB)).astype(np.float32) h, w im.shape[:2] corners [ im[5:25, 5:25], # 左上 im[5:25, w-25:w-5], # 右上 im[h-25:h-5, 5:25], # 左下 im[h-25:h-5, w-25:w-5], # 右下 ] corner_std np.mean([c.std() for c in corners]) return corner_std白底图的四角区域方差非常低接近 0有真实背景的图角落方差通常大于 30。这个clutter_score是连续值可以按阈值把图片分成两组white组和clutter组。分组结果先不参与训练留到第五章做分桶评估用来观察同一个模型在不同背景下的 AP 差多少。背景类型目检特征clutter_score 参考白底四角纯白主体居中阴影少 10轻度干扰角落有浅色阴影或纹理10 ~ 30强干扰桌面、树叶、其他物体入镜 30分桶时注意别看单张图下结论可以写个小脚本把分组后的图片各拼成一张大图用 montage 命令快速目检montage white/*.jpg -tile 5x5 -geometry 22 white.png几十秒就能把 300 张图看完比逐张打开效率高得多。3.3 生成 VOC 规范的 train.txt 与 val.txt统计和分桶之后要划分训练集和验证集。VOC 原生流程要求在ImageSets/Main下生成 ID 列表YOLOv5 虽然不直接读这个文件但保留它可以兼容 labelImg 和基于 VOC 的加载器import os import random img_dir VOCdevkit/VOC2007/JPEGImages main_dir VOCdevkit/VOC2007/ImageSets/Main os.makedirs(main_dir, exist_okTrue) ids [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)] random.seed(42) random.shuffle(ids) split_idx int(len(ids) * 0.8) train_ids ids[:split_idx] val_ids ids[split_idx:] with open(os.path.join(main_dir, train.txt), w) as fp: fp.write(\n.join(train_ids) \n) with open(os.path.join(main_dir, val.txt), w) as fp: fp.write(\n.join(val_ids) \n) with open(os.path.join(main_dir, trainval.txt), w) as fp: fp.write(\n.join(ids) \n)random.seed(42)固定随机种子保证每次划分结果一致方便复现实验。8:2 划分在 300 张图上大约是 240 张训练、60 张验证数量不大但验证集承担的是「过拟合预警」职能60 张足够看出趋势。如果后面做交叉验证可以把 seed 换掉再跑几次对比不同划分下模型表现的稳定性。4. 基于 YOLOv5 的水果识别VOC 转 YOLO 格式与训练配置4.1 坐标转换从 bndbox 到归一化中心点YOLO 系列训练时不读 XML只认 txt 标签。VOC 到 YOLO 的转换公式是x_center 和 y_center 是 bndbox 中心点坐标除以图宽高w 和 h 是框宽高除以图宽高全部归一化到 0~1 区间。一个容易忽略的细节是坐标系起点VOC 的像素坐标以左上角为 (1, 1)而 YOLO 的归一化坐标以 (0, 0) 为起点严格转换应该在 xmin、ymin 上减 1、xmax、ymax 不变。对 640 分辨率的图1 像素的偏差只有千分之一点几但小目标上会体现出来我一般会做这个修正。import os import xml.etree.ElementTree as ET CLASSES [apple, banana, orange] CLASS_TO_ID {name: i for i, name in enumerate(CLASSES)} XML_DIR VOCdevkit/VOC2007/Annotations OUT_DIR VOCdevkit/VOC2007/labels os.makedirs(OUT_DIR, exist_okTrue) def convert(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.findtext(width)) h int(size.findtext(height)) lines [] for obj in root.iter(object): name obj.findtext(name).strip() if name not in CLASS_TO_ID: print(f[warn] {xml_path}: 未知类别 {name}跳过) continue if obj.findtext(difficult, 0) 1: continue box obj.find(bndbox) xmin float(box.findtext(xmin)) - 1 ymin float(box.findtext(ymin)) - 1 xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h lines.append(f{CLASS_TO_ID[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) return lines for f in sorted(os.listdir(XML_DIR)): if not f.endswith(.xml): continue out_name os.path.splitext(f)[0] .txt with open(os.path.join(OUT_DIR, out_name), w) as fp: fp.write(\n.join(convert(os.path.join(XML_DIR, f))))代码里做了三件关键的事将difficult1的框直接过滤将xmin/ymin减 1 修正坐标系起点类别 ID 按[apple, banana, orange]的顺序从 0 开始编号这个顺序必须和后面 YAML 配置文件里的names列表一一对应顺序错了损失函数不会报错但检测结果类别全乱套。4.2 组织 images/labels 同级目录并生成 data.yamlYOLOv5 找标签文件的逻辑是从 data.yaml 里train字段读到图片目录再去同级的labels目录找同名 txt。它不是通过文件名映射而是通过目录结构约定。所以最省事的做法是把图片和标签整理成下面这种同级结构datasets/fruits ├── images │ ├── train │ └── val ├── labels │ ├── train │ └── val └── fruits.yaml用一个短脚本完成拷贝和划分把上一节生成的labels目录里的 txt 按第三张划分的 train/val 分类复制过去import os import shutil main VOCdevkit/VOC2007 dst datasets/fruits for split in [train, val]: os.makedirs(f{dst}/images/{split}, exist_okTrue) os.makedirs(f{dst}/labels/{split}, exist_okTrue) with open(f{main}/ImageSets/Main/{split}.txt) as fp: ids [line.strip() for line in fp if line.strip()] for img_id in ids: shutil.copy(f{main}/JPEGImages/{img_id}.jpg, f{dst}/images/{split}/{img_id}.jpg) shutil.copy(f{main}/labels/{img_id}.txt, f{dst}/labels/{split}/{img_id}.txt)对应的fruits.yamlpath: datasets/fruits train: images/train val: images/val nc: 3 names: [apple, banana, orange]path是相对运行train.py时的当前目录。做过多人协作会发现绝对路径换一台机器就失效而path train的相对写法在大家统一目录结构后可以直接跑。nc必须和 names 长度一致names 顺序必须和 4.1 里 CLASSES 的顺序一致这三点是 YOLOv5 数据配置里最高频的出错点。4.3 训练启动命令与关键参数数据集整理好之后训练命令本身不复杂python train.py \ --data datasets/fruits.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache \ --freeze 10几个参数在小样本场景下的选择逻辑参数推荐值理由weightsyolov5s.pt用 COCO 预训练权重做迁移学习s 规模在 300 张图上刚好x 系列必过拟合img640白底占比高时可以试 512但带干扰背景的图用 640 更稳batch16显存允许就放大到 32小数据集上大 batch 能压低梯度噪声epochs100300 张图 100 epoch 足够观察收敛趋势再多容易过拟合cache开启把训练图缓存进 RAM300 张图占用很小能快很多freeze10冻结 backbone 前 10 层减少可训练参数量是防过拟合的有效手段这里有个容易被低估的点COCO 的 80 类里没有 apple、banana、orange 这三个水果类所以预训练权重对这两类的分类头没有任何直接帮助。迁移学习真正迁移的是主干网络的边缘、纹理、形状特征——VOC 数据里水果出现在桌面、手掌、白色背景中的构图和 ImageNet 上物体居中、背景多样的情况接近底层特征可复用。训练时 YOLOv5 检测头会自动替换类别输出维度并打印一条 warning看到那个 warning 不用担心属于正常行为。4.4 小数据集训练的三个典型坑坑一anchor 与目标尺度不匹配。训练启动后日志里会出现autoanchor: Analyzing anchors... anchors/target 4.21, Best Possible Recall (BPR) 0.98之类的输出。当 BPR 低于 0.98 时 YOLOv5 会自动重算 anchor。如果目标在图中占比很小比如白底图里的香蕉只有全图面积的 5%默认 anchor 偏大anchors/target会明显偏高此时要留意 autoanchor 是否触发重算也可以直接把--img提高到 960 让目标相对尺寸变大。坑二mosaic 增强在白底数据上的副作用。YOLOv5 默认训练时开了 mosaic 数据增强把 4 张图拼成一张。但如果数据集里白底图占多数mosaic 拼接后会产生大量人为的白边和块状边界模型可能学到「白色拼缝 背景」这种假特征。YOLOv5 在训练最后 10 个 epoch 会自动关闭 mosaic让模型在接近真实分布的图上微调这是官方默认行为不需要改。但如果发现前 90 个 epoch 的 loss 下降很快、最后 10 个 epoch 却反弹剧烈多半是 mosaic 关掉后分布切换太猛可以考虑把 mosaic 权重直接从 1.0 降到 0.5 再试。坑三类别不平衡时少数类 AP 被牺牲。训练完成后如果发现 orange 的 AP 明显低于另外两类问题通常不在检测头而在样本量。300 张图里 orange 目标最少时可以在data/hyps/hyp.scratch-low.yaml里适当调大分类损失权重cls默认 0.5可以试 0.7或者对 orange 做横向翻转、HSV 扰动这类不改变语义的增强而不是粗暴地重复采样重复采样很容易让模型对少量样本死记硬背。5. 小样本下验证水果识别模型学到了什么特征5.1 用混淆矩阵定位类间误检训练完成后第一件事不是看 mAP而是看混淆矩阵python val.py \ --data datasets/fruits.yaml \ --weights runs/train/exp/weights/best.pt \ --save-conf \ --conf-thres 0.001--save-conf会在保存的预测结果里附带每个框的置信度--conf-thres 0.001把置信度阈值压到极低目的是让模型把「虽然犹豫但本质能检出的框」也吐出来而不是被默认的 0.25 阈值挡住。输出目录下confusion_matrix.png能直接看出 apple 和 orange 是不是互相误检。如果橙色水果大量被判成 apple 且置信度集中在 0.4~0.6说明模型在靠颜色和圆形轮廓分类300 张图里苹果和橙子的区分度不足这是数据问题不是网络结构问题。5.2 按背景干扰度分桶对比 AP3.2 节分好的 white 组和 clutter 组在这里派上用场。分别对两组跑一次验证看同一套权重在不同背景下的 AP 差值import yaml, os, sys subsets {white: [apple_84, banana_89], clutter: [orange_34, orange_81]} base yaml.safe_load(open(datasets/fruits.yaml)) for tag, ids in subsets.items(): cfg base.copy() cfg[val] fdatasets/fruits_subsets/{tag}.txt tmp fdatasets/fruits_{tag}.yaml yaml.safe_dump(cfg, open(tmp, w)) os.system(fpython val.py --data {tmp} --weights runs/.../best.pt --save-conf)这个脚本对每个子集生成临时 yaml 再调用 val.pysubsets里手动维护两个分组的图片 ID 列表即可。对比两个子集的 mAP如果 white 子集 mAP 0.93、clutter 子集只有 0.71差值超过 15 个百分点模型就是在走「白色背景即安全区」的捷径。后续改进方向不是堆训练时长而是去采集带真实背景的水果照片补进训练集或者把白底图做背景替换增强。5.3 一个实用技巧冻结主干并做二分类诊断实验这里给出一个真正常用的诊断技巧当三分类 mAP 卡在 0.75 以下上不去时把 apple 和 orange 合并成一个大类 fruitbanana 不变重新映射标签得到二分类数据集再训练一次python train.py \ --data datasets/fruits_2cls.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --freeze 10fruits_2cls.yaml里的 nc 改为 2names 改为[fruit, banana]XML 转换时把 apple 和 orange 的类别 ID 统一映射到 0。对照结论很干净如果二分类 mAP 显著提升说明模型能定位水果但分不开苹果和橙子瓶颈在细粒度分类特征不足要针对性收集难例如果二分类也不见好转瓶颈在目标定位或标注质量问题。这个对照实验比看单一指标更能定位问题跑完一次就能决定下一步是采集数据还是标数据。本文还有配套的精品资源点击获取