☰
乳腺癌图像分类实战:从数据集选型到迁移学习的深度学习指南
2026/10/5 7:15:30 网站建设 项目流程

简介:面向深度学习和医学图像分类初学者,这份乳腺癌症图像分类数据集可作为图像二分类任务的练手与教学素材。数据按目录整理,同类别样本归于同一文件夹,整体划分为训练集约480张、验证集约140张、测试集约70张,并附有JSON类别文件和Python脚本,便于读取标签与构建数据管道。压缩包共692个文件,以JPG图像为主体,辅以PNG示意图、py脚本和JSON配置,大小约17.85MB,轻量易下载,适合在普通算力环境下快速跑通CNN、ResNet等分类模型。目前已有286人学习下载,适合入门者做实验、课堂演示或毕业设计预研,尤其便于从数据预处理、模型训练到结果评估形成完整流程。

1. 乳腺癌症图像分类数据集:先搞清楚你在跟什么样的小众视觉任务打交道

深度学习数据集里的乳腺癌症图像分类任务,这几年被越来越多算法团队当作医学影像落地的第一站。标题里写的“乳腺癌症”就是乳腺癌,而这项任务的核心矛盾很反直觉:真正决定模型好坏的往往不是网络结构,而是数据集怎么切、标签怎么定义、评价指标怎么选。钼靶、超声、核磁或者病理切片上,真正有病灶的区域可能只占一张图的百分之几甚至千分之几,标签也常常不是“整图有癌”,而是“这个可疑区域是良性还是恶性”。这篇文章写给两类人:一是想拿真实小数据任务练手的深度学习工程师,二是医疗影像产品落地前要把数据、模型和评估体系理顺的算法同学。后面按“选数据集 → 预处理 → 训练 → 排查 → 验证”展开,每一步都给出具体命令和可调参数。

2. 公开数据集选型:CBIS-DDSM、INbreast、BUSI 怎么挑,以及类不平衡问题

2.1 乳腺影像的四种模态:同一“乳腺癌”标签,视觉差异巨大

做乳腺图像分类,第一步不是找模型,而是确定你的原始图像是什么模态。钼靶是 X 光把乳房压扁后拍出来的,视野大、分辨率高,病灶通常是白色小团块或细条状钙化,背景里还有大量腺体纹理;超声是动态扫查里截出的单帧,噪声明显,病灶边界相对清晰,但设备、探头频率对图像影响很大;磁共振是序列扫描,一张病例可能带几十上百个层面,标签往往按病例而不是按层给;病理切片是染色后的细胞图像,目标不再是“找肿块”,而是“看细胞核密度和结构”。

这个模态差异直接决定你该不该做颜色标准化、该不该用灰度图、该不该把训练限制在某个 ROI 内。很多公开数据集来自不同国家地区的医院,扫描仪品牌和曝光条件各不相同,亮度、对比度分布差很多。哪怕都叫“乳腺图像分类”,你在一个数据集上学到的亮度规律换到另一台设备上可能完全失效。我一般会在选定数据集后单独做一个“按设备/按拍摄中心分组”的探索性分析,而不是直接全量随机划分训练集和验证集。

2.2 五个常见公开数据集对比

做乳腺图像分类,绕不开的公开数据集主要是以下几类:

数据集模态常见规模标签粒度主要用途
CBIS-DDSM钼靶1000+ 病例,ROI 级别病灶级 ROI、良性/恶性、钙化/肿块二分类、检测、ROI 分类
INbreast钼靶115 例 / 约 400 张病例级 + 病灶标注检测、分类、分割
MIAS钼靶(胶片扫描)322 张图像级 + 病灶位置快速原型、入门
BACH病理切片400 张图像级四分类病理图像分类
BUSI超声约 780 张图像级、正常/良性/恶性超声二分类/三分类

选型时我一般按这个顺序问自己:要解决的是“整图有没有病”还是“这个 ROI 是良性还是恶性”。前者可以选 INbreast、MIAS 这类带图像级标签的数据;后者建议直接看 CBIS-DDSM,因为它把 DDSM 里的病灶裁剪出来,提供 ROI 掩码和良恶性标注,能省掉大量在全图上找病灶的预处理。BACH 是病理模态,如果不是做病理,不要因为它“图多”就选,染色差异和临床场景都不一样。

