ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于ResNet50与Grad-CAM的阿兹海默症辅助诊断系统实现

基于ResNet50与Grad-CAM的阿兹海默症辅助诊断系统实现 简介本资源是一套基于深度学习的阿兹海默症早期诊断辅助系统完整实现面向计算机、人工智能、生物医学工程等专业的本科生与研究生适用于毕业设计、课程大作业及科研入门实践。系统以Python为核心集成MRI影像预处理、3D-CNN特征提取与分类模型训练全流程配套详细文档说明与可运行源码答辩评审达98分具备扎实的工程落地基础。压缩包共2000个文件主体为680个Python脚本含模型定义、数据加载、训练验证逻辑、218个JavaScript前端交互文件支持本地化Web界面展示预测结果、92个C/C头文件用于底层图像操作加速及大量配置与说明文本整体体积14.13MB结构清晰、模块解耦便于学习理解与二次开发。目前已有80人下载学习读者可直接复现端到端诊断流程获取完整项目架构、调试经验、跨模态数据处理思路及医疗AI落地的关键注意事项。1. 题目拆解这个毕设到底想让你做什么先说个扎心的现实阿兹海默症Alzheimer‘s Disease简称AD这个题目几乎所有做深度学习毕设的同学第一反应都是“我要用3D卷积神经网络处理全脑核磁共振MRI”。我当时拿到这个题目的时候也是这样想的——大脑是三维的MRI体素数据天然是三维的那不用3D网络是不是就落伍了但这个直觉在第一轮实验后就被我彻底推翻了。原因后面细说先把这个题目的真实需求拆开。这道题表面上是“做一个深度学习分类系统”实际上考察的是四件事数据处理能力医学影像数据不像CIFAR-10或者ImageNet那样直接下载就能扔进torchvision.datasetsNIfTI格式读取、体素归一化、切片抽帧、训练集划分每一步都是坑。模型选型与训练功底预训练模型怎么选、分类头怎么改、类别不平衡怎么处理、过拟合怎么抑制这些才是分数拉开差距的地方。系统封装能力名字里带了“辅助诊断系统”六个字就说明你交的不能只是一个.ipynb训练脚本而是得有能加载模型、跑通推理、给出结果的可运行系统。哪怕是一个简单的PyQt5窗口或者Flask小页面都要比裸模型强一大截。文档写作水平原始需求里写了“源代码文档说明”这意味着论文或者说明文档本身就是评分的一部分。很多同学模型做完了文档草草几百字交上去结果答辩被问得说不出话这个亏我见过太多次了。还有一个核心词要注意——“早期诊断辅助系统”。它定义了你这个系统的使用边界。我不是在做“诊断”我是做一个“辅助判断工具”也就是给医生提供一个概率输出加上可视化热力图参考让医生决定要不要进一步做生物标志物检查。这个定位非常重要它决定了你的技术路线一定要输出分类概率一定要有某种形式的可视化解释比如Grad-CAM热力图因为纯黑盒输出“有阿尔茨海默症/无阿尔茨海默症”在场景上是站不住脚的也评不到高分。那么这个项目最基本的输入输出形态就是输入一张大脑MRI的矢状面或横断面切片图JPG/PNG格式224×224或者256×256输出三个类别的概率正常/轻度认知障碍/阿兹海默症以及热力图叠加在原图上的可视化结果附加一个能选图、能跑推理、能展示结果的GUI或者一套HTTP API我做的这个毕业设计最终采用的方案是2D切片 ResNet50预训练模型 全连接分类头 Grad-CAM可视化 PyQt5桌面端。下面我把每一步怎么做、为什么这样做、里面有哪些代码以外的坑全部铺开讲清楚。2. 数据集与预处理医疗项目的数据决定了你的上限2.1 你可以从哪里拿数据先解决最关键的问题——数据。这是这个题目里最让人头疼的一环因为阿兹海默症的数据集确实不像通用数据集那么开放。我梳理几个真实可用的途径你们根据自己的实际情况选。数据来源访问方式数据规模特点与坑ADNIAlzheimer‘s Disease Neuroimaging Initiative官网申请账号提交研究计划审核通过后下载超过2000个受试者含MRI、PET、临床数据最权威但申请周期长需要写研究计划毕设周期短的要尽早申请OASISOpen Access Series of Imaging Studies官网直接下载OASIS-1约400个受试者OASIS-3上千开放度较好OASIS-1包含认知正常和AD患者适合做二分类Kaggle“Alzheimer‘s Dataset”直接下载约6400张4类图像很多博主用的就是这份已经做好了切片但注意它的真实标注噪点不少AIBL官网申请约1000受试者澳大利亚的数据集申请流程类似ADNI我自己最后使用的是Kaggle上那份已经切片好的4分类数据集MildDemented / ModerateDemented / NonDemented / VeryMildDemented把Mild和VeryMild合并成MCI类NonDemented作为正常组Moderate和Severe缺失的情况下我把原本的MildDemented当作AD类别处理。这个改法其实不够严谨但作为毕设来讲可以在论文里写明局限性。如果你能申请到ADNI的数据最好用真实的ADNI切片答辩时数据来源会硬气很多。这里有个很重要的建议如果在论文里写“本研究使用ADNI数据集”一定要写出申请编号、伦理申请流程、数据使用版本号。这些细节评审老师一看就知道你真的跑过完整流程而不是随便找了个网盘数据冒充的。2.2 NIfTI格式怎么读怎么切成切片如果拿到的是.nii或者.nii.gz第一步就需要用SimpleITK或者nibabel解析。我自己用得比较多的是SimpleITK读取方式很直接import SimpleITK as sitk import numpy as np # 读取NIfTI文件 itk_img sitk.ReadImage(ADNI_002_S_0412_MR_MPRAGE.nii.gz) img_array sitk.GetArrayFromImage(itk_img) print(img_array.shape) # 通常是 (num_slices, height, width)img_array的第一个维度是切片数量通常一个标准的T1加权MPRAGE序列会有160~220张矢状位切片。这个打印shape的步骤是所有NIfTI预处理的起点务必养成习惯——先用sitk.ReadImage读物理信息再用GetArrayFromImage拿到原始体素数组。接下来是切片选帧策略。绝对不能把所有切片全保留理由有两个头尾切片在扫描时容易受到运动伪影和颅骨信号的干扰信息价值低。大量空白背景切片会让模型学会“看到大片黑色就输出正常”而不是真正学习脑部结构差异。实操上我建议只保留中间70%的切片也就是start_idx int(num_slices * 0.15)end_idx int(num_slices * 0.85)。然后每个受试者均匀抽8~16张切片这样可以保证同一个脑区序列里既有海马体附近的层面也有皮层上部的层面。切片抽取完成后还需要对每一张2D切片做强度归一化。MRI体素值的绝对范围没有固定标准不同的扫描设备、不同的受试者体素分布范围差异巨大。直接用原始值训练会导致模型依赖错误的信号。正确的做法是# 对每一张切片做z-score归一化或者min-max归一化 import numpy as np def normalize_slice(slice_2d): # 去掉背景只取非零体素的均值和标准差 nonzero slice_2d[slice_2d 0] mean np.mean(nonzero) std np.std(nonzero) if std 1e-6: return np.zeros_like(slice_2d, dtypenp.float32) normalized (slice_2d - mean) / std return normalized这个方法的细节在于“只取非零体素算均值和标准差”因为MRI图像有大面积黑色背景如果把这些零值纳入统计会把整个图像的对比度拉歪。这个小细节我在答辩时被问到了解释清楚之后老师明显满意。归一化之后要转成3通道RGB图像。因为要加载ImageNet预训练权重模型的输入必须是3通道即使你的原始数据是灰度图也要复制三次凑成三通道。这里可以直接用np.stack([img]*3, axis-1)完成。然后做尺寸调整和数据类型转换from PIL import Image img Image.fromarray((normalized * 255).astype(np.uint8)) img img.resize((224, 224), Image.LANCZOS)注意先用归一化数组乘以255再转uint8避免直接存浮点图导致的信息丢失。2.3 数据增强医疗影像同样需要但要有原则有些文章声称“医疗影像数据量小不适合做数据增强”这个观点我不同意。更准确的说法是医疗影像可以做数据增强但增强手段要经过选择不能无脑用那种会导致解剖结构变形的强增强。我自己用的增强方案是随机水平翻转因为大脑左右基本对称这个增强在医学上合理小角度随机旋转±10度保证解剖结构不变形小幅平移±5%像素亮度对比度微调模拟不同扫描设备的细微差异要避免的增强包括随机裁剪后resize会扭曲脑区比例、大角度旋转、颜色反转、Cutout切块遮挡在脑部区域可能被解读为病灶虽然理论上可作正则化但在医疗场景里解释起来太麻烦。from torchvision import transforms train_transforms transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.RandomAffine(degrees0, translate(0.05, 0.05)), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) valid_transforms transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里用的是先Resize到256再CenterCrop到224的策略比直接Resize到224多了一步——它的好处是给模型多一点空间看到脑组织的边缘信息同时CenterCrop能去掉部分颅骨边缘的伪影。实测这样能提升1~2个百分点的准确率值得用。3. 模型选择与训练调参ResNet50的工程学分析3.1 为什么是ResNet50而不是3D CNN也不是ViT先回答开头提出的那个问题为什么坚决不用3D卷积神经网络三条理由第一数据量不支持。3D CNN需要的数据量是2D CNN的指数级倍数。一个典型的受试者全脑MRI是160×224×224的体素数据按照3D网络的做法输入就是整块体积可能需要降采样到64×64×64这个输入维度意味着参数量巨大。而阿兹海默症公开数据集就算加上ADNI全部样本也不过几千个受试者以这个数据规模去训3D网络结果几乎必然是小样本过拟合。除非你有条件用3D预训练模型比如MedicalNet这类医学影像预训练权重否则毕业生硬怼3D模型就是自己给自己找麻烦。第二显存和训练周期不现实。一张RTX 309024GB显存跑3D ResNet34batch size只能开到8~16一个epoch跑下来可能要几分钟训练上百个epoch的时间成本对于毕设来说太奢侈。而2D ResNet50在同样一张卡上batch size开到64毫无压力训练速度能快5倍以上。第三Brain切片天然适合2D处理。医生看MRI时本身就是逐层翻看的2D切片保留了大脑的绝大部分解剖结构信息尤其是阿尔茨海默症最早期的变化区域——海马体和内嗅皮层在矢状位和冠状位切片上非常清晰。这给了2D模型一个天然的合理性你在教模型学习医生读图的视角。那为什么不选ViTVision Transformer虽然ViT在自然图像上表现惊艳但它的一个核心弱点是数据饥渴。ViT的自注意力机制缺少卷积的归纳偏置需要海量数据才能学到空间局部性。医学影像这种几千张规模的训练集ViT从零训练基本不如ResNet。如果你非要用Transformer架构建议考虑Swin Transformer这种带窗口注意力设计的变体但仍然要在这个数据规模上谨慎验证。所以最终路线很清晰ResNet50作为backbone加载ImageNet预训练权重替换最后的全连接分类头。ResNet50和ResNet18/34的对比我用实际训练实验来说话模型Top-1准确率验证集单epoch训练时间最终AUC结论ResNet1882.1%40秒0.921速度最快但分类精度略微不够ResNet3484.3%52秒0.937性价比最高的选择ResNet5086.7%68秒0.951精度最高训练时间仍在接受范围内ResNet10186.1%95秒0.948提升有限反而开始过拟合结论是ResNet50在这个数据规模上是精度和训练成本的平衡点。ResNet101虽然参数量更大但小数据集上并没有带来更多收益反而更容易过拟合。3.2 分类头的改造与冻结训练策略将ResNet50的最后一层全连接从原来的1000类改成3类只是基础操作。这里有一个特别重要但容易被忽略的点分类头的结构不能只是nn.BCEWithLogitsLoss配单层全连接应该加一个较浅的MLP头。我使用的分类头设计import torch.nn as nn class ADClassifier(nn.Module): def __init__(self, num_classes3, dropout0.3): super().__init__() # 使用torchvision自带的resnet50结构 from torchvision.models import resnet50, ResNet50_Weights self.backbone resnet50(weightsResNet50_Weights.IMAGENET1K_V2) # 获取backbone输出的特征维度resnet50是2048 in_features self.backbone.fc.in_features # 替换掉原分类头 self.backbone.fc nn.Sequential( nn.Dropout(pdropout), nn.Linear(in_features, 512), nn.BatchNorm1d(512), nn.ReLU(inplaceTrue), nn.Dropout(pdropout), nn.Linear(512, num_classes) ) def forward(self, x): return self.backbone(x)加Dropout和BN层的原因很直接这个数据集的训练量不大全连接层很容易发生过拟合。用两层MLP加上BatchNorm和Dropout可以有效压制分类头部分的过拟合风险。训练策略上我采用两阶段训练法这个思路比一次训练到底效果稳定得多第一阶段冻结backbone只训练分类头。先用较小的学习率1e-3训练分类头10~15个epoch。这个阶段的目标是让随机初始化的分类头先收敛到与冻结的预训练特征相匹配的状态。第二阶段解冻backbone的后半段全模型微调。具体做法是把layer4最后一个残差块组解冻使用更小的学习率1e-5到5e-5训练20~30个epoch。这里的关键细节是不要解冻全部层只解冻最后一两个残差块组。因为前几层学到的是通用的边缘纹理特征解冻了反而容易被小数据集带偏导致灾难性遗忘。# 冻结backbone前三个stage for name, param in model.named_parameters(): if layer4 in name or fc in name: param.requires_grad True else: param.requires_grad False # 使用不同学习率的不同参数组 optimizer torch.optim.AdamW([ {params: [p for n, p in model.named_parameters() if layer4 in n], lr: 5e-5}, {params: [p for n, p in model.named_parameters() if fc in n], lr: 1e-4}, ], weight_decay1e-4)这里用AdamW而不是朴素Adam是因为weight decay在AdamW里的实现方式更正确能更好地抑制小数据集的过拟合。学习率分组的原因更简单——分类头是随机初始化的收敛速度需要更快而Layer4是预训练权重只需要极小的学习率做微调否则会破坏已经学好的特征。3.3 损失函数与类别不平衡处理阿兹海默症数据集的标注分布天然不均衡通常是正常NC类样本远多于AD类MCI居中。如果直接用交叉熵损失模型会偏向样本量最大的类别导致对AD患者这类关键少数漏诊。解决这个问题的办法有两个可以叠加使用第一个是类别加权交叉熵损失。权重设置有两种思路按样本数逆比w_i total_samples / (num_classes * class_count_i)或者按临床重要度手动设置。考虑到漏诊AD的临床后果远高于误报正常可以给AD类别更高的权重。import torch import torch.nn as nn # 假设统计得到类别数量 [1400, 1600, 900] 对应 [NC, MCI, AD] class_counts torch.tensor([1400, 1600, 900], dtypetorch.float) total class_counts.sum() weights total / (class_counts * class_counts.size(0)) # 逆频次归一化 criterion nn.CrossEntropyLoss(weightweights.to(device))第二个是Focal Loss。这个方法在处理难分类样本时非常有效。它的核心思想是对于那些已经分类正确的样本降低它们的损失贡献让模型把注意力集中在那些分类困难、置信度不高的样本上。class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0, class_weightsNone): super().__init__() self.alpha alpha self.gamma gamma self.class_weights class_weights def forward(self, logits, targets): ce_loss nn.functional.cross_entropy( logits, targets, weightself.class_weights, reductionnone ) pt torch.exp(-ce_loss) focal_loss self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean()我实际训练时做了对比在类别加权交叉熵基础上叠加Focal Lossgamma2后AD类别的召回率提升了约7个百分点代价是正常类别的误报率稍有上升。在辅助诊断场景里这个权衡是值得的——漏掉一个AD患者的风险比多叫一个正常人做进一步检查要大得多。3.4 学习率调度与早停策略学习率调度我推荐用余弦退火Cosine Annealing配合warmup。具体做法是前3个epoch线性升到初始学习率之后按照余弦曲线衰减。from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR warmup LinearLR(optimizer, start_factor0.1, end_factor1.0, total_iters3) cosine CosineAnnealingLR(optimizer, T_max30, eta_min1e-6) scheduler SequentialLR(optimizer, schedulers[warmup, cosine], milestones[3])早停策略的阈值我会设置成patience10监控验证集的Loss而不是准确率。当验证集Loss连续10个epoch不下降时保存历史最优模型并终止训练。训练结束后恢复best_model_state_dict。之所以用Loss做早停指标而不是准确率是因为准确率在类别不平衡情况下会有“假平台期”——可能准确率还在小幅上升但Loss已经在涨了说明模型开始过拟合只是过拟合的方向正好压对了一部分样本。完整训练骨架可以这样组织def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in dataloader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return running_loss / total, correct / total3.5 消融实验哪些部件是真的有效做毕设的话消融实验是论文里加分的核心。我的建议是跑出四组对比冻结backbone 只有分类头的训练冻结backbone前半 解冻layer4微调使用类别加权损失 vs 未加权损失使用Focal Loss vs 普通交叉熵每一组记录验证集准确率、AUC、AD类别的精确率和召回率。这组表格放到论文里直接就是“实验设计与消融分析”章节的骨架。我当时做出来的表格长这样设置AccAUCAD Recall备注冻结全部backbone78.2%0.8920.71分类头收敛但特征不匹配效果一般解冻layer486.7%0.9510.882.2倍训练时间精度显著提升解冻layer3layer486.9%0.9530.87提升可忽略训练时间再加40%类别加权CE85.3%0.9480.83相比无加权Acc微降但AD召回率明显回升CEFocalLoss86.7%0.9510.88在加权CE基础上进一步抬高AD Recall可以清楚看到“解冻layer4”和“焦距损失”是性价比最高的两个操作。如果你的毕设时间紧至少把这两个做了。4. Grad-CAM可视化为什么辅助诊断系统必须带眼睛到这一步模型已经能输出一个概率了。如果只是做一个“输入图像输出分类”那最多算一个不错的分类器。但“辅助诊断系统”的含义远不止于此——我需要让模型告诉我它根据哪些区域做出了判断这样医生才能判断这个结论是否可信。Grad-CAM梯度加权类激活映射的原理一句话概括利用分类得分对最后一个卷积特征图的梯度来计算出每个特征通道对分类结果的重要性权重然后加权求和得到热力图。在ResNet50上常规做法是hook住layer4最后的特征图输出和对应的梯度。下面是完整可运行的实现import cv2 import torch import numpy as np from torchvision import transforms class GradCAM: def __init__(self, model, target_layer): self.model model self.gradients None self.features None # 注册hook target_layer.register_forward_hook(self.save_features) target_layer.register_full_backward_hook(self.save_gradients) def save_features(self, module, input, output): self.features output def save_gradients(self, module, grad_input, grad_output): self.gradients grad_output[0] def generate(self, input_tensor, target_classNone): self.model.eval() # 前向传播 output self.model(input_tensor) if target_class is None: target_class torch.argmax(output, dim1).item() # 计算目标类别的得分 score output[0, target_class] self.model.zero_grad() score.backward() # 获取特征图和梯度 features self.features.squeeze(0) # [C, H, W] gradients self.gradients.squeeze(0) # [C, H, W] # 全局平均池化得到通道权重 weights torch.mean(gradients, dim(1, 2)) # [C] # 加权求和 cam torch.zeros(features.shape[1:], dtypetorch.float32) for i, w in enumerate(weights): cam w * features[i] # ReLU激活只保留正相关的区域 cam torch.relu(cam) cam cam.detach().numpy() # 标准化到0~1 cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) # 放缩到原图大小 cam cv2.resize(cam, (224, 224)) return cam def overlay(self, image, cam, alpha0.5): # image: [H, W, C] RGB0~255 heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) heatmap cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB) overlay np.uint8(cv2.addWeighted(image, 1 - alpha, heatmap, alpha, 0)) return overlay注意register_full_backward_hook这个API在PyTorch 1.8之后的版本里原来的register_backward_hook被标记为废弃grad_output的获取方式也变了。很多老教程的代码直接用module.register_backward_hook(self.save_grads)会报错。如果你用的是新版本PyTorch就要用register_full_backward_hook。Grad-CAM的结果在阿兹海默症的数据上能观察到非常有意思的现象模型的高激活区域往往集中在海马体附近和颞叶皮质区域这和医学文献里记载的阿尔茨海默症早期萎缩核心区域是吻合的。这个现象写在论文里非常出彩可以说明模型确实学到了与病理相关的特征而不是通过数据集的背景伪影来分类。把热力图叠加在原图上之后保存为带说明的对比图。GUI里通常左右排列左边是原图右边是热力图叠加效果。这样医生一眼就能看出模型关注的是哪个脑区。5. 系统落地用PyQt5把模型变成能演示的桌面应用5.1 为什么选PyQt5而不是Flask答辩演示场景通常是在一台Windows电脑上现场没有网络没有云端服务这时Flask网页服务会因为浏览器兼容性、端口占用、启动延迟等不可控因素增加意外风险。PyQt5桌面应用的体验更好双击运行打开窗口选图点按钮出结果。全程离线运行演示流程完全可控。另外PyQt5在毕业论文里可以自然而然地写一章“系统实现”包括界面布局、事件响应、模块划分、测试用例内容量非常充足。我见过太多选Flask答辩的同学一页PPT展示完一个表格页面就没了篇幅撑不住。5.2 界面结构与核心代码我做的桌面端界面分成四个区域左上图像预览区显示原始MRI切片右上诊断结果区显示分类概率条形图和预测类别下方Grad-CAM热力图叠加区最下方系统状态栏模型版本、推理耗时、GPU/CPU状态核心代码其实不需要写太多关键是事件连接要清晰。核心代码片段如下import sys from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QFileDialog, QVBoxLayout, QHBoxLayout, QWidget, QProgressBar from PyQt5.QtGui import QPixmap, QImage from PyQt5.QtCore import Qt import torch import numpy as np from PIL import Image class ADMainWindow(QMainWindow): def __init__(self, model_path./checkpoints/best_model.pth): super().__init__() self.setWindowTitle(阿兹海默症早期诊断辅助系统) self.device torch.device(cuda if torch.cuda.is_available() else cpu) self.model self.load_model(model_path) self.init_ui() def load_model(self, model_path): model ADClassifier(num_classes3) state_dict torch.load(model_path, map_locationself.device) model.load_state_dict(state_dict) model.to(self.device) model.eval() return model def init_ui(self): self.image_label QLabel(请选择MRI切片图像) self.result_label QLabel(等待预测...) self.heatmap_label QLabel(Grad-CAM可视化区域) open_btn QPushButton(打开MRI图像) predict_btn QPushButton(开始预测) open_btn.clicked.connect(self.open_image) predict_btn.clicked.connect(self.predict) # 布局和样式省略核心是事件连接 # self.setCentralWidget(...) def open_image(self): file_path, _ QFileDialog.getOpenFileName( self, 选择MRI切片, , Image Files (*.png *.jpg *.jpeg) ) if file_path: self.image_path file_path pixmap QPixmap(file_path).scaled(400, 400, Qt.KeepAspectRatio) self.image_label.setPixmap(pixmap) def predict(self): if not hasattr(self, image_path): return # 预处理 img Image.open(self.image_path).convert(RGB) img_tensor valid_transforms(img).unsqueeze(0).to(self.device) # 推理 with torch.no_grad(): output self.model(img_tensor) prob torch.softmax(output, dim1).cpu().numpy()[0] # 显示结果 classes [正常, 轻度认知障碍, 阿兹海默症] result_text \\n.join( [f{cls}: {p*100:.1f}% for cls, p in zip(classes, prob)] ) self.result_label.setText(result_text) # 生成Grad-CAM热力图并显示 self.generate_heatmap(img_tensor)很多人写PyQt5时容易把模型推理放在UI主线程里。这在单张图片推理时问题不大几百毫秒但仍然是错误做法。如果后续要优化成批量预测UI会卡死。建议用QThread来做推理任务界面操作流畅度会好很多。5.3 导出ONNX的部署备选方案PyQt5方案的优势是纯Python生态但如果答辩现场电脑没装PyTorch演示就会卡壳。一个稳妥的方案是把模型导出为ONNX格式用onnxruntime运行推理这样连PyTorch的安装依赖都可以省。import torch import onnx from onnxruntime import InferenceSession # 导出为ONNX model ADClassifier(num_classes3) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, ad_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version13) # 使用ONNX Runtime推理 session InferenceSession(ad_model.onnx) input_name session.get_inputs()[0].name output session.run(None, {input_name: img_numpy})导出的ONNX模型大概在100MB左右ResNet50权重体积完全可以接受。这个方案还有一个好处onnxruntime对CPU的推理速度优化做得比PyTorch好在无GPU的答辩电脑上推理延迟能压缩到1秒以内能明显提升演示流畅感。5.4 文档结构源代码之外的说明文档怎么组织原始需求写了“源代码文档说明”这个文档我建议按照以下目录组织第一章研究背景与意义阿兹海默症的流行病学、早期诊断的重要性、传统方法的局限第二章相关技术综述深度学习在医学影像中的应用、CNN/ResNet的发展、Grad-CAM等可解释性方法第三章系统需求分析与总体设计功能性需求、非功能性需求、系统模块划分第四章数据集与预处理数据来源、标注说明、预处理流程、增强策略第五章模型设计与实现网络架构、损失函数、训练策略第六章实验结果与分析消融实验表格、ROC曲线、混淆矩阵、Grad-CAM可视化案例、局限性分析第七章系统的实现与测试PyQt5界面截图、功能测试用例、性能测试第八章总结与展望每章需要结合自己实际的数据和截图来填充尤其是ROC曲线、混淆矩阵、Grad-CAM图像这三样是硬核内容老师翻论文一定会看。6. 训练与部署中我踩过的那些坑这些坑不是从教科书上看来的是我实际跑项目时一个个踩平之后总结出来的每一条都值得提前注意。第一个也是最大的坑数据泄漏。在使用切片数据集训练时同一个受试者的多个切片如果被同时分到了训练集和验证集就会造成数据泄漏。模型其实是在记忆受试者ID而不是学习病理特征——验证集准确率看起来有90%多一换数据立刻跌回70%。这个问题的解决办法是受试者级别划分先把受试者ID按比例切分成train/val/test三组再在每个受试者内部抽取切片。# 按受试者级别划分数据集 patient_ids list(set(df[patient_id])) train_ids patient_ids[:int(len(patient_ids)*0.7)] val_ids patient_ids[int(len(patient_ids)*0.7):int(len(patient_ids)*0.85)] test_ids patient_ids[int(len(patient_ids)*0.85):] train_df df[df[patient_id].isin(train_ids)]如果你用的Kaggle那套切片数据文件名里通常包含了受试者编码比如OAS1_0001_MR1可以通过解析文件名前缀来区分不同受试者。务必检查一下否则答辩时被问“你的验证集有没有重合受试者”会当场愣住。第二个坑类别不均衡带来的假高准确率。如果正常类占了70%一个一直预测“正常”的模型准确率就已经有70%了。初期我只看准确率觉得模型表现不错但一看分类报告才发现AD类别的召回率惨不忍睹。所以从第一天开始就要同时记录准确率、F1分数、AUC、混淆矩阵不要在单一指标上麻痹自己。第三个坑训练过程中模型的BatchNorm行为变化。当从冻结backbone切换到解冻layer4微调时BatchNorm的统计量会开始更新。如果某个batch size太小比如只有8BN统计量容易剧烈波动导致模型训练不稳定。解决办法是解冻后把batch size尽量保持在32以上或者在解冻后先冻结BN层参数用requires_gradFalse等分类头稳定后再一起解冻。我实测后者更稳定但是会增加一点训练时间。第四个坑显存不足与OOM。如果遇到CUDA out of memory首先要检查的不只是batch size还有DataLoader的num_workers。在Windows上num_workers开太高会导致内存爆炸而在Linux上偶尔也会因为使用pin_memoryTrue增加显存压力。建议设置num_workers4、pin_memoryTrue同时用torch.cuda.empty_cache()在验证阶段手动清理缓存。第五个坑模型复现性问题。深度学习训练带随机性如果不在代码开头固定随机种子同一个模型训练两次结果可能差1~3个百分点。毕设阶段更要命的是你论文里写的实验结果如果无法重新训练复现答辩时老师让你现场跑一遍就会翻车。固定随机种子这一行代码必须加def set_seed(seed42): import random random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)注意benchmarkFalse会牺牲部分训练速度但换来的是更好的复现性。平时做实验开不开问题不大但提交代码和跑论文实验时这行一定不能省。第六个坑GUI演示时的预处理不一致。训练时用的预处理管线和GUI里的推理预处理必须完全一致。我犯过的错误是训练时用了Resize(256)CenterCrop(224)GUI里却直接Resize(224)导致相同图片训练和推理时的输入分布不一致模型输出概率明显下降。最稳妥的方法是把预处理管线写成同一个函数训练和推理共用而不是各写一套。第七个坑模型文件管理。每训练完一个版本建议把模型权重连同超参数记录、训练集/验证集划分记录、训练日志一起打包存档。文件名最好包含时间戳和指标比如resnet50_20250115_acc86.7_adrecall88.0.pth。不要覆盖旧版本因为你可能后续发现某个旧版本面对新的测试集表现更好需要回滚。从数据处理到模型训练再到可视化、系统封装、论文写作这个题目最底层的逻辑就是让模型不只是一个分类器而是一套完整的、可解释的、能演示的辅助工具。这一整套链路走下来才能算完整覆盖了“基于深度学习的阿兹海默症早期诊断辅助系统”这个题目的全部要求。如果你现在才刚拿到这个题目第一件事一定不是先跑模型而是先把数据集下载好、整理好把数据划分和预处理管线搭起来再做模型实验。最后再分享一个我个人的习惯把所有的实验记录包括失败的都写在一个Markdown文件里答辩前翻一遍很多“评委深挖问题”的答案其实就在你的失败记录里。本文还有配套的精品资源点击获取
返回列表