简介这份动物数据集资源聚焦熊猫单类别目标检测面向计算机视觉入门者、课程实验与算法验证场景提供VOC与YOLO双格式标注可直接接入主流检测框架训练。包内共332个文件含110张jpg原图、110个VOC格式xml标注和110个yolo格式txt标注另有少量说明文件压缩包约36.34MB图片与标注一一对应省去格式转换环节。标注由labelImg完成类别为Panda共114个矩形框框选位置准确合理适合小样本快速跑通训练流程或做数据增强实验。已有189人学习下载可作为熊猫识别demo、教学案例或迁移学习预训练素材。需要说明的是数据集不对训练所得模型或权重精度作任何保证仅提供准确且合理的标注使用者可据此验证数据加载、标签解析与训练链路是否正常再按需扩充样本。1. 熊猫数据集只有110张小样本VOC与YOLO双格式到底能训出什么拿到「动物数据集65熊猫数据集VOC格式yolo格式110张1类别.zip」这个标题第一反应往往是嫌它小——110张图、1个类别放在动辄上万张的公开数据集面前确实不起眼。但真正做过落地项目的人会反过来看这恰恰是「快速验证一个检测想法能不能跑通」的最小可用单元。熊猫目标检测在动物园监控、保护区红外相机、科普互动装置里都有真实需求而这些场景初期能拿到的标注数据往往就是一两百张。VOC格式负责兼容LabelImg这类老牌标注工具和Pascal VOC时代的训练脚本YOLO格式负责直接喂给YOLOv5/v8/v11这条主流链路一份数据两种格式省掉的是格式转换那半小时的折腾。这篇笔记面向三类人手里已经有一批小样本动物图想快速验证的算法同学、需要给课程设计或毕设找可复现数据集的在校生、以及想搞清楚VOC和YOLO到底差在哪的工程新手。下面从格式差异讲到训练参数再到小样本必踩的坑全部按能照着敲命令的粒度写。2. VOC与YOLO双格式拆解110张图里到底存了什么2.1 两种格式的目录结构与标注差异先把这个压缩包解开后大概率会看到的结构说清楚。VOC格式遵循Pascal VOC的标准布局核心是三个东西JPEGImages放原图Annotations放同名的.xml标注文件ImageSets/Main放划分好的train.txt、val.txt。每个xml里记录图片尺寸、类别名和每个目标的xmin/ymin/xmax/ymax坐标是左上角和右下角的绝对像素值。YOLO格式则简单粗暴images放图labels放同名.txt每行是类别索引 cx cy w h五个值全部归一化到0到1之间cx cy是框中心点w h是宽高。110张图、1个类别意味着YOLO的类别索引永远是0txt里每行开头都是0这一点在写data.yaml时别写错。对比项VOC格式YOLO格式标注文件.xml.txt坐标形式绝对像素 xmin ymin xmax ymax归一化 cx cy w h类别表示字符串 name整数索引划分文件ImageSets/Main/*.txt通常靠目录或txt列表主流工具LabelImg、labelmeLabelImg(YOLO模式)、roboflow2.2 为什么小样本还要坚持双格式有人会问110张图直接转成YOLO不就行了为什么原始包要同时给VOC。血泪经验是VOC的xml是人类可读的出问题时你能直接打开看某个框的坐标对不对YOLO的txt归一化之后肉眼很难判断一个0.512 0.334 0.21 0.45到底框在哪。调试阶段用VOC核对训练阶段用YOLO提速这是最省心的组合。另外很多老论文的baseline脚本只认VOC你要做对比实验时手里有VOC原始标注就不用反向转换。转换本身不难但反向从YOLO转VOC需要知道原图尺寸一旦图片被resize过就会出错所以保留VOC原始标注等于留了一颗后悔药。2.3 用脚本核对标注完整性拿到数据第一件事不是训练是核对。下面这段脚本遍历VOC的xml检查有没有越界框、有没有空标注、类别名是否统一。import os import xml.etree.ElementTree as ET ann_dir VOCdevkit/VOC2007/Annotations img_dir VOCdevkit/VOC2007/JPEGImages class_set set() bad [] for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() size root.find(size) W int(size.find(width).text) H int(size.find(height).text) objs root.findall(object) if len(objs) 0: bad.append((xml_file, empty)) for obj in objs: name obj.find(name).text class_set.add(name) bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) # 检查越界和零面积框 if xmin 0 or ymin 0 or xmax W or ymax H or xmax xmin or ymax ymin: bad.append((xml_file, fbad box {xmin},{ymin},{xmax},{ymax})) print(类别集合:, class_set) print(问题文件数:, len(bad)) for b in bad[:20]: print(b)逻辑说明先读size拿到原图宽高再逐个object取框判断是否越界或宽高非正。参数上class_set应该只输出{panda}这一个值如果出现别的拼写比如Panda、pandas说明标注不统一训练时会被当成多个类。bad列表里如果出现empty说明有图没标框小样本里这种图必须删掉否则会教模型学背景。跑完这一步再往下走能省掉后面loss不下降时的一半排查时间。3. 从VOC转YOLO转换脚本与四个边界坑3.1 转换脚本逐行拆解VOC转YOLO的核心就一个公式cx(xminxmax)/2/Wcy(yminymax)/2/Hw(xmax-xmin)/Wh(ymax-ymin)/H。下面脚本同时生成labels目录和train.txt/val.txt。import os import xml.etree.ElementTree as ET import random ann_dir VOCdevkit/VOC2007/Annotations img_dir VOCdevkit/VOC2007/JPEGImages out_label_dir datasets/panda/labels out_img_dir datasets/panda/images os.makedirs(out_label_dir, exist_okTrue) os.makedirs(out_img_dir, exist_okTrue) classes [panda] # 单类别索引固定为0 xml_files [f for f in os.listdir(ann_dir) if f.endswith(.xml)] random.seed(42) random.shuffle(xml_files) split int(len(xml_files) * 0.8) # 8:2划分 train_list, val_list [], [] for idx, xml_file in enumerate(xml_files): stem os.path.splitext(xml_file)[0] tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() size root.find(size) W int(size.find(width).text) H int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue cls_id classes.index(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) # 裁剪到图像边界内防止归一化后超出0-1 xmin max(0, min(xmin, W)) xmax max(0, min(xmax, W)) ymin max(0, min(ymin, H)) ymax max(0, min(ymax, H)) cx (xmin xmax) / 2.0 / W cy (ymin ymax) / 2.0 / H w (xmax - xmin) / W h (ymax - ymin) / H lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_label_dir, stem .txt), w) as f: f.write(\n.join(lines)) # 复制图片到统一目录 src_img os.path.join(img_dir, stem .jpg) dst_img os.path.join(out_img_dir, stem .jpg) if os.path.exists(src_img): import shutil shutil.copy(src_img, dst_img) if idx split: train_list.append(fimages/{stem}.jpg) else: val_list.append(fimages/{stem}.jpg) with open(datasets/panda/train.txt, w) as f: f.write(\n.join(train_list)) with open(datasets/panda/val.txt, w) as f: f.write(\n.join(val_list)) print(train:, len(train_list), val:, len(val_list))逻辑说明random.seed(42)固定划分保证每次跑结果一致做对比实验时这点很重要。split按8:2切110张图就是88张训练、22张验证。坐标裁剪那几行是防越界的关键有些xml的框会超出图片1到2个像素不裁的话归一化后会出现大于1的值YOLO训练时直接报错。参数上classes列表顺序决定索引单类别就一个panda如果以后加类别新类别往后追加不要插到前面否则旧标签全错。3.2 四个必踩的边界坑第一个坑是图片格式不统一。VOC的JPEGImages里可能混着.jpg和.png脚本里写死.jpg就会漏图。解决方法是先os.listdir统计后缀或者用glob匹配多种格式。第二个坑是文件名带空格或中文YOLO读txt列表时按空格分割路径会直接崩转换前统一重命名成纯英文数字。第三个坑是xml里size的宽高和实际图片不符常见于图片被外部工具压缩过这时要以PIL读到的实际尺寸为准别信xml。第四个坑是空标注文件转换后会生成0字节的txtYOLO训练时读到空文件会报ZeroDivisionError转换脚本里加一句if not lines: continue跳过或者直接删掉对应图片。3.3 转换后的自检命令转完别急着训先跑两条命令确认。第一条统计txt行数分布看有没有异常多的框小样本里一张图超过10个框基本是标错了。第二条用PIL读几张图把归一化坐标还原画框肉眼确认。# 统计每个label文件的行数 for f in datasets/panda/labels/*.txt; do echo $(wc -l $f) $f; done | sort -rn | head # 确认图片和标签一一对应 ls datasets/panda/images | wc -l ls datasets/panda/labels | wc -l两个数字应该相等都是110。如果labels比images少说明有图没标或转换漏了如果多说明有孤儿标签。这一步花两分钟能避免训练跑一半发现数据对不上。4. 110张图训练YOLOv8参数怎么设才不浪费数据4.1 data.yaml与目录组织YOLOv8对目录结构有约定最省事的方式是images/train、images/val、labels/train、labels/val四层。但110张图用txt列表更灵活data.yaml这样写path: datasets/panda train: train.txt val: val.txt nc: 1 names: 0: pandanc是类别数单类别写1。names用字典或列表都行但索引必须和转换时的classes一致。常见错误是把nc写成0或者names里写中文YOLOv8读yaml时中文在某些环境下会乱码类别名统一用英文。4.2 小样本的关键训练参数110张图属于典型小样本默认参数会过拟合。下面是我一般会改的几个参数默认值小样本建议原因epochs100150-300小数据收敛慢需要更多轮batch168或4显存够就小batch梯度噪声有正则效果imgsz640640别降降了熊猫小目标更看不清lr00.010.001-0.005小数据大学习率容易震荡patience5030早停别等太久过拟合前停mosaic1.00.5马赛克增强太强会破坏小样本语义degrees0.05.0轻微旋转增强熊猫姿态多样fliplr0.50.5水平翻转安全熊猫左右对称训练命令yolo detect train \ datadatasets/panda/data.yaml \ modelyolov8n.pt \ epochs200 \ batch8 \ imgsz640 \ lr00.003 \ patience30 \ mosaic0.5 \ degrees5.0 \ projectruns/panda \ nameexp1逻辑说明modelyolov8n.pt用nano版110张图用大模型纯属浪费nano足够。project和name决定输出目录方便多次实验对比。mosaic0.5是折中完全关掉增强小样本容易过拟合开到1.0又会让4张图拼接后的熊猫比例失真。4.3 训练过程看什么指标小样本训练时mAP50会波动很大别盯着单轮看。重点看三个train/box_loss是否稳定下降、val/box_loss是否在某个点后回升回升就是过拟合、metrics/mAP50-95的总体趋势。110张图正常能到mAP500.7到0.9之间如果低于0.5先回去查标注别急着调参。另外instances这个指标会告诉你每张图平均有多少个目标熊猫数据集里如果这个值突然变成0说明某个batch全是空标签。4.4 数据增强的取舍小样本最容易犯的错是增强拉满。mixup、copy_paste这些在COCO上有效的增强在110张图上会让模型学不到真实分布。我的习惯是只开fliplr、轻微degrees和hsv_h/s/v的色调抖动mosaic最多0.5。如果熊猫图片背景单一比如都是竹林可以适当开scale做尺度增强如果背景多样scale反而会让模型混淆。增强策略没有标准答案但小样本的原则是「宁少勿多」先跑一版弱增强的baseline再逐个加增强看验证集涨不涨。5. 小样本熊猫检测的避坑清单从loss不降到框偏移5.1 现象训练loss一直不降mAP卡在0.1原因通常是标注格式没对上。YOLO读标签时如果发现坐标大于1或者类别索引超出nc会静默跳过该标签等于这张图变成纯背景。110张图里如果有20张标签被跳过模型就只学了90张效果自然差。解决方法是训练前跑一遍校验脚本统计每个txt的最大坐标值和最大类别索引超过1或超过nc-1的直接报出来。5.2 现象验证集mAP高但实际推理框全偏这是典型的过拟合加数据泄漏。检查train.txt和val.txt有没有重复图片110张图如果划分时没去重同一张图既在训练又在验证mAP会虚高。另一个原因是验证集太小22张图里如果只有5张有熊猫mAP波动极大。解决办法是把验证集比例提到0.25或者做5折交叉验证虽然110张图做5折每折只有88张训练但能看出模型稳不稳定。5.3 现象推理时同一只熊猫出多个框NMS的iou阈值默认0.7小样本训练出的模型置信度分布比较散容易出现重叠框。把推理时的conf从0.25提到0.4iou从0.7降到0.5能压掉大部分重复框。如果还不行说明训练时正样本太少模型对熊猫的特征学得不够自信需要回去检查标注框是不是画得太松。5.4 现象小熊猫幼崽完全检测不到110张图里如果幼崽只占几张模型会把它当成背景。这是小样本的类别不平衡问题但单类别数据集里表现为尺度不平衡。解决办法是在data.yaml里没法直接解决得在训练时开scale0.5做尺度增强或者手动把幼崽图片复制几份过采样到训练集。注意复制时文件名要改否则YOLO会当成同一张图去重。5.5 现象训练到一半报CUDA out of memory110张图用batch8在4G显存卡上可能爆。先把batch降到4还爆就降到2同时开ampTrueYOLOv8默认开。如果imgsz是640可以临时降到512跑通流程但最终训练建议还是640因为熊猫在画面里占比通常不大降分辨率会丢小目标。另一个隐藏原因是workers设太高数据加载线程占显存设成2或4就够。6. 把110张用到极致交叉验证与推理阈值调优小样本数据集的价值不在于一次训出SOTA而在于用最低成本摸清一个检测任务的难度上限。110张熊猫图跑通之后我一般会做两件事来榨干它的信息量。第一件是5折交叉验证把110张分成5份每份22张轮流做验证这样能得到5个mAP值看方差比看单次结果靠谱得多。代码上不用改数据写个循环改train.txt/val.txt再调yolo detect train就行5次跑完大概半小时nano模型很快。如果5折的mAP方差超过0.15说明数据分布太不均匀得回去检查是不是某些角度或光照的图集中在一折里。第二件是推理阈值网格搜索。训练完的best.pt在验证集上跑预测把conf从0.1到0.6、iou从0.4到0.8各取5个值做25组组合统计每组的precision和recall。熊猫检测这种单类别场景通常conf0.35、iou0.55附近是F1最优点。下面这段脚本直接调YOLO的Python API做网格from ultralytics import YOLO import itertools model YOLO(runs/panda/exp1/weights/best.pt) confs [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] ious [0.4, 0.5, 0.6, 0.7, 0.8] best None for c, i in itertools.product(confs, ious): metrics model.val(datadatasets/panda/data.yaml, confc, ioui, verboseFalse) f1 2 * metrics.box.mp * metrics.box.mr / (metrics.box.mp metrics.box.mr 1e-9) if best is None or f1 best[0]: best (f1, c, i) print(best F1:, best[0], conf:, best[1], iou:, best[2])逻辑说明metrics.box.mp是平均precisionmr是平均recallF1由两者算出。verboseFalse关掉每组的详细输出不然25组刷屏。跑完拿到的conf和iou直接写进部署脚本比用默认值稳。注意这个搜索是在验证集上做的如果验证集只有22张过拟合阈值的风险存在所以5折交叉验证和阈值搜索最好一起做用不同折的验证集互相验证阈值稳定性。最后说个习惯每次拿到新的小样本数据集我都会先花20分钟做三件事——核对标注、跑一遍转换、用nano模型训10个epoch看loss曲线。这三步能筛掉80%的数据问题剩下的才是调参的活。110张熊猫图不大但把它跑透比盲目堆数据更能练出对检测任务的手感。希望帮到你。本文还有配套的精品资源点击获取