CBIS-DDSM 是很多钼靶分类论文的基准,但有个容易踩的细节:它本身是 DDSM 的重整理版本,原始扫描是 16bit 灰度 DICOM,包含“钙化簇”和“肿块”两类病灶,统计口径和病例数经常被论文引用得不一样。复现时一定要确认别人用的是哪个任务定义,是 ROI 分类还是全片分类,有没有排除“正常”类别。我见过不少同学拿 CBIS-DDSM 的 ROI 分类结果去跟全片分类结果比精度,然后怀疑自己代码有 bug,其实只是任务定义没对齐。

BUSI 在超声赛道很常用,但它存在一个不太被提及的问题:病例数少、类别比例不均衡,恶性样本占比较少,且不同论文对同一张图的预处理方式差异很大。如果你拿 BUSI 做“正常/良性/恶性”三分类,要留意正常样本数量最多,直接看 accuracy 会得到一个虚高的值。后面讲损失函数时会专门说怎么用加权方式处理这个不平衡。

2.3 ROI 与全图标签:选错标签粒度,模型学到的就是“亮度”

乳腺图像分类最隐蔽的坑是标签粒度选错。拿全图标签直接训 CNN,模型很容易学到全局背景特征,比如“这台扫描仪的图画偏亮,所以是恶性”。这在实际部署里就是灾难,换一台机器立刻崩。更稳妥的做法是优先使用带 ROI 或病灶框的数据集,先切到病灶区域再分类;如果没有 ROI,就用分割模型或医生画的框先自动裁一遍,宁可多裁,再用分类模型过滤。

另外,公开数据集的标签里经常出现“良性但可疑”这类过渡类别。很多论文会把它直接归入良性,或者直接丢弃,两种做法都合法,但必须写进实验记录。否则复现时你会发现别人源码里设了两类,自己怎么跑都是三类问题。还要确认数据集有没有区分“钙化”和“肿块”,这两种病灶形态和病理机制完全不同,混在一起训练会让模型学成“大而白的是肿块,小而白的是钙化”,这不是你真正要的分类能力。

类不平衡是乳腺数据绕不开的问题:恶性样本通常比良性少,正常样本又比前两者多。我处理不平衡的第一原则不是急着上采样或下采样,而是先把评估指标定下来。这个任务里灵敏度(召回)、特异性、ROC-AUC 比 accuracy 有意义得多。如果测试集里的恶性样本只有几十张,别只看 top-1 准确率,一定要打开混淆矩阵看每类到底分成了什么样。

原始数据格式也差别很大。CBIS-DDSM 和 INbreast 大多是 DICOM,MIAS 是老胶片扫描的 PGM 类格式,BACH 发布的是 TIFF 病理大图,BUSI 常见 PNG。预处理第一步就该把 DICOM 的 16bit 值域搞清楚,在转成普通 8bit 图之前,先存一份中间产物,比如 HDF5 或 NumPy 数组。所有后续实验都从同一份预处理结果出发,能省掉很多“我预处理和你不一样”的扯皮。

3. 数据预处理与增强:乳腺图像分类里的前处理链路与参数

3.1 加载 DICOM 与归一化:先解决“图是 16 bit”的问题

钼靶 DICOM 通常不是 8bit 图像,像素值范围可能在 0-4096 甚至更宽。直接用图像库打开再存成 8bit,等于把大量低对比度信息直接压没。常见做法是用 pydicom 读出像素数组再做百分位裁剪。我一般用 2% 到 98% 分位作为上下界,把两端离群像素截掉,再线性映射到 [0,1]。如果图里有定位贴片、过曝区域或者扫描仪自带的字符,这几个高亮点会把 min-max 归一化带偏,整张图对比度会变得很奇怪。

import numpy as np import pydicom def dicom_to_float(path: str, lo: float = 2.0, hi: float = 98.0) -> np.ndarray: dcm = pydicom.dcmread(path) img = dcm.pixel_array.astype(np.float32) if dcm.RescaleIntercept is not None and dcm.RescaleSlope is not None: img = img * float(dcm.RescaleSlope) + float(dcm.RescaleIntercept) low, high = np.percentile(img, [lo, hi]) img = np.clip(img, low, high) img = (img - low) / (high - low + 1e-8) return img

这里先把 RescaleSlope 和 RescaleIntercept 应用上,再做百分位截断。pydicom 读出来的 pixel_array 有时已经应用过 rescale,但不同设备未必一致,所以最好先检查元数据里有没有这两个字段。1e-8 是为了避免低和高相等时除零。输出保持为 float32 的 [0,1] 数组,先不要乘 255 存成整数,因为后面要做数据增强和归一化,精度保留得越多越好。

3.2 ROI 裁剪与背景去除:把分类问题先变成“只看病灶区”

全片图直接缩放到 224x224,会把病灶细节压得非常小,而且背景占绝对主导。如果数据集自带了掩码,就按掩码裁剪;如果没有掩码,常见做法是先做一个简单阈值粗分割,乳腺影像里乳房区域的灰度通常高于纯黑背景,把最大连通域之外的部分清掉。技巧是不要只裁最小外接矩形,要留一点余量,让分类器能看到病灶周围的组织纹理,因为乳腺影像诊断本身就依赖“病灶与周围腺体的对比”。

def crop_from_mask(img: np.ndarray, mask: np.ndarray, pad: int = 20, min_side: int = 64): ys, xs = np.where(mask > 0) if len(ys) < min_side: return img y0, y1 = max(ys.min() - pad, 0), min(ys.max() + pad, img.shape[0]) x0, x1 = max(xs.min() - pad, 0), min(xs.max() + pad, img.shape[1]) return img[y0:y1, x0:x1]

边界裁剪时要注意坐标不能变负数,所以用 max、min 夹住。pad 我一般设置在 16 到 32 像素之间;太小会让增强旋转后病灶出画,太大又会重新引入背景。min_side 用来过滤标注噪声:如果某个掩码只有几十个像素,这种 ROI 大概率是标错或极小伪影,强行裁给模型只会让模型学噪声。

如果做的是全片分类,我建议把左右翻转增强先关掉。乳腺结构有解剖侧向性,左乳和右乳的组织纹理并不是完全镜像对称的。公开数据集实验里可以开,但到真实产品里,左右翻转会给模型解释引入非常别扭的结果。

3.3 数据增强:小数据集最值得先调的旋钮

乳腺数据集通常只有几百到几千张,直接上深层 CNN 几乎必过拟合,所以增强不是“锦上添花”,而是训练管线里的主功能模块。我常用 Albumentations,因为它把裁剪、旋转、亮度抖动、归一化拼在一起,还方便后续对同一个样本做多次采样。要注意乳腺影像的增强策略跟 ImageNet 不一样:不建议开水平翻转,不建议用会把腺体纹理抹掉的强模糊,也不太建议直接用 AutoAugment 那种带随机擦除的增强策略。更稳妥的组合是随机小角度旋转、随机缩放裁剪、轻微亮度对比度扰动。

import albumentations as A train_transform = A.Compose([ A.RandomResizedCrop(size=(224, 224), scale=(0.75, 1.0), ratio=(0.9, 1.1)), A.Rotate(limit=15, border_mode=0, value=0), A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.15, p=0.3), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) valid_transform = A.Compose([ A.Resize(224, 224), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

RandomResizedCrop 的 scale 下限不要设太低,0.75 以上我比较放心,因为病灶占比本来就小,裁得太狠等于把病灶裁没了。Rotate 限制在 15 度左右,钼靶影像中病灶形态与拍摄方向有关,过度旋转会制造出临床上不存在的形态。border_mode=0 并设置 value=0 做黑色填充,而不是反射填充,原因是医学影像里反射填充会伪造组织纹理,模型可能学到以画幅边缘判断类别,这是容易翻车的地方。Normalize 先用 ImageNet 的 mean 和 std,因为后续要用 ImageNet 预训练权重,输入分布保持一致。

做完这套预处理,建议把中间产物统一保存成 HDF5 或 npy 文件,同时记录每个样本的原始来源、裁剪坐标和是否翻转。这个“后悔药”很重要:后面任何人改完模型,都不需要重新做几十 GB 的影像 IO,而且还能追溯到底哪个样本被裁坏了。

4. 用 ResNet/EfficientNet 训练乳腺图像分类模型:最小训练流程与关键参数

4.1 迁移学习:为什么选 ImageNet 预训练而非随机初始化

乳腺图像分类数据集通常只有几百到几千张,随机初始化训练深层 CNN,大多数情况下只会得到“loss 缓慢下降但泛化很差”的模型。常见做法是用 ImageNet 预训练权重做迁移学习,网络底层提取的是边缘、纹理这类通用特征,跟医学图像并不冲突。真正的争议在于该全量微调,还是冻结前几层只训后半段。我一般分两步:先整网用小学习率微调,如果发现前几层梯度更新过大而后层更新过慢,再把前几层冻结掉,避免破坏底层特征。

在模型结构上,ResNet50 是保守选择,计算成本低、复现点多;EfficientNet-B0 或 B1 在同样精度下参数更少,适合快速原型。这几年也有一些适合小数据的混合模型,但从工程角度看,先跑通 ResNet 再换模型是最高效的路径。不要太早依赖论文里“最新图像分类算法”的复现结果,那个前提往往是几万张图的训练集,而你现在可能只有几百张。

4.2 损失函数和类别权重:不平衡数据上的三选一

对于正常、良性、恶性三分类,直接交叉熵会在正常样本占比高时让模型偏向多数类。常用替换方案有三个:给交叉熵加类别权重、用 Focal Loss、用加权采样。我对小数据集的第一选择是“类别权重 + 加权采样”搭配使用。权重按 1 除以类别样本数计算再归一化;采样时给少数类多复制几次,但不要无脑复制到完全均匀,因为恶性样本总数少,过分重复会让验证集和训练集出现隐式重叠。

实际训练时,我会先拿 5 折验证比较加权交叉熵和 Focal Loss。Focal Loss 的 gamma 从 2 开始调,alpha 按类别比例设置。但在几百张训练集上,gamma 太大比如 5 会让难样本主导训练,反而把 loss 拖得很高。很多论文推荐 gamma=2,但在小数据上我见过 gamma=1 结果更稳定。这条属于比较反直觉的经验,需要用验证集 AUC 来判断,不要凭训练 loss 决定。

4.3 训练参数的落地:lr、batch size、checkpoint

迁移学习里学习率是最难一次调对的参数。我的起步值是 1e-4(AdamW)或 1e-3(SGD with momentum,配合余弦衰减)。用 ImageNet 预训练权重做全量微调时,一开始就用 1e-3 的 AdamW,很容易让底层 BN 统计量出问题,loss 变成一个黑匣子。所以我会先用 batch size 16 或 32 跑 10 个 epoch 的观察阶段,确认 train loss 在合理下降,再决定放大学习率还是增加总轮数。

import torch import torch.nn as nn from torchvision import models model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) model.fc = nn.Linear(model.fc.in_features, 3) criterion = nn.CrossEntropyLoss( weight=torch.tensor([0.3, 0.4, 1.2]) # 正常/良性/恶性的反频率权重 ) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2) for epoch in range(30): model.train() for images, labels in train_loader: images, labels = images.to(device), labels.to(device) preds = model(images) loss = criterion(preds, labels) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step()

CrossEntropyLoss 的 weight 决定了对少数类的补偿力度。注意不要直接把“恶性比正常等于 1 比 4”按原值填进去,那样权重过大,模型会把一切判成恶性。先按样本数反比算好再整体归一化。clip_grad_norm 在医学小数据上很有用,预训练模型微调的第一个 batch 梯度常会出现异常大值,卡一下能避免 loss 直接变成 NaN。

这里还有个常见单位陷阱,常有人问“深度学习里的 parameter 应该不是 MB 吧”。ResNet50 的参数量约 2560 万,PyTorch 保存 fp32 的 checkpoint 约 100MB。如果你训练时显存爆了,先调 batch size,而不是急着换小模型。训练日志我会记录四个数:train loss、验证 accuracy、验证 AUC、混淆矩阵。不要只盯 accuracy,这个任务里恶性样本的召回比整体准确率更有价值。每 5 个 epoch 存一次 checkpoint,同时保存优化器状态,避免训练中断后要从头再来。

5. 常见问题与排查:乳腺图像分类的五个坑,现象、原因和解决

5.1 训练 Loss 不降,准确率卡在水平线附近

现象:第一个 epoch 之后 train loss 几乎不动,准确率一直停留在随机水平,换 ResNet、EfficientNet 都一样。

原因:最常见的是非 ROI 样本太多,模型收到一堆全黑或纯背景图;另一个高频原因是标签和文件名错位,比如数据加载时按文件名排序,但 CSV 里的标签没有对齐,模型在学一张“文件名到随机标签”的映射。

解决:先做数据加载自检,取一个 batch,把 images 和 labels 一起打印出来,确认图片内容与标签真实对应。然后统计每张样本的有效像素比例,把全黑、全白样本单独过滤或放进困难样本集合。我自己的习惯是先训练 3 个 epoch 再看混淆矩阵,如果某一类完全没被预测出来,九成是标签错位,而不是模型结构有问题。

5.2 准确率高但 ROC-AUC 低,指标打架

现象:验证集 accuracy 有 90%,但 ROC-AUC 只有 0.75 左右,临床同事反馈漏检了不少恶性。

原因:类别不平衡下 accuracy 被多数类抬起来了,模型只是学会了把大多数样本判成正常,恶性样本的召回率很低。

解决:立即把优化目标从 accuracy 换到加权损失,并把恶性样本召回率作为主要报告指标。查看验证集 PR 曲线,在乳腺数据上 PR-AUC 比 ROC-AUC 更贴近真实场景,因为医生更关心“我检出的人里到底有多少是真的恶性”。如果测试集中正常样本特别多,ROC 容易显得乐观,这种情况用 PR 曲线能看到更真实的下限。

5.3 验证集正常,测试时被全黑或全白样本打穿

现象:公开数据集上指标很好看,换到另一家医院的测试集,准确率从 90% 掉到 60%。

原因:模型学到了设备风格,包括亮度、边框、分辨率纹理,而不是病灶结构。这种特征偏移在乳腺影像里特别常见,尤其是直接从 DICOM 读出原始像素不做归一化的团队。

解决:两个方向。数据层面做颜色归一化和对比度标准化,上面 3.1 的百分位截断就是为这一步。评估层面做“跨数据域验证”,比如用 CBIS-DDSM 训练,再用 INbreast 做外部验证,哪怕样本量不大,也能暴露亮度相关泄漏。不要只迷信公开测试集分数,那只是同分布成绩。

5.4 数据量小、模型严重过拟合

现象:train loss 一路降到 0.1,验证 loss 在第 10 个 epoch 后开始回升,验证 AUC 停滞在 0.8 附近上不去。

原因:几百张训练图配深层 CNN,参数空间太大,ROI 裁剪后同一病灶的增样互相太像,模型等于记住了训练样本。

解决:先减模型复杂度,把 ResNet50 换成 ResNet18 或高效模型,观察验证 AUC 是否反而上升;然后调强增强,特别是旋转和随机缩放裁剪的概率;最后配合第 6 章的 K 折验证。这里记住一个心理预期:乳腺 ROI 分类在小数据集上,验证 AUC 到 0.8 已经是打磨得不错的状态,别拿 ImageNet 上那种 95% 的预期来带这个任务。

5.5 调参后结果忽高忽低,复现不稳定

现象:同样的代码、同样的数据集,昨天跑 AUC 0.84,今天重跑变成 0.79,看起来像玄学。

原因:训练时 shuffle、随机增强和 Dropout 的随机性在小数据集上影响很大。另一个隐藏点是验证集划分没固定,每次脚本重启都重新随机划分。

解决:固定全局随机种子,把训练集和验证集的划分文件写成 CSV 提交进版本库,数据加载器也使用固定顺序。我把划分配置也当代码一样管理,因为对乳腺影像这种小数据来说,划分方式对分数的影响可能比换模型还大。下一步再做重复多次实验,取均值和标准差,而不是拿单次结果写结论。

6. 让模型从“能跑”变成“可信”:分层交叉验证与部署前检查清单

6.1 五次分层 K 折:小数据集上最值得先做的一步

单次随机划分在乳腺影像上很难给出可发布的结论。我通常用分层 K 折做五次重复,统计均值与标准差,并且保证同一个患者的多个视图被分到同一折。这样才能避免“同一个病人的两张图分别出现在训练集和验证集”导致的乐观偏差。数据集如果带 patient_id,这一步是必做的;没有 patient_id 时,我会先做去重检查,把高度相似的样本挑出来。

6.2 部署前检查清单:把该验证的指标逐项跑一遍

每次实验结果记录混乱,是乳腺影像项目里最容易被低估的返工原因。我收尾时通常会逐项核对下面这些内容:

检查项具体做法通过标准
每折稳定性记录每折的 AUC、恶性召回、混淆矩阵五折标准差小于 0.03
跨设备稳定性用另一家医院的数据做外部验证外部 AUC 不低于内部 0.8 倍
输入尺寸敏感度测试 192、224、256 三种尺寸分数波动小于 0.02
概率阈值校准找让恶性召回优先的阈值召回率显著高于默认 0.5
低置信度分析单独挑出概率在 0.4-0.6 的样本确认这些样本可人工复核

乳腺影像不是“模型准确率高就能上线”的任务。临床上你更需要知道模型什么时候会犹豫,而不是强行给出二分类。我最后悔的一次实验就是只看平均 AUC 上线测试,结果漏检的两个样本刚好都是早期钙化。现在我会在项目收尾前专门跑一次低置信度样本分析,把概率在 0.4 到 0.6 之间的图挑出来交给医生复核。这个工作占不了多少时间,但能大大降低漏检风险。希望对你有所帮助。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询