简介:这是面向深度学习和医学图像分类场景的乳腺癌症图像二分类数据集,适用于需要训练卷积神经网络、进行迁移学习或验证分类算法的研究者和开发者,目前已有286人学习使用。类别由JSON类别文件定义,图像按训练集、验证集、测试集目录存放,训练集约480张、验证集约140张、测试集约70张,便于直接划分训练与评估。资源包共692个文件,以689张JPG图像为主,另附1个JSON类别文件、1个Python脚本和1个PNG示意文件,整体约17.85MB。JSON文件帮助快速读取类别映射,Python脚本可辅助数据加载与预处理,PNG示意文件则用于查看目录结构或样例说明。使用者下载后即可获得一套组织清晰、可直接用于深度学习实验的乳腺癌症图像数据,省去自行采集、清洗和标注的繁琐流程,能快速开展模型训练、效果对比和分类性能验证,适合课程设计、毕业设计及科研入门使用。
1. 乳腺癌图像分类数据集上手:先看清东西再谈训练
「乳腺癌图像分类」听起来像个标准任务,但真拿一份只有 690 张左右的图像分类数据集动手时,你很快会发现,深度学习最考验人的地方不在前向传播,而在数据有没有被正确解读。这份资源是一个已经按目录分好类的乳腺癌症二分类数据集:训练集约 480 张、验证集约 140 张、测试集约 70 张,类别个数为 2,具体类别名以随包附带的 JSON 类别文件为准。它解决的是「拿到一份可复现的图像分类数据集,不用自己清洗和划分」的诉求,适合两类人:一是正在跑图像分类全流程入门实践的初学者,二是需要在课题或算法验证里快速评估迁移学习、数据增强、伪标签等思路的从业者。下面按我实际拆包和复现的顺序,把目录结构、预处理参数、训练代码与避坑点完整过一遍。
2. 读懂这份数据集:目录组织、Roboflow 命名与 JSON 标签
2.1 目录组织方式:先弄清楚三个文件夹各自装什么
拿到压缩包先别急着解压训练,第一步是打印目录结构。这个数据集保留了「按类别分文件夹」的导出结构,和那种「一张 manifest.txt 配一堆散图」的扁平数据集完全不一样。
import os root = './breast_cancer_cls' for split in ['train', 'valid', 'test']: split_path = os.path.join(root, split) if not os.path.isdir(split_path): print(f'[缺失] {split_path}') continue for class_name in sorted(os.listdir(split_path)): class_path = os.path.join(split_path, class_name) if os.path.isdir(class_path): n = len([f for f in os.listdir(class_path) if f.lower().endswith('.jpg')]) print(f'{split:>5} / {class_name:>8} : {n:>4} 张')这段代码会输出类似train / cancer : 260的分布表。为什么第一步就做这件事?因为「训练集约 480 张」是一个总数量,两个类别各自多少必须亲眼确认。如果打印出来发现 cancer 只有 120 张、normal 有 360 张,那就是明显的不平衡,后面损失函数就不能默认用不带权重的 CrossEntropyLoss。
我习惯把扫描结果直接存成 manifest.csv,后面加载数据统一走这个清单,而不是每次重新读目录。这样既避免了 Windows 和 Linux 路径分隔符带来的差异,也能让 DataLoader 在训练中断续跑的时候行为完全一致。
| 路径 | 含义 | 使用建议 |
|---|---|---|
| train/cancer/ | 癌症类样本,参与梯度更新 | 先统计数量,决定是否做类别加权 |
| train/normal/ | 正常类样本,参与梯度更新 | 同上 |
| valid/cancer/ | 验证样本,只用于选模型和调参 | 绝不参与训练,包括 BatchNorm 统计 |
| valid/normal/ | 验证样本,正常类 | 只在验证阶段访问 |
| test/ | 测试样本,最终统一评测一次 | 训练过程中一次都不碰 |
2.2 文件名拆解:_png_jpg.rf.后缀在说什么
文件名在训练脚本里可能只是一个 path 字符串,但放到真实工作流里,它是追踪数据血缘的唯一线索。以1877249993_png_jpg.rf.f195e5e3c8d3964bf27194ac97ec7d32.jpg为例,拆开看有三层含义:
前面1877249993是源图在标注平台内部的 ID,相当于「源图主键」。中间_png_jpg表示原始上传是 PNG 格式,导出时转成了 JPG,这个信息在 OpenCV 读取时没有任何影响,但它在提醒你:这份数据集里的部分图像经历过有损压缩,如果后续做图像质量过滤,这个字段是一个可参考的信号。最后的.rf.加 32 位十六进制是这条数据在数据集导出时的唯一哈希,适合当作行级去重键。
我在项目里只用这串 hash 做去重。原因很简单:Roboflow 系数据集经常出现同一张源图被导出两次的情况,两个.rf.哈希不同但源图 ID 相同,下一步数据清洗时可以用源图 ID 去重,而不是用文件名整体去重。
2.3 类别文件读法:把标签写死在代码里是给自己留坑
项目描述里明确写着「具体类别参考 json 类别文件」。这类导出数据的标注信息可能叫_classes.json,也可能叫data.yaml,不同工具命名习惯不同,核心都是一个「类别名 → 索引」的映射。我的做法是先解析成字典,再传给训练脚本,而不是在代码里写死0代表癌症、1代表正常。
import json with open('./breast_cancer_cls/classes.json') as f: label_map = json.load(f) print(label_map) # 输出示例:{'cancer': 0, 'normal': 1} class_names = [name for name, idx in sorted(label_map.items(), key=lambda x: x[1])] print(class_names)这里有个细节值得说:JSON 的键顺序在 Python 3.7 之后虽然是插入序,但不同来源的 JSON 文件键顺序可能不一样,所以取class_names时一定要按 value 排序,而不是直接依赖字典的遍历顺序。后面画混淆矩阵、写分类报告时,都靠这个排序保证类别名和索引始终对得上。
3. 数据预处理与加载:把目录结构变成可复现的训练管线
3.1 用清单文件固化数据集
有了目录扫描结果,下一步就是把 train/valid/test 三套数据全部固化成一份 CSV。这个文件会成为训练管线的唯一数据入口。
import json, csv from pathlib import Path root = Path('./breast_cancer_cls') with open(root / 'classes.json') as f: label_map = json.load(f) rows = [] for split in ['train', 'valid', 'test']: for class_name, label in label_map.items(): class_dir = root / split / class_name if not class_dir.exists(): continue for img_path in sorted(class_dir.glob('*.jpg')): rows.append({ 'split': split, 'path': str(img_path), 'label': label, 'class_name': class_name, }) with open('manifest.csv', 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=['split', 'path', 'label', 'class_name']) writer.writeheader() writer.writerows(rows) print(f'共 {len(rows)} 条样本,已写入 manifest.csv')这里有三件事值得展开。第一,glob('*.jpg')只收 JPG 后缀,解压过程中如果混入 PNG 或者损坏的 JPG,后面我会用PIL.Image.verify()再统一校验一次。第二,class_name和label同时存下来,是为了最后画混淆矩阵时能直接显示可读的类别名,而不是 0 和 1。第三,CSV 里的路径在 Windows 下是反斜杠,在 Linux 下是正斜杠,后续代码统一用pathlib.Path处理,避免在字符串拼接时踩分隔符的坑。
3.2 预处理参数:尺寸、归一化、增强怎么设才不翻车
这一节是整个预处理管线里最容易翻车的地方。先给一组「不保证最优但能跑得很稳」的参数,再逐个解释为什么这么设。
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ]) eval_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ])输入尺寸设为 224×224 是第一个要点。ResNet 这类迁移学习模型的 ImageNet 预训练权重,其卷积核设计就是为 224 附近的输入服务的,不按这个尺寸走,预训练特征的空间结构就承接不上。第二个要点是Resize(256)后接RandomResizedCrop(224),而不是直接Resize(224):前者等价于随机裁剪,相当于免费给模型加了一种尺度变换,比简单缩放更能抵抗小数据集的过拟合。第三个要点是Normalize必须沿用 ImageNet 的均值标准差,因为预训练权重是在这个归一化分布上收敛的,换成自算的统计量反而会破坏预训练特征。
注意:这份乳腺癌症图像数据如果以灰度纹理为主,建议去掉ColorJitter这类彩色扰动,或者把强度压得很低。灰度结构对颜色抖动非常敏感,随便调 hue 和 saturation 就可能把对比度信息洗掉,这一点在第 5 章会专门展开。
3.3 保持官方划分:别拿验证集和测试集去凑训练量
480/140/70 的划分是导出时就定好的,我强烈建议不要为了凑训练量把验证集或测试集重新混进去。验证集的作用是选模型、调超参,测试集的作用是最终评测,两套数据的分布一旦被打乱,后面跑的指标就失去了对比价值。
配套的 Dataset 封装可以这样写:
import pandas as pd from PIL import Image from torch.utils.data import Dataset class ImageListDataset(Dataset): def __init__(self, df, transform=None): self.df = df.reset_index(drop=True) 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 is not None: img = self.transform(img) return img, int(row['label']) train_df = pd.read_csv('manifest.csv') train_df = train_df[train_df['split'] == 'train'] train_ds = ImageListDataset(train_df, train_transform) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4)Image.open之后必须convert('RGB'),因为数据集里可能混入灰度 PNG 转出的 JPG,不做三通道统一,PyTorch 在 batch 拼接时会直接报 shape 不一致。shuffle=True只放在训练集上,验证和测试的 DataLoader 保持shuffle=False,这样每次评估顺序稳定,调试时对比结果更方便。
4. 用 ResNet18 迁移学习跑出第一个准确率:完整训练脚本解读
4.1 为什么选 ResNet18:小数据集的默认开局
面对 690 张图像的二分类任务,我一般不会从头训练一个深层 CNN,这几乎等于让模型在黑匣子里重新发明视觉特征。常见做法是迁移学习:使用在 ImageNet 上预训练的 ResNet18,替换最后的全连接层,然后微调。ResNet18 在「特征表达能力」和「过拟合风险」之间是最稳的平衡点,ResNet50 在小数据集上反而更容易把训练集的纹理细节背下来,验证集准确率却纹丝不动。
还有一个实际考量:ResNet18 即使只用 CPU 也能在十几分钟内跑完一个 epoch 的验证,这对调参阶段的快速迭代非常友好。如果后续发现模型容量不够,再换 EfficientNet-B0 或 ResNet34 也不迟。
4.2 完整训练脚本骨架:分组学习率与余弦退火
import torch import torch.nn as nn from torchvision import models device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 2) model = model.to(device) optimizer = torch.optim.AdamW([ {'params': model.fc.parameters(), 'lr': 1e-3}, {'params': [p for n, p in model.named_parameters() if 'fc' not in n], 'lr': 1e-5}, ], weight_decay=5e-4) criterion = nn.CrossEntropyLoss() scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=30, eta_min=1e-6 ) for epoch in range(30): model.train() total_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() print(f'epoch {epoch+1:02d} / loss {total_loss / len(train_loader):.4f}')这段代码的核心是分组学习率:新替换的fc层用 1e-3 从零学起,预训练主干用 1e-5 做微调,避免一上来就把 ImageNet 学到的底层边缘纹理特征冲掉。weight_decay=5e-4是调参时最先尝试的正则化强度,在小数据集上能明显抑制过拟合。CosineAnnealingLR配合T_max=30,让学习率在 30 个 epoch 内从初始值平滑降到 1e-6,相比每 10 个 epoch 直接除以 10 的 StepLR,曲线更顺滑,也省去反复试下降节点的精力。
| 超参数 | 取值 | 为什么这么定 |
|---|---|---|
| batch_size | 32 | 480 张训练图每轮约 15 步,BN 统计量更新足够稳定 |
| fc 层学习率 | 1e-3 | 新分类头没有预训练,需要更大的更新步长 |
| 主干学习率 | 1e-5 | 预训练特征只做微调,学习率过大会灾难性遗忘 |
| weight_decay | 5e-4 | 几百张图的规模下,L2 正则是最便宜的防过拟合手段 |
| T_max | 30 | 训练总 epoch 设为 30,与余弦周期对齐,末尾自动收敛 |
4.3 训练中的观察点:怎么判断模型「真的在学」
我判断一个训练是否正常的习惯,是同时盯三个信号:训练 loss 是否在下降;验证集准确率是否在最初几个 epoch 内就从随机水平(约 50%)爬升;以及验证集的 loss 是否在训练 loss 降到 0.1 附近时还保持在 0.4 以上。第三个信号尤其关键。
如果训练 loss 已经降到 0.03,验证 loss 还在 0.5 上下震荡,说明模型开始背训练集了。此时先做两件事:一是把主干学习率再降一个量级,二是把增强中的随机裁剪 scale 下界从 0.7 调到 0.6,从数据侧增加难度。都不要同时做,一次只变一个变量,否则你根本分不清是哪个改动带来的收益。训练结束后,把验证集上准确率最高的 checkpoint 单独保存成best_model.pt,而不是用最后一个 epoch 的权重,这是最朴素的早停手段。
5. 避坑与排查:五件这个数据集会翻车的事
5.1 模型预测全落在某一类上:准确率卡在比例基线
现象:训练了 20 个 epoch,验证准确率一直躺在 55%~62% 不动,查看预测结果发现模型把几乎所有样本都判成「normal」,cancer 类几乎全军覆没。
原因:两个类别的样本数量不一致,默认的 CrossEntropyLoss 在类别不平衡时会偏向学「全猜多数类」这种懒策略。二分类在阈值 0.5 附近尤其容易被这种假象骗到。
解决:先在 manifest.csv 里统计每类数量,给损失函数加类别权重。常见做法是weight = n_samples / (n_classes * n_class_samples),然后传入CrossEntropyLoss(weight=torch.tensor([w0, w1]))。如果训练已经跑完,也可以不动损失函数,直接在验证集上遍历阈值 0.45~0.55,选 F1 最高的那个阈值作为最终决策线。
5.2 训练 loss 接近 0,验证集准确率却反复震荡
现象:训练 loss 一路跌到 0.03,看起来一切正常,但验证准确率在 68%~72% 之间来回跳,怎么都不往上走。
原因:典型过拟合。模型把训练集里不变的背景纹理、拍摄亮度当成了判别特征,到了验证集这些伪特征一变,预测就乱了。690 张图的规模对 ResNet18 来说本来就偏少,不做够强的增强,过拟合几乎是必然。
解决:把增强力度加上去,重点改RandomResizedCrop的scale下界,比如从(0.7, 1.0)放宽到(0.55, 1.0),让每次裁剪差异更大;同时给CrossEntropyLoss加label_smoothing=0.1,把「硬标签 0/1」换成软标签,模型就不会把置信度押得太满。这个组合在小数据集上通常能立刻看到验证曲线从震荡变成单边爬升。
5.3 打开 ColorJitter 后训练 loss 变大:增强也可能帮倒忙
现象:在预处理里加入了ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3),训练 loss 从 0.4 涨到 1.0 以上,验证准确率不升反降。
原因:这份乳腺癌症图像很多是从灰度源转出来的,颜色通道本身信息量很弱。ColorJitter 对 saturation 和 hue 的随机扰动,等于在图像里注入噪声,模型反而学不到稳定特征。
解决:按灰度优先的思路处理,把颜色类增强全部去掉,只保留RandomRotation(10)和RandomHorizontalFlip这类几何增强;如果确实想加,只保留亮度扰动,强度压在 0.15 以内。这也是乳腺影像和自然图像在预处理上最大的差别。
5.4 验证集准确率比训练集高:一个容易误读的信号
现象:训练集准确率 76%,验证集反而有 81%,看起来「模型泛化得很好」,但测试集一跑又回落到 72%。
原因:验证集和训练集在类别比例上的随机差异导致曲线波动;更隐蔽的原因是验证集和测试集虽然都来自同一批导出,但拍摄条件或预处理细节不完全一致,验证集恰好分布在模型擅长的那部分空间里。690 张图的小规模下,这个现象不需要过度惊慌,但它提醒你:别被某个单次验证分数绑架。
解决:训练期间不频繁用测试集,而是固定一个验证集用于模型选择;最终只用测试集的结果下结论。同时观察验证集 loss 而不是只看准确率,准确率对概率校准不敏感,loss 能反映更多预测置信度的变化。
5.5 重新划分导致的数据泄漏:一个必须反复强调的坑
现象:训练结果在验证集上刷到 92%,换到另一台机器复现同一份数据,准确率掉到 75%。
原因:有人为了「充分利用数据」,把验证集或测试集合进训练集重新洗牌。验证集和测试集的任务本来就是扮演「从没见过的数据」,一旦参与训练,所有后续指标都失去意义,跨机器复现时自然露馅。
解决:只使用官方划分,保持 train/valid/test 三层结构不变。如果嫌训练数据少,正确做法是第 6 章要讲的伪标签自训练,而不是偷偷合并数据集。我自己的习惯是每次训练前先跑一遍目录扫描脚本,确认三个文件夹的图片数量与项目描述一致,再动手。
6. 榨干 690 张图:伪标签自训练与置信度阈值
训练完基线模型后,690 张训练图显然不够满意。与其去网上再找一份来路不明的数据合并,我更喜欢先用伪标签自训练把测试集的剩余价值挖掘出来,整个过程不需要额外的标注成本。
第一步,用训练好的best_model.pt对测试集做推理,得到 softmax 概率向量。只把最大概率 ≥ 0.95 的样本挑出来,打上伪标签。置信度阈值很关键:定高了挑不出几张图,定低了会把错误标签混进训练集。0.95 是我在二分类小数据集上的默认起点。第二步,把这些高置信伪标签样本追加到训练集尾部,把训练载体的train_loader重建一次,用很保守的学习率继续微调:主干 1e-6、fc 层 1e-4,训练 10~15 个 epoch,每个 epoch 结束后都回到验证集上检查分数。
第三步是迭代控制。每轮微调后记录验证集准确率,如果相比之前的最优值没有提升,直接丢弃这一轮的新伪标签,停止迭代;如果提升了,可以尝试把阈值从 0.95 降到 0.90 再试一轮。这里最忌讳的就是贪心,一次把全部测试集都吞进训练集,错误的伪标签会直接把模型带偏。我在实际项目里每轮只接受几十张高置信样本,迭代两到三轮后收益就会明显衰减,这时候就该停了。
顺便提醒一句:这类乳腺癌症图像分类数据集是算法研究和教学复现的载体,不宜直接当作临床诊断依据,训练出的模型要做落地评估,必须拿到目标设备上的真实数据重新验证。从那以后,我每次拿到小规模分类数据集都会强制走一遍固定流程:官方划分不动、增先从几何出发、迁移学习起步、伪标签只吃高置信样本。这套流程帮我挡掉了好几次「模型刷分很高但一部署就翻车」的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取