基于Jupyter的糖尿病视网膜病变诊断:ResNet迁移学习与项目实践

发布时间:2026/9/26 5:38:19
基于Jupyter的糖尿病视网膜病变诊断:ResNet迁移学习与项目实践 简介面向毕业设计、课程设计与项目开发的糖尿病视网膜病变智能诊断完整项目基于Jupyter Notebook实现。项目围绕眼底图像分类任务完整覆盖需求分析、数据来源说明、EDA探索、数据预处理与增强、模型建模、训练调参、交叉验证、错误分析及方案总结全流程代码可复现并针对类别间距离不等、训练与测试分布差异明显等实际问题给出了具体处理策略。资源包共30个文件包含16个ipynb分析训练脚本、2个py工具脚本、9张图表训练曲线、结果可视化、模型结构图等、csv提交文件与md项目文档整体约30.44MB分层目录便于按模块学习。目前已有179人学习下载。方案提供EfficientNet-B4/B5模型与augment-s10训练配置、多折交叉验证、logits stacking集成思路读者可直接参考数据增强、调参与错误分析过程在此基础上扩展降噪、分割等后续研究。1. 基于Jupyter实现糖网诊断这个毕设项目到底在做什么如果你正在准备毕业设计或课程设计想找一个“既有实际应用场景、又有明确评判指标”的深度学习项目糖尿病视网膜疾病诊断Diabetic Retinopathy简称糖网/DR几乎是医学图像方向最稳妥的选择。项目的一整套链路——眼底图像数据集、二分类或多分类标签、卷积神经网络训练、混淆矩阵评估——都可以在 Jupyter Notebook 里直接跑完。Jupyter 不只是写代码的地方它同时充当实验记录本和论文材料生成器每个 cell 的输入输出、训练过程的loss曲线、中间层特征图都能被自然保存下来最后变成项目文档里的“实验过程”章节。这个项目适合两类人一类是没做过医学图像课题的同学想用一套可控的流程完成毕设另一类是刚入门深度学习需要一份可扩展的源码模板。前面一种看重“项目完整度”后面一种看重“代码能不能读懂”。基于 Jupyter 实现的好处是你不需要把数据和代码分开在两个工具里倒腾所有里程碑节点缩成一个 notebook答辩演示时也能直接看到运行痕迹。这篇笔记会把数据集组织、模型训练、项目文档和常见翻车点拆开讲让你照着做就能复现一套可验收的糖网诊断流程。2. 从零搭建项目数据集组织、目录设计和一个可复用的数据加载类2.1 为什么选Jupyter而不是PyCharm/VS Code很多初学者在 PyCharm 里写完一个训练脚本跑完结果就关掉最后写文档时发现“跑了什么、为什么调参”全都想不起来。Jupyter notebook 的核心价值在于它把代码和解释性文本放到同一个 cell 序列里天然适合做“实验记录型”项目。训练糖网模型的过程本来就包含大量试错先小数据量试跑、看分类分布、调整图像增强策略、再上全量训练。每一步的判断依据如果散落在命令行里回头写毕业论文时很难还原而放在 notebook 里你可以在每个 cell 之间穿插 Markdown把参数选择理由、观察到的现象、下一步准备改动什么全部记录下来。另外Jupyter 对图像类任务的调试非常友好。眼底图像读进来是什么样的、裁剪后是否丢失病变特征、数据增强后的样本是否可接受这些都可以用matplotlib直接显示在 cell 下方。传统的脚本方式要反复plt.savefig再打开图片检查效率差很多。如果你之前只会 Jupyter 网页版的基础操作这个项目足够逼你把“创建文件夹、管理内核、重启 kernel、导出报告”都练一遍答辩时有东西可讲。2.2 数据集选择和目录组织常见做法是使用公开的 APTOS 2019 或 IDRiD 数据集。这类数据集的格式基本都是“一个图片文件夹 一个 CSV 标签文件”标签为0到4分别对应无病变、轻度、中度、重度和增殖性病变。考虑到毕设周期我建议从 APTOS 的 train 文件夹里自己划分训练集和验证集不要把全部数据都塞进训练否则一次 epoch 就要跑很久。拿到原始数据后第一步是建立固定目录结构。我通常这样做mkdir -p diabetic-retinopathy/{data/{raw,processed},notebooks,src,models,logs,docs}这会在项目根目录下生成data/raw原始图片和CSV、data/processed划分好的训练/验证子集、notebooksJupyter 文件、src可复用的代码模块、models权重文件、logs日志和docs文档与图表。把数据、代码、产物分开的好处是Jupyter 里的相对路径不会因为新增了某个临时文件夹而失效模型训练出的权重统一放在models下毕设文档中的目录说明可以直接复制这部分。CSV 文件里一般有三列id_code图片名称、diagnosis标签、有时候会有patient_id。不要全信 label 分布先做一次最基础的统计import pandas as pd df pd.read_csv(data/raw/aptos_train.csv) print(df[diagnosis].value_counts()) print(f总共图片数: {len(df)})这段代码的目的不是单纯看数量而是确认类别不平衡程度。糖网数据集的0类正常通常占一半左右4类增殖非常少后面必须针对这个情况做处理。另一个要看的是有没有patient_id字段。如果存在同一患者的两只眼都出现在数据集里划分训练集和验证集时就要按患者分组不能直接随机切否则会存在数据泄漏验证结果虚高。2.3 自定义Dataset类的完整实现PyTorch 的ImageFolder对“文件夹名即标签”的数据自然好用但糖网数据集是 CSV 提供标签而且后续要做患者分层我更习惯写一个自定义 Dataset。这样加载策略、图像增强路径和样本权重都能集中管理作为一个独立的.py文件放进src里notebook 直接导入。from torch.utils.data import Dataset from PIL import Image import pandas as pd import os import torch class DRDataset(Dataset): def __init__(self, csv_path, img_dir, transformNone): self.df pd.read_csv(csv_path) self.img_dir img_dir self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img_name str(row[id_code]) .png img_path os.path.join(self.img_dir, img_name) image Image.open(img_path).convert(RGB) # 统一转RGB防止部分图像是灰度 label int(row[diagnosis]) if self.transform: image self.transform(image) return image, torch.tensor(label, dtypetorch.long)这里有两个容易忽略的点。第一convert(RGB)是必须的因为眼底相机输出有时是 PNG 灰度模式如果直接交给预训练模型要求3通道会报错或某些库自动广播成单通道导致训练特征不对。第二返回的 label 要转成torch.long因为 PyTorch 的交叉熵损失函数要求目标不是 one-hot而是整数索引的 LongTensor漏掉这一步会在训练时撞上类型不匹配。在 notebbook 的调用侧还需要结合图像增强from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomRotation(degrees15), transforms.RandomAffine(degrees0, translate(0.05, 0.05)), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Resize 到 256 再 CenterCrop 到 224 是 ImageNet 预训练模型的标准套路。注意不要把随机旋转放在验证集上验证集必须是可复现的确定性变换。ColorJitter 的亮度调节对眼底图像比较安全因为原始眼底图存在亮度不均的问题适当的增强能提高泛化能力但对比度不要调太大否则病变区域的细微颜色会被洗掉。2.4 按患者分层验证集划分的关键操作如果你用的数据集没有patient_id但同一张图可能被复制过两次也需要先查重。用文件名去重其实不靠谱因为同患者两只眼的照片内容不一样只有原始 CSV 里的患者字段才能识别。假设 CSV 中有patient_id我会用 scikit-learn 的GroupShuffleSplit来划分from sklearn.model_selection import GroupShuffleSplit groups df[patient_id].values gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsgroups)) train_df df.iloc[train_idx] val_df df.iloc[val_idx] print(f训练集样本数: {len(train_df)}) print(f验证集样本数: {len(val_df)}) print(训练集包含患者数:, train_df[patient_id].nunique()) print(验证集包含患者数:, val_df[patient_id].nunique())GroupShuffleSplit跟普通train_test_split的差别在于它会保证同一个patient_id的所有行只出现在训练或验证其中一侧。你可以把划分好的 DataFrame 分别保存成train.csv和val.csv放到data/processed/目录下。这一步虽然简单但直接决定了后面模型评估是否可信——如果同一个患者的双眼同时出现在训练和验证集模型相当于“见过”这个人验证指标会漂亮不少但这在真实临床场景里不可能答辩时一旦被问到数据泄漏问题很难解释。3. 模型训练用ResNet迁移学习跑通糖网分类的完整训练脚本3.1 模型选型为什么是ResNet34糖网分类任务本质上是医学图像细粒度识别病变特征通常出现在血管、出血点、渗出位置这种任务不适合从头训练大型网络因为公开数据量远不够。常见做法是采用 ImageNet 预训练权重做迁移学习。以 ResNet 系列为例ResNet34 与 ResNet50 在单卡 11GB 显存下都能跑但 ResNet34 参数量更小训练速度更快对四分类糖网这种任务精度已经足够ResNet152 这类深层网络在小数据集上反而更容易过拟合。torchvision.models里直接加载预训练权重然后把最后一层全连接替换成自定义输出维度import torch.nn as nn import torchvision.models as models def build_resnet34(num_classes5, pretrainedTrue): model models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1 if pretrained else None) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(512, num_classes) ) return model加两层 Dropout 是有效抑制过拟合的手段。糖网任务里类别之间差异不明显模型容易直接“背”训练集所以全连接层千万不要原样保留建议换成带 Dropout 的小型分类头。in_features是从原模型动态获取的这样即使以后换成 ResNet101 也只需要改函数名。迁移学习时要决定是否微调前面的卷积层。我的习惯是第一轮冻结所有卷积层只训练新加的分类头跑 5 到 10 个 epoch等 loss 下降稳定后再解冻最后几层卷积进行联合微调。这样避免一开始就乱改预训练特征。3.2 训练脚本从优化器到训练循环训练部分的代码是项目的核心也是我在多次踩坑后沉淀下来的模板。下面这份源码可以直接放进notebooks/train.ipynb的单元格也可以存成src/train.py供后台跑两者共用配置。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models from tqdm import tqdm import os import time def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for inputs, labels in tqdm(dataloader, descTraining): inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * inputs.size(0) _, preds torch.max(outputs, 1) correct torch.sum(preds labels.data) total labels.size(0) epoch_loss total_loss / total epoch_acc correct.double() / total return epoch_loss, epoch_acc def validate(model, dataloader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in tqdm(dataloader, descValidating): inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) total_loss loss.item() * inputs.size(0) _, preds torch.max(outputs, 1) correct torch.sum(preds labels.data) total labels.size(0) epoch_loss total_loss / total epoch_acc correct.double() / total return epoch_loss, epoch_acc这段代码里有两个细节值得说明。第一total_loss用loss.item() * inputs.size(0)而不是直接累加loss.item()因为每个 batch 的 loss 是一个均值如果最后一个 batch 样本数和其他 batch 不同直接求平均会扭曲整个 epoch 的 loss。第二validate里必须写with torch.no_grad()否则验证阶段会额外消耗大量显存而且会误更新 BatchNorm 的统计量。训练主循环加上模型保存逻辑device torch.device(cuda if torch.cuda.is_available() else cpu) model build_resnet34(num_classes5).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience3, factor0.5) num_epochs 30 best_val_loss float(inf) best_model_path models/best_resnet34.pth for epoch in range(num_epochs): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc validate(model, val_loader, criterion, device) scheduler.step(val_loss) print(fEpoch {epoch1}/{num_epochs}: ftrain_loss{train_loss:.4f}, train_acc{train_acc:.4f}, fval_loss{val_loss:.4f}, val_acc{val_acc:.4f}) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model_path) print(fBest model saved at epoch {epoch1})选择保存“验证集 loss 最低”的模型而不是“验证集准确率最高”的模型这是一个容易被忽略的教训。准确率最高的点往往出现在某个峰值过拟合时刻之后立刻跌落而 loss 最低对应的模型通常在收敛性和泛化性之间更均衡。ReduceLROnPlateau的patience3意思是验证集 loss 连续 3 个 epoch 没有下降就把学习率减半遇到训练曲线震荡时factor0.5这样改动足够保守不会一下把学习率降没。3.3 类别不均衡class weights 怎么算如果不做任何处理模型会把 0 类全部蒙对其他类别一塌糊涂最后准确率看着有 70% 以上但中间类别的召回率低得可怕。缓解办法有两种第一种是给损失函数加权第二种是采样器。我两个都做但权重计算要用实际样本分布import collections label_counts train_df[diagnosis].value_counts().sort_index().values total_samples len(train_df) class_weights [total_samples / (len(label_counts) * count) for count in label_counts] class_weights torch.tensor(class_weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)CrossEntropyLoss的weight参数接受一个形状为[num_classes]的张量loss 内部会对每个样本的 loss 乘上对应类别权重。上面的公式会让少样本类别获得更高权重同时又不会出现权重值相差几十倍导致训练发散。除了权重还可以使用 PyTorch 的WeightedRandomSamplersample_weights [1.0 / label_counts[label] for label in train_df[diagnosis]] from torch.utils.data import WeightedRandomSampler sampler WeightedRandomSampler(sample_weights, num_sampleslen(train_df), replacementTrue) train_loader DataLoader(train_dataset, batch_size16, samplersampler)注意replacementTrue时每个 epoch 不会保证所有样本都出现但会强制每个 batch 里各类别比例接近均衡。这种采样法对训练集样本少的类别非常有用不过采样器只在训练集使用验证集保持自然分布才能真实反映模型在临床数据上的表现。4. 评估与项目文档把模型指标写进毕设文档的关键过程4.1 不只盯准确率kappa、召回率和混淆矩阵糖网诊断这个领域Kaggle 竞赛普遍采用 Cohen‘s Kappa 作为主指标因为它比准确率更关心类别间的一致性尤其适合分类别间有天然顺序的医学任务。写项目文档时只用准确率会显得不专业。我一般在评估阶段把四个指标一次性算出来from sklearn.metrics import cohen_kappa_score, confusion_matrix, classification_report import numpy as np model.load_state_dict(torch.load(best_model_path)) model.to(device) model.eval() all_preds [] all_labels [] with torch.no_grad(): for inputs, labels in val_loader: inputs inputs.to(device) outputs model(inputs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(Kappa:, cohen_kappa_score(all_labels, all_preds, weightsquadratic)) print(Confusion Matrix:) print(confusion_matrix(all_labels, all_preds)) print(Classification Report:) print(classification_report(all_labels, all_preds, digits4))quadratic权重会让“相邻类别预测错误”的惩罚小于“跨两级错误”这符合糖网病变程度递进的医学逻辑。如果模型把0类预测成2类是严重低级错误预测成1类还可以接受。这一点在答辩时非常加分能显示出你理解了领域评价指标而不只是套用 sklearn 默认函数。混淆矩阵建议画成热力图放进论文的“结果”章节import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(all_labels, all_preds) class_names [0, 1, 2, 3, 4] plt.figure(figsize(7, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(Predicted) plt.ylabel(True) plt.title(Confusion Matrix on Validation Set) plt.tight_layout() plt.savefig(docs/confusion_matrix.png, dpi150) plt.show()注意fmtd如果热力图里数值是小数用fmtd才能正确显示整数否则会报格式错误。另外Matplotlib 的字体问题在国内环境中经常遇到中文乱码如果论文要求中文标题建议在代码开头设置plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False4.2 训练曲线自动化记录训练过程中每个 epoch 的 loss 和 accuracy 最好实时记录到日志而不是手工复制。我会在训练循环里额外收集到 Python 列表训练结束后绘制两张图import matplotlib.pyplot as plt plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(train_losses, labelTrain Loss) plt.plot(val_losses, labelVal Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Loss Curve) plt.subplot(1, 2, 2) plt.plot(train_accs, labelTrain Acc) plt.plot(val_accs, labelVal Acc) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.title(Accuracy Curve) plt.tight_layout() plt.savefig(docs/training_curves.png, dpi150) plt.show()很多同学的论文里训练曲线是后期用 Excel 手搓的不仅麻烦还容易被质疑真实性。在 Jupyter 里这套代码直接从上一步的变量生成和实验时间线一一对应。建议在训练结束后立刻保存 plot同时把train_losses、val_losses等列表保存为 CSV 或 numpy 文件论文里要补充不同 batch_size 的对比实验时直接把多次训练结果合并到一张图上。4.3 项目文档结构和Jupyter导出毕设项目文档一般包括需求背景、数据说明、系统设计、模型实现、实验结果、总结与展望。这些章节如果全用 Word 写代码截图和实验结果很难同步更新。更好的做法是先在 Jupyter 里把每个阶段写成 cell 之间的 Markdown最后用nbconvert导出 Notebook 作为实验报告附件再用 Word 写正式论文。导出命令jupyter nbconvert --to markdown train.ipynb --output-dir docs/ jupyter nbconvert --to html train.ipynb --output-dir docs/--to markdown会把 notebook 转成带代码块的 Markdown可以直接粘贴到 Typora 或 CSDN如果允许。--to html则保留所有 cell 的输出结果方便直接作为材料提交。这里有一个很实用的细节notebook 里的图片输出默认是 base64 内嵌在 HTML 里如果文件太大会卡可以在导出之前用jupyter nbconvert --to markdown --NbConvertApp.output_images_dirdocs/images把图片单独提取到docs/images目录Markdown 引用相对路径这样文档和图片的耦合更清晰。docs/目录下的文件组织建议是这样的路径内容docs/training_curves.png训练和验证曲线docs/confusion_matrix.png混淆矩阵docs/classification_report.txt分类指标输出docs/train.ipynb带注释的完整笔记本docs/README.md项目说明和运行入口README 里写清楚环境依赖pip install torch torchvision pandas scikit-learn matplotlib、数据集路径、训练命令。答辩老师通常不看代码细节但会扫描 README 有没有写清楚复现方法这一步能让项目完整度提升一个档次。5. 避坑指南Jupyter环境、数据集划分和训练过程的5个真实教训5.1 坑一anaconda的jupyter打不开日志里报 kernel 错误现象在 Anaconda Prompt 输入jupyter notebook后浏览器里没有页面或者打开后新建 notebook 时显示“内核已挂掉”。原因最常见是 conda base 环境跟系统 Python 的路径冲突或者是 jupyter 客户端与 jupyter-server 版本不匹配。特别是安装 PyTorch 或 TensorFlow 后包管理器自动升级了多个依赖导致当前环境的jupyter_core和jupyter_client版本不一致。解决不要盲目重装 Jupyter。先执行jupyter --version conda list | findstr jupyter然后尝试更新所有相关包pip install --upgrade jupyterlab notebook jupyter_client jupyter_core如果还是打不开考虑是否会串了多个 Python 环境。jupyter notebook 网页版登录入口有时候被很多网上教程绕得云里雾里其实只需要在终端启动后复制生成的 token 链接或者用jupyter notebook --ip127.0.0.1 --port8888手动指定端口重启。5.2 坑二数据集路径包含中文或空格读图时 FileNotFoundError现象明明文件在数据集里Image.open(img_path)却报FileNotFoundError或者OSError: [Errno 22] Invalid argument。原因Windows 系统下的 Python 处理包含中文、空格或括号的路径时有时候没有问题但如果数据放在桌面上而你用的图片加载库是从 PIL 或 opencv 两个入口混着读输入编码不一致就会翻车。另外pd.read_csv读到的图片名如果带有不可见字符例如换行符拼接路径后就会找不到。解决把整个项目路径设为纯英文例如C:\Projects\DiabeticRetinopathy并在代码里统一使用import pathlib img_dir pathlib.Path(data/raw/train_images) img_path img_dir / (img_name .png)下载的数据集如果文件名自带中文建议先批量重命名import os import re for fname in os.listdir(data/raw/train_images): new_name re.sub(r[^\w\-.], _, fname) os.rename(os.path.join(data/raw/train_images, fname), os.path.join(data/raw/train_images, new_name))5.3 坑三显存溢出 CUDA out of memory训练中断现象跑第一、第二个 epoch 正常第三个 epoch 突然报CUDA out of memory。原因训练过程中 PyTorch 不会立即清理中间变量前向传播时保存的所有激活值在反向传播后可能仍被计算图持有。很多时候是因为验证集使用了过大的batch_size或者验证集加载时没有torch.no_grad()。解决先把 batch size 从 16 降到 8同时把验证阶段包在with torch.no_grad():里。还可以在每轮结束后调用两个清理语句torch.cuda.empty_cache() gc.collect()注意empty_cache()只清理 PyTorch 缓存不是直接的显存释放剂不要把它写进训练循环每 step 调用否则性能损失很大。如果在多卡训练还要检查torch.cuda.set_device是否把默认显卡指向了显存更大的卡。5.4 坑四训练损失不降准确率一直在 20% 左右现象loss 在 1.5 左右纹丝不动准确率基本等于随机猜其中某一类偶尔上升之后又掉下去。原因常见原因有三个。第一学习率太大模型在 loss 表面震荡第二图像没有做归一化像素值在 0 到 255而预训练模型期望 0 到 1 之间且按 RGB 均值做标准化第三预训练权重加载失败模型从零初始化的状态开始训练收敛速度自然很慢。解决在build_resnet34里显式打印第一层卷积权重均值with torch.no_grad(): print(model.conv1.weight.mean().item())如果输出是 0.02 左右的小数说明预训练权重成功加载如果是随机初始化常见的 0.002 附近检查pretrainedTrue是否被外层代码覆盖。另外把学习率直接设成1e-4并确认transforms.Normalize的 mean 和 std 是 ImageNet 默认值不要随手换成自己瞎猜的数字。5.5 坑五验证集准确率虚高提交测试一塌糊涂现象训练完在验证集上准确率 90% 以上杀进测试集或者重新跑一份新数据后直接掉到 50%。原因最常见的是没有做患者分层或者数据增强过度导致模型学到了图片背景噪声。另一个隐蔽原因是验证集是从原始 CSV 里随机抽的图片可能带有医院设备水印或暗角模型通过这些伪特征判断了类别。解决检查划分代码里是否真的用了GroupShuffleSplit。如果没有患者字段退而求其次也要保证同一个id_code的重复图片不出现在划分两端。同时在数据探索阶段去看几张对应类别的图片判断数据集本身是不是存在“某一类图片明显更亮”的系统性偏差。这种系统偏差在糖网公开数据集里很少见但遇到假按钮病时一定要警惕。6. 进阶技巧用Grad-CAM给模型结果做“医学解释”并验证可靠性6.1 Grad-CAM可视化眼底图像的关注区域模型预测的可靠性能否被医学解释几乎决定了一个毕设作品的上限。糖网病变特征集中在血管周围、出血斑点和渗出位置如果模型能关注到这些区域说明它学到的是真正的生理特征而不是背景噪声。Grad-CAM 是最常见的类激活热力图做法不需要重新训练模型只要一行代码包装模型即可from torchvision.models.feature_extraction import create_feature_extractor def grad_cam(model, image_tensor, target_class): model.eval() model.zero_grad() extractor create_feature_extractor(model, {layer4: features}) features, outputs None, None with torch.enable_grad(): out extractor(image_tensor) features out[features] logits model(image_tensor) score logits[0][target_class] score.backward() grads features.grad.mean(dim(2, 3), keepdimTrue) cam torch.relu((grads * features).sum(dim1, keepdimTrue)) cam torch.nn.functional.interpolate(cam, size(224, 224), modebilinear) return cam.squeeze().cpu().detach().numpy()这段代码要对单张图调用注意features的requires_grad需要在 extractor 里默认开启建议直接把模型放到train模式但不用 batch norm 更新统计量时干脆在with torch.enable_grad()里重建一次前向。实际调试中用create_feature_extractor很容易把layer4的输出 hook 到错误位置更稳妥的方法是把模型替换为带register_forward_hook的简易类。不管怎么实现最终热力图叠加到原始图像上要能看出高亮区域集中在微血管密集区域这可以作为毕设论文的“讨论”章节素材。6.2 最后的小习惯做完 Grad-CAM 之后记得检查模型在验证集上预测错误的样本把错分的图片和热力图拼在一起看。如果模型把重度病变预测成轻度而热力图焦点完全不在病变区域那就说明特征表征还不够好可以考虑解冻更多卷积层再训练如果热力图高亮区域恰好是出血点哪怕最终指标一般答辩时也可以讲“模型具有一定可解释性”。另外每次修改参数都另存一个 notebook不要在一个文件里反复覆盖——版本是毕设的后悔药。我习惯保存实验记录表模型名称、学习率、batch size、epoch、验证 kappa、准确率、备注最后对比表格一出来论文的“对比实验”章节就有依据。希望这篇笔记能帮到你按这个“Jupyter 数据组织 ResNet 迁移学习 项目文档导出”的路线糖网诊断完全可以在 30 个 epoch 内跑出一个可靠结果。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询