简介:Pascal-5i数据集是面向小样本学习研究的标准化数据集,源自PASCAL VOC,将20个类别划分为基础类与新类,支持5-way 1-shot和5-way 5-shot任务,适合需要验证原型网络、元学习或知识迁移方法的科研人员与算法工程师使用。压缩包内共6个文件,约151KB,包含Markdown说明文档、Python读取脚本、Jupyter示例程序以及依赖清单等,从环境配置到数据加载与实验演示均有覆盖,结构紧凑。已有1444人学习下载,说明这一数据集在小样本学习社区中具有一定关注度。借助其中的数据划分与读取工具,使用者可以跳过繁琐的数据预处理,直接聚焦算法设计与效果对比,还能基于示例程序快速复现基础实验,适合用于开题验证、论文复现以及课程设计等场景。
1. 从 pascal-5i 说起:小样本学习评测绕不开的基准
某开发者第一次复现小样本学习分割论文时,跑完一个 split,mIoU 和论文差了 12 个点。折腾两天后发现问题不在模型,而在数据划分:他把支持集和查询集的图像混在了一起。pascal-5i 是小样本学习(few-shot learning)领域曝光率最高的语义分割基准,它没有独立的下载链接,而是基于 PASCAL VOC 2012 和 SBD 标注衍生出来的评测协议。核心是一个「5 类一组」的划分方案:20 个物体类被切成 4 个 split,每个 split 中 5 类作为查询类、15 类作为支持类,用来验证模型见过某类 1 张或 5 张标注图之后的分割能力。适合正在做小样本分割研究、准备对比 SOTA 方法、或者只是想跑通一个可信 baseline 的从业者。
2. 数据集的底子:VOC 2012、SBD 与四个 split 的划分规则
2.1 目录结构与标注格式
PASCAL-5i 不是一个会随论文打包下载的文件,它是「协议 + 两套数据集的组合体」。协议指定用哪些图像做 support、哪些做 query;底层标注来自 PASCAL VOC 2012 devkit 和 SBD(Semantic Boundaries Dataset)。复现之前先把这些东西装进脑子:VOC 的语义分割标注是 8-bit 彩色 PNG,用调色板(palette)存储,背景是 0,物体类从 1 到 20,边界是 255;SBD 的标注是以类别为中心的边界标注集,在 VOC 2012 的 trainval 图之外还多了一批新图,它的 cls 掩码是单通道 PNG,像素值直接就是 0(背景)到 20(类别),同样把 255 作为未知区域。
目录结构如下(以从官网解压后的默认路径为例):
| 目录 | 内容 | 标注格式 |
|---|---|---|
| VOCdevkit/VOC2012/JPEGImages | 约 1.7 万张原图 | JPG |
| VOCdevkit/VOC2012/SegmentationClass | VOC 语义分割标注 | palette PNG |
| VOCdevkit/VOC2012/ImageSets/Segmentation | train.txt / val.txt 等列表 | txt |
| SBD/dataset/img | SBD 图像,含 VOC 没有的图 | JPG |
| SBD/dataset/cls | SBD 单通道语义掩码 | PNG |
需要特别留意,VOC2012 的 trainval 一共 2913 张有分割标注,SBD 额外提供了这 2913 张的改进标注和 5585 张新图的标注,因此 SBD 的 cls 目录文件比 VOC 的 SegmentationClass 多出不少。绝大多数 PASCAL-5i 实现用 SBD 的 train 部分作为训练时的支持图像来源,用 VOC 2012 的 val 作为查询图像来源,这就是为什么你下载的每个 repo 都会同时要求这两份数据。
2.2 四个 split 的类别划分:谁是支持类,谁是查询类
PASCAL-5i 的核心协议是把 VOC 的 20 个类别切成 4 个互斥的 split,每个 split 包含 5 个查询类。划分方式是固定的,对比实验必须按同一张表来:
| split | 查询类(5 类) | 支持类(剩余 15 类) |
|---|---|---|
| fold0 | aeroplane, bicycle, bird, boat, bottle | 其余 15 类 |
| fold1 | bus, car, cat, chair, cow | 其余 15 类 |
| fold2 | diningtable, dog, horse, motorbike, person | 其余 15 类 |
| fold3 | pottedplant, sheep, sofa, train, tvmonitor | 其余 15 类 |
这里的「查询类」就是在该 split 的测试中需要被分割的类别;训练阶段模型不允许看到查询类的标注,只能在支持图像里看到支持类的掩码。每个 split 之间类别不重叠,保证了「跨类泛化」的测试可信度。有的论文把 fold3 称为 fold4,甚至在 4 个 split 之外再加自定义划分,报告结果时务必标明自己用的是哪几个,否则复现时数值对不上,第一反应常是「模型写错了」,实际只是划分表不同。
2.3 支持图像与查询图像的来源边界
理解 PASCAL-5i 的第二个关键点在于「图像来源」而不是「类别来源」。训练时 support 图像通常从 SBD 的 train 列表里取,query 图像从 VOC 2012 的 val 里取;验证时 support 和 query 都从 val 里取。这意味着同一张图在整个实验里扮演的角色要提前定好,不能既当支持又当查询。
遵守这个边界的原因是防止信息泄露:如果支持图像和查询图像来自同一张图,模型在测试时见过同一场景的标注,mIoU 会显著虚高。我见过某开发者把 SBD 的 trainval 全部塞进支持集,又用 VOC val 做查询,导致 fold 结果比论文还高 8 个点,这不是模型强,是泄露。跑实验前先把每个 split 的 support 图清单和 query 图清单导出成 txt,各自去重,再检查交集是否为空,这一步值得做成固定流程。
SBD 的 train 和 val 数量也常被忽略:SBD train 有 8498 张、val 有 2857 张;VOC 2012 train 1464 张、val 1449 张。有的实现为了图省事,只用 VOC train 构建支持类索引,类别样本量少一半,训练出来的模型在验证时 mIoU 会偏低 3-5 个点。复现任何比较实验前,先确认对方用的训练支持图像源是 SBD 还是 VOC,这直接决定了你的实验能不能对齐。
3. 下载与预处理:把 VOC 2012 与 SBD 对齐成可直接读取的数据集
3.1 数据获取与目录检查
PASCAL-5i 的安装需要手动获取两套数据集并放到同一目录下。常见做法是建一个 data 目录,下载 VOC 2012 devkit 压缩包和 SBD 的 dataset 压缩包,解压后保持默认目录名不变。下载完成后先做一次目录体检:
find . -maxdepth 3 -type d | sort正常情况应该能看到 VOCdevkit/VOC2012/JPEGImages、VOCdevkit/VOC2012/SegmentationClass、SBD/dataset/img、SBD/dataset/cls 这几个关键目录。这时候不要急着写训练代码,先确认两件事。
第一,VOC 的 mask 文件数和 SBD 的 mask 文件数是否完整,缺少标注会在训练中断断续续报错,难以排查。第二,JPEGImages 里全部是 JPG 后缀,SBD/img 里也都是 JPG,但有些镜像下载的包会把图片转成 PNG,格式不一致会在图像解码时报错。检查文件数用 find 命令:
find VOCdevkit/VOC2012/JPEGImages -name "*.jpg" | wc -l find SBD/dataset/cls -name "*.png" | wc -l比较官方公布的数量,数量对不上就去检查下载包是否完整。这一步做得好,后面能省很多「图片读不出来」的调试时间。如果从网盘或别人分享的镜像下载,解压后务必确认两级目录名,PASCAL 相关代码里路径写死成 VOCdevkit/VOC2012 的情况很常见,目录名一变,所有相对路径全部失效。
3.2 统一标注格式:从彩色掩码到 class id
两套数据的标注格式不同:VOC 的 mask 是 palette PNG,用 PIL 读取后得到索引数组;SBD 的 cls 是单通道 PNG,读取后直接是类别数值。为了让训练代码只认一种格式,我的习惯是把两份 mask 全部统一成 uint8 的 npy 缓存,只保留类别索引。
import numpy as np from PIL import Image from pathlib import Path def voc_mask_to_id(path): img = Image.open(path).convert('P') return np.array(img, dtype=np.uint8) def sbd_mask_to_id(path): arr = np.array(Image.open(path), dtype=np.uint8) if arr.ndim == 3: arr = arr[..., 0] # SBD 偶见三通道存储,取第一通道 return arr def unify_mask(mask): mask = mask.copy() mask[mask == 255] = 255 # ignore 区域统一为 255 return mask # 批量缓存示例 root = Path('./data') out_dir = root / 'SegmentationClass_npy' out_dir.mkdir(exist_ok=True) for p in (root / 'VOCdevkit/VOC2012/SegmentationClass').glob('*.png'): mask = voc_mask_to_id(p) np.save(out_dir / f'{p.stem}.npy', unify_mask(mask))逻辑说明:VOC 的 P 模式图像,np.array 拿到的就是像素索引,0 是背景、1-20 是类别、255 是未标注边界;SBD 的 cls 本身也是 0-20,少数文件存在 255。统一处理后所有 mask 的语义就是「0 背景,1-20 类别,255 忽略」。参数表里唯一需要留意的值是 255 的忽略编号,它必须和损失函数里的 ignore_index 一致。
统一成 npy 后,训练时的读取开销也会下降:PIL 每次加载 PNG 需要解码调色板,npy 直接 np.load 即可。缓存放在 SegmentationClass_npy 目录,原目录保留不改动。这个预处理脚本看起来不起眼,但它是整个复现流程里最值得保留的一段代码,后期换数据集只需要改读取函数,主训练代码一行不用动。
3.3 生成 split 对应的图像列表
PASCAL-5i 实验里几乎每个 repo 都会生成自己的 split 列表,格式无非是 txt,每一行是「图像相对路径 + 类别」。为了避免采样时反复扫描全量数据浪费时间,按照类别建立一张「类别到图像列表」的映射表最实用。
import numpy as np from collections import defaultdict def build_class_index(mask_dir, img_dir): class_to_images = defaultdict(list) for npy_path in sorted(mask_dir.glob('*.npy')): mask = np.load(npy_path) classes = np.unique(mask) classes = classes[(classes > 0) & (classes < 255)] for c in classes: name = npy_path.stem class_to_images[int(c)].append(str(img_dir / f'{name}.jpg')) return class_to_images逻辑说明:读取每张图的 mask,找出它包含的所有物体类别,把图路径登记到对应类别的桶里。class_to_images 的 key 是 1-20 的类别编号,value 是该类别出现的所有图像路径列表。后续构建 episode 时不再扫描 mask,只需要查表,速度提升明显。参数说明:sorted 保证多进程下顺序稳定;过滤掉 255 是因为 ignore 不能参与分类计数;考虑到 1-20 类别编号与 VOC 类别名的映射,建议同时生成一个 id2name 字典,方便可视化时在图像上打印类别名。
生成列表后,按 fold 导出成文件也是家常便饭:
def save_split(class_to_images, fold_classes, out_file): with open(out_file, 'w') as f: for c in fold_classes: for img_path in class_to_images[c]: f.write(f'{img_path} {c}\n')这个文件在验证时用来统计「每个查询类有多少张可采样图」。一个常见现象是 bottle、pottedplant 这类小类别在 VOC val 里只有 30 张左右带标注的图,采样支持图像时很容易反复抽到同一批,造成验证结果方差增大。把类别样例数打印出来,就能知道该不该固定随机种子、该用多少个 episode 做评估,而不是盲从论文里写的固定数值。
4. 构建 few-shot episode:n_way、n_shot 采样逻辑与 batch 组织
4.1 episode 采样的核心流程
few-shot 分割的训练单元不是一张图,而是一个 episode。一个 episode 通常由一个查询图(query)和一组支持图(support)组成,支持图提供「当前任务的目标类别外观」,查询图是模型需要实际分割的图。以 PASCAL-5i 的标准做法为例,训练时随机从支持类里抽 n_way 个类别,每个类别抽 n_shot 张支持图,查询图指定为其中一个类别。整体流程如下:
第一步,从训练用支持类里随机选出 n_way 个类别。第二步,对每个类别,在 class_to_images 里随机选出 n_shot 张支持图和 1 张查询图,查询图不能和该 episode 的任何支持图重复。第三步,将支持图裁剪后与对应 mask 在通道维拼接,作为一个分支的输入;查询图与查询 mask 作为另一个分支的监督目标。第四步,模型从支持图像上学到目标类别的嵌入,再在查询图上预测。
这里最容易搞错的是「支持类别」和「查询类别」的关系。训练阶段,支持类和查询类来自同一个类别集合——模型要学习的是「从支持图中提取目标类特征并应用到查询图」这个能力,而不是学习某个固定类别的参数。因此训练时各类别不断随机组合,验证时再把 fold 里的 5 个查询类单独拿出来测。理解这一点,才能理解为什么 PASCAL-5i 的划分会让每个 fold 的查询类逻辑上互斥。
4.2 一个可以直接跑通的 PyTorch Sampler
下面这段是一个可运行的 episode 采样器骨架,核心是上面的类桶查表,输出每个 batch 的样本索引:
import random from torch.utils.data import Sampler class EpisodeSampler(Sampler): def __init__(self, class_to_images, train_classes, n_way, n_shot, episodes): self.c2i = class_to_images self.train_classes = train_classes # 训练阶段所有支持类 self.n_way = n_way # 每个 episode 的类别数 self.n_shot = n_shot # 每类支持图数量 self.episodes = episodes # 总 episode 数 def __iter__(self): for _ in range(self.episodes): # 1. 随机选 n_way 个类别 ways = random.sample(self.train_classes, self.n_way) episode_files = [] for cls in ways: images = self.c2i[cls] # 2. 每类取 n_shot 支持图 + 1 查询图 picked = random.sample(images, self.n_shot + 1) support = picked[:self.n_shot] query = picked[self.n_shot:] for sp in support: episode_files.append((sp, cls, 'support')) episode_files.append((query[0], cls, 'query')) yield episode_files逻辑说明:外层循环控制总 episode 数,内层对每个类别取 n_shot+1 张图,最后一张当查询图。yield 返回的是一个元组列表,每个元组包含图像路径、类别编号、角色标记。Dataset 拿到这些元组后,按角色读取图像并返回对应张量。这样设计的好处是采样逻辑和图像读取解耦,后续想改成 2-way、增加负样本类别桶,只需要改这一个 Sampler。
参数说明:n_way 在训练时常见取 1 或 2,取值太大会让支持信息溢出,模型要同时区分多个类别;n_shot 常见取 1 或 5,论文里的 1-shot 和 5-shot 结果必须分开报告;episodes 控制训练步数,一般与迭代次数对齐。如果用的是分布式训练,这个 Sampler 需要重写为每张卡独立采样,否则多卡会拿到完全相同的 episode,batch 意义消失。
4.3 关键参数怎么调:shot、分辨率与增强
支持图分辨率是 few-shot 分割里最容易影响结果的参数之一。常用做法是先把图片缩放到 321×321 或 473×473,再随机裁剪到训练尺寸;验证阶段用整图缩放加 padding。分辨率太低时,bottle、pottedplant 这类小物体的掩码很容易缩成一个像素点,类别信息几乎消失。我一般先开启 dataset 的可视化,打印几组 episode 看看支持图 mask 里目标像素的最小占比,如果某个类目标区域不足 100 像素,先考虑提升输入分辨率,而不是改模型。
| 参数 | 常见取值 | 对结果的影响 |
|---|---|---|
| n_shot | 1 / 5 | 5-shot 通常比 1-shot 高 8-12 个点 mIoU |
| 输入分辨率 | 321 / 473 / 512 | 分辨率太低时小物体类别直接失效 |
| 增强强度 | 翻转 + 缩放 + 颜色抖动 | 过强增强会导致小目标掩码被裁掉 |
| ignore_index | 255 | 与预处理和损失函数不一致会出现 NaN |
数据增强方面,few-shot 任务比普通分割更依赖颜色与形状不变性。常用组合是随机水平翻转、随机缩放、颜色抖动,但对 query 的增强不应改变类别的相对位置。注意:SBD 的 mask 是严格对齐原图的,增强时必须对图像和 mask 用同一套随机参数,翻转方向、缩放尺度、裁剪区域都要一致。做法是把 mask 也当作图像送入变换,用 torchvision.transforms 里的 functional 统一处理,不要分别设置随机种子,否则会出现 mask 和图像错位的翻车现场。
增强的强度建议从弱到强:很多小样本分割论文的结果差异就来自增强策略,翻车后先关掉增强再对比,确定基线可信后再逐步加增强。支持分支的梯度还有一个细节:训练时支持图只需要输出特征,不需要像素级梯度,反向传播时对支持分支 detach 一次,显存能省下接近三分之一,结果几乎不变。
5. 踩坑记录:PASCAL-5i 复现中的五个常见问题
5.1 现象:SBD 掩码加载后类别整体错位
现象:训练第一轮 loss 正常,但验证时 mIoU 只有个位数,打印预测图发现预测的类别和真实类别总是差一个固定偏移,比如真实是 car,模型预测成 bus。
原因:SBD 的类别索引在某些实现里不是 0-20,而是遵循 SBD 自身的数据集约定,用 1-20 对应 PASCAL 的 1-20,但下载到的版本里有的文件实际存的是 1-20、背景是 255。读取时如果直接沿用 VOC 的「0 背景」假设,就会把背景类和物体类整体偏移。
解决:在预处理阶段用 3.2 节的 unify_mask,打印一张图 mask 的 np.unique 结果,确认类别集合是 0-20 加 255,而不是 1-21 或者 255 之外还有 254。把 mask 统计脚本跑一遍再进入训练,能避免大多数这类问题。
5.2 现象:mIoU 比论文高太多,怀疑数据泄露
现象:某开发者跑完 fold0,mIoU 比文献高 7 个点,第一反应是模型更强,仔细检查后发现支持图像列表和查询图像列表有交集。
原因:构建 class_to_images 时用了全部 trainval 图像,没有区分 train 和 val。等于有些查询图在支持集里也出现过,模型其实见过测试场景的标注,结果虚高。
解决:class_to_images 必须按数据来源分区。训练支持图像只从 SBD train 构建,验证支持图像和查询图像都从 VOC val 构建,两个列表必须做一次交集校验。写一个断言:len(set(support_list) & set(query_list)) == 0,每次启动训练前执行,成为固定习惯。
5.3 现象:mask 全部是背景,loss 变成 NaN
现象:训练几轮后 loss 突然变成 NaN,检查代码逻辑没找到问题,打印查询 mask 后发现整张图只有一个类别 0。
原因:随机采样时选到了「目标类别有效像素极少」的图像,缩小分辨率后目标区域被完全吞掉,mask 退化为全背景。
解决:在采样后加一步过滤,统计每组 episode 中每个类别 mask 的非零像素数,少于阈值(比如 50 像素)就重新采样。另外检查损失函数是否对空目标做了保护,常见做法是让 loss 在 target 全为背景时返回 0,但更推荐直接过滤采样,因为它不会掩盖真实边界样本的分布。
5.4 现象:四个 fold 之间的 mIoU 差距非常大
现象:fold1 的 mIoU 45,fold3 只有 25,看上去像某个 split 的实现 bug。
原因:PASCAL-5i 的四个 fold 测试类别天然不平衡。fold3 里包含 diningtable、pottedplant 这类小物体和屋内场景,支持图中目标占比普遍偏低,分割难度本来就大;fold0 里的 aeroplane、bus 这类大物体更容易被预测准。
解决:报告结果时必须逐个 split 写,不能只报一个平均。对比方法时固定同一套 fold 顺序,先确认别人的实验用了哪几个 split,再对齐评估的 episode 数量与随机种子。如果发现 fold 之间差异超过 10 个点,这通常不是实现 bug,而是数据偏移,属于小样本分割的常态。
5.5 现象:支持集 15 张图一起前向,显存直接溢出
现象:训练时 batch size 设为 1,把支持图像拼接后输入,显存还是爆了,报 CUDA out of memory。
原因:支持图和查询图在同一个 batch 里一起前向,支持图数量等于 n_way×n_shot,加上高分辨率输入,显存占用翻倍。有些实现为了简单,把支持图和查询图在 batch 维拼接,这会让显存占用变成普通分割的数倍。
解决:显存紧张时把支持图和查询图分两次前向,支持图计算结果保存下来再与查询图特征做交互。或者缩小输入分辨率,用 321×321 而不是 473×473 跑通实验,验证结果后再调大。还有一个做法是把支持图分支的梯度截断,只对支持分支 detach,因为支持图像只需要输出特征,不需要像素级梯度。
6. 评估与验证:mIoU 计算、可视化抽检与样本量统计
6.1 mIoU 计算口径
PASCAL-5i 的评估指标是 mIoU,但计算细节写不对会直接影响结果可信度。常见口径是对每个查询类分别统计所有测试 episode 的预测掩码和真实掩码,计算该类别的 IoU,最后求 5 个类的平均;也有实现是先算每个 episode 的 mIoU 再平均,两种口径在类别样本不均衡时差异明显。建议采用「按类别汇总像素再算 IoU」的口径,因为它不受每类测试图像数量影响,和多数论文报告一致。
import numpy as np def compute_miou(pred, gt, num_classes=21, ignore_index=255): ious = [] for c in range(1, num_classes): p = (pred == c) g = (gt == c) inter = (p & g).sum() union = (p | g).sum() if union > 0: ious.append(inter.item() / union.item()) return np.mean(ious)逻辑说明:只计算 1-20 类的 IoU,背景和 ignore 不参与评估;union 为 0 说明该类别在当前 episode 中未出现,直接跳过。参数说明:num_classes 设为 21 是因为含背景,实际参与平均的是 20 个物体类,这一步要和验证集协议保持一致。如果某个查询类在所有 episode 中从未被预测到,mIoU 会被拉低,这是正常现象,不必当作代码 bug。
6.2 可视化抽检与样本量统计
评估之后,我有一个长期保留的习惯:把每个 episode 的支持图、查询图、真实掩码和预测掩码拼成一张画布保存下来,跑 100 个 episode 做人工抽检。这套做法的价值在于,数据划分、mask 统一、采样逻辑存在的 bug 几乎都会在可视化里暴露:支持图和查询图重叠会出现同一场景的重复图像,mask 错位会看到预测类别与颜色严重不符,支持图的类别外观不对会导致可视化图像里出现奇怪的颜色。
import cv2 import numpy as np def visualize_episode(support_imgs, query_img, pred, gt, save_path): h, w = query_img.shape[:2] canvas = np.zeros((h * 2, w * (1 + len(support_imgs)), 3), dtype=np.uint8) for i, s_img in enumerate(support_imgs): canvas[:h, i*w:(i+1)*w] = s_img canvas[h:, 0:w] = query_img canvas[h:, w:2*w] = pred canvas[h:, 2*w:3*w] = gt cv2.imwrite(save_path, canvas)这段是把所有图放到同一张画布上,横向排支持图,纵向排查询图、预测和真实掩码。参数说明:pred 和 gt 需要先转成可视化用的 3 通道伪彩色,最简单的方法是把 class id 乘以 20 作为像素值,虽然颜色不标准,但能看出大致轮廓;保存路径按 fold 和 episode 编号组织,方便回查。除了可视化,还要定期运行一个类别样本量统计脚本,打印每个类在 val 里有多少张图,这个数字决定了评估 episode 的总数设置。
从那以后,我每次换数据集、改划分、调采样逻辑,都强制自己先跑一轮 20 个 episode 的可视化抽检,再开始训练。这套「可视化 + 交集校验 + 类别样本统计」的检查流程,比任何调试器都能更快定位数据问题。PASCAL-5i 的数据规模不大,但协议的细节密度一点不低,希望上面的拆解和踩坑记录能帮你少走几趟弯路,希望帮到你。
本文还有配套的精品资源,点击获取