ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Jupyter的糖尿病视网膜病变分级:ResNet50迁移学习实战

基于Jupyter的糖尿病视网膜病变分级:ResNet50迁移学习实战 简介卷积神经网络CNN是医学图像分析的核心技术之一其通过多层特征提取实现病变的自动识别。在医学影像样本有限的情况下迁移学习成为提升模型性能的关键策略——利用ImageNet预训练权重初始化网络再以眼底彩照进行微调可有效解决小样本训练难题。该技术路线已被广泛应用于糖尿病视网膜病变DR的自动分级任务中通过将眼底图像映射至0-4级病变程度辅助医生进行筛查与诊断。基于Jupyter Notebook的工程化实现能够完整覆盖数据预处理、模型训练、评估与可视化全流程提供可复现的医学图像分类方案。本文以ResNet50为例详细阐述从数据准备、迁移学习训练到Grad-CAM可视化的实践细节为医学图像方向的毕业设计提供参考。 糖尿病视网膜病变DR是糖尿病最常见的微血管并发症之一也是全球工作年龄人群致盲的首要原因。这个基于Jupyter的毕设项目本质上是做了一件事用卷积神经网络对眼底彩照做自动分级判断病变属于哪一期。它把从数据处理、模型训练到结果可视化的完整流程都装进了一套可复现的工程里。如果你正好在准备医学图像相关的毕设或课程项目这篇内容可以帮你少踩很多坑。我按照自己当年做类似项目时的思路把整个项目从设计理念到落地细节拆开讲顺便把那些文档里一般不写、但实际操作一定会遇到的坑也一并列出来。1. 项目整体设计与技术选型思路先聊最核心的问题为什么这个项目适合作为毕业设计以及为什么技术栈是Jupyter Python 深度学习这几样搭配。1.1 为什么用Jupyter而不是IDEJupyter Notebook在这个项目里的优势不是写代码效率高而是过程可记录、结果可展示。医学图像分类项目通常不是一次写出完整脚本就跑通的中间会反复试实验换预训练模型、调学习率、看某类样本有没有被分错。Jupyter的单元格执行模式非常适合这种探索式工作流每一个处理步骤、每一张训练曲线图、每一次预测结果都留在Notebook里导师一眼就能看懂整个思考过程不用去翻几十个.py文件。而且对于毕业设计来说最终答辩时直接展示Notebook里的执行过程和可视化图表比打开一个黑乎乎的终端窗口跑训练要有说服力得多。我的建议是实验阶段用Notebook跑通全部流程最后再抽一个干净的.py脚本放进源码里两者不冲突。1.2 核心任务拆解分类而不是检测糖尿病视网膜病变的诊断有多种切入点。有人做微血管瘤检测有人做出血点分割这个项目选择的是严重程度分级也就是把一张眼底图映射到0-4级或简化为二分类这种方案在公开数据集上最容易验证也最容易讲清楚实验结论。任务定义清楚了后面的网络结构选择才有依据。分级任务本质上是图像分类最合适的就是卷积神经网络。你不需要去写什么复杂的自定义网络直接用现成的分类骨干网络做迁移学习就能达到不错的效果。项目里用的ResNet50是经验之选——它足够深能学到复杂的病理纹理特征又比EfficientNet这些更贴近教科书上的经典结构写进论文里更容易解释。1.3 迁移学习为什么是必选项医学图像数据集和大规模的ImageNet相比样本量小了不止一个数量级。公开的糖尿病视网膜病变数据集训练集通常只有几千到几万张这点数据量不足以从头训练一个深层CNN。迁移学习的思路是先在ImageNet这种千万级数据集上预训练让网络学会通用的边缘、纹理、形状等底层特征然后拿这些特征作为起点用眼底图像做微调。有人可能会担心ImageNet里都是猫猫狗狗日常物体和眼底图像差异那么大迁移学习还有用吗实测下来是有的。因为卷积神经网络的前几层学到的是非常底层的视觉特征比如边缘方向、颜色块、纹理重复模式这些特征在任何图像上都通用。真正需要重新学习的只是后面的高层语义特征。当然也不是说拿过来就能用关键有几个坑一是ResNet50的输入尺寸是224x224但原图是几千像素的高清眼底图直接硬压缩会丢失微小的病理细节二是预训练权重里ImageNet的归一化参数必须保留用错均值方差会让训练一开始就出问题。这些细节在第三节里细说。2. 数据集准备与预处理细节这个项目的完整程度很大一部分体现在数据处理上。模型结构可以照搬开源仓库但数据处理好坏直接决定最终准确率的上限。2.1 数据集来源与选型对比我实测常用的公开眼底数据集有三个APTOS 2019、EyePACS和Messidor-2。APTOS 2019是我最推荐使用的它在Kaggle上公开包含3662张训练图标注是0-4共5个等级0表示无病变1-4分别对应轻度NPDR、中度NPDR、重度NPDR和PDR。图像已经做过裁边和统一尺寸处理质量比较整齐。EyePACS的数据量更大超过35000张但噪声也大很多图像拍摄质量差、标注争议多处理起来非常费时间。Messidor-2是法国公开数据集标注质量好但需要申请流程相对麻烦。如果你在答辩时被问到为什么选这个数据集可以回答APTOS 2019已经有明确的等级标注标准且数据集本身是经过筛查的、可用于学术研究的公开资源能保证实验可复现。2.2 数据读取与标签处理项目文件里通常会有一个train.csv之类的标注文件包含id_code和diagnosis两列。读取逻辑很简单import pandas as pd df pd.read_csv(train.csv) print(df[diagnosis].value_counts()) # 输出示例 # 0 1805 # 2 999 # 1 370 # 4 295 # 3 193注意看这个类别分布五分类存在明显的类别不平衡0类正常比其他类多出好几倍。如果不处理直接训练模型会倾向于把所有样本都预测成0类整体准确率看起来还挺高但3级和4级的召回率会惨不忍睹。这对医学类项目来说是不可接受的——漏掉一个重症患者的代价远高于把正常人多检查一次。最常见也最简单的方案是加权采样。在DataLoader里给每个类别设置采样权重让每轮epoch里每个类被抽到的概率接近均等from torch.utils.data import WeightedRandomSampler class_counts df[diagnosis].value_counts().sort_index().values class_weights 1.0 / class_counts sample_weights [class_weights[label] for label in df[diagnosis]] sampler WeightedRandomSampler(sample_weights, num_sampleslen(df), replacementTrue)如果用Keras/TensorFlow就用class_weight参数来设定权重效果是一样的。2.3 图像预处理的关键细节图像预处理直接决定了训练能不能收敛、收敛得快不快。第一步是统一尺寸。ResNet50默认输入是224x224但很多项目实践表明用299x299或者384x384效果会更好因为眼底图像中的微血管瘤、棉絮斑这些病理特征非常细小分辨率太低会把它们直接抹掉。我在实验里对比过224x224跑出来的验证集AUC大约是0.93提升到299之后能到0.95左右。代价是训练时间增加约40%对毕设来说这个交换是值得的。第二步是归一化。ImageNet预训练模型的归一化参数是mean[0.485,0.456,0.406]std[0.229,0.224,0.225]注意这个参数是固定在ImageNet上统计出来的迁移学习时不能改。如果改了会对权重初始化造成错位。第三步也和医学图像有关眼底图像有一个特点是中心亮、边缘暗很多数据集还带有黑色的圆边。如果不处理模型很容易学到边缘是不是黑的这种伪特征。常规做法是先用圆形遮罩把眼底区域裁剪出来再进行尺寸缩放import cv2 import numpy as np def crop_eye_region(image): # 将图像转为灰度通过阈值找到眼底圆盘轮廓 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return image max_contour max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(max_contour) return image[y:yh, x:xw]2.4 数据增强策略医学图像领域的数据增强和自然图像不太一样要对翻转会不会改变医学含义保持敏感。比如左右翻转在眼底图像上是合理的因为左右眼结构对称病变在这张图上出现在左侧还是右侧不影响分级。旋转和细微尺度缩放也安全。但像随机裁剪这种增强要谨慎使用——如果裁剪的幅度太大可能把关键病灶区域裁掉导致标签失真。我习惯的增强组合是水平翻转概率0.5、水平垂直翻转概率0.25、随机旋转±30度、小范围缩放0.9-1.1、色度微调HSV空间的小抖动。不使用随机擦除和强形变。另外逻辑上增强应该应用在图片送入模型之前而不是预先做离线增强。离线增强会把所有图存到磁盘里增加不止一倍的存空间而且每个epoch看到的增强数据是固定的反而比在线增强更容易过拟合。3. 模型构建与训练实操这个部分是整个项目的技术核心我把代码结构、训练策略和参数选择都展开讲一遍。3.1 模型结构在ResNet50上做迁移学习使用PyTorch的话模型构建代码非常简洁import torch import torch.nn as nn import torchvision.models as models class DRClassifier(nn.Module): def __init__(self, num_classes5, pretrainedTrue): super().__init__() self.backbone models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) in_features self.backbone.fc.in_features self.backbone.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(in_features, num_classes) ) def forward(self, x): return self.backbone(x)替换最后的全连接层时有一个细节值得说原来的fc层是Linear(2048,1000)我们把1000维的ImageNet分类输出替换成5维的分级输出。在fc之前加上一个Dropout层可以缓解全连接层的过拟合问题医学数据本身噪声大这层Dropout很有效。如果你想做对比实验比较推荐的模型候选是ResNet34、ResNet50、EfficientNet-B3。ResNet34更轻量但效果稍弱EfficientNet-B3在相同FLOPs下精度更高但调参要更细腻吃显存也更重。特别提醒如果机器显存不够小于6GB优先考虑ResNet34别硬上ResNet50。3.2 训练策略分阶段微调训练策略上我强烈推荐冻结主干、先训分类头、再解冻微调这个三步法不要一步到位直接全量微调。第一步把backbone里所有参数都冻结只训练新加的全连接层跑5个epoch。这一步相当于让随机初始化的分类头先学会在预训练特征上做分类避免在训练初期用巨大梯度去破坏预训练权重。第二步解冻最后一个残差块layer4连同分类头一起训练用较小的学习率跑10-15个epoch。在这一步模型开始把更高层的特征往眼底图像的特征空间上适配。第三步如果数据量比较充足或者第二阶段loss还在明显下降可以考虑解冻更多层全量微调3-5个epoch。实现冻结和解冻的代码逻辑for param in model.backbone.parameters(): param.requires_grad False # 第一步只训练fc层 optimizer torch.optim.Adam(model.backbone.fc.parameters(), lr1e-3) # 第二步解冻layer4 for param in model.backbone.layer4.parameters(): param.requires_grad True optimizer torch.optim.Adam([ {params: model.backbone.fc.parameters(), lr: 1e-3}, {params: model.backbone.layer4.parameters(), lr: 1e-4} ])学习率使用余弦退火或者ReduceLROnPlateau都可以实测下来ReduceLROnPlateau在医学图像上的稳定性更好——因为它按验证集表现来降低学习率不容易在epoch末尾冲过头。初始学习率在第一阶段用1e-3微调阶段用1e-4或5e-5这是个体力活建议用上面说的分段策略不要一上来就学别人用1e-4全量训练。3.3 训练过程中的监控指标训练医学分类模型不要只盯着准确率看。在类别不平衡的情况下准确率是严重失真的指标。我在前面章节提过如果正常样本占70%模型全预测为正常也有70%准确率这个模型实际上对重症患者完全无效。所以我在训练循环里同时监控以下指标敏感度Sensitivity/召回率真实重症样本中被正确预测出来的比例。这个指标对医学诊断最重要宁可误判也不能漏判。特异性Specificity正常样本中正确判为正常的比例用于衡量误报率。AUC综合评价模型在不同阈值下的表现适合跨类别比较。Kappa科恩卡帕系数在APTOS竞赛中就是这个评价标准衡量的是模型预测和医生标注的吻合度比准确率更适合有序分类问题。在每个epoch结束时计算这些指标并保存验证集loss最低的模型权重。这里踩过一个坑如果不加限制地保存准确率最高的模型往往保存下来的是那个把所有样本预测成0类的模型。一定要用loss或AUC作为模型选择的依据。3.4 训练参数参考值贴一份我在单个RTX 306012GB显存上实测能跑通的参数配置配置项参考值说明输入尺寸299x299比224保留更多病理细节batch size3212GB显存下上限优化器Adam初始lr按阶段设置第一阶段epoch5只训练FC层第二阶段epoch12-15解冻layer4全量微调epoch3-5可选梯度裁剪max_norm1.0防止震荡混合精度torch.cuda.amp显存不够时使用早停patience8连续8轮无提升则停止4. 模型评估与可视化分析训练完成之后如何把模型的性能客观地展示出来决定毕设文档的说服力。这个环节不需要那么多代码但需要你对怎么讲故事有清晰思路。4.1 混淆矩阵与核心指标在测试集上跑完预测后第一件事是画出混淆矩阵。混淆矩阵可以直观地展示哪些类别容易混淆。常见现象是1级轻度NPDR和2级中度NPDR之间大量混淆这是因为轻度和中度的边界在临床上本身就模糊医生标注都可能不一致。如果看到了这种混淆不用慌可以在论文里把它作为任务本身具有挑战性的论据。核心指标建议用sklearn.metrics一键计算from sklearn.metrics import classification_report, cohen_kappa_score, roc_auc_score print(classification_report(y_true, y_pred)) kappa cohen_kappa_score(y_true, y_pred, weightsquadratic) print(fQuadratic Kappa: {kappa:.4f})**二次加权KappaQuadratic Weighted Kappa**是眼底图像分级领域最常用的指标它考虑到分级是有序的——预测成相邻等级比预测成相隔很远的等级错误更小。4.2 把预测结果可视化光是输出一堆数字指标不够直观医生和答辩老师更想看的是模型到底看哪里做出了判断。我强烈建议在项目中加一个Grad-CAM梯度加权类激活映射可视化模块。Grad-CAM的思路是找到最后一个卷积层的特征图用类别对应的梯度给特征图做加权把加权结果上采样到原图像尺寸叠加成热力图。热力图亮的区域就是模型做出判断时最关注的区域。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget cam GradCAM(modelmodel, target_layers[model.backbone.layer4[-1]], use_cudaTrue) target [ClassifierOutputTarget(pred_class)] grayscale_cam cam(input_tensor, targetstarget)把热力图叠加到原图上你会看到模型确实在关注血管区域、出血点、渗出斑块。这个可视化不只在答辩时加分在调试阶段也很有用——如果某个样本被预测错了通过热力图能分析出模型是不是关注了图像上的噪声区域比如照相机反光、图像边缘。4.3 错误样本分析毕设论文的差异化亮点大部分毕设做到评估指标就停了但作为过来人我提醒你错误样本分析是让毕设从做得对变成做得好的关键一步。具体操作从测试集找出所有预测类别和真实类别不一致的样本逐张汇总分析。通常你会发现几类典型的错误模式图像质量差模糊、曝光不足、有遮挡病变处于早期特征非常微弱病灶出现在视网膜周边区域主模型没有关注到左右眼混淆导致的语义特征偏差在论文里把这些错误模式总结成表格或者示例图然后提出未来工作的方向——比如引入图像质量评估模块、使用多尺度特征融合、或者用注意力机制强化周边区域的特征权重。这个部分可以让研究方向有深度答辩时老师问你的工作还有什么不足你就有了一个有理有据的回答。5. 常见问题与排查技巧实录这部分内容是我实际跑这个项目时踩过的坑有些问题是百度都搜不太到明确答案的写下来供参考。5.1 Jupyter环境相关的坑Jupyter Notebook里训练模型时内核无响应/崩溃这几乎是100%会碰上的问题。最常见的原因是内存溢出数据集被一次性加载到内存或者数据增强时中间变量太多占满了内存。解决办法有两个方向不要一次性resize所有图片再存成numpy数组改成在Dataset类里做预处理每次只处理当前batch的图片。设置torch.cuda.empty_cache()在每个epoch结束后清理缓存GPU显存碎片。另外Jupyter内核崩溃后之前的变量会全部丢失代码虽然还在但模型状态没了。实用的做法是每跑完一个阶段就保存checkpoint不要等整个训练跑完再一次性保存。DataLoader在Windows下报多进程错误Jupyter在Windows下用DataLoader(num_workers0)经常报BrokenPipeError。这个是PyTorch在Windows上多进程的已知坑。解决办法把num_workers设为0慢但稳定或者在训练代码外层加if __name__ __main__:保护。更推荐的做法是在Jupyter里直接设num_workers0等实验稳定后再把训练代码整理成.py脚本在脚本里用num_workers4。5.2 数据与训练过程异常训练一开始loss就是NaN导致NaN的原因主要有学习率过大常见于Adam1e-2以上、数据归一化没做好导致梯度爆炸、标签里包含NaN值。排查顺序先检查df[diagnosis]是否有缺失值再用小学习率1e-5跑一两个batch看loss是否正常能跑后再逐步把学习率调回去。验证集AUC一直卡在0.85左右上不去我遇到这个现象时折腾了很久调参都没有大提升。后来仔细检查发现是数据划分出了问题——训练集和测试集是在同一个人多次就诊的图像之间划分的存在信息泄露导致验证结果虚高。解决方法是按患者ID分组划分数据集确保同一患者的所有图像只在训练集或测试集一边。因为眼底图像分类的目标是判断新患者的病情不是判断看过的患者的病情。过拟合严重训练集准确率接近100%但验证集只有80%这是医学小数据集上的典型问题应对手段按优先级排序增强数据增强强度增加旋转角度和色度抖动、加大Dropout率从0.5调到0.6、减少Trainable层数退回到只训练FC层layer4的last block、引入Label Smoothingtorch.nn.CrossEntropyLoss(label_smoothing0.1)。5.3 训练效率优化如果训练一个epoch需要很长时间除了换GPU还可以做两件事。第一检查数据加载是不是瓶颈在训练循环里加一个简单的计时如果加载一个batch的时间超过模型forwardbackward的时间那就该用num_workers0pin_memoryTrue或者把图像预先resize到较小的临时目录。第二开混合精度训练AMP在支持Tensor Core的显卡上可以提速约40%-60%显存占用也略有下降对图像分类任务精度影响非常小。6. 毕业设计的文档撰写与答辩准备最后聊一下模型、代码都跑通了之后怎么把项目转换成一份高质量的毕设文档。6.1 文档结构建议和写作重点毕设说明书通常在1.5万字到3万字之间我的建议结构是绪论/背景写清楚糖尿病视网膜病变的医学背景和筛查困境引出课题的研究意义。这里要引真实数据比如全球糖尿病患者人群中DR的患病率注意引用来源要可查。相关工作不要把相关工作的篇幅写太长重点是分类梳理比如传统机器学习方法SVM/HOG特征和深度学习方法从AlexNet到ResNet的演进弱化堆砌论文综述强化对课题的承接。系统设计这部分是整个文档的核心详细描述数据→预处理→模型→训练→评估→可视化的完整链路。每个模块配代码核心片段、输入输出描述和设计理由。实验与结果用表格列出各模型对比ResNet34 vs ResNet50 vs EfficientNet、各数据增强策略的消融实验、混淆矩阵和指标。多做消融实验是提高毕设含金量的最经济手段。总结与展望客观总结项目的优势和不足展望方向要具体一点。写文档时最容易被忽视的一点是所有图表都必须有对应说明和解读。图表标题规范统一不要出现实验结果这种空泛标题而是写表4-2三种模型在APTOS 2019测试集上的AUC对比图的横纵坐标、图例、单位一个都不能少。6.2 答辩前需准备的核心问答模拟一下答辩老师最可能问的方向为什么要做多分类而不是二分类——因为临床上有5级分级标准多分类信息更丰富如果需要筛查场景可以退化成二分类。为什么选择ResNet50而不是更新的模型——因为它在精度和资源消耗之间平衡好且结构经典易于解释如果数据量更充足可以尝试更大的模型。怎么验证模型的泛化能力——通过交叉验证、按患者分组划分数据、在另一个独立数据集上做外部验证。如果数据增加模型效果会更好吗——理论上会但也要考虑噪声数据的影响实际中可以设计增量实验验证。6.3 关于项目目录的整理最后给一个我调试多次后觉得最舒服的项目结构建议直接照抄DR-Detection/ ├── data/ │ ├── train_images/ │ └── train.csv ├── notebooks/ │ ├── 01-data-exploration.ipynb │ ├── 02-training-resnet50.ipynb │ └── 03-evaluation-and-visualization.ipynb ├── src/ │ ├── dataset.py │ ├── model.py │ ├── train.py │ └── evaluate.py ├── models/ │ ├── resnet50_best.pth │ └── training_log.csv ├── output/ │ ├── confusion_matrix.png │ └── grad_cam_samples/ ├── docs/ │ ├── 开题报告.docx │ ├── 中期报告.docx │ └── 毕业论文.docx ├── requirements.txt └── README.md注意一个细节数据目录和模型权重文件不要直接放在Git仓库里。数据集动辄几个GBGit根本追不动模型权重也很大适合放网盘连接并在README里写清楚下载和解压方式。源码仓库里只保留必要代码和README说明这样导师或评审老师拿到手里也方便运行。我做这个项目时的一些体会整个项目做下来我发现最有价值的并不是最终精度那个数字而是这套问题定义——数据组织——模型选择——实验迭代——结果表达的完整流程。把一个开放性的医学问题转化成机器可以学习的分类问题中间需要大量工程细节比如数据划分方式、采样策略、评估指标选择这些环节在标准的深度学习教程里很少系统讲清但它才是决定一个项目能不能落地的关键。如果你也打算做类似的医学图像方向我的建议是先跑通一个小模型的全流程再逐步扩大数据和模型规模。第一次就把ResNet50从零开始完整训练大概率会在数据处理阶段就被搞崩。先小后大先简后繁每一步看到结果再走下一步这是最稳妥的路径。最后一个小提示训练代码里一定要把随机种子固定好包括PyTorch、NumPy和Python自带的random。这样实验结果才可复现写论文的时候也经得起推敲。这个细节我一开始也没注意后来发现同一次实验跑两次结果差不少排查半天才想起来是随机种子的问题。本文还有配套的精品资源点击获取
返回列表