ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习的阿兹海默症早期诊断辅助系统:MRI影像分类与可解释性实战

基于深度学习的阿兹海默症早期诊断辅助系统:MRI影像分类与可解释性实战 简介这份资源是面向计算机、人工智能、自动化等专业学生与教师的毕业设计完整项目包主题为基于深度学习的阿兹海默症早期诊断辅助系统。项目为个人毕设成果答辩评审分达98分代码均经过调试测试可稳定运行适合作为毕业设计、课程大作业或期末课程设计的参考方案也适合基础较好的学习者在此基础上修改扩展功能。压缩包共约2000个文件整体14.12MB以680个Python源码文件为核心辅以218个JavaScript、62个CSS、40个HTML等前端资源以及119个Tcl脚本、92个C头文件、11个Markdown说明文档和若干配置文件覆盖模型训练、界面交互与文档说明等模块。目前已有66人学习关注。读者可获得一套结构完整的深度学习医疗辅助诊断实现方案包括数据预处理、模型构建与推理流程、可视化界面代码及配套文档说明便于快速理解项目架构、复现实验并完成二次开发。1. 阿兹海默症早期诊断辅助系统从 MRI 到可解释分类的工程落地家里老人开始记不住刚放下的钥匙反复问同一个问题很多人第一反应是“年纪大了正常”。但阿兹海默症的病理改变在临床症状出现前 10 到 20 年就已经开始等到明显健忘时神经元损失往往已经不可逆。这就是为什么“早期诊断”四个字在临床上分量极重。python 毕业设计基于深度学习的阿兹海默症早期诊断辅助系统本质上是把公开的脑部 MRI 影像数据集用卷积神经网络做一次多分类或二分类输出正常、轻度认知障碍、阿兹海默症的概率再配上可视化热力图让医生或家属看到模型“看的是哪块脑区”。它适合两类人一是做毕设需要完整可复现流程的学生二是想入门医学影像深度学习的工程师。核心难点不在模型多深而在数据量小、类别不均衡、切片选择玄学以及可解释性怎么做得让人信服。下面按数据、模型、训练、避坑、进阶五块讲透。2. 数据准备与预处理OASIS 与 ADNI 的切片策略2.1 为什么选 OASIS-1 而不是直接上 ADNI公开的脑部 MRI 数据集里OASIS-1 是毕设最稳的起点。它包含 416 位受试者的横断面 T1 加权 MRI附带 CDR临床痴呆评定量表评分CDR 0 对应正常0.5 对应轻度认知障碍1 及以上对应阿兹海默症。ADNI 数据更全但申请周期长、需要机构资质毕设时间线扛不住。常见做法是先用 OASIS-1 跑通全流程再考虑用 ADNI 做外部验证。OASIS-1 的原始格式是 Analyze 7.5.hdr .img不是常见的 NIfTI。读取时用nibabel可以直接加载但要注意方向矩阵。我一般会先统一转成 NIfTI 再处理避免后续工具链不兼容。import nibabel as nib import numpy as np import os def load_oasis_volume(img_path): 读取 OASIS-1 的 Analyze 格式返回 numpy 数组 img nib.load(img_path) # 获取数据OASIS 原始数据通常是 uint8 或 int16 data img.get_fdata() # 统一方向到 RAS避免左右脑翻转 data nib.as_closest_canonical(img).get_fdata() return data def normalize_intensity(volume): Z-score 归一化医学影像常用 mask volume 0 # 去掉背景 mean volume[mask].mean() std volume[mask].std() volume (volume - mean) / (std 1e-8) volume[~mask] 0 return volume逻辑说明as_closest_canonical把图像重定向到标准 RAS 坐标系这一步不做的话不同受试者的左右方向可能相反模型会学到错误的解剖对应关系。强度归一化用 Z-score 而不是简单除以 255因为 MRI 的强度没有绝对物理意义只反映组织间的相对差异。参数上1e-8是防止除零实际数据里 std 不会这么小但保留这个习惯。2.2 切片选择为什么不能把所有 slice 都丢给模型一个 3D MRI 体积通常有 100 到 200 个轴状切片但包含海马体的切片集中在中间 30 到 50 层。海马体萎缩是阿兹海默症最敏感的影像学标志所以切片选择直接决定模型上限。我一般用两种策略一是固定取中间 20 层简单粗暴但有效二是用脑区模板如 AAL atlas定位海马体所在层只取覆盖海马体的切片。def extract_middle_slices(volume, num_slices20): 取中间 num_slices 层返回 (N, H, W) total volume.shape[2] start total // 2 - num_slices // 2 end start num_slices slices volume[:, :, start:end] # 转成 (N, H, W) slices np.transpose(slices, (2, 0, 1)) return slices def resize_slices(slices, target_size(224, 224)): 统一尺寸方便用预训练模型 import cv2 resized np.zeros((slices.shape[0], target_size[0], target_size[1])) for i, s in enumerate(slices): resized[i] cv2.resize(s, target_size, interpolationcv2.INTER_LINEAR) return resized参数说明num_slices20是经验值太少覆盖不全海马体太多引入冗余且显存吃紧。target_size(224, 224)是为了复用 ImageNet 预训练权重如果从零训练可以用 128 或 160 省显存。注意cv2.resize对医学影像的插值方式有影响INTER_LINEAR比INTER_CUBIC更不容易产生伪影。2.3 类别不均衡的处理别只用 WeightedRandomSamplerOASIS-1 里 CDR 0 的样本远多于 CDR 1 和 0.5直接训练模型会偏向多数类。常见做法有三种加权损失、重采样、数据增强。我一般先用WeightedRandomSampler让每个 batch 里类别大致均衡再配合 Focal Loss 压一压易分样本。但要注意重采样会让少数类过拟合所以增强必须跟上。from torch.utils.data import WeightedRandomSampler import torch def make_weighted_sampler(labels): labels: list of int class_counts np.bincount(labels) class_weights 1.0 / class_counts sample_weights [class_weights[l] for l in labels] sampler WeightedRandomSampler( weightssample_weights, num_sampleslen(labels), replacementTrue ) return sampler逻辑说明replacementTrue表示有放回采样少数类会被反复抽到。num_samples设成总样本数保证每个 epoch 看到的样本量和原始一致。如果发现验证集 loss 震荡厉害可以把num_samples降到总样本数的 0.8 倍减少重复样本的冲击。3. 模型选型与训练从 ResNet 到可解释热力图3.1 为什么用 2D CNN 而不是 3D CNN3D CNN 能直接处理体积数据理论上保留更多空间信息但参数量大、显存需求高OASIS-1 只有几百个样本3D 模型极易过拟合。我一般用 2D CNN 逐切片分类再对切片结果做投票或取平均。这样既能复用 ImageNet 预训练权重又能通过切片级热力图定位异常区域。如果非要用 3D建议用 3D ResNet-10 这种轻量结构配合强增强和早停。import torch.nn as nn from torchvision import models def build_model(num_classes3, pretrainedTrue): 基于 ResNet-18 的切片分类器 model models.resnet18(pretrainedpretrained) # 替换最后一层 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(in_features, num_classes) ) return model def slice_level_predict(model, slices): 对单个受试者的所有切片做预测返回平均概率 model.eval() with torch.no_grad(): inputs torch.tensor(slices).unsqueeze(1).float() # (N, 1, H, W) inputs inputs.repeat(1, 3, 1, 1) # 复制成 3 通道 logits model(inputs) probs torch.softmax(logits, dim1) avg_probs probs.mean(dim0) return avg_probs参数说明Dropout(0.5)在小数据集上是必要的正则化如果验证集准确率远低于训练集可以加到 0.6。repeat(1, 3, 1, 1)是把单通道灰度图复制成三通道以适配预训练权重如果自己从头训练可以把第一层改成单通道输入省掉这一步。3.2 训练循环里的三个必调参数学习率、batch size、早停耐心值这三个参数直接决定能不能收敛。我一般用 AdamW学习率 1e-4batch size 16 或 32早停耐心 10 个 epoch。如果显存不够用梯度累积模拟大 batch。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) # 初始化 model build_model(num_classes3).to(device) optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50) criterion nn.CrossEntropyLoss()逻辑说明weight_decay1e-4是 AdamW 的默认推荐值比 Adam 的 L2 正则更稳。CosineAnnealingLR让学习率从 1e-4 余弦下降到 0避免后期震荡。如果训练 loss 不降先检查数据归一化是不是漏了再看学习率是不是太大。3.3 可解释性Grad-CAM 热力图怎么画才不糊Grad-CAM 是毕设里最常被要求加的可解释性模块。原理是用目标类别对最后一层卷积特征图的梯度做加权得到热力图。关键点是目标层选哪里选太浅热力图太散选太深分辨率不够。ResNet-18 我一般选layer4的最后一个 block。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image def generate_cam(model, input_tensor, target_layer, target_class): cam GradCAM(modelmodel, target_layers[target_layer]) grayscale_cam cam(input_tensorinput_tensor, target_categorytarget_class) return grayscale_cam[0] # 使用示例 target_layer model.layer4[-1] cam_image generate_cam(model, input_tensor, target_layer, target_class2)参数说明target_class2表示针对阿兹海默症类别生成热力图如果想看模型对正常类的关注区域改成 0。热力图叠加时用show_cam_on_image的默认参数即可但要注意输入图像要先归一化到 0 到 1。如果热力图全红或全蓝检查梯度是不是被zero_grad清掉了。4. 避坑与排查小数据集医学影像的五个血泪教训4.1 验证集准确率 99% 但测试集崩了现象训练时验证集准确率冲到 99%换一批数据测试掉到 60%。原因OASIS-1 里同一受试者可能有多张扫描如果按图像随机划分同一人的切片会同时出现在训练和验证集造成数据泄露。解决按受试者 ID 划分确保同一个人的所有切片只出现在一个集合里。from sklearn.model_selection import GroupShuffleSplit def split_by_subject(subject_ids, labels, test_size0.2): gss GroupShuffleSplit(n_splits1, test_sizetest_size, random_state42) for train_idx, val_idx in gss.split(subject_ids, labels, groupssubject_ids): return train_idx, val_idx4.2 模型把所有样本都预测成多数类现象训练几个 epoch 后模型输出全是 CDR 0。原因类别不均衡加上交叉熵损失模型发现全预测多数类就能拿到不低的准确率。解决换 Focal Loss 或带权重的交叉熵同时监控少数类的召回率而不是只看准确率。class FocalLoss(nn.Module): def __init__(self, alpha1, gamma2): super().__init__() self.alpha alpha self.gamma gamma def forward(self, inputs, targets): ce_loss nn.CrossEntropyLoss(reductionnone)(inputs, targets) pt torch.exp(-ce_loss) focal_loss self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean()参数说明gamma2是原论文推荐值越大越关注难分样本。alpha可以设成类别频率的倒数但要注意和 gamma 一起调容易过拟合。4.3 热力图定位到非脑区现象Grad-CAM 热力图高亮在头骨或背景。原因预处理时没有去颅骨模型学到了头骨形状这种伪特征。解决用nilearn或FSL做颅骨剥离或者用脑模板做掩膜只保留脑组织区域。from nilearn.masking import compute_brain_mask def skull_strip(volume_img): 用 nilearn 计算脑掩膜 mask compute_brain_mask(volume_img) masked_data volume_img.get_fdata() * mask.get_fdata() return masked_data4.4 训练 loss 突然变成 NaN现象训练到一半 loss 变成 NaN。原因学习率太大、梯度爆炸或者数据里有异常值。解决加梯度裁剪检查数据里有没有全零或全常数的切片。torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)4.5 推理时显存溢出现象训练时正常推理时 OOM。原因推理时没有用torch.no_grad()或者一次把整个受试者的所有切片塞进模型。解决推理时加torch.no_grad()切片分批送入。5. 进阶技巧用切片投票和不确定性估计提升可信度5.1 切片投票比平均概率更稳前面用的是切片概率取平均但少数切片可能预测完全错误拉低整体。我一般改成投票每个切片投一票取众数。如果众数票数不过半输出“不确定”。这样在临床辅助场景里更安全。def slice_voting(model, slices, threshold0.5): model.eval() with torch.no_grad(): inputs torch.tensor(slices).unsqueeze(1).float() inputs inputs.repeat(1, 3, 1, 1) logits model(inputs) preds torch.argmax(logits, dim1).numpy() counts np.bincount(preds, minlength3) majority counts.max() / len(preds) if majority threshold: return -1, counts # 不确定 return np.argmax(counts), counts参数说明threshold0.5表示超过一半切片同意才输出结果实际可以调到 0.6 更保守。返回 -1 表示不确定前端可以提示“建议人工复核”。5.2 用 MC Dropout 做不确定性估计MC Dropout 是在推理时保持 Dropout 开启多次前向传播看预测方差。方差大说明模型对这个样本没把握。实现上把model.train()打开但只跑前向跑 20 次取均值和方差。def mc_dropout_predict(model, input_tensor, n_iter20): model.train() # 保持 dropout 开启 preds [] with torch.no_grad(): for _ in range(n_iter): logits model(input_tensor) probs torch.softmax(logits, dim1) preds.append(probs.numpy()) preds np.array(preds) mean_probs preds.mean(axis0) std_probs preds.std(axis0) return mean_probs, std_probs逻辑说明model.train()会同时开启 BatchNorm 的训练模式如果不想动 BN可以只对 Dropout 层设train()。n_iter20是精度和速度的折中10 次也能用但方差估计不稳。拿到std_probs后可以设一个阈值比如最大类概率的标准差超过 0.1 就标记为低置信。5.3 一个具体技巧用 TTA 提升推理鲁棒性测试时增强TTA是对同一张切片做轻微平移、旋转、翻转分别预测后平均。医学影像里左右翻转要慎用因为脑区不对称但小角度旋转和平移是安全的。import torchvision.transforms as T def tta_predict(model, image, n_aug5): model.eval() transforms [ T.Compose([]), T.Compose([T.RandomRotation(5)]), T.Compose([T.RandomAffine(0, translate(0.05, 0.05))]), T.Compose([T.RandomHorizontalFlip(p1.0)]), T.Compose([T.RandomRotation(-5)]), ] all_probs [] with torch.no_grad(): for t in transforms[:n_aug]: aug_img t(image) logits model(aug_img.unsqueeze(0)) all_probs.append(torch.softmax(logits, dim1).numpy()) return np.mean(all_probs, axis0)参数说明RandomRotation(5)是正负 5 度再大可能切掉脑区边缘。RandomHorizontalFlip在脑影像里要谨慎如果数据集左右方向已经统一翻转会制造错误样本建议只在验证阶段试不要用在最终推理。我自己做这类毕设时最大的教训是别一上来就堆模型。先把数据划分做对把类别不均衡处理干净再谈模型结构。很多同学卡在 60% 准确率上不去回头一查都是按图像随机划分导致的数据泄露。另外可解释性不是装饰Grad-CAM 热力图如果高亮在头骨上说明预处理没做到位这种结果拿给答辩老师看反而扣分。希望帮到你。本文还有配套的精品资源点击获取
返回列表