简介:这份资源面向计算机、人工智能相关专业的毕业设计、课程设计及项目开发学习者,提供一套基于Jupyter实现的糖尿病视网膜病变(DR)诊断完整方案。内容围绕眼底图像分类任务展开,涵盖需求分析、数据来源说明、EDA探索、数据预处理与增强、建模、模型训练、调参、模型验证、错误分析及方案总结等环节,并针对类别间距不等、训练集与公开测试集分布差异大导致的过拟合、预处理选择、外部数据源利用等实际问题给出解决思路。资源包共30个文件,包含16个ipynb笔记本、9张png图表、2个py脚本、1个csv数据文件及md说明文档,压缩包约30.44MB,目录结构清晰,便于按模块查阅与复现。目前已有180人学习下载。读者可据此获得从数据到模型的完整实验流程、可运行的源码与数据集,并在此基础上延伸改造,适合作为深度学习图像分类项目的参考模板。
1. 糖尿病视网膜病变诊断项目:从 Jupyter 到可复现的模型训练
糖尿病视网膜病变(Diabetic Retinopathy,DR)是导致成年人失明的主要原因之一,而眼底图像分级诊断恰好是深度学习落地最成熟的医学场景之一。这个项目标题里塞了五个关键词:Jupyter、诊断、源码、数据集、模型训练,本质上是一套「在 Jupyter Notebook 里跑通 DR 五级分类」的完整工程链路。它解决的不是算法创新问题,而是让一个毕业设计或课程设计能在本地从零复现:数据怎么读、模型怎么搭、训练怎么调、结果怎么验证。适合手里有眼底数据集、想用 Python 快速出结果的学生和初级算法工程师,也适合需要一套可交付代码框架的开发者。我做过几轮类似的医学图像分类,踩过的坑比想象中多,下面把整条路径拆开讲。
2. 眼底图像分级任务:先搞清楚标签体系和数据格式
2.1 DR 五级分类的标签定义与临床边界
糖尿病视网膜病变按国际临床分级标准通常分五级:0 级无病变、1 级轻度非增殖期、2 级中度非增殖期、3 级重度非增殖期、4 级增殖期。这个标签体系直接决定模型输出维度是 5,损失函数用交叉熵。但实际数据集里经常出现标签不均衡——0 级样本可能占 70% 以上,4 级不到 5%。如果直接训练,模型会倾向于全预测 0 级,准确率看着高,但完全没用。
常见做法是对训练集做重采样或加类别权重。我一般先用sklearn算一下各类分布,再决定用WeightedRandomSampler还是class_weight。注意验证集和测试集不能动,必须保持原始分布,否则评估结果会虚高。
2.2 眼底图像预处理:裁剪、去黑边、归一化
原始眼底图像通常带大量黑色背景,直接缩放到 224×224 会把病灶区域压得很小。标准流程是:先检测圆形视场区域,裁掉黑边,再缩放到模型输入尺寸。常用方法是转灰度后做阈值分割,找最大连通域的外接矩形。
import cv2 import numpy as np def crop_black_border(img, tol=7): """裁剪眼底图像黑边,tol 控制灰度阈值""" gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) mask = gray > tol # 找非零区域边界 rows = np.any(mask, axis=1) cols = np.any(mask, axis=0) rmin, rmax = np.where(rows)[0][[0, -1]] cmin, cmax = np.where(cols)[0][[0, -1]] return img[rmin:rmax, cmin:cmax] def preprocess(img, size=224): img = crop_black_border(img) img = cv2.resize(img, (size, size)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 归一化到 [0,1] img = img.astype(np.float32) / 255.0 return imgtol=7是经验值,对多数公开眼底数据集够用;如果图像偏暗可以调到 10。裁剪后统一 resize 到 224×224,这是 ResNet 系列的标准输入。归一化用简单除以 255,也可以换成 ImageNet 均值方差标准化,但医学图像上差别不大。注意不要用直方图均衡化过度增强,容易把噪声当病灶。
2.3 数据集划分与 Jupyter 里的路径管理
Jupyter Notebook 默认保存路径经常让人找不到文件,尤其是用jupyter notebook启动后下载的数据集不知道放哪了。我一般会在项目根目录建data/文件夹,用绝对路径或os.path动态拼接,避免相对路径在 Notebook 里翻车。
import os import pandas as pd from sklearn.model_selection import train_test_split DATA_DIR = os.path.abspath("./data") IMG_DIR = os.path.join(DATA_DIR, "images") LABEL_CSV = os.path.join(DATA_DIR, "labels.csv") df = pd.read_csv(LABEL_CSV) # 假设列名为 image_name, level train_df, val_df = train_test_split( df, test_size=0.2, stratify=df["level"], random_state=42 ) print(train_df["level"].value_counts())stratify=df["level"]保证训练集和验证集类别比例一致,random_state=42固定划分方便复现。如果数据集本身有官方划分,优先用官方的,别自己切。Jupyter 里建议每个 Notebook 开头都重新定义路径变量,不要依赖上一个单元格的变量,否则重启内核后容易报NameError。
3. 在 Jupyter 里搭 ResNet 分类模型:迁移学习与训练循环
3.1 为什么选 ResNet50 而不是从零训练
医学图像数据集通常几千到几万张,从零训练 CNN 基本过拟合。迁移学习是标准做法:用 ImageNet 预训练的 ResNet50,替换最后一层全连接为 5 输出,先冻结主干训练分类头,再解冻部分层微调。ResNet50 在眼底图像上的表现稳定,参数量适中,单卡 8G 显存就能跑 batch size 16。
import torch import torch.nn as nn from torchvision import models def build_model(num_classes=5, pretrained=True): model = models.resnet50(pretrained=pretrained) # 冻结主干 for param in model.parameters(): param.requires_grad = False # 替换分类头 in_features = model.fc.in_features model.fc = nn.Sequential( nn.Linear(in_features, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) return modelpretrained=True会下载 ImageNet 预训练权重,第一次运行需要联网。Dropout(0.5)是医学图像上常用的正则化强度,再高容易欠拟合。冻结主干后只训练model.fc,学习率可以设 1e-3;解冻后微调时学习率降到 1e-4 或更低。
3.2 训练循环与关键参数设置
训练循环里最容易被忽视的是学习率调度和早停。我一般用CosineAnnealingLR配合EarlyStopping,验证集 loss 连续 5 个 epoch 不降就停。
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_model().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) scheduler = CosineAnnealingLR(optimizer, T_max=20) 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() # 验证逻辑省略T_max=20表示余弦周期 20 个 epoch,配合总 epoch 30 够用。Adam比SGD收敛快,适合快速出结果。如果显存不够,把 batch size 降到 8,同时学习率减半。注意model.fc.parameters()只优化分类头,解冻后要改成model.parameters()。
3.3 类别不均衡的处理:加权损失与采样器
前面提到 DR 数据集类别不均衡,除了重采样,还可以在损失函数里加权重。CrossEntropyLoss的weight参数接收一个 tensor,每个类别的权重与样本数成反比。
from collections import Counter labels = train_df["level"].tolist() counts = Counter(labels) total = sum(counts.values()) weights = [total / (5 * counts[i]) for i in range(5)] class_weights = torch.tensor(weights, dtype=torch.float32).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)这样少数类样本的 loss 会被放大,模型更关注它们。权重不要设得太极端,否则训练不稳定。我一般会把最大权重限制在 10 以内,超过就配合采样器一起用。
4. 模型评估与结果验证:别只看准确率
4.1 混淆矩阵与每类召回率
医学诊断里漏诊比误诊更严重,所以每类召回率(Recall)比整体准确率重要。用sklearn的confusion_matrix和classification_report直接出表。
from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) outputs = model(imgs) preds = outputs.argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, digits=4))重点看 3 级和 4 级的召回率,如果低于 0.6,说明模型对重度病变识别不足,需要调整类别权重或增加数据增强。0 级和 1 级之间容易混淆,这是数据本身边界模糊导致的,不用过度优化。
4.2 可视化验证:Grad-CAM 看模型关注区域
Grad-CAM 能生成热力图,看模型到底在关注眼底的哪个区域。如果热力图集中在视盘或血管弓以外的地方,说明模型可能学到了伪影。
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image target_layers = [model.layer4[-1]] cam = GradCAM(model=model, target_layers=target_layers) grayscale_cam = cam(input_tensor=img_tensor, target_category=pred_class) visualization = show_cam_on_image(img_np, grayscale_cam[0], use_rgb=True)target_layers选最后一层卷积,分辨率够用。target_category指定要看哪一类的激活。如果热力图很散,说明模型没学到病灶特征,需要检查数据标注质量或增加数据量。
4.3 交叉验证与模型保存
单次划分的验证结果波动大,条件允许时做 5 折交叉验证。每折保存最佳模型权重,最后取平均或投票。
torch.save(model.state_dict(), "dr_resnet50_fold0.pth") # 加载 model.load_state_dict(torch.load("dr_resnet50_fold0.pth"))保存时只存state_dict,不存整个模型,方便跨版本加载。Jupyter 里记得把模型文件存到项目目录,不要留在默认临时路径。
5. 避坑与排查:Jupyter 训练 DR 模型常见的 5 个翻车点
5.1 现象:训练 loss 不降,准确率卡在 0.7 左右
原因:学习率太大或类别权重设得太极端,导致梯度震荡。解决:先把学习率降到 1e-4,去掉类别权重跑一轮,确认模型能正常拟合小批量数据,再逐步加回权重。
5.2 现象:验证集准确率远高于训练集
原因:验证集太小或划分时没做分层,导致分布偏差。解决:用stratify重新划分,确保验证集至少每类 50 张。如果数据量实在少,改用交叉验证。
5.3 现象:Jupyter 内核崩溃,报 CUDA out of memory
原因:batch size 太大或没有及时释放中间变量。解决:把 batch size 降到 8,在训练循环里加torch.cuda.empty_cache(),或者用del删掉不用的 tensor。另外检查是不是在 Notebook 里同时加载了多个模型。
5.4 现象:图像预处理后病灶消失
原因:裁剪黑边时阈值设太高,把暗区病灶一起裁掉了。解决:把tol降到 3 或 5,或者改用圆形霍夫变换检测视场边界,只裁外围纯黑区域。
5.5 现象:模型保存后重新加载预测结果不一致
原因:保存时没固定随机种子,或者加载时模型结构有改动。解决:在 Notebook 开头设torch.manual_seed(42)、np.random.seed(42),加载时先实例化相同结构再load_state_dict。
6. 进阶技巧:用混合精度和 TTA 把验证指标再提一档
训练到后期,如果指标卡住了,可以试两个技巧:混合精度训练(AMP)和测试时增强(TTA)。AMP 能省显存、加速训练,TTA 能小幅提升召回率。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() with autocast(): outputs = model(imgs) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()autocast自动把部分运算转成 float16,GradScaler防止梯度下溢。注意 AMP 和CosineAnnealingLR兼容,但和某些自定义损失函数可能冲突,报错就关掉。
TTA 的做法是对验证集每张图做水平翻转、垂直翻转、旋转 90 度,分别预测后取平均。
def tta_predict(model, img): preds = [] for aug in [lambda x: x, lambda x: torch.flip(x, [3]), lambda x: torch.flip(x, [2]), lambda x: torch.rot90(x, 1, [2, 3])]: with torch.no_grad(): out = model(aug(img.unsqueeze(0)).to(device)) preds.append(torch.softmax(out, dim=1)) return torch.mean(torch.stack(preds), dim=0)TTA 一般能提 1 到 2 个点召回率,但推理时间翻 4 倍。如果只是交毕业设计,单模型加 AMP 就够了;如果追求指标,TTA 值得加。
我自己的习惯是:每次跑完实验,先把classification_report和混淆矩阵截图存下来,再决定下一步调什么。不要凭感觉改参数,数据说话。另外 Jupyter Notebook 记得定期重启内核,不然变量污染会让你怀疑人生。希望帮到你。
本文还有配套的精品资源,点击获取