简介:面向深度学习和医学影像分类任务,这份乳腺癌症图像分类数据集可直接用于二分类模型的训练与验证,适用于科研教学和辅助诊断模型搭建等场景。资源已按目录结构存放,同一类别放在同一文件夹内,并附有JSON类别映射文件和Python读取脚本,方便快速加载图像与标签,适合入门或进阶的图像分类项目实践。压缩包共包含六百九十二个文件,以六百八十九张JPG图像为主,另有Python脚本、PNG示意图片和JSON配置文件各一个,整体仅十七点八五MB,轻量易用,便于快速下载与本地部署。数据已划分为训练集约四百八十张、验证集约一百四十张、测试集约七十张,目录结构清晰,能够直接用于模型训练和效果评估,也便于理解标注结构与数据组织方式。目前已有二百八十四人学习下载,是一份即取即用的乳腺癌症图像分类基准数据资源。
1. 乳腺癌症图像分类:数据集质量决定模型上限
乳腺癌症图像分类往往被误以为是一个模型问题,实际运作后会发现是数据工程问题。不少团队直接拿公开的乳腺超声或病理数据集跑ResNet,精确调参后训练集准确率能做到98%,但换一个来源的数据又掉到70%。这通常不是模型能力不足,而是数据集划分不当、染色差异未处理、类别不均衡被忽略。下面这套思路围绕“深度学习数据集”这个核心,从选型、预处理、加载、模型训练、评估到调参,梳理一套能在本地复现的乳腺癌症图像分类方案。适合具备PyTorch基础、想把医学图像分类做扎实的工程师和算法实习生。
2. 乳腺癌症图像分类数据集选型与预处理
2.1 公开数据集与私有数据集的取舍
做乳腺癌症图像分类,第一步是确定数据长什么样。常见的公开数据集中在三类:乳腺超声图像、组织病理图像、钼靶X光图像。我一般先按任务模态和硬件条件选。下表是一个快速选型参考:
| 数据集 | 模态 | 典型样本量级 | 类别 | 适用场景 |
|---|---|---|---|---|
| BUSI | 超声 | 数百张 | 良性/恶性/正常 | 小样本入门、目标检测 |
| BreakHis | 病理 | 数千张 | 良性/恶性(多种亚型) | 二分类、迁移学习 |
| CBIS-DDSM | 钼靶 | 上千张 | 良性/恶性/正常 | 病灶分类、弱监督 |
| Camelyon16 | 病理全切片 | 数百张切片,可切patch | 有癌/无癌 | 大规模patch分类 |
选定数据集不能只参考榜单准确率。公开数据通常背景干净、标注统一;私有临床数据则带有设备噪声、标记框不一致、多中心染色差异。如果手头有私有数据,更值得在预处理上花时间,而不是先换更复杂的图像分类算法。对刚开始接触的人,我建议从BreakHis的100倍放大子集入手,因为图像尺寸适中、类别相对均衡,能在单卡上快速迭代。
2.2 乳腺病理图像的染色归一化与标准化
乳腺病理图像以H&E染色为主,不同实验室、不同批次的染色会带来明显颜色偏移。直接用ImageNet统计量做归一化只能解决亮度问题,无法消除染色差异。常见做法是用Macenko或Reinhard染色归一化,把每张图的颜色统计量对齐到参考图。下面是一个简化的Reinhard实现:
import numpy as np import cv2 def reinhard_color_normalize(img, target_mean, target_std): # 转换到LAB空间,只对齐A/B通道,L通道保留 lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB).astype(np.float32) mean, std = cv2.meanStdDev(lab) mean = mean.flatten() std = std.flatten() # 防止分母为0 std[std < 1e-6] = 1e-6 # 将当前图统计量映射到目标统计量 lab = (lab - mean) / std * target_std + target_mean lab = np.clip(lab, 0, 255).astype(np.uint8) return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)函数先把图像从BGR转到LAB色彩空间,再分别对通道做z-score标准化,最后映射到target_mean和target_std。target_mean和target_std需要预先从一张质量稳定的参考图计算,通常取10张参考图的均值。实际训练时,我会把这个变换放在Dataset.__getitem__中,而不是离线保存到磁盘,因为不同任务可能需要不同参考统计量。
染色归一化之外,绕不开的是数据增强。对病理图像,我这里用一组实际验证过的组合:
import torchvision.transforms as T train_transform = T.Compose([ T.ToPILImage(), T.RandomResizedCrop(224, scale=(0.8, 1.0)), T.RandomHorizontalFlip(p=0.5), T.RandomRotation(15), T.ColorJitter(brightness=0.2, contrast=0.2), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomResizedCrop模拟不同放大倍率下视野变化,ColorJitter增加颜色鲁棒性。要注意的是,超声图像不能使用太大旋转角度,因为探头方向有物理意义,我通常限制在10度内;病理图像旋转对称性较强,15度没大问题。
2.3 类别不均衡:采样器与加权损失
乳腺癌症数据集中恶性样本经常远少于良性样本,直接训练会让多数类淹没少数类。两种常用手段是采样层面和损失函数层面。先看基于权重的采样:
from torch.utils.data import WeightedRandomSampler # labels 是全体训练样本的标签列表,0=良性,1=恶性 labels = np.array([sample['label'] for sample in train_samples]) class_counts = np.bincount(labels) class_weights = 1.0 / class_counts sample_weights = class_weights[labels] sampler = WeightedRandomSampler(sample_weights, num_samples=len(labels), replacement=True)sample_weights是每个样本所属类别权重的倒数,恶性样本所在的稀少类别权重更高,被采样概率更大。replacement=True允许重复采样,让每个epoch都能看到更多少样本类别。这里有一个隐藏注意点:如果少样本类别本身存在噪声,过度采样会把噪声也放大。因此我会同时给损失函数加权重,并保留一个干净的人工复核集。
class_counts = np.bincount(labels) weight = torch.tensor([class_counts[0] / len(labels), class_counts[1] / len(labels)], device=device) criterion = nn.CrossEntropyLoss(weight=weight)这里weight直接用训练集的类别频率。实际调试时,我会让权重的和等于类别数,或者使用中位数归一化,避免极端类别权重导致训练震荡。如果使用Focal Loss,还可以进一步抑制易分样本的梯度贡献,但那是在类别严重样本不均衡时才会真正需要。
3. 用PyTorch构建乳腺图像分类的Dataset与训练循环
3.1 自定义Dataset:从路径表格到样本
医学数据集往往带有患者编号、病理等级、扫描设备等额外信息。我不用torchvision.datasets.ImageFolder,而是先构造一个包含路径和标签的DataFrame,再自定义Dataset:
import pandas as pd from torch.utils.data import Dataset from PIL import Image class BreastCancerDataset(Dataset): def __init__(self, df, transform=None): self.df = df self.transform = transform def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] img = Image.open(row['path']).convert('RGB') if self.transform: img = self.transform(img) # 返回图像、标签、和图片所属患者ID return img, row['label'], row['patient_id']patient_id不是给模型用的特征,而是用于评估时按患者分组。很多病理项目会把一张全切片切成几百个patch,如果训练集和验证集混入同一切片的patch,模型会靠背景纹理“认出”训练集,验证指标虚高。返回patient_id就能在验证时按患者聚合指标。
3.2 DataLoader与验证集划分:按患者而不是按样本
划分验证集时,最忌讳直接random_split。乳腺癌症图像分类中,同一患者的patch高度相似,应该保证一个患者的全部patch只落在训练或验证集一侧。这里用GroupShuffleSplit:
from sklearn.model_selection import GroupShuffleSplit groups = df['patient_id'] gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(df, groups=groups)) train_df = df.iloc[train_idx] val_df = df.iloc[val_idx]GroupShuffleSplit根据groups参数将整组数据分配进同一边。test_size=0.2表示验证集占20%。这个划分方式比随机划分更慢,但能有效防止数据泄漏带来的乐观估计。
然后是DataLoader:
from torch.utils.data import DataLoader train_dataset = BreastCancerDataset(train_df, transform=train_transform) val_dataset = BreastCancerDataset(val_df, transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)val_transform不包含随机增强,只有Resize和Normalize,保证验证指标稳定。shuffle=False在验证集很重要,方便把预测结果与patient_id一一对齐。num_workers在Windows上可能遇到运行时错误,出现时先把它设成0。
3.3 模型选择:从ResNet到Transformer的实际取舍
乳腺图像分类最常见的起点是ImageNet预训练ResNet50。几千张数据规模下,微调预训练模型是稳定性最高的方案;如果只有几百张,应该考虑ResNet18,并提高正则强度。ViT这类Transformer图像分类模型需要更大数据量,通常数万张起步,否则收敛慢且泛化差。
import torch import torch.nn as nn from torchvision import models model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) # 冻结前两层,让低层特征保留通用纹理 for name, param in model.named_parameters(): if 'layer3' not in name and 'layer4' not in name: param.requires_grad = False num_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(num_features, 2) )冻结layer3之前的层是因为自然图像的边缘、纹理特征与病理图像有一定共性,而更深层语义差异大。Dropout(0.3)加在分类头前,对平稳小样本训练有明显帮助。如果显存充足,可以尝试解冻更多层,但解冻层越多,学习率应该越小。
3.4 训练循环:混合精度与学习率调度
训练循环的核心是三件事:混合精度、损失记录、学习率调度。下面是最简训练函数:
from torch.cuda.amp import autocast, GradScaler def train_one_epoch(model, loader, criterion, optimizer, scaler, device): model.train() running_loss = 0.0 for images, labels, _ in loader: images = images.to(device) labels = labels.to(device) optimizer.zero_grad() with autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() running_loss += loss.item() * images.size(0) return running_loss / len(loader.dataset)autocast负责在前向过程中自动选择FP16和FP32,GradScaler防止梯度下溢。如果你的显卡不支持混合精度,可以去掉autocast和scaler,直接调用loss.backward()和optimizer.step()。每epoch结束后,我还会用一个验证函数计算AUC,并把模型保存到文件。
4. 乳腺癌症图像分类的模型评估与调参优化
4.1 不只看准确率:混淆矩阵与敏感度
乳腺癌症二分类里,恶性样本经常只占10%到20%,全猜良性的准确率也能到80%以上。因此要同时观察混淆矩阵、敏感度和特异度。一个简单的评估函数:
from sklearn.metrics import confusion_matrix def evaluate(model, loader, device): model.eval() all_preds, all_labels, all_ids = [], [], [] all_probs = [] with torch.no_grad(): for images, labels, ids in loader: images = images.to(device) outputs = model(images) probs = torch.softmax(outputs, dim=1).cpu().numpy() preds = probs.argmax(axis=1) all_preds.extend(preds) all_labels.extend(labels.numpy()) all_ids.extend(ids) all_probs.extend(probs) cm = confusion_matrix(all_labels, all_preds) tn, fp, fn, tp = cm.ravel() sensitivity = tp / (tp + fn) specificity = tn / (tn + fp) return { 'cm': cm, 'sensitivity': sensitivity, 'specificity': specificity, 'probs': np.array(all_probs), 'ids': all_ids }probs保存softmax输出的类别概率,后面绘制ROC曲线时会用到。这个函数中cm.ravel()解包顺序只适用于二分类;多分类需要写成tn, fp, fn, tp = cm.reshape(-1)四段,但语义不同,自行替换。
4.2 训练曲线与早停:epoch到底设多少
epoch是检索时很常见的参数问题。在小数据集上,训练通常在第10个epoch左右就接近收敛,继续训练容易过拟合。我会设置一个早停机制,监控验证AUC:
best_auc = 0.0 patience = 5 bad_epochs = 0 for epoch in range(50): train_loss = train_one_epoch(...) val_metric = evaluate(model, val_loader, device) val_auc = roc_auc_score(val_metric['labels'], val_metric['probs'][:, 1]) if val_auc > best_auc: best_auc = val_auc torch.save(model.state_dict(), 'best_model.pth') bad_epochs = 0 else: bad_epochs += 1 if bad_epochs >= patience: print(f'Early stop at epoch {epoch}') breakpatience=5是指连续5个epoch AUC没有提升就停止。数据量小的时候我会改用3,使用余弦退火调度器时还会同时记录学习率变化。早停前保存了best_model.pth,之后加载这个文件用于评估。
4.3 超参数记录与ROC-AUC对比
调参过程中最容易犯的错误是忘记哪组参数对应哪个结果。我习惯把关键参数和指标写成一个表格,方便回溯:
| 实验 | 模型 | 学习率 | batch | 冻结层 | 增强 | AUC | 敏感度 |
|---|---|---|---|---|---|---|---|
| exp01 | ResNet50 | 1e-4 | 32 | layer3之后 | 基础 | 0.91 | 0.86 |
| exp02 | ResNet50 | 1e-4 | 32 | 全部微调 | 基础 | 0.90 | 0.88 |
| exp03 | ResNet50 | 1e-4 | 16 | layer3之后 | 强增强 | 0.93 | 0.90 |
从表格能看到,当前数据上“适度冻结 + 强增强”效果最好。记录实验可以直接用CSV,也可以接wandb或tensorboard,但建议每跑一个实验就把config.json一起存下来,否则后期很难还原。
绘制ROC曲线也简单,使用上面evaluate函数返回的probs:
from sklearn.metrics import roc_curve, auc fpr, tpr, _ = roc_curve(all_labels, all_probs[:, 1]) roc_auc = auc(fpr, tpr)AUC对类别不均衡不如准确率敏感,因此更适合乳腺癌症筛查场景。如果只看准确率,很可能会选择一个对恶性样本召回率极低的模型。
5. 乳腺癌症图像分类的实际部署技巧与坑
5.1 用置信学习清洗错标样本
病理数据集常存在标注偏差。如果训练集里混了错标样本,模型会学到错误特征。我会用cleanlab做一次置信学习筛选:
from cleanlab.filter import find_label_issues issues = find_label_issues(labels, probs, return_indices_ranked_by='self_confidence') print(train_df.iloc[issues[:20]][['patient_id', 'label']])筛选出的样本不一定错标,但值得病理医生复核。医学项目里最忌讳自己“修正”标签,这会把模型推向确认偏误。数据清洗只能在训练集上进行,验证集和测试集必须保持原样。
5.2 按患者聚合patch预测
实际应用时,一个患者会有多个图像或patch。我不会只取单个patch的预测,而是把该患者所有patch的概率取平均:
import pandas as pd patient_probs = pd.DataFrame({ 'patient_id': ids, 'prob': probs[:, 1] }).groupby('patient_id')['prob'].mean()平均法比最大法更稳,最大法会被一个异常patch带偏。如果患者档案里同时有左右乳腺图像,最好按左右侧分别聚合,避免混入对称组织信息。
5.3 用学习率查找器快速定位初始学习率
调参最花时间的是找学习率。我常用学习率查找器:让学习率从极小值线性增长,观察损失曲线下降最快的点。PyTorch可以使用LambdaLR逐步放大学习率,跑一个epoch后画loss曲线。曲线最低点附近往往就是合适的初始学习率。这个技巧对迁移学习特别有用,能避免盲目使用1e-4导致的收敛过慢。
乳腺癌症图像分类做到这个程度,模型层面已经没有秘密,真正影响上线效果的是数据链路是否干净、验证是否按患者隔离、评估指标是否贴合临床筛查需求。下一步建议把注意力放在数据清洗和按患者评估上,从这两处重新跑一遍实验。
本文还有配套的精品资源,点击获取