简介:这是一套面向图像分割任务的大型宠物语义分割数据集,适合计算机视觉学习者、算法工程师用于分割模型的数据准备与效果验证。压缩包共2000个文件,以PNG图像与对应掩码为主体,另含TXT类别说明和PY可视化脚本,整包约765.67MB;包内按images与masks目录组织样本,便于在主流分割框架中直接读取。数据已完成训练集与验证集划分,标注覆盖背景、宠物、边缘三类像素级标签,可直接支撑U-Net、DeepLab等常见分割网络的训练与评估;可视化脚本可随机抽取样本,将原始图像、GT标签图与GT叠加蒙版图并行展示,便于快速检查标注质量。目前已有48人浏览/学习,适合需要现成标注数据开展宠物分割实验的初学者和进阶研究者,也能节省数据采集与标注环节的时间成本。
1. 图像分割数据集:先想清楚这 7000 张图能替你解决什么
在图像分割项目里,真正劝退人的往往不是模型结构,而是数据集:自己用标注工具画一张带宠物的图,毛发边缘要描半天,画 100 张就没了耐心。这个大型宠物图像语义分割数据集直接把约 7000 张带像素级标签的宠物图片给你,省掉的是最耗时、最影响模型上限的数据准备环节。它能解决三个最实际的诉求:验证语义分割算法、做宠物抠图相关的前置模型,以及拿一份现成数据测试数据增强和迁移学习方案。适合拿来做算法对比实验的学生,也适合给宠物类产品做背景替换、美化功能的一线工程师。下面按“数据集长什么样 → 怎么训练 → 怎么评估 → 有哪些坑”往下拆。
2. 数据集的构成与标签体系:先摸清这 7000 张图的家底
2.1 图像来源与样本分布:分布决定训练上限
拿到数据集的第一步不是直接开训,而是先花半小时把数据摸清楚。约 7000 张的规模听上去不少,但对语义分割来说只是入门配置:一张 512×512 的输入图有超过 26 万个像素,模型要在这么多像素上学会区分猫、狗和背景,样本量稍一不足就会过拟合。很多宠物数据集的图片来源是日常拍摄和网络抓取的混合体,室内室外、白天夜晚都有,猫狗占比并不均衡,背景更是五花八门。这种分布决定了你后面要是不做任何处理直接训练,最终 mIoU 的上限就是被这些占比小的类别和场景拉低的。
先把类别像素占比统计出来,才知道损失函数里该不该加权:
import numpy as np from PIL import Image from pathlib import Path # class_map 示例:0 是背景,1 是猫,2 是狗,颜色需要和掩码保持一致 class_map = { 0: (0, 0, 0), 1: (255, 0, 0), 2: (0, 0, 255), } mask_dir = Path("data/masks") class_pixels = {} total_pixels = 0 for i, mask_path in enumerate(mask_dir.glob("*.png")): mask = np.array(Image.open(mask_path).convert("RGB")) for class_id, color in class_map.items(): class_mask = (mask == np.array(color)).all(axis=-1) class_pixels[class_id] = class_pixels.get(class_id, 0) + int(class_mask.sum()) total_pixels += mask.shape[0] * mask.shape[1] if i % 500 == 0: print(f"processed {i} masks")逻辑说明:遍历每张掩码,把 RGB 颜色值逐像素匹配到类别 ID,累加每个类别的像素总数,最后用各类像素数除以 total_pixels 得到占比。匹配时用np.all(mask == color, axis=-1)才能同时比较 RGB 三个通道,只比较单通道会把颜色相近的不同类别混到一起。统计完通常会发现背景占 70% 以上,猫和狗的像素占比可能相差两到三倍。如果在这个数据集上做的是“背景 / 猫 / 狗”三分类,类别权重建议按像素占比倒数来设;如果只做前景背景二分类,问题会少一些,但边缘依然难。
另一个容易被忽略的步骤是数据清洗。我一般会先删掉几类图:掩码中宠物像素占比过小的图(例如小于整张图 0.5%)、没有宠物主体的空标签图、以及无法正常打开的损坏文件。约 7000 张里挑出几十张问题数据很正常,它们不会让 mIoU 掉太多,但会让训练过程出现莫名其妙的 loss 抖动。清洗逻辑很简单:统计每张掩码的类别像素数量,低于阈值的单独列出来人工看一眼再决定删除还是保留。
2.2 标签格式与类别设计:从 RGB 掩码到单通道索引
宠物语义分割数据集的标签格式不统一,常见的有两种:一种是单通道索引图,像素值直接是类别 ID,常见于 P 模式 PNG;另一种是 RGB 颜色编码图,每个类别固定对应一种颜色。拿到数据集第一件事就是判断掩码的 mode,否则后续转换很容易埋雷。
from PIL import Image mask = Image.open("data/masks/0001.png") print(mask.mode, mask.getextrema())P 模式输出mode='P',getextrema 返回调色板索引范围;RGB 模式输出mode='RGB',getextrema 返回每个通道的最小最大值。把这一行放到数据加载脚本里跑一遍,能避免后面所有颜色映射相关的错误。
如果是 RGB 颜色编码,训练前必须把它转成单通道索引,类别 ID 从 0 开始连续递增。转换时注意三点:第一,类别 ID 一定要连续,模型输出通道数等于类别数,中间跳一个 ID 会导致训练时报 shape 不匹配;第二,未标注区域建议约定为 255,在损失函数里用 ignore_index 跳过,不要把它当成一个类别去训练;第三,颜色表要跟掩码完全一致,有的数据集里同一个类别可能有两种相近的颜色,需要先做颜色量化。
import numpy as np from PIL import Image def rgb_mask_to_index(mask_path, class_map): mask = np.array(Image.open(mask_path).convert("RGB")) h, w = mask.shape[:2] index = np.zeros((h, w), dtype=np.uint8) for class_id, color in class_map.items(): index[np.all(mask == np.array(color), axis=-1)] = class_id return index转换代码里 class_map 的 key 是最终类别 ID,value 是 RGB 颜色;for 循环里np.all(mask == color, axis=-1)逐像素匹配,把匹配到的位置填成对应类别 ID。转换后做一次合法性检查,统计索引图里最大类别 ID 是否小于类别数,如果出现超范围 ID,说明标签体系里混入了未处理的类别,要回查类别定义而不是急着调模型。
还有一个常见误区是直接对 RGB 掩码做灰度化再当标签用。灰度化之后不同类别的灰度值可能相同,比如 (255, 0, 0) 和 (0, 0, 255) 转成灰度后都是 76 附近,模型学到的是错误映射。判断方式就是上面那段 mode 检查,RGB 模式一律走颜色字典,P 模式走调色板。
2.3 数据划分:按 8:1:1 拆分并避免同一只宠物泄漏
数据划分直接决定你调参时看到的是真实水平还是虚高的分数。约 7000 张不算大,如果只用 8:2 切成两份,验证集只有 1400 张,随机性太大,调几轮学习率就会发现结果忽高忽低。我一般按 8:1:1 拆成训练、验证、测试三份,测试集只在最终评估时用一次。
data/ train/ images/ 001.jpg 002.jpg ... masks/ 001.png 002.png ... val/ images/ 011.jpg 012.jpg ... masks/ 011.png 012.png ... test/ images/ 021.jpg 022.jpg ... masks/ 021.png 022.png ...划分之前还有一个更重要的检查:同一只宠物不能既出现在训练集又出现在验证集或测试集。宠物数据集的图片往往来自同一只动物多次采集,同一只猫换个角度拍了七八张,如果按文件名随机切分,模型等于提前见过了这只猫,验证 mIoU 虚高五到八个点都很正常。判断有没有个体泄漏的办法是看文件名前缀,很多数据集命名带主体 ID,比如cat_001_02.jpg里的前两段就是个体编号。如果有这种规则,先按主体 ID 分组再划分:
import random from pathlib import Path random.seed(42) samples = sorted(p.stem for p in Path("data/images").glob("*.jpg")) random.shuffle(samples) n = len(samples) train = samples[:int(n * 0.8)] val = samples[int(n * 0.8):int(n * 0.9)] test = samples[int(n * 0.9):]固定 seed 的目的是让每次实验可复现,别人也能按同样的逻辑对比。如果文件名能看到主体 ID,不要直接这样切,先 group by 前缀再划分。文件组织上我推荐用 train.txt、val.txt 清单文件,每行一个样本名,DataLoader 按清单读取,省磁盘也不容易因为复制漏掉配对。
划分完成后记录每个集合的类别像素占比,确认三份数据的类别分布大致一致,避免训练集里猫多、测试集里狗多这种失衡。另外看一眼光照分布:如果测试集全部是室内白天的图,验证集最好也不要全选户外夜晚,否则调参时你会误以为模型泛化很好。语义分割最怕的就是验证集分布和真实使用场景不一致,这一点比模型选型更容易在后期翻车。
3. 把数据集喂给语义分割模型:U-Net 最小训练闭环
3.1 数据加载器:图片与掩码同步读取与预处理
训练语义分割模型的开端是写一个正确的 Dataset。最容易出错的地方不在模型,而在图片和掩码的同步问题:resize 不一致、随机增强不同步、掩码用了会插值的算法。下面这个 Dataset 类是 U-Net 训练的最小骨架:
import torch from torch.utils.data import Dataset from PIL import Image import numpy as np from pathlib import Path class PetSegDataset(Dataset): def __init__(self, image_dir, mask_dir, class_map, image_size=(512, 512)): self.image_size = image_size self.class_map = class_map self.ids = sorted(p.stem for p in Path(image_dir).glob("*.jpg")) self.image_dir = Path(image_dir) self.mask_dir = Path(mask_dir) def __len__(self): return len(self.ids) def __getitem__(self, idx): sid = self.ids[idx] image = Image.open(self.image_dir / f"{sid}.jpg").convert("RGB") mask = Image.open(self.mask_dir / f"{sid}.png").convert("RGB") image = image.resize(self.image_size, Image.BILINEAR) mask = mask.resize(self.image_size, Image.NEAREST) image = np.array(image).astype(np.float32) / 127.5 - 1.0 mask = rgb_mask_to_index(np.array(mask), self.class_map) image = torch.from_numpy(image.transpose(2, 0, 1)) mask = torch.from_numpy(mask).long() return image, mask核心约定有两条:图像用双线性缩放,掩码用最近邻缩放;掩码在__getitem__里转成单通道索引并转成 long tensor。rgb_mask_to_index沿用 2.2 里的转换函数,class_map 的传入让 Dataset 和标签体系解耦,换数据集时只要改颜色字典。返回的 image 是 float32 的 [-1, 1] tensor,mask 是 long tensor,CrossEntropy 要求标签必须是 long 且形状为 (H, W)。
提示:掩码任何一步都不能用双线性或立方插值,否则类别边界会出现“中间类别”,损失函数直接崩。
这里没写数据增强,是为了保持代码逻辑直观;实际训练时把随机水平翻转、随机裁剪、颜色抖动放在同步块里,具体见 5.5。加载时如果发现图片是 RGBA 模式,先convert("RGB"),否则后面 batch 时会因为 4 通道和 3 通道不一致报错。输入尺寸我习惯固定成 512×512,这是显存和精度的折中点;如果显存只有 8G,降到 384 也没问题,但不要随便用非方形尺寸,U-Net 下采样到最低层时非对称尺寸会带来额外 padding 问题。
3.2 模型选型与损失函数:为什么我从 U-Net 起步
语义分割算法选型要匹配数据规模。FCN 是较早把全连接层替换成卷积的全卷积语义分割模型,结构简单但上采样太粗糙,在宠物这种细毛边缘场景里表现不好。U-Net 的编码器-解码器加跳接结构对小数据集非常友好,跳接把低层细节直接送到 decoder,弥补了上采样丢失的边界信息。约 7000 张的规模下,U-Net 是门槛最低、最容易训稳的选择。显存够且想把分数推高时,再换 DeepLabV3+ 这类带空洞卷积和 ASPP 的结构,多尺度能力更强,但对显存和调参耐心要求也更高。
损失函数上,宠物分割最直接的痛点是背景像素占绝对多数。直接 CrossEntropy 能训,但模型很容易用“全预测背景”来骗过高准确率,所以我用加权交叉熵加 Dice 的混合损失:
import torch import torch.nn as nn import torch.nn.functional as F class WeightedCELoss(nn.Module): def __init__(self, class_weights): super().__init__() self.register_buffer("weight", torch.tensor(class_weights, dtype=torch.float32)) def forward(self, logits, labels): return F.cross_entropy(logits, labels, weight=self.weight, ignore_index=255) class DiceLoss(nn.Module): def forward(self, logits, labels): probs = F.softmax(logits, dim=1) one_hot = F.one_hot(labels, num_classes=probs.shape[1]).permute(0, 3, 1, 2) smooth = 1.0 intersection = (probs * one_hot).sum(dim=(2, 3)) union = probs.sum(dim=(2, 3)) + one_hot.sum(dim=(2, 3)) return 1 - ((2 * intersection + smooth) / (union + smooth)).mean() # loss = 0.5 * WeightedCELoss([0.3, 1.0, 1.5])(logits, labels) \ # + 0.5 * DiceLoss()(logits, labels)权重来自 2.1 统计的像素占比倒数,比如背景 0.3、猫 1.0、狗 1.5,具体值按实际分布调。Dice 损失对前景小物体更敏感,两个损失按 0.5 和 0.5 相加是我常用的起点。ignore_index=255用来跳过未标注区域,这在宠物数据集里很常见。需要说明的是,one_hot 转换会占额外显存,512×512×3 类还好,如果类别数超过 10,建议只在计算 Dice 时把 one_hot 下采样到低分辨率再做。
3.3 训练参数:batch size、学习率与 epoch 怎么调
模型和损失定好后,超参决定能不能收敛。我按 12G 显存、512×512 输入、U-Net 的配置给出下面的起点,显存小就优先降 batch size,其次降输入分辨率到 384。batch size 太小会让 BatchNorm 统计量抖动,所以即便显存小也不要低于 4。
| 参数 | 推荐起点 | 说明 |
|---|---|---|
| image_size | 512×512 | 八的倍数,方便下采样 |
| batch_size | 8 | 12G 显存稳妥值 |
| 优化器 | AdamW | weight_decay 1e-4 |
| 初始学习率 | 1e-4 | 预训练 backbone 解冻后可降到 5e-5 |
| 学习率调度 | poly, power=0.9 | 也可以每 20 epoch 乘 0.5 |
| epochs | 80~120 | 配合早停 patience 15 |
| 混合精度 | AMP | 显存减半,速度提升明显 |
poly 调度的公式是lr = base_lr * (1 - iter / total_iter) ** 0.9,语义分割里用它的原因是训练后期希望学习率缓慢降到一个极小值,让 decoder 的细粒度输出稳定下来。用 step 调度也能训,但需要多试几个衰减节点,poly 省心一点。
使用 ImageNet 预训练权重是约 7000 张这个规模下非常值得做的迁移学习手段。常见做法是取 imagenet1k 上训练好的分类骨干(ResNet34 或 ResNet50)作为 U-Net 编码器的初始权重,前 10 个 epoch 冻结 backbone 只训练 decoder,之后再解冻整个网络用更小学习率微调:
for name, param in model.named_parameters(): if "encoder" in name and "norm" not in name: param.requires_grad = False冻结操作的核心是遍历模型参数,把 encoder 里除了 norm 层的参数requires_grad设为 False。norm 层(BatchNorm 的 gamma 和 beta)无论如何都要训练,否则 BN 统计量和冻结参数不匹配,训练后期 loss 容易出现锯齿。前 10 个 epoch 里 decoder 会先学会基础轮廓,解冻后整个网络的收敛速度会快很多,这也是小数据集上最稳的训练顺序。
4. 评估语义分割:mIoU 之外再看这几张可视化
4.1 指标计算:从混淆矩阵到逐类 IoU
训练跑到一半,最关心的问题就是:模型到底变好了没有?语义分割的标准指标是 mIoU,但只看最终数值会让你漏掉大量问题。正确做法是先构建混淆矩阵,再算逐类 IoU,最后取平均。构建混淆矩阵时注意过滤掉 ignore_index 的像素,否则未标注区域会污染结果。宠物分割里背景占大头,像素准确率可以轻松到 0.9 以上,但猫狗的 IoU 可能只有 0.5 上下,所以打印指标时要逐类打印,不要只打一个 mIoU。
import numpy as np def build_confusion_matrix(labels, preds, num_classes): labels = labels.flatten() preds = preds.flatten() mask = (labels >= 0) & (labels < num_classes) return np.bincount( labels[mask] * num_classes + preds[mask], minlength=num_classes * num_classes ).reshape(num_classes, num_classes) def compute_iou(confusion_matrix): intersection = np.diag(confusion_matrix) union = confusion_matrix.sum(axis=0) + confusion_matrix.sum(axis=1) - intersection per_class_iou = intersection / (union + 1e-6) return per_class_iou逻辑说明:labels 和 preds 的形状都是 (H, W),flatten 后通过labels * num_classes + preds把二维混淆矩阵的坐标映射成一维索引,再 bincount 回矩阵。这个写法比双重循环快很多,验证集几千张图也能在几秒内算完。per_class_iou 里加 1e-6 防除零,因为某些小类别可能没出现在样本里。mIoU 是所有类别的平均值,遇到样本极不均衡时,建议同时输出 weighted IoU,按各类像素占比加权,那个值更贴近产品实际体验。
4.2 错误分析:把 IoU 最低的失败样本挑出来看
指标只能告诉你模型多差,不能告诉你差在哪。我习惯在验证阶段把每个样本的逐类 IoU 存下来,然后把猫或狗 IoU 最低的 20 张图做成拼版:左边原图、中间 GT、右边预测,错的地方一眼就能看出来。
import numpy as np from PIL import Image def save_overlay(image, label, pred, path): # label_color 和 pred_color 分别是 GT、预测类别映射后的 RGB 颜色图 overlay = image * 0.4 + label_color * 0.3 + pred_color * 0.3 Image.fromarray((overlay * 255).astype(np.uint8)).save(path)可视化代码里用半透明叠加而不是直接拼接,因为把原图和预测并排时,人眼对微小错位的敏感性远低于半透明叠上去的色差。重合区域会自然变成黄色,错分区域一眼可见。宠物分割里最常见的失败模式是深色宠物在暗光环境下的边缘和背景融为一体,其次是细尾巴、耳朵尖和胡须这类长尾结构被模型直接丢弃。如果低 IoU 样本集中在某个场景,说明不是模型问题,是训练数据里这个场景太少,这时候加增强比换模型更有效。
保存下来的失败样本还可以当标注质量的检查材料。有时 mIoU 低不是模型差,而是 GT 本身就画错了:耳朵尖标进去或者尾巴缺了一截。这种情况要先修标签再骂模型,否则你对着一个错标签调三个月网络也是白搭。
最后补充验证策略。约 7000 张这个规模下,训练时用 512,推理时可以试试把图像放大到 768,或者做水平翻转的 TTA,通常能提升 1~2 个点的 mIoU,代价是推理时间翻几倍。验证阶段算指标比较慢,常见做法是每 5 个 epoch 跑一次完整验证,而不是每 epoch 都跑,mIoU 曲线平滑度也更好。最终报告里要写清楚输入尺寸、是否做 TTA、是否用 CRF 后处理,否则复现你对不上数字。
5. 宠物分割的常见坑与排查:五条高频踩坑记录
5.1 PNG 掩码读出来全黑或颜色对不上
现象:训练 loss 正常,可视化掩码时全黑,或者预测结果和 GT 完全错位。
原因:掩码是 P 模式调色板 PNG,直接用convert("RGB")或convert("L")读出来以后,颜色顺序和数据集文档里的颜色表对不上;P 模式的调色板索引到 RGB 需要查表,直接当灰度图读就是黑乎乎一片。
解决:先打印 mask.mode,P 模式用 getpalette() 取调色板,按调色板索引转类别;RGB 模式用颜色字典逐像素匹配。一个小技巧是随机抽三张掩码,把每个类别的像素取出来看平均 RGB,和 color map 对比后再写进 class_map。
im = Image.open(mask_path) if im.mode == "P": palette = im.getpalette() # 每 3 个值是一个 RGB 颜色 elif im.mode == "RGB": # 按颜色字典逐像素匹配 pass5.2 背景占比太大,模型直接摆烂预测全背景
现象:验证时总 mIoU 有 0.6 以上,但猫和狗的逐类 IoU 不到 0.1。
原因:CrossEntropyLoss 被背景 70% 以上的像素主导,模型发现全预测背景就能拿到不低的 loss 下降,直接放弃前景。另一个隐藏问题是未标注区域没设 ignore_index=255,模型把不该学的像素也学进去了。
解决:换加权 CE 或 CE+Dice 混合损失,类别权重按像素占比倒数设置;训练脚本里同时输出 per_class IoU,不让总 mIoU 掩盖问题。检查 loss 曲线的话,背景类占比高时 loss 下降很快,但前景类 IoU 迟迟不动,基本就是这个坑。
5.3 毛发边缘和细尾很容易被割断
现象:预测图里胡须消失、尾巴中断、耳朵边缘锯齿严重。
原因:输入分辨率低、掩码 resize 用了插值、模型缺少多尺度特征,三者叠加导致细长结构被吞掉。耳朵尖和尾巴尖只占几个像素,下采样一次就可能消失。
解决:把输入提高到 512 以上,DeepLabV3+ 的 ASPP 或 U-Net 的深监督都会有帮助;推理时用多尺度拼接,把原图、缩放 0.8 倍和 1.2 倍的预测取平均;边缘后处理上 CRF 能稍微修整,但治标不治本。如果项目对边缘要求高,可以在损失里加一个边缘感知项,对 GT 的 Sobel 梯度图区域加大权重。
5.4 图片尺寸不统一导致 DataLoader 报错
现象:训练跑到一半RuntimeError: stack expects each tensor to be equal size。
原因:数据里有几张宽高不一致的图,甚至包含 4 通道 RGBA,Dataset 里没统一尺寸就进了 batch。宠物数据集常见上传图片尺寸各异,文件名还不一定按尺寸分组。
解决:__getitem__里强制 resize 到固定尺寸,所有输入先convert("RGB");再写一行检查所有 mask 的 size 集合,确认没有漏网之鱼:
sizes = {Image.open(p).size for p in mask_dir.glob("*.png")} print(sizes)如果 size 集合里出现一个奇怪的值,单独处理那几张,不要直接删,先看是掩码本身画错了还是文件损坏。用 collate_fn 做 padding 也能强行跑起来,但 padding 区域最后预测结果会被忽略,指标会偏乐观,工程上不推荐。
5.5 数据增强后图像和掩码错位
现象:加了随机翻转和裁剪后验证 mIoU 不升反降,loss 震荡加剧。
原因:图像和掩码各自执行了随机变换,随机种子不一致,或者掩码 resize 用了双线性。翻转后猫跑到图左边,掩码还留在右边,模型等于在学习乱码。
解决:图像和掩码共享同一个随机种子,颜色抖动只作用于图像。同步变换的代码框架是这样:
seed = torch.randint(0, 2 ** 32, ()) torch.manual_seed(seed) image = transform(image) torch.manual_seed(seed) mask = transform(mask)水平翻转、随机裁剪、缩放都要执行这个双 seed 操作;颜色抖动只在图像部分做,掩码绝对不能做颜色变换。把同步 transform 封装成一个函数,避免在 Dataset 里反复复制粘贴,增强逻辑变更时也只改一处。
6. 把 7000 张图用出 7 万张的效果:迁移学习与 CutMix
这个规模的数据集,换一个更复杂的模型往往不如在数据和训练策略上下工夫。第一个必做项是迁移学习:U-Net 编码器用 ImageNet 预训练权重初始化,前 10 个 epoch 冻结 encoder,只让 decoder 收敛,之后再解冻全网络、把学习率降到 5e-5 微调。以前总觉得 7000 张够多,随机初始化也能训,后来发现 train mIoU 0.97、val mIoU 0.72 的过拟合差距,就是从换预训练权重开始缩小的。
第二个值得做的是 CutMix 风格的增强。语义分割里不能直接把两张图拼在一起,标签图必须同步拼。做法是随机从 batch 里取另一张图,生成一个矩形区域,把当前图和标签对应区域替换成另一张图的区域。这个增强对宠物分割特别有效,让模型不能依赖背景来判断类别,能强迫它学习猫狗形体本身。
import numpy as np def cutmix(image, label, image2, label2, region=(0.25, 0.5)): h, w = image.shape[1:] bh = int(h * np.random.uniform(*region)) bw = int(w * np.random.uniform(*region)) y0 = np.random.randint(0, h - bh) x0 = np.random.randint(0, w - bw) image[:, y0:y0+bh, x0:x0+bw] = image2[:, y0:y0+bh, x0:x0+bw] label[:, y0:y0+bh, x0:x0+bw] = label2[:, y0:y0+bh, x0:x0+bw] return image, label矩形区域面积控制在整张图的 0.25 到 0.5 倍之间,太大让模型困惑,太小起不到强迫学习的作用。配合 CE+Dice 混合损失用,比单独用任何一种都稳。选择 CutMix 的样本时尽量选类别差异大的组合,比如猫和狗拼一起,效果比猫拼猫好。
最后是一条老教训:用随机初始化 U-Net 直接训练这 7000 张图,训练曲线看起来一切正常,翻车点全在验证阶段;把预训练、同步增强、CutMix 三者叠加后,val mIoU 才稳定到能落地的水平。数据增强和迁移学习不是玄学,是这个规模下最值得先花时间的两个方向。希望帮到你。
本文还有配套的精品资源,点击获取