
简介这份深度学习课程设计资源聚焦阿尔茨海默症的医学影像识别面向希望入门或进阶医学图像分析的学生与开发者可作为课程设计、毕业设计、大作业或工程实训的参考方案。资源围绕ResNet优化模型展开提供2D切片多通道识别、3D卷积建模以及MedicalNet医疗影像预训练模型三条技术路线并附有ResNet基础模型生成脚本便于读者对比不同建模思路并自行调试扩展。压缩包共13个文件约2.06MB包含6个py脚本、3个ipynb交互式笔记本、2份pdf参考论文与说明、1个csv预测结果及1个md说明文档覆盖从模型搭建、训练测试到结果输出的完整流程。目前已有90人学习浏览适合具备一定Python与深度学习基础、希望理解医学影像分类任务实现细节的读者参考借鉴。1. 从一份 ResNet.zip 说起阿尔茨海默症识别课设到底交付了什么如果你正在做深度学习课程设计选题又落在医学影像方向大概率绕不开阿尔茨海默症AD识别这个题目。它比手写数字识别有分量比目标检测好落地数据量要求也不算离谱。但真正动手时你会发现公开的 AD 影像数据大多是 3D 的 NIfTI 格式而教程里讲的 ResNet 全是 2D 彩色图中间隔着一道不小的鸿沟。这份 ResNet.zip 的价值就在于它把这道鸿沟用四种不同的技术路线填了一遍2D 切片堆叠、3D 卷积、MedicalNet 预训练迁移、以及基础 ResNet 生成脚本。解压后你会看到 2DModel.ipynb、3DModel.ipynb、MedicaNet.ipynb 三个可直接运行的 notebook配套的 MyDataSet.py、Base3DModel.py、medicalnet_model.py、resnet.py、help.py 等模块外加一份测试集预测结果.csv 和两篇参考论文 PDF。它适合两类人一类是刚学完 CNN 想找个完整项目练手的另一类是课程设计 deadline 逼近、需要一份能跑通、能改、能写进报告的参考实现。下面我按自己拆包复现的顺序把这份资源从环境到训练到排错完整走一遍。2. 环境配置与数据管线让 3D 影像变成 ResNet 能吃的张量2.1 依赖安装与目录结构确认拿到压缩包先别急着跑 notebook。我一般会先建一个干净的 conda 环境把版本锁死避免后面因为 torch 版本差异导致 3D 卷积报奇怪的错。这份代码用到的核心依赖不多但医学影像处理这块有几个容易漏装的库。conda create -n ad_resnet python3.8 -y conda activate ad_resnet pip install torch1.12.0 torchvision0.13.0 -f https://download.pytorch.org/whl/torch_stable.html pip install numpy pandas nibabel scikit-learn matplotlib tqdm jupyter这里选 Python 3.8 配 torch 1.12 是常见做法因为 MedicalNet 的预训练权重是在这个量级的版本上导出的太新的 torch 加载旧权重偶尔会遇到 key 不匹配。nibabel 是读 NIfTI 文件的关键库没有它 MyDataSet.py 里的数据加载会直接抛 ImportError。装完后确认目录结构ResNet.zip 解压后应该能看到 ResNet 文件夹里面平铺着所有 .py 和 .ipynb 文件没有额外的子目录嵌套。如果你的解压结果多了一层文件夹记得把工作目录切到真正含 MyDataSet.py 的那一层否则 notebook 里的相对路径导入会失败。2.2 MyDataSet.py 的数据组织逻辑打开 MyDataSet.py核心是一个继承自 torch.utils.data.Dataset 的类。它做的事情可以概括为扫描数据目录、按受试者划分、读取 3D 影像、做归一化和裁剪、返回张量。AD 影像数据通常按类别分文件夹存放比如 AD、MCI、CN 三个子目录每个子目录下是若干 .nii 或 .nii.gz 文件。代码里一般用 os.listdir 遍历把文件路径和标签配成对。import os import nibabel as nib import numpy as np import torch from torch.utils.data import Dataset class ADDataset(Dataset): def __init__(self, root_dir, transformNone): self.samples [] self.transform transform # 假设目录结构为 root/AD/*.nii, root/CN/*.nii for label, cls in enumerate(sorted(os.listdir(root_dir))): cls_dir os.path.join(root_dir, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.endswith((.nii, .nii.gz)): self.samples.append((os.path.join(cls_dir, fname), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] vol nib.load(path).get_fdata() # 读成 numpy 数组形状 (H, W, D) vol (vol - vol.mean()) / (vol.std() 1e-8) # 逐样本标准化 vol np.expand_dims(vol, axis0) # 加通道维 - (1, H, W, D) return torch.tensor(vol, dtypetorch.float32), label这段代码的逻辑很直白__init__负责建索引__getitem__负责单样本读取和预处理。参数上最需要注意的是标准化那一步vol.std()后面加 1e-8 是防止全黑切片导致除零。另外np.expand_dims加的是通道维3D 卷积要求输入形状是 (C, D, H, W) 或 (C, H, W, D)具体哪个在前取决于你后面用的卷积层定义这一点在 3DModel 里要对齐否则会报维度不匹配。常见做法是在 Dataset 里就固定好顺序后面模型不再转置。2.3 2D 切片堆叠策略79 张切片变 79 通道这是这份资源里比较有意思的一个设计。2DModel.ipynb 的思路不是简单取单张切片喂给 ResNet而是把每个 3D 体积沿轴向切成 79 张 2D 切片然后把这 79 张切片当成 79 个通道组成一张 (79, H, W) 的“超厚”图片。这样做的好处是保留了部分深度信息同时能直接复用 torchvision 里的 resnet50 预训练权重——只不过第一层卷积的输入通道要从 3 改成 79。import torchvision.models as models import torch.nn as nn # 加载预训练 resnet50 model models.resnet50(pretrainedTrue) # 替换第一层输入通道改为 79 old_conv model.conv1 model.conv1 nn.Conv2d(79, old_conv.out_channels, kernel_sizeold_conv.kernel_size, strideold_conv.stride, paddingold_conv.padding, biasFalse) # 替换最后的全连接层输出类别数按你的任务定 model.fc nn.Linear(model.fc.in_features, 3)参数说明pretrainedTrue会下载 ImageNet 权重如果网络不通就提前手动下载放到~/.cache/torch/hub/checkpoints/。第一层替换后新卷积层的权重是随机初始化的但其余层保留了预训练参数这在数据量不大时能明显加快收敛。model.fc的输出改成 3 是因为 AD/CN/MCI 三分类如果你的数据只有两类就改成 2。这里有个坑79 通道的输入对显存占用不小batch size 可能只能开到 4 或 8具体看你显卡。我一般会先用小 batch 跑通一个 epoch确认不 OOM 再调。3. 三条模型路线的实现差异2D、3D 与 MedicalNet 怎么选3.1 3D 卷积路线Base3DModel.py 的结构拆解3DModel.ipynb 走的是原生 3D 卷积路线底层模型定义在 Base3DModel.py。和 2D ResNet 最大的区别在于所有卷积核、池化核都多了一个深度维度。一个典型的 3D 残差块长这样import torch.nn as nn class BasicBlock3D(nn.Module): 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.relu nn.ReLU(inplaceTrue) self.downsample None if stride ! 1 or in_planes ! planes: self.downsample nn.Sequential( nn.Conv3d(in_planes, planes, kernel_size1, stridestride, biasFalse), nn.BatchNorm3d(planes)) def forward(self, x): identity x out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) if self.downsample is not None: identity self.downsample(x) return self.relu(out identity)逻辑上这就是把 2D 残差块原封不动搬到 3D但参数量和计算量是立方级增长。一个 3x3x3 的 3D 卷积核参数量是 2D 3x3 的 3 倍显存占用更是成倍上升。参数上stride控制下采样downsample分支负责在通道数变化时对齐 identity 的维度这是残差连接能成立的前提。3D 路线的优势是天然保留了空间结构信息理论上限更高劣势是训练慢、吃显存、小数据集上容易过拟合。如果你的数据只有几十个受试者3D 模型大概率不如 2D 堆叠稳。3.2 MedicalNet 迁移路线预训练权重怎么接MedicaNet.ipynb 和 medicalnet_model.py 是这份资源里技术含量最高的一块。MedicalNet 是在大规模医学影像数据集上预训练好的 3D ResNet它的权重已经学到了医学影像的通用特征。用它做迁移相当于站在别人的肩膀上对小数据集特别友好。import torch def load_medicalnet(model, weight_path): # 加载预训练权重 state_dict torch.load(weight_path, map_locationcpu) # MedicalNet 权重 key 可能带 module. 前缀需要去掉 new_state_dict {} for k, v in state_dict.items(): name k.replace(module., ) if k.startswith(module.) else k new_state_dict[name] v # strictFalse 允许部分层不匹配比如你改过 fc 层 missing, unexpected model.load_state_dict(new_state_dict, strictFalse) print(f缺失层: {len(missing)}, 多余层: {len(unexpected)}) return model参数说明map_locationcpu是为了避免在没有 GPU 的机器上加载时报错加载完再.to(device)。strictFalse是关键因为你的分类头和 MedicalNet 原始的分类头类别数大概率不同强行 strict 会直接抛异常。打印 missing 和 unexpected 能帮你确认到底哪些层没对上——如果 conv1 出现在 missing 里说明权重 key 名字对不上需要检查前缀。常见做法是先用model.state_dict().keys()和权重文件的 keys 做一次交集比对确认主干层都能匹配上再跑训练。3.3 三条路线的选型对比路线输入形式预训练来源显存需求适合场景2D 切片堆叠(79, H, W)ImageNet中数据量小、想快速出结果3D 卷积(1, D, H, W)无/自训练高数据量充足、追求精度MedicalNet(1, D, H, W)医学影像高小数据、想用迁移学习选哪条路线取决于你的约束条件。课程设计通常时间紧、数据少、显卡一般我会优先推荐 2D 堆叠路线因为它能直接吃 ImageNet 预训练收敛快调试成本低。3D 路线适合作为对比实验写进报告体现你尝试了不同方案。MedicalNet 路线是加分项但前提是你能拿到预训练权重文件并且愿意花时间处理 key 匹配的问题。4. 训练、验证与结果输出从 notebook 到预测结果.csv4.1 训练循环的关键参数三个 notebook 的训练循环结构大同小异核心都是标准的 PyTorch 训练范式。以 2DModel.ipynb 为例关键参数集中在优化器和学习率调度上。import torch.optim as optim from torch.optim.lr_scheduler import StepLR device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4) scheduler StepLR(optimizer, step_size10, gamma0.1) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step()参数说明lr0.001配 SGD 是迁移学习的常见起点如果你发现 loss 震荡厉害就降到 5e-4。weight_decay1e-4是 L2 正则小数据集上防止过拟合。StepLR每 10 个 epoch 把学习率乘 0.1让后期收敛更稳。CrossEntropyLoss内部带了 softmax所以模型输出不要再加 softmax 层否则会重复计算导致梯度异常。训练时建议每个 epoch 打印一次训练 loss 和验证准确率方便判断是否过拟合。4.2 测试集预测与结果.csv 的生成Test.py 和 notebook 末尾一般会做一件事加载验证集或测试集跑推理把预测结果写进 测试集预测结果.csv。这个文件通常包含文件名、真实标签、预测标签、各类别概率。import pandas as pd model.eval() results [] with torch.no_grad(): for imgs, labels, names in test_loader: imgs imgs.to(device) outputs model(imgs) probs torch.softmax(outputs, dim1) preds probs.argmax(dim1).cpu().numpy() for name, label, pred, prob in zip(names, labels.numpy(), preds, probs.cpu().numpy()): results.append({ filename: name, true_label: label, pred_label: pred, prob_AD: prob[0], prob_CN: prob[1], prob_MCI: prob[2] }) df pd.DataFrame(results) df.to_csv(测试集预测结果.csv, indexFalse)逻辑说明model.eval()会关闭 dropout 和 batchnorm 的训练模式这是推理前必须做的。torch.no_grad()省显存。torch.softmax把 logits 转成概率方便后续分析。写 CSV 时indexFalse避免多出一列索引。拿到这个 CSV 后你可以直接用 pandas 算混淆矩阵、分类报告写进课程设计报告里。注意 DataLoader 要返回文件名否则 CSV 里只有序号没法对应到具体受试者。4.3 评估指标与报告素材课程设计报告里通常需要准确率、精确率、召回率、F1 和混淆矩阵。这些用 sklearn 几行就能算出来。from sklearn.metrics import classification_report, confusion_matrix print(classification_report(df[true_label], df[pred_label], target_names[AD, CN, MCI])) print(confusion_matrix(df[true_label], df[pred_label]))classification_report会输出每个类别的 precision、recall、f1-score 和 support直接截图就能放进报告。混淆矩阵能看出模型在哪些类别之间容易混淆——AD 和 MCI 之间的误判通常最多因为两者影像特征接近。如果某一类 recall 特别低说明样本不均衡可以考虑加 class_weight 或者做数据增强。5. 避坑与排查跑这份代码最容易翻车的五个地方5.1 维度不匹配Conv3d 报 expected 5D input现象运行 3DModel 时报RuntimeError: Expected 5-dimensional input for 5-dimensional weight。原因Dataset 返回的张量形状是 (C, H, W, D) 或 (H, W, D)而 Conv3d 要求 (N, C, D, H, W)。解决在 Dataset 的__getitem__里确认np.expand_dims的位置或者在模型 forward 开头加x x.unsqueeze(0)补 batch 维。最稳妥的办法是打印一个 batch 的 shape和模型第一层的期望输入对一遍。5.2 显存溢出CUDA out of memory现象训练几个 batch 后报 OOM。原因3D 卷积或 79 通道输入的显存占用远超预期batch size 开太大。解决先把 batch size 降到 2 或 4用torch.cuda.empty_cache()清理缓存。如果还不行考虑把输入体积裁剪到更小的尺寸比如从 128³ 降到 96³。另外检查是否有张量忘了.detach()导致计算图一直累积。5.3 预训练权重加载失败Missing keys 一大堆现象加载 MedicalNet 权重后打印出大量 missing keys主干层也没匹配上。原因权重文件的 key 命名和你的模型定义不一致常见的是多了module.前缀或者层名拼写不同。解决先打印权重文件的 keys 和你模型的 keys肉眼比对差异。如果是前缀问题用字符串替换去掉。如果是层名不同需要手动建一个映射字典。实在对不上就只加载能对上的部分剩余层随机初始化。5.4 数据泄漏训练集和验证集混了同一个受试者现象验证集准确率异常高但测试集一塌糊涂。原因同一个受试者的多个切片被分到了训练集和验证集模型记住了这个人而不是学到了特征。解决划分数据集时按受试者 ID 划分而不是按切片划分。确保同一个受试者的所有切片只出现在一个集合里。这个坑在医学影像里非常常见也是审稿人最爱挑的问题。5.5 notebook 相对路径报错FileNotFoundError现象在 notebook 里跑open(data/xxx.nii)报文件找不到。原因notebook 的工作目录和你以为的不一样Jupyter 启动目录决定了相对路径的基准。解决在 notebook 开头加import os; print(os.getcwd())确认当前目录然后用绝对路径或者os.path.join拼接。我一般会在 notebook 第一格就把BASE_DIR定义好后面所有路径都基于它拼。6. 进阶技巧用测试时增强和模型集成把准确率再抬一截课程设计如果只跑一个模型报告容易显得单薄。我一般会在 baseline 跑通后加两个小技巧成本不高但效果肉眼可见。第一个是测试时增强TTA。思路很简单对同一张测试图像做多次轻微变换比如翻转、旋转小角度分别推理后把概率平均。这样能降低单次推理的随机性尤其在小数据集上通常能涨 1 到 3 个百分点。def predict_tta(model, img, n_aug4): model.eval() probs [] with torch.no_grad(): for i in range(n_aug): # 交替做水平翻转和垂直翻转 aug torch.flip(img, dims[3]) if i % 2 0 else torch.flip(img, dims[2]) out model(aug.to(device)) probs.append(torch.softmax(out, dim1)) return torch.stack(probs).mean(dim0)参数说明n_aug控制增强次数一般 4 到 8 次就够了太多收益递减还费时间。dims[3]和dims[2]分别对应宽度和高度方向的翻转具体维度索引取决于你的张量排列。注意翻转操作要在标准化之后做否则均值方差会对不上。第二个技巧是模型集成。把 2D 堆叠模型和 MedicalNet 模型的预测概率加权平均权重可以按各自在验证集上的准确率来分配。比如 2D 模型验证准确率 0.82MedicalNet 是 0.86那就按 0.82:0.86 归一化后加权。集成的前提是两个模型的错误不太相关如果它们在同一批样本上都错集成也救不回来。我一般会先看两个模型的混淆矩阵确认它们的误判模式有差异再集成。还有一个容易被忽略的点是学习率预热。小数据集上直接上大学习率容易把预训练权重带偏常见做法是前几个 epoch 用线性预热从 1e-5 慢慢升到 1e-3再交给 StepLR 衰减。这个改动只需要几行代码但能让训练曲线平滑很多。从那以后我每次跑医学影像的迁移学习都强制先跑一个 epoch 的 sanity check确认输入 shape、确认 loss 在下降、确认验证集没泄漏然后再开完整训练。这三步走完后面基本不会出大问题。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。