ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习医疗诊断系统Python源码实战:从模型训练到可解释输出

深度学习医疗诊断系统Python源码实战:从模型训练到可解释输出

简介:这份基于深度学习的医疗诊断系统Python源码,面向医疗AI开发者、深度学习初学者及相关课题研究者,可直接用于搭建医疗影像分类、病历数据分析等诊断辅助流程。资源包共48个文件,压缩后大小4.44MB,以13个Python源文件为核心,覆盖模型设计、训练与测试,另含7个pyc字节码、3个Qt界面文件、13张JPEG图像以及JSON、SQLite数据库等支撑文件,完整展现了从数据预处理、模型推理到界面交互的工程链路。系统实现了登录认证、图像诊断、病历数据管理、日志记录等关键模块,其中main.py、diagnose.py等源码展示了深度学习模型与PyQt界面集成、xlsx与sql模块处理业务数据的典型写法;界面样式由qss文件定义,配置与日志分别由conf和log模块管理,代码结构清晰,便于二次开发。目前已有454人学习下载,这份源码既能作为医疗AI项目落地的参考样板,也可作为Python工程实践与深度学习的入门案例,帮助读者理解真实系统如何组织代码、管理配置与异常日志,甚至在此基础上接入新的医学影像数据。

1. 基于深度学习的医疗诊断系统:为什么值得用 Python 从头搭一套源码

“基于深度学习的医疗诊断系统Python实现源码”这个标题,背后是一整套把影像科医生的工作方式搬进代码的做法。常见的落地形态是:输入一张胸部 X 光片、CT 切片或病理切片,模型输出“有没有病灶、病灶在哪个区域、恶性概率多高”。它适合两类人来读:一是想给自己的科研课题或产品原型搭一套可复现基线的工程师,二是刚入门深度学习、想找一个比 MNIST 更真实场景练手的 Python 开发者。这套东西真正的难点不在网络结构抄得对不对,而在数据怎么切、损失函数怎么加权、阈值怎么定——这些恰恰是网上开源项目里最容易敷衍、又最影响最终效果的部分。

2. 模型选型与损失函数:从 ResNet 到 EfficientNet 的取舍

2.1 为什么医疗场景首选 CNN 而不是直接上 Transformer

医疗诊断系统里,输入数据绝大多数是医学影像——X 光、CT、MRI 或病理全切片。这类数据的共同特点是:数据量小(一个科室攒几年的标注数据可能也就几千到几万张)、病灶尺寸占比小、类别分布极度不均衡。在这些约束下,CNN 的归纳偏置(局部连接、权值共享、平移等变性)让它天然比 Transformer 更省数据、更容易收敛。

我一般会按这样的顺序做选型:

  • 数据量在 1 万张以下:ResNet-50 或 ResNet-101 作为骨干。预训练权重好找,PyTorch 里torchvision.models一行就能加载,微调成本低。
  • 数据量在 1 万到 5 万张:换 EfficientNet-b3 或 b4,它在同样 FLOPs 下精度更高,但对预训练权重和输入分辨率更敏感。
  • 数据量在 5 万张以上、且有大显存:可以考虑 Swin Transformer 小版本,但训练技巧要求高,学习率、warmup、正则都要单独调,不建议作为第一版基线。

这里有个现实考量:医疗影像本身没有 ImageNet 里的那种物体多样性,所以很多人觉得加载预训练权重意义不大。实测下来恰恰相反,用 ImageNet 预训练权重做初始化,即便像素分布差异很大,迁移后的收敛速度和最终 auc 也显著优于随机初始化。原因在于浅层卷积核学到了通用的边缘、纹理、频率特征,这些在医学影像里同样有用。

2.2 损失函数的中医:带权重的交叉熵与 Focal Loss

医疗诊断里最常见的公开数据集(比如 ChestX-ray14、RSNA 肺炎检测)有一个共同特点:正常样本远多于阳性样本,有的标签阳性率只有 1% 到 3%。直接拿普通交叉熵训练,模型会学会“永远预测正常”来拿低 loss。

解决这个问题有两个常用手段,实践中可以组合使用:

