
简介结合深度学习与医疗诊断的Python工程源码面向医疗AI开发者、算法工程师及Python学习者可用于快速搭建医疗影像或病历数据分析的诊断系统。资源共48个文件压缩包仅4.44MB包含13个Python源代码覆盖模型设计、训练、测试及主流程、13张JPG图像用作医学影像样例或结果展示、7个pyc编译文件、3个UI界面文件以及日志、数据库、JSON配置、样式表等辅助文件目录结构清晰便于按模块查阅。目前已有454人学习下载。源码整合了登录、图像诊断、日志记录、SQLite数据存储、Excel处理等模块并提供Qt界面与样式配置可完整体现从医疗数据读取到诊断结果输出的工程链路对希望了解深度学习在医疗场景落地细节、或需要参考完整Python工程组织方式的读者具有直接的实操借鉴价值。1. 基于深度学习的医疗诊断系统先想清楚它能做什么再动手写代码去医院拍一张胸片医生盯着屏幕上黑白影像几分钟后给出结论疑似肺炎、建议CT复查。这个场景里诊断依赖的是医生肉眼观察和经验积累。而基于深度学习的医疗诊断系统Python实现源码做的就是把这双眼睛换成卷积神经网络输入影像输出病变类别、位置和置信度医生只需复核结果。我做了几年医学影像AI最深的体会是——很多人把这个项目当成普通图像分类来做用一套通用代码直接跑医疗数据结果训练出来的模型看似准确率高达95%一放到真实影像上就失灵。问题不在深度学习而在你压根没弄清楚医疗诊断的独特约束样本量小、类别极度不平衡、标注噪声大、漏诊代价远高于误诊、设备之间的成像差异巨大。这套源码的价值不在于把ResNet跑通而是把数据处理、模型训练、评估验证、可解释性分析这一整条链路按医疗场景的要求重新组织。它适合两类人医工交叉方向的学生想用源码快速搭建实验原型以及刚转行医疗AI的工程师想在一个完整的系统里理解医疗模型的训练逻辑和陷坑。2. 系统整体架构与医学数据集准备数据质量决定诊断上限2.1 医疗诊断系统的四层架构从影像输入到可解释输出我见过很多初学者的源码把医疗诊断系统写成单个Python文件读图、缩放、扔进模型、打印结果。这种做法跑demo没问题但离系统差了十万八千里。一个能实际支撑实验迭代的医疗诊断系统至少要拆成四个清晰的层次。数据层负责影像读取、格式解析和预处理。医疗影像和自然图像最大的不同在于来源格式复杂胸部X光可能是DICOM格式皮肤镜图像可能是JPG病理切片是SVS等金字塔格式。预处理必须考虑像素值含义DICOM里的像素值不是简单的0-255而是和窗宽窗位、设备校准相关的原始值。模型层承担特征提取和诊断预测典型选择是ResNet、EfficientNet、DenseNet这类CNN骨干或者Vision Transformer。医疗场景里做迁移学习是主流因为医学数据集通常只有几千到几万张从头训练一个大模型几乎没有可能收敛。评估层负责输出医学指标包括AUC、灵敏度、特异度、F1分数不是只算准确率。训练层则包含损失函数设计、样本加权策略、学习率调度、早停等训练细节。这四个层次在源码里应该对应不同的目录和模块数据层对应dataloader模块模型层对应models目录评估层对应metrics模块训练层对应train脚本。切分清楚之后替换数据集、换backbone、改损失函数就变成改一个模块的事而不是在几百行代码里翻找。这种组织方式也方便后续把训练好的模型导出为ONNX或TensorRT部署因为推理路径和数据训练路径是解耦的。2.2 公开医学数据集的选择CheXpert、ISIC、RSNA怎么取舍开始写代码之前先把数据定下来。医疗诊断系统的源码通常配套使用公开的医学影像数据集。我用过的数据集里这三个最值得优先考虑CheXpert是斯坦福发布的胸部X光数据集包含超过22万张影像每张图标注了14种胸部病变标签适合做多标签分类ISIC是国际皮肤成像协作组发布的皮肤镜图像数据集包含超过2.5万张皮损图像标签包括黑色素瘤、脂溢性角化病等适合做单标签多分类RSNA肺炎检测数据集由RSNA联合NIH发布包含约3万张胸片标注了肺炎的边界框和类别适合做目标检测。选择数据集时不要只看规模。CheXpert虽然样本量大但有明显的标注噪声问题——部分标签通过自然语言处理从放射报告中自动提取准确性低于人工标注。ISIC的类别分布极度不均黑色素瘤只占很小比例这直接决定了你要用加权损失还是重采样策略。下载完数据后先做一次目录扫描写一段脚本检查文件完整性把损坏图片、尺寸异常图片筛出来。医疗公开数据集中偶发文件损坏不处理的话训练到一半会直接报错浪费几个小时。数据集准备好之后按7:2:1的比例划分训练集、验证集和测试集。注意划分时按患者维度进行同一个患者的多次检查影像必须放进同一个集合否则会导致数据泄露模型在测试集上的AUC虚高0.05以上部署时立刻现原形。2.3 数据加载与增强用PyTorch写出工业级DataLoader这块是源码里最容易出问题的部分。医疗影像数据加载有两个核心诉求一是内存不能爆炸二是数据增强不能破坏医学特征。先看基础实现用CheXpert格式的CSV标签文件为例import pandas as pd import torch from torch.utils.data import Dataset from PIL import Image import os class CheXpertDataset(Dataset): def __init__(self, csv_path, img_dir, transformNone): self.df pd.read_csv(csv_path) self.img_dir img_dir self.transform transform # 选14个病变类别标签排除PatientID等非标签列 self.label_cols [c for c in self.df.columns if c not in [Path, Sex, Age, Frontal/Lateral, AP/PA]] def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(os.path.join(self.img_dir, row[Path])).convert(RGB) # 对于NA标签统一填充为0后续用loss权重屏蔽掉 label torch.tensor( [0.0 if pd.isna(row[c]) else float(row[c]) for c in self.label_cols] ) if self.transform: img self.transform(img) return img, label这个实现的关键点在于标签处理。CheXpert的标注里存在NA值表示该类别没有被明确提及。常见做法是把NA填充为0表示阴性但更严谨的做法是在损失函数里为NA样本设置mask让模型不学习这些不确定的标签——因为NA并不等同于阴性。源码里通常会提供一个带mask的损失函数版本建议优先使用。接下来是数据增强。医疗影像有两类增强不能乱用水平翻转在大多数部位可用但左右结构不对称的部位要小心颜色抖动、高斯噪声对自然图像有效但对医学影像可能改变病理特征的表现。我的做法是用更温和的增强组合并把强度调小from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomAffine(degrees5, translate(0.02, 0.02)), # 轻微旋转和平移 transforms.ColorJitter(brightness0.1, contrast0.1), # 低强度色彩扰动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])参数说明RandomAffine的degrees设为5度而不是通用代码里的15度translate设为0.02而不是0.1因为医学影像的解剖结构相对固定大幅度旋转变换会生成不符合生理结构的样本。ColorJitter的亮度对比度控制在0.1以内过强的色彩扰动会让模型学到设备差异而非病变特征。Normalize使用ImageNet预训练权重的标准参数因为我们要做迁移学习输入分布需要和预训练权重对齐。3. 诊断模型搭建与训练迁移学习是唯一不翻车的路线3.1 用EfficientNet做backbone把预训练权重迁移到医学影像医疗影像数据集的规模通常在几千到几万张从零训练一个深度CNN几乎不可能动辄几十上百个epoch过拟合还没到底训练时间先爆炸。迁移学习是行业内最稳妥的方案用ImageNet上预训练好的模型权重做初始化然后到医学数据上微调。backbone选择上ResNet50和EfficientNet-B0/B1是医疗诊断源码中最常见的两个选择。ResNet50成熟稳定显存占用低调试方便很多论文的baseline都是它。EfficientNet通过复合缩放深度、宽度、分辨率同时调整在相同参数量下精度更高但EfficientNet的BatchNorm参数在模态差异大的数据集上更容易失稳。我的习惯是数据量在1万以下先用ResNet50跑通流程有余力再换EfficientNet调精度。用PyTorch加载预训练权重的标准写法如下import torchvision.models as models import torch.nn as nn def build_model(num_classes14, model_nameefficientnet_b0, pretrainedTrue, dropout0.3): if model_name efficientnet_b0: weights models.EfficientNet_B0_Weights.IMAGENET1K_V1 if pretrained else None model models.efficientnet_b0(weightsweights) in_features model.classifier[1].in_features model.classifier nn.Sequential( nn.Dropout(pdropout, inplaceTrue), nn.Linear(in_features, num_classes) ) elif model_name resnet50: weights models.ResNet50_Weights.IMAGENET1K_V1 if pretrained else None model models.resnet50(weightsweights) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model这个build_model函数有两个关键设计一是把dropout单独提出来作为参数医疗场景中过拟合风险高dropout通常要比ImageNet默认值大一些0.3到0.5之间调二是替换分类头时保留了原模型的全局池化层和特征提取部分只换掉最后一层映射这能让预训练的特征提取能力完整保留下来。训练策略上有两个流派一是冻结backbone只训练分类头适合数据量极少的场景二是全网络微调学习率设小一点适合数据量相对充足的场景。我一般先冻结backbone跑5个epoch观察验证集AUC是否持续上升如果上升就解冻全部参数用1/10的学习率做微调。这比直接全量微调稳得多也更容易排查梯度爆炸的问题。3.2 类别不平衡下的训练策略Focal Loss与加权采样医疗数据的类别分布极其残酷。以CheXpert为例肺不张的阳性率可能只有5%肺水肿可能不到3%ISIC的黑色素瘤占比同样很低。如果用标准交叉熵训练模型会学出一个全部预测为阴性的偷懒方案——准确率很高但毫无诊断价值。处理不平衡有两类手段它们解决的是不同层面的问题。第一类是重采样让每个batch里正负样本比例接近第二类是改损失函数让少数类样本的梯度贡献更大。两者可以同时用但我的习惯是先上损失函数不够再加采样。Focal Loss是医疗诊断源码里最常见的损失函数选择它通过调制因子降低易分类样本的权重让模型聚焦在难分类的少数类样本上import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alphaNone, gamma2.0, reductionmean): super().__init__() self.gamma gamma self.reduction reduction # alpha可以是标量正样本权重也可以是每个类别的权重向量 self.register_buffer(alpha, alpha) def forward(self, logits, targets, maskNone): ce_loss F.binary_cross_entropy_with_logits( logits, targets, reductionnone ) p torch.sigmoid(logits) # 调制因子(1-p)^gamma易分类样本p接近1权重趋向0 p_t p * targets (1 - p) * (1 - targets) focal_weight (1 - p_t) ** self.gamma if self.alpha is not None: # 正样本乘alpha负样本乘(1-alpha) alpha_t self.alpha * targets (1 - self.alpha) * (1 - targets) focal_weight focal_weight * alpha_t loss focal_weight * ce_loss if mask is not None: loss loss * mask # mask0的位置不参与梯度计算 if self.reduction mean: return loss.sum() / mask.sum() if mask is not None else loss.mean() return loss.sum()参数说明gamma控制困难样本聚焦程度gamma2是Focal Loss论文的默认值在医疗场景中建议从1.0开始调——gamma过大容易让模型对噪声标签过度敏感把标注错误的困难样本当成重要学习对象。alpha用于调整正负样本权重当正样本占比约5%时alpha设为0.75左右效果较好具体数值可以在0.5到0.9之间做网格搜索。注意这个实现是multi-label版本每个类别独立做sigmoid二分类适合CheXpert这种一张图多个标签的数据集。如果是ISIC这种单标签多分类alpha应该设为类别先验概率的倒数gamma依然按1到2调整。两者不能混用这是初学者最常见的混淆点。3.3 训练循环完整实现验证集监控与模型保存训练循环本身不复杂但医疗场景有几个特殊要求一是必须同时监控训练集和验证集指标因为医疗数据噪声大验证集AUC的震荡幅度比自然图像大得多二是模型保存条件不能只看验证AUC还要考虑灵敏度与特异度的平衡三是训练过程要记录每个epoch的指标到CSV文件方便后续复盘。以下是一个可直接使用的训练循环核心代码import torch import numpy as np from sklearn.metrics import roc_auc_score def train_one_epoch(model, dataloader, criterion, optimizer, device, accumulation_steps2): model.train() running_loss 0.0 all_targets [] all_probs [] for i, (images, labels) in enumerate(dataloader): images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) loss loss / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad() running_loss loss.item() * accumulation_steps probs torch.sigmoid(outputs).detach().cpu().numpy() all_probs.append(probs) all_targets.append(labels.cpu().numpy()) y_true np.concatenate(all_targets, axis0) y_prob np.concatenate(all_probs, axis0) return running_loss / len(dataloader), y_true, y_prob梯度累积的设置在显存不足时非常实用。accumulation_steps2意味着把2个batch的梯度累计后再更新一次参数等效于batch_size扩大2倍。医疗影像分辨率高一个batch往往只能塞下16或32张图梯度累积用较小的显存模拟较大的batch_size对BatchNorm层在batch较小时的震荡有抑制作用。训练完成后保存模型时要同时保存两份信息checkpoint { model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_auc: best_auc, config: {model_name: model_name, num_classes: num_classes}, } torch.save(checkpoint, fcheckpoints/chexpert_{model_name}_best.pt)保存optimizer状态字典的目的是支持断点续训医疗数据的训练时长通常以数小时计中途断电、CUDA OOM是家常便饭。没有optimizer状态恢复训练后学习率调度器会错乱导致后续epoch的效果打折扣。4. 模型评估与可视化医疗场景的指标体系和Grad-CAM热力图4.1 多指标协同评估AUC、灵敏度、特异度、混淆矩阵一个都不能少自然图像分类任务看Top-1准确率就够了医疗诊断不行。一个所有病例都预测为正常的模型准确率可以达到90%以上但它在临床上是完全不可用的。医疗场景里每一个误诊都有代价漏诊一个恶性肿瘤的后果远大于误诊一个良性病变。所以医疗诊断系统的评估模块必须输出一组互补的指标。AUC衡量模型排序能力即随机取一个正样本和负样本模型给正样本打分更高的概率它对类别不平衡不敏感是模型性能的首筛指标。灵敏度Recall衡量所有真实病变中有多少被找出来了医疗场景里灵敏度通常要求很高——宁可多看几个假阳性也不能漏掉真病变。特异度衡量所有正常样本中有多少被正确判为正常它决定了模型会不会过度报警浪费医生的复核时间。误诊率是这两者的此消彼长。评估代码的骨架如下from sklearn.metrics import roc_auc_score, confusion_matrix, roc_curve def evaluate_model(model, dataloader, device, label_cols, threshold0.5): model.eval() all_targets [] all_probs [] with torch.no_grad(): for images, labels in dataloader: images images.to(device) outputs model(images) probs torch.sigmoid(outputs).cpu().numpy() all_probs.append(probs) all_targets.append(labels.numpy()) y_true np.concatenate(all_targets, axis0) y_prob np.concatenate(all_probs, axis0) aucs {} for i, col in enumerate(label_cols): if len(np.unique(y_true[:, i])) 1: # 跳过全阴性的类别 aucs[col] roc_auc_score(y_true[:, i], y_prob[:, i]) y_pred (y_prob threshold).astype(int) return {auc: aucs, y_true: y_true, y_prob: y_prob, y_pred: y_pred}评估时的阈值选择是另一个坑。默认threshold0.5在类别不平衡的数据集上往往不适用因为模型输出的概率分布整体偏低0.5的截断会把大多数正样本判为阴性。正确的做法是画ROC曲线然后根据临床需求在那个权衡点上选阈值——如果目标是高灵敏度筛查选择灵敏度达到0.95对应的阈值如果目标是减少假阳性选约登指数Youdens Index灵敏度特异度-1最大的点。源码里通常预留一个threshold参数建议在评估脚本里把0.3到0.7每隔0.05跑一遍输出每个阈值下的混淆矩阵和F1分数再做决定。4.2 Grad-CAM可视化让诊断结果不只是黑匣子医生不会因为模型输出一个肺炎置信度0.82就信任它。医疗诊断系统设计的初衷是辅助决策不是替代医生。这就要求模型不仅给出结论还能指出为什么这样判断——Grad-CAM热力图就是干这个的。Grad-CAM的核心原理是通过目标类别的梯度计算特征图各通道的权重再加权求和得到空间注意力。在PyTorch里最简洁的实现方式是注册forward hook和backward hook在推理时同时拿到特征图和梯度import cv2 import numpy as np import torch def grad_cam(model, img_tensor, target_class, device, feature_layerNone): img_tensor: 预处理后的图像shape(1, 3, H, W) target_class: 要解释的类别索引多标签场景下每个阳性类别分别做 gradients {} activations {} # 默认取最后一个卷积块的输出EfficientNet的features模块最后一层 if feature_layer is None: feature_layer model.features def forward_hook(module, input, output): activations[value] output.detach() def backward_hook(module, grad_input, grad_output): gradients[value] grad_output[0].detach() # 注册hook hook_f feature_layer.register_forward_hook(forward_hook) hook_b feature_layer.register_full_backward_hook(backward_hook) model.eval() img_tensor img_tensor.to(device) output model(img_tensor) # 多标签分类用sigmoid单类别用softmax probs torch.sigmoid(output) score probs[0, target_class] model.zero_grad() score.backward() hook_f.remove() hook_b.remove() # 梯度全局平均池化作为通道权重 weights gradients[value].mean(dim(2, 3), keepdimTrue) cam (weights * activations[value]).sum(dim1, keepdimTrue) cam F.relu(cam) # 只保留正向贡献 cam F.interpolate(cam, size(img_tensor.shape[2], img_tensor.shape[3]), modebilinear, align_cornersFalse) cam cam.squeeze().cpu().numpy() # 归一化到0-1 cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam def overlay_heatmap(img_path, cam, alpha0.4): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) heatmap cv2.resize(heatmap, (img.shape[1], img.shape[0])) overlay cv2.addWeighted(img, 1 - alpha, heatmap, alpha, 0) return overlay这段代码的核心是register_full_backward_hookPyTorch 1.8之前的旧接口register_backward_hook在某些版本上取不到梯度值如果你复现时grad_output是空的检查一下PyTorch版本切换到完整后向钩子。Grad-CAM的局限是只用最后一个卷积层感受野大热力图偏粗。精细结构调整时可以用中间层的输出叠加但不建议初学者一上来就把可视化做得过于复杂先把粗粒度热力图做出来确认模型确实在学习病灶区域再优化细节。4.3 测试集与外部验证跨中心泛化的黑匣子与玄学手法训练集和验证集上的指标只能说明模型在同类数据分布上表现良好真实世界里的泛化是更残酷的考验。不同医院使用的X光设备型号不同成像参数不同甚至同一台设备不同批次的图像噪声水平都有差异。模型在一个数据集上训练后换到另一个医院的影像上性能下降5%到10%是正常现象。这部分的正确策略是保留一个完全独立的测试集——来源不同或至少有明确时间跨度的数据在模型训练和调参的整个过程中绝不触碰。源码里通常会把evaluation脚本和train脚本分开这就是为了强制部署前再做一次最终验证。如果条件允许做跨数据集验证会更有说服力用CheXpert训练在NIH的ChestX-ray14上测试。你会发现AUC普遍下降这是正常的关键要看下降幅度是否在接受范围内。如果下降超过10个百分点说明模型过拟合了训练集的设备特征而非病变特征需要回到数据增强层考虑加入随机对比度扰动或频域适配来增强模型对成像差异的鲁棒性。5. 避坑基于深度学习的医疗诊断系统最常翻车的5个地方5.1 现象训练正常但验证AUC只有0.6接近随机猜测我遇到过不止一次这种情况训练日志一切正常损失函数持续下降训练集AUC已经0.95验证集AUC却低得离谱。最先想到的是数据划分出了问题——数据泄露造成了虚高的训练集指标或者验证集本身太特殊。检查一下划分代码是否按患者维度做了分组同一个病人的多张片子是否被同时分到训练集和验证集。还有一个高频原因是CSV文件按时间排序后直接切分导致训练集和验证集的设备批次不同分布偏移过大。解决方法是按患者ID做随机分组并在划分前对数据做shuffle确保两组数据在病变类型、严重程度上的分布接近。5.2 现象类别不平衡导致模型学出全阴性捷径训练过程中如果你的观察指标只有准确率模型很快会学到一个捷径——所有样本预测为阴性准确率可以达到90%以上而且损失函数也在正常下降只是下降幅度越来越小。这说明交叉熵损失在极度不平衡的数据上失去了对正样本的惩罚信号。不要在训练时才想起来处理不平衡在构建数据集之后、训练之前就统计各类别的样本分布计算正样本比例。如果正样本低于10%需要选择加权交叉熵、Focal Loss或重采样三者可以按模型表现逐步叠加。验证方式很简单打印模型在验证集上预测的类别分布如果预测为阳性的样本数几乎为零说明模型已经摆烂赶紧改损失函数。5.3 现象训练时正常推理时在CPU上慢到不可接受医疗诊断系统真正的部署环境往往是医院的普通PC没有NVIDIA显卡。一个ResNet50模型在GPU上推理一张图只要几十毫秒到了CPU上可能要一两秒如果是EfficientNet-B4这种稍大的模型CPU推理时间可能超过5秒。急诊医生可没耐心等这么长时间。在开发早期就要考虑推理效率问题别等训练完了再想到要部署。先量化模型——把FP32权重转成ONNX格式并开启int8量化一般能把推理速度提升3到5倍然后测量CPU上的延迟如果还不能接受就把backbone换成MobileNetV3或EfficientNet-Lite。真正上线前的压测一定是模拟真实环境测试集要覆盖不同分辨率的输入因为实际部署输入的图像大小可能和你训练时的固定尺寸不一致。5.4 现象依赖环境不一致换一台机器就崩溃医疗项目的开发环境千奇百怪——有人用Python 3.8有人用3.11有人电脑上装的是CUDA 11.3有人是CUDA 12.1torchvision的接口在版本之间经常变化。源码写得再完美环境不统一也跑不起来。项目里必须带上requirements.txt并且标注每个包的版本号。更稳妥的做法是给出固定版本的安装命令。我见过太多次我这能跑啊的情况最后发现是对方的PyTorch版本太新导致某个API变了。如果源码里用到了register_full_backward_hook这类新接口要确认你的PyTorch版本在1.8以上否则会直接报错。5.5 现象过拟合严重训练和验证差距越拉越大医疗数据量小、噪声大过拟合几乎是必然的。主要表现是训练集AUC持续上升验证集AUC在第10个epoch左右见顶后开始下降。不要等到第50个epoch才来判断过不过拟合每个epoch都保存验证集指标设置早停——验证AUC连续10个epoch没有提升就停止训练恢复最佳权重。Dropout、数据增强、权重衰减都要做但更重要的是确认增强手段对医学影像的合理性。另外要特别注意测试集上做多次推理取平均的TTATest-Time Augmentation这个技巧对医学影像有效但代价是推理时间翻倍自主判断是否值得。6. 从代码到可用系统ONNX导出与本地Web诊断服务训练好的模型不能只是Jupyter notebook里的一个实验记录得有实际的使用界面。我通常把系统收尾在两层部署把PyTorch模型导出为ONNX格式再用Flask封装一个极简的Web诊断服务。这样做的好处是浏览器里上传一张图片就能看到诊断结果和热力图直观感受完整的业务流程后续要接DICOM工作站或PACS系统也有明确的代码路径。ONNX导出的核心代码只需几行import torch.onnx model build_model(num_classes14, model_nameefficientnet_b0, pretrainedFalse) checkpoint torch.load(checkpoints/chexpert_efficientnet_b0_best.pt, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, chexpert_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version14 )dynamic_axes让模型支持batch维度为任意值这样既可以单张图推理也可以批量处理目录下的多张影像。ONNX的兼容性坑主要在opset版本上opset_version14兼容大部分ONNX Runtime版本如果想在ARM平台部署需要进一步转成ORT的格式。ONNX Runtime的推理时可以做优化import onnxruntime as ort import numpy as np from PIL import Image sess_options ort.SessionOptions() sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL ort_session ort.InferenceSession(chexpert_model.onnx, sess_options) def predict_single_image(img_path): img Image.open(img_path).convert(RGB).resize((224, 224)) img_array np.array(img, dtypenp.float32) / 255.0 # 按ImageNet的均值和标准差做归一化 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img_array (img_array - mean) / std img_array np.transpose(img_array, (2, 0, 1))[None, ...] outputs ort_session.run(None, {input: img_array.astype(np.float32)})[0] probs 1 / (1 np.exp(-outputs)) # sigmoid激活 return np.squeeze(probs)要注意的是这个predict_single_image函数里的预处理必须和训练时的预处理完全一致包括resize、归一化、通道顺序。预处理不一致是部署后模型效果变差最常见的隐性原因。Web服务我用Flask搭一个最小的接口只做三件事接收上传图片、调用ONNX推理、返回JSON格式的诊断结果并附带热力图。核心代码from flask import Flask, request, jsonify, send_file import io app Flask(__name__) app.route(/predict, methods[POST]) def predict(): if file not in request.files: return jsonify({error: no file uploaded}), 400 f request.files[file] img_path f/tmp/{f.filename} f.save(img_path) probs predict_single_image(img_path) # 设定阈值为0.35高于该值判定为阳性 threshold 0.35 result {col: float(prob) for col, prob in zip(label_cols, probs)} positives [col for col, prob in result.items() if prob threshold] return jsonify({probabilities: result, positive_labels: positives}) if __name__ __main__: app.run(host0.0.0.0, port5000)这里的threshold0.35来自验证集上的阈值调优结果具体数值要按你自己训练得到的AUC和临床需求调整。这个服务的代码量很小但已经把一套能跑通整个流程的源码闭环了——数据加载、模型训练、评估可视化、部署推理一条线串到底。这么多年做医疗AI我对自己代码最感激的一个习惯是每跑完一个实验把模型文件、训练参数、评估日志三个文件打成一个压缩包按日期命名。没有这个习惯后面翻旧账的时候会非常痛苦甚至怀疑自己当初是怎么跑出那个AUC的。另外在网上参考别人的开源医疗诊断项目时小心那些代码结构混乱、没写清楚数据来源的版本——代码能用和小型数据上效果好是两回事别被漂亮的README误导了。希望这份走通全流程的源码思路帮到你。本文还有配套的精品资源点击获取