简介:Pascal-5i小样本学习数据集为研究者提供了基于PASCAL VOC构建的标准评测环境,专攻5-way 1-shot与5-way 5-shot场景,适合从事小样本图像分类、原型学习、元学习等方向的研究人员和学生使用。数据集将原始20类划分为基础类与新类别,前者用于训练基础知识,后者用于评估模型仅凭少数样本识别新类的能力,能够清晰检验算法的泛化性能。整个资源包共6个文件,约151KB,包含说明文档(md)、示例脚本(ipynb)、Python读取代码(py)、依赖清单(txt)以及许可证等,结构简洁。配合现成的读取脚本和示例notebook,读者可从零开始加载数据、查看图像与掩码,省去自行解析PASCAL VOC标注的繁琐过程,直接开展原型网络或元学习对比实验。目前已有1443人学习使用,对于入门小样本学习或复现论文的实验环境搭建,都是高性价比的起点。 月底调模型的时候,我从一个朋友那收到一个消息:他说手头有个“pascal-5i数据集”,半天没跑通,问我是哪一年的VOC。我当时就明白,这就是很多刚上手少样本目标检测(few-shot object detection)的人第一道坎。pascal-5i 不是什么新拍的数据集,也不是某个人的“私藏”,它是从经典 PASCAL VOC 里按照固定规则切出来的一个基准(benchmark),专门用来验证模型“只看几张带框标注的图,能不能学会一个新类别”。
这篇文章就把这件事讲透:pascal-5i 的来历、四个 fold 怎么划分、K-shot 到底按图算还是按框算、怎么从零手写 split 脚本,以及训练和评估中那些论文不会告诉你的坑。适合刚入坑小样本检测的研究生,也适合想复现 baseline 结果的工程师。
1. Pascal-5i 到底是什么:少样本目标检测的“出场设置”
1.1 从 PASCAL VOC 20 类说起
PASCAL VOC 是计算机视觉里最有“资历”的目标检测数据集之一,它本身包含20个类别:
aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa, train, tvmonitor。
大家一般会把 VOC2007 和 VOC2012 搭配着用:2007 的 trainval 做训练,2007 的 test 做测试,2012 的 trainval 做额外训练数据。两个版本的类别定义完全一致,这也是后续各种变体能够基于它做切分的前提。我第一次看 Pascal-5i 时,第一反应也是“这不就是 VOC 吗”,后来才意识到,它的关键不在数据本身,而在“怎么切”。
pascal-5i 仍然使用这20个大类,但把它们拆成了“基类”(base classes)和“新类”(novel classes)。基类拥有完整的大量标注,新类在训练阶段只保留极少量标注样本。这种设计模拟的是现实中的一个常见矛盾:某些类别你手上数据很多,某些类别刚刚出现、只有几张图,但你又希望检测器能快速学会它们。
所以 pascal-5i 不是一个孤立的数据集,它是一个“协议”。你下载的还是原始的 VOC 图片和 XML 标注,要跑 pascal-5i,关键是一套你认可的“划分规则”和“采样规则”。不同论文、不同开源代码对 fold 的划分可能有细微差别,这才是很多人最终没复现出论文数据的原因。
1.2 “5i”这两个字符怎么拆解
“5”指的是 few-shot 设定中的 shot 数,也就是 K。最常用的设定是 1-shot 和 5-shot。这儿的 shot 含义非常容易踩坑,后面我会专门讲。
“i”指的是第几个 fold(数据划分),常见变量名是 split 0 到 split 3,或者 split 1 到 split 4。因为一次切分带有偶然性,只在一个划分上做实验不能说明任何问题,所以大家约定俗成地做多次划分,把结果平均,才算是比较可信的结论。pascal-5i 里的 i,就是提醒你“别只跑一个划分”。
这套命名方式最早可以追溯到语义分割领域的 PASCAL-5i 基准。后来少样本目标检测的研究者们沿用了这一套思路:把 VOC 的 20 类拆成多个划分,每个划分里的 novel 类都不相同,这样每个类都有机会成为“少数类”,模型不能在某个特定类别组合上过拟合。所以你在读论文时经常看到 “average over 4 splits” 这类描述,指的就是在四个划分上分别训练、分别测试,最后取平均结果。
1.3 为什么少样本检测需要这种“人造基准”
目标检测的数据标注成本远高于图像分类。分类只需要告诉模型“图里有什么”,检测必须告诉它“东西在哪个位置”,也就是要画 bounding box。如果某个新类别只有三五张图,用传统检测流程去训练,很容易过拟合到背景、光线、角度这些无关因素上。
pascal-5i 的价值在于,它把所有模型放在同一个起跑线上:大家都是同样的20类,同样的 K-shot 标注量,同样的 base 类预训练数据。这样一来,论文之间能比较的就不再是“谁的数据集更好”,而是“谁能在同样的小样本条件下学到更通用的特征”。这是它和普通“数据集”最大的不同,它更像一套标准化考题。
2. 四个 Fold 怎么划分的:类别取舍与实验协议
2.1 常见划分示例与“以代码库为准”的出处
不同代码库对四个 fold 的类别划分细节略有不同,我先把最常见的一组列出来供参考(这套划分在 Meta R-CNN、TFA、DeFRCN 等不少工作里都能看到):
- fold 1(split 0):novel = {bird, bus, cow, motorbike, sofa}
- fold 2(split 1):novel = {aeroplane, bottle, cow, horse, tvmonitor}
- fold 3(split 2):novel = {boat, cat, motorbike, sofa, tvmonitor}
- fold 4(split 3):novel = {cat, chair, cow, dog, pottedplant}
| 划分 | novel 类(每个类提供 K-shot) | 基类数量 |
|---|---|---|
| fold 1 | bird, bus, cow, motorbike, sofa | 15 |
| fold 2 | aeroplane, bottle, cow, horse, tvmonitor | 15 |
| fold 3 | boat, cat, motorbike, sofa, tvmonitor | 15 |
| fold 4 | cat, chair, cow, dog, pottedplant | 15 |
注意,fold 2 和 fold 3 里都出现了 cow、motorbike、sofa、tvmonitor,这说明不同划分之间并不要求“完全互斥”。有的刚入门的同学以为四个 fold 是硬切四份,novel 类不能重复,这完全是误解。pascal-5i 关注的是模型在“任意指定 K-shot novel 类”上的泛化能力,不是让类的集合干干净净不重叠。
更重要的是:既然各个开源库之间存在微调版本差异,你在任何实验开始前都必须确认自己使用的常量表。我看过太多实验结果对不上,最后发现是别人 repo 里 fold 划分的类别顺序跟你理解的不一样。不要盲目照抄论文附录里的表格,以你跑的那份代码里的pascal_voc.py或voc_split.py中的定义为最终标准。
2.2 shot 的定义:按实例还是按图片?这是最常见的误区
K-shot 中的 shot 绝大多数情况下指的是“标注实例数”,也就是 bounding box 的数量,而不是图片数量。这一点新手特别容易搞错,因为分类领域的 few-shot 通常天然按图片数算,一张图就是一个样本。到了检测领域,一张图里可能同时出现三只猫、五个人,那么这张图能提供的“样本数”就不是1,而是对应类别框的数量。
举个例子:5-shot 设定下,如果“猫”这个类别只有一张图片,但这一张图里恰好有 5 只猫(5 个标注框),那么它就直接满足了这个类的 5-shot 限额。如果你按“5张图”来采样,你会多拿一些本来不该出现的训练图,这相当于偷偷增加了数据量,最终结果会比论文偏高,也不公平。
构建 split 时,你需要逐类解析 XML 里的<object>数量,直到某一类的累计框数达到 K 才停止添加该类的图片。对于既包含 base 类又包含 novel 类的图片,处理方式要格外小心:通常的做法是整张图都会进入 support 集,但在训练时只会保留 novel 类的标注框,base 类框要不要保留取决于你用的训练框架。有些方法为了避免泄漏,会直接把 base 类框过滤掉,有些方法则会保留当作上下文。这个选择要在实验记录里写明白,不然别人复现时又会对不上。
2.3 实验协议:base 预训练、novel 微调、四折平均
绝大多数基于 pascal-5i 的算法走的是“两阶段”路线:第一阶段用全部 base 类训练一个常规检测器,比如 Faster R-CNN;第二阶段冻结或微调部分权重,用 K-shot 的 novel 数据做小样本适配。这是检测任务和分类任务一个显著差异,因为检测模型包含 proposal 生成、RoI 特征提取、分类与回归多个模块,你不能像图像分类那样简单地把新类别挂到分类头上就完事。
在完整实验中,你需要对每个 fold 都做同样的流程:基类预训练、抽取对应 novel 的 K-shot、微调、在 VOC2007 test 上评估。最后把四个 fold 的结果平均,上报的是“4-split 平均 mAP”,而且惯例上会单独报告 novel 类上的 mAP,因为这才是小样本能力的核心体现。只看 20 类整体 mAP 会被 base 类结果稀释,掩盖真实差距。
3. 手把手构建自己的 Pascal-5i:从 VOC 下载到 split 脚本
3.1 下载与目录结构
第一步当然是拿到原始 VOC 数据。虽然名叫“pascal-5i 数据集”,但你并不需要找专门的下载链接,只需要去官网下载 VOC2007 和 VOC2012。常见目录结构如下:
VOCdevkit/ ├── VOC2007/ │ ├── Annotations/ │ ├── ImageSets/ │ └── JPEGImages/ └── VOC2012/ ├── Annotations/ ├── ImageSets/ └── JPEGImages/如果你用的是第三方库,比如很多少样本检测的开源代码,它们已经提供了自动化下载脚本。但我的建议是手动确认一下 Annotations 里的 XML 文件数量和 JPEGImages 里的图片数量是否对得上。因为后处理脚本通常会全量扫描这两个目录,缺失一个文件就可能导致整个类别实例计数出错,而且这种错误肉眼很难发现。
3.2 解析 XML、筛选 K-shot 实例的核心逻辑
构建 split 的核心功能就两件事:统计类别实例数、按 K-shot 限制挑选图片。下面这个简化版 Python 脚本能说明大部分逻辑:
import os import random import xml.etree.ElementTree as ET VOC_CLASSES = [ 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor' ] novel_classes = {'bird', 'bus', 'cow', 'motorbike', 'sofa'} base_classes = [c for c in VOC_CLASSES if c not in novel_classes] K_SHOT = 5 random.seed(0) def parse_annotation(xml_path): tree = ET.parse(xml_path) root = tree.getroot() instances = [] for obj in root.findall('object'): name = obj.find('name').text if name not in novel_classes: continue difficult = int(obj.find('difficult').text) if difficult: continue bndbox = obj.find('bndbox') box = [ int(round(float(bndbox.find('xmin').text))), int(round(float(bndbox.find('ymin').text))), int(round(float(bndbox.find('xmax').text))), int(round(float(bndbox.find('ymax').text))) ] instances.append((name, box)) return instances xml_dir = 'VOCdevkit/VOC2012/Annotations' all_xmls = [f for f in os.listdir(xml_dir) if f.endswith('.xml')] random.shuffle(all_xmls) shot_collect = {cls_name: [] for cls_name in novel_classes} for xml_file in all_xmls: instances = parse_annotation(os.path.join(xml_dir, xml_file)) if not instances: continue for cls_name, box in instances: if len(shot_collect[cls_name]) >= K_SHOT: continue shot_collect[cls_name].append((xml_file, box)) if all(len(v) >= K_SHOT for v in shot_collect.values()): break这段代码里有两个关键点。第一,difficult样本建议过滤掉,这是 VOC 官方的惯例,difficult 表示该目标本身极难辨认,通常不符合小样本训练的预期,不排除会让结果不稳定。第二,采样前必须random.seed,并且把采样得到的图片列表保存成文件。因为四个 fold 之间、不同 K 值之间都需要可复现,后面对比实验或别人复现都依赖这个文件。
3.3 类别重映射与避免基类泄漏
拿到 K-shot 图片后,你不应该直接把原始标注喂给检测器。因为检测器的分类头在 base 预训练阶段只有 15 类,你要么在第二阶段扩展分类头增加 novel 类,要么把标注重映射成“base + novel 统一的新索引”。这两条路对应不同的微调策略,前者常见于把 novel 类当作增量类处理,后者常见于统一 fine-tune。很多框架默认你会在数据加载器里完成重映射,如果你没有做,模型输出的类别维度会对不上,训练直接崩。
基类泄漏是另一个隐蔽问题。假设一张 K-shot 图片里既有 novel 类“猫”,又有 base 类“人”。从最简单的角度讲,这张图包含了人的完整标注,如果在微调阶段你把人的 ground truth 也拿来训练,就等于让模型在 novel 场景中额外复习了 base 类。这对检测器的整体性能未必是坏事,但它会混淆实验结论:novel mAP 的提升到底是因为学到了“猫”的通用特征,还是因为在少数几张图里同时强化了“人”的检测能力?所以在写实验说明时,最好明确一个原则:novel 微调阶段只使用 novel 类的框参与 loss 计算。
4. 训练与评测中绕不开的细节:mAP、种子与数据增强
4.1 评估指标:novel 类 mAP 才是主线
pascal-5i 的测试阶段通常是用 VOC2007 test 作为评估集,你会对测试图片上的目标类别做预测,然后按标准 mAP 流程计算。对于小样本检测,大家最看重的是 novel 类的 mAP@0.5。原因很直接:base 类有几百上千张训练图,检测器在 base 上的性能主要反映“常规训练”能力,不是小样本能力。只有在 novel 类上拉开差距,才能说明算法在小样本条件下的泛化性能。
你可能会在网上看到不同的汇报方式:有的论文报mAP_novel,有的报mAP_all,还有的报mAP_base。做实验时我的建议是三个都算一遍,上报时以 novel 为主、另外两个作为辅助参考。因为只报 novel 类可能掩盖模型在 base 类上的灾难性遗忘,比如某些方法在 novel 上提了 2 个点,但 base 掉了 8 个点,这在实际部署中根本不可用。
4.2 两阶段微调的主流范式
从复现角度讲,R101-C4 骨架的 Faster R-CNN 配合余弦学习率衰减、在 K-shot 上微调若干个 epoch,到今天依然是性价比最高的 baseline 之一。NVIDIA 的 TFA 工作非常透彻地验证了这个思路:它不需要复杂的元学习模块,先用 base 类预训练,再在 novel 类上微调分类头和 bbox 回归头,就已经能超过很多花哨的元学习算法。
一个我反复使用的微调策略是:第一阶段(base)用正常的 1x 或 2x schedule,第二阶段(novel)把学习率降到原来的 1/10 左右,同时加大 weight decay,训练轮数不用多。小样本阶段你要是沿用大学习率,几个 batch 下来 loss 就飞了。并且在微调阶段做数据增强时,必须警惕“增强过头”:小样本场景下随机裁剪、水平翻转有帮助,但过强的 color jitter 反而会把本来就是少数类的特征进一步扭曲。
4.3 影响结果稳定性的三个隐藏因素
第一个是随机种子。很多人做 few-shot 实验时只设定一次全局随机种子,但真正影响结果的是构建 K-shot 时的采样种子、数据加载时的 shuffle 种子,以及并行训练时数据加载器的 worker 种子。我的习惯是全链路固定,并且在代码开头用日志打印出每个环节的 seed 值。
第二个是 base 预训练模型的选择。你用同一个算法、同一个 K-shot,但 base 预训练权重不同,novel 性能可能差 1 到 2 个点。社区里有些代码会公布他们训练好的 base 模型权重,建议第一次复现时直接用官方权重,搭起完整流程后再考虑自己训练 base。
第三个是“微调多少轮”。few-shot 场景下模型非常容易在 support 集上快速过拟合,训练损失下降很快,但测试 mAP 可能已经拐头向下了。我自己的做法是每 500 步保存一次 checkpoint,在验证集上选点。不要轻易相信“最后一个 epoch 最好”。
5. 我踩过的坑与给新手的检查清单
5.1 看似简单但很致命的配置错误
先分享两个真实遇到的坑。第一次跑 pascal-5i 时,我下载的 VOC 数据里混入了一些损坏的 JPEG 图片,解码时报Corrupt JPEG data警告。我一开始觉得无伤大雅,结果某个 novel 类的实例数统计一直差几个,后来才发现对应 XML 挺好,但图片文件根本打不开。从那以后我写了个脚本,逐张验证图片能否正常解码,再进 split 流程。
第二个坑是类别索引偏移。VOC 官方没有提供统一的“从 0 到 19”的类别 ID 表,很多代码库自己写了一套。如果你把一个 repo 里生成的 K-shot 标注文件直接拿到另一个 repo 里用,类别 ID 很可能对不上,出现“模型把猫预测成牛”这类完全错位的结果。解决办法很简单:每次用新代码库都先打印一遍类别索引表,确认 novel 类的 ID 和该库内部定义一致。
第三个坑容易被忽略:你在构建 K-shot 时,可能会把同一张图片同时选入 novel 类的 support 集和测试集。如果这张图来自 VOC2007 test,或者来自 VOC2012 但被某个库混进了评估集,那测试结果会虚高,模型可能只是在记忆图片而不是泛化。必须做好集合去重检查。
5.2 如何判断模型是真的学会了这些新类
小样本检测里有个常见现象:模型在 novel 类的 mAP 上看起来不错,但一可视化就露馅,它其实是在“猜”。判断模型有没有真正学会新类的通用做法有两个。
第一个做法是做一次“反向验证”。你把 novel 类 K-shot 里的图片隐藏一半,只保留 K/2 的 shot,看看 mAP 是否随之下降。如果 mAP 几乎不动,说明模型主要靠 base 预训练特征硬撑,并没有从 support 集中获取有效信息,这个模型在真实小样本场景里不太可靠。
第二个做法是画 PR 曲线而不是只看 mAP 数字。小样本下 PR 曲线通常抖动厉害,但你可以关注 recall 的极限位置。如果模型在 novel 类上的最高 recall 只有 50%,即使 AP 被调得还行,说明 proposal 阶段就已经漏掉了大量新类目标。很多小样本检测模型的瓶颈不在分类头,而在区域提议网络根本没有把新类目标提出来。
5.3 一个让实验对比更有说服力的习惯
做研究时,把四个 fold 的结果分别记录下来非常有必要。很多论文只报平均 mAP,但如果你看过每个 fold 的具体结果,会发现某些 fold 的性能明显偏低。一个能够诊断模型偏置的习惯是:打印每个 novel 类的 AP,而不是只打印总平均。比如一个方法在 fold 1 上 AP 全是 30,在 fold 2 上靠“bottle”刷到 50,平均下来看着不错,但实际挑类别。这个信息只有拆到单类才能发现。
最后分享一个小技巧:如果你在跑基线实验,建议把每个 fold 的 K-shot 标注文件单独存成 JSON 或 pickle,不要每次都重新采样。一来能保证多个方法在完全相同的 support 集上进行公平对比,二来能省掉不少调试时间。我吃过一次亏,两个算法“官方代码”各自采样了一套 support 集,结果微小差距根本分不清是算法原因还是数据原因。统一 K-shot 文件之后,实验结论一下子清晰多了。
本文还有配套的精品资源,点击获取