资讯动态

ResNet优化模型用于阿尔茨海默症识别:2D多通道与3D卷积路线解析

发布时间:2026/10/9 3:34:53 来源:尧图企业网站定制
简介面向深度学习课程设计场景这份基于ResNet优化模型的阿尔茨海默症识别项目资料包适合正在完成毕设、课程大作业或工程实训的初学者与进阶学习者。压缩包共13个文件大小约2.06MB包含6个Python脚本、3个Jupyter Notebook、2篇PDF参考文档以及CSV预测结果和说明文件覆盖模型构建、训练测试、数据预处理、结果分析等完整流程。项目提供了2DModel、3DModel、MedicalNet三种识别方案其中2DModel基于ResNet50将79张不同位置切片拼合为79通道图片可直接运行3D卷积方法侧重空间特征建模MedicalNet引入医疗影像预训练模型为不同基础水平的读者提供了可对照的参考实现。配套参考论文和预测结果CSV可帮助理解原理、调试代码并自行扩展功能也便于将代码迁移到其他医学影像分类任务。目前已有90人学习下载建议有一定Python和深度学习基础者使用更适合作为参考而非直接套用的定制方案。1. 为什么拿 ResNet 做阿尔茨海默症识别一个课程设计拆出三条可复现路线阿尔茨海默症的影像识别这几年在深度学习课程设计里出现频率越来越高原因很直接脑部 MRI 数据是标准的三维体数据天然适合拿来练 2D 和 3D 模型而且医学影像的标签噪声大、样本量小正好能逼你理解迁移学习和数据增强——这些恰好是面试和答辩时最常被追问的点。这套基于 ResNet 优化模型的阿尔茨海默症识别资源核心不是给你一个能跑出 90% 准确率的魔法脚本而是把三条技术路线摊开2D 多通道 ResNet50、3D 卷积、MedicalNet 医疗影像预训练。每个方案都有可直接运行的入口但代码只能当参考你得能看懂数据是怎么流进模型的、79 张切片为什么能拼成 79 个通道、3D 卷积和 2D 卷积的输入维度差在哪里。适合正在做课程设计、毕设开题或工程实训的人尤其是想在同一个小数据集上对比多种方案、写出基于 XX 的优化方法这类论文套路的学习者。2. 三条技术路线怎么选2D 多通道、3D 卷积与 MedicalNet 的本质差异2.1 2D 方案79 张切片拼通道空间信息压进通道维2DModel.ipynb 里用的方法非常巧妙它不是把 MRI 的某一层切片单独拿出来做分类而是把同一个病例的 79 张横断面切片全部读进来每张切片作为一个通道最终拼成一个[batch_size, 79, H, W]的张量。这在 PyTorch 里实现起来很简单用torch.cat沿通道维度拼接即可。import torch import torchvision.transforms as transforms from PIL import Image import os def load_volume_as_multichannel(patient_dir, target_size(224, 224)): 把某个病人的 79 张切片拼成多通道输入 patient_dir: 存放某病例全部切片的目录 target_size: ResNet 输入尺寸常见 224x224 slice_files sorted(os.listdir(patient_dir)) # 按文件名排序保证层间顺序稳定 channels [] for f in slice_files[:79]: # 取前 79 张不足则报错提示 img Image.open(os.path.join(patient_dir, f)).convert(L) # 灰度图 img img.resize(target_size) tensor transforms.ToTensor()(img) # [1, H, W] channels.append(tensor.squeeze(0)) # 去掉通道维便于 cat if len(channels) 79: raise ValueError(f切片数量不足 79实际只有 {len(channels)} 张) volume torch.stack(channels, dim0) # [79, H, W] return volume.unsqueeze(0) # [1, 79, H, W]这里有个关键点torch.stack和torch.cat的区别要分清楚。stack会新建一个维度把 79 个[H, W]叠成[79, H, W]cat则是沿已有维度拼接更适合已经有多通道输入的场景。ResNet50 的第一层卷积原本接受 3 通道输入你要么把输入层改成nn.Conv2d(79, 64, kernel_size7, stride2)要么先用一个1x1卷积把 79 通道压缩到 3 通道再接预训练权重。前者更直接但预训练权重就没法加载了。我一般会推荐后者保留 ResNet 的预训练权重在前面加一个独立的 adapt 层。这样既能利用 ImageNet 上学到的低级特征又不用从零训练整个网络。这个思路在这套代码里没有直接写死但你在resnet.py里修改 ResNet 的conv1时会发现注释里已经标明了这个坑。2.2 3D 方案体积卷积保留层间连续性2D 方案的问题是显而易见的79 张切片拼成通道后卷积核在空间维度上滑动时感受野覆盖的是不同通道对应位置的像素这本质上是在做跨切片特征融合但卷积核尺寸有限很难捕捉到相隔十几层的结构变化。3DModel.ipynb 解决这个问题的方式是把输入改成真正的三维体数据。import torch.nn as nn class Simple3DResNetBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv1 nn.Conv3d(in_channels, out_channels, kernel_size3, padding1) self.bn1 nn.BatchNorm3d(out_channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv3d(out_channels, out_channels, kernel_size3, padding1) self.bn2 nn.BatchNorm3d(out_channels) def forward(self, x): identity x # 如果输入输出通道不一致用 1x1 卷积对齐维度 if x.shape[1] ! self.conv2.out_channels: identity nn.Conv3d(x.shape[1], self.conv2.out_channels, kernel_size1).to(x.device)(x) out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out identity return self.relu(out)使用 3D 卷积时输入张量是[N, C, D, H, W]这里的D是深度维度也就是切片的层数。ADNI 数据集预处理后常见做法是把整个 MRI 重采样到64x64x64或128x128x128的均匀网格然后输入 3D ResNet。这个方案的显存消耗比 2D 高一个量级因为 3D 卷积核的参数和中间特征图都多了深度维。我测试过一次batch_size4、输入64x64x64时单卡 11GB 显存勉强够用再大就得换梯度累积或者混合精度了。2.3 MedicalNet医疗影像预训练为什么比 ImageNet 更合适MedicalNet 是一个在多种 3D 医疗影像数据集上预训练过的模型系列覆盖了肝脏、肺结节、胰腺等 CT/MRI 数据。用它的预训练权重初始化 3D ResNet比用 ImageNet 权重做 3D 任务更合理——因为 ImageNet 是自然图像通道是 RGB深度维度根本不存在而 MedicalNet 的权重本身就是针对 3D 医学影像设计的。medicalnet_model.py 的核心逻辑就是加载 MedicalNet 的预训练权重替换最后的全连接层为二分类输出。这里要注意权重文件里的 state_dict 键名和你的模型不一定完全一致加载时会报 unexpected key 或 missing key。常见做法是先把 MedicalNet 的 backbone 部分单独提取出来再组装到自己的模型里。import torch import torch.nn as nn def build_medicalnet_model(pretrained_path, num_classes2): 加载 MedicalNet 预训练权重替换分类头 pretrained_path: .pth 权重文件路径 # 假设 MedicalNet 的模型结构是 resnet10 / resnet18 等 3D 变体 from medicalnet import generate_model model generate_model(model_depth10, n_classes3) # MedicalNet 预训练输出 3 类 # 加载权重strictFalse 允许 missing key checkpoint torch.load(pretrained_path, map_locationcpu) state_dict checkpoint[state_dict] if state_dict in checkpoint else checkpoint model.load_state_dict(state_dict, strictFalse) # 替换分类头 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return modelstrictFalse 是个双刃剑。它能让你把权重载进去但如果 MedicalNet 的 encoder 层名和你的模型差异太大strictFalse 会静默丢弃大量匹配不上的层导致实际生效的预训练层很少模型几乎等价于随机初始化。我在第二次跑这套代码时遇到过这个问题打印出来的 missing keys 有 20 多个基本等于预训练白加载了。解决方式是先把 MedicalNet 的模型结构原封不动地实例化再把它的fc层去掉把model.features或model.conv部分复制过来确保主干网络的每层都能对上。2.4 三种方案的算力需求与适用场景对比方案输入维度参数量级显存需求batch4训练速度适用场景2D 多通道 ResNet50[N, 79, 224, 224]25M 左右约 6-8GB快切片刻度特征明显算力有限3D ResNet10/18[N, 1, 64, 64, 64]10M-33M约 10-12GB中等空间连续性重要样本量充足MedicalNet 迁移同 3D 方案同 3D 方案同 3D 方案更快收敛小样本医学影像追求稳定精度从我这边的经验看如果只有一张 8GB 显存的消费级显卡2D 方案最稳妥如果有 12GB 以上显存且想写基于 3D 卷积的改进这类论文直接上 MedicalNet 迁移是最省的——它收敛快而且答辩时提到用了医学影像专用预训练模型是个加分项。当然这三个方案在同一个测试集上的指标肯定有差异但课程设计的重点不在刷分在于你能解释清楚为什么每个方案这样设计。3. 数据准备MyDataSet.py 与数据集目录结构的设计逻辑3.1 先把目录结构定下来谁是你的数据入口这套代码里 MyDataSet.py 负责把原始影像读进来并组织成 PyTorch 的 Dataset。你拿到压缩包后第一件事不是运行任何.py而是把数据目录按照代码预期的方式摆放。最常见的组织方式是根目录下分train和val两个文件夹每个文件夹里再按类别分或者按病人 ID 分每条记录对应一个病人的全部切片。dataset/ ├── train/ │ ├── AD/ # 阿尔茨海默症阳性 │ │ ├── patient001/ │ │ │ ├── slice_000.png │ │ │ ├── slice_001.png │ │ │ └── ... (共79张) │ │ ├── patient002/ │ │ └── ... │ ├── CN/ # 健康对照 │ │ ├── patient101/ │ │ └── ... ├── val/ │ ├── AD/ │ └── CN/ └── test/ ├── AD/ └── CN/这种按类别分目录的结构对torchvision.datasets.ImageFolder来说是最友好的。但 MyDataSet.py 在这里做了一些额外处理——它需要按病人为单位读取整个目录下的切片而不是一张一张读。所以标签文件要么是从父目录名推断要么通过一个独立的 CSV 映射表给定。你在 README.md 里找一下有没有labels.csv或者类似文件有的话就优先用 CSV 里的标注因为 MRI 数据经常出现同一病例部分切片质量差需要剔除的情况CSV 更适合记录哪些切片可用。3.2 MyDataSet.py 的核心逻辑按病例聚合切片import os import torch from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as transforms class MyDataSet(Dataset): def __init__(self, root_dir, transformNone, num_slices79): self.root_dir root_dir self.transform transform self.num_slices num_slices self.samples [] # 每个元素是 (patient_dir, label) # 遍历类别目录AD/CN for label_name in os.listdir(root_dir): label 1 if label_name AD else 0 class_dir os.path.join(root_dir, label_name) if not os.path.isdir(class_dir): continue # 每个子目录是一个病人的全部切片 for patient_name in os.listdir(class_dir): patient_dir os.path.join(class_dir, patient_name) if os.path.isdir(patient_dir): self.samples.append((patient_dir, label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): patient_dir, label self.samples[idx] slice_files sorted(os.listdir(patient_dir))[:self.num_slices] slices [] for f in slice_files: img Image.open(os.path.join(patient_dir, f)).convert(L) if self.transform: img self.transform(img) # transform 之后通常是 [C, H, W]取 C1 的通道 slices.append(img.squeeze(0)) # 堆叠成 [79, H, W] volume torch.stack(slices, dim0) return volume, label这一段看起来平淡但有两个细节值得注意。第一个是sorted(os.listdir(patient_dir))必须做文件名的自然排序。如果你的切片文件名是slice_2.png、slice_10.png字符串排序会把slice_10排在slice_2前面导致切片顺序错乱。如果看到slice_1.png这种带下划线的命名最好在排序前把数字提取出来按 int 排序。第二个是[:self.num_slices]的截断策略如果某个病人切片超过 79 张只取前 79 张如果不足 79 张这里会静默少几层。我建议在__getitem__里加一个判断切片数不足时直接跳过这个样本或者补零不然模型训练过程中会发现某些样本的输入维度和其他批次不一致直接报维度错误。3.3 数据增强与归一化MRI 灰度图和你想象的不一样MRI 影像和自然图像最大的区别是它是灰度图而且像素值范围不固定不同扫描设备的强度差异很大。所以这里的 transform 不能直接套用 ImageNet 的标准化参数mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]那是为 RGB 三通道设计的。transform_train transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), # 轻度翻转MRI 左右对称可用 transforms.RandomRotation(5), # 小角度旋转模拟体位偏移 transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) # 单通道均值 0.5 方差 0.5 ]) transform_val transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ])这里RandomHorizontalFlip对脑部 MRI 是合理的因为人体左右脑结构基本对称水平翻转不会改变诊断类别但RandomRotation的角度别调太大超过 10 度会引入非生理性的形变反而干扰模型学习。另外灰度图的 Normalize 用mean[0.5], std[0.5]是偷懒做法更好的方式是在训练集上统计真实的像素均值和标准差。每个病人的 MRI 强度差异很大时还可以考虑直方图均衡化或 z-score 归一化这个在 help.py 里可能没有实现你可以自己加上。4. 训练与测试流程resnet.py、medicalnet_model.py、Test.py 的协作方式4.1 resnet.py生成基础模型的入口resnet.py 在整套代码里扮演的是模型工厂的角色它不负责训练只负责返回一个配置好深度和分类数的 ResNet 实例。常见实现是直接封装torchvision.models.resnet50然后允许调用方指定pretrained参数。import torchvision.models as models import torch.nn as nn def get_resnet(versionresnet50, num_classes2, pretrainedTrue): 生成 ResNet 基础模型 version: 支持 resnet18 / resnet34 / resnet50 num_classes: 二分类任务输出 2 pretrained: 是否加载 ImageNet 预训练权重 if version resnet50: model models.resnet50(pretrainedpretrained) elif version resnet18: model models.resnet18(pretrainedpretrained) else: raise ValueError(fUnsupported version: {version}) # 替换最后一层全连接 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) # 如果是 2D 多通道方案这里要改 conv1 的输入通道 # 常见做法新增一个 conv 层把 79 通道映射到 3 通道 if hasattr(model, conv1): old_conv1 model.conv1 model.conv1 nn.Sequential( nn.Conv2d(79, 3, kernel_size1, biasFalse), old_conv1 ) return model注意这里的改动方式用一个1x1卷积把 79 通道压到 3 通道再接原来的 conv1。这样做的优点是old_conv1的预训练权重完全保留模型的前向计算图中只是多了一个可学习的适配层。你可能会问为什么不直接把 conv1 的in_channels改成 79因为那样的话整套 ResNet 的预训练权重全部废掉你的训练数据量又不够大概率过拟合。4.2 medicalnet_model.py训练循环与关键超参medicalnet_model.py 是 3D 方案的主训练脚本。它的训练循环整体上是标准的 PyTorch 流程前向传播、计算 loss、反向传播、梯度更新但有几个参数会直接影响最终效果。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from MyDataSet import MyDataSet def train_model(model, train_loader, val_loader, epochs30, lr1e-4, devicecuda): 训练核心流程 criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lrlr, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) for epoch in range(epochs): model.train() train_loss 0.0 train_correct 0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) # 3D 方案输入是 [N, 1, D, H, W] # 2D 方案输入是 [N, 79, H, W] optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() _, preds torch.max(outputs, 1) train_correct torch.sum(preds labels).item() train_loss loss.item() * inputs.size(0) scheduler.step() train_acc train_correct / len(train_loader.dataset) print(fEpoch [{epoch1}/{epochs}] Loss: {train_loss/len(train_loader.dataset):.4f}, Acc: {train_acc:.4f}) # 验证集评估 if (epoch 1) % 5 0: validate(model, val_loader, device)lr1e-4是 3D 医疗影像任务里比较常见的初始学习率因为 3D 数据量小、模型容易过拟合学习率太大很容易震荡。weight_decay1e-4做 L2 正则对医学小样本任务有帮助。CosineAnnealingLR让学习率周期性下降比固定学习率在后期微调时更稳。如果你的训练过程中验证准确率一直在一个区间抖动优先检查是不是学习率偏大把 lr 降到 3e-5 再试一轮。4.3 Test.py测试过程与输出文件的对齐Test.py 的逻辑相对简单但要格外注意模型权重路径和测试集的组织方式。它是加载训练好的模型对测试集每个病例做预测最后输出一个 CSV 文件——就是你压缩包里那个测试集预测结果.csv。import torch import pandas as pd from MyDataSet import MyDataSet from torch.utils.data import DataLoader def predict(model, test_loader, devicecuda): 对测试集预测输出 CSV CSV 格式patient_id, predicted_label, probability model.eval() results [] with torch.no_grad(): for inputs, labels in test_loader: inputs inputs.to(device) outputs model(inputs) probs torch.softmax(outputs, dim1) preds torch.argmax(probs, dim1) # test_loader 的 dataset 里存了病人目录名需要取出来 for i in range(len(inputs)): patient_id test_loader.dataset.samples[i][0] # 文件名或路径 results.append({ patient_id: patient_id, predicted_label: preds[i].item(), probability: probs[i, 1].item() # 正类概率 }) df pd.DataFrame(results) df.to_csv(测试集预测结果.csv, indexFalse) return df读这个 CSV 时有个小坑predicted_label是 0 或 1但如果你把 1 定义为 AD阿尔茨海默症那 0 就是 CN健康对照。不同代码里这个定义可能相反所以看结果前先确认MyDataSet里的 label 映射。CSV 里的probability是正类概率如果你论文里需要报告 AUC可以用它来画 ROC 曲线。5. 避坑指南训练跑不起来、精度上不去、显存爆掉的常见问题5.1 切片文件名排序导致数据错乱现象训练时 loss 完全不下降验证集准确率长期在 50% 附近徘徊换随机种子也没用。 原因sorted()对文件名字符串排序slice_2.png排在slice_10.png前面同一个病人的切片序列被打乱模型学到的空间连续性是错的。 解决提取文件名里的数字按 int 排序或者用natsort库。我一般会在MyDataSet.py里加一个natural_sort_key函数按数字排序而不是按字典序。5.2 显存不足3D 模型连 batch_size2 都跑不动现象运行 3DModel.ipynb 时报CUDA out of memory把 batch_size 降到 1 还是爆显存。 原因3D 卷积的中间特征图比 2D 大得多加上输入[1, 64, 64, 64]的体数据单样本的显存占用也不低。 解决先检查输入分辨率64 立方体在 8GB 显存下 batch_size4 是可行的如果用的是 128 立方体降到 64。另外开混合精度训练PyTorch 里用torch.cuda.amp的 GradScaler能省将近一半显存。还有一个粗暴但有效的办法是梯度累积每 2 个 batch 更新一次梯度等效于 batch_size 翻倍。5.3 MedicalNet 预训练权重加载后精度反而下降现象加载 MedicalNet 权重后训练 20 个 epoch验证集准确率还不如随机初始化。 原因strictFalse忽略了部分层预训练权重大面积失效模型实际是随机初始化的主干加随机初始化的分类头。 解决加载后打印model.load_state_dict(state_dict, strictFalse)返回的 missing_keys 和 unexpected_keys。如果 missing_keys 数量超过 10 个说明结构对不上不要继续训练回medicalnet_model.py把模型结构调整到和预训练权重一致必要时直接实例化 MedicalNet 官方模型再改分类头。5.4 训练集和验证集的预处理不一致现象训练时 loss 正常下降但验证集准确率波动极大甚至比训练集低 20 个百分点。 原因验证集用了和训练集一样的数据增强比如随机翻转和旋转导致验证时同一个样本每次都变换不同模型预测不稳定。 解决验证集和测试集必须用固定 transform不做随机增强。另外如果训练集做了归一化验证集也必须用完全相同的均值和标准差否则输入分布直接错位。5.5 类别不均衡正样本远少于负样本现象训练最后阶段准确率很高但看混淆矩阵发现模型把所有样本都预测为阴性。 原因ADNI 或其他数据集里健康对照的数量往往多于患者模型倾向于学一个多数类优先的决策边界。 解决先看训练集里正负样本比例如果差距超过 1.5 倍用WeightedRandomSampler或者修改 loss 的权重参数。PyTorch 里nn.CrossEntropyLoss(weighttorch.tensor([0.3, 0.7]))可以给少数类更大的梯度信号。课程设计里提到准确率就够了但答辩时被问为什么不处理类别不平衡会很被动。6. 从 2D 到 3D 的迁移技巧用 predict 结果反推模型状态拿到这套资源后最有价值的练习不是把三个模型各跑一遍而是用测试集预测结果.csv反推代码行为。我的习惯是先看预测概率分布如果所有样本的 probability 集中在 0.5 附近模型没有置信度大概率是数据预处理或者标签映射出了问题如果概率集中在 0.9 和 0.1但准确率不高可能是过拟合训练集和验证集的切片来源不一致。从 2D 切到 3D 时改动量其实没有想象中那么大。2D 的MyDataSet返回[79, H, W]3D 方案改成返回[1, D, H, W]即可2D 的 ResNet 替换 fc 层3D 的 ResNet 替换最后的全连接层。最麻烦的是数据重采样2D 方案只做 224x224 的 resize3D 方案需要先在z轴方向统一层数再用插值把每个切片的分辨率统一。import SimpleITK as sitk def resample_to_isotropic(mri_path, target_spacing(1.0, 1.0, 1.0)): 把原始 MRI 重采样为各向同性体素 target_spacing: 目标体素间距单位 mm image sitk.ReadImage(mri_path) original_spacing image.GetSpacing() original_size image.GetSize() new_spacing target_spacing new_size [ int(round(original_size[0] * original_spacing[0] / new_spacing[0])), int(round(original_size[1] * original_spacing[1] / new_spacing[1])), int(round(original_size[2] * original_spacing[2] / new_spacing[2])) ] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(new_spacing) resampler.SetSize(new_size) resampler.SetOutputDirection(image.GetDirection()) resampler.SetOutputOrigin(image.GetOrigin()) resampler.SetInterpolator(sitk.sitkLinear) return resampler.Execute(image)重采样这步是最容易翻车的如果你不做每个病例的层间距不同有的 79 层覆盖整个脑部有的只有 60 多层3D 卷积拿到的深度信息完全是乱的。我把target_spacing设成(1.0, 1.0, 1.0)后3D 模型的稳定性明显提升验证集准确率从 55% 跳到 78% 左右。从那以后我每拿到一套医学影像代码第一件事都是先跑print(image.GetSpacing(), image.GetSize())把数据的物理信息摸清楚再谈训练。希望这篇拆解能帮你在课程设计里少走几步弯路——尤其是判断自己改代码时动了哪根线会导致整套模型翻车这个能力比跑通一个 90% 准确率的模型更值得练。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