简介:面向深度学习图像分类任务,这份乳腺癌症图像分类数据集提供了完整的二分类实验数据,适合计算机视觉方向的学生、科研人员及算法学习者使用。数据按目录规范存放,训练集约480张、验证集约140张、测试集约70张,类别定义一目了然,可直接用于卷积神经网络、迁移学习等模型的训练、验证与评估。资源共692个文件,包含689张jpg图像、1个python脚本、1张png预览图及1个json类别配置文件,python脚本便于完成数据读取与批次划分,json文件提供准确的类别标签映射,png可快速预览图像样例;压缩包整体仅17.85MB,轻量易用。目前已有286人学习使用。获取后即可得到整理好的训练集、验证集与测试集,配合辅助脚本可省去数据清洗时间,快速搭建乳腺癌症图像二分类实验流程,适用于课程设计、论文复现或算法对比。
1. 乳腺癌症图像分类:先别急着训模型,把数据集的口径搞明白再动手
“深度学习数据集:乳腺癌症图像分类”这个标题,落到工程上其实是一道标准的图像二分类问题:输入乳腺X光片、超声图或病理切片,输出良性/恶性(进阶可以做到BI-RADS分级)。它难的地方从来不是“深度学习”四个字,而是数据集怎么挑、标签怎么信、验证指标怎么定——同一个模型,在论文里能报AUC 0.95,换到临床场景却可能漏掉最该被拦下来的那个恶性病例。这篇笔记就把这条链路完整拆开:公开数据源怎么选、预处理管线怎么写、ResNet基线怎么微调、哪些坑会让你白跑一周,以及最后怎么用一份医生能看懂的验证报告收尾。适合正在复现医学影像分类项目、准备投稿或做毕业设计的从业者,也适合刚接触深度学习、手里只有几千张图的入门工程师。
2. 数据集先行:三类影像形态、四个公开数据源与选型依据
2.1 影像形态决定任务边界:全图、ROI 还是 patch
“乳腺癌症图像分类”不是一个统一的任务,拿到影像先得确认你到底在分什么东西。乳腺影像有好几种来源,对应的分类粒度完全不同:
| 影像形态 | 常见来源 | 分类对象 | 标签形态 | 典型公开数据集 |
|---|---|---|---|---|
| 全图X光(钼靶) | 体检筛查中心 | 整张影像 | 有无恶性病变 / BI-RADS | CBIS-DDSM、MIAS |
| 局部ROI(肿块或钙化区域) | 检测框裁出 | 单个病灶 | 良 / 恶性 | CBIS-DDSM裁剪版 |
| 病理切片patch | 活检组织扫描 | 每张patch图像 | 癌 / 非癌,部分带分级 | BreaKHis |
| 超声图像 | 门诊检查设备 | 整帧或病灶框 | 良 / 恶性 | 各类超声公开集 |
全图分类最简单,直接把一张乳腺X光整图送进去,输出概率。缺点是早期病灶可能只占几个像素,模型很容易被周围大面积的正常腺体带偏,敏感度做不上去。ROI分类等于先把病灶位置切出来再分良恶性,难度降了一截,但你需要一个前置的检测或分割模型来提供ROI。病理切片patch分类则是把全切片切成几百张小块,逐块判断,最后聚合出整片结论,训练最容易,推理时要做多patch融合。
我一般建议第一次跑通的人从病理patch或ROI入手,而不是一上来就做全图分类。原因很实在:全图分类对硬件和模型容量要求更高,而且指标一旦低了,你很难判断是模型问题还是病灶太小定位不上。先在一个“边界清楚”的子任务上把管线跑通,再往全图扩展,是性价比最高的路线。
2.2 公开数据源怎么挑:CBIS-DDSM、MIAS 与 BreaKHis 的差异
公开乳腺影像数据集常见的有几个,但彼此差别非常大。先说最常见的CBIS-DDSM,它是DDSM的整理版,提供裁剪版和全图版两类数据,裁剪版就是现成的ROI分类任务,标签质量在公开集里算很规整的,下载需要走学术申请流程,审核通过后在网站上凭邮箱获取。MIAS是老牌数据集,规模小、结构简单,图像数量只有几百到一千张级别,作为练手验证管线可以,想拿来发文章基本不够用。INbreast也来自欧洲,带BI-RADS分级标注,适合做多分类进阶,但同样规模有限。BreaKHis是病理组织切片图像,按不同放大倍率提供patch,任务就是良恶性二分类,不需要检测前置,是入门最友好的选择。
这些数据集放到自然图像领域都属于“袖珍型”——几千到一万张左右,远不能跟ImageNet、OpenImages比规模。选型时我按三条原则走:第一条,模态必须和你最终要落地的场景一致,做钼靶筛查就选CBIS-DDSM,做病理AI就选BreaKHis,不要拿病理模型去证明X光效果;第二条,优先选有官方划分或明确病人编号的数据集,方便做按病人切分,防止数据泄漏;第三条,先小后大,先用MIAS这类集把训练脚本和评估脚本调通,再换大数据集出正式指标。申请下载医学数据时要填学术用途,这是数据授权的常规流程,不是说你在跟平台打交道,而是原作者在用协议约束数据不被滥用。
还有一个经常被忽略的点:公开数据集的“金标准”并不完美。病理切片标签来自医生标注,不同医生对同一张切片的判读存在分歧,尤其是不典型增生这类交界性病变。做数据清洗时别盲目相信文件夹名字,留出时间抽查一部分训练样本,把可疑的挑出来人工复核,这一步在后面避坑章还会细说。
2.3 为什么迁移学习在这里几乎是必选项
很多深度学习入门教程拿CIFAR-10带着你从零搭CNN,这套流程搬到乳腺影像上会直接翻车。原因很直白:CIFAR-10有五万张训练图,乳腺公开集通常只有几千张,从零训练的CNN在这种数据量下方差极大,跑三次能出三个不同的验证指标。可迁移特征又是医学图像最需要的——预训练权重里已经学会的纹理、边缘、对比度变化这些低级特征,正好是钙化点和腺体边界判读的基础。
近两年Transformer也杀进了图像分类,但小数据集上它并不占优。ViT没有CNN那种内置的局部归纳偏置,几千张图很难训出稳定结果,除非你有在医学大图上预训练过的权重,或者用蒸馏版轻量模型配强增强。所以我给基线模型的排序一般是:ResNet系列优先,ResNet18跑通,ResNet50提精度,EfficientNet做效率优化,ViT放到最后去试。硬件上也不用被“深度学习环境配置”吓住,一张消费级显卡就能跑这种规模的数据集,PyTorch装好就能开工。
顺带说一个经验值:医学影像分类里恶性样本占比经常只有百分之十几,这是常态而不是异常。选型时就要预先想好类别不平衡怎么处理,否则模型训练完你得到的是一个“把所有图都判成良性、准确率还挺高”的废物。这个会在第四章给具体代码方案。
3. 把原始影像变成可训练集:按病人划分、CLAHE预处理与克制的增强管线
3.1 按病人分组做数据集划分,而不是按图片随机切
拿到原始影像后的第一件事不是写模型,而是先建目录、做划分。这里有一个必须守住的原则:同一个病人的所有图像必须划到同一个集合里。乳腺影像经常一个病人有多张视图或几十个patch,它们之间高度相关,如果随机按图片切分,验证集里会出现训练集的“近亲”,指标虚高到让你误以为模型已经能用。先建好目录骨架:
data/ train/0_benign/ train/1_malignant/ val/ 0_benign/ val/ 1_malignant/ test/ 0_benign/ test/ 1_malignant/假设原始数据按raw_images/病人编号/子目录/图片.png组织,文件名或路径里带有benign或malignant标记,用下面这段做按病人划分:
import os import random import shutil from glob import glob random.seed(42) SPLIT = {"train": 0.70, "val": 0.15, "test": 0.15} SRC_DIR = "raw_images" DST_DIR = "data" patients = [p for p in os.listdir(SRC_DIR) if os.path.isdir(os.path.join(SRC_DIR, p))] random.shuffle(patients) n_train = int(len(patients) * SPLIT["train"]) n_val = int(len(patients) * SPLIT["val"]) for idx, patient in enumerate(patients): if idx < n_train: split = "train" elif idx < n_train + n_val: split = "val" else: split = "test" for img_path in glob(os.path.join(SRC_DIR, patient, "*", "*.png")): label = "1_malignant" if "malignant" in img_path else "0_benign" dst_dir = os.path.join(DST_DIR, split, label) os.makedirs(dst_dir, exist_ok=True) shutil.copy(img_path, os.path.join(dst_dir, os.path.basename(img_path)))划分比例70/15/15是常见做法,但如果总样本只有一千张,我更建议换成五折交叉验证来出最终指标,单留一个test集太奢侈。随机种子固定是为了可复现,换一个种子指标波动大说明数据集太小,这是信号而不是bug。另外注意label的判断我用了路径匹配,如果你的文件名不含良恶性字样,就得先维护一个病人ID到标签的映射表,别靠猜。
3.2 灰度影像的预处理顺序:CLAHE、尺寸统一与三通道转换
乳腺X光和多数病理切片是灰度图或近似灰度图。医学影像对比度普遍偏低,直接丢给模型很容易让模型把注意力放在亮度分布而不是组织纹理上。我习惯先做CLAHE(对比度受限的自适应直方图均衡),再缩放尺寸,最后复制成三通道。离线预处理脚本如下:
import cv2 import numpy as np from glob import glob DST = "preprocessed" def preprocess(img_path, out_path, target_size=512): # 用IMREAD_GRAYSCALE读入,不管原图是彩色的还是伪彩色的都统一成灰度 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: return # CLAHE只在局部窗口内做对比度增强,避免全局均衡把噪声一起放大 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) img = clahe.apply(img) # 原图分辨率一般远大于512,IMREAD_GRAYSCALE读入后用INTER_AREA降采样 img = cv2.resize(img, (target_size, target_size), interpolation=cv2.INTER_AREA) # 灰度图复制成3通道,保持与ImageNet预训练输入一致 img_bgr = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) cv2.imwrite(out_path, img_bgr) for img_path in glob("data/train/*/*.png"): _, split, label, fname = img_path.replace("\\", "/").split("/") out_dir = f"{DST}/{split}/{label}" os.makedirs(out_dir, exist_ok=True) preprocess(img_path, f"{out_dir}/{fname}")三个参数决定了预处理效果:clipLimit=2.0控制对比度增强的上限,调太大会把背景噪声一起抬起来,太小则增强不明显,2.0 是个保守起点;tileGridSize=(8, 8)是局部窗口划分,图像越大窗口越应该大,512 输入下8x8够用;target_size=512是分辨率和显存的折中,224会丢掉微小钙化细节,而1024会让ResNet50在消费级显卡上很难训练。注意我没做归一化,归一化放到训练时的 transform 里做,离线只做几何和光度层面的处理,这个分工后面会清楚。
3.3 数据增强要“克制”:别把病灶增强没了
数据增强对小数据集是救命稻草,但医学影像的增强策略必须比自然图像保守。ColorJitter这类色彩抖动我从来不用于乳腺影像,不同医院设备的灰度/染色本来就不统一,你再人为加色彩扰动,模型只会更抓不住关键纹理。训练集和验证集使用两套transform:
from torchvision import transforms # 训练集:轻度几何扰动 train_transform = transforms.Compose([ transforms.RandomResizedCrop(size=512, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=10, fill=0), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) # 验证/测试集:只做缩放和归一化 val_transform = transforms.Compose([ transforms.Resize(512), transforms.CenterCrop(512), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])RandomResizedCrop的scale=(0.8, 1.0)是关键参数,它限制裁剪最少保留原图80%面积,防止把病灶主体切掉;乳腺影像不是自然场景,上下翻转会让CC位变成MLO位的视角,方向信息全乱,所以我只保留了水平翻转;旋转限制在10度内,超过这个角度乳头、皮肤轮廓这些解剖结构会失真。Normalize用的均值方差是ImageNet的统计量,原因是迁移学习权重在ImageNet分布上预训练,输入也保持同分布,模型收敛最稳。
提示:如果离线预处理已经做过 resize,在线 transform 就不要再加 Resize,两处尺寸不一致会让训练图被二次缩放,增加不必要的插值偏差。
另外,如果训练集只有几百张,可以加上CutOut或RandomErasing,随机遮挡一小块区域迫使模型不依赖单一纹理。但遮挡尺寸别超过原图的15%,医学病灶常常就是一小块,遮大了等于把标签擦掉。
4. 从预训练权重到医学基线:ResNet微调的训练脚本、采样策略与关键超参
4.1 加载预训练模型、替换分类头与冻结策略
有了可训练的图像数据,接下来就是搭模型。我一般直接用torchvision加载ImageNet预训练的ResNet,替换最后一层分类头,再按数据集规模冻结一部分浅层。先说结论:几千张医学图像这个量级,全量微调ResNet50容易过拟合,冻结前两个stage的特征提取器反而是常规操作。
import torch import torch.nn as nn import torchvision.models as models model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 2) # 二分类:良性 / 恶性 # 冻结 layer1、layer2 之前的参数,只让深层特征和分类头参与训练 for name, param in model.named_parameters(): if name.startswith("layer1.") or name.startswith("layer2."): param.requires_grad = False冻结浅层的逻辑是:ResNet的layer1学到的是边缘、纹理、斑点这类通用低级特征,自然图像和医学图像在这些特征上是共享的,没必要重新学;而靠近分类头的layer3、layer4学的是任务相关的语义组合,必须微调。如果你的训练集只有几百张,可以把冻结范围扩大到layer3;如果数据量上万,全部解冻做完整微调效果更好。分类头输入维度直接用model.fc.in_features拿,不要写死2048,免得换模型时忘改。
4.2 类别不平衡的双层保险:WeightedRandomSampler 与损失加权
恶性样本占比低是乳腺影像分类的常态,不做处理的话,模型会把所有输入都倾向判成良性。我见过最离谱的一次,训练集准确率0.97,召回率只有0.2,模型等于白训。解决办法是双层保险:采样器层面提高恶性样本被抽中的概率,损失函数层面给恶性样本更高的权重。
from torch.utils.data import DataLoader, WeightedRandomSampler from torchvision import datasets train_set = datasets.ImageFolder("preprocessed/train", transform=train_transform) labels = train_set.targets # 每个类别的权重 = 样本数的倒数,少数类样本权重更大 counts = torch.bincount(torch.tensor(labels)) class_weights = 1.0 / counts.float() # 对每一个样本,按其类别权重赋采样概率 sample_weights = class_weights[labels] sampler = WeightedRandomSampler(sample_weights, num_samples=len(labels), replacement=True) train_loader = DataLoader( train_set, batch_size=16, sampler=sampler, num_workers=4, pin_memory=True, ) criterion = nn.CrossEntropyLoss(weight=class_weights.cuda())replacement=True表示有放回采样,少数类样本会被重复抽到,虽然缓解了不平衡,但也意味着同一batch可能出现许多重复图,模型容易对单张图过拟合。所以在采样器已经做了加权的前提下,损失权重别设太大,用class_weights的原始倒数即可,不要额外放大倍数。num_workers=4是通用设置,Windows下建议降为0或2,否则Dataloader容易卡死。
4.3 训练循环、差分学习率与显存不够的处理
优化器我推荐AdamW而不是SGD。医学小数据集上SGD收敛慢、对学习率敏感,AdamW的逐参数自适应特性让小数据微调更稳定。这里采用差分学习率:分类头是新初始化的,需要快一点用1e-3,预训练主干已经比较成熟,用1e-4慢调。
optimizer = torch.optim.AdamW([ {"params": model.fc.parameters(), "lr": 1e-3}, {"params": (p for n, p in model.named_parameters() if not n.startswith("fc") and p.requires_grad), "lr": 1e-4}, ], weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) best_auc, best_epoch = 0.0, 0 for epoch in range(30): model.train() for x, y in train_loader: x, y = x.cuda(), y.cuda() optimizer.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() # 验证集的AUC计算代码见第6章,这里先留接口 val_auc = evaluate_auc(model, val_loader) if val_auc > best_auc: best_auc = val_auc best_epoch = epoch torch.save({"state_dict": model.state_dict(), "epoch": epoch, "auc": val_auc}, "best.pt") if epoch - best_epoch > 8: print("early stop at epoch", epoch) breakclip_grad_norm_的作用是梯度裁剪,max_norm=1.0 防止个别样本产生超大梯度把预训练权重冲坏。混合精度训练用torch.cuda.amp可以省一半显存,batch_size=16加上512输入在8G显存的卡上刚好能跑,如果你的卡只有4G,就把输入降到384或者batch降到8并配合梯度累积。早停 patience 设成8个epoch比较稳妥,医学数据噪声大,验证指标波动正常,太短的patience会过早停掉。
不同模型的取舍我按下面的经验表来定:
| 模型 | 显存开销(512输入) | 小数据集风险 | 我的用法 |
|---|---|---|---|
| ResNet18 | 低,batch=32可跑 | 欠拟合风险低,指标稳定 | 首次跑通、快速验证管线 |
| ResNet50 | 中,batch=16需半精度 | 需要冻结浅层+强增强 | 正式基线 |
| EfficientNet-B3 | 中低,输入需调 | 输出层会随scale变化 | 追求推理速度时 |
| ViT-B/16 | 高,需要大batch | 几千张图容易过拟合 | 有可靠的预训练权重时再试 |
很多人一上来就想追“最新的图像分类模型”,但在医学小数据上,旧而稳的ResNet往往比新架构更可靠。先把ResNet18的基线跑出来,确认预处理、采样、评估链路没问题,再换ResNet50提点,这个顺序能省下大量排错时间。
5. 乳腺癌症图像分类的5个避坑记录:现象、原因与排查方法
5.1 训练集F1接近0.98,验证集召回率却只有0.2
现象:训练过程一切正常,loss稳定下降,训练集准确率也很高,一到验证集,恶性样本的召回率惨不忍睹,模型几乎把所有图都判成良性。看混淆矩阵会发现良性样本全都对,恶性样本基本全错。
原因:类别不平衡没有真正处理。训练集里恶性样本占比低,交叉熵损失被多数类主导,模型学到的决策边界就是“都判良性”,因为这样能让全局损失最小。如果只用了损失加权但没改采样,或者只改采样但没配合合适的评估指标,都会出现这个结果。
解决:采样器加权和损失加权同时开,并且监控指标以AUC和召回率为主,不要看准确率。我习惯在训练每个epoch后打印验证集的混淆矩阵,而不是只打印loss。如果召回率长期上不去,把损失权重里的恶性类再乘1.5到2倍,同时观察精确率的变化,找到两者平衡点。
5.2 按图片随机切分,AUC虚高到0.95,换成按病人切分只剩0.78
现象:数据集划分时直接train_test_split按图片路径随机切,训练出来的模型在验证集上AUC异常高,换一个划分种子指标波动很大。等到按病人编号重新划分,指标掉了十几个点。
原因:同一个病人的多张乳腺影像来自同一个扫描设备、同一个体位、同一套组织纹理,本质上不是独立样本。随机按图片切分等于把同一病人的影像同时放进训练集和验证集,模型其实在“认病人”而不是“认病灶”,这是典型的数据泄漏。
解决:坚持按病人ID划分,划分代码见3.1节。如果公开数据集没有明确病人编号,就用文件名前缀或目录名推断,推断不出来就按影像采集时间分组。做交叉验证时也要保证同一个病人只出现在一折里,这是医学影像评估组的铁律。
5.3 灰度图复制三通道后loss不降,模型压根不收敛
现象:预处理把灰度图复制成RGB三通道,训练loss在初值附近震荡,十几个epoch都没有下降趋势,验证AUC徘徊在0.5。
原因:常见的有两个。一是把灰度图直接交给ImageNet预训练模型,但没有按ImageNet的mean/std做归一化,输入分布与预训练分布严重错位。二是灰度图复制三通道后,三个通道完全相同,模型在通道维度上拿不到额外信息,这本身不是问题,但会放大归一化错误的影响。
解决:先检查预处理输出图的像素值范围,如果直接是0-255的uint8,喂给模型前必须做ToTensor加Normalize,公式是(x - mean) / std。如果确认归一化没问题还是不收敛,把学习率降到1e-5试跑5个epoch,排除学习率过大的影响。最后检查数据路径有没有读错,用torchvision.utils.make_grid可视化一个batch,看图像内容是否正常、标签和图像是否对应。这步可视化经常能发现文件命名错位这种低级问题。
5.4 模型学的是影像上的白标签和扫描仪伪影,不是病灶
现象:验证集AUC很高,但把Grad-CAM热力图叠加到原图后,发现模型关注区域集中在图像左上角或边缘,而不是肿块或钙化区域。更蹊跷的是,图像旋转90度后预测概率明显下降。
原因:公开数据集的乳腺X光片上常带有设备信息水印、白色标记贴纸或扫描伪影。这些标记在图像位置上高度固定,模型很容易把它们当成捷径——只要识别到白点或边缘文字就能“正确”分类,因为同一个数据集的标签和扫描设备往往是绑定在一起的。模型学的不是医学特征,而是数据集本身的无意泄露。
解决:预处理阶段用固定ROI裁剪掉图像四周边框和标记区域,或者在分析时只保留乳腺区域。更关键的是用Grad-CAM做坏例检查,把验证集里预测正确但激活区域明显错误的样本捞出来看,如果这类样本占比较高,说明模型泛化能力被高估了。我现在的习惯是每个项目正式出指标前,必须先过一遍热力图,这步省不掉。
5.5 验证集里也有错标:病理金标准并不完美
现象:训练脚本和数据管线都没问题,但某个特定类别的样本反复被模型分错,人工抽查后发现验证集标签本身就是错的。比如一张被标为良性的图,病理报告里其实是低度恶性。
原因:医学标注本身存在主观性和误判,不典型病变在医生之间也有分歧。公开数据集整理时可能只经历一轮标注,没有复核流程,错标样本混进验证集后,模型再怎么调参也达不到那个“正确”指标。
解决:先用训练好的模型找出验证集中预测置信度最高但标签为负例的样本,以及loss最高的样本,人工重点复查这批。发现错标后,把标签修正过来再重新评估。对训练集也可以采用标签平滑,CrossEntropyLoss的label_smoothing=0.05能减少模型对单一样本标签的过度自信,提升对标注噪声的鲁棒性。在投稿或交付前,务必记录人工复核了多少样本、修正了几个标签,这个数字本身也是报告的一部分。
6. 交付一份医生愿意签字的验证报告:先锁敏感度,再谈准确率
模型训练完,紧接着要做的是把指标从“深度学习黑匣子”翻译成临床语言。放射科医生不关心你的F1值,他们关心的是漏诊率和误诊率。所以我的验证脚本里第一个指标永远是ROC-AUC,第二个是PR曲线,最后才是准确率。乳腺影像恶性样本占比低,ROC曲线会被大量良性样本“撑高”,PR曲线直接反映模型在恶性样本上的表现,两者一起看才不会自欺欺人。
from sklearn.metrics import roc_auc_score, precision_recall_curve, auc import numpy as np # all_probs 是模型在测试集上输出的正类概率,all_labels 是真实标签 probs = np.concatenate(all_probs) y_true = np.concatenate(all_labels) roc_auc = roc_auc_score(y_true, probs) precision, recall, thresholds = precision_recall_curve(y_true, probs) pr_auc = auc(recall, precision) print(f"ROC-AUC: {roc_auc:.3f} | PR-AUC: {pr_auc:.3f}")分类阈值怎么定也有讲究。二分类默认阈值0.5在医学场景里几乎总是错的,因为漏掉一个恶性病例的代价远高于多做一个良性活检。我一般会先锁定敏感度,比如要求检出95%的恶性病例,再反过来求阈值:
# 在不漏诊的前提下,选择敏感度 >= 0.95 的最低阈值 idx = np.argmax(recall >= 0.95) clinical_threshold = thresholds[idx] print(f"在敏感度 {recall[idx]:.2f} 时,阈值取 {clinical_threshold:.3f}")确定阈值后,用它重算混淆矩阵,报告里写清楚敏感度、特异度、阳性预测值和阴性预测值。Grad-CAM热力图也要挑几个典型patch叠加到原图上,一张图能抵过十行文字,医生能直观判断模型看的是病灶还是设备水印。
进阶一步,我还会做多尺度推理:同一张图分别用256和512输入各跑一次,取平均概率。这个小技巧能稳定提两三个点AUC,代价只是推理时间翻倍。最后把最佳模型权重、阈值、验证脚本和CAM截图归档到一个目录,三个月后回来还能完整复现当时的指标。这是我自己的习惯,也建议你从第一个项目就养成。希望帮到你。
本文还有配套的精品资源,点击获取