第一个是加权交叉熵。给每个类别的 loss 乘一个权重,权重的常见设置是w = 样本总数 / (类别数 * 该类样本数)。PyTorch 里nn.CrossEntropyLoss(weight=torch.tensor([...]))直接支持这个参数,不需要手写。

第二个是 Focal Loss。它把交叉熵改造成-alpha * (1 - p)^gamma * log(p),gamma 通常在 2.0 附近。当模型对难样本的输出概率 p 较低时,(1-p)^gamma接近 1,梯度贡献保留;当模型已经预测得很自信时,这个系数会把梯度压下去。这样模型就不会被大量简单负样本带偏。

我的经验是:当正负样本比在 1:10 以内时,加权交叉熵就够用了,调参更简单;超过 1:10 时再上 Focal Loss,gamma 从 2.0 起步,alpha 按负样本比例设成 0.25 左右。不要一上来就 Focal Loss,它的超参数更多,翻车后不好定位问题。

2.3 常见的设计取舍:二分类还是多标签,单任务还是多任务

很多初学者拿到胸部 X 光数据集,看到里面有“肺不张、肺炎、气胸、结节”等十几个标签,就直接套多分类 Softmax。这是个很容易踩的坑:医疗影像的诊断标签往往是共存的——一个病人可能同时有肺炎和胸腔积液,多分类 Softmax 强制所有标签概率加起来等于 1,等于在数学上排除了共存可能。

正确的做法是拆成多标签二分类:每个标签独立一个二分类输出头,用 Sigmoid + BCEWithLogitsLoss。如果你的任务同时要“判断有没有病灶”和“定位病灶在哪”,那更合理的架构是检测网络(比如 Faster R-CNN 或 YOLOv8),它天然输出去边框加类别,而不是靠分类网络出定位能力。

多任务是另一个提效技巧:主任务是疾病分类,副任务可以是分割 mask 预测或关键点回归(比如心脏边界)。共享骨干、多分支输出,副任务能为骨干提供更强的特征监督,这在数据量不足时有明显收益。代价是标注成本翻倍、训练脚本复杂度上升,建议主任务稳定之后再加。

3. 医疗影像数据准备:归一化、窗宽窗位与按病人划分

3.1 影像读入与 NumPy 化的标准流程

医疗影像和自然图像最大的差异在读取方式。自然图像用PIL.Image.open或者cv2.imread就能拿到一个 0 到 255 的 RGB 数组,但医学影像常见格式是 DICOM(后缀 .dcm)和 NIfTI(后缀 .nii/.nii.gz)。

处理 DICOM 的标准工具是pydicom,读取后拿到的是像素值和一堆关键元数据。CT 图像的像素值单位是 Hounsfield Unit(HU),范围大约从 -1024 到 +3000 以上,直接丢给神经网络等于把数值范围完全搞乱。下面是处理 CT 切片的标准代码:

import pydicom import numpy as np def load_dicom_to_hu(dicom_path, window_center=40, window_width=400): ds = pydicom.dcmread(dicom_path) # 有些设备存的像素值带缩放,这里还原成真实的 HU if hasattr(ds, 'RescaleIntercept') and hasattr(ds, 'RescaleSlope'): slope = float(ds.RescaleSlope) intercept = float(ds.RescaleIntercept) hu = ds.pixel_array.astype(np.float32) * slope + intercept else: hu = ds.pixel_array.astype(np.float32) # 窗宽窗位:把感兴趣范围内的灰度拉伸到 [0, 1] lower = window_center - window_width / 2.0 upper = window_center + window_width / 2.0 hu = np.clip(hu, lower, upper) hu = (hu - lower) / (upper - lower) return hu

这段代码的核心是两个操作:RescaleSlope/RescaleIntercept把设备原始存储值换算成真实的 CT 值;窗宽窗位把关注范围外的像素裁掉并映射到 [0,1]。不同任务要选不同的窗宽窗位——看肺部选窗宽 1500、窗位 -600,看纵隔选窗宽 400、窗位 40。这个参数直接影响模型能看到的对比度,比换网络结构影响还大。

