简介:这份毕业设计资源围绕糖尿病视网膜疾病诊断展开,基于Jupyter Notebook实现,面向计算机、人工智能、自动化等专业的学生与教师,可用于毕业设计、课程大作业或期末项目参考。资源包共30个文件,约30.44MB,以16个ipynb交互式笔记为核心,配合2个py脚本、1个csv数据文件、9张png结果图及md说明文档,覆盖数据预处理、EDA探索性分析、EfficientNet系列模型训练与交叉验证、stacking集成等完整流程。项目答辩评审分达98分,代码均经过调试测试,确保可运行。读者可从中获取视网膜病变数据的划分脚本、多折交叉验证方案、模型训练与集成思路,以及可视化图表和提交结果文件,既能直接复现实验,也可在此基础上调整网络结构或数据增强策略,实现不同诊断功能。目前已有58人学习,适合作为医学图像分类方向的入门与进阶参考。
1. 从一张眼底照到一份诊断报告:这套 Jupyter 糖尿病视网膜诊断方案到底解决什么
糖尿病视网膜病变(DR)筛查最现实的矛盾是:读片医生不够,而需要筛查的人太多。一个地市级医院内分泌科,一天可能积累上百张眼底照,真正能判读的眼科医生只有一两个。这套「基于 Jupyter 实现的糖尿病视网膜疾病诊断」方案,本质是把「眼底图 → 五级病变分级」这条链路,用 Python 在 Jupyter Notebook 里完整跑通:数据读取、图像预处理、模型训练、指标评估、单张推理可视化,全部在一个.ipynb里可复现。它适合三类人:做医学图像毕业设计的学生、想入门医疗 AI 的算法工程师、以及需要快速验证 DR 分级可行性的临床科研人员。你拿到的不只是「源码 + 数据集 + 模型 + 文档」四个词,而是一条从零到能出诊断结论的最小闭环。Jupyter 在这里不是噱头,而是让每一步中间结果——原图、增强后图像、热力图、混淆矩阵——都能当场看到,这对医学图像调试太重要了。
2. 数据先行:糖尿病视网膜数据集的获取、清洗与五级标签处理
2.1 为什么 DR 数据集必须先解决类别极不平衡
公开的 DR 数据集(如 Kaggle 糖尿病视网膜病变检测数据集这类常见来源)通常按 0~4 五级标注:0 无病变、1 轻度、2 中度、3 重度、4 增殖期。真实分布极度倾斜,0 级往往占七成以上,3、4 级可能不到 5%。如果直接丢进模型训练,准确率能刷到 80% 以上,但模型其实只会喊「无病变」,这种翻车在医学场景里是致命的——漏诊重度患者比误诊更严重。
常见做法是两步走:先做类别重采样或加权,再用分层划分保证验证集里每个等级都有样本。我一般会在 Notebook 开头就把标签分布画出来,肉眼确认倾斜程度,再决定用class_weight还是过采样。别一上来就上 SMOTE,医学图像过采样容易生成不真实的病灶纹理,反而污染训练集。
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split # 读取标签文件,常见格式:id_code, diagnosis df = pd.read_csv("train_labels.csv") # 1. 查看五级分布,确认倾斜程度 print(df["diagnosis"].value_counts().sort_index()) # 2. 分层划分,保证验证集每个等级都有样本 train_df, val_df = train_test_split( df, test_size=0.2, stratify=df["diagnosis"], # 关键:按标签分层 random_state=42 ) # 3. 计算类别权重,供后续模型使用 from sklearn.utils.class_weight import compute_class_weight classes = np.unique(train_df["diagnosis"]) weights = compute_class_weight("balanced", classes=classes, y=train_df["diagnosis"]) class_weight = dict(zip(classes, weights)) print(class_weight)逻辑说明:stratify参数是这里的关键,它保证训练集和验证集的五级比例一致,否则验证集可能一个 4 级样本都没有,评估结果毫无意义。compute_class_weight用balanced模式,权重与类别频率成反比,少数类权重自动放大。参数上,test_size=0.2是常规起点,数据量小于 3000 张时建议调到 0.15 保留更多训练样本;random_state固定住,保证每次跑结果可复现,这在毕业设计答辩时很重要。
2.2 图像预处理:圆形裁剪、去黑边与尺寸归一
眼底照有个典型特征:有效区域是一个圆,四角是黑色背景。如果直接 resize 到 224×224,黑边会占据大量像素,模型学到的是「黑边形状」而不是病灶。标准流程是:先找圆形视场的边界,裁掉黑边,再统一尺寸。
import cv2 import numpy as np def crop_circle_black(img, tol=7): """裁掉眼底图四周黑边,返回有效圆形区域""" gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 阈值化,找出非黑区域 mask = gray > tol # 按行列求和,定位有效边界 rows = np.where(mask.sum(axis=1) > 0)[0] cols = np.where(mask.sum(axis=0) > 0)[0] if len(rows) == 0 or len(cols) == 0: return img return img[rows[0]:rows[-1], cols[0]:cols[-1]] def preprocess(path, size=224): img = cv2.imread(path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = crop_circle_black(img) img = cv2.resize(img, (size, size)) # 归一化到 [0,1] img = img.astype(np.float32) / 255.0 return img逻辑说明:tol=7是灰度阈值,用来区分「接近纯黑」和「有效像素」,眼底照黑边通常在 0~5 灰度,设 7 比较稳。rows、cols通过行列求和定位有效区域边界,比直接找轮廓更鲁棒,不会因为病灶暗区误判。size=224是 ImageNet 预训练模型的标配输入,如果你用 EfficientNet-B3 可以调到 300。注意归一化放在 resize 之后,顺序反了会导致插值计算在 0~255 上做,数值不稳定。
2.3 标签的两种处理路线:五分类还是二分类
毕业设计里最常见的纠结:到底做五分类还是「有病/没病」二分类。我的建议是两条都做,但主指标放二分类。原因很实际——五分类的 3、4 级样本太少,模型很难学稳,答辩时 F1 掉到 0.5 以下很尴尬;而二分类把 1~4 合并为「有病变」,样本量立刻平衡很多,AUC 能稳定在 0.9 以上,临床上也更贴近「先筛出可疑,再转诊」的真实流程。
| 路线 | 标签处理 | 优点 | 风险 |
|---|---|---|---|
| 五分类 | 0/1/2/3/4 原样 | 分级细,学术价值高 | 少数类样本不足,指标波动大 |
| 二分类 | 0 为阴性,1~4 为阳性 | 样本均衡,指标稳 | 丢失严重程度信息 |
| 混合 | 主任务二分类 + 辅助五分类 | 兼顾筛查与分级 | 实现复杂度上升 |
实操上,我会在 Notebook 里用同一个数据加载器,通过一个task参数切换标签映射,这样两套实验共用代码,改一行就能跑。
3. 在 Jupyter 里搭模型:迁移学习、训练循环与 Notebook 工程化
3.1 为什么选迁移学习而不是从零训 CNN
DR 数据集规模通常几千到几万张,从零训练一个 ResNet 需要百万级数据,否则过拟合到验证集准确率虚高、测试集崩盘。迁移学习用 ImageNet 预训练权重做初始化,卷积层已经学会了边缘、纹理、颜色这些底层特征,眼底图虽然域不同,但底层特征可迁移。常见做法是冻结主干前几层,只训练后几层加分类头,等 loss 稳定后再解冻微调。
import tensorflow as tf from tensorflow.keras import layers, Model def build_model(num_classes=5, img_size=224): # 加载 ImageNet 预训练主干,不含顶层分类器 base = tf.keras.applications.EfficientNetB0( include_top=False, weights="imagenet", input_shape=(img_size, img_size, 3) ) # 第一阶段:冻结主干 base.trainable = False inputs = layers.Input(shape=(img_size, img_size, 3)) x = base(inputs, training=False) x = layers.GlobalAveragePooling2D()(x) x = layers.Dropout(0.3)(x) outputs = layers.Dense(num_classes, activation="softmax")(x) model = Model(inputs, outputs) model.compile( optimizer=tf.keras.optimizers.Adam(1e-3), loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) return model, base逻辑说明:include_top=False去掉 ImageNet 的 1000 类分类头,换成我们自己的num_classes。base.trainable=False冻结主干,第一阶段只训练分类头,学习率可以设大一点(1e-3)。Dropout(0.3)是医学图像常用的正则,防止小数据集过拟合。sparse_categorical_crossentropy对应整数标签,不用做 one-hot,省内存。参数上,EfficientNetB0参数量小、推理快,适合毕业设计的算力条件;如果你有 GPU 且数据量大,可以换 B3,输入尺寸同步调到 300。
3.2 两阶段训练:先冻结后微调的具体参数
第一阶段训练到验证 loss 不再下降,通常 10~15 个 epoch。然后解冻主干的后 20~30 层做微调,学习率必须降下来,否则预训练权重会被大梯度冲垮。
# 第一阶段:冻结训练 model, base = build_model(num_classes=5) history1 = model.fit( train_ds, validation_data=val_ds, epochs=15, class_weight=class_weight # 用 2.1 算出的权重 ) # 第二阶段:解冻后 30 层微调 base.trainable = True for layer in base.layers[:-30]: layer.trainable = False model.compile( optimizer=tf.keras.optimizers.Adam(1e-5), # 学习率降 100 倍 loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) history2 = model.fit( train_ds, validation_data=val_ds, epochs=10, class_weight=class_weight )逻辑说明:微调阶段学习率从 1e-3 降到 1e-5,这是血泪经验——用 1e-3 微调,预训练特征几个 batch 就被破坏,验证准确率会先涨后暴跌。base.layers[:-30]只解冻最后 30 层,浅层特征通用性强,没必要动。class_weight两阶段都要传,否则少数类在微调时又被淹没。如果显存不够,把 batch size 降到 16,同时学习率按比例微调。
3.3 把 Notebook 当工程用:随机种子、检查点与可复现
Jupyter 最大的坑是「这次跑对了,下次跑不对」。三个动作必须做:固定所有随机种子、保存最佳模型检查点、把关键超参写进一个配置单元格。
import random, os import numpy as np import tensorflow as tf def set_seed(seed=42): random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) os.environ["PYTHONHASHSEED"] = str(seed) set_seed(42) # 保存验证集上最优模型,而不是最后一个 epoch checkpoint = tf.keras.callbacks.ModelCheckpoint( "best_dr_model.h5", monitor="val_accuracy", save_best_only=True, mode="max", verbose=1 ) early_stop = tf.keras.callbacks.EarlyStopping( monitor="val_loss", patience=5, restore_best_weights=True )逻辑说明:set_seed覆盖 Python、NumPy、TensorFlow 三层随机源,缺一个都可能导致结果漂移。ModelCheckpoint的save_best_only=True保证存下来的是验证集最优,不是过拟合的最后一轮。EarlyStopping的patience=5表示验证 loss 连续 5 轮不降就停,避免无效训练。注意.h5格式在老版本 Keras 通用,新版本建议用.keras,但毕业设计环境往往版本偏旧,.h5兼容性更好。
4. 评估与可视化:别只看准确率,混淆矩阵和热力图才是诊断依据
4.1 医学图像为什么准确率会骗人
前面提过,0 级占七成,模型全预测 0 级就有 70% 准确率。所以评估必须看混淆矩阵、每类召回率、AUC。尤其是 3、4 级的召回率——漏诊一个增殖期患者,临床后果远比误诊严重。我一般会在 Notebook 里同时打印 classification_report 和混淆矩阵热力图,一眼看出模型在哪个等级上「瞎」。
from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt # 预测验证集 y_pred_prob = model.predict(val_ds) y_pred = np.argmax(y_pred_prob, axis=1) y_true = np.concatenate([y for _, y in val_ds], axis=0) print(classification_report(y_true, y_pred, digits=4)) cm = confusion_matrix(y_true, y_pred) plt.figure(figsize=(8, 6)) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues") plt.xlabel("Predicted") plt.ylabel("True") plt.title("DR Grading Confusion Matrix") plt.show()逻辑说明:classification_report给出每类的 precision、recall、f1,重点看 3、4 级的 recall。confusion_matrix配合 heatmap,能看出模型是否把 3 级误判成 2 级(相邻等级混淆最常见)。如果 4 级 recall 低于 0.6,说明少数类学习不足,回去调 class_weight 或做过采样。digits=4保留四位小数,方便对比不同实验的细微差异。
4.2 Grad-CAM 热力图:让模型告诉你它在看哪里
医学 AI 最被诟病的是黑匣子。Grad-CAM 能把模型决策区域叠加回原图,如果热力图集中在视盘、黄斑或出血点,说明模型学到了病灶;如果集中在黑边或图像角落,说明模型在作弊。这个可视化在毕业设计答辩时是加分项,也是自查利器。
import tensorflow as tf import numpy as np import cv2 def grad_cam(model, img_array, last_conv_layer_name): grad_model = tf.keras.Model( inputs=model.inputs, outputs=[model.get_layer(last_conv_layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_outputs, predictions = grad_model(img_array) pred_index = tf.argmax(predictions[0]) class_channel = predictions[:, pred_index] grads = tape.gradient(class_channel, conv_outputs) pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)) conv_outputs = conv_outputs[0] heatmap = conv_outputs @ pooled_grads[..., tf.newaxis] heatmap = tf.squeeze(heatmap) heatmap = tf.maximum(heatmap, 0) / (tf.math.reduce_max(heatmap) + 1e-8) return heatmap.numpy() # 叠加到原图 heatmap = grad_cam(model, img_array, "top_conv") heatmap = cv2.resize(heatmap, (224, 224)) heatmap = np.uint8(255 * heatmap) heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) superimposed = cv2.addWeighted(orig_img, 0.6, heatmap, 0.4, 0)逻辑说明:last_conv_layer_name要填模型最后一个卷积层名,EfficientNetB0 通常是top_conv,用model.summary()确认。pooled_grads是每个通道的梯度均值,代表该通道对目标类的重要性。tf.maximum(heatmap, 0)只保留正贡献区域,负值置零。addWeighted的 0.6/0.4 是原图与热力图的混合比例,热力图太浓会盖住病灶细节,0.4 比较合适。如果热力图全图泛红没有焦点,说明模型没学到局部特征,检查预处理是否把病灶裁掉了。
5. 避坑与排查:这套 DR 诊断方案里最容易翻车的 5 个地方
5.1 验证集准确率 0.95,测试集掉到 0.6
现象:训练时验证准确率一路涨到 0.95,换一批数据测试只有 0.6 左右。原因:数据泄漏或分布不一致。常见情况是同一患者的左右眼分别进了训练集和验证集,模型记住了患者特征而非病灶;或者验证集和训练集来自不同设备,图像色调差异大。解决:按患者 ID 分组划分,用GroupShuffleSplit替代train_test_split;同时检查两个集合的图像来源是否一致,必要时做颜色归一化。
5.2 模型把所有样本预测成 0 级
现象:混淆矩阵第一列全满,其他列全空。原因:类别极不平衡 + 没用 class_weight,模型发现全预测多数类就能让 loss 最低。解决:确认class_weight真的传进了fit,很多人算了权重却忘了传;如果传了还这样,把少数类过采样到和多数类同量级,或者改用 focal loss 压制易分类样本。
5.3 Jupyter 里训练到一半 kernel 崩了
现象:训练到第 8 个 epoch,kernel died,前面全白跑。原因:显存溢出或内存泄漏,常见于把整个数据集一次性读进内存。解决:用tf.data.Dataset做流式加载,配合.cache()和.prefetch();训练前用nvidia-smi看显存余量,batch size 别贪大;把ModelCheckpoint设好,崩了也能从最优权重恢复,不用从头再来。
5.4 图像预处理后病灶消失
现象:Grad-CAM 热力图集中在图像边缘,模型准确率虚高但临床无意义。原因:圆形裁剪的阈值tol设太大,把暗区病灶一起裁掉了;或者 resize 时用了最近邻插值,病灶纹理被破坏。解决:tol从 7 往下调,肉眼对比裁剪前后图像;resize 统一用cv2.INTER_AREA(缩小)或INTER_LINEAR(放大),别用INTER_NEAREST。
5.5 换了台机器,Notebook 跑不起来
现象:在自己电脑上跑通的.ipynb,换到实验室服务器报各种 import error 或版本冲突。原因:依赖没锁定,TensorFlow、OpenCV、NumPy 版本不一致。解决:在 Notebook 第一个单元格打印所有关键库版本,导出requirements.txt时带上版本号;用虚拟环境隔离,别在 base 环境里装。毕业设计提交前,务必在一台干净机器上完整跑一遍。
6. 从能跑到能用:单张推理、批量筛查与模型导出
把模型训好只是起点,真正交付要能对一张新眼底照出诊断结论。我一般会在 Notebook 最后加一个推理单元格,输入图片路径,输出预测等级、置信度和热力图,三样一起给,临床医生才敢参考。
def diagnose(image_path, model, class_names=None): class_names = class_names or ["No DR", "Mild", "Moderate", "Severe", "Proliferative"] img = preprocess(image_path, size=224) img_array = np.expand_dims(img, axis=0) probs = model.predict(img_array, verbose=0)[0] pred = np.argmax(probs) print(f"诊断结果: {class_names[pred]}") print(f"置信度: {probs[pred]:.4f}") for i, p in enumerate(probs): print(f" {class_names[i]}: {p:.4f}") return pred, probs diagnose("test_images/sample_01.png", model)逻辑说明:preprocess必须和训练时完全一致,包括裁剪、resize、归一化,任何一步不同都会导致推理结果偏移。verbose=0关掉进度条,批量调用时输出干净。置信度低于 0.6 的样本建议标记为「需人工复核」,这是临床落地的安全阀。批量筛查时把diagnose套一个for循环遍历文件夹,结果写进 CSV,按置信度排序,低置信度的排前面优先复核。
模型导出有两个方向:一是存成 SavedModel 或.keras供后续加载;二是转成 TFLite 或 ONNX,方便部署到边缘设备或集成进其他系统。毕业设计里,导出 SavedModel 加一份推理脚本就够了,别过度工程化。
最后说个我自己的习惯:每次改完预处理或模型结构,先拿 5 张图跑一遍推理,肉眼确认热力图落在病灶上,再启动完整训练。这个动作花不了两分钟,但能省下几小时的无效训练。医学图像这行,肉眼验证永远比指标更早发现问题。希望帮到你。
本文还有配套的精品资源,点击获取