Pascal-5i小样本分割实战:从数据划分到mIoU提升的避坑指南
简介Pascal-5i 数据集面向小样本学习方向的研究者与算法工程师基于经典 PASCAL VOC 的 20 个类别通过采样策略划分基础类别与新类别支持 5-way 1-shot 与 5-way 5-shot 场景用于评估模型在极少样本下的泛化与适应能力。资源包共 6 个文件压缩后约 151KB包含 Python 读取脚本、Jupyter 示例笔记、依赖清单、说明文档及开源协议等便于快速加载数据、复现实验流程并接入原型学习、元学习或知识迁移等方法的训练与评测。目前已有 1444 人学习下载适合希望系统入门小样本学习、搭建标准化对比实验或优化评估指标的读者参考使用。1. 从一次翻车现场说起pascal-5i 数据集到底该怎么用去年帮一个做小样本分割的朋友跑 baseline他信誓旦旦说“pascal-5i 不就是 PASCAL VOC 切一切吗”结果训练时类别对不上、mask 全黑、mIoU 卡在 0.1 上不去。翻开源码一看他把trainaug和train的划分搞混了又把 5i 的 fold 顺序理解反了。这个数据集看着简单实际上坑全藏在“划分”和“标注格式”里。pascal-5i 是 PASCAL VOC 2012 的一个小样本学习few-shot learning衍生划分专门用于评估模型在“只给少量标注样本”条件下的分割或分类能力。它把 VOC 的 20 类前景按 5 类一组切成 4 个 fold每个 fold 里再分 support训练用少量样本和 query评估用。做小样本语义分割、少样本目标检测、原型网络对比实验的人基本绕不开它。如果你正在复现 FSS、PANet、PFENet 这类工作或者想验证自己的小样本策略这份资源就是你的起点。2. pascal-5i 的划分逻辑fold、support 与 query 到底怎么切2.1 四个 fold 与 5-way 的由来PASCAL VOC 2012 一共 20 类前景加上背景共 21 类。pascal-5i 的做法是把这 20 类随机均分成 4 组每组 5 类称为一个 fold。训练时模型只见过这 5 类中的 support 样本测试时要求模型对这 5 类中的 query 样本做分割。4 个 fold 轮流当测试集最终报告的是 4 个 fold 的平均 mIoU。这里有个容易误解的点fold 不是“数据子集”而是“类别子集”。同一个 fold 内support 和 query 共享这 5 个类别但图像不重叠。常见做法是 support 每类取 1-shot 或 5-shotquery 则用该 fold 剩余的所有标注图像。注意不同论文对 fold 的类别划分可能不同。有的用固定列表有的用随机种子生成。复现时务必以原论文或官方仓库的 split 文件为准不要自己随手切。2.2 support/query 的文件组织方式一份典型的 pascal-5i 目录结构如下以 VOC 格式为基础pascal-5i/ ├── fold0/ │ ├── train/ # support 图像与标注 │ │ ├── JPEGImages/ │ │ └── SegmentationClass/ │ ├── val/ # query 图像与标注 │ │ ├── JPEGImages/ │ │ └── SegmentationClass/ │ └── split.txt # 类别列表与样本 ID ├── fold1/ ├── fold2/ └── fold3/split.txt里通常每行是类别名 图像ID的格式或者直接给出 support/query 的 txt 列表。如果你拿到的包只有图像没有 split那基本没法直接用——这是第一个要检查的点。2.3 与 VOC 原始标注的对应关系pascal-5i 的 mask 仍然是 VOC 的SegmentationClass格式单通道 PNG像素值 0 表示背景1-20 表示对应类别255 表示忽略区域。但小样本场景下support 的 mask 往往只保留当前 fold 的 5 类其余类别被置为 0 或 255。如果你直接拿 VOC 原始 mask 训练模型会“偷看”到其他类别的像素评估结果虚高。我一般会写个脚本检查 mask 的像素值分布确认没有漏掉的类别import numpy as np from PIL import Image import os def check_mask_values(mask_dir, fold_classes): 检查 mask 中是否只包含 fold 内类别和背景 valid set([0, 255] fold_classes) for fname in os.listdir(mask_dir): if not fname.endswith(.png): continue mask np.array(Image.open(os.path.join(mask_dir, fname))) uniq set(np.unique(mask).tolist()) extra uniq - valid if extra: print(f{fname} 含非法类别: {extra})fold_classes是当前 fold 的 5 个类别 ID比如[1, 2, 3, 4, 5]。valid集合把背景 0 和忽略 255 也算进去。如果输出里有extra说明 mask 没清理干净需要重新生成。3. 从零跑通一个小样本分割 baseline数据加载与训练配置3.1 构建 Dataset 与 DataLoader小样本分割的 DataLoader 和普通分割不一样每个 batch 要包含 support 和 query 两组数据。常见做法是自定义EpisodeSampler每次采样一个 episode从 support 里抽 N 类各 K 个样本再从 query 里抽这 N 类的若干样本。import torch from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np import os import random class Pascal5iEpisode(Dataset): def __init__(self, root, fold, splittrain, n_way5, k_shot1): self.root os.path.join(root, ffold{fold}, split) self.n_way n_way self.k_shot k_shot self.img_dir os.path.join(self.root, JPEGImages) self.mask_dir os.path.join(self.root, SegmentationClass) self.samples self._load_split() def _load_split(self): # 假设 split.txt 每行: 类别ID 图像ID samples {} with open(os.path.join(self.root, .., split.txt)) as f: for line in f: cls, img_id line.strip().split() samples.setdefault(int(cls), []).append(img_id) return samples def __len__(self): return 1000 # 每个 epoch 的 episode 数 def __getitem__(self, idx): classes random.sample(list(self.samples.keys()), self.n_way) support_imgs, support_masks [], [] query_imgs, query_masks [], [] for c in classes: ids self.samples[c] chosen random.sample(ids, self.k_shot 1) for i, img_id in enumerate(chosen): img np.array(Image.open(os.path.join(self.img_dir, img_id .jpg))) mask np.array(Image.open(os.path.join(self.mask_dir, img_id .png))) if i self.k_shot: support_imgs.append(img) support_masks.append(mask) else: query_imgs.append(img) query_masks.append(mask) # 转 tensor 并归一化此处省略 return support_imgs, support_masks, query_imgs, query_masksn_way和k_shot是核心参数。5-way 1-shot 是最常见的设定也是 pascal-5i 论文里的标准配置。__len__返回的是 episode 数不是图像数这点和普通 Dataset 不同。_load_split里假设了split.txt的格式如果你的包格式不一样改这里就行。3.2 训练循环里的 support/query 前向逻辑小样本分割的 forward 一般分两步先用 support 算原型或特征再对 query 做预测。以原型网络为例def forward_episode(model, support_imgs, support_masks, query_imgs): # support: 提取特征并算原型 support_feats model.encode(support_imgs) # [N*K, C, H, W] prototypes [] for c in range(n_way): feat support_feats[c * k_shot:(c 1) * k_shot] mask support_masks[c * k_shot:(c 1) * k_shot] # 用 mask 做加权平均得到该类原型 proto (feat * mask).sum(dim(2, 3)) / (mask.sum(dim(2, 3)) 1e-8) prototypes.append(proto) prototypes torch.stack(prototypes) # [N, C] # query: 与原型算相似度 query_feats model.encode(query_imgs) # [Q, C, H, W] logits torch.einsum(qchw,nc-qnhw, query_feats, prototypes) return logitsmask在这里是二值化的 support mask只保留当前类为 1其余为 0。einsum那行做的是每个 query 像素与 N 个原型的余弦相似度假设特征已归一化。如果显存不够把query_imgs分批送进去。3.3 评估指标与常见参数评估用 mIoU但小样本场景下通常只算 fold 内 5 类的 IoU背景类有时算有时不算。常见做法是报告mIoU-55 类平均和mIoU-all含背景。学习率一般设 1e-3 到 1e-4用 Adam 或 SGD。batch size 指 episode 数通常 4 到 8 个 episode 一个 batch。参数常见取值说明n_way5fold 内类别数固定k_shot1 或 5support 每类样本数episode 数1000-5000每个 epoch 的采样次数学习率1e-3 ~ 1e-4Adam 常用 1e-3输入尺寸321x321 或 473x473对齐原论文4. 避坑与排查那些让 mIoU 掉一半的细节4.1 现象训练 loss 正常但 mIoU 接近 0原因query 的 mask 里包含了 support 类以外的像素模型预测被这些像素拉偏。或者 support 和 query 的类别 ID 没有对齐比如 support 里类别 1 在 query 里变成了类别 6。解决用 2.3 的脚本检查 mask 像素值确保只含 fold 内类别。再检查split.txt里类别 ID 是否和 mask 像素值一致。不一致就重新映射。4.2 现象4 个 fold 结果方差极大原因某个 fold 的 support 样本太少或质量差比如 1-shot 时恰好抽到一张遮挡严重的图。或者 fold 的类别划分本身难度不均。解决固定随机种子多跑几个 episode 取平均。如果某个 fold 持续低检查该 fold 的 support 图像列表看是否有损坏或全黑图。常见做法是每个 fold 跑 5 次不同种子报告均值和标准差。4.3 现象显存爆炸batch size 只能设 1原因episode 里 support 和 query 一起送进网络5-way 5-shot 时 support 就有 25 张图加上 query 轻松超过 30 张。如果输入尺寸还是 473显存直接拉满。解决把 support 的特征提取和 query 分开做support 算完原型后释放中间变量。或者用梯度累积模拟大 batch。输入尺寸降到 321 也能省不少显存。4.4 现象复现结果比论文低 10 个点以上原因论文里可能用了 base training在 VOC 全部 20 类上预训练再 fine-tune 到 5i而你直接从 5i 训。或者用了额外的数据增强、多尺度测试。解决先确认论文的 training protocol。如果它用了 base training你也得在 VOC 全量上先训一轮。多尺度测试翻转、缩放通常能涨 2-3 个点但推理时间翻倍。提示pascal-5i 的 split 文件在不同仓库间可能有细微差异复现前先对齐 split再谈模型结构。5. 进阶技巧用 5i 做跨 fold 验证与结果复现跑通 baseline 之后我一般会做两件事一是跨 fold 验证模型的稳定性二是把结果和公开 benchmark 对齐。跨 fold 验证的做法很简单把 4 个 fold 轮流当测试集每个 fold 跑 3 个不同随机种子记录 mIoU-5 和 mIoU-all。如果某个 fold 的 mIoU 比均值低超过 5 个点就要单独看这个 fold 的 support 样本。我遇到过 fold2 的 support 里有一张图 mask 全 255导致原型算出来是 NaNloss 直接炸了。后来加了个过滤逻辑mask 有效像素少于 100 的样本直接跳过。def filter_valid_samples(samples, mask_dir, min_pixels100): 过滤掉 mask 有效像素过少的样本 valid [] for img_id in samples: mask np.array(Image.open(os.path.join(mask_dir, img_id .png))) if (mask 0).sum() min_pixels: valid.append(img_id) return validmin_pixels根据图像尺寸调321x321 下 100 像素已经很少了再低基本是噪声。这个过滤只在 support 采样时做query 不动避免评估偏差。和公开 benchmark 对齐时注意论文报告的到底是mIoU-5还是mIoU-all以及是否用了 CRF 后处理。有些仓库默认开了 CRF你不开就低几个点。我一般先关掉所有后处理跑一个裸模型结果再逐步加 trick这样每一步涨多少心里有数。还有一个容易忽略的点pascal-5i 的图像尺寸不统一VOC 原始图有各种分辨率。常见做法是统一 resize 到 321 或 473但 resize 后小目标的 mask 会糊。如果做小目标分割建议用 padding 而不是直接 resize保持原始比例。从那以后我每次拿到新的小样本数据集都强制走一遍“查 split → 验 mask → 跑单 fold → 跨 fold 平均”的流程再也不敢跳过任何一步。希望这些踩坑记录能帮你少走点弯路。本文还有配套的精品资源点击获取