3.2 数据增强要克制:翻转方向与像素操作有讲究

自然图像里随手用的 RandomCrop、RandomRotation(90度、180度)在医疗影像里不能直接用。解剖结构有严格的方位语义:胸部 X 光片左右颠倒会导致心脏和主动脉弓位置错位,模型学到的是错误的位置相关性,测试时遇到正常摆位的图反而更差。

我一般保留下面几种增强,它们不破坏解剖语义:

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.5), # 左右翻转:胸部X光左右对称,可用 transforms.RandomAffine(degrees=10, translate=(0.05, 0.05)), # 小角度旋转、小位移 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]) ])

注意这里没有用垂直翻转,也没有大于 15 度的旋转。胸部 X 光如果旋转角度过大,肺尖和肋膈角的位置关系会被破坏。RandomHorizontalFlip能用是因为人体左右基本对称,但如果你做的是乳腺癌钼靶检查,左右乳在图像位置不一样、纹理不对称,连水平翻转都要慎用。增强策略要和影像科医生确认一遍再定,这是血泪经验。

3.3 数据集划分最容易忽略的一步:按病人分组切分

医疗数据集的划分有一个和自然图像完全不同的铁律:训练集、验证集、测试集必须按病人(Patient ID)分组,而不是按图像切片分组。同一个病人的多张切片之间高度相关,如果一部分进了训练集、一部分进了测试集,等于把答案提前给了模型——测试指标会被虚高很多。

常见的正确划分代码是:

import random from collections import defaultdict def split_by_patient(df, patient_col='patient_id', ratios=(0.7, 0.15, 0.15), seed=42): random.seed(seed) patients = sorted(df[patient_col].unique()) random.shuffle(patients) n_train = int(len(patients) * ratios[0]) n_val = int(len(patients) * ratios[1]) train_patients = set(patients[:n_train]) val_patients = set(patients[n_train:n_train + n_val]) test_patients = set(patients[n_train + n_val:]) df_train = df[df[patient_col].isin(train_patients)] df_val = df[df[patient_col].isin(val_patients)] df_test = df[df[patient_col].isin(test_patients)] print(f"train: {len(df_train)} 张 / {len(train_patients)} 人, " f"val: {len(df_val)} 张 / {len(val_patients)} 人, " f"test: {len(df_test)} 张 / {len(test_patients)} 人") return df_train, df_val, df_test

这段代码先按patient_id去重、打乱、按比例分成三个集合,再去 DataFrame 里筛出对应切片。这样能确保同一个病人的所有图像只出现在一个集合里。打印出来的信息里,如果发现某个集合的病人数量很少、切片很多,要当心是不是某个病人的切片数量异常大,必要时按病人权重采样做平衡。

4. PyTorch 训练与评估源码实现:训练循环、早停与 AUC

4.1 完整的训练脚本骨架(可直接改路径跑通)

下面给出一套完整的 PyTorch 训练脚本骨架,它基于 ResNet-50 预训练权重做迁移学习,适配上一节的数据划分。这套代码我反复用了很多个医疗项目,结构稳定、排错容易。

import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from torchvision import models, transforms import numpy as np from sklearn.metrics import roc_auc_score, accuracy_score, confusion_matrix class MedicalDataset(Dataset): def __init__(self, df, img_dir, transform=None, target_col='label'): self.df = df.reset_index(drop=True) self.img_dir = img_dir self.transform = transform self.target_col = target_col def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] img_path = f"{self.img_dir}/{row['image_id']}.npy" img = np.load(img_path) # 内存里存处理好的npy,避免DICOM读盘耗时 img = torch.from_numpy(img).float().unsqueeze(0) # [1, H, W] img = img.repeat(3, 1, 1) # 灰度转3通道,适配ImageNet预训练 if self.transform: img = self.transform(img) label = torch.tensor(row[self.target_col], dtype=torch.float32) return img, label def build_model(num_classes=1, model_name='resnet50'): if model_name == 'resnet50': model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) elif model_name == 'efficientnet_b3': model = models.efficientnet_b3(weights=models.EfficientNet_B3_Weights.IMAGENET1K_V1) in_features = model.classifier[1].in_features model.classifier[1] = nn.Linear(in_features, num_classes) return model def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, total_correct, total_num = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() logits = model(imgs).squeeze(1) loss = criterion(logits, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) preds = (torch.sigmoid(logits) >= 0.5).long() total_correct += (preds == labels.long()).sum().item() total_num += imgs.size(0) return total_loss / total_num, total_correct / total_num

这段代码的关键设计有三处,对应三个实际项目里容易翻车的点:

一是把 DICOM 预处理成 npy 存在本地而不是每次读.dcm。DICOM 解析非常慢,训练时实时解析二进制的开销会让每个 epoch 多出几倍时间。离线批量把 DICOM 转成 npy 后,__getitem__只做数组加载,速度快一个数量级。

二是灰度图复制三通道。ImageNet 预训练模型要求 3 通道输入,把单通道复制三次是最简单且不引入信息损失的适配方式。不要随便用伪彩色映射,那会改变像素语义。

三是分类头改成nn.Linear(in_features, 1)输出单值,配合BCEWithLogitsLoss。这是单标签二分类的标准组合,squeeze(1)把 batch 维度恢复,避免 shape 不匹配。

4.2 验证与早停:只看 loss 不够,要看 AUC

医疗诊断场景里,验证阶段通常关注两个指标:auc(ROC 曲线下面积)和敏感度/特异度这对组合。准确率在正负样本比 1:9 的场景下参考价值很低——模型全部预测负样本,准确率也有 90%。

完整的验证和早停逻辑我一般这样写:

def evaluate(model, loader, device): model.eval() all_labels, all_preds, all_probs = [], [], [] with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) logits = model(imgs).squeeze(1) probs = torch.sigmoid(logits) all_labels.append(labels.cpu().numpy()) all_probs.append(probs.cpu().numpy()) all_preds.append((probs >= 0.5).long().cpu().numpy()) y_true = np.concatenate(all_labels) y_prob = np.concatenate(all_probs) y_pred = np.concatenate(all_preds) auc = roc_auc_score(y_true, y_prob) acc = accuracy_score(y_true, y_pred) tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel() sensitivity = tp / (tp + fn) if (tp + fn) > 0 else 0.0 specificity = tn / (tn + fp) if (tn + fp) > 0 else 0.0 return {'auc': auc, 'acc': acc, 'sensitivity': sensitivity, 'specificity': specificity} best_auc = 0.0 patience = 8 bad_epochs = 0 save_path = 'best_model.pth' for epoch in range(1, 31): train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) val_metrics = evaluate(model, val_loader, device) print(f"Epoch {epoch:02d} | train_loss {train_loss:.4f} | train_acc {train_acc:.4f} | " f"val_auc {val_metrics['auc']:.4f} | val_sens {val_metrics['sensitivity']:.4f}") if val_metrics['auc'] > best_auc: best_auc = val_metrics['auc'] bad_epochs = 0 torch.save({'model_state_dict': model.state_dict(), 'epoch': epoch, 'val_auc': best_auc}, save_path) print(f" -> 保存新最优模型,val_auc={best_auc:.4f}") else: bad_epochs += 1 if bad_epochs >= patience: print(f"连续 {patience} 轮没有提升,早停。") break

4.3 训练参数怎么设:抄这套基线再微调

医疗影像的迁移学习训练参数和从头训练差很多,我常用的基线参数是下表这样,直接作为起点通常能得到一个不错的结果:

参数推荐值说明
优化器AdamW比 Adam 多了权重衰减解耦,正则更稳
初始学习率3e-5 ~ 1e-4骨干用预训练权重,学习率必须小,否则微调震荡
权重衰减1e-4 ~ 5e-4对医疗小数据集非常关键
Batch size16 / 32取决于显存;小于 16 时考虑增大学习率衰减频率
学习率调度CosineAnnealingLR总 epoch 30~50 时效果好
输入分辨率224×224 起步,可提到 384病灶小的话 384 有明显提升,但显存翻倍

一个容易忽略的参数是骨干网络和分类头要用不同的学习率。分类头是随机初始化的,梯度尺度大;骨干是预训练权重,只需要微调。常见做法是给骨干乘 0.1 的系数,代码上可以用两个param_group:

optimizer = torch.optim.AdamW([ {'params': model.fc.parameters(), 'lr': 1e-4}, {'params': [p for n, p in model.named_parameters() if 'fc' not in n], 'lr': 1e-5} ], weight_decay=1e-4)

这个设置的逻辑是:新初始化的分类头需要更大的步长去拟合目标任务,而骨干特征提取器改动过大会遗忘 ImageNet 上学到的通用特征,小学习率让它在原有特征空间里做微调。

5. 避坑排查:医疗 AI 最常见的 5 个翻车现场

5.1 验证集 AUC 极高但测试集崩了:大概率是数据划分泄漏

现象:验证集 AUC 能到 0.98,测试集 AUC 却只有 0.75,且差的不是一点点。

原因:最常见的是同一个病人的切片同时出现在训练集和测试集。肺部 CT 一个病人动辄几百张切片,相邻切片之间的相似度极高,模型其实在记忆病人特征而不是病灶特征。另一个常见泄漏源是数据预处理时用了全局统计量(比如用整个数据集的均值和标准差做归一化),信息间接泄漏。

解决:严格按第 3.3 节的split_by_patient按病人分集合。归一化的均值和标准差只从训练集统计,保存下来供验证集和测试集复用。检查原始数据里有没有重复图像名,用 MD5 哈希去重是最后一道保险。

5.2 训练 loss 正常下降,但敏感度一直为 0:阈值问题还是类别权重问题

现象:AUC 有 0.9,但设阈值 0.5 时敏感度极低,阳性样本几乎全被判定为阴性。

原因:这不是模型坏了,而是正负样本比悬殊时,模型输出概率整体偏低,0.5 这个默认阈值完全不适合。AUC 只看排序能力,不看绝对概率值,所以 AUC 高和 0.5 阈值下敏感度低可以同时成立。

解决:不要在 0.5 处卡死,改用验证集寻找最优阈值。最常用的方法是遍历概率阈值,找约登指数(Youden"s J = 敏感度 + 特异度 - 1)最大的点。实际部署时还要考虑临床需求——如果任务是筛查(目的是不漏掉病人),宁可特异度低一些也要优先保证敏感度。这个取舍要写进模型报告,不能只给一个 AUC 数字。

5.3 用了预训练权重反而比随机初始化差:输入通道和预处理不匹配

现象:加载 ImageNet 权重后,训练初期 loss 比随机初始化还高,最终收敛也差。

原因:有三类问题会触发这个现象。第一,输入不是 3 通道,或者用ToTensor把值映射到 [0,1] 同时又把 Mean/Std 设成 ImageNet 的统计量,两套归一化打架。第二,Resize 尺寸和预训练时的尺寸不一致,比如预训练用 224 微调用 96,卷积感受野完全变形。第三,输入图像有大量黑色背景,全局均值拉低,等于喂给模型一张“灰蒙蒙”的图。

解决:统一走transforms.ToTensor()+Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),保证输入分布接近 ImageNet 分布。图像中背景占比超 30% 时,考虑用肺部分割 mask 裁掉背景再进网络,这个预处理通常能带来 3 到 5 个点的 AUC 提升。

5.4 显存不够,batch size 调到 4:训练不稳定怎么办

现象:8G 显存跑 ResNet-50 224 输入,batch size 只能设 4,loss 曲线震荡剧烈,验证指标上不去。

原因:batch size 太小导致每个 batch 的梯度方差大,BN 层的统计量也不稳定。这不是调学习率能解决的,是批量统计本身的数学问题。

解决:两层办法。第一层,用梯度累积模拟大 batch。第二层,把输入分辨率从 224 降到 192 或 176,ResNet-50 显存占用会明显下降,分辨率略降对病灶检测的影响通常小于 batch 太小的影响。第三层,如果任务里病灶尺寸很小,干脆换轻量骨干如 ResNet-34 或 EfficientNet-b1。注意梯度累积时学习率要按真实 batch 倍数上调,否则收敛变慢。

accum_steps = 4 # 实际batch = 4 * 4 = 16 optimizer.zero_grad() for step, (imgs, labels) in enumerate(loader): logits = model(imgs.to(device)).squeeze(1) loss = criterion(logits, labels.to(device)) loss = loss / accum_steps # 平均每个mini-batch的loss loss.backward() if (step + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()

5.5 模型反复预测同一类别:标签噪声和数据质量的问题

现象:不管输入什么图,模型输出概率都偏向某个类别,例如总是预测“正常”。

原因:除了类别不平衡,更隐蔽的原因是标签噪声——医疗影像标注本身就存在 observer variability,同一个病灶不同医生判读结果不一定一致。如果训练集里阳性样本有 20% 是误标,模型学到的是“阳性样本长这个样 + 一部分乱标噪声”,预测自然会偏向样本量大的正常类。

解决:先做标签清洗。最简单有效的方式是用模型本身的预测做交叉验证,把训练集里多次预测为相反类别的样本筛出来人工复审。也可以做 Hard Example Mining:每个 epoch 结束按 loss 排序,挑出 loss 最高的 20% 样本,打印出来人工看一遍,通常能发现一批标注错误。这个步骤脏,但收益大,很多项目靠清洗标签比换网络涨点更多。

6. 用 Grad-CAM 给模型做体检:热力图验证与临床交付习惯

模型训练完、AUC 也达标了,距离能交付还差一步:医生不会相信一个黑匣子。即使你只是做技术原型,也要能回答“模型根据什么做的判断”这个问题。Grad-CAM 是最常用也最好解释的工具,它用最后一层卷积特征图的梯度加权来生成热力图,标出模型注意力集中在图像哪个区域。

下面这段代码基于 PyTorch 实现 Grad-CAM,核心是注册两个 hook 分别拿到中间层特征图和梯度:

import cv2 import numpy as np import torch class GradCAM: def __init__(self, model, target_layer): self.model = model self.gradients = None self.activations = None handle_a = target_layer.register_forward_hook(self._save_activation) handle_g = target_layer.register_full_backward_hook(self._save_gradient) def _save_activation(self, module, input, output): self.activations = output.detach() def _save_gradient(self, module, grad_input, grad_output): self.gradients = grad_output[0].detach() def generate(self, img_tensor): logits = self.model(img_tensor.unsqueeze(0)) self.model.zero_grad() logits[0, 0].backward() # 对目标类别的logit反向传播 weights = self.gradients.mean(dim=(2, 3), keepdim=True) cam = (weights * self.activations).sum(dim=1, keepdim=True) cam = torch.relu(cam).squeeze().cpu().numpy() cam = cv2.resize(cam, (img_tensor.shape[2], img_tensor.shape[1])) cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8) return cam

这段代码里值得注意的有两点。一是register_full_backward_hook拿到的grad_output是 loss 对输出 feature map 的梯度,Grad-CAM 需要的就是它;二是cam做了一次cv2.resize把热力图恢复到输入图像分辨率,叠加显示时才能对齐。生成热力图后,把一个批次的假阳性和假阴性样本全部可视化出来,人工过一遍——如果模型把正常区域标红,说明它学到了数据集里的伪影(比如图片角落的设备标签、病床边框),必须回到数据层面清洗掉。

这个习惯我在所有项目里都保留:每次训练出一个候选模型,先跑一批 Grad-CAM,再谈指标。有一次模型 AUC 0.93,热力图显示它的注意力集中在每张图的左上角——那里恰好是设备水印的位置。删掉这批带水印的图重新训练,AUC 降到 0.90,但热力图终于落在肺野上了。前者是好看的假成绩,后者才是能上线的东西。

我现在的交付习惯是:每条测试样本都保留模型输出概率、Grad-CAM 热力图和医生判读记录三件套。术前验证时,让医生直接看热力图评“有没有道理”,比任何数值指标都更有说服力。希望这个从数据到可解释输出的完整路径,能帮你少走几次弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进