简介:这套乳腺癌细胞分割图片数据集面向病理图像分析与深度学习研究者,围绕H&E染色组织病理图像中的细胞分割任务构建,旨在支撑良性细胞与恶性细胞的自动分类研究。压缩包内共232个文件,含116张TIF格式组织病理图像及116个对应的XML标注文件,图像与真实标注逐张匹配,整体约93.7MB,便于直接用于模型训练与验证。目前已有270人浏览学习,适合作为医学影像分割、目标检测等方向的实验数据。H&E染色是常规组织学手段,因大多数细胞近乎透明且缺乏固有色素,需借助特定染料识别生物结构;该数据集正是针对此类图像提供细胞区域标注,使研究者能省去繁琐的人工标注步骤,专注分割网络的搭建与调优,为后续细胞良恶性判别奠定数据基础。
1. 乳腺癌细胞分割:58 张 H&E 病理图能做什么,值得下吗
做病理图像的从业者都清楚,分割模型在公开自然图像上再强,换到 H&E 染色切片上照样失灵。这个乳腺癌细胞分割图片数据集只有 58 张组织病理学图像,统一是 H&E 染色,附有真实标注,文件命名里直接标了 benign 和 malignant。它的价值不在量大,而在于样本小到刚好能逼你把数据管线处理干净:染色归一化怎么做、patch 怎么切、验证集怎么分才不穿帮。适合想跑通细胞级分割流程的算法工程师、做医学影像课题的学生,以及需要一份小样本基准数据验证 U-Net 类模型的从业者。别嫌它小,小数据反而更容易暴露工程问题,也更适合用有限的显存做完整实验。
2. 先读透数据:H&E 染色规律、文件名编码与目录摆放
这个数据集的命名并不复杂,但信息密度很高,最值得关注的几个字段直接决定了标签怎么组织、验证集怎么划分。如果跳过这一步直接进入训练,很容易在数据划分上翻车。这一章先把染色背景、文件名规则和目录组织讲清楚。
2.1 为什么是 H&E 染色:颜色信息对细胞分割的意义
常规组织学使用苏木素和伊红染色,也就是常说的 H&E。大多数细胞基本上是透明的,几乎没有固有色素,所以必须用染料把特定结构显示出来。苏木素会把细胞核染成蓝紫色,伊红则让细胞质和细胞外基质呈现粉红色。这种对比让细胞核的轮廓在图像里变得相对清晰,因此 H&E 染色图像成为病理细胞分割最基础的训练素材。
从深度学习的角度看,颜色在 H&E 图里是强特称。模型可以依靠核的蓝紫色和胞浆的粉红色来识别区域,而不需要理解复杂纹理。但问题也随之而来:不同切片、不同批次的染色浓度、扫描设备差异都会导致颜色漂移。同一个病例的切片可能偏蓝,另一个病例可能偏红。58 张图虽然量不大,但只要是来自不同 ytma 编号的病例,染色差异基本躲不掉。后续做染色归一化,不是因为模型学不会颜色,而是为了让模型不要过度依赖“这个蓝紫色只属于某个病例”这类偷懒特征。
2.2 从文件名里拆出标签信息:benign / malignant 与 CCD 后缀
文件名是理解数据集的第一入口。以 ytma49_042203_benign2_ccd.tif 和 ytma12_010804_malignant2_ccd.tif 为例,按常见病理图像命名习惯,可以拆成几个有意义的部分。
| 文件名片段 | 示例 | 常见含义 |
|---|---|---|
| ytma49 / ytma12 | ytma49 | 病例编号或组织芯片编号,代表样本来源 |
| 042203 / 010804 | 042203 | 切片编号、时间批次或位置编号,用来区分同一病例的不同切片 |
| benign2 / malignant2 | benign2 | 图像级标签:良性/恶性,数字是该类别样本序号 |
| ccd | ccd | 采集设备类型,说明该图由 CCD 相机拍摄 |
在实际操作里,ytma 后面的编号可能是患者编号,也可能是组织阵列编号,我一般直接把它当作 case_id 来处理。benign2 和 malignant2 是图像级类别标签,本来用于后续的良恶性分类任务。但分割任务需要像素级标注,图像级标签只是辅助信息,不能替代真实标签。ccd 后缀对模型没有直接影响,但提醒你这类图的拍摄设备比较统一,如果以后混入 scanner 采样的图,光照和纹理会有明显差异。
2.3 目录组织与划分:按病例分组,避免数据“穿帮”
拿到数据后我不会急着写模型,先把目录整理成 images/ 和 masks/ 两棵平行树。真实标签不管是什么格式,最终都要能跟原始 tif 文件名一一对齐。常见做法是:
mkdir -p breast_cell_seg/{images,masks} # 假设原始 tif 在 ./raw,标签文件在 ./raw_masks find ./raw -name '*.tif' | while read f; do base=$(basename "$f") # 提取病例号,用于后续按病例划分 case_id=$(echo "$base" | cut -d'_' -f1) echo "$base belongs to $case_id" cp "$f" breast_cell_seg/images/ # 如果标签文件存在同名文件,复制到 masks if [ -f "./raw_masks/$base" ]; then cp "./raw_masks/$base" breast_cell_seg/masks/ fi done这段脚本用 cut 提取下划线分隔的第一段作为病例号,然后把图像复制到 images 目录。注意这里用的是 cp,如果磁盘紧张可以改成 ln -s 或者直接 mv。关键点是 mask 文件名必须和图像文件名完全一致,否则后续 Dataloader 按文件名配对时会出现错位,而且这种错位不会报错,只会造成训练标签张冠李戴。
真正要重视的是验证集划分。58 张图如果直接随机 split,同一个病例的不同切片很可能同时出现在训练集和验证集中。模型在训练时已经见过该病例的染色风格和同源组织纹理,验证指标会比真实场景高出一截。我一般会写一个小脚本统计病例分布:
import os import re files = [f for f in os.listdir('breast_cell_seg/images') if f.endswith('.tif')] case_ids = [] for f in files: m = re.match(r'(ytma\d+)_', f) case_ids.append(m.group(1) if m else f) case_counts = {} for c in case_ids: case_counts[c] = case_counts.get(c, 0) + 1 print(case_counts)这个脚本用正则把 ytma 开头的病例号提出来,统计每个病例有多少张图。如果某个病例只有 1 张图,把它放到训练集,验证集就永远见不到这个病例的表现;把它放到验证集,训练时又少了这个病例的信息。对于这种情况,宁可做留一病例交叉验证,也不要按文件名随机分配。58 张的小数据集,千万不能在这个环节节省时间。
2.4 真实标注与图像配对:动手前先做一次像素级体检
很多数据集自带的 ground truth 并不都是标准二值 mask,可能是灰度标签图,也可能是带颜色编码的 PNG。这个数据集的摘要里只说“附有真实数据”,没有细说文件格式,所以第一件事就是检查标签跟图像的 shape 和值域是否匹配。
import tifffile import numpy as np img = tifffile.imread('breast_cell_seg/images/ytma49_042203_benign2_ccd.tif') mask_path = 'breast_cell_seg/masks/ytma49_042203_benign2_ccd.tif' mask = tifffile.imread(mask_path) print('image shape:', img.shape, img.dtype) print('mask shape:', mask.shape, mask.dtype) print('mask unique values:', np.unique(mask))如果 mask 的 shape 和 img 不一致,通常是因为标签图是单通道而原图是三通道,或者标签图经过缩放。这时需要根据原始分辨率做对齐,常见做法是先用 PIL 或 cv2 检查尺寸,再用最近邻插值把 mask 缩回原图大小,注意不能使用线性插值,否则会引入中间灰度值,导致二值标签变成软标签。如果 mask 的值域范围是 0 和 255,要除以 255 转成 0 和 1;如果存在 1 和 2 这类编码,需要重新映射。这一步最好写进预处理脚本,每次加载数据前自动断言一次,而不是靠肉眼抽查。
3. 训练前的预处理:TIF 读取、染色归一化与 patch 化
模型训练之前,预处理占了整个工作量的百分之六十。TIF 的读法、归一化的选择、patch 的大小和重叠度,最后都会显著影响分割结果。这一章按实际执行顺序来写,从文件读取一路到训练样本生成。
3.1 正确读取 TIF:位深、通道与分辨率
病理图像通常是 16 位 TIF,而不是常见的 8 位 JPG。直接用 PIL 打开 16 位 TIF 时,很多版本会默认转成 8 位,造成信息截断。更稳妥的做法是用 tifffile 读取,保留原始 dtype 和通道信息。
import tifffile import numpy as np img = tifffile.imread('breast_cell_seg/images/ytma49_042203_benign2_ccd.tif') print('shape:', img.shape, 'dtype:', img.dtype)输出可能是 (H, W, 3),也可能是 (H, W),三通道是 RGB 彩色图,单通道是灰度图。如果是 uint16,范围在 0 到 65535,需要缩放到 0 到 1,再转 float32。不要盲目使用 cv2.imread,因为 cv2 对某些多页 TIF 只读第一页,而且会把颜色通道变成 BGR 顺序。读取之后,我还会顺手把所有图的尺寸和 dtype 打印到一个文件里,确认 58 张图分辨率是否一致。如果发现某张图是 2000x2000,另一张是 4000x4000,就要在 patch 化之前统一步长,否则后面 batch 拼接会出问题。
3.2 染色归一化:小样本下最容易被跳过的一步
染色归一化在 H&E 病理图里几乎是标配。常见的做法叫 Macenko 染色分离,它把图像分解成染色浓度矩阵,再映射到一个标准染色空间。这种方法效果好,但需要额外依赖,而且参数调起来比较麻烦。对于初学者或者只想跑通流程的人来说,做逐通道标准化也可以起到很大作用。
def normalize_channel_wise(img, mean=None, std=None): img = img.astype(np.float32) / 255.0 # 输入是 (H, W, C) flat = img.reshape(-1, img.shape[-1]) if mean is None or std is None: mean = flat.mean(axis=0) std = flat.std(axis=0) + 1e-6 img = (img - mean) / std return img.astype(np.float32)这段代码按 RGB 三个通道分别做标准化,把每个通道拉到零均值单位方差。关键点在于训练集和验证集必须用同一组 mean/std。有些工程上的常见误用是每张图用自己的 mean/std 做归一化,这样确实把颜色都拉到了同一个范围,但会抹掉染色差异信息,而且训练和验证的统计口径不一致。正确做法是先在整个训练集上统计 mean/std,保存成 npy,推理时再用这组统计量。
如果你的项目里染色差异特别明显,还是建议上 Macenko 或 SPCN 这类染色归一化库。58 张小数据集里,模型很容易把颜色当捷径,染色归一化可以强迫模型去学形态结构。
3.3 把大图切成 patch:尺寸、重叠与边缘处理
病理切片整张图动辄几千乘几千像素,显存再大也很难直接训练。常见做法是滑窗切 patch,常用尺寸是 256x256 或 512x512。为了让细胞核不被切在边界上,patch 之间要有重叠,一般设置 64 到 128 像素。
import numpy as np def extract_patches(img, mask, patch_size=256, overlap=64): h, w = img.shape[:2] stride = patch_size - overlap patches_img = [] patches_mask = [] for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): p_img = img[y:y + patch_size, x:x + patch_size] p_mask = mask[y:y + patch_size, x:x + patch_size] patches_img.append(p_img) patches_mask.append(p_mask) return np.stack(patches_img), np.stack(patches_mask)代码逻辑是从左上角开始,以 stride 为步长向右向下滑窗,直到窗口无法完整覆盖为止。overlap 越大,同一区域被切的次数越多,样本数量越大,但样本重复度也越高。如果原图是 2000x2000,patch_size 256,overlap 64,相当于 stride 192,大约能切出 10x10 共 100 个 patch。如果 overlap 改为 128,stride 128,patch 数量变成 14x14 共 196 个。注意边缘处理:代码里没有 padding,所以右边和下边会漏掉一部分像素。我一般会先把图像用 reflect 模式 padding 到可被 stride 整除的尺寸,避免漏掉边缘组织。
3.4 mask 二值化与前景占比检查
切完 patch 后,很多 patch 可能是全背景,里面没有任何细胞区域。这类 patch 如果直接丢进训练,会让模型不停地把背景预测为背景,造成类别不平衡。正确做法是统计每个 patch 中前景像素占比,把全背景 patch 过滤掉。
def filter_background(images, masks, threshold=0.01): out_img = [] out_mask = [] for img, mask in zip(images, masks): foreground_ratio = (mask > 0).mean() if foreground_ratio >= threshold: out_img.append(img) out_mask.append(mask) return np.stack(out_img), np.stack(out_mask)threshold 一般取 0.01,也就是说一个 256x256 的 patch 中至少有大约 650 个像素属于前景,才进入训练。如果 threshold 设得过高,会把只有零星细胞核的弱阳性 patch 丢掉,导致模型对小细胞团不敏感。对于这个乳腺癌数据集,我建议先用 0.005 试一下,看剩余 patch 数量是否足够训练,再决定要不要提高。
4. 用 U-Net 做细胞分割:模型搭建、训练与评估
预处理做完后,模型选型就变成了核心问题。用 U-Net 不是因为它是最新最火的架构,而是它在小样本医学分割场景里足够稳。这一章从选型理由开始,给出可运行的网络结构和训练参数。
4.1 选型依据:U-Net 在病理小数据上的优势
U-Net 是编码器-解码器结构,编码器逐步下采样,解码器逐步恢复分辨率,skip connection 把编码器浅层细节传给解码器,因此小目标边界保留得比单纯空洞卷积类网络更好。在 58 张图这种规模下,模型参数量太大很容易过拟合,U-Net 从 32 通道起步的版本参数量适中,一个普通 GPU 就能训练。
对比之下,Mask R-CNN 做实例分割,能区分每一个独立细胞,但需要更精细的标注,而且训练更不稳定。DeepLab 系列擅长多尺度语义分割,但在病理图上感受野太大,反而容易丢失细胞核边缘。因此,第一轮实验用 U-Net 是性价比最高的方案。后续如果想把单个细胞分开,可以在 U-Net 输出上做分水岭后处理,不必一开始就上实例分割模型。
4.2 U-Net 网络结构搭建
这里给出一个三下采样、三上采样的简化 U-Net,用 PyTorch 实现,核心结构完整且容易修改。
import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch=3, out_ch=1, base=32): super().__init__() self.enc1 = DoubleConv(in_ch, base) self.pool1 = nn.MaxPool2d(2) self.enc2 = DoubleConv(base, base * 2) self.pool2 = nn.MaxPool2d(2) self.enc3 = DoubleConv(base * 2, base * 4) self.pool3 = nn.MaxPool2d(2) self.bridge = DoubleConv(base * 4, base * 8) self.up1 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2) self.dec1 = DoubleConv(base * 8, base * 4) self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2) self.dec2 = DoubleConv(base * 4, base * 2) self.up3 = nn.ConvTranspose2d(base * 2, base, 2, stride=2) self.dec3 = DoubleConv(base * 2, base) self.out = nn.Conv2d(base, out_ch, 1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool1(e1)) e3 = self.enc3(self.pool2(e2)) b = self.bridge(self.pool3(e3)) d1 = self.dec1(torch.cat([self.up1(b), e3], dim=1)) d2 = self.dec2(torch.cat([self.up2(d1), e2], dim=1)) d3 = self.dec3(torch.cat([self.up3(d2), e1], dim=1)) return self.out(d3)网络的核心是 DoubleConv,每个模块包含两个 3x3 卷积、BatchNorm 和 ReLU。编码器部分每经过一次池化,通道数翻倍,从 32 到 64 再到 128,bridge 部分是 256 通道。上采样用转置卷积把特征图尺寸翻倍,然后与对应编码器输出在通道维度做 concat。由于 concat 后的通道数是原来的两倍,所以 decoder 的第一个 DoubleConv 输入通道也要跟着翻倍,具体就是 base8 和 base4 的写法。
参数设定方面,in_ch=3 对应 RGB 输入;out_ch=1 是二分类输出,接 Sigmoid 变成概率图;base=32 是基础通道数,如果显存紧张改成 16。需要注意的是,如果把输入改成灰度单通道,in_ch 要改为 1,并且预处理也要改。
4.3 损失函数与训练参数:Dice 加权是关键
细胞分割标签里背景占比通常远高于前景,纯二元交叉熵会让模型偏向把所有像素预测为背景。常见解决方法是让 Dice Loss 和 BCE 联合使用。
import torch import torch.nn.functional as F def dice_loss(logits, targets): probs = torch.sigmoid(logits) smooth = 1.0 intersection = (probs * targets).sum() dice = (2.0 * intersection + smooth) / (probs.sum() + targets.sum() + smooth) return 1 - dice def bce_dice_loss(logits, targets): bce = F.binary_cross_entropy_with_logits(logits, targets) return bce + dice_loss(logits, targets)bce 负责逐像素的类别判断,dice 负责整体区域的形状约束。smooth 参数防止前景和背景均为空时出现除零。targets 需要用 float 类型的张量,值域 0 到 1,不能是 LongTensor。实际使用时,我会把 Dice Loss 的权重放到 1,BCE 权重也保留为 1,不额外调权重。如果遇到前景特别稀疏,可以尝试把 Dice Loss 权重提高到 2。
训练参数上,这套配置在小数据集上比较稳。
| 参数 | 值 | 说明 |
|---|---|---|
| 优化器 | Adam / AdamW | Adam 适应性学习率,适合医学分割 |
| 学习率 | 1e-4 | 过高容易震荡,过低收敛慢 |
| weight_decay | 1e-5 | 轻微正则,防止过拟合 |
| batch_size | 8 | 256x256 patch 时显存约 6-8G |
| epochs | 50 | 配合早停,不一定跑满 |
| 早停 patience | 10 | 验证集 Dice 连续 10 个 epoch 不升则停止 |
4.4 训练循环骨架:固定随机种子、保存最佳模型
小数据训练一定要固定随机种子,否则每次结果差异很大,很难判断改参数到底是变好还是随机波动。训练循环里还需要保存验证集 Dice 最高的模型,而不是最后一次 epoch 的模型。
import torch import numpy as np def set_seed(seed): np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) def train_one_epoch(model, loader, optimizer, device): model.train() total_loss = 0.0 for img, mask in loader: img = img.to(device) mask = mask.to(device) logits = model(img) loss = bce_dice_loss(logits, mask) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(loader)这里输入 mask 需要是 float 类型,网络输出 logits 后直接在损失函数里算 Sigmoid。注意如果标签是 0 和 1 之外的整数,要先转成 float。训练时 monitor 验证集 Dice,每个 epoch 后计算滑动平均,保存 best state_dict。显存不够时,可以把 batch_size 降到 4,相应学习率也降到 5e-5,否则梯度更新不稳定。
4.5 评估指标与可视化
分割模型不能只看准确率,因为即使全部预测为背景,准确率也可能很高。最常用的指标是 IoU 和 Dice,计算方式如下:
import numpy as np def compute_metrics(pred_mask, gt_mask): pred = (pred_mask > 0.5).astype(np.uint8) gt = (gt_mask > 0.5).astype(np.uint8) inter = np.logical_and(pred, gt).sum() union = np.logical_or(pred, gt).sum() iou = inter / (union + 1e-6) dice = (2 * inter) / (pred.sum() + gt.sum() + 1e-6) return iou, dicepred_mask 是模型输出的 0 到 1 概率图,阈值取 0.5。如果发现前景像素占比低导致 Dice 波动大,可以把阈值降低到 0.3 再看。58 张图规模下,我一般做 5 折交叉验证,每一折输出一个平均 IoU 和一个平均 Dice,同时保存几张可视化的测试图。指标不是唯一标准,肉眼检查边界是否贴着细胞核轮廓同样重要。如果模型把一片模糊细胞团全预测成前景,Dice 可能不低,但病理上不可用。
5. 避坑与常见问题:58 张小数据集上的四个血泪教训
小数据集训练像走钢丝,每一条踩进去都要花大半天排查。下面按现象到原因再到解决的结构写,都是我在类似病理数据集上真实遇到过的问题。
5.1 染色差异让模型学到“颜色分布”而不是细胞形态
现象:模型在训练集上 Dice 0.85,但换到一个没见过的病例,预测结果出现大片假阳性,尤其在切片边缘和空白区域。
原因:H&E 染色批次不同,背景颜色差异很大。模型抓住的背景规律其实是“这个病例染色偏蓝”,而不是“深色团块是细胞核”。如果训练集里某个病例的图像特别多,颜色过拟合会非常明显。
解决:先做染色归一化或至少逐通道标准化,并且保证训练集和验证集用同一组统计量。另外可以做一个消融实验:把输入改成灰度图,只保留结构信息,如果灰度输入的表现接近 RGB,说明模型确实在靠颜色作弊。从工程角度,我还会把所有病例的像素均值画出来,如果有病例明显偏离整体分布,优先检查它是否被单独落在某个 fold 里。
5.2 不按病例划分数据,验证指标虚高
现象:随机拆分做交叉验证时,平均 Dice 很高,但把模型部署到其他数据集上表现明显下降。
原因:同一个 ytma 编号下的不同切片在训练集和验证集之间发生了泄漏。模型在训练时已经见过这个病例的染色风格,验证指标自然好看,但真实场景遇到的是陌生病例。
解决:严格按病例分组划分数据。建议写一个按 case_id 进行 GroupShuffleSplit 的脚本,而不是用 random split。具体实现上,先收集所有 case_id,按病例编号分 fold,保证一个 case 的所有图像只出现在训练侧或验证侧。如果某个病例只有 1 张图,可以做留一病例验证,或者直接把它排除在训练集外,用其余数据训练,再用它做测试。这样评估出来的指标才有参考价值。
5.3 patch 尺寸选错,细胞被切碎或显存爆炸
现象:用 512x512 patch 训练时,显存经常溢出,验证时边缘细胞的预测断裂;换成 128x128 patch,显存没问题,但大块肿瘤区域预测出很多空洞。
原因:patch 尺寸和细胞核尺寸不匹配。如果 patch 只有细胞核直径的两三倍,一个完整的细胞核会被窗口切到几个 patch 里,每个 patch 中都只看到半个核,模型自然学不出完整边界。反过来,patch 太大导致 batch size 只能设小,训练不稳定。
解决:在预处理阶段先统计 mask 中细胞核的等效直径。用连通域分析找到每个前景区域,算出中位直径 d,patch size 取 4d 左右,overlap 取 64 或 d 附近的整数值。如果显存不足,优先减小 U-Net 的 base 通道数,而不是压缩 patch。因为通道数减少只是牺牲模型容量,patch 太小会直接破坏目标结构。
5.4 数据增强没同步,图像和 mask 错位
现象:训练早期 loss 不降,甚至出现 NaN。检查训练图时,发现细胞核在图像里是正着的,mask 里的对应区域却是翻转的。
原因:对图像和 mask 分别用了不同的随机种子做随机翻转或旋转。比如图像随机水平翻转时,mask 没有做相同的变换,导致两者完全错位。很多新手写增强代码时,习惯对 img 调用一次 random,再对 mask 调用一次 random,这样两边的随机状态不一样。
解决:所有随机几何变换必须同时作用于图像和 mask。用 PyTorch 写自定义 Dataset 时,可以先取一个随机数决定是否翻转,然后对 img 和 mask 调用同一个 flip 函数;如果使用 albumentations,直接在一个 transform 里同时传 image 和 mask。另外,H&E 图像不建议做颜色抖动或随机亮度调整,因为这会和染色归一化的目标冲突。想增强数据多样性,应该用弹性形变或轻微旋转。
还有一条容易忽略的:mask 在增强后要保持二值状态。如果用某些插值方式放大或旋转 mask,会产生 0.4 这样的中间值,计算 Dice 时被当成模糊标签,导致指标偏低。处理 mask 时一律使用最近邻插值。
6. 把 58 张小数据用到极致:伪标签迭代与级联验证
小数据集训练 U-Net 只是第一步,真正让模型效果再上一个台阶的,是伪标签迭代和级联验证。
第一轮训练完成后,用训练好的模型对没有标注或置信度不高的测试图进行预测,得到概率图。把概率大于 0.9 且周围区域稳定的像素作为伪 mask,人工挑几张边界清晰的 patch 校正后混入训练集,做第二轮微调。这里有个前提:伪标签只能加入训练集,绝不能混入验证集。如果验证集里混入了带伪标签的数据,评估结果就不再可靠。
更实用的是级联分类。既然文件名已经带了 benign 和 malignant 的标签,可以用分割结果反推良恶性分类。具体做法是,用 U-Net 输出的 mask 计算形态学特征,比如细胞核总面积、平均核周长、核密度、核质比,把这些特征输入随机森林或单层 MLP,做二分类。如果分割质量差,这些特征会乱成一团,分类准确率也上不去。这相当于用分类任务反过来监督分割模型的质量。
我自己的验证习惯是固定一个随机种子,按病例做 5 折交叉验证,每一折都保存三样东西:一是这一折的 IoU 和 Dice,二是两张典型测试图的可视化结果,三是整图的预测概率图。概率图不仅用来调阈值,也会留作伪标签迭代的输入。
| 保存内容 | 用途 |
|---|---|
| 每折 IoU / Dice | 判断模型稳定性,是否受某个病例影响 |
| 2-3 张可视化切片 | 确认边界走向是否符合细胞核形态 |
| 预测概率图 | 伪标签迭代、级联分类特征提取 |
从那以后,我每碰一个小样本病理数据集,都会强制走一遍染色归一化、按病例划分、同源增强、交叉验证的流程。指标虚高时第一反应不是调模型,而是回头查数据切分和标签对齐。这些东西比换一个更复杂的网络更能决定项目能不能落地。希望帮到你。
本文还有配套的精品资源,点击获取