简介:这份数据资源专为深度学习图像分类任务设计,聚焦乳腺癌症影像的自动识别场景,适合计算机视觉初学者、医工交叉研究者及需要标准数据集的算法工程师使用。数据集已完成类别整理与训练/验证/测试划分,共包含2个类别(具体类别可参考随包的json类别文件),其中训练集约480张、验证集约140张、测试集约70张,可支撑完整的图像分类模型训练与评估流程。资源包共692个文件,主体为689张jpg图像,附有类别说明json、数据预览png及用于数据加载或预处理的py脚本,压缩包约17.85MB,轻量易下载,目录按类别与集合划分清晰,方便接入PyTorch、TensorFlow等框架。已有286人下载学习,适合快速搭建乳腺癌症图像分类基线实验、验证模型效果,也可作为课程设计与毕业设计的数据基础。
1. 一份 480 张训练图的乳腺癌症二分类数据集,够不够跑深度学习?
做深度学习图像分类,最容易被卡住的往往不是模型,而是数据。很多公开数据集动辄几万张,下载下来发现标注格式五花八门,还得花大量时间清洗。这份乳腺癌症图像分类数据集属于小而精的类型:一共 2 个类别,训练集 480 张左右、验证集 140 张左右、测试集 70 张左右,按目录归档,配套 JSON 格式的类别配置文件。它覆盖了图像分类任务的标准环节——数据读取、划分、预处理、训练、评估,适合作为深度学习入门或迁移学习的实战素材。
我拆这个数据集时,最先做的一件事不是摆模型,而是搞清楚目录结构、类别映射和样本分布。因为这些直接决定后面的数据和模型能不能对上号。如果你正准备拿它复现一个二分类训练流程,或者想用它练手 ResNet、EfficientNet 这类常见架构,这篇笔记应该能帮你少走几步弯路。接下来我会把从数据集解压到训练出第一个模型的完整路径捋一遍,包括参数怎么设、坑在哪里。
2. 先读懂目录结构:类别映射与数据划分是第一步
拿到数据集后不要急着开训,先把目录结构和标注文件摸清楚。这个数据集的命名和归档方式很有代表性,值得花几分钟理清。
2.1 文件命名与目录归档规则
数据集的图片文件名长这样:1877249993_png_jpg.rf.f195e5e3c8d3964bf27194ac97ec7d32.jpg。这种格式是 Roboflow 导出数据集时常见的命名规则——原始文件名加.rf.再加一串哈希值,目的是防止重名,路径信息被哈希替代。好处是不会因为目录迁移导致文件名冲突,坏处是光看文件名猜不出类别,必须依赖目录结构或标注文件。
按摘要描述,数据集按目录保存,相同类别的数据放在同一个目录下,目录结构大致是:
breast_cancer_dataset/ ├── train/ │ ├── cancer/ │ │ └── *.jpg │ └── normal/ │ └── *.jpg ├── valid/ │ ├── cancer/ │ │ └── *.jpg │ └── normal/ │ └── *.jpg ├── test/ │ ├── cancer/ │ │ └── *.jpg │ └── normal/ │ └── *.jpg └── classes.jsonclasses.json里存的是类别索引与类别名的映射,一般长这样:{"0": "cancer", "1": "normal"}或者反过来。我读取这类 JSON 时习惯显式指定编码,避免在 Windows 下用默认编码读到乱码。
import json with open('classes.json', 'r', encoding='utf-8') as f: class_map = json.load(f) print(class_map) # 输出示例:{'0': 'cancer', '1': 'normal'} # 注意:正式类别名以你实际解压出的 JSON 内容为准这段代码的目的只有一个,就是先确认类别索引和类别名的对应关系。后续写 Dataset 时,类别索引直接从目录名或标注里取,不需要人工硬编码。如果 JSON 里的键是字符串类型,传给 PyTorch 做交叉熵损失时记得转成 int。
2.2 训练集、验证集、测试集的角色分工
数据集把图片拆成了三份:训练集 480 张左右、验证集 140 张左右、测试集 70 张左右。这个比例大约是 7:2:1,是一个很常见的小数据集划分方式。
- 训练集:用于更新模型权重,模型从这里学习类别的判别特征。
- 验证集:用于训练过程中评估模型表现、调整超参数,比如学习率、早停轮次。
- 测试集:训练结束后做最终评估,模拟模型在未见数据上的表现。
很多人会把验证集和测试集混用,这是一个常见的错误。验证集参与过调参,模型对它多多少少有过拟合,用它做最终指标会虚高。这个数据集专门留出测试集,就是为了让你在全部调参结束后再碰它。
我拿到数据后会先统计三个目录下的图片数量,确认和描述一致,再顺手看一下两个类别的样本均衡度。
import os from glob import glob for split in ['train', 'valid', 'test']: for class_name in ['cancer', 'normal']: path = f'breast_cancer_dataset/{split}/{class_name}' count = len(glob(os.path.join(path, '*.jpg'))) print(f'{split}/{class_name}: {count} 张')对于 690 张左右的小数据集,类别不均衡的情况很常见。如果发现某一类比另一类多很多,后面训练时就要考虑类别加权损失或者数据增强,不能直接硬训。
3. 用 PyTorch 把图片文件变成可训练的数据集
目录结构摸清之后,下一步就是把磁盘上的 JPG 文件变成 PyTorch 能吃的 Dataset 和 DataLoader。这一步的核心是让图片路径、标签索引、图像张量三者对齐,后面训练才不会出张冠李戴的问题。
3.1 自定义 Dataset 类的实现
虽然torchvision.datasets.ImageFolder可以直接按子目录读取图片,但自定义 Dataset 能让你更清楚地看到每条数据是怎么被读进内存的,调试时也不至于抓瞎。对于 700 张不到的图片量,训练之前全部读进内存也占不了多少空间,但为了保留处理大数据的习惯,我还是按流式读取来写。
import torch from torch.utils.data import Dataset from PIL import Image import os import json class BreastCancerDataset(Dataset): def __init__(self, data_dir, class_map_path, transform=None): self.transform = transform self.image_paths = [] self.labels = [] with open(class_map_path, 'r', encoding='utf-8') as f: class_map = json.load(f) # 假设 class_map 形如 {'0': 'cancer', '1': 'normal'} self.class_to_idx = {v: int(k) for k, v in class_map.items()} for class_name, idx in self.class_to_idx.items(): class_dir = os.path.join(data_dir, class_name) if not os.path.isdir(class_dir): continue for filename in os.listdir(class_dir): if filename.lower().endswith(('.jpg', '.jpeg', '.png')): self.image_paths.append(os.path.join(class_dir, filename)) self.labels.append(idx) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = Image.open(self.image_paths[idx]).convert('RGB') label = self.labels[idx] if self.transform: image = self.transform(image) return image, label这个 Dataset 的核心逻辑在__init__里:先用 JSON 建立类别名到索引的映射{'cancer': 0, 'normal': 1},再遍历每个类别的目录,把图片路径和标签成对存进列表。__getitem__只做三件事——打开图片、取标签、做变换。convert('RGB')是必须的,因为部分 JPG 可能是灰度图或带透明度,统一转成三通道避免后续 Tensor 维度不一致。
3.2 训练前的预处理:尺寸统一与归一化
病理图像最常见的坑是尺寸和长宽比不统一。乳腺病理切片缩略图有的接近正方形,有的明显是矩形,如果直接随机裁剪,可能会丢掉关键区域。我这里采用两步走的预处理策略:先 Resize 到较小一边,再做 CenterCrop 得到正方形输入,最后归一化。
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=10), 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]) ]) valid_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])这里的mean和std直接沿用 ImageNet 的统计值,这是迁移学习的常见做法。如果从零训练而不是微调预训练模型,就需要重新统计数据集自身的均值和方差,否则归一化没有意义。Resize 先统一到 256×256,既避免信息丢失,又给随机增强留了一点空间。彩色转 RGB 后做归一化,是为了让输入值落在接近标准正态分布的区间内,模型训练会更稳。
接下来把三个目录分别包装成 Dataset,再通过 DataLoader 加载。数据量小,batch_size可以适当调大一点。
from torch.utils.data import DataLoader train_dataset = BreastCancerDataset( data_dir='breast_cancer_dataset/train', class_map_path='classes.json', transform=train_transform ) valid_dataset = BreastCancerDataset( data_dir='breast_cancer_dataset/valid', class_map_path='classes.json', transform=valid_transform ) test_dataset = BreastCancerDataset( data_dir='breast_cancer_dataset/test', class_map_path='classes.json', transform=valid_transform ) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2) valid_loader = DataLoader(valid_dataset, batch_size=32, shuffle=False, num_workers=2) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=2)shuffle=True只在训练集上开,验证和测试阶段保持顺序不变,方便后面计算混淆矩阵时和预测结果一一对应。如果机器是 Windows,num_workers别设太高,2 或者 4 就够,设太多反而可能报错。
4. 训练一个乳腺癌症分类模型:模型选择与参数设定
数据管道跑通之后,终于可以开始训练了。对于 480 张训练图的数据规模,我的建议是用预训练模型做迁移学习,而不是从零训练。深层网络在小数据上很容易过拟合,而 ImageNet 预训练权重已经提供了很好的底层特征。
4.1 为什么选 ResNet18 而不是更深的网络
常见的选择包括 ResNet18、ResNet50、EfficientNet-B0。对于 480 张图,ResNet18 参数量约 1120 万,结构简单,不容易把训练集整个背下来。ResNet50 更深更强,但对这个数据量来说有些浪费,而且训练时间翻倍。EfficientNet-B0 精度不错,但输入分辨率更大的话显存占用也更大。
我一般首选 ResNet18 做基线,理由很朴素:训练快、易收敛、跑出来的结果最有参考性。如果你之前跑过别的模型,想刷新准确率,再换成 ResNet50 或 EfficientNet 也不迟。用 torchvision 加载预训练 ResNet18 时,需要把最后一层全连接从 1000 类换成 2 类。
import torch.nn as nn from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_features = model.fc.in_features model.fc = nn.Linear(num_features, 2)替换全连接层后,分类输出从 1000 维变成 2 维,对应 cancer 和 normal 两个类别。如果显存充足,可以把所有层都解冻微调;如果怕过拟合,可以先冻结 backbone,只训练全连接层跑几个 epoch 再解冻。
4.2 训练参数与完整训练循环
参数方面,批量大小设为 32,学习率 1e-4(微调场景),优化器用 Adam,损失函数交叉熵。类别不平衡的话,可以给交叉熵加weight,把少数类的权重调高。
import torch.optim as optim epochs = 30 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) model.to(device) for epoch in range(epochs): model.train() running_loss = 0.0 correct = 0 total = 0 for images, labels in train_loader: images = images.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() train_loss = running_loss / total train_acc = correct / total # 验证 model.eval() val_loss = 0.0 val_correct = 0 val_total = 0 with torch.no_grad(): for images, labels in valid_loader: images = images.to(device) labels = labels.to(device) outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) val_total += labels.size(0) val_correct += (predicted == labels).sum().item() val_loss = val_loss / val_total val_acc = val_correct / val_total print(f'Epoch {epoch+1}/{epochs} | ' f'Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | ' f'Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}') scheduler.step()这里有几个参数值得解释一下。step_size=10, gamma=0.1表示每 10 个 epoch 学习率乘以 0.1,让训练后期步长变小,有利于收敛。optimizer.zero_grad()必须在每个 batch 前调用,否则梯度会累加。model.eval()和torch.no_grad()一起用,才真正把 Dropout 等随机行为关掉。
训练过程中重点盯两个信号:训练损失是否持续下降、验证准确率是否同步提升。如果训练损失降得很快但验证准确率停滞甚至下跌,典型的过拟合信号,此时应该加数据增强、加 dropout,或者提前结束训练。
5. 乳腺图像分类训练中的常见问题与避坑
这个数据集本身不大,但小数据集往往坑更多。以下是我在实际复现过程中遇到的几个典型问题,按现象到原因到解决的思路整理。
5.1 验证准确率比训练准确率高一大截
现象:训练集准确率 92%,验证集准确率却有 97%,训练损失也偏高。
原因:数据划分的随机性导致验证集恰好比训练集简单——小数据集里这种概率很高。另外,训练集里做了 RandomHorizontalFlip 和 ColorJitter 之类的增强,等于把任务变难了,而验证集只做了缩放和归一化,相对简单。
解决:先看一下两个集合的类别分布是否一致。如果一致,通常不用太担心,属于正常现象。如果分布不一致,重做划分,用train_test_split的stratify参数按类别比例分层抽样。
5.2 图片读出来颜色偏暗或发绿
现象:用 PIL 打开 JPG 图片,发现颜色和预览软件里看到的明显不同,色调偏暗。
原因:这类病理图像大多是染色切片图,颜色本身就是诊断特征。如果图片是灰度保存的,而 Dataset 里convert('RGB')强行复制了三通道,模型会看到三个相同的灰度通道,等于给模型输入了无效的重复信息。
解决:读取图片后先检查通道数,判断是灰度还是 RGB。如果是灰度图,就不要强行做三通道归一化,直接按单通道走,或者在预处理里做灰度到 RGB 的合理映射。我的习惯是写一个小脚本批量检查图片的模式:
from PIL import Image from glob import glob paths = glob('breast_cancer_dataset/**/*.jpg', recursive=True) modes = set() for p in paths: with Image.open(p) as img: modes.add(img.mode) print(modes) # 输出可能是 {'RGB', 'L'}如果同时出现RGB和L两种模式,预处理阶段必须统一转成 RGB,否则同一个 DataLoader 里会出现张量维度不一致的报错。
5.3 训练损失下降但验证集指标抖动剧烈
现象:每个 epoch 的验证准确率忽高忽低,比如上一轮 0.88,这一轮 0.82,下一轮又 0.90。
原因:验证集只有 140 张左右,每 32 张一个 batch,总共不到 5 个 batch。任何一个 batch 里多预测错一两张,准确率就会波动好几个百分点。
解决:看趋势而不是看单点。我的习惯是每训练完保存一个最佳模型,以验证集上表现最好的一次为准;同时把验证集也做成多次采样评估,或者直接用测试集做最终对比。不要因为某一次验证准确率下跌就急着调整模型结构。
5.4 换台电脑后 JSON 读取失败
现象:代码在自己机器上跑没问题,换到服务器或另一台 Windows 电脑上,读 classes.json 直接报编码错误。
原因:绝大对数情况是文件编码不统一,比如在 Windows 上被保存成 GBK,在 Linux 上默认按 UTF-8 读取就崩了。
解决:读取时显式指定encoding='utf-8',同时用errors='ignore'做兜底。更稳妥的做法是提前把 JSON 文件统一转成 UTF-8 编码,省得后面每次都要考虑平台差异。
5.5 测试集准确率比验证集低 5% 以上
现象:验证集准确率 0.93,测试集一测只有 0.87,差异超过统计波动。
原因:最常见的两个原因,一是验证集在调参过程中被反复参考,模型对验证集产生了隐式过拟合;二是训练过程中使用的最佳模型是以验证集指标为选取标准的,测试集作为完全没见过的数据,指标略低是正常的。
解决:不要一上来就反复调参。先用默认参数跑通一遍,记录测试集指标,再考虑调整。最终对外报告的数字一律以测试集为准,验证集只用来做训练过程中的模型选择。
6. 从准确率到诊断价值:用混淆矩阵和单图推理做最终验证
准确率只是第一步。对于医学图像分类,真正有参考价值的是混淆矩阵、精确率、召回率,以及单张图片的推理过程。这个数据集最好的一点是提供了独立的测试集,可以完整跑一遍模型评估。
测试集评估时,分类阈值默认是 0.5,但对于二分类医学诊断,往往需要根据临床场景调整阈值。如果你更关心不漏掉癌症患者,就选择召回率更高的阈值;如果更关心不要误报,就选择精确率更高的阈值。我用测试集计算完整指标的代码如下:
from sklearn.metrics import confusion_matrix, classification_report all_preds = [] all_labels = [] model.eval() with torch.no_grad(): for images, labels in test_loader: images = images.to(device) labels = labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) print('Confusion Matrix:') print(cm) print(classification_report(all_labels, all_preds, target_names=['cancer', 'normal']))混淆矩阵的四格分别对应真阴、假阳、假阴、真阳。对于乳腺癌筛查,假阴性是最不愿意看到的——明明是癌症被诊断为正常,后果比其他误判严重得多。所以看指标时,我不只看准确率,还会专门看 cancer 这个类别的召回率。
单图推理函数也值得提前写好,方便后续对任意一张新图片做预测,而不必每次都重新走训练循环:
def predict_single_image(image_path, model, transform, device): image = Image.open(image_path).convert('RGB') tensor = transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): outputs = model(tensor) probs = torch.softmax(outputs, dim=1) pred = torch.argmax(probs, dim=1).item() return pred, probs.cpu().numpy()[0]这个函数返回预测类别索引和两个类别的概率分布,适合做单样本验证。我的习惯是每次训练完模型,从测试集里随机挑 6-8 张图,把原图、真实标签、预测标签、概率值拼成一张对比图肉眼检查,确认模型不是靠图片背景之类的无关特征蒙对的。这对医学图像尤其重要,因为数据量小,模型很容易学到捷径。
从那以后,我每次拿到新的数据集,都会强制走一遍流程:读 JSON 确认类别映射、统计各类别数量、检查图片模式和尺寸是否统一、确认 train/valid/test 三份划分的类别分布一致,然后才开始训练。为了这套流程,我还专门写过一个小脚本,输入目录就能自动打印数据集的统计摘要,省得每次手动查。这个小习惯救了我很多次,尤其是从网上下载的数据集,数据分布和描述对不上是常态。希望这篇拆解能帮到正在做乳腺癌症图像分类的你,也欢迎在实际复现过程中多关注数据本身,毕竟模型只是工具,数据质量才是决定上限的东西。
本文还有配套的精品资源,点击获取