
简介这份乳腺癌细胞分割图片数据集面向医学图像处理、病理AI与深度学习方向的学习者和研究者用于解决HE染色组织病理图像中细胞分割及良性、恶性细胞分类的典型难题。数据集包含58张真实HE染色组织病理学图像图像经苏木素和伊红染色以凸显原本近乎透明的细胞结构配套真实标注数据适合开展分割模型训练、分类算法验证与医学图像预处理练习。压缩包共232个文件由116个tif图像与116个xml标注文件组成tif用于承载原始病理切片图像xml则记录对应的细胞标注信息整体约93.7MB结构规整便于直接读取与建模。目前已有270人学习关注可作为乳腺癌细胞分割与良恶性分类任务的入门到进阶实践素材帮助读者快速搭建训练与评估流程理解病理图像中细胞边界提取和类别判别的关键环节。1. 58 张 HE 病理图能干什么先看清这份乳腺癌细胞分割数据集的底子如果你正在找一份能直接跑通「细胞分割 → 良恶性分类」全流程的小体量病理数据集这份 58 张 HE 染色组织病理学图像大概率能省掉你不少前期找数据的时间。它解决的不是「数据量够不够训大模型」的问题而是「我能不能先在一个干净、带标注、格式统一的小数据集上把 pipeline 跑通」的问题。每张图都是 CCD 相机拍的组织切片文件名里直接编码了样本来源、日期和良恶性标签比如ytma49_042203_benign2_ccd.tif和ytma12_010804_malignant2_ccd.tif这种命名方式对批量处理和标签提取非常友好。适合谁做医学图像分割入门的研究生、需要快速验证 U-Net 类模型效果的算法工程师以及想理解 HE 染色图像特点的深度学习从业者。不适合指望靠它刷 SOTA 的人——58 张图连一个像样的训练集都撑不起来但用来做数据增强策略对比、损失函数消融或者标注质量检查它足够真实。2. 从文件名到标签张量把 TIFF 图喂进分割网络前的四步预处理2.1 为什么 HE 图像不能直接 resize 就完事HE 染色的核心是苏木素把细胞核染成蓝紫色伊红把细胞质和细胞间质染成粉红色。这个染色过程本身就有批次差异同一张切片不同区域的颜色深浅都可能不一样。如果你直接把 RGB 三通道丢进网络模型很可能先学会「蓝色多就是恶性」这种伪特征而不是真正的细胞形态差异。常见做法是在训练前做颜色归一化把每张图的染色分布对齐到一个参考模板上。我一般用 Reinhard 方法做 lab 空间的均值方差匹配或者用 Macenko 方法做 SVD 分解后重新映射。这一步不做后面分割出来的 mask 边界会非常毛糙良恶性分类的准确率也上不去。另一个坑是 TIFF 格式的位深。病理图常见 8 位和 16 位两种16 位图的像素值范围是 0-65535直接转成 8 位会丢失暗部细节。用tifffile读的时候先看一眼dtype如果是uint16要么线性拉伸到 0-255要么在归一化时除以 65535。别用 PIL 默认读PIL 对 16 位 TIFF 的支持一直不太稳。2.2 用 Python 批量读取并解析文件名标签文件名里的benign和malignant就是现成的弱标签benign2、malignant3后面的数字大概率是样本编号或区域编号。下面这段脚本把目录下所有 TIFF 读进来同时把标签和样本 ID 抽出来存成 DataFrame方便后面按样本划分训练集和验证集。import os import re import tifffile import numpy as np import pandas as pd def parse_filename(fname): # 匹配 ytma49_042203_benign2_ccd.tif 这种格式 pattern r(ytma\d)_(\d)_(benign|malignant)(\d)_ccd\.tif m re.match(pattern, fname) if m: return { sample_id: m.group(1), date: m.group(2), label: 0 if m.group(3) benign else 1, region: int(m.group(4)), filename: fname } return None data_dir ./breast_cancer_dataset records [] for f in os.listdir(data_dir): if f.endswith(.tif): info parse_filename(f) if info: img tifffile.imread(os.path.join(data_dir, f)) info[shape] img.shape info[dtype] str(img.dtype) records.append(info) df pd.DataFrame(records) print(df[[filename, label, shape, dtype]].head()) print(f总样本数: {len(df)}, 良性: {sum(df.label0)}, 恶性: {sum(df.label1)})这段代码的关键在正则的捕获组设计。ytma\d匹配样本来源编号\d匹配日期(benign|malignant)直接映射成 0/1 标签。跑完先看shape和dtype两列如果 shape 不一致说明图像尺寸有差异后面要么统一 pad 到最大尺寸要么在 DataLoader 里做动态裁剪。dtype 如果是 uint16记得在归一化时用 65535 做分母。2.3 按样本划分训练验证集别按图像随机分58 张图如果随机按 8:2 分很可能同一个样本的不同区域同时出现在训练集和验证集里。比如ytma49这个样本有 benign1、benign2、benign3 多张图随机分的话验证集里的 benign2 和训练集里的 benign1 来自同一个组织块染色特征高度相似验证指标会虚高。正确做法是按sample_id分组划分用GroupShuffleSplit或者手动按样本 ID 切分。我一般留出 2-3 个完整样本做验证剩下的做训练。58 张图本身就不多验证集控制在 10-15 张比较合适。from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsdf[sample_id])) train_df df.iloc[train_idx] val_df df.iloc[val_idx] print(f训练集样本来源: {train_df.sample_id.unique()}) print(f验证集样本来源: {val_df.sample_id.unique()})跑完检查一下两个集合的sample_id有没有交集有交集就说明划分逻辑写错了。这个细节在论文里经常被审稿人揪出来提前做好能省很多返工时间。2.4 数据增强旋转和翻转够用颜色抖动要克制病理图像的方向没有语义意义旋转 90 度、180 度、270 度和水平垂直翻转都是安全的增强手段。但颜色抖动要小心HE 的染色差异是真实存在的生物学变异过度抖动会让模型把染色差异当成噪声忽略掉反而降低泛化能力。我一般只做轻微的亮度对比度扰动幅度控制在 ±10% 以内。另外弹性形变对细胞分割有帮助但参数要调小形变太强会把细胞核拉成奇怪形状标注 mask 对不上。import albumentations as A train_transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.ElasticTransform(alpha1, sigma50, alpha_affine50, p0.2) ])ElasticTransform的alpha控制形变强度sigma控制平滑程度alpha_affine控制仿射变换幅度。这三个参数在细胞分割任务里建议从alpha1, sigma50起步跑几个 epoch 看 mask 边界有没有明显扭曲再微调。3. U-Net 还是 DeepLabV3小数据集上的分割模型选型与训练参数3.1 为什么 58 张图反而更适合 U-Net 类结构DeepLabV3 和 Mask R-CNN 这些模型参数量大在 58 张图上训很容易过拟合。U-Net 的编码器-解码器结构加跳跃连接能在小数据集上更快收敛而且对边界细节的保留更好。如果你非要用预训练权重ImageNet 上训的 ResNet 编码器可以拿来初始化但要注意病理图和自然图像的域差异很大冻结编码器前几层、只微调后面几层效果更稳。我试过在 58 张图上从头训一个轻量 U-Net用 Dice Loss 加 BCE 的组合损失30 个 epoch 左右验证集 Dice 能到 0.75 上下再往上就过拟合了。3.2 损失函数选 Dice BCE别只用交叉熵细胞分割的本质是像素级二分类但前景细胞和背景的面积比可能到 1:10 甚至更悬殊。只用 BCE 的话模型会倾向于全预测背景准确率看着高但 Dice 很低。Dice Loss 直接优化重叠区域对类别不平衡更鲁棒。组合损失里 BCE 负责稳定梯度Dice 负责拉高重叠度权重一般设 0.5:0.5 或者 0.3:0.7。下面是一个可以直接用的损失函数实现。import torch import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, weight_bce0.5, weight_dice0.5): super().__init__() self.weight_bce weight_bce self.weight_dice weight_dice self.bce nn.BCEWithLogitsLoss() def forward(self, pred, target): # pred: (B, 1, H, W) logits, target: (B, 1, H, W) 0/1 bce_loss self.bce(pred, target) pred_sigmoid torch.sigmoid(pred) intersection (pred_sigmoid * target).sum(dim(2, 3)) union pred_sigmoid.sum(dim(2, 3)) target.sum(dim(2, 3)) dice_loss 1 - (2 * intersection 1e-6) / (union 1e-6) dice_loss dice_loss.mean() return self.weight_bce * bce_loss self.weight_dice * dice_lossBCEWithLogitsLoss内部做了 sigmoid 和 BCE 的数值稳定合并比先 sigmoid 再 BCE 更安全。Dice 计算里加1e-6是防止分母为零intersection和union在 H、W 维度求和后得到每个样本的 Dice再取平均。如果显存够可以把dim(2,3)改成dim(1,2,3)做全局 Dice但小批量下全局 Dice 方差大逐样本平均更稳。3.3 学习率用余弦退火初始值别超过 1e-3小数据集上 Adam 的默认学习率 1e-3 有时候会震荡我一般从 3e-4 起步配合 CosineAnnealingLR 让学习率在训练后期降到 1e-6 附近。Batch size 受显存限制的话用 4 或 8 都行但要注意 BatchNorm 在 batch size 小于 8 时统计量不准可以换成 GroupNorm 或者 InstanceNorm。下面是一个训练循环的骨架重点看学习率调度和验证指标的计算。from torch.optim.lr_scheduler import CosineAnnealingLR model UNet(in_channels3, out_channels1).cuda() optimizer torch.optim.Adam(model.parameters(), lr3e-4) scheduler CosineAnnealingLR(optimizer, T_max50, eta_min1e-6) criterion DiceBCELoss(weight_bce0.5, weight_dice0.5) for epoch in range(50): model.train() for imgs, masks in train_loader: imgs, masks imgs.cuda(), masks.cuda() preds model(imgs) loss criterion(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 验证阶段算 Dice model.eval() dice_scores [] with torch.no_grad(): for imgs, masks in val_loader: imgs, masks imgs.cuda(), masks.cuda() preds torch.sigmoid(model(imgs)) preds_bin (preds 0.5).float() inter (preds_bin * masks).sum() union preds_bin.sum() masks.sum() dice_scores.append((2 * inter / (union 1e-6)).item()) print(fEpoch {epoch}, Val Dice: {np.mean(dice_scores):.4f})T_max50表示 50 个 epoch 完成一个余弦周期eta_min是最小学习率。验证时先 sigmoid 再阈值 0.5 二值化Dice 计算用整个验证集的累加 intersection 和 union比逐样本平均更能反映全局分割质量。如果验证 Dice 在 20 个 epoch 后还在涨但训练 Dice 已经接近 1说明过拟合了早点停。3.4 从分割 mask 到良恶性分类两步走还是端到端这份数据集的最终目标是良性/恶性分类分割只是中间步骤。两条路线一是先训分割网络得到细胞 mask再在 mask 区域上提取形态学特征面积、周长、圆形度、核质比喂给 SVM 或随机森林做分类二是直接训一个多任务网络共享编码器一个头输出分割 mask一个头输出分类 logits。58 张图的情况下两步走更稳因为分类器可以用分割结果做特征工程解释性也强。端到端多任务学习需要更多数据才能让两个任务互相促进而不是互相拖累。我一般先用 U-Net 把 mask 训到 Dice 0.7 以上再把 mask 叠加到原图上裁出细胞区域提取 10-15 个形态特征用 5 折交叉验证训一个梯度提升树分类准确率通常比直接端到端高 5-10 个百分点。4. 避坑与排查58 张图训分割模型时最容易翻车的五个地方4.1 验证集 Dice 很高但测试时一塌糊涂现象训练时验证集 Dice 到 0.85换一批新图跑推理mask 全是噪点或者全黑。原因验证集和训练集来自同一个样本染色分布和细胞密度几乎一样模型只是记住了这个样本的纹理。解决按sample_id分组划分确保验证集的样本来源在训练集里完全没出现过。如果样本数太少没法分组至少做 5 折交叉验证每折留一个样本做验证。4.2 损失降到很低但 mask 边界像锯齿现象训练 loss 收敛到 0.05 以下但预测的细胞边界呈阶梯状不光滑。原因下采样过程中空间信息丢失U-Net 的跳跃连接虽然能补回来一些但如果输入图像分辨率本身很低比如 256x256细胞核只有几个像素宽边界自然粗糙。解决把输入尺寸提到 512x512 或 1024x1024或者在解码器最后加一个亚像素卷积层做上采样。另外检查一下标注 mask 本身是不是就有锯齿如果标注质量差模型学出来的边界也好不了。4.3 显存不够batch size 只能设 1现象512x512 的图 batch size 设 2 就 OOM设 1 又导致 BatchNorm 统计量不准训练震荡。原因U-Net 在浅层特征图通道数多显存占用大。解决把 BatchNorm 换成 GroupNormGroupNorm 对 batch size 不敏感。或者用梯度累积每 4 个 batch 更新一次参数等效 batch size 等于 4。再不行就用混合精度训练torch.cuda.amp能省 30%-40% 显存。4.4 颜色归一化后图像发灰细胞核看不清现象做了 Reinhard 颜色归一化后原本蓝紫色的细胞核变成灰蒙蒙一片对比度下降。原因参考模板选得不好或者 lab 空间的均值方差匹配过度校正了。解决换一个染色均匀、细胞核清晰的图做参考模板或者改用 Macenko 方法它对 HE 的特定染色通道做分解保留的生物学信息更多。归一化后记得目视检查几张图别只看指标。4.5 推理时忘记把 logits 转成概率现象模型输出一堆负数和小数直接当 mask 用全是黑的。原因训练时用了BCEWithLogitsLoss模型输出的是 logits推理时忘了加 sigmoid。解决推理代码里显式加torch.sigmoid(preds)再阈值化。这个坑很低级但经常发生尤其是从训练脚本复制到推理脚本时容易漏掉。5. 把 58 张图用到极致交叉验证、伪标签与形态学后处理的组合拳58 张图确实少但少不等于不能用。我的习惯是先用 5 折交叉验证把 U-Net 训出 5 个模型每折留一个样本做验证这样每个样本都有一次当验证集的机会。5 个模型的预测结果做平均mask 的鲁棒性比单模型高不少。然后拿这 5 个模型去预测所有训练图把预测概率高于 0.9 的像素当伪标签低于 0.1 的当背景中间不确定的区域忽略掉扩充出一批带噪声的标注再微调一轮。这一步能把有效训练像素数翻两三倍Dice 通常能再涨 3-5 个点。形态学后处理也别忽略。分割出来的 mask 经常有孤立的小噪点或者细胞核内部空洞用cv2.morphologyEx做开运算去掉小噪点再做闭运算填内部空洞。核的大小根据细胞核的实际像素直径来定一般 3x3 或 5x5 的椭圆核就够。下面这段后处理代码可以直接接在推理后面。import cv2 import numpy as np def postprocess_mask(mask, kernel_size3): # mask: 0/1 二值图uint8 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) # 开运算去噪点 opened cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 闭运算填空洞 closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel) return closed # 对每个预测 mask 做后处理 pred_mask (torch.sigmoid(model(img)) 0.5).cpu().numpy().astype(np.uint8) clean_mask postprocess_mask(pred_mask[0, 0], kernel_size3)MORPH_OPEN先腐蚀后膨胀去掉比核小的亮斑MORPH_CLOSE先膨胀后腐蚀填掉比核小的暗洞。kernel_size别设太大3 或 5 就够了再大会把相邻细胞核粘在一起。后处理完再算 Dice通常能比原始输出高 1-2 个点尤其是边界区域。还有一个技巧是把分割和分类的标签一致性检查加进去。如果一张图文件名标的是malignant但分割出来的细胞核形态特征比如核质比、圆形度跟良性细胞更接近就把这张图挑出来人工复核。58 张图里我遇到过两三张标签存疑的复核后发现确实是标注错误。这种一致性检查在数据量小的时候特别值得做能避免模型学到错误信号。从那以后我每次拿到小样本病理数据集都强制先跑一遍文件名解析和标签一致性检查再开始训模型。这个习惯帮我省掉了至少两次因为标签错误导致的整轮返工。希望帮到你。本文还有配套的精品资源点击获取