简介:面向深度学习图像分类入门与乳腺癌症课题研究的数据集资源,适合需要标准化训练/验证/测试划分的初学者或竞赛团队。数据按目录存放,类别信息见随附json文件,共2类,其中训练集约480张、验证集约140张、测试集约70张,可直接用于模型训练与效果评估,省去自行整理数据集的耗时。资源共692个文件,包括689张jpg样本图、1个分类json、1个Python脚本及1张说明图,压缩包整体约17.85MB,轻量易下载,便于快速开展实验。目前已有286人学习参考。配套的Python脚本可用于数据加载或预处理,json文件帮助理解类别映射,整体目录结构清晰、命名规范,适合作为乳腺癌症图像二分类任务的基线数据集,也可用于迁移学习、数据增强等扩展实践。
1. 乳腺癌症图像分类:数据质量决定模型上限
乳腺癌的钼靶影像二分类任务是医学图像分类里最像“深度学习数据集工程”的项目之一。很多人一上来就调 ResNet,跑完发现:训练集 loss 降得很漂亮,验证集 AUC 也有 0.95,但把模型丢到另一个医院的数据上,准确率直接跳水。问题通常不在模型,而在数据集——标签是怎么来的、同一个病人有没有被拆散到不同集合、类别的先验分布是什么。这篇文章从公开数据集选型、预处理、模型训练到踩坑排查,走一遍乳腺癌症图像分类的落地流程。适合正在做毕设、医学影像算法入门的工程师,也适合要从头搭一套乳腺图像分类流程的团队。
2. 公开数据集怎么选:从成像模态到标签分布的取舍
乳腺影像分类数据集不像 CIFAR-10 那样“下载即用”。除了图像本身,你还要关心成像设备、标注来源、文件格式和标签分布。我见过太多项目在数据集上省时间,最后把时间加倍赔在调模型上。选数据集这一步,值得先花半天看清楚再动手。
2.1 三大公开数据集:CBIS-DDSM、BreakHis 与 MIAS 的取舍
做乳腺癌症图像分类,最常见的公开数据集是这四个:
| 数据集 | 成像模态 | 分类粒度 | 规模 | 标签类型 | 适用场景 |
|---|---|---|---|---|---|
| CBIS-DDSM | 钼靶 X 光 | ROI/图像级 | 约 1600 病例 | 良性/恶性,含亚型 | 钼靶筛查分类 |
| BreakHis | 组织病理 | 图像级 | 约 7909 张 | 良性/恶性,8 个亚型 | 病理图像分类 |
| MIAS | 钼靶 X 光 | 图像级 | 322 张 | 正常/良性/恶性 | 流程验证、教学 |
| IDC | 组织病理 Patch | Patch 级 | 数十万 Patch | 浸润性导管癌有/无 | Patch 级二分类 |
CBIS-DDSM 是 DDSM 的清洗版,提供 CSV 元数据和 ROI 掩膜,下载后不需要自己画框,适合做标准的图像分类流程。BreakHis 是病理图像,同一个病例有多张不同放大倍率的图,如果直接把 40x 和 400x 混在一起训练,模型很容易学“倍率”这个捷径特征,而不是病灶本身。MIAS 数据量太小,只适合用来跑通代码,不建议当最终实验集。IDC 数据集全是 50x50 的小 Patch,拿来验证“小图分类”场景很方便,但不能直接回答整张病理切片的分类问题。
我一般会先问一个问题:你要做钼靶筛查,还是做病理辅助诊断?前者选 CBIS-DDSM,后者选 BreakHis。两个都做也不是不行,但训练和评估要分开,不能把两个模态的数据混在一个训练集里,否则模型会通过图像背景色调来分类,而不是通过病灶特征。
2.2 按任务倒推选数据:二分类标签的获取与清洗
拿到数据集后,第一件事不是写 DataLoader,而是把标签分布打印出来看。很多人直接从网上下载打包好的图像文件夹,用文件夹名当标签,结果跑完实验才发现“良性”文件夹里混了“正常”样本,模型从头到尾学的都是三分类任务。
import pandas as pd # CBIS-DDSM 官方 CSV 里的 pathology 字段是金标准标签 df = pd.read_csv("cbis_ddsm.csv") print(df["pathology"].value_counts()) print(df.head())这段代码的意图很直白:先用 pandas 查看pathology字段的类别分布,确认良性、恶性、正常三类各有多少,再看 CSV 里是否同时存在mass和calcification两种病变类型。CBIS-DDSM 里这两种病变的病理确认途径不同,合并成一个二分类任务时,要先确认它们在标签分布上没有严重倾斜,否则模型可能只学会了区分病变类型,而不是区分良恶性。
这一步还会暴露一个常见问题:同一个病人的图像,可能因为左右乳、不同拍摄角度而被重复记录。标签分布检查完,紧接着就要做按病人的数据集划分。
2.3 数据划分铁律:按病人 ID 切集,不按图像 ID
乳腺影像分类最重要的一个划分原则,是保证同一个病人的所有图像只出现在一个集合里。如果训练集和验证集里混入了同一个病人的多张图,模型会通过病人自身特征(乳腺组织密度、皮肤纹理、机器伪影)来分类,而不是通过病灶特征。这会让验证指标虚高,到了新病人身上立刻现原形。
from sklearn.model_selection import GroupShuffleSplit # group 传 patient_id,确保同一个病人的图像不会被拆到不同集合 gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(df, groups=df["patient_id"])) train_df = df.iloc[train_idx] val_df = df.iloc[val_idx] # 划分后检查两个集合的类别占比是否接近 print(train_df["pathology"].value_counts(normalize=True)) print(val_df["pathology"].value_counts(normalize=True))GroupShuffleSplit的groups参数是这里的核心。test_size=0.2表示留出 20% 的病人作为验证集,random_state=42固定随机种子,保证实验可复现。划完之后还要看两个集合里良恶性的占比是否接近,如果训练集里恶性占 60%、验证集里恶性占 30%,那验证曲线就没法直接解读。
很多公开代码只按文件名分层抽样,不做 Group Split。这是乳腺图像分类里最常见的隐性 bug,甚至比模型选型的影响更大。划分完最好再打印一份“病人 ID 数量”和“图像数量”的对照表,确认没有病人同时出现在两个集合里。
3. 数据预处理与增强:把有限样本撑出泛化能力
医学影像数据集的规模通常不会太大,CBIS-DDSM 的 ROI 图像也就是几千张的量级。这种规模下,预处理和增强不是“锦上添花”,而是决定模型能不能收敛、会不会过拟合的关键步骤。但乳腺图像的增强不能照搬自然图像的套路,加得太猛反而会让模型学到虚假的纹理。
3.1 读图管线:从原始文件到模型输入的标准化
乳腺影像常见两种存储格式:DICOM 和 PNG/TIFF。CBIS-DDSM 里两种都有,很多人直接拿 PIL 打开 DICOM 会失败,或者读出来是一张纯黑图,因为 DICOM 的像素值是 12 bit 或 16 bit 的原始值,不能直接按 8 bit 处理。
import numpy as np from PIL import Image def load_mammogram(path, target_size=(512, 512), roi_box=None): # 支持 DICOM 与 PNG/TIFF 两种常见格式 if path.endswith(".dcm"): import pydicom dcm = pydicom.dcmread(path) img = dcm.pixel_array # 12bit 或 16bit 原始像素 else: img = np.array(Image.open(path).convert("L")) # 统一转灰度 # 16bit 数据不能直接除以 255,需要按位深或最大最小值归一化 img = img.astype(np.float32) img = (img - img.min()) / (img.max() - img.min() + 1e-8) # 转成 RGB 三通道,适配 ImageNet 预训练模型的输入要求 img = Image.fromarray((img * 255).astype(np.uint8)).convert("RGB") # 若有 ROI 框,先裁剪再缩放,避免全图缩放丢失病灶细节 if roi_box is not None: img = img.crop(roi_box) img = img.resize(target_size, Image.BILINEAR) return img这段代码做了三件关键事。第一,DICOM 走 pydicom 读取,pixel_array拿到的是原始像素矩阵,不做转换。第二,归一化用“单张图最大最小值”而不是固定除 255,避免高位数像素值整体偏移。第三,如果有 ROI 框,先裁剪再 resize,而不是先缩放整张图再裁,后者会把微钙化这类小病灶直接被插值抹掉。
需要注意roi_box这个参数。CBIS-DDSM 的 CSV 里提供 ROI 坐标和掩膜,我建议先用掩膜外接矩形裁剪出病灶区域,再缩放,这样模型看到的是“病灶特写”,而不是大片的黑色背景。对钼靶图像来说,背景区域占比很大,直接全图缩放会稀释病灶特征。
3.2 数据增强的边界:哪些增强对乳腺影像有效
乳腺图像增强的第一原则是“不改变解剖语义”。水平翻转可以用,因为左右乳成对出现,翻转后依然是合理的乳腺结构;小角度旋转可以用,但要控制在 ±10 度以内。垂直翻转不建议,乳腺结构有固定的上下方位,翻转后模型会学到错误的空间先验。
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=10), transforms.RandomResizedCrop(size=512, scale=(0.85, 1.0), ratio=(0.9, 1.1)), transforms.ColorJitter(brightness=0.1, contrast=0.1), # 只小幅调整 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])RandomResizedCrop的scale范围收在 0.85 到 1.0,不要像 ImageNet 那样设到 0.08,乳腺病灶的尺度变化有限,过度裁剪会直接裁掉病灶。ColorJitter 只动 brightness 和 contrast,幅度都在 0.1 以内。钼靶图像的对比度本身就是诊断信息,调太大会把“恶性病灶的放射状边缘”这种关键纹理给抹掉。
CutMix、MixUp 这类混合增强在乳腺图像上要非常谨慎。CBIS-DDSM 的病灶区域在整个图像里占比很小,两张图混合后,模型很难有效学习到“哪个区域的病灶对应哪个标签”。我在病理图像上用过 MixUp,效果尚可;在钼靶图像上试过一次,AUC 反而掉了 0.03。先跑通基线,再决定要不要上混合增强,不要一步到位。
3.3 类别不平衡的预处理侧解法:加权采样器与样本复制
乳腺图像分类的训练集里,良性和恶性的比例通常不是 1:1。CBIS-DDSM 里良性 ROI 数量往往多于恶性,差的可能到 1.5:1 甚至 2:1。如果直接按原始分布训练,模型会倾向于把所有样本都判成多数类。
from torch.utils.data import WeightedRandomSampler # 统计每个类别的样本数 counts = train_df["pathology"].value_counts().to_dict() # 每个样本的权重 = 总样本数 / (类别数 * 该类别样本数) sample_weights = [1.0 / counts[label] for label in train_df["pathology"]] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) # 使用方式:train_loader = DataLoader(dataset, batch_size=32, sampler=sampler)这里sample_weights的计算方式是经典的“逆频率”加权。比如良性和恶性样本数分别是 1000 和 500,那么恶性样本的权重是1/500,良性是1/1000,采样器会让恶性样本被抽到的概率翻倍。replacement=True表示每次采样允许重复抽到同一个样本,这对少数类是必要的——样本量太少时,不放回采样会让每个 epoch 里少数类出现的次数仍然很少。
加权采样器要和后面的加权损失函数配合使用。采样器改变模型“看到什么”,损失函数改变模型“更重视什么”。两个都做,类别不平衡问题才算真正被处理。
4. 模型选型与训练:迁移学习与不平衡调优的完整流程
数据集准备好之后,才轮到模型。乳腺图像分类领域现在有各种新架构,但绝大多数落地项目默认从 ImageNet 预训练的 ResNet 系或 EfficientNet 系起步。原因很简单:医学图像数据量撑不起从头训练。
4.1 为什么选迁移学习:预训练权重在医学图像上的适用边界
ImageNet 和乳腺钼靶图像看起来完全不像,但预训练模型学到的底层特征——边缘、纹理、形状成分——是可以迁移的。病灶的边缘是否毛糙、钙化点的分布形态,这些判断依据本质上还是低层视觉特征。医学图像数据集通常只有几千到几万张,在这个量级上从头训练 CNN,效果明显不如迁移学习。
最新的图像分类模型里,ViT 和 ConvNeXt 值得关注,但 ViT 在小数据集上需要更长的训练时间和更强的正则化,否则容易欠拟合。我一般建议先用 ResNet50 搭一条完整基线,把数据管线、损失函数、评估方式全部跑通,然后再换 EfficientNet 或 ConvNeXt 做对比。基线模型的价值不是精度最高,而是让项目先有一个可以信赖的参照系。
4.2 最小可用的训练脚本:ResNet50 加权重损失
下面是一个可以直接改路径就跑的训练脚本骨架,包含模型替换、冻结策略和不平衡损失:
import torch import torch.nn as nn from torchvision import models # 使用 ImageNet 预训练权重,替换最后一层为二分类 model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 2) # 根据验证集分布设定类别权重,少数类给更大权重 class_weights = torch.tensor([1.0, 2.0]) # [良性, 恶性] criterion = nn.CrossEntropyLoss(weight=class_weights) # 冻结前两层,只训练高语义层的参数,保护预训练特征 for name, param in model.named_parameters(): if name.startswith("layer1") or name.startswith("layer2"): param.requires_grad = False optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=3e-4, weight_decay=1e-4 ) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)class_weights是这段代码的核心。如果验证集里恶性占比 33%,那么把恶性权重设为1/0.33 ≈ 3.0更合适,代码里的2.0只是一个初始值。CrossEntropyLoss的weight参数会直接改变每个样本的梯度贡献,少数类分错时损失更大。冻结layer1和layer2是为了让预训练模型的低层特征不被破坏,乳腺图像虽然和 ImageNet 差异大,但边缘和纹理的低层结构是通用的,先冻结能加快收敛。
训练时还有一个容易被忽略的点:不要用最后一轮的权重,而是保存验证集 AUC 最高的那一轮。我在代码里会加一个简单的val_auc监听,每轮结束对比当前 AUC 和历史最优,有提升才覆盖保存。
4.3 损失函数与评价指标:不平衡任务不看准确率
加了class_weights之后,如果效果还不理想,下一步尝试的是 Focal Loss。Focal Loss 的表达式是-(1-p_t)^γ * log(p_t),其中 γ 通常取 2。它的作用是降低容易分类样本的损失权重,让模型把注意力放在难分类的少数类样本上。加权交叉熵是“所有少数类样本统一放大损失”,Focal Loss 是“每个样本按置信度动态调整损失”,两种思路可以叠加使用。
评价指标方面,首先放弃 accuracy。乳腺癌症分类里,漏检恶性的代价远高于误报良性,所以重点看这几个指标:AUC、敏感性(召回率)、特异性和 F1。AUC 反映的是模型把所有样本正确排序的能力,不依赖分类阈值,最适合做模型对比。敏感性是“恶性样本被正确识别出来的比例”,这是临床最关心的数字。
如果模型在验证集上 AUC 有 0.92,但敏感性只有 0.70,说明阈值定高了。可以画 ROC 曲线,在验证集上选一个让敏感性最大化的阈值,而不是默认 0.5。这个操作叫阈值移动,实现只需要几行代码,但经常被忽略。
4.4 训练过程的参数边界:学习率、Batch Size 与 Epoch
ResNet50 迁移学习的典型配置是:输入尺寸 512、batch size 16、学习率 3e-4(AdamW)、epoch 30。如果显存不够,先把输入降到 384,不要直接降 batch size,因为小 batch 会让 BN 层的统计量不稳定。实在要降,就要配合梯度累积。
# 梯度累积示例:显存不够时,每 4 个 step 更新一次参数 accumulation_steps = 4 for i, (images, labels) in enumerate(train_loader): outputs = model(images) loss = criterion(outputs, labels) loss = loss / accumulation_steps # 平均化,等效于放大 batch size loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()学习率是最容易翻车的参数。迁移学习阶段,3e-4 是一个安全的基线;如果训练初期 loss 迅速飙升,基本是学习率过大破坏了预训练权重。全模型微调时,学习率要比只训最后的 FC 层更低一些,最好分两阶段:先只训练 FC 层 5 个 epoch,再解冻全部层,用 1e-4 继续微调。早停的条件设为“验证集 AUC 连续 5 个 epoch 不上升就停止”,比固定 epoch 数更省时间。
这些参数本质上还是要随数据集微调的。ResNet50 在 CBIS-DDSM 上收敛快,但在 BreakHis 的病理图像上可能需要更长的训练时间,因为病理图像的纹理复杂度远高于钼靶。跑实验时把每组参数组合的结果记录到 CSV,包括 lr、batch size、冻结策略、AUC 和敏感性,别凭记忆做对比。
5. 乳腺图像分类的 5 个高频坑:现象、原因与解决办法
乳腺图像分类里有些坑,几乎每个新手都会踩一遍,而且很多坑靠调模型是解决不了的。下面五条是我做过多个医学影像分类项目后沉淀下来的高频问题,按“现象→原因→解决”写清楚。
5.1 坑一:病人图像跨集合导致验证集数据泄漏
现象:训练集 AUC 快速冲到 0.99,验证集 AUC 也很高,但模型一到外部数据上表现明显下降。
原因:划分数据集时只按图像 ID 分层,没有按病人 ID 分组。同一个病人的左右乳图像、不同拍摄角度的图像被拆散到训练集和验证集里,模型学到的是病人身份特征。
解决:用GroupShuffleSplit按patient_id划分,划分后统计两个集合的病人 ID 交集,确认交集为空。这一步同时要做类别比例检查,保证验证集和训练集的标签分布接近。
5.2 坑二:标签噪声让训练 loss 在争议样本上反复震荡
现象:训练 loss 降到一定程度后不再下降,验证集 F1 在连续几个 epoch 里反复横跳。
原因:CBIS-DDSM 这类数据集的标签来自病理报告,但部分病例的良恶性诊断本身存在争议。尤其是“不典型增生”这类交界性病变,不同病理医生的判断可能不同,模型在这些样本上无法一致地学习。
解决:把训练集里 loss 最高的 20 个样本打印出来人工复核。如果确实标签存疑,可以对这些样本做标签平滑(Label Smoothing),或者用torch.nn.CrossEntropyLoss(label_smoothing=0.1)降低模型对争议样本的绝对信任。
5.3 坑三:类别不平衡导致模型“全猜多数类”
现象:验证集 accuracy 有 75%,看起来还可以,但敏感性只有 20%,恶性样本几乎全被漏掉。
原因:未做任何不平衡处理,模型学会把不确定性样本全部判成多数类。准确率被多数类的高数量掩盖了。
解决:训练侧用加权采样器或加权损失,评估侧用 AUC、敏感性、特异性替代 accuracy,并在验证集上重新搜索分类阈值。我在 4.3 节里说的阈值移动,主要就是针对这个问题。
5.4 坑四:ImageNet 归一化直接套到灰度图上
现象:训练第一个 epoch 时 loss 不降,或初始 loss 比预期高一截,图像可视化时出现明显偏色。
原因:钼靶图像是单通道灰度,被复制成三通道后,通道之间的相关性远超自然图像。直接用 ImageNet 的mean=[0.485, 0.456, 0.406]和std=[0.229, 0.224, 0.225],理论上不是最优匹配。偏色本身不致命,但会让模型在最初的几个 epoch 里多花时间去适应输入分布。
解决:对灰度图像复制三通道时,保持每个通道的值一致。可以在验证集上计算数据集的真实均值与标准差,替换 ImageNet 统计量。我在实际项目里会用 ImageNet 统计量先跑通基线,再换真实统计量对比,通常真实统计量在早期收敛更快。
5.5 坑五:大分辨率重采样把微钙化细节直接抹掉
现象:模型在病理图像上表现不错,但切换到钼靶图像后 AUC 明显下降,尤其是微钙化类病例。
原因:钼靶原图分辨率可能在 3000x4000 以上,直接 resize 到 224x224 时,微钙化这类细小病灶的像素占比可能只剩一两个像素,信息彻底丢失。这是预处理层级的不可逆损失。
解决:先用 ROI 掩膜裁剪出病灶区域,再做 resize,保证裁剪后的图像里病灶占据主体。如果任务本身是“微钙化分类”,用 Patch 级识别更合适,先在全图上做粗定位,再把定位区域切出来交给分类模型。我一般会在读图管线里保留两个分支:全图分支用于上下文,ROI 分支用于病灶细节,最后把两个分支的特征拼接起来。
6. 用混淆矩阵和难样本复盘:验证模型真的能上线
很多项目停在了“验证集 AUC 不错”这一步,但 AUC 是排序指标,不能告诉你模型具体错在哪。上线前最后一步,是把验证集所有的判错样本翻出来看一遍。这一步的产出不是数字,而是对错误模式的判断。
6.1 评估脚本:混淆矩阵、AUC 与混淆样本导出
from sklearn.metrics import confusion_matrix, classification_report, roc_auc_score import torch model.eval() all_preds, all_probs, all_labels, all_paths = [], [], [], [] with torch.no_grad(): for images, labels, paths in val_loader: images = images.to(device) outputs = model(images) probs = torch.softmax(outputs, dim=1) preds = probs.argmax(dim=1) all_probs.extend(probs[:, 1].cpu().numpy()) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) all_paths.extend(paths) # 一次性输出精确率、召回率、F1 与各类别的支持度 print(classification_report(all_labels, all_preds, target_names=["benign", "malignant"])) print(confusion_matrix(all_labels, all_preds)) print("AUC:", roc_auc_score(all_labels, all_probs)) # 把判错的样本挑出来,按置信度差距排序,方便人工复核 wrong = [(p, l, pa, pr) for p, l, pa, pr in zip(all_preds, all_labels, all_paths, all_probs) if p != l] wrong.sort(key=lambda x: abs(x[3] - (1 - x[3])))classification_report会一次性给出每个类别的精确率、召回率和 F1,这比只看 accuracy 有用得多。roc_auc_score输入的是恶性类别的预测概率,而不是类别 ID。wrong.sort按置信度差距排序,把模型“最有底气但判错”的样本排在前面,这些样本最值得人工检查。
我一般会导出 20 到 30 张判错样本。如果发现判错的恶性样本集中在某种特定拍摄角度或乳腺密度类型上,那说明训练集在这个子类上的覆盖不足,需要回到数据层面补样本或做针对性增强。
6.2 用 CAM 看模型关注区域:病灶定位是加分项
混淆矩阵只能告诉你“哪里错了”,不能告诉你“为什么错”。要回答这个问题,用 CAM(Class Activation Mapping)看模型最后卷积层的激活区域。如果模型判错一张恶性样本,而 CAM 显示模型关注的是图像边缘的皮肤或背景伪影,说明模型在靠背景泄漏特征做决策,这是个危险信号,比单纯“精度不够”严重得多。
检查 CAM 时,我会把激活热力图叠加到原图上,和 ROI 掩膜做重叠度计算。一个靠谱的模型,对恶性样本的关注中心应该落在 ROI 区域内。如果在多张样本上重叠度都低,就需要回到训练策略,考虑是否要引入 ROI 监督信号,比如给模型输入裁剪后的 ROI,或者在损失函数里加一个定位辅助分支。
我自己做乳腺图像分类的教训是:一开始把 80% 时间花在换模型上,后来发现数据集划分和标签检查才是真正决定上限的地方。到现在我拿到任何医学图像数据集,都会先花半天做分布检查、病人分组、边界样本复核,再开始跑模型。这会让人觉得“没在干活”,但这一步做扎实了,后面的训练几乎不会出大问题。希望帮到你。
本文还有配套的精品资源,点击获取