简介:一套基于深度学习的阿兹海默症早期诊断辅助系统源码及配套文档,属于Python毕业设计项目,面向计算机、人工智能、自动化等专业学生、老师及从业者,尤其适合作为课程设计、大作业或毕设参考。系统以深度学习模型为核心,覆盖数据预处理、模型构建、训练评估与辅助诊断界面等环节,代码经调试可运行,便于二次开发与功能扩展。资源包约14.13MB,共2000个文件,其中以680个Python脚本、218个JavaScript文件、92个C/C++头文件、62个CSS样式表、40个HTML页面为主体,并含22个TXT说明与11个Markdown文档,整体涵盖算法实现、前端交互、界面样式与项目文档,结构清晰便于按需查阅。目前已有80人学习下载,适合想快速上手深度学习医疗项目的入门者;基础较好的读者可在此基础上调整模型结构、优化诊断逻辑或扩展数据集,实现个性化功能。
1. 阿兹海默症早期诊断辅助系统:一份能直接跑的 Python 毕设源码
放射科医生在阅片灯前对比一组脑部 MRI 的时候,最头疼的不是明显萎缩的中晚期病灶,而是轻度认知障碍和正常老化之间那点细微差别——海马体缩小了几个像素、皮层厚度薄了不到一毫米,肉眼往往判断不准。深度学习这几年在医学影像分类上的表现恰恰补上了这个短板:它能从大量标注样本里自己学出「什么样的脑结构更像早期阿兹海默症」的视觉模式。这套基于深度学习的阿兹海默症早期诊断辅助系统,就是一份面向毕业设计的 Python 完整源码加文档说明,把数据预处理、CNN 模型训练、评估可视化和 GUI 诊断界面整条链路都串好了。适合两类人:拿它做本科或硕士毕设的学生,以及想入门医学影像 AI、手里缺一套可复现工程骨架的开发者。
2. CNN 架构与数据预处理:先想清楚选型,再动手写代码
2.1 为什么用 CNN 而不是手工特征:局部感受野对应脑区萎缩
早期 AD 诊断的传统做法是先提取手工特征,比如用 HOG 描述局部梯度、用 GLCM 计算纹理统计量,再丢给 SVM 或随机森林分类。这套思路不是不能跑,但有两个硬伤:特征设计极度依赖专家经验,海马体萎缩和皮层变薄在不同切片上的表现差异很大,一套固定算子很难覆盖全;特征维度一高,小样本下特别容易过拟合。
CNN 的思路完全反过来——卷积核就是一组可学习的局部特征提取器,浅层学边缘纹理、深层学「这片区域像不像萎缩组织」的高层语义,整个过程端到端,不需要人工设计特征。对二维脑部 MRI 切片来说,局部感受野天然匹配「局部脑区出现异常信号」这个诊断逻辑,池化层又给了平移不变性,所以深度学习算法里 CNN 是这类切片分类任务的首选,而不是拿 Transformer 硬上——毕设数据量通常只有几千张图,Transformer 喂不饱。
2.2 网络骨架选型:ResNet-18 迁移学习是毕设最稳的起点
网络骨架的选择直接决定训练能不能收敛。我见过不少毕设一上来就上 ResNet-50 甚至 EfficientNet-B4,结果在小数据集上训练 loss 死活降不下去,这就是选型失误。数据量只有几百到几千张时,ResNet-50 参数太多,极容易过拟合;ResNet-18 层数浅、参数量小,配合 ImageNet 预训练权重,是最稳的起点。
import torch import torch.nn as nn import torchvision.models as models def build_model(num_classes=2, pretrained=True): model = models.resnet18(pretrained=pretrained) # 原版 fc 层输出 1000 类,替换成二分类输出 model.fc = nn.Linear(in_features=512, out_features=num_classes) return model model = build_model(num_classes=2, pretrained=True)替换输出层时注意:ResNet-18 最后一个卷积块输出的特征图是 512 通道,全局平均池化后得到 512 维向量,所以 fc 层的 in_features 必须写 512。pretrained=True 会加载在 ImageNet 上预训练好的主干权重,这些权重学到的低级视觉特征(边缘、纹理、形状)对医学图像同样有效,能明显加速收敛。如果你手里的 MRI 是灰度单通道图,喂给模型前记得复制成三通道,否则输入维度对不上。
2.3 数据预处理三件套:切片抽取、灰度归一化与数据增强
原始脑部 MRI 是 NIfTI 格式的三维体数据,不能直接塞进二维 CNN。常见做法是沿轴向抽取若干张二维切片,取中间层最稳,因为海马体在中间层面的结构最清晰。灰度归一化这里有个坑:MRI 不是 CT,没有统一的 HU 值窗口,直接用 Min-Max 归一化容易被少数离群体素带偏,我一般用百分位截断。
import nibabel as nib import numpy as np def load_mri_slice(nifti_path, slice_idx=60): """读取 NIfTI 体数据并抽取一张轴向切片""" img = nib.load(nifti_path) data = img.get_fdata() # 常见排列: (depth, height, width) slice_2d = data[slice_idx, :, :] # 1%~99% 百分位截断,抑制离群体素 p_low, p_high = np.percentile(slice_2d, (1, 99)) slice_2d = np.clip((slice_2d - p_low) / (p_high - p_low + 1e-6), 0, 1) return slice_2d参数说明:slice_idx 取 60 是假设体数据深度在 120 层左右,取正中间一层病灶信息最完整;percentile 的 1 和 99 是经验值,过大截断太少、过小损失对比度。数据增强方面,RandomHorizontalFlip、RandomRotation(10) 和 RandomAffine 我都建议开着——水平翻转对脑部 MRI 是安全的,左右脑萎缩模式在统计上基本对称,很多方法学论文也这么用;但如果你心里没底,就只开旋转和仿射,宁缺毋滥。
3. 把训练跑起来:数据集组织、训练脚本与超参数调优
3.1 数据集目录结构:ImageFolder 的约定你最好遵守
PyTorch 里加载图像分类数据最省事的方案是torchvision.datasets.ImageFolder,它强制要求目录按「类别名作为子文件夹名」来组织。很多新手把图片全扔在一个文件夹里,再自己写 CSV 映射标签,绕了一大圈还容易在 train/val/test 划分时出bug。直接用约定俗成的结构,少写一百行代码。
data/ ├── train/ │ ├── NORMAL/ # 正常老化样本 │ └── AD/ # 阿兹海默症样本 ├── val/ │ ├── NORMAL/ │ └── AD/ └── test/ ├── NORMAL/ └── AD/划分比例我一般按 7:1.5:1.5 或者 8:1:1 走,关键是划分前先固定随机种子,否则每次跑出来的实验结论都不一样。代码里用random.seed(42)和torch.manual_seed(42)锁住,划分脚本也单独存一份,方便复现。
from torchvision import datasets, transforms transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_set = datasets.ImageFolder('data/train', transform=transform) train_loader = torch.utils.data.DataLoader( train_set, batch_size=32, shuffle=True, num_workers=4)Normalize 这里用的是 ImageNet 的统计量,因为模型主干是 ImageNet 预训练的,输入分布保持一致对迁移效果最有利。num_workers 在 Windows 上建议设成 0,否则多进程加载容易报错,这是 PyTorch 在 Windows 下的老毛病。
3.2 训练主循环:损失函数、优化器与学习率调度
二分类任务有两个选择:nn.CrossEntropyLoss或nn.BCEWithLogitsLoss。我选 CrossEntropyLoss,理由很直接:它自带 Softmax,输出维度是 2,后续评估直接argmax(dim=1)拿类别;BCE 输出维度是 1,换到四分类时还得改网络结构。优化器用 Adam,比 SGD 好调参,毕设场景下足够。
import torch import torch.nn as nn def train_one_epoch(model, loader, optimizer, criterion, device): """跑一个 epoch 并返回平均 loss 与准确率""" model.train() total_loss, correct, total = 0.0, 0, 0 for images, labels in loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() logits = model(images) # shape: [B, num_classes] loss = criterion(logits, labels) loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) correct += (logits.argmax(dim=1) == labels).sum().item() total += images.size(0) return total_loss / total, correct / totallogits.argmax(dim=1)取概率最大的类别索引作为预测结果;loss.item() * images.size(0)是按样本数加权累加,这样最后除以 total 得到的是整个 epoch 的平均 loss,而不是每个 batch loss 的简单平均——后者在小 batch 下会被噪声掩盖。加一个 CosineAnnealingLR 学习率调度,在 30 个 epoch 内把学习率从初始值余弦退火到接近 0,收敛更稳。
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| epoch | 30~50 | 小数据集 30 足够,加早停防过拟合 |
| batch_size | 16 或 32 | 显存不够先降 batch,不要降输入分辨率 |
| 初始学习率 | 1e-3 | Adam 的默认值偏激进,1e-3 起步安全 |
| weight_decay | 1e-4 | 缓解过拟合,常规设置 |
| 调度器 | CosineAnnealingLR | T_max 设成总 epoch 数 |
3.3 参数调优的血泪经验:学习率与 batch size 必须配套
调参这块我踩过的坑比写过的 bug 还多,最典型的就是学习率和 batch size 不配套。batch size 一大,梯度估计更准,学习率可以适当调高;batch size 只有 8 的时候还拿 1e-3 起步,loss 经常在前几个 epoch 直接震荡到 NaN。我现在的习惯是:batch size 16 起步就用 1e-4 学习率,32 就用 1e-3,然后观察前三个 epoch 的 loss 曲线,如果 loss 在 0.7~0.75 附近横盘不动,说明学习率太低或者主干被冻住了,调高一档再试。另外迁移学习有个常见误用:直接冻结整个主干只训练 fc 层,在小数据集上效果尚可,但当你手里的样本量超过一两千张时,解冻 layer4 的全部参数微调,测试集准确率通常能再涨两三个点。
4. 训练时的踩坑排查:五类常见问题的现象、原因与解决
4.1 三维体数据直接进网络,几轮就把内存撑爆
现象:torch.cuda.OutOfMemoryError,或者 CPU 模式下内存占用直接吃掉十几个 GB,训练卡死不动。原因:惯性思维把 NIfTI 三维数据当成一个整体喂给了 CNN,但 3D 卷积输入动辄[B, 1, 128, 128, 128],中间特征图的显存占用是二维的数倍,毕设级别的单卡根本扛不住。解决:切成二维切片训练,每例只取中间 3~5 张切片参与训练和验证;如果切片数量还不够,用 Dataset 的__getitem__即时读取文件,而不是在初始化时把所有数据一次性 load 进内存。
4.2 二分类 loss 卡在 0.69 死活不降
现象:训练了好几个 epoch,loss 稳定在 0.69 附近,准确率在 50% 上下浮动。原因:二分类的随机猜测熵正好是ln(2) ≈ 0.693,loss 停在这个值说明模型什么都没学到。常见原因有三个——学习率太低导致参数几乎不更新;优化器在更新而 loss 反传路径断了(比如某个张量被detach());标签顺序写反了,模型学到的是反的映射。解决:先用一个 batch 做 overfit 测试,把train_one_epoch跑一步看 loss 能不能降到 0.1 以下;能降说明代码没毛病,去调学习率和解冻层;不能降就是标签或数据管道的 bug,逐段打印 logits 的幅度检查。
4.3 验证集准确率很高,测试集一塌糊涂
现象:val 集准确率 90% 以上,换到没见过的测试集直接掉到 70% 甚至更低。原因:数据泄露最常见的是归一化统计量在划分前算了全体数据,也就是Normalize的 mean/std 用了 train+val+test 全量统计;还有一种隐蔽情况是增强操作在划分前做完了,模型在训练期已经见过 val 图像的变换版本。解决:严格先划分 train/val/test,再在 train 集上拟合归一化参数和增强流程,val 和 test 只做和训练集一致的归一化,不做随机增强。固定随机种子是这里的最低要求。
4.4 加载预训练权重报 KeyError 或 size mismatch
现象:torch.load之后加载 state_dict,报size mismatch for fc.weight或者一堆Missing key(s)。原因:模型换了分类头之后,fc.weight的形状从[1000, 512]变成了[2, 512],直接load_state_dict必然报错。Missing key则一般是网络结构定义和预训练权重来源不一致,比如用了resnet18但权重是从resnet34转换来的。解决:加载时用strict=False,只恢复主干参数,fc 层的权重保持随机初始化,这是迁移学习的标准姿势。
state_dict = torch.load('resnet18.pth') model.load_state_dict(state_dict, strict=False) # fc 层 KEY 不匹配会跳过4.5 类别不平衡,模型学成「无脑预测多数类」
现象:训练集里 NORMAL 有 1800 张,AD 只有 200 张,训练完测试准确率 88%,看上去不错,看混淆矩阵才发现 AD 的检出率接近 0。原因:CrossEntropyLoss 默认把每个样本平等对待,多数类的梯度完全主导了训练。解决:给 loss 加类别权重,权重和样本数成反比;或者用WeightedRandomSampler重采样,让每个 batch 里两类比例接近均衡。评估时务必同时看两个类别的 recall,而不是只看总准确率。
5. 评估与可视化:混淆矩阵、ROC 曲线与 Grad-CAM 可解释性
5.1 医学诊断场景,别只看准确率
准确率这个指标在类别不平衡的医学数据上非常容易骗人。早期 AD 诊断漏掉一个阳性病例的代价远大于把正常人误判成患者,所以敏感度(recall)才是这个场景的核心指标。一份合格的评估报告至少要包含准确率、敏感度、特异度和 F1,混淆矩阵是必须有的。
| 指标 | 公式 | 医学诊断里的含义 |
|---|---|---|
| Accuracy | (TP+TN)/(P+N) | 整体判对比例,样本不平衡时会虚高 |
| Sensitivity/Recall | TP/(TP+FN) | 患者被正确检出的比例,漏诊率=1-Sensitivity |
| Specificity | TN/(TN+FP) | 正常人被正确排除的比例,误诊率=1-Specificity |
| F1-score | 2PR/(P+R) | 精确率与敏感度的调和平均 |
from sklearn.metrics import classification_report, confusion_matrix y_true, y_pred = [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: logits = model(images.to(device)) y_true += labels.tolist() y_pred += logits.argmax(dim=1).cpu().tolist() print(classification_report(y_true, y_pred, target_names=['NORMAL', 'AD'])) print(confusion_matrix(y_true, y_pred))把classification_report输出里的 AD 行 recall 单独圈出来,这个是答辩时导师最可能追问的数字。
5.2 画 ROC 曲线算 AUC:区分度的硬指标
准确率可以靠调阈值改变,ROC 曲线和 AUC 则衡量模型在所有阈值下的区分能力,不受类别不平衡影响。测试时拿 Softmax 输出里 AD 类别的概率作为打分,再用 sklearn 的roc_curve出图。
from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt y_score = [] with torch.no_grad(): for images, labels in test_loader: prob = torch.softmax(model(images.to(device)), dim=1) y_score += prob[:, 1].cpu().tolist() # AD 类别概率 fpr, tpr, _ = roc_curve(y_true, y_score) auc_val = auc(fpr, tpr) plt.figure(figsize=(6, 6)) plt.plot(fpr, tpr, label=f'AUC = {auc_val:.3f}') plt.plot([0, 1], [0, 1], linestyle='--', color='gray') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.legend() plt.savefig('roc_curve.png', dpi=150)AUC 大于 0.9 才算有实际诊断价值,0.8~0.9 属于中等偏上。我没见过谁拿 AUC 低于 0.8 的模型去做毕设答辩还能不被问住的。
5.3 Grad-CAM 热力图:让模型「说」出它看的是哪块脑区
导师最常问的一个问题是:凭什么相信 CNN 的结论。Grad-CAM 能直接把模型决策依据可视化成一幅叠加在 MRI 切片上的热力图,红色区域就是模型认为最像 AD 萎缩特征的区域。实现思路不复杂:对最后一层卷积输出的特征图按梯度加权求和,得到一个和特征图同尺寸的权重图,再放大到输入尺寸叠加显示。
def grad_cam(model, tensor, target_layer): """计算 Grad-CAM 热力图,target_layer 传 model.layer4[-1]""" features, grads = None, None def forward_hook(module, input, output): nonlocal features features = output def backward_hook(module, grad_input, grad_output): nonlocal grads grads = grad_output[0] h1 = target_layer.register_forward_hook(forward_hook) h2 = target_layer.register_full_backward_hook(backward_hook) output = model(tensor) score = output[0, 1] # AD 类别的得分 model.zero_grad() score.backward() h1.remove() h2.remove() weights = grads.mean(dim=(2, 3), keepdim=True) cam = torch.relu((weights * features).sum(dim=1)) cam = cam.squeeze().cpu().numpy() return cam注意register_full_backward_hook是 PyTorch 1.8 之后的推荐写法,旧版本用register_backward_hook有时拿不到梯度。热力图叠加到原图上的操作留给小工具包torchcam或者手工plt.imshow叠加。如果你的模型用的是 ResNet-18,target_layer传model.layer4[-1]就行。把几张阳性样本的热力图整理成一页,放进毕设论文的实验分析章节,整篇文档的说服力会上一个档次。
6. 把二分类改成四分类:从 Normal/AD 扩展到 MCI 分级
现实里临床诊断不只要区分「正常」和「阿兹海默症」,更关键的是把轻度认知障碍(MCI)从两者里单独拎出来——这才是「早期诊断」四个字的核心价值。把这份资源里的二分类模型改成四分类,只需要动三处。
第一处是数据目录,在data/train、val、test下各加两个文件夹,常见分法是NORMAL、VERY_MILD、MILD、MODERATE,对应 OASIS 数据集的四级标注。第二处是模型输出维度,build_model(num_classes=2)改成num_classes=4,fc 层输出从 2 变成 4。第三处是评估代码,classification_report的target_names同步换成四个类别。损失函数不用改,CrossEntropyLoss 天然支持多分类;梯度反传也不用动。关键验证点是混淆矩阵的对角线——四分类场景下最容易翻车的是相邻类别互相混,比如 VERY_MILD 和 NORMAL 分不开,这属于正常现象,毕竟两者影像差异本来就小,但如果 MILD 和 MODERATE 也大面积混在一起,就要回头检查切片抽取的位置是不是太靠上偏离海马体了。
改完先拿一个 subclass(比如只保留 NORMAL 和 VERY_MILD 做一个轻量二分类)验证代码能跑通,再全量训练。评估指标从二分类的敏感度/特异度换成 macro F1,避免少数类被多数类淹没。我自己第一次做多分类扩展时,一上来就全量训练,跑了 20 个 epoch 才发现 val 里漏加了两个类别的文件夹,整个实验白烧了两小时。从那以后我每次拿到新数据集都强制先打印类别分布和每个类抽样一张图看一眼,再动模型结构。这套资源和文档里把目录组织、训练命令和评估脚本都写清楚了,你照着走一遍再改多分类,会少踩我当年踩过的大部分坑。希望帮到你。
本文还有配套的精品资源,点击获取