简介:本资源面向计算机与人工智能方向的毕业设计、课程设计及项目开发学习者,提供一套基于Jupyter实现的糖尿病视网膜病变(DR)诊断完整方案。项目围绕眼底图像分类任务,涵盖需求分析、数据来源说明、EDA探索、数据预处理与增强、建模、模型训练、调参、模型验证、错误分析及方案总结等环节,并针对类别间距不等、训练集与公开测试集分布差异大导致的过拟合、预处理选择、外部数据源利用等实际问题给出解决思路。压缩包共30个文件,约30.44MB,包含16个ipynb笔记本、9张png图表、2个py脚本、1个csv数据文件及md说明文档,覆盖EfficientNet系列模型训练、交叉验证、数据划分与堆叠集成等模块。已有180人学习下载。读者可获得经过严格测试的源码、数据集与项目文档,直接参考并在此基础上延伸开发,适合作为医学图像分类方向的实战参考。
1. 从一份眼底照片到 DR 分级:这套 Jupyter 项目到底能跑出什么
糖尿病视网膜病变(DR)的分级诊断,是医学图像分类里少见的「类别边界模糊 + 数据分布偏移」双重难题。这套基于 Jupyter 的糖尿病视网膜疾病诊断项目,把从 EDA、预处理、数据增强到 EfficientNet 建模、交叉验证、stacking 集成的完整链路都拆成了可单独运行的 notebook,配套源码、数据集划分脚本和项目文档。它适合正在做毕业设计、课程设计,或者想找一个真实医学影像多分类场景练手的同学——不是玩具级的 MNIST 替换,而是带类别不平衡、带 public/private 分布差异、带外部数据融合讨论的完整工程。你拿到手能直接跑通训练,也能顺着 notebook 的注释理解每一步为什么这么做。
2. 数据管线拆解:从 cv_data_split.py 到增强后的 Tensor
2.1 先看清目录结构和数据流向
拿到DiabeticRetinopathy-master之后,别急着打开 notebook 就 run all。先花五分钟把目录理一遍,这套项目的文件组织其实已经把数据流暗示得很清楚了:
DiabeticRetinopathy-master/ ├── src/ │ ├── cv_data_split.py # 交叉验证划分脚本 │ └── utils.py # 通用工具函数 ├── notebooks/ │ ├── EDA.ipynb # 探索性数据分析 │ ├── data_preprocessing.ipynb # 预处理主流程 │ ├── efficientnet-b4_augament-s10.ipynb │ ├── efficientnet-b5_augament-s10-cv.ipynb │ ├── efficientnet_augament-s10-cv.ipynb │ ├── stacking-logits.ipynb # 集成学习 │ └── ... ├── images/ # 训练曲线、混淆矩阵等输出图 ├── submission.csv # 提交格式示例 └── README.mdcv_data_split.py是整个管线的起点。它负责把原始标注按分层抽样切成 K 折,保证每一折里 0 到 4 级的比例和整体一致。这一步在 DR 场景里特别关键,因为原始数据里 No DR(0 级)可能占 70% 以上,如果随机切分,某一折可能几乎没有 3、4 级样本,训练出来的模型在验证集上看着还行,一到真实分布就崩。
常见做法是用StratifiedKFold,但要注意:分层字段不能直接用原始 label,得先把 0-4 的整数映射成字符串或做 one-hot,否则 sklearn 在某些版本下会报「类别数不匹配」。我一般会先跑一遍value_counts()确认每级样本量,再决定 K 取多少——样本量最小的那一级如果少于 K 的 3 倍,K 就得往下调。
2.2 预处理 notebook 里的三个关键决策
打开data_preprocessing.ipynb,核心就三件事:裁剪黑边、调整尺寸、归一化。但每一步都有坑。
裁剪黑边:眼底照片四周通常有黑色圆形边界,直接 resize 会把有效区域压得很小。项目里用的是基于像素阈值的圆形裁剪,大致逻辑是找到图像中非零区域的 bounding box,然后裁掉外围。这一步不做,模型会学到大量无意义的黑色像素。
尺寸选择:EfficientNet-B4 的标准输入是 380×380,B5 是 456×456。项目里 B4 和 B5 的 notebook 分别对应不同尺寸。如果你显存不够,可以降到 224×224 跑通流程,但最终精度会掉——视网膜病变的微动脉瘤在低分辨率下基本看不见。
归一化:这里有个容易翻车的地方。EfficientNet 预训练权重用的是 ImageNet 的 mean/std,但眼底图像的亮度分布和自然图像差异很大。项目里保留了 ImageNet 归一化,同时加了 CLAHE(限制对比度自适应直方图均衡)来增强局部对比。CLAHE 的clipLimit和tileGridSize两个参数需要根据你的数据集亮度调,默认 2.0 和 (8,8) 在大多数公开 DR 数据集上够用,但如果你的图像偏暗,clipLimit 可以提到 3.0。
import cv2 import numpy as np def crop_circle(img, tol=7): """裁掉眼底图四周黑边,tol 控制阈值容忍度""" gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) mask = gray > tol if mask.sum() == 0: return img 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+1, cmin:cmax+1] def apply_clahe(img, clip=2.0, grid=(8,8)): """在 LAB 空间的 L 通道做 CLAHE,避免颜色偏移""" lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB) clahe = cv2.createCLAHE(clipLimit=clip, tileGridSize=grid) lab[:,:,0] = clahe.apply(lab[:,:,0]) return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)crop_circle的tol参数控制「多暗算黑」,默认 7 对大多数眼底图够用,但如果图像本身偏暗,可以降到 5。apply_clahe在 LAB 空间操作而不是 RGB,是为了只增强亮度对比、不改变色相——视网膜出血点和渗出物的颜色是重要特征,在 RGB 上做均衡容易把红色和橙色混在一起。
2.3 数据增强策略与 s10 的含义
notebook 文件名里的augament-s10指的是增强方案的第 10 版(seed 10 或 strategy 10,看 README 里的说明)。项目里用的增强组合包括:随机旋转 ±180°、水平/垂直翻转、随机亮度对比度调整、以及轻微的弹性形变。
为什么旋转要用 ±180° 而不是常见的 ±15°?因为眼底照片的拍摄角度本身就不固定,视网膜的解剖结构在旋转后仍然合理。但弹性形变要小心——alpha和sigma设大了会把微动脉瘤拉成一条线,模型反而学不到东西。我一般把alpha控制在 1.0 以内,sigma在 8 到 12 之间。
增强只在训练时做,验证和测试集一律不做。这一点在 notebook 里是通过ImageDataGenerator的validation_split或者单独的val_datagen实现的,别图省事把增强套到验证集上,否则验证 loss 会虚低,你根本不知道模型真实水平。
3. EfficientNet 建模与交叉验证:B4 和 B5 怎么选
3.1 为什么是 EfficientNet 而不是 ResNet
项目选了 EfficientNet-B4 和 B5 作为 backbone,而不是更常见的 ResNet50。原因在 DR 场景里很实际:EfficientNet 的复合缩放(compound scaling)在同等参数量下对细粒度特征的提取能力更强,而 DR 分级恰恰依赖微小的病灶差异。B4 的 380×380 输入比 ResNet 默认的 224×224 保留了更多细节,B5 的 456×456 更进一步,但显存占用也上去了。
选型建议:如果你只有单张 8GB 显存的卡,B4 + batch size 8 是能跑起来的极限;B5 基本要 12GB 以上,或者用梯度累积把等效 batch size 撑上去。项目里 B4 和 B5 的 notebook 是分开的,可以先跑 B4 验证流程,再切 B5 看精度提升值不值得那个显存。
3.2 交叉验证 notebook 的执行逻辑
efficientnet-b5_augament-s10-cv.ipynb里的cv就是交叉验证。整个流程是:对每一折,用cv_data_split.py生成的划分索引加载训练/验证数据,训练一个 EfficientNet,保存验证集上的 logits 和标签。跑完 K 折之后,你会得到 K 组 out-of-fold 预测,这些预测在stacking-logits.ipynb里作为第二层模型的输入。
from sklearn.model_selection import StratifiedKFold import pandas as pd df = pd.read_csv('train_labels.csv') skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for fold, (tr_idx, val_idx) in enumerate(skf.split(df, df['level'])): train_df = df.iloc[tr_idx].reset_index(drop=True) val_df = df.iloc[val_idx].reset_index(drop=True) # 后续用 train_df / val_df 构建 generator print(f"Fold {fold}: train {len(train_df)}, val {len(val_df)}") print(val_df['level'].value_counts(normalize=True))n_splits=5是精度和训练成本的折中。如果你样本量少(比如每级只有几十张),可以提到 10 折,但训练时间线性增加。random_state=42固定住,保证每次划分一致,否则你调参调半天发现是数据划分变了,血泪经验。
每个 fold 训练完后,notebook 会把验证集的 logits 存成 npy 或 csv。注意:存的是 logits 不是 softmax 概率,因为 stacking 的时候用 logits 做特征更稳定,softmax 会压缩动态范围。
3.3 训练时的关键超参和回调
项目里用的优化器是 AdamW,学习率带 cosine 衰减,初始 lr 在 1e-3 到 1e-4 之间(B4 用 1e-3,B5 因为模型更大用 5e-4)。损失函数是 categorical crossentropy,但加了类别权重——权重按每级样本量的倒数算,再归一化。这一步不做的话,模型会把所有样本都预测成 0 级,准确率看着有 70%,但 QWK(二次加权 kappa)接近 0。
回调方面,ModelCheckpoint保存验证集 QWK 最高的权重,ReduceLROnPlateau在 QWK 连续 3 个 epoch 不升时把 lr 砍半,EarlyStopping的 patience 设 7 到 10。这几个回调在 notebook 里都有,但 patience 值可能需要根据你的数据集大小调——小数据集上 patience 设太大容易过拟合。
提示:QWK 是 DR 分级任务的标准评估指标,不是准确率。因为 0 级和 1 级的混淆比 0 级和 4 级的混淆「轻」得多,QWK 能反映这种距离关系。notebook 里应该有
cohen_kappa_score(y_true, y_pred, weights='quadratic')的调用,如果没有,自己补上。
4. 避坑与排查:那些跑不通、跑歪了的常见问题
4.1 现象:notebook 跑起来就报 CUDA out of memory
原因:B5 的 456×456 输入加上 batch size 默认值,在 8GB 卡上必炸。另外 Jupyter 不会自动释放上一个 notebook 占的显存,你跑完一个再跑另一个,显存是叠加的。
解决:先把 batch size 降到 4 或 2,用tf.keras.mixed_precision开混合精度。如果还不行,在 notebook 开头加tf.keras.backend.clear_session(),或者干脆重启 kernel。跑 B5 之前先把 B4 的 kernel shutdown 掉。
4.2 现象:验证集 QWK 一直在 0 附近晃
原因:最常见的是标签没做映射。原始数据里 level 可能是 0/1/2/3/4,但 generator 输出的类别索引和标签对不上,或者类别权重算反了。另一个可能是预处理时把图像归一化到了 [0,1] 但模型期望的是 [-1,1] 或 ImageNet 标准化。
解决:在data_preprocessing.ipynb里加一行打印,确认train_generator.class_indices和你的标签映射一致。然后检查归一化:EfficientNet 的preprocess_input做的是x/127.5 - 1,如果你手动做了/255,就会双重归一化,模型输入分布完全错位。
4.3 现象:训练 loss 下降但验证 loss 上升,差距越来越大
原因:过拟合。DR 数据集通常不大(几千张量级),EfficientNet 参数量又大,不加正则必过拟合。项目里虽然加了 dropout 和 weight decay,但如果你把dropout_rate改小了或者weight_decay设成了 0,就会这样。
解决:先把dropout_rate恢复到 0.3 到 0.5,weight_decay设 1e-5 到 1e-4。然后检查增强强度——如果增强太弱(比如只翻转),模型见到的样本多样性不够。可以加 CutMix 或 MixUp,但注意这两种增强在医学图像上要慎用,把两张眼底图混在一起可能产生解剖上不合理的伪影。
4.4 现象:stacking-logits.ipynb 跑出来的结果比单模型还差
原因:stacking 的第二层模型(通常是逻辑回归或小 MLP)过拟合了 out-of-fold 预测。K 折的 logits 虽然是无泄漏的,但如果 K 太小(比如 3),第二层模型见到的样本太少,容易过拟合。
解决:把 K 提到 5 或 10,第二层模型加 L2 正则,或者干脆用简单的加权平均代替 stacking。项目里 stacking 用的是逻辑回归,C值默认 1.0,可以降到 0.1 试试。另外确认一下:stacking 的输入特征维度是n_classes * n_folds还是n_classes,如果是前者,特征维度太高,必须加正则。
4.5 现象:submission.csv 的格式和平台要求对不上
原因:项目里的submission.csv是示例格式,列名可能是id,level或image,level,但实际提交平台可能要求id,level且 id 不带扩展名。
解决:打开submission.csv看前几行,确认列名和 id 格式。如果平台要求概率而不是类别,把 logits 过 softmax 后输出。注意:有些平台要求每级一列的概率,有些只要最终类别,看 README 或平台说明。
5. 从单模型到集成:把 QWK 再往上推一点的具体技巧
跑通单折 B4 之后,你大概能拿到 0.75 到 0.82 的 QWK(取决于数据集和增强强度)。想再往上走,项目里的stacking-logits.ipynb给了方向,但有几个细节值得单独拎出来说。
第一,TTA(测试时增强)的收益比你想的大。在推理阶段对每张测试图做 4 到 8 种变换(翻转、旋转 90° 的倍数),把 logits 平均。这一步不需要重新训练,直接在预测脚本里加循环就行。在 DR 任务上,TTA 通常能带来 0.01 到 0.02 的 QWK 提升。注意:TTA 的变换要和训练增强一致,训练时用了旋转,TTA 就加旋转;训练时没用弹性形变,TTA 也别加。
第二,B4 和 B5 的 logits 融合要加权。别直接平均。B5 通常比 B4 强,但强多少取决于你的数据量。我一般用验证集 QWK 做权重:w_b4 = qwk_b4 / (qwk_b4 + qwk_b5),然后final_logits = w_b4 * logits_b4 + w_b5 * logits_b5。如果两个模型差距很大(比如 B5 比 B4 高 0.05 以上),直接给 B5 更高权重甚至只用 B5。
第三,外部数据的用法。项目需求分析里提到了「利用外部数据源提高 performance」。常见做法是:先在外部大数据集(比如 ImageNet 或其它眼底数据集)上预训练,再在你的目标数据集上微调。但要注意,外部数据和目标数据的标签体系可能不一致——比如外部数据只有「有病/没病」二分类,而你的任务是 0-4 五分类。这时候只能拿外部数据做 backbone 的预训练,不能直接拿标签来训分类头。
第四,错误分析的落点。项目文档里列了「错误分析」环节,具体做法是:把验证集里预测错的样本按真实类别和预测类别分组,看混淆矩阵。DR 任务里最常见的错误是 1 级和 2 级互混,因为这两个级别的病灶特征本身就重叠。针对这种情况,可以在损失函数里给 1/2 级的混淆加更高的惩罚权重,或者单独训一个二分类器来区分 1 和 2。
import numpy as np from sklearn.metrics import cohen_kappa_score def tta_predict(model, images, n_aug=8): """对每张图做 n_aug 种变换,平均 logits""" preds = [] for i in range(n_aug): # 根据 i 决定变换方式:翻转、旋转等 aug_imgs = apply_aug(images, i) preds.append(model.predict(aug_imgs)) return np.mean(preds, axis=0) def weighted_ensemble(logits_list, weights): """按权重融合多个模型的 logits""" assert len(logits_list) == len(weights) return sum(w * l for w, l in zip(weights, logits_list)) # 用验证集 QWK 算权重 qwk_b4 = cohen_kappa_score(y_val, pred_b4, weights='quadratic') qwk_b5 = cohen_kappa_score(y_val, pred_b5, weights='quadratic') w = [qwk_b4 / (qwk_b4 + qwk_b5), qwk_b5 / (qwk_b4 + qwk_b5)] final = weighted_ensemble([logits_b4, logits_b5], w)tta_predict里的n_aug不是越大越好,8 次之后收益递减,但推理时间线性增加。weighted_ensemble的权重来自验证集,别用测试集算权重——那是泄漏。如果验证集太小导致 QWK 波动大,可以用交叉验证的 out-of-fold QWK 来算权重,更稳。
从那以后我每次跑医学图像分类,都强制先跑一遍 EDA 确认类别分布,再跑一折小模型验证管线,最后才上完整交叉验证。这套项目把该踩的坑基本都踩过了,你顺着 notebook 走能省不少时间。希望帮到你。
本文还有配套的精品资源,点击获取