MRI+PET双模态与3D ResNet-18实现阿尔兹海默症分类

发布时间:2026/9/29 19:10:36
MRI+PET双模态与3D ResNet-18实现阿尔兹海默症分类 简介这是一份面向医学影像深度学习方向的本科毕业设计资源聚焦阿尔兹海默症AD的早期辅助诊断提出融合MRI与PET双模态影像的3D ResNet-18检测模型。项目涵盖影像预处理、多模态特征融合模块设计、迁移学习初始化及ADNI数据集上的对比实验代码结构清晰适合生物医学工程或计算机专业的毕设参考与二次开发。资源共27个文件以Python脚本为主9个py包含模型定义、训练与测试流程另有7个pyc编译文件、4张结果可视化图损失曲线、准确率、ROC曲线等以及README说明文档压缩包仅1.6MB便于快速下载和查阅。目前已有119人学习具有一定的参考价值。通过该资源可系统掌握多模态医学影像分类的完整流程包括数据预处理、3D卷积网络构建、特征融合策略与评估方法同时还能获得可运行的工程代码和训练结果图表为复现实验或拓展相关研究提供扎实起点。1. 这个毕设在做什么MRIPET 双模态与 3D ResNet-18 的选型逻辑用单一模态的 T1 MRI 做阿尔兹海默症分类不少公开论文能报到 85% 左右的准确率但很多人以为把 FDG-PET 加进来就能冲到 90% 以上实际做下来往往只涨三五个点预处理链路却长了一倍。标题里 MRIPET、3D ResNet-18、阿尔兹海默症这三个词放到一起本质就是一套多模态医学影像分类方案把 T1 结构像和 FDG-PET 代谢像对齐到同一个三维空间让 3D 卷积网络同时从结构和代谢两个视角判断对象是 AD 还是认知正常。适合正在做毕业设计、或者刚入门医学图像深度学习的人。这套东西真正的门槛不在模型本身而在数据准备和评估口径这两步没做好后续所有指标都不可信。这篇博文从 ADNI 数据申请讲起覆盖 SPM 处理 PET 的标准流程、3D ResNet-18 的 PyTorch 实现、训练时的显存与数据划分问题最后用 3D Grad-CAM 给模型找证据正好对应一条完整可复现的落地链路。2. 数据准备从 ADNI 原始扫描到能喂给 3D ResNet-18 的 64×64×64 张量2.1 数据集选型与模态对齐MRI 和 PET 为什么不能直接拼接做这个课题绝大多数人会选 ADNI 公开数据库。它里面包含阿尔兹海默症AD、轻度认知障碍MCI、认知正常CN三类受试者每个受试者有 T1 加权 MRI、FDG-PET有的还有淀粉样 PET、临床量表分数和诊断标签。数据格式是医学影像通用的 NIfTI一个受试者一组.nii文件加一个.json头信息。申请需要走 ADNI 官网的协议填研究用途和负责人信息审核通过后从 LONI 下载整个过程一到两周。拿到原始数据后第一个坑就来了MRI 和 PET 根本不在同一个空间里。T1 加权 MRI 一般是 1mm 各向同性体素分辨率高、解剖结构清楚PET 的体素通常是 2 到 3mm图像模糊反映的是葡萄糖代谢的分布。两者的图像坐标系、体素大小、强度单位也完全不一致——MRI 的 T1 强度是组织属性PET 的 SUVR 是相对参考区的比值。把两卷图像直接沿通道拼起来喂给网络等于把没对齐的两个世界硬凑到一起模型学到的是配准误差而不是疾病信号。所以标准做法是先把 PET 配准到同一个受试者的 MRI 上再用 MRI 到 MNI 标准空间的变形场把 PET 也带过去。这一整套通常用 SPM12 完成。另外要提一句现在很多新采集的数据来自西门子设备用了 Deep Resolve 这类 AI 加速重建技术图像纹理更干净、噪声更低但和传统重建算法的图像在细节上存在系统性差异。后面做跨中心验证或者混合多个来源数据时这个差异会被模型当成有用特征导致在外部数据上性能大跌需要提前留意。2.2 用 SPM 处理 PET 数据coregister、normalise、smooth 的批处理脚本SPM 是 MATLAB 里最常用的 PET 数据处理工具。它把流程组织为 matlabbatch 结构处理好一个受试者的 GUI 操作后可以通过spm_jobman(run, matlabbatch)批量执行。我一般对每个受试者跑三段先把 PET coregister 到同受试者的 T1再用 T1 到 MNI 的变形场做 normalise最后用 8mm 高斯核 smooth。下面是一个可直接改路径用的脚本骨架。spm(defaults, PET); spm_jobman(initcfg); subjects { {/data/subj01/t1.nii, /data/subj01/pet.nii} {/data/subj02/t1.nii, /data/subj02/pet.nii} }; matlabbatch {}; % 1. Coregister: 把 PET 配准到 T1 matlabbatch{1}.spm.spatial.coreg.estimate.ref {subjects{1}{1}}; matlabbatch{1}.spm.spatial.coreg.estimate.source {subjects{1}{2}}; matlabbatch{1}.spm.spatial.coreg.estimate.other {}; matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.cost_fun nmi; matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.tol [0.02 0.02 0.02 0.001 0.001 0.001 0.01 0.01 0.01 0.001 0.001 0.001]; matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.sep [4 2]; % 2. Normalise: 用 T1 到 MNI 的变形场写入 PET matlabbatch{2}.spm.spatial.normalise.write.subj.def {}; % 留空实际用 T1 的 y_*.nii matlabbatch{2}.spm.spatial.normalise.write.subj.resample {subjects{1}{2}}; matlabbatch{2}.spm.spatial.normalise.write.woptions.bb [-78 -112 -70; 78 76 85]; matlabbatch{2}.spm.spatial.normalise.write.woptions.vox [2 2 2]; matlabbatch{2}.spm.spatial.normalise.write.woptions.interp 4; % 3. Smooth: 高斯平滑FWHM 8mm matlabbatch{3}.spm.spatial.smooth.data {subjects{1}{2}}; matlabbatch{3}.spm.spatial.smooth.fwhm [8 8 8]; matlabbatch{3}.spm.spatial.smooth.dtype 0; matlabbatch{3}.spm.spatial.smooth.im 0; spm_jobman(run, matlabbatch);上面脚本里 coregister 的cost_fun我用的是nmi即归一化互信息对多模态配准最稳。sep是高斯金字塔层的采样间隔[4 2]表示从粗到细配准既能加快收敛又能避免陷入局部最优。normalise 的vox设为[2 2 2]把所有 PET 统一重采样成 2mm 各向同性体素这是后续能直接裁成 64×64×64 的关键。interp用 4 表示三阶 B 样条插值比线性插值更平滑适合 PET 这种低频图像。smooth 的 FWHM 8mm 是 FDG-PET 分析的常见默认值太小会保留噪声太大会把海马体、颞叶这些关键区域的代谢差异抹掉。实际跑批处理时normalise 需要先对 T1 单独做分割配准得到y_*.nii变形场再把变形场路径填进上面def字段。很多新手卡在这一步以为 coregister 完 PET 就能直接 normalise结果脚本报错说找不到变形场。我的做法是先跑spm.spatial.normalise.estimate处理全部 T1确认生成了y_开头的文件再跑上面的 write 脚本回写 PET。2.3 从 NIfTI 到训练张量裁剪、重采样与归一化SPM 输出的图像是 MNI152 空间、2mm 各向同性、大小约 91×109×91。这个尺寸直接进 3D ResNet-18 会吃掉大量显存所以一般先裁出大脑区域并缩到 64×64×64 或 96×96×96。我用 Python 的 nibabel 读取配合 numpy 和 scipy.ndimage 做重采样与裁剪。import nibabel as nib import numpy as np from scipy.ndimage import zoom def load_and_crop(path, target_size64): img nib.load(path) data img.get_fdata().astype(np.float32) # 裁掉背景用 5% 分位数估计脑区边界 brain_mask data np.percentile(data, 5) idx np.argwhere(brain_mask) z_min, z_max idx[:, 0].min(), idx[:, 0].max() y_min, y_max idx[:, 1].min(), idx[:, 1].max() x_min, x_max idx[:, 2].min(), idx[:, 2].max() cropped data[z_min:z_max1, y_min:y_max1, x_min:x_max1] # 各向同性缩放 重采样到 target_size factors [target_size / s for s in cropped.shape] resized zoom(cropped, factors, order3) # z-score 归一化 mean, std resized.mean(), resized.std() resized (resized - mean) / (std 1e-7) return resized这段代码里percentile(data, 5)是用来找脑组织边界的粗略方法对 MRI 和 PET 都适用。zoom的order3表示三阶样条插值重采样时尽量保留原本的解剖细节。z-score 归一化是医学影像训练的标配比 min-max 归一化更能消除不同扫描仪、不同重建算法带来的整体亮度差异。target_size 选 64 还是 96直接决定显存需求64³ 输入、batch size 8 在 24GB 显卡上能跑96³ 的话同一张卡只能跑 batch size 2 到 3训练速度慢很多。对本科毕设来说64³ 加上合适的随机裁剪效果已经完全够用。MRI 和 PET 分别处理后我会存成两个独立数组文件训练时在 Dataset 里按受试者读取、再沿通道或双流方式送进网络。这样预处理只做一次后面调整模型结构不用重新读原始图像。3. 搭 3D ResNet-18双流融合、损失函数与训练脚本3.1 从 2D 到 3D把 ResNet-18 的卷积、池化和 BN 改成三维版本PyTorch 官方没有现成的 3D ResNet-18但改动量很小把所有nn.Conv2d换成nn.Conv3dnn.BatchNorm2d换成nn.BatchNorm3dnn.MaxPool2d换成nn.MaxPool3d全连接层前的adaptive_avg_pool2d换成adaptive_avg_pool3d。下面是一个精简可用的 BasicBlock 和完整前向结构。import torch import torch.nn as nn class BasicBlock3D(nn.Module): expansion 1 def __init__(self, in_planes, planes, stride1): super().__init__() self.conv1 nn.Conv3d(in_planes, planes, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm3d(planes) self.conv2 nn.Conv3d(planes, planes, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm3d(planes) self.shortcut nn.Sequential() if stride ! 1 or in_planes ! self.expansion * planes: self.shortcut nn.Sequential( nn.Conv3d(in_planes, self.expansion * planes, kernel_size1, stridestride, biasFalse), nn.BatchNorm3d(self.expansion * planes) ) def forward(self, x): out torch.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) out torch.relu(out) return out class ResNet3D18(nn.Module): def __init__(self, in_channels1, num_classes2): super().__init__() self.in_planes 64 self.conv1 nn.Conv3d(in_channels, 64, kernel_size7, stride2, padding3, biasFalse) self.bn1 nn.BatchNorm3d(64) self.layer1 self._make_layer(64, 2, stride1) self.layer2 self._make_layer(128, 2, stride2) self.layer3 self._make_layer(256, 2, stride2) self.layer4 self._make_layer(512, 2, stride2) self.avgpool nn.AdaptiveAvgPool3d(1) self.fc nn.Linear(512, num_classes) def _make_layer(self, planes, num_blocks, stride): strides [stride] [1] * (num_blocks - 1) layers [] for s in strides: layers.append(BasicBlock3D(self.in_planes, planes, strides)) self.in_planes planes * BasicBlock3D.expansion return nn.Sequential(*layers) def forward(self, x): out torch.relu(self.bn1(self.conv1(x))) out self.layer1(out) out self.layer2(out) out self.layer3(out) out self.layer4(out) out self.avgpool(out) out out.view(out.size(0), -1) out self.fc(out) return out这段代码和标准 2D ResNet-18 几乎逐行对应核心改动只有维度。stride2的下采样发生在 layer2 到 layer4 的第一层每次把空间分辨率减半通道翻倍最终 64×64×64 的输入在 layer4 输出变成 4×4×4 的特征图经过全局平均池化后得到 512 维向量。in_channels1时处理单模态双模态时可以直接设in_channels2但我更推荐下面要讲的双流结构。3.2 双模态融合双流结构比通道拼接更可靠把 MRI 和 PET 直接 concat 成 2 通道输入网络的第一层卷积就要同时处理两种分布差异极大的图像。MRI 的强度代表组织密度PET 代表代谢活性底层特征互相干扰实验里往往比单模态还差。更可靠的做法是两个独立的 3D ResNet-18 分支各自提取模态特征在全局池化后拼接再接分类头。class DualStreamAD(nn.Module): def __init__(self, num_classes2): super().__init__() self.mri_stream ResNet3D18(in_channels1, num_classesnum_classes) self.pet_stream ResNet3D18(in_channels1, num_classesnum_classes) # 去掉各自的 fc保留到 512 维特征 self.mri_stream.fc nn.Identity() self.pet_stream.fc nn.Identity() self.classifier nn.Sequential( nn.Dropout(0.3), nn.Linear(512 * 2, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, mri, pet): f_mri self.mri_stream(mri) f_pet self.pet_stream(pet) fused torch.cat([f_mri, f_pet], dim1) return self.classifier(fused)双流结构有两个好处。第一两个分支的 BatchNorm 各自统计不受模态强度分布影响第二特征拼接发生在全局池化之后模型先各自把结构特征和代谢特征压缩成 512 维向量再让分类器学它们之间的关系。Dropout 我放了两层分别在拼接后和最终分类前因为 1024 维特征对只有几百例的医学影像数据集来说很容易过拟合。如果显存紧张可以先把每个分支的 fc 输出改成 256 维再拼接成 512 维分类器第一层输入随之调整性能下降很小但显存占用能省约四分之一。3.3 按 subject 划分数据集与评估指标医学影像最忌讳按 scan 随机划分数据集。同一个受试者可能在不同时间点拍了多次 MRI 和 PET如果这些重复扫描同时出现在训练集和验证集模型记住的是这个人长什么样而不是疾病模式评估结果会虚高。必须按受试者 ID 分组划分。from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(patient_ids, labels, groupspatient_ids)) train_patients np.array(patient_ids)[train_idx] val_patients np.array(patient_ids)[val_idx]这段代码用GroupShuffleSplit的groups参数保证同一个受试者的所有扫描只落在一边。random_state固定下来方便复现实验。评估指标不能只看 accuracy因为 AD 和 CN 类别往往不平衡我一般同时计算敏感性、特异性和 ROC AUC用下面这个函数一次算全。from sklearn.metrics import roc_auc_score, confusion_matrix def evaluate(y_true, y_prob): y_pred (y_prob[:, 1] 0.5).astype(int) tn, fp, fn, tp confusion_matrix(y_true, y_pred).ravel() sensitivity tp / (tp fn) specificity tn / (tn fp) auc roc_auc_score(y_true, y_prob[:, 1]) return { accuracy: (tp tn) / (tp tn fp fn), sensitivity: sensitivity, specificity: specificity, auc: auc }敏感性在医学场景里比准确率更重要——漏掉一个 AD 患者的代价远大于误报一个健康人。y_prob[:, 1]取的是模型输出中 AD 类别的概率阈值默认 0.5如果类别不平衡严重可以根据验证集 ROC 曲线重新选阈值而不是死守 0.5。训练时类别不平衡就用加权交叉熵权重设为各类别样本数的倒数。4. 训练与验证中的避坑5个让实验白做的常见问题4.1 显存溢出先降输入尺寸而不是换轻量网络现象3D ResNet-18 在单卡 2080Ti 上跑 64×64×64、batch size 8前向一两步就CUDA out of memory。原因3D 卷积的计算量和显存占用随输入尺寸呈立方增长。从 64³ 升到 96³显存需求不是翻一倍而是翻三倍左右。很多人第一反应换成 3D ResNet-10 或 MobileNet其实瓶颈在特征图大小不在层数。解决先降 batch size直到 batch size 1 还能跑如果 batch size 1 都跑不动把输入从 64×64×64 降到 48×48×48或者把 layer3、layer4 的 stride 提前。我自己的习惯是固定 64³ 输入batch size 设 424GB 卡上能比较舒服地训练。如果梯度噪声大就开梯度累积每 4 个 batch 更新一次参数等效 batch size 16显存占用不变。4.2 SPM 处理顺序错了PET 全飘了现象训练时 MRI 那个分支收敛正常PET 分支 loss 降不下去验证集 AUC 一直在 0.6 以下徘徊。把 PET 单独拎出来可视化发现整卷图像位置偏了半个脑区海马体代谢低得离谱。原因PET 的 normalise 必须在 coregister 之后。如果直接用原始 PET 做 normaliseSPM 尝试用 PET 本身估计到 MNI 的变形。PET 没有清晰的解剖结构估计出的变形场不稳定图像经常被拉歪。更隐蔽的错误是先 smooth 再做 normalise高斯平滑会把配准需要的梯度信息抹掉。解决严格按 coregister - normalise - smooth 的顺序来。每跑完一步用 SPM 的 Check Reg 功能看一眼coregister 后 PET 边缘应该贴住 T1 的皮质边缘normalise 后两个模态应该在同一个 MNI 坐标下叠加显示没有偏移最后 smooth 完只做确认不做任何几何操作。这个检查流程每次数据换来源都值得重跑一遍尤其是混入了西门子 Deep Resolve 或 GE 不同重建算法的图像时配准质量差异明显。4.3 同一受试者多个时间点混进训练集和验证集现象五折交叉验证平均准确率 88%看起来不错换成按受试者 ID 划分后准确率掉到 74%。两组实验结果对不上。原因ADNI 里大量受试者有随访数据同一个人的 MRI 和 PET 可能在不同月份拍了三四次。按文件名随机划分时同一个人的图像会同时出现在训练集和验证集模型相当于开卷考试。解决所有数据划分、交叉验证、数据增强的随机种子都必须基于 subject ID而不是 scan 文件。用上一章写的GroupShuffleSplit是最省事的方法。另外要注意同一个人的 T1 和 PET 即使来自同一次随访也应该视为同一组不能把 MRI 放训练集、PET 放验证集否则构成更隐蔽的跨模态泄漏。4.4 Deep Resolve 等加速重建带来的域偏移现象在 ADNI 上训好的模型拿到某个医院用西门子设备新采的数据上做测试准确率骤降 15 个百分点。单独看预处理后的图像新数据的 PET 纹理比 ADNI 老数据干净得多噪声也少。原因ADNI 早期数据大多是传统迭代重建最近几年的数据里越来越多用 Deep Resolve 这类 AI 加速重建。这类技术通过深度网络降噪和超分辨图像在视觉上更清晰但和高斯平滑后的传统重建图像存在系统性差异。模型可能把「传统重建纹理」当成 AD 的特征之一换到新数据就翻车。解决最省事的做法是统一平滑参数把 FWHM 从 8mm 提到 10mm压低纹理差异的影响。更彻底的做法是把 Deep Resolve 处理的图像做轻度高斯扰动或弹性形变增强模拟传统重建的图像质感。如果数据量允许训练集里混入不同厂商、不同重建算法的样本比任何域适应技巧都管用。这个问题的本质是域偏移不是模型 bug别把时间浪费在调学习率上。4.5 数据增强里最常见的翻车点左右翻转与过大旋转现象加了一堆数据增强验证集准确率不升反降可视化模型关注的区域发现它盯着大脑的左侧颞叶不放而真实病灶明明在右侧海马体。原因左右翻转在自然图像里是安全的但大脑左右半球功能不对称语言区在左半球AD 早期的代谢改变也不是完全对称的。把左半球的图像翻到右边相当于给模型制造了错误标签。旋转角度过大同样危险旋转 30 度后的大脑解剖结构已经不属于任何真实人类。解决3D 医学影像的默认增强只保留随机平移 ±5 体素、绕轴向旋转 ±10 度、随机缩放 0.95 到 1.05 倍。左右翻转默认关闭除非你有明确证据表明任务对镜像对称。弹性形变可以用但控制在小幅度的局部扰动别让解剖结构变样。增强参数要在训练前用肉眼抽查一批增强后的切片确认没有产生明显畸变这一步看着耗时实际是省后面调参的时间。5. 给模型找证据3D Grad-CAM 显著性图的实现与答辩用法深度学习模型在医学影像上最大的质疑是「黑匣子」。答辩时最有效的一个加分项是把模型决策时的注意力区域可视化出来证明它学到的确实是疾病相关脑区而不是噪声或扫描伪影。对 3D 模型来说Grad-CAM 的实现并不复杂关键是拿对最后一层卷积的特征图和梯度。def grad_cam_3d(model, mri, pet, target_layer, class_idx): gradients {} activations {} 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_f target_layer.register_forward_hook(forward_hook) hook_b target_layer.register_full_backward_hook(backward_hook) output model(mri, pet) model.zero_grad() score output[0, class_idx] score.backward() hook_f.remove() hook_b.remove() weights gradients[value].mean(dim(2, 3, 4), keepdimTrue) cam torch.relu((weights * activations[value]).sum(dim1, keepdimTrue)) cam torch.nn.functional.interpolate( cam, sizemri.shape[2:], modetrilinear, align_cornersFalse) return cam.squeeze().cpu().numpy()这里target_layer我通常选layer4的最后一层因为它保留了最丰富的语义信息用register_full_backward_hook比旧版的register_backward_hook在新版 PyTorch 里更可靠。得到 3D 显著性图后把 cam 值归一化叠加到 MRI 的同一层切片上用 hot colormap 输出轴向、冠状、矢状三个视面。重点看显著性区域是否落在海马体、颞叶、后扣带回这些 AD 已知受累区域——如果模型注意力集中在脑室或颅骨边缘说明它在利用配准伪影这个模型就是不合格的。我自己的习惯是训练完每个模型先跑一遍 Grad-CAM把有代表性的错误预测样本和正确预测样本各存一组图。这不仅仅是答辩材料更是给模型做体检。有一次我发现模型把注意力放在了小脑区域排查后确认是 PET 的 SUVR 归一化参考区选错了导致小脑的强度信号成了最强特征。这类问题只有可视化才能暴露出来。这套流程做下来模型到底学到了什么、边界在哪基本心里有数答辩时也能从容应对。希望帮到你。本文还有配套的精品资源点击获取