简介:面向计算机相关专业学生与科研人员的Python毕业设计项目,聚焦基于多模态融合的阿尔兹海默症智能诊断方法,通过融合临床影像等多维特征完成脑疾病分类判断,覆盖从数据预处理、特征提取到模型训练与评估的完整流程,适合本科毕设、课程设计或项目初期立项演示,也适合对深度学习感兴趣的小白进阶参考。资源包为zip格式,共26个文件、1.6MB,主体是9个Python源码文件,分别承担数据加载、多模态融合、骨干网络搭建、训练测试等关键环节;另含7个pyc编译文件、4张可视化图片(模型结构图、Loss曲线、Accuracy曲线、ROC_AUC曲线)、1个EfficientNet checkpoint权重文件及README文档,便于快速理解与复现。代码均经作者测试运行成功,答辩评审平均分达96分,文档内附运行说明,遇到问题可私聊远程教学;可视化图表能直观呈现训练过程和融合策略,既适合快速跑通基准实验,也便于在此基础上做算法改进,有基础者还可尝试修改模块实现其他功能。目前已有196人学习下载,可在多模态深度学习、医学影像分类等场景中作为直接参考。
1. 毕业设计选这个题:多模态融合AD诊断的含金量与真实工作量
选“基于多模态融合的阿尔兹海默症智能诊断方法”做毕业设计,表面上是把热门的多模态融合算法搬进医学图像,实际第一步不是搭网络,而是把MRI体素、MMSE量表、基因型三类完全不同构的数据变成同一批能进训练循环的样本。这个方向能解决的问题很明确:在结构影像之外,把量表这类廉价但有效的信号一起放进诊断模型,对阿尔兹海默症和轻度认知障碍做辅助判断。下面按我做过同类项目的顺序,把整套主题拆成五段:模态选型、融合层设计、PyTorch闭环、源码文档避坑、答辩验证。适合有python基础、想拿现成源代码和文档快速起步的学生,也适合第一次接触医学多模态的算法工程师当路线图。
2. 多模态融合的模态怎么选:MRI、量表与基因特征的对齐方案
2.1 从阿尔兹海默症诊断角度看模态:结构像、代谢像、量表与基因
AD相关公开数据集里最常见的是ADNI这种组织形式:每个受试者入组后拍T1加权MRI,做MMSE、CDR、ADAS-Cog等神经心理量表,一部分人补做FDG-PET,还有一部分人查了ApoE基因分型。你要做的“多模态融合”,落在真实文件结构里就是一堆 .nii.gz 扫描、一个包含量表分数的临床CSV、可能还有一列基因标签,不是一个封装好的张量。
三种模态在诊断里的定位差异值得先讲清楚。T1结构像显示的是海马体体积萎缩、脑室扩大,属于AD的中晚期改变,胜在数据量大、标注最全;FDG-PET显示的是颞顶叶葡萄糖代谢下降,出现更早,但公开数据集里PET样本经常只有MRI的零头,直接进训练,类别均衡会很难看;MMSE、CDR这类量表虽然维度低,判别力却不弱,临床医生本来也是靠量表加影像综合判断。
所以我的建议是:毕设的主模态组合用“MRI影像+临床量表”,把ApoE、PET写进论文的扩展讨论,不要硬凑齐全模态。源代码包里的通道数、特征维度如果对不上某个模态,八成是公开数据本身缺样本,不是代码写错了。这个判断在动手前就定下来,能帮你省掉大量整理数据的无效时间。
2.2 特征对齐:三个模态怎么变成一行训练样本
多模态融合算法的第一步不是网络结构,是对齐。对齐有三个层面,任何一个不做干净,模型结果就是黑匣子里掺了脏数据。第一是ID对齐:同一受试者在影像文件、临床CSV、基因表格里必须用同一个subject_id关联,扫描日期不一致时,常规做法是取基线期数据,因为ADNI这类队列研究里每个受试者入组基线是最完整的。第二是形态对齐:MRI体素网格和一条量表记录完全不是一个量纲,所以要么统一重采样到固定尺寸,要么让影像分支自己抽特征,在融合层之前把图像压缩成固定维度的特征向量。第三是缺失值对齐:MMSE缺失、CDR缺失、某受试者没做MRI,各自要有记录,影像缺失的样本直接剔除,量表缺失先用均值填充再在文档里写明填充比例。
这里值得强调一个容易被当成“简单预处理”的细节:如果只按“有影像且有效量表分数”的样本做筛,数据会大幅萎缩。常见做法是数据清洗脚本把“剔除多少、填充多少、保留多少”逐行打印出来。我的经验是,这行日志在答辩时很好用,能直接回应“你的数据有没有挑样本”这种问题。用pandas做这步就是典型的python数据分析流程,先把subject_id设成索引,再按时间点排序去重,最后合并影像路径和量表列,逐列查空值。
2.3 前端、后端、中间融合:选哪个当主方案
多模态融合论文里最常见的三条路径是前端融合、后端融合、中间融合。前端融合在输入层把图像和其他模态拼成一个多通道张量,体素和量表数值混在一起,模态间尺度差异太大,收敛会非常吃力,毕设里我一般不推荐。后端融合是把影像分支和临床分支分别编码,输出两个特征向量,拼起来喂给分类头,结构清晰、好改好调,是稳妥的第一步。中间融合是在网络中间层做跨模态交互,可以用注意力门控给两个模态算权重,效果上限更高,但训练敏感,调参成本明显涨。
| 融合方式 | 实现成本 | 主要风险 | 适用阶段 |
|---|---|---|---|
| 前端融合 | 低 | 模态尺度差异大,训练难收敛 | 模态类型接近时 |
| 后端融合 | 低 | 某一模态特征主导,需调正则 | 毕设首选方案 |
| 中间融合 | 中 | 训练敏感,调参成本高 | 有充足数据和训练时间 |
对于这个题目,我的做法是先写一版后端融合作为baseline,把AUC跑到可接受范围,再用门控注意力替换简单拼接,和baseline对比。这样论文里有“从简单融合到注意力融合”的递进,源代码和文档说明也更好组织,而不是一上来就端出一个训不动的融合模块,变成进度事故的重灾区。在3.3节里我会把这两种融合模块都给出能运行的代码。
2.4 最小数据预处理:从ADNI目录到统一的npy样本
import pandas as pd import nibabel as nib import numpy as np from pathlib import Path from scipy.ndimage import zoom def load_mri_volume(mri_path, target_size=(64, 64, 64)): data = nib.load(str(mri_path)).get_fdata() factors = ( target_size[0] / data.shape[0], target_size[1] / data.shape[1], target_size[2] / data.shape[2], ) resized = zoom(data, factors, order=1) return resized.astype(np.float16) def build_dataset(mri_dir, clinical_csv): clinical = pd.read_csv(clinical_csv) samples, dropped = [], 0 for _, row in clinical.iterrows(): mri_file = Path(mri_dir) / f"{row['subject_id']}.nii.gz" if not mri_file.exists(): dropped += 1 continue volume = load_mri_volume(mri_file) # 表格特征:年龄、MMSE、CDR,性别编码成0/1 features = row[["age", "MMSE", "CDR", "gender_code"]].values.astype(np.float32) label = int(row["label"]) samples.append((volume, features, label)) print(f"保留 {len(samples)} 个样本,剔除 {dropped} 个缺失影像") return samples这段脚本不追求医学图像处理精度,目的是让你先看到数据流全貌。zoom是线性插值重采样,把原始体素压到64的立方体,order=1表示线性插值,计算量小但对细小结构有平滑;如果后面要出正式结果,应该换成ANTs或FSL的配准和偏置场校正。float16是为了省内存,训练时转回float32。注意continue只是跳过缺失影像,所有被跳过的subject_id一定要单独记到日志里,后面做数据分布分析要用。
3. 用PyTorch实现智能诊断模型:影像分支、临床分支与融合层
3.1 影像分支:3D CNN、2.5D切片还是微调2D?
理论最合理的影像编码器是3D CNN,因为AD诊断的本质是体素级萎缩模式;但8GB显存一跑3D ResNet,batch size瞬间掉到2左右,训练时间拉得很长,而且3D版本的ImageNet预训练权重几乎没有,每次训练都像从零开始。个人经验是,毕设阶段用2.5D切片策略最划算:把体积在轴向等距抽三张相邻切片,堆成一个3通道图,直接吃ResNet18的ImageNet预训练权重,训练成本和2D一致,又保留了一定三维上下文。
class SliceBranch(nn.Module): def __init__(self, in_channels=3, out_dim=256): super().__init__() self.backbone = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) self.backbone.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3, bias=False) self.backbone.fc = nn.Linear(self.backbone.fc.in_features, out_dim) def forward(self, x): return self.backbone(x)这段代码重点在两个替换:第一,conv1的输入通道改成3,适配三张切片堆叠;第二,fc改成out_dim=256,与临床分支保持同一维度。也有人把in_channels改成1,直接对单张中间层切片做诊断,那是纯2D方案,会丢失上下层信息。2.5D是折中,你在文档里把它定位成“计算资源受限下的工程选择”,别硬吹成医学上的创新,答辩评委会认可这种诚实。
3.2 临床分支:MLP与特征归一化
临床特征也就四五个维度,一个带Dropout的MLP足够。难点在于特征尺度:MMSE满分30,年龄是几十,性别编码是0/1,不归一化会让MLP第一层的梯度被大数值列带走。我一般在训练脚本里用sklearn的StandardScaler先在训练集上拟合,再transform训练集和测试集。准则是绝不能用全量数据去fit scaler,否则信息泄漏,道理和数据划分泄漏一致。
class ClinicalBranch(nn.Module): def __init__(self, in_dim, hidden_dim=64, out_dim=256): super().__init__() self.net = nn.Sequential( nn.Linear(in_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, out_dim), nn.ReLU(), ) def forward(self, x): return self.net(x)in_dim由表格列数决定,如果用了年龄、MMSE、CDR、性别编码就是4;out_dim必须和影像分支一致才能做后面的融合。Dropout取0.3在这种低维输入上不会太激进,如果临床特征本身噪声大,可以提到0.5。注意StandardScaler放在训练脚本里,不要放进模型类。因为模型forward里做标准化会在推理时引入训练集统计量的耦合,形式上是错的。
3.3 拼接与门控注意力:融合模块怎么写
后端融合最简单的是concat,直接把影像特征和临床特征拼起来,接全连接分类头。另一种常见改进是门控注意力,用sigmoid生成0到1的权重,按元素加权两个分支的特征,让模型自己决定更相信哪一路。门控融合理论上能在模态置信度低时自动降低该模态的权重,实践中这个权重最容易过拟合。
class ConcatClassifier(nn.Module): def __init__(self, img_dim=256, cli_dim=256, num_classes=2): super().__init__() self.head = nn.Sequential( nn.Linear(img_dim + cli_dim, 128), nn.ReLU(), nn.Dropout(0.4), nn.Linear(128, num_classes), ) def forward(self, img_emb, cli_emb): return self.head(torch.cat([img_emb, cli_emb], dim=1)) class GatedFusion(nn.Module): def __init__(self, dim=256): super().__init__() self.gate = nn.Linear(dim * 2, dim) def forward(self, img_emb, cli_emb): g = torch.sigmoid(self.gate(torch.cat([img_emb, cli_emb], dim=1))) # 加权融合后保持维度不变,再接分类头 return g * img_emb + (1 - g) * cli_embConcatClassifier里分类头单独写,用门控融合时,gate的输出是256维加权特征,后面再挂一个和分类头一致的线性层。特征维度不一致时,把dim=256改成一致维度即可。这里的0.4是分类头Dropout,融合层本身不用太大,主要防住单模态主导,后面5.3节还会展开。
3.4 训练循环与AUC评估:把闭环跑通
固定下来的数据通路是:预处理脚本返回(volume, clinical, label),再封装成Dataset。下面这个__getitem__从64立方体中取中心层以及上下各gap一层,拼成3通道图:
class ADDataset(Dataset): def __init__(self, samples, slice_gap=2): self.samples = samples self.slice_gap = slice_gap def __len__(self): return len(self.samples) def __getitem__(self, idx): volume, clin, label = self.samples[idx] mid = volume.shape[0] // 2 # 以中心层为基准,抽中间层以及上下gap各一层 slices = [volume[mid - self.slice_gap], volume[mid], volume[mid + self.slice_gap]] img = np.stack(slices, axis=0).astype(np.float32) clin = clin.astype(np.float32) return torch.from_numpy(img), torch.from_numpy(clin), torch.tensor(label, dtype=torch.long)slice_gap取2表示抽第30、32、34层(以64层为例),gap太小三张切片过于相似、和单张2D没区别,gap太大会跨出海马体层面。影像尺寸不是64时,先按2.4节的resize处理。Dataset里不要做随机增强,训练时想翻转、平移干扰可以放transform,测试集保持原样并固定随机种子。
训练循环用AdamW、交叉熵loss,评估指标用AUC而不是准确率。原因很简单:AD数据集里正常对照组往往多于患者,准确率会被多数类拉高。
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total = 0 for img, clin, label in loader: img, clin, label = img.to(device), clin.to(device), label.to(device) optimizer.zero_grad() logits = model(img, clin) loss = criterion(logits, label) loss.backward() optimizer.step() total += loss.item() return total / len(loader) def evaluate_auc(model, loader, device): model.eval() preds, labels = [], [] with torch.no_grad(): for img, clin, label in loader: img, clin = img.to(device), clin.to(device) logits = model(img, clin) prob = torch.softmax(logits, dim=1)[:, 1].cpu().numpy() preds.extend(prob) labels.extend(label.cpu().numpy()) return roc_auc_score(labels, preds), preds, labelstrain_one_epoch的返回值只是loss均值,训练中真正要盯的是验证集AUC;evaluate_auc返回概率向量,方便后面画ROC曲线。注意model是包括了分支和融合层的整体Module,forward签名是(img, clin),和这段代码的调用一致。
4. 跑源码和写文档前必看:五个常见踩坑记录
4.1 环境不一致:torch没有attribute、算子缺失的翻车现场
现象:按README装完依赖,跑模型构造就报错,常见的是module 'torch' has no attribute 'concat',或者DataLoader的num_workers>0在一跑就死锁。原因多半是README只写pip install torch太随意,Python 3.11配的torch版本和代码里用到的API不匹配。这跟python入门教程里只装一个解释器不一样,项目复现必须锁版本。解决:用conda虚拟环境把Python和torch版本一并固定:
conda create -n ad python=3.8 conda activate ad pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install -r requirements.txtrequirements.txt里需要把torch包用==或>=,<锁定,不要只写一行torch。torchvision版本要和torch对应,PyTorch官方安装页的对应关系才重要。如果你是Ampere显卡,CUDA11.7配torch1.13.1是常见可靠搭配;不要盲目追最新版,目标是跑通能复现。
4.2 显存溢出:OOM不一定是显存不够,也可能输入尺寸失控
现象:CUDA out of memory,把batch size改成1还是溢出。原因:直接拿160×192×160的原始体素进3D卷积分支,或者2.5D分支输入的切片图没有resize,分辨率太高。解决:按2.4节把体积压到64×64×64,或把切片图统一resize到112或96;如果仍有OOM,用梯度累积模拟大batch:
accum_steps = 4 for i, (img, clin, label) in enumerate(loader): out = model(img, clin) loss = criterion(out, label) / accum_steps loss.backward() if (i + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()loss / accum_steps把梯度平均,每4步更新一次参数,等效于batch size扩大4倍但显存不变。注意zero_grad()要放在if块里,否则步数不是整除时梯度残留,这是很多人实现梯度累积的一点血泪经验。遇到OOM先把输入分辨率降下来,而不是盲目换小batch,因为过小的batch会让BatchNorm统计不稳定。
4.3 数据划分泄漏:同一个人被同时放进训练集和测试集
现象:训练AUC一路冲到0.98,换外部验证集跌到0.7,怎么看都觉得不对劲。原因:数据划分按“样本”切而不是按“受试者”切,同一个人的多张扫描或多次随访切片,一部分在训练集一部分在验证集,模型记住的是人而不是病变模式。ADNI这类数据里一个人有基线和随访多个时间点,按subject_id划分是唯一正确做法:
from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(X, y, groups=subject_ids))groups=subject_ids保证同一subject的所有样本都落进同一边。除分组外,还要防止同一受试者的两张相近切片在2.5D策略下天然相似,造成数据冗余,建议每个subject只保留基线一行。如果项目不是ADNI而是自定义数据,就把subject_id列作为group字段。测试时如果不同模态的采集时间不一致,也最好先按时间点过滤。
4.4 准确率虚高、AUC却不好看:类别不均衡的陷阱
现象:模型报告准确率92%,但敏感度只有55%,AD患者大量漏检。原因:样本里正常人比患者多,模型只要全预测正常类就能拿高准确率。解决:报告AUC、敏感度、特异度三件套,不要只贴准确率;同时在损失函数里调类别权重:
from torch.nn import CrossEntropyLoss weight = torch.tensor([1.0, 2.0]).to(device) # 患者类别权重更高 criterion = CrossEntropyLoss(weight=weight)权重的取法我见过两类:一是直接按样本数的反比,比如正常类样本数:患者类样本数=3:1,权重就是1:3;二是先用AUC调一轮,再看混淆矩阵里漏检那一类的比例,把权重往漏检类方向微调。第二种在测试场景更实用,但要在文档里写明换权重的依据,不然看起来像在调参凑指标。类别权重改动后,AUC和准确率会朝着不同方向走,记得用自己定义的评估结论而不是默认指标画ROC。
4.5 README写得很顺,代码一跑就挂:文档与代码版本脱节
现象:README说“运行run.py --mode=train即可”,你照着做报KeyError: 'mode',打开argparse发现参数叫--model_mode,说明文档写到一半后代码改过,文档没同步。这类问题对毕设源码的杀伤力特别大,因为答辩老师拿到代码第一件事就是看README能不能复现。解决:写一份REPRODUCE.md,包含四段:数据目录结构、运行命令、环境依赖、预期指标范围。文档里放一张跑通后的截图,把AUC范围写成“0.85~0.92”,不要只写“效果很好”。如果项目只有一两个人开发,文档同步靠纪律:每次跑通训练就顺手更新README的命令和参数,改参数字段时把argparse里对应的默认值也改了。
5. 让模型真正收敛:5个必调参数与训练策略
5.1 学习率与warmup:融合模型为什么更容易震荡
融合模型有两个分支一个融合头,学习率一个值走到底容易让影像分支预训练部分被破坏。做法是参数分组:预训练backbone用较小学习率,新加的MLP、融合头用较大学习率。
optimizer = AdamW([ {"params": model.slice_branch.backbone.parameters(), "lr": 1e-5}, {"params": model.slice_branch.fc.parameters(), "lr": 1e-4}, {"params": model.clinical_branch.parameters(), "lr": 1e-4}, {"params": model.fusion_head.parameters(), "lr": 1e-3}, ], weight_decay=1e-4)weight_decay=1e-4对融合部分有效,backbone预训练部分增强太强反而会破坏预训练权重。warmup在医学图像上很实用:前5个epoch把学习率从0线性升到指定值,避免模型在数据量少时一开始就迈大步跑飞。实现不复杂,在训练循环里写一个step counter即可;也可以用torch.optim.lr_scheduler.OneCycleLR,但那个对max_lr的计算不够直观,毕设里能讲清楚原理更重要。
5.2 patch size与batch size:显存与收敛的二者权衡
处理3D医学图像时,patch size比batch size更决定显存占用,常规经验是显存不足时先降patch再降batch。哪怕是2.5D切片分支,slice图的height和width也是关键参数。如果直接拿ResNet18的224×224输入去读64×64的切片,会触发像素放大插值,占显存且丢失细节;常见做法是resize到112或96。
一个实用组合是patch=96×96、batch=32、2.5D三通道,显存占用约8GB。如果设备只有6GB显存,把patch降到80,batch降到16,效果下降比想象中小得多。slice_gap也要跟着patch调整:patch越小,三张切片之间的gap维持2~3层比较合理。这个组合写在文档参数表里,方便复现时快速对齐。
5.3 Dropout与特征正则:别让影像分支单方面碾压临床分支
临床分支的输入维度低,训练样本少,影像分支从预训练模型里抽特征也容易过拟合。把两个分支的out_dim统一到256后,常见情况是影像特征方差大、临床特征方差小,模型直接用影像当主判断,临床变成一个点缀。两个处理:加Dropout,并在融合前对特征做L2归一化:
img_emb = F.normalize(img_emb, p=2, dim=1) cli_emb = F.normalize(cli_emb, p=2, dim=1) h = torch.cat([img_emb, cli_emb], dim=1)p=2表示L2范数归一化,两个模态特征都映射到单位超球面,尺度差异基本消除,融合层的线性层学习更平稳。训练时留意门控权重g的平均值,如果长期接近1或0,说明某一模态被彻底忽略,这时候可以给被忽略分支把hidden_dim调大,或把该分支的Dropout调小,而不是急着改融合结构。
5.4 类别权重与Focal Loss:少数类为什么还是学不动
调了类别权重后,还会出现敏感度不够的情况,一般是难分点集中在少数类上。Focal Loss是处理这个问题的常见做法,它给难分样本更高权重,给易分样本更低权重:
class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, logits, targets): ce = nn.functional.cross_entropy(logits, targets, reduction="none") pt = torch.exp(-ce) alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets) return (alpha_t * (1 - pt) ** self.gamma * ce).mean()gamma=2.0是目标检测里常用的默认值,用在AD诊断上建议从gamma=1.0调起,否则容易过拟合。比Focal Loss更简单的是在DataLoader里用WeightedRandomSampler做样本重采样,这两个策略可以一起用,但小心别把训练集分布改得太狠,导致验证集和训练集分布差异变大。每改一次权重,都要重新看AUC和敏感度的变化,别只盯着loss下降。
5.5 早停与Best模型:别在最后一个epoch就打包走人
医学图像训练里,验证AUC随epoch的走势经常是先升后降,最后一个epoch的模型不一定最泛化。最佳实践是每个epoch末尾算验证AUC,比历史最高才保存权重,并记录对应epoch数:
best_auc = 0.0 for epoch in range(epochs): train_loss = train_one_epoch(...) auc, _, _ = evaluate_auc(model, val_loader, device) if auc > best_auc: best_auc = auc torch.save(model.state_dict(), f"checkpoints/best_auc_{auc:.3f}.pt") with open("best_epoch.txt", "w") as f: f.write(f"{epoch}\n")以best_auc_{auc:.3f}.pt这种方式命名,看到文件名就知道验证集效果,答辩时直接按路径加载推理。别忘了固定随机种子的代码块要放在数据划分、模型初始化之前,否则不同机器上同一份代码会跑出不同结果。随机种子固定后,验证AUC波动一般能控制在1个百分点内;做不到这个范围,先回头检查数据划分是否有泄漏。
| 参数 | 推荐起点 | 调整方向 |
|---|---|---|
| backbone_lr | 1e-5 | 收敛太慢时涨到3e-5 |
| fusion_lr | 1e-3 | 训练震荡时降到3e-4 |
| patch_size | 96×96 | 显存不足时降到64 |
| slice_gap | 2 | 切片过于相似时升到3 |
| fusion_dropout | 0.4 | 过拟合明显时升到0.5 |
6. 把毕设从“跑通”做到“能答辩”:消融实验与复现检查
6.1 消融实验:为什么多模态真的有用
答辩老师最常问的一句是:“你不做多模态,换单模态会不会更高?”为此要跑消融实验。最简设计是三个对比:只用MRI、只用临床、MRI+临床融合。每组超参数、随机种子保持一致,结果用AUC和敏感度做对比展示。如果多模态低于单模态,常见原因有两个——数据划分泄漏或临床分支没调好。多模态AUC比单模态高0.05以上,才能撑起“融合有效”这个结论,否则就老实承认在这个数据规模下提升有限,讨论部分再分析原因。
6.2 Grad-CAM:看模型在哪个脑区做判断
Grad-CAM可以打印影像分支在切片上的注意力,定位海马体、颞叶这类AD关注区。实现不复杂:取slice_branch.backbone最后一个卷积层的特征图,用类别得分对特征图求梯度,做全局平均池化得到权重,加权求和后过ReLU,再叠加到原切片上。这个可视化不证明模型绝对正确,但能说明模型关注区域与医学知识相关,答辩现场比一百句“效果不错”都有说服力。注意只在验证集上挑样本画,不要拿训练集样本做展示。
6.3 答辩前做一次全量复现检查
交源码前我会做一次最短复现:从干净的conda环境开始,按REPRODUCE.md敲命令,把数据放进去,一路看到best_auc文件生成,耗时控制在30分钟内。走过的命令逐行记进文档,代码里用到的中间变量名如果和README不一致,马上改过来。现在我做这类项目,两个习惯改不掉:一是训练日志必须落盘并长期保留,二是文档里只写能直接复制的bash命令,不凭记忆补参数。希望这些能帮到你。
本文还有配套的精品资源,点击获取