ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于EfficientNet的糖尿病视网膜病变分级实战:从数据预处理到Stacking集成

基于EfficientNet的糖尿病视网膜病变分级实战:从数据预处理到Stacking集成 简介本资源面向计算机与人工智能方向的毕业设计、课程设计及项目开发学习者提供一套基于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 到增强后的 Tensor2.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 DR0 级可能占 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×380B5 是 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, tol7): 裁掉眼底图四周黑边tol 控制阈值容忍度 gray cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) mask gray tol if mask.sum() 0: return img rows np.any(mask, axis1) cols np.any(mask, axis0) rmin, rmax np.where(rows)[0][[0, -1]] cmin, cmax np.where(cols)[0][[0, -1]] return img[rmin:rmax1, cmin:cmax1] def apply_clahe(img, clip2.0, grid(8,8)): 在 LAB 空间的 L 通道做 CLAHE避免颜色偏移 lab cv2.cvtColor(img, cv2.COLOR_RGB2LAB) clahe cv2.createCLAHE(clipLimitclip, tileGridSizegrid) 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_splits5, shuffleTrue, random_state42) for fold, (tr_idx, val_idx) in enumerate(skf.split(df, df[level])): train_df df.iloc[tr_idx].reset_index(dropTrue) val_df df.iloc[val_idx].reset_index(dropTrue) # 后续用 train_df / val_df 构建 generator print(fFold {fold}: train {len(train_df)}, val {len(val_df)}) print(val_df[level].value_counts(normalizeTrue))n_splits5是精度和训练成本的折中。如果你样本量少比如每级只有几十张可以提到 10 折但训练时间线性增加。random_state42固定住保证每次划分一致否则你调参调半天发现是数据划分变了血泪经验。每个 fold 训练完后notebook 会把验证集的 logits 存成 npy 或 csv。注意存的是 logits 不是 softmax 概率因为 stacking 的时候用 logits 做特征更稳定softmax 会压缩动态范围。3.3 训练时的关键超参和回调项目里用的优化器是 AdamW学习率带 cosine 衰减初始 lr 在 1e-3 到 1e-4 之间B4 用 1e-3B5 因为模型更大用 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, weightsquadratic)的调用如果没有自己补上。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.5weight_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_aug8): 对每张图做 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, axis0) 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, weightsquadratic) qwk_b5 cohen_kappa_score(y_val, pred_b5, weightsquadratic) 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 走能省不少时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表