简介:这是面向深度学习的乳腺癌症图像分类数据集,属于典型的医学图像二分类任务,共包含两个类别,具体标签可参考配套JSON类别文件,适合医学影像分类入门、算法效果对比以及相关课程设计。数据已按照目录清晰整理,训练集约480张、验证集约140张、测试集约70张,可直接输入分类模型完成训练、验证与测试全流程。资源包内共692个文件,主体为689张JPG分类图像,另附1个JSON类别文件、1个Python脚本和1张PNG示意图,Python脚本便于快速读取样本与划分数据;整体大小仅17.85MB,下载和解压都非常轻量。目前已有284人学习使用,对于需要快速开展二分类实验的开发者而言,能够省去数据收集、清洗和标注环节,拿到即可复现训练流程。同时,目录采用分文件夹方式保存不同类别样本,配合JSON标签文件可显著降低前期处理成本,也适合作为迁移学习或数据增强方法的验证基准。
1. 所有的乳腺癌分类模型,都是先输在数据集上
乳腺癌症图像分类这两年看起来谁都能跑,U-Net、EfficientNet甚至Transformer都能上,但真正让模型拉开差距的往往是数据集本身。同样一个二分类问题,用CBIS-DDSM切出来的patch和用整张钼靶图分类,模型结构完全不是一回事;同一份病理图像,按患者划分和按图片划分,验证指标能差出十几个百分点。下面会从公开数据集选型、标签组织、基线CNN训练、类别不均衡处理到迁移学习微调,走一遍深度学习数据集与图像分类的完整操作链路,最后给出验证和调试技巧。这套流程适合正在准备医学图像分类实验的工程师和研究生,也适合被准确率虚高问题困扰的从业者。
2. 怎么选乳腺癌图像数据集:主数据集与结构
2.1 公开数据集怎么选:没有万能数据,只有适配任务的数据
乳腺图像分类公开数据集分为两大类:X线影像和病理图像。CBIS-DDSM是乳腺钼靶影像,提供良恶性标注和病变区域ROI掩膜,适合做二分类;BreakHis是乳腺组织病理图像,包含良性和恶性两大类,每个类别又细分为多种亚型,放大倍率有40x、100x、200x、400x四档,适合做精细分类。此外,INBreast包含全视野乳腺X线影像,样本数量少但标注质量高,常用于测试而不是训练。
选数据集时先确认问题粒度。如果只是想跑通一个深度学习CNN流程,BreakHis的patch大小固定,处理成本低;如果要模拟真实临床筛查场景,CBIS-DDSM的病灶通常小、背景占比高,分类难度更大,也更容易暴露模型泛化能力弱的毛病。我一般会把原始数据描述和标注文件先解压到本地,统计文件大小、格式和类别数量,再决定要不要做重采样。
2.2 目录组织:按患者维度分割,避免标签泄漏
乳腺癌数据有一个天然陷阱:同一个病人的多张图像,或者同一张切片的不同patch,在内容上高度相似。如果按图片随机划分,模型会在训练时见过同一个病人的类似图像,验证时相当于开卷考试,得到的高分没有意义。正确做法是把patient_id作为分组键,按患者划分数据集。
比如BreakHis从病人组织样本中生成多个patch,目录通常按patient编号组织。下面用GroupShuffleSplit按患者完成分割:
from sklearn.model_selection import GroupShuffleSplit import pandas as pd df = pd.read_csv('metadata.csv') # 包含 image_path、label、patient_id split = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(split.split(df, groups=df['patient_id'])) train_df, val_df = df.iloc[train_idx], df.iloc[val_idx]groups参数传入patient_id列表,使同一患者的所有图像被放到同一个集合中。如果没有groups,sklearn会退化成随机划分,这是很多复现问题出现时查不出来的原因。测试集建议也按患者单独隔离开,不要和验证集混在一起调参。
注意:按患者分割是乳腺图像分类的红线,宁可少用训练样本,也不要让同一患者同时出现在多个集合中。
2.3 标签一致性检查:先看数据再写网络
很多人拿到数据后直接训练,结果训练到一半发现类别标签反了,白跑几个小时。这里建议在训练之前做一个快速一致性检查,确认每个患者的标签是否唯一。
grouped = df.groupby('patient_id')['label'].nunique() print(grouped[grouped > 1])如果发现同一个patient_id下既有良性又有恶性,不能简单用patient_id进行整体分割,需要确认是否属于同一病灶。这种情况下,可以选择按更细的“病灶块”划分,或者去读数据说明,确认各病灶之间是否独立。
另一个容易忽略的问题是图像格式。病理图像有时是PNG但实际是RGB,有时是灰度图单通道。用PIL打开后检查mode,统一转换成目标格式,比在模型里做分支判断更省心。
3. 从零搭一个乳腺病理图像分类CNN
3.1 最小PyTorch数据管道,先跑通再调参
配置好深度学习环境后,建议先用PyTorch写一个极简的数据管道,跑通一个batch,再考虑换网络。乳腺图像分类的错误很多出在数据读取和尺寸不一致上。下面是一个自定义Dataset的例子:
import pandas as pd from PIL import Image from torch.utils.data import Dataset class BreastDataset(Dataset): def __init__(self, df, img_dir, transform=None): self.df = df self.img_dir = img_dir self.transform = transform def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] img = Image.open(f"{self.img_dir}/{row['image_path']}") img = img.convert("RGB") label = int(row['label']) if self.transform: img = self.transform(img) return img, label这里convert("RGB")会把灰度病理图复制成三通道,这样可以直接用ImageNet预训练模型。如果数据集本身是RGB,也不会失真。__getitem__里不需要自己转Tensor,交给transform处理更高效。
构造训练集和验证集的不同transform:
from torchvision import transforms transform_train = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) transform_val = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])验证集不要加随机翻转,否则每个epoch的预测结果会抖动,影响early stopping判断。Normalize的均值方差用的是ImageNet统计量,如果后续要加载预训练权重,这个数值不要改。
3.2 CNN结构与参数:先别上大模型,用3层卷积看数据量
在迁移学习之前,先用一个三层卷积的小CNN做基线。它的参数量很小,CPU也能几分钟跑完一个epoch,可以快速判断数据本身有没有问题。
import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.conv1 = nn.Conv2d(3, 32, 3, padding=1) self.conv2 = nn.Conv2d(32, 64, 3, padding=1) self.conv3 = nn.Conv2d(64, 128, 3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.fc = nn.Linear(128 * 28 * 28, num_classes) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = torch.flatten(x, 1) return self.fc(x)输入224x224,经过三次池化后特征图边长变成28,全连接层输入维度是128x28x28。如果改了输入分辨率,这里必须同步改,否则会报维度错误。先跑这个模型,如果训练loss降低但验证loss不降,说明模型容量或数据增强不够;如果训练loss也不降,应该检查标签是否反了、输入数据有没有被正确归一化。
3.3 训练循环与epoch设置:loss不再下降就停
乳腺图像分类的深度学习epoch设置没有固定值,公开数据集一般50到100个epoch就足够。不要盲目增加epoch,重点看验证集loss。
import torch optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) criterion = nn.CrossEntropyLoss() best_val_loss = float('inf') for epoch in range(100): model.train() train_loss = 0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() train_loss /= len(train_loader) model.eval() val_loss = 0 with torch.no_grad(): for images, labels in val_loader: outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() val_loss /= len(val_loader) print(f"epoch {epoch}, train_loss {train_loss:.4f}, val_loss {val_loss:.4f}") if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model.pth')Adam在医学小数据集上通常比SGD好调,初始学习率1e-4是相对安全的起点。best_model.pth保存的是验证loss最低的模型,而不是最后一个epoch,因为最后一轮可能已经过拟合。注意model.eval()必须放在验证循环前,否则BatchNorm和Dropout行为不一致,验证loss会出现周期性抖动。
4. 乳腺癌类别不均衡:三种可落地的处理方法
4.1 为什么乳腺数据容易出现类别不均衡
乳腺图像集中恶性样本通常比良性少。CBIS-DDSM中恶性病变的比例并不高,真实临床数据更是阴性样本占大多数。如果直接用准确率评价,模型把所有样本都判为良性也能拿到很高的准确率,但这个模型在筛查场景中毫无价值。对于乳腺癌症图像分类,必须优先关注恶性类别的召回率,也就是多少个真正的癌症样本被找出来了。
另一个被忽视的不均衡是“难易不均衡”。恶性病变在不同染色条件下外观差异很大,少量难样本主导了loss,清晰简单样本占了大多数。单纯过采样不一定能解决难样本问题,还需要配合有效的增强或损失函数。
4.2 重采样:用WeightedRandomSampler调整训练分布
采样层面的处理就是让恶性样本在训练时更常被看到。PyTorch内置WeightedRandomSampler,只需要传入每个样本的权重。
from torch.utils.data import WeightedRandomSampler labels = train_df['label'].values labels_tensor = torch.tensor(labels) class_counts = torch.bincount(labels_tensor).float() weights = 1.0 / class_counts[labels_tensor] sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True) train_loader = DataLoader(train_dataset, batch_size=32, sampler=sampler)class_counts计算每个类别的样本数,weights是每个样本所在类别的样本数的倒数。良性样本如果特别多,它的权重就小;恶性样本权重就大。replacement=True表示有放回采样,能保证恶性样本以较高概率出现在每个batch里。
需要提醒的是,过采样容易让模型对少数类过拟合。使用加权采样时,验证集必须保持原始分布,否则评估结果无法代表真实场景。
4.3 数据增强和加权损失,怎么选
数据增强的主要目的是缓解染色差异和位置差异。乳腺病理图像中,同一组织在不同扫描仪下颜色差异明显,所以我建议一定要加ColorJitter。
transform_train = transforms.Compose([ transforms.RandomResizedCrop((224, 224), scale=(0.8, 1.0)), transforms.RandomRotation(20), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])RandomResizedCrop模拟不同放大倍率下的局部视野,ColorJitter的取值不要太大,hue=0.05已经是极限,否则组织颜色会失真到影响病理判断。除了增强,还可以用加权CE Loss:
class_weights = torch.tensor([1.0, 3.0]) criterion = nn.CrossEntropyLoss(weight=class_weights)1.0对应良性,3.0对应恶性。权重的具体数值可以按样本比例反比设置,但不必完全精确。Focal Loss在难样本多的数据集上更有效,但多了一个gamma超参,需要额外调参。
下面是一个快速选型对照表,方便你决定先试哪个。
| 方法 | 实现成本 | 主要风险 | 适用场景 |
|---|---|---|---|
| WeightedRandomSampler | 低 | 少数类过拟合 | 恶性样本占比低于20% |
| 数据增强 | 中 | 破坏病理结构 | 染色、倍率差异明显 |
| 加权CE Loss | 低 | 权重不好定 | 需要快速迭代 |
| Focal Loss | 中 | gamma难调 | 难样本多、类别重叠大 |
实际项目中,我一般先做数据增强,再用加权采样,最后才考虑Focal Loss,因为每多一个超参就多一分调试成本。
5. 用迁移学习微调EfficientNet来提升乳腺分类
5.1 为什么医学小数据集适合迁移学习
前面用SimpleCNN是拿来看数据状态的,真要获得能用的乳腺图像分类模型,迁移学习是性价比最高的路线。乳腺癌公开数据集通常只有数千到数万张patch,从头训练CNN很难学到鲁棒的高层语义特征。ImageNet预训练模型已经具备颜色、纹理、边缘等低层能力,这些能力对病理图像同样有效。微调只需要让高层特征适配乳腺组织的形态差异,能显著降低对数据量的需求。
5.2 微调的冻结与解冻策略
常见做法分两步。第一步冻结所有骨干参数,只训练新分类头,让模型先适应目标类别;第二步解冻所有参数,用很小的学习率整体微调。用timm加载EfficientNet-B0:
import timm import torch.nn as nn model = timm.create_model('efficientnet_b0', pretrained=True, num_classes=2) # 记住原始分类头输入维度 in_features = model.classifier.in_features # 冻结全部骨干参数 for param in model.parameters(): param.requires_grad = False # 替换分类头 model.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 2) ) optimizer = torch.optim.Adam( [p for p in model.parameters() if p.requires_grad], lr=1e-3 )timm.create_model会自动下载预训练权重,num_classes=2会替换默认的1000类输出。in_features从原分类头读取,不硬编码,方便切换EfficientNet-B1/B2。第一阶段只训练分类头,学习率可以用1e-3。
第一阶段训练10个epoch左右,等分类头的loss下降后,再解冻全部参数:
for param in model.parameters(): param.requires_grad = True optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)第二阶段学习率必须降到1e-5这个量级。如果学习率太大,预训练权重会被迅速破坏,验证loss反而升高。
5.3 调参:学习率、dropout、early stopping
微调阶段最容易被忽略的是BatchNorm层。如果之前冻结了所有参数,解冻后BatchNorm的running_mean和running_var会开始更新,但这需要合适的batch size,否则统计量抖动。我通常用batch size 16或32,如果显存不够就降分辨率而不是降batch size。
dropout也可以加大一点。EfficientNet的默认drop_rate是0.2,对于几千张的小数据集,我会在分类头里额外加一层Dropout(0.3),也就是上面代码里nn.Sequential中间那层。如果训练集很小,还可以把分类头的dropout加到0.5,代价是需要更多epoch才能收敛。
计算资源允许的情况下,用验证集AUC而不是准确率来筛选模型。因为乳腺癌症图像分类的类别不均衡,准确率这条曲线会把模型引向“全部判良性”的陷阱。迁移学习的epoch数建议控制在20-50,如果验证loss在10个epoch内不下降,说明学习率或dropout需要调整,不要硬撑到100个epoch。
6. 验证不只看准确率:乳腺癌分类的进阶技巧
6.1 用混淆矩阵和ROC定位坏样本
乳腺癌症图像分类要关注的指标不是准确率,而是恶性类别的召回率和AUC。训练完成后,在测试集上输出混淆矩阵和ROC:
from sklearn.metrics import confusion_matrix, roc_auc_score import numpy as np all_labels = [] all_probs = [] model.eval() with torch.no_grad(): for images, labels in test_loader: outputs = model(images) probs = torch.softmax(outputs, dim=1)[:, 1] all_labels.extend(labels.cpu().numpy()) all_probs.extend(probs.cpu().numpy()) preds = (np.array(all_probs) > 0.5).astype(int) print(confusion_matrix(all_labels, preds)) print('AUC:', roc_auc_score(all_labels, all_probs))混淆矩阵会告诉你模型是把良性误判成恶性,还是把恶性漏判。AUC不依赖0.5这个阈值,更接近模型对两类样本的排序能力。
6.2 Grad-CAM检查模型关注的区域
如果模型在高置信度下关注了背景而不是病变区域,说明模型学到的是假捷径。常见做法是拿Grad-CAM可视化最后一层卷积的热力图,人工确认模型看到了哪里。我习惯用pytorch-grad-cam这个库,安装后直接调用:
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget cam = GradCAM(model=model, target_layers=[model.blocks[-1]]) targets = [ClassifierOutputTarget(1)] grayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0, :]target_layers选择的是EfficientNet最后一个block,ClassifierOutputTarget(1)指定要解释恶性类别。热力图应该落在细胞团或者病变组织区域,而不是玻璃片边缘。如果热力图集中在空白背景,就要检查训练数据里是否存在背景偏移造成的误导。
6.3 一个容易踩的坑:数据顺序和随机种子
最后一个容易忽略的调试点:数据加载顺序。如果DataLoader的shuffle=True但没有固定随机种子,每次训练得到的验证集顺序和增强顺序都会变化,AUC会有小抖动。复现实验时,在所有随机模块前统一设置一次:
import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.backends.cudnn.deterministic = True固定种子之后,配合早停保存的best_model.pth,你可以在不同机器上得到一致的分类结果。把数据分割、数据加载和模型初始化三个部分的随机源分别固定,调试效率会好很多。
本文还有配套的精品资源,点击获取