ARTICLE DETAIL

资讯详情

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

多模态融合诊断阿尔兹海默症:PyTorch毕设源码解析

多模态融合诊断阿尔兹海默症:PyTorch毕设源码解析 简介基于多模态融合的阿尔兹海默症智能诊断方法源码包面向计算机、人工智能、电子信息等专业学生及毕业设计/课程设计场景解决脑疾病诊断模型从数据准备、特征融合到模型评估的完整实现问题。项目覆盖数据预处理、多模态特征融合、卷积神经网络ResNet、CBAM注意力构建、训练测试与结果可视化等环节适合作为毕设源码复现、课程设计参考或深度学习入门实战模板。压缩包共26个文件主体为9个Python脚本与7个Pyc编译文件分别承担数据集读取、图像变换、模型定义、融合网络与训练评估等功能另附README说明文档、EfficientNet模型检查点及Loss、Accuracy、ROC曲线结果图整体仅1.6MB模块划分清晰便于按需阅读和复用。目前已吸引196人学习下载可支撑进一步改进算法或迁移到其他医学影像分类任务。下载后建议先读README理清项目结构再结合源码理解多模态融合和注意力机制实现细节代码均已测试通过作者还提供远程答疑支持对快速上手、完成课设和答辩准备均有实际参考价值。1. 多模态融合诊断阿尔兹海默症这套 PyTorch 毕设源码的打开方式医学影像类的毕设每年都不少但真正把“多模态融合”落到代码里、还能跑通出图的并不多。这套基于 Python 的阿尔兹海默症智能诊断项目核心是把结构 MRI 和功能影像特征揉进同一个分类网络骨干用 ResNet 加 CBAM 注意力融合层单独拆成 FusionNet分类头单独做成 FusionClsN训练和测试脚本分离目录里还带了 checkpoints 权重和 Loss、Accuracy、ROC_AUC_Curve 三张结果图。换句话说它不是只给你一个模型文件而是给了一条完整的实验链路数据怎么读、模型怎么搭、训练怎么跑、指标怎么出。对正在准备毕设答辩、课程设计或者想入门多模态学习的同学来说这套代码最大的价值是“能复现”。我拿到手第一件事就是按 README 流程把训练跑起来确认 checkpoints 能直接加载做推理。本文就把拆解过程写清楚从数据组织到融合设计再到避坑按真正跑代码的顺序来。2. 数据与预处理先读懂 dataset 和 transform 的组织逻辑2.1 数据集目录结构双模态样本怎么配对多模态融合的第一步不是模型是数据对齐。这个项目里的 dataset 模块承担的就是“把不同模态的样本组织成一个 batch”的活。MRI 和 PET 影像分别放在不同子目录下通过文件名里的编号一一对应。我一般会先把整个数据目录梳理一遍确认每个 subject 都同时存在两个模态的输入再去碰模型代码。常见做法是把样本索引写进一个 CSV 或者直接用文件夹名做 subject id。dataset 加载时传入两个路径列表一个指向 MRI一个指向 PET通过共同的索引取值配对。下面这段逻辑就是从项目 dataset 里最能看出坑的地方class MultimodalDataset(Dataset): def __init__(self, mri_dir, pet_dir, label_file, transformNone): self.mri_paths sorted(glob.glob(os.path.join(mri_dir, *.nii))) self.pet_paths sorted(glob.glob(os.path.join(pet_dir, *.nii))) self.labels self._load_labels(label_file) self.transform transform def __len__(self): return len(self.mri_paths) def __getitem__(self, idx): mri sitk.ReadImage(self.mri_paths[idx]) pet sitk.ReadImage(self.pet_paths[idx]) mri self._normalize(sitk.GetArrayFromImage(mri)) pet self._normalize(sitk.GetArrayFromImage(pet)) label self.labels[idx] if self.transform: mri self.transform(mri) pet self.transform(pet) return {mri: mri, pet: pet, label: label}这里最关键的是sorted()排序。MRI 和 PET 两个目录里的文件名如果不做统一排序配对的样本就会错位训练出来的模型指标会非常诡异loss 能降准确率始终上不去。另一个细节是_normalize()医学影像每个被试的灰度范围差异很大N4 偏置场校正或者 z-score 归一化至少要做一个。该函数建议实现如下逻辑def _normalize(self, volume): volume (volume - volume.min()) / (volume.max() - volume.min() 1e-8) volume volume.astype(np.float32) return volume归一化参数要按每个样本独立计算不能在整个数据集上算全局统计量。每个被试的头部位置、扫描参数都不一样全局归一化会把个体差异放大。2.2 transform.py 里的数据增强组合与参数边界数据增强在这个项目里不是为了刷分是为了让模型在小数据集上不快速过拟合。transform.py 中常见的组合是随机裁剪、随机旋转、水平翻转外加 ToTensor 和 Normalize。但医学影像的增强和自然图像有个关键区别旋转和平移的幅度不能太大否则会破坏解剖结构的语义一致性。比如 MRI 图像旋转超过 15 度脑室形态就会失真模型学到的是伪影而不是病理特征。train_transform transforms.Compose([ transforms.RandomResizedCrop(size224, scale(0.85, 1.0)), transforms.RandomRotation(degrees10), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean[0.485], std[0.229]) ])注意这里的RandomResizedCrop的 scale 参数。我通常不会让裁剪比例低于 0.85因为医学影像里 ROI 往往占比较小裁剪太狠会把关键脑区裁掉。RandomHorizontalFlip在脑疾病诊断里是否启用有争议因为左右脑的萎缩模式在某些疾病下是不对称的但阿尔兹海默症的海马体萎缩很多研究认为大体对称所以翻转是可接受的。保守做法是用 0.3 的概率而不是 0.5。val_transform transforms.Compose([ transforms.Resize(size(224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485], std[0.229]) ])验证集和测试集绝对不能加随机增强这是所有医学影像实验的第一铁律。验证集只用 Resize 加归一化保证评测结果的确定性。很多同学把训练用的 RandomRotation 也用在测试上结果每次跑测试指标都不会完全一样答辩时被评委问一句“你的结果可复现吗”就容易翻车。3. 骨干与注意力模块resnet.py 和 cbam.py 的配合方式3.1 选 ResNet 做骨干的迁移学习考量多模态医学影像分类的骨干网络选择ResNet 几乎是默认起点。主要原因有两条一是 PyTorch 的 torchvision 里自带在 ImageNet 上预训练过的权重微调成本低二是 ResNet 的残差结构对深层特征的回传更友好在数据量不大的医学影像任务上不容易梯度消失。项目里 resnet.py 文件本质上是对 torchvision 模型的封装核心是替换最后的全连接层。这里需要留心的是预训练权重的适用范围。ImageNet 是自然图像和 MRI 的灰度切片差距非常大所以实践中通常的做法是加载预训练权重但把前几层解冻让它们适应医学图像的低级纹理特征深层保持预训练参数再慢慢微调。这个项目里 resnet.py 的文件名暗示它支持选择不同层数的 ResNet常见的做法是用 ResNet18 或 ResNet34。层数越深小数据集上越容易过拟合不是越深越好。import torchvision.models as models import torch.nn as nn def get_backbone(backbone_nameresnet18, out_dim512): if backbone_name resnet18: model models.resnet18(pretrainedTrue) elif backbone_name resnet34: model models.resnet34(pretrainedTrue) else: raise ValueError(fUnsupported backbone: {backbone_name}) # 替换最后的全连接层输出 512 维特征向量 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(p0.3), nn.Linear(in_features, out_dim), nn.ReLU(inplaceTrue) ) return model这个替换逻辑有两个参数值得调整。out_dim决定了喂给融合层的特征维度取 512 是因为与 CBAM 模块的输出维度做拼接时两个模态各 512 维正好形成 1024 维的融合向量。Dropout的 p 值在数据量只有几百例时建议设到 0.3 或 0.4防止模型把训练集的噪声模式背下来。输出的 512 维向量不是分类结果而是给后端融合层用的特征表示这一点在源码注释里通常会有说明。if backbone_name in [resnet34, resnet50]: model models.resnet34(pretrainedTrue) if backbone_name resnet34 else models.resnet50(pretrainedTrue)3.2 CBAM 注意力模块的实现要点与放置位置CBAM 在 resnet.py 之后单独存在说明它是作为一个即插即用的模块插入到骨干网络中的。CBAM 包含两个子模块通道注意力Channel Attention和空间注意力Spatial Attention前者告诉网络“看什么特征”后者告诉网络“看哪里”。对于脑疾病诊断通道注意力可以理解为筛选对疾病敏感的纹理特征空间注意力则是定位海马体、脑皮层等关键解剖区域。import torch import torch.nn as nn class CBAM(nn.Module): def __init__(self, channels, reduction16, kernel_size7): super(CBAM, self).__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) self.mlp nn.Sequential( nn.Conv2d(channels, channels // reduction, kernel_size1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, kernel_size1, biasFalse) ) self.spatial_conv nn.Conv2d(2, 1, kernel_sizekernel_size, paddingkernel_size // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): # 通道注意力 avg_out self.mlp(self.avg_pool(x)) max_out self.mlp(self.max_pool(x)) channel_att self.sigmoid(avg_out max_out) x x * channel_att # 空间注意力 avg_spatial torch.mean(x, dim1, keepdimTrue) max_spatial, _ torch.max(x, dim1, keepdimTrue) spatial_cat torch.cat([avg_spatial, max_spatial], dim1) spatial_att self.sigmoid(self.spatial_conv(spatial_cat)) x x * spatial_att return xreduction16是通道压缩比例控制 MLP 中间层的宽度。reduction 太大会丢失通道间的相关性信息太小则参数量骤增。在这个项目里因为骨干输出通道有限我建议 reduction 保持在 16 不变优先调整 kernel_size。kernel_size7是空间注意力的卷积核大小它决定了空间注意力能覆盖多大范围的邻域信息。对于 224x224 的输入特征图7 是相对安全的取值。放置位置比模块本身更能影响结果。常见的插入方式有两种一种是在 ResNet 的每个 BasicBlock 之后插入 CBAM另一种是只在 layer3 或 layer4 的输出后插入。前一种更精细但训练显存占用更大后一种更省资源而且高层特征的语义信息更丰富。这个项目既然把 cbam.py 单独拆出来实际使用时大概率是只插入到 layer3 和 layer4 之后这样既兼顾了性能又不会让训练被显存卡死。3.3 骨干输出维度的对齐技巧两个模态的骨干网络结构是相同的参数是否共享取决于实现方式。常见做法是不共享因为 MRI 和 PET 的底层纹理特征差异很大共享参数会迫使两个分支学习到相同的特征表达反而削弱多模态的互补性。在代码层面就是实例化两个独立的 get_backbone 对象mri_backbone get_backbone(resnet18, out_dim512) pet_backbone get_backbone(resnet18, out_dim512)每个 backbone 输出的 512 维向量后续会在融合网络里拼接成 1024 维。如果 backbone 输出的特征图不是一维向量而是二维特征图就需要先经过全局平均池化再送进 FC 层。项目里 resnet.py 已经替你把model.fc替换成了输出 512 维的序列结构所以后端的 fusion 模块可以直接拿到向量输入不必做额外的展平操作。这里有一个容易踩的坑如果预训练权重是从 torchvision 加载的替换 FC 层之前加载的权重会包含原 1000 维分类头的参数需要确保严格按 state_dict 的键名加载遇到尺寸不匹配时跳过。常见做法是model.load_state_dict(pretrained_dict, strictFalse)这样骨干层的预训练参数正常加载自定义的 FC 层保持随机初始化。4. 多模态融合设计FusionNet 与 FusionClsN 的两级分工4.1 融合策略对比为什么选择特征级融合多模态融合大致分三个层次数据级、特征级、决策级。数据级融合是把 MRI 和 PET 配准后拼成一个多通道输入实现最简单但要求两模态严格配准实际中很难做到像素级对齐。决策级融合是各模态独立出分类概率再加权平均实现也简单但丢掉了模态间的相关性信息。特征级融合介于两者之间先把每个模态提取成向量再拼接或加权让分类头学习模态间的交互模式。项目里 FusionNet 和 FusionClsN 两个文件并存说明走的就是特征级融合路线。MRI 和 PET 分别通过各自的 ResNet 骨干提取到 512 维特征在 FusionNet 里拼接成 1024 维向量再经过全连接层压缩为 256 维最后交给 FusionClsN 做二分类。这个过程可以用一张示意表来描述融合层次 | 实现方式 | 优点 | 缺点 数据级 | 多通道输入 | 实现简单 | 对配准要求苛刻 决策级 | 概率加权平均 | 简单可靠 | 丢失模态间相关性 特征级 | 向量拼接或加权 | 保留互补信息 | 特征维度需要调参4.2 FusionNet 的拼接与压缩实现FusionNet 的核心逻辑是接收两个 512 维向量拼接后经过若干个全连接层和激活函数得到融合特征。这里的关键参数是中间隐藏层的维度。1024 直接压到 256 会让信息丢失过多常见做法是分两步压缩1024 到 512再 512 到 256。import torch.nn as nn class FusionNet(nn.Module): def __init__(self, input_dim1024, hidden_dim512, fusion_dim256, dropout0.3): super(FusionNet, self).__init__() self.fusion nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.BatchNorm1d(hidden_dim), nn.ReLU(inplaceTrue), nn.Dropout(pdropout), nn.Linear(hidden_dim, fusion_dim), nn.ReLU(inplaceTrue) ) def forward(self, mri_feat, pet_feat): # mri_feat: [B, 512], pet_feat: [B, 512] combined torch.cat([mri_feat, pet_feat], dim1) # [B, 1024] return self.fusion(combined) # [B, 256]cat操作沿特征维度拼接BatchNorm1d 放在全连接层后面作用是对融合特征的分布做归一化稳定训练。Dropout 防止两个模态的特征相互过拟合p0.3 是折中取值。如果你的显存充足hidden_dim 可以提高到 768但对精度提升有限。需要注意的是BatchNorm1d 在小 batch 下会不稳定如果你的 batch_size 小于 16建议把它去掉。multimodal.py 文件里定义的就是这个层面的内容。它负责把两个骨干输出的特征送到 FusionNet再把 FusionNet 的输出送到 FusionClsN。严格来说multimodal.py 是整个项目里最值得读的一个文件因为多模态融合的全部接口都在这里面改动它就能切换不同的融合策略。4.3 FusionClsN 分类头的结构FusionClsN 是最后一级分类网络输入是 FusionNet 输出的 256 维特征输出是 2 个类别AD 和 NC的 logits。结构非常简单但细节里有一个重要的设计最后一层前接 Dropout避免分类头过拟合融合特征。class FusionClsN(nn.Module): def __init__(self, fusion_dim256, num_classes2, dropout0.4): super(FusionClsN, self).__init__() self.classifier nn.Sequential( nn.Dropout(pdropout), nn.Linear(fusion_dim, num_classes) ) def forward(self, fused_feat): return self.classifier(fused_feat)分类头不需要太深因为融合特征已经是从两个骨干网络中提取的高层语义信息再做多层非线性变换容易过拟合。Dropout 值设到 0.4 比 FusionNet 里高一些这是为了让分类头对融合特征的扰动更鲁棒。如果你的数据集只有几百例甚至可以设到 0.5。4.4 完整的推理链路整合把 backbone、FusionNet、FusionClsN 串起来就是一次完整的前向传播。FusionClsN.py 里应该有一个顶层模型封装负责调用两个骨干和两个融合模块整体输出预测概率。代码层面的逻辑大概是class MultimodalModel(nn.Module): def __init__(self): super(MultimodalModel, self).__init__() self.mri_backbone get_backbone(resnet18, out_dim512) self.pet_backbone get_backbone(resnet18, out_dim512) self.fusion_net FusionNet() self.fusion_clsn FusionClsN() def forward(self, mri, pet): mri_feat self.mri_backbone(mri) pet_feat self.pet_backbone(pet) fused self.fusion_net(mri_feat, pet_feat) logits self.fusion_clsn(fused) return logits训练时用交叉熵损失评估时用准确率和 ROC-AUC。整个链路的设计思路是每个模块各司其职骨干负责模态内特征提取FusionNet 负责模态间特征融合FusionClsN 负责最终分类决策。读懂这条链路你就有能力在它基础上替换骨干、改融合方式甚至扩展成三模态。5. 训练测试与跨设备复现避坑指南5.1 现象loss 持续下降但准确率纹丝不动训练时 loss 从 0.6 一路降到 0.3看起来正常但训练准确率始终在 60% 左右徘徊跟随机猜测差不多。这种“loss 在降、指标不变”的组合在这类双模态项目里最常见的原因是样本配对顺序错乱。MRI 和 PET 文件列表分别做了排序但两个排序规则不一致导致模型看到的是一个模态是样本 A、另一个模态是样本 B学习不到有效特征。解决方法是打印一个 batch 的样本路径对照检查。在 dataset 的__getitem__里临时加一行 print输出self.mri_paths[idx]和self.pet_paths[idx]确认两个路径中的编号是否一致。另一个常见原因是标签文件里的标签顺序和图像列表顺序不一致改法统一以图像列表为准重新索引标签。从那以后我每次跑多模态任务都会先做一个“配对可视化”小脚本把两个模态的样本名和标签一次性打印出来核对。这个问题很多时候表现得很隐蔽因为你不会马上想到是数据配对错了。5.2 现象训练准确率 95%验证准确率只有 70%典型的过拟合特征但在这类毕设里过拟合有几个常见的加速因素。第一个是数据增强太弱。如果 transform 里只做了 Resize 和 ToTensor没有旋转、翻转模型在小数据集上会把训练集的纹理细节背下来。第二个是 Dropout 太少。骨干网络的全连接层替换后如果没加 Dropout512 维特征会直接暴露给分类头。第三个原因是训练轮数太长小数据集训练 100 个 epoch后 20 个 epoch 几乎完全在记忆噪声。解决方法是把增强强度提升一个档位比如旋转角度从 5 度提高到 10 度裁剪比例下限从 0.9 降到 0.85。同时把骨干 FC 层的 Dropout 从 0.3 提到 0.4。还有一个有效手段是降低学习率并配合早停策略验证集指标连续 5 个 epoch 不升就停止训练。5.3 现象CUDA out of memory 报错这个项目默认输入可能是 224x224batch_size 设为 8 或 16。但如果你的显卡显存只有 6G两个 ResNet18 同时前向传播很容易直接爆显存。报错信息会出现在第二个 batch 的前向过程原因是两个骨干网络的中间特征图同时驻留显存。解决手段依次尝试把 batch_size 降到 4把输入尺寸从 224 降到 192在 dataloader 里设置pin_memoryTrue减少 CPU 到 GPU 的拷贝开销。最后一个更有效的手段是开启混合精度训练PyTorch 的 GradScaler 可以把显存占用降低近一半。这个项目作为毕设答辩时如果被问“你怎么解决显存问题”能把混合精度的原理说出来是很加分的。5.4 现象加载 checkpoints 时参数名不匹配checkpoints 目录下保存的权重是用整套模型保存的直接torch.load后load_state_dict会报Missing key(s)或Unexpected key(s)。主要原因有两种一是你改了骨干网络的结构比如把 resnet18 换成了 resnet34全连接层的参数名就变了二是保存时用的是torch.save(model.state_dict(), ...)而加载时你传入的是整个模型对象而不是 state_dict。解决方法是加载时加strictFalse只加载匹配的键。如果修改了 FC 层结构旧权重中 FC 层的参数会被跳过不会报错但也不会生效。更稳妥的做法是保存时同时存模型结构参数和 state_dicttorch.save({ model_state_dict: model.state_dict(), backbone: resnet18, fusion_dim: 256, num_classes: 2 }, checkpoints/best_model.pth)这样一个 checkpoint 文件就携带了完整的训练配置信息跨设备复现时不容易因为超参数不一致而对不上。5.5 现象ROC-AUC 曲线异常但准确率正常这个现象在二分类不平衡数据集上经常出现。准确率看起来有 85%但 ROC 曲线的 AUC 只有 0.6说明模型偏向预测多数类。AD 诊断数据集中正常对照组NC数量通常多于患者模型学到了“不管输入什么输出都偏向 NC”的捷径。解决方法是改用加权交叉熵损失根据样本比例给少数类更高的权重。在 train.py 里把nn.CrossEntropyLoss()改成nn.CrossEntropyLoss(weighttorch.tensor([1.0, ratio]))其中 ratio 是多数类样本数与少数类样本数的比值。或者使用 Focal Loss 这类专门应对类别不平衡的损失函数。修改后再次训练AUC 会明显回升。6. 进阶用法加载预训练 checkpoints 做推理与迁移到新数据6.1 直接用 checkpoints 做单样本推理checkpoints 目录里的权重已经训练到收敛直接加载做推理可以验证模型状态。为了不破坏原项目代码我一般建议单独写一个推理脚本只依赖模型定义和权重文件import torch from models.fusion_net import FusionNet from models.fusion_clsn import FusionClsN from models.resnet import get_backbone checkpoint torch.load(checkpoints/best_model.pth, map_locationcpu) model MultimodalModel() model.load_state_dict(checkpoint[model_state_dict], strictFalse) model.eval() with torch.no_grad(): mri_tensor torch.randn(1, 1, 224, 224) # 模拟一张 MRI pet_tensor torch.randn(1, 1, 224, 224) # 模拟一张 PET logits model(mri_tensor, pet_tensor) prob torch.softmax(logits, dim1) print(fAD 概率: {prob[0][1].item():.4f})如果你要加载的是纯 state_dict 而不是带配置的 checkpoint请确认你实例化的模型结构与原训练时完全一致。虚幻数据可以验证前向传播通不通但不能验证模型效果所以真实推理前一定要替换成真实的 3D MRI 和 PET 数据。6.2 在 AD 之外的脑疾病数据上做迁移实验这套代码的设计并不绑定阿尔兹海默症。如果你手里有帕金森、脑肿瘤或者轻度认知障碍MCI的影像数据只需要改两个地方数据集的标签文件改成你自己的类别以及最后的 num_classes二分类改成你的类别数。骨架和融合部分不需要改动。迁移实验有一个必须注意的边界骨干网络加载的 ImageNet 预训练权重对医学灰质图像的表达能力有限新数据量如果只有一两百例建议冻结 backbone 前两层只训练后三层和融合层。如果数据量超过 500 例再考虑全量微调。这个取舍直接决定你迁移实验是“收敛良好”还是“剧烈震荡”。6.3 把 Loss.png、Accuracy.png、ROC 图变成答辩素材的走查顺序目录下已经给出训练过程的图表但在答辩前建议自己重新生成一版用统一的图表风格替换默认的 matplotlib 样式。走查顺序是先看 Loss 曲线是否平滑下降是否存在跳变再看 Accuracy 曲线在验证集上是否震荡最后看 ROC 曲线的 AUC 值这是答辩评委最容易问的点。把三条曲线对应到训练代码中的日志输出评委问任何一条曲线你都能现场指出来自哪个 epoch。从那以后我每次接触这类多模态毕设项目都会强制自己走一遍这样的流程先理清数据配对再拆分模型结构然后逐模块替换训练最后核对指标曲线。这套方法让我少踩了很多隐形的坑也希望帮到你。拿到资源后别急着跑按顺序读完 README把目录结构画一遍再动手改代码你会少走很多弯路。本文还有配套的精品资源点击获取
返回列表