简介:这是一篇关于乳腺癌病理图像分类的深度学习方法论文,面向医学图像分析研究者、深度学习实践者以及临床辅助诊断系统开发者。乳腺癌是全球女性最常见的癌症之一,早期诊断极为关键,而传统CNN在处理小规模病理图像时容易出现过拟合,导致分类准确率受限。论文提出的AlexNet-BC模型基于经典AlexNet结构进行改进,先通过ImageNet数据集预训练,再在增强的乳腺病理图像数据上微调,并设计了一种改进的交叉熵损失函数,能够惩罚过度自信的低熵输出,从而提升模型鲁棒性和泛化能力。资源为1个PDF文件,全文约2.4MB,已获153人浏览学习。读者可从中完整看到模型架构设计、预训练与微调流程、损失函数修改思路,以及在BreaKHis、IDC、UCSB等数据集上的对比实验验证,对复现实验、开展病理图像分类研究或构建辅助诊断系统均具有直接参考价值。
1. AlexNet-BC 到底解决什么问题:病理图像分类和普通图像分类的三处不同
乳腺癌病理图像分类这行,最常被忽略的事实是:新出的 Transformer 模型不一定比 2012 年的 AlexNet 强多少。原因出在任务形态上——整张病理切片(WSI)动辄几万乘几万像素,本质上不该被当作“一张大图”塞进网络,而是要先切成 patch、逐个分类、再聚合成判断。AlexNet-BC 就是顺着这条路线做的适配:输入尺寸、分类头、训练策略全部针对 H.E. 染色组织切片调整,最终输出一张切片属于良性还是恶性的概率。它不追求网络结构复杂度,而是靠迁移学习、切片策略和阈值投票把模型行为稳定下来。这篇笔记适合正在做医学影像落地、或者刚进入病理 AI 方向的工程开发者;如果你已经跑过几个图像分类模型,直接翻到第四、五章看调参和排错,收获会更直接。
2. 为什么用 AlexNet 做乳腺癌病理分类:架构选择与两条实现路线
2.1 病理图像的三类难点与 AlexNet 的应对
病理图像和自然图像之间有相当大的距离,这是选型时第一个要认清的事。
第一,分辨率极高。一张 40 倍放大的 WSI 长宽可以到十万像素量级,而常见分类网络输入只有几百像素。这意味着“全局上下文”天然稀缺,模型只能在小 patch 上做判别,再靠聚合策略补全上下文。第二,染色差异明显。不同医院、不同批次、甚至同一张切片的切片厚度差异,都会让同一种组织呈现不同的颜色分布。传统特征如颜色直方图、手工纹理特征在这种漂移下很容易失真。第三,组织没有明确的“物体边界”。肿瘤区域往往由细胞核密度、管状结构排列模式和间质比例来定义,而不是一条清楚的轮廓线。
AlexNet 在这类任务上仍然适用的原因,和它的结构特点直接相关。它的前几层是大卷积核(11×11、5×5),适合捕获细胞核簇的纹理模式;后面的三个卷积层逐步把局部纹理组合成高阶结构表达,这个递进恰好匹配病理诊断由“细胞形态”到“组织结构”的判断过程。与之对比,ResNet 的残差连接和 Transformer 的自注意力在超大图像上优势明显,但在 224×224 组织 patch 上,优势会被小样本、高噪声的数据集方差抹平。我实际跑过的几次对比里,ResNet18 在多个 patch 分类任务上都比 AlexNet 高不了 1~2 个点,训练时间却多出一倍。
2.2 预训练权重用不用:迁移学习与从零训练的分水岭
病理图像和 ImageNet 的图像域差距很大,所以不少刚入行的人会问:预训练权重还有意义吗?答案是看数据量。
如果你的标注 patch 数不超过 5 万,从零训练很容易把模型训进过拟合区域。病理 patch 之间高度相似,同一张切片切出来的相邻 patch 几乎只有平移差异,这让模型很容易记住训练集里的特定纹理,而不是学到通用的组织判别特征。常见做法是用 ImageNet 预训练的 AlexNet 作为起点,然后冻结前两层卷积,只微调最后的卷积层和全连接层。
我一般会先把前两层 feature 层冻结,训练 10 个 epoch 后再解冻全部参数,用更小的学习率统一微调。这样做的思路是:浅层卷积学到的是边缘、颜色块、局部纹理这类通用特征,在病理图像上同样有效,前期冻结它们相当于给模型一个稳定的“底座”;深层特征更偏向具体任务,应该随病理数据充分调整。
从零训练的情况也不是没有。当你的标注 patch 超过 10 万,且经过强颜色增强和旋转增强后,从零训练的 AlexNet 可以达到与预训练微调接近的效果。但医疗数据标注成本高,多数项目达不到这个量级。所以我的默认选择是:无论数据多少,先用预训练权重,再用“冻结浅层 + 微调深层”策略。如果后期发现浅层特征与病理图像不匹配,或者验证集指标停滞,再放开浅层也不迟。
2.3 AlexNet-BC 的分类头改造:输入尺寸与输出层
torchvision 里的 AlexNet 结构是固定的:features 部分输出 256 个通道的特征图,分类头是三层全连接。改造时只需要调整分类头的输出维度,并注意输入尺寸要与预训练权重对齐。
import torch.nn as nn from torchvision import models def build_alexnet_bc(num_classes=2, pretrained=True): # 加载预训练权重,weights 参数是 torchvision 的推荐写法 if pretrained: model = models.alexnet(weights=models.AlexNet_Weights.IMAGENET1K_V1) else: model = models.alexnet(weights=None) # 冻结 features 的前两层,浅层特征在病理图像上依然通用 # features 前 6 个参数对应 conv1 的 weight/bias 和 conv2 的 weight/bias frozen = 0 for param in model.features.parameters(): param.requires_grad = False frozen += 1 if frozen >= 6: break # 替换分类头:AlexNet 在 224 输入下,特征图是 6x6,通道数 256 model.classifier = nn.Sequential( nn.Dropout(0.5), nn.Linear(256 * 6 * 6, 1024), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(1024, 512), nn.ReLU(inplace=True), nn.Linear(512, num_classes), ) return model这段代码的关键在分类头的输入维度:torchvision 的 AlexNet 在features后面接了一个AdaptiveAvgPool2d((6, 6)),所以无论输入是 224 还是 227,最终展平后都是256 * 6 * 6 = 9216。如果改用其他输入尺寸,需要先用torchsummary或者实际跑一次前向传播确认输出的空间尺寸。
分类头改成三层全连接保留了两个 0.5 的 Dropout。病理 patch 数据冗余度高、样本少,Dropout 比在自然图像上的作用更明显。如果你用的是torchvision自带分类头的输出层,往往只有一层 Linear,换成上面的三层结构后,验证集的表现通常会更好。
输入尺寸方面,原始 AlexNet 论文用的是 227×227,但 torchvision 实现按 224×224 来组织卷积和池化步长,而且预训练权重就是按 224 输入做的微调。所以训练和推理统一用 224×224 即可。不建议为了保留更多信息把输入放大到 512,因为预训练权重的感受野设计是按 224 来的,硬改输入尺寸后,卷积感受野和语义特征都会错位。
3. 从 WSI 到模型输入:切片、训练代码与增强策略
3.1 把 WSI 切成 patch:两个必调参数
进入训练之前,必须把高分辨率全切片图像处理成模型能吃的 patch。这一步的质量直接影响模型性能,比后面调学习率重要得多。常见做法是用 OpenSlide 读取 WSI 格式,滑动窗口切 patch,再按组织占比过滤背景。
import openslide import numpy as np def extract_tissue_patches(slide_path, patch_size=224, overlap=56, tissue_threshold=0.7): slide = openslide.OpenSlide(slide_path) w, h = slide.dimensions step = patch_size - overlap patches = [] y = 0 while y + patch_size <= h: x = 0 while x + patch_size <= w: # 读取 RGB patch,忽略透明度通道 tile = slide.read_region((x, y), 0, (patch_size, patch_size)) arr = np.array(tile)[:, :, :3] # 白色背景像素占比过高说明该区域没有组织 non_white = (arr < 220).all(axis=2).mean() if non_white >= tissue_threshold: patches.append((x, y, arr)) x += step y += step return patches两个必调参数:一个是overlap,一个是tissue_threshold。overlap默认为 56(即 patch_size 的 1/4),这样做的原因是细胞核可能恰好横跨两个 patch 的边界,如果不重叠切,一个完整的细胞核会被拆成两半,导致两个 patch 的标签语义都不完整。重叠比例越高,训练 patch 越多,但相邻 patch 高度相似,相当于变相提高了相同信息的采样次数,一般控制在 0~50% 之间。
tissue_threshold设为 0.7 意味着至少 70% 的像素不是白色背景,这个 patch 才保留。不同切片的墨迹、出血区域、折叠区域有时会被误判为组织,如果训练集里这类噪声太多,可以再加一层简单的形态学过滤(比如用 OpenCV 做连通域面积筛选)。我习惯把阈值设在 0.6~0.8 之间,太低会把大量背景带进训练,太高会让某些间质稀疏的切片几乎没有可用 patch。
3.2 最小可运行的 AlexNet-BC 训练代码
模型结构和切片策略都定了,接下来是训练主流程。下面这份代码可以直接跑通,但只保留了核心步骤,实际项目里你还需要加上日志、模型保存和断点恢复。
import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset import torch.optim as optim class PatchDataset(Dataset): def __init__(self, patches, labels, transform=None): self.patches = patches # list of (x, y, array) 或直接存 array self.labels = labels # 0=良性, 1=恶性 self.transform = transform def __len__(self): return len(self.labels) def __getitem__(self, idx): img = self.patches[idx][2] label = self.labels[idx] if self.transform: img = self.transform(img) return img, label训练循环里有个容易翻车的细节:病理 patch 的内存占用。一个 224×224×3 的 uint8 patch 不到 150KB,但一次读几千个进内存就会到 GB 级别。常见做法是__getitem__里按索引重新从 WSI 读取,而不是把所有 patch 一次性塞进内存。上面的PatchDataset只做演示,实际使用时请把 patch 的路径或坐标存进列表,读取放到__getitem__内部。
def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct = 0.0, 0 for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * inputs.size(0) correct += (outputs.argmax(dim=1) == labels).sum().item() return total_loss / len(loader.dataset), correct / len(loader.dataset) # 关键参数:迁移学习用 1e-4,从零训练可以放大到 1e-3 model = build_alexnet_bc(num_classes=2, pretrained=True) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) criterion = nn.CrossEntropyLoss( weight=torch.tensor([1.0, 3.0]).to(device) # 恶性样本少时给高权重 ) optimizer = optim.SGD( model.parameters(), lr=1e-4, momentum=0.9, weight_decay=5e-4 ) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)几个参数的说明。SGD 加动量的收敛效果在这个任务上稳定,Adam 也能用但更容易在小样本情况下提前收敛到锐利极小值。weight=1.0:3.0的意思是恶性类别的损失权重是良性的 3 倍,实际比例应该根据训练集中两个类别的样本数倒过来设。如果恶性:良性 = 1:4,那就设1.0:4.0。weight_decay的 5e-4 是 ImageNet 训练时常用的值,病理 patch 特征方差大,正则稍强一点能抑制过拟合。
学习率 1e-4 针对的是“已经冻结浅层 + 微调深层”的情况。如果你解冻了全部层或者从零训练,初始学习率要相应调整,详见第四章的参数表。
3.3 数据增强策略:病理图像和自然图像不一样
病理图像增强的第一原则是:不要改变组织结构。自然图像里常用的 RandomResizedCrop(随机缩放裁剪)在这里会改变细胞核的相对大小,核直径的差异本身是病理诊断的重要依据,不能随便缩放。
我自己常用的增强组合是旋转 90 度的倍数、翻转和颜色扰动。由于病理切片的方向没有先验(扫描时切片放正或放反都有可能),模型必须对四个旋转方向都不敏感。ColorJitter 用来模拟不同染色批次之间的颜色波动,但把 hue 的扰动幅度调小,因为 H.E. 染色下细胞核和胞浆的颜色偏移有一定生理约束,过大的色调变化反而会把模型带偏。
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomRotation(90), # 90的倍数旋转,不改变组织朝向的统计结构 transforms.RandomHorizontalFlip(p=0.5), transforms.RandomVerticalFlip(p=0.5), transforms.ColorJitter( brightness=0.2, contrast=0.2, saturation=0.1, hue=0.05 ), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])RandomRotation(90)会做任意角度的旋转插值,实际用RandomRotation([0, 90, 180, 270])避免插值带来的伪纹理更稳妥。Normalize 的均值标准差直接沿用了 ImageNet 的统计值,因为预训练权重是按这套统计值做的归一化,换了会导致迁移效果变差。如果从零训练,可以用训练集本身的统计值重新计算。
类别不均衡的处理上,我给损失函数加weight参数就够了,不需要额外设计采样器。如果恶性样本占比极小(比如低于 10%),再考虑用WeightedRandomSampler或 Focal Loss。先从损失权重开始调,简单有效。
4. 评估 AlexNet-BC:三个指标与四个必调参数
4.1 别只看准确率:混淆矩阵、AUC 与假阴性率
病理分类任务里,类别不均衡是常态。良恶性比例可能到 4:1 甚至 5:1,这时准确率几乎失去参考价值。如果模型把全部样本预测为良性,准确率也有 80%,但这显然是不可用的。
评估 AlexNet-BC 至少要同时看三个东西:混淆矩阵、AUC 和恶性类别的召回率。混淆矩阵能直接看出模型把良性误判为恶性的比例(假阳性)和把恶性漏掉的比率(假阴性)。在辅助筛查场景里,假阴性的代价远高于假阳性——漏掉一个恶性病例可能导致患者错过最佳治疗窗口,而良性误报还有复查和进一步检查的机会。
from sklearn.metrics import roc_auc_score, confusion_matrix def evaluate_model(model, loader, device): model.eval() y_true, y_prob = [], [] with torch.no_grad(): for inputs, labels in loader: inputs = inputs.to(device) probs = torch.softmax(model(inputs), dim=1) y_prob.extend(probs[:, 1].cpu().tolist()) y_true.extend(labels.cpu().tolist()) auc = roc_auc_score(y_true, y_prob) cm = confusion_matrix(y_true, (np.array(y_prob) >= 0.5).astype(int)) tn, fp, fn, tp = cm.ravel() fnr = fn / (fn + tp) # 恶性漏诊率 return {"auc": auc, "fnr": fnr, "cm": cm}推理时输出的是 softmax 概率而不是硬标签,这是为了不同阈值下可以动态调整敏感度。临床上如果接受复查成本较高,可以把阈值从 0.5 调到 0.6,用更高的假阳性率换取更低的假阴性率。这样调阈值的操作在部署阶段才有意义,训练阶段只需关注 AUC 和损失曲线。
4.2 四个必调超参数
| 参数 | 推荐初始值 | 调整方向 | 判断方式 |
|---|---|---|---|
| 学习率 | 1e-4(迁移学) / 1e-3(从零) | 验证集 loss 震荡则减半 | 前 5 个 epoch 看 loss 下降速度 |
| batch size | 64(224×224 patch) | 显存不够降到 32 | 观察显存和单 epoch 耗时 |
| Dropout | 0.5 | 过拟合明显时提到 0.7 | 训练/验证准确率差距 > 8% 时 |
| 组织占比阈值 | 0.7 | patch 噪声多则提高 | 检查过滤后 patch 的分布 |
学习率是最容易出问题的参数。迁移学习场景下,预训练权重已经把特征空间调整得相对平滑,过大的学习率会快速破坏已有特征的分布。我的习惯是从 1e-4 起步,如果前两个 epoch 的 loss 没有明显下降,先别急着调大学习率,检查数据加载和标签是否对应才是更常见的修法。
Batch size 的调整会影响统计噪声和内存占用之间的平衡。病理 patch 的特征方差比较大,较小的 batch size(32)会带来更明显的梯度噪声,有时反而帮助模型跳出局部极小值。如果你的显存能放下 128,可以先试 64,看验证集 AUC 的波动幅度再决定要不要降。
Dropout 在这个任务里是“后悔药”。训练集和验证集准确率差距超过 8% 时,先把 Dropout 从 0.5 提到 0.7,通常比加数据增强更有效。注意改了分类头的 Dropout 之后,模型文件要重新保存,避免部署时加载旧结构报错。
4.3 早停、模型保存与日志记录
病理数据集的标注成本高,单 fold 的提升空间有限,所以训练过程中的“每轮都存”策略很重要。我一般会按验证集的 AUC 来决定是否保存当前模型,而不是看 loss。Loss 在类别不均衡时会对多数类过拟合,AUC 对类别比例不敏感,更适合做模型选择的依据。
best_auc = 0.0 patience = 15 epochs_no_improve = 0 for epoch in range(50): train_loss, train_acc = train_one_epoch(...) val_metrics = evaluate_model(model, val_loader, device) if val_metrics["auc"] > best_auc: best_auc = val_metrics["auc"] torch.save(model.state_dict(), "alexnet_bc_best.pt") epochs_no_improve = 0 else: epochs_no_improve += 1 if epochs_no_improve >= patience: break早停的 patience 一般设 15 左右。病理数据的训练曲线比自然图像抖动更大,patience 太小会把一个暂时掉点但后续会涨的模型停掉。保存模型时建议连同 transform 配置和训练超参数一起存成 JSON,否则模型和预处理对不上,推理阶段会无声地退化。
还有一个容易忽略的点:验证集一定要按病人来切,不要按 patch 来切。同一个病人的所有 patch 不应该同时出现在训练集和验证集里,否则就是数据泄漏。这个坑我在下一章详细展开。
5. AlexNet-BC 高频踩坑与排查:按 patient 切分是底线
5.1 按 patch 切分导致的数据泄漏
现象:训练时验证集 AUC 冲到 0.97 以上,模型表现近乎完美,但部署到一张全新的切片上后,预测结果明显变差。
原因:同一个患者的全部 patch 被随机分到了训练集和验证集。相邻 patch 之间高度相似,模型在训练阶段“见”过验证集 patch 的纹理变体,验证指标虚高。这不是模型学到判别的表现,而是半张卷子提前泄露了答案。
解决:数据划分必须按 patient(或按 WSI 文件)分组,一个患者的全部 patch 只能出现在同一个集合里。用 sklearn 的GroupKFold或手动按 patient 目录划分。实践里常见做法是在数据清单里维护 patient_id 列,划分后顺带统计两组之间的类别比例,确保良恶性分布一致。
5.2 染色差异让模型跨院区翻车
现象:模型在 A 医院的数据上测试 AUC 0.92,直接套到 B 医院的数据上掉到 0.63,所有类别都偏向良性。
原因:两院的切片染色流程不同,导致同一组织在颜色分布上有系统性偏移。模型可能把“偏红的胞浆”学成了“良性”的关键特征,而不是基于细胞核形态判断。病理图像的染色差异是跨中心迁移最大的敌人,比模型结构的影响大得多。
解决:训练阶段加 ColorJitter 模拟染色波动,推理阶段做染色归一化。训练增强是让模型见过更多颜色变化,推理归一化是把所有切片变换到同一参考分布。两者配合才能把染色差异的影响降到最低。如果目标部署到多个中心,最好把各中心的一部分数据混合进训练集,或在验证时按中心拆分来评估泛化能力。
5.3 还有三个高频坑:背景 patch、类别失衡与方向漂移
背景 patch 的坑最常见。切 patch 时组织占比阈值过低,大量空白背景混进训练集,模型学到“全白 = 良性”,因为恶性区域很少有大片纯背景。排查方法:抽 30 个训练 patch 可视化,看是否存在明显的非组织区域。解决:把tissue_threshold提高到 0.7 以上,推理时也做同样的过滤,否则背景 patch 的低置信度投票会稀释整切片的判断。
类别失衡的坑是准确率虚高。当恶性样本只占 10% 时,模型只要全预测为良性就能拿到 90% 准确率。只盯着准确率调参很容易被误导。解决:训练时看验证集的 AUC 和混淆矩阵,损失函数加类别权重,推理时用 0.5 以上的阈值并单独评估假阴性率。
方向漂移的坑常常被忽略:某些 patch 旋转 90 度后,模型给出的预测从恶性翻到良性。原因是 WSJ 扫描时组织块的朝向没有统一标准,训练时如果只用了水平翻转而没有做四方向旋转增强,模型会把某个固定方向的特征误当成判别依据。解决:训练增强加入 90/180/270 度旋转,或推理时做 TTA(把同一 patch 旋转 4 次,取平均概率后再判类别)。
6. 验证模型真的在看肿瘤区域:Grad-CAM 与高置信投票
训练完成后,多数人只关注 AUC,但病理科复核时一定会问一个问题:模型是根据什么区域做出的判断?这个问题决定模型能不能被信任,也直接关系到模型能不能上线。
我常用的验证方式是 Grad-CAM,把最后一个卷积层的梯度加权到特征图上,生成热力图,然后叠加到原 patch 上。和医生沟通时,把热力图与病理标注区域对齐看,比任何数字指标都有说服力。
def grad_cam_patch(model, patch_tensor, device): model.eval() patch_tensor = patch_tensor.unsqueeze(0).to(device) # 注册 hook 拿到最后一个卷积层的输出和梯度 activations = {} def forward_hook(module, input, output): activations["out"] = output.detach() handle = model.features[9].register_forward_hook(forward_hook) pred = model(patch_tensor) pred[:, 1].backward() # 只对“恶性”类别的梯度做反传 grads = model.features[9].weight.grad # 实际应从 output 的梯度取,这里做简化说明上面的代码是简化示意。真正的 Grad-CAM 需要注意两个细节:钩子要注册在最后一个卷积层而不是分类头;backward()之后取的梯度是model.features[9].output.grad而非权重梯度。用 PyTorch 的register_full_backward_hook拿输出梯度更干净,否则算出来的热力图会偏。热力图生成后,按通道维度对特征图做加权求和、ReLU 截断、再归一化到 0~255,叠回原图。
如果热力图能集中在细胞核密集区域和异型细胞区域,说明模型的行为和病理逻辑是一致的。如果热力图高亮部位落在染色边缘或折叠区域,说明模型学到了伪影,需要回到数据预处理阶段做针对性过滤。
模型确认“看对了地方”之后,还有一个把 patch 级预测聚合到整张 WSI 的技巧:高置信投票。只让 softmax 概率超过阈值的 patch 参与投票,既能过滤背景噪声,又能避免边界 patch 的不确定预测把整体结果带偏。
def predict_slide(model, patches, device, confidence_threshold=0.7): model.eval() votes = {0: 0, 1: 0} confs = [] with torch.no_grad(): for _, _, patch in patches: patch = preprocess_patch(patch).unsqueeze(0).to(device) prob = torch.softmax(model(patch), dim=1) pred = prob.argmax(dim=1).item() conf = prob.max(dim=1).values.item() if conf >= confidence_threshold: votes[pred] += 1 confs.append(conf) if sum(votes.values()) == 0: return 0, 0.0 # 全部低置信,判为良性并返回 0 置信度 return max(votes, key=votes.get), sum(confs) / len(confs)confidence_threshold的设定要看验证集上混淆矩阵的分布。如果高置信 patch 的假阴性率仍然偏高,把阈值从 0.7 降到 0.5,观察恶性投票数是否有足够提升。还要注意,如果某张切片在恶性区域只切出了 5 个 patch,而良性区域切出了 500 个 patch,直接投票会被多数类带偏。改进做法是按 patch 数归一化后再比较两个类别的平均置信度,或者引入位置聚类(恶性 patch 应该成片出现,而不是孤立的少数几个)。
至于阈值选多少合适,可以用验证集做一个简单扫描:从 0.5 到 0.95 按 0.05 步进,画出一条假阴性率对假阳性率曲线,然后挑一个业务上能接受的当前点。这是病理 AI 落地里最朴素但有效的校准方法。
我在这个方向踩过最深的一个坑,是 tensorboard 里 loss 曲线漂亮得不行,换到外院数据直接崩盘。后来查下来,问题不是模型结构,而是切 patch 时用了随机划分,验证集里混进了同一个病人的相邻 block。从那以后,我养成了两个习惯:第一个,任何实验先按 patient 划分测试集再算指标;第二个,模型保存前一定会做一次四方向 TTA 推理,确认预测不随方向漂移。这两个习惯帮我挡掉了后面好几次“看起来好极了、实际不能用”的假象。希望帮到你。
本文还有配套的精品资源,点击获取