ARTICLE DETAIL

资讯详情

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

PyTorch+ResNet50眼科图像分类实战:迁移学习与工程化实现

PyTorch+ResNet50眼科图像分类实战:迁移学习与工程化实现 简介基于PyTorch与ResNet50的眼部疾病图片分类项目是一份面向计算机专业课程设计与期末大作业的高分完整方案适合正在完成图像分类方向课题或希望借助实战项目熟悉深度学习流程的学生。资源包共含10个文件以7个Python脚本为核心覆盖数据预处理、划分训练集与验证集、ResNet50模型定义、分类训练以及准确率等指标评估的完整链路预处理脚本负责图像增强与尺寸统一训练脚本支持超参数配置评估脚本可输出分类指标便于复现与二次开发。另附2个Markdown说明文档对项目背景、运行环境和实现思路进行补充压缩包整体仅22KB结构紧凑、便于本地快速部署。该项目已经过导师指导并验证可运行评审分数为98分代码模块划分清晰可直接作为课程设计源码或在此基础上继续改进。资源已有291人学习下载口碑与参考价值较为可靠。1. 眼科图像分类大作业为什么要用 PyTorch ResNet50医学图像分类的课程设计里眼科疾病是出现频率很高的题目原因很实在眼底图像公开数据多、类别定义清晰而且任务难度足够撑起一篇完整的技术报告。真正上手跑一遍会发现直接用 torchvision 里的 ResNet50 加载 ImageNet 预训练权重、替换全连接层、跑几十个 epoch得到的结果往往并不差但这门大作业的高分点反而在数据划分和评估模块上。导师和助教看重的不是一张准确率截图而是整个管线是否严谨、代码是否可复现、指标是否可信。这个项目里恰好把这几个环节都拆开了divide_trdev.py 做划分preprocess.py 做预处理dataset.py 封装数据管道resnet.py 负责迁移学习train_3d.py 承载训练流程metric.py 完成评估。对正在做期末大作业或者需要一份可运行视觉项目源码的人来说这套结构本身就是很好的工程模板。适合已经掌握 Python 基础、想快速完成一个高质量图像分类项目的学习者。2. divide_trdev.py 分层划分与 preprocess.py 图像预处理第一步不是训练很多课程设计把注意力全放在模型上拿到图片文件夹就直接往 DataLoader 里塞最后 report 里“数据集划分”只有一句“按 8:2 随机切分”。但眼科疾病数据本身类别不平衡某些疾病样本数量明显偏少如果随机划分验证集和测试集可能恰好缺少部分类别评估结果会失真。项目里专门留出划分和预处理脚本就是为了先解决这个基础问题。2.1 按类别分层的 train/valid/test 划分逻辑divide_trdev.py 的名字对应 train/dev验证集划分实际做得更细。常见做法是先读取所有图片路径和标签按照类别比例做两层划分第一层把总数据集拆成 train 和 temp第二层再把 temp 拆成 valid 和 test。关键在于stratify参数它保证每个类别在训练集、验证集、测试集中的占比与原始数据集一致。from sklearn.model_selection import train_test_split # image_paths: 所有图片的绝对路径列表 # labels: 与 image_paths 一一对应的类别索引列表 indices list(range(len(image_paths))) train_idx, temp_idx train_test_split( indices, test_size0.3, # 先留出 30% 作为验证测试 stratifylabels, # 按类别比例分层 random_state42 # 固定随机种子保证可复现 ) temp_labels [labels[i] for i in temp_idx] valid_idx, test_idx train_test_split( temp_idx, test_size0.5, # 30% 的一半 15% 测试15% 验证 stratifytemp_labels, random_state42 )这段代码里stratifylabels是核心。如果数据集中正常眼样本 3000 张青光眼样本 400 张随机划分可能让测试集的 400 张青光眼样本全部进入训练集模型验证时永远看不到这种病。分层划分能避免这类极端情况。划分完成后脚本会把train_idx、valid_idx、test_idx对应的文件路径分别写入三个文本文件格式为图片路径,标签索引。后续 dataset.py 直接读取这些文件不需要在每次运行时重新划分保证不同实验之间使用同一份数据切分这是可复现性中的一个容易被忽略的细节。2.2 preprocess.py 中的图像缩放、裁剪与归一化预处理脚本处理的是原始图片。医学图像来源复杂有的图片分辨率是几千乘几千有的是手机翻拍屏幕还有损坏文件混入。preprocess.py 会先做基础清洗剔除无法解码的图片再把所有有效图片统一转换为 RGB 三通道 JPG。在送入 ResNet50 之前图像被处理成模型期望的输入尺寸。ResNet50 在 ImageNet 上训练时输入是 224×224 像素因此这里需要把眼底图缩放并裁剪到同样尺寸。常见做法是先 resize 到 256×256再随机裁切出 224×224这样能保留图像边缘信息也起到数据增强作用。from torchvision import transforms preprocess transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), # 将 HWC 的 0~255 uint8 转为 CHW 的 0~1 float32 transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize里使用的 mean 和 std 不是随便取的而是 ImageNet 数据集各通道的统计值。使用预训练权重时输入分布必须跟训练分布一致否则特征分布会偏移。项目里这个 transform 会被 dataset.py 复用后面验证阶段也使用同一套归一化参数。预处理步骤目标尺寸/参数作用Resize256 × 256缩小计算量保留细节RandomCrop/CenterCrop224 × 224与 ResNet50 输入对齐ToTensor0 ~ 1 float32便于梯度计算NormalizeImageNet mean/std对齐预训练输入分布补充一点如果原始数据集中图像宽高不一致预处理时不要直接作为 ResNet50 输入因为预训练模型对输入尺寸有最低要求。项目中的 256 后再裁剪 224 是通用方案兼顾信息量和训练速度。3. dataset.py把数据管道封装成可迭代的 Dataset模型训练之前需要把划分好的文件列表转换成 PyTorch 能直接迭代的数据源。dataset.py 做的事情不复杂但决定了数据加载效率。如果这个类写得不够健壮训练时容易遇到“图片读取失败”“标签对不上”“多线程数据加载卡死”这类问题。3.1 继承 Dataset 实现眼病图片加载EyeDataset类继承自torch.utils.data.Dataset初始化时接收文件列表路径和图像根目录在__getitem__中按索引读取单张图片。这个设计的好处是 DataLoader 可以按 batch 随机采样不用一次性把所有图片加载进内存。import os import torch from PIL import Image from torch.utils.data import Dataset class EyeDataset(Dataset): def __init__(self, split_file, root_dir, transformNone): self.samples [] self.transform transform with open(split_file, r) as f: for line in f: img_path, label line.strip().split(,) self.samples.append((os.path.join(root_dir, img_path), int(label))) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] image Image.open(img_path).convert(RGB) if self.transform is not None: image self.transform(image) return image, label注意convert(RGB)这一步不能省。眼底图可能是灰度图也可能是带 alpha 通道的 PNG统一转成 RGB 能避免通道数不一致导致的 tensor shape 异常。DataLoader 使用时shuffleTrue只对训练集开启。验证集和测试集保持原始顺序即可因为评估阶段不需要打乱样本。from torch.utils.data import DataLoader train_dataset EyeDataset(train.txt, root_dirdata, transformtrain_transform) valid_dataset EyeDataset(valid.txt, root_dirdata, transformvalid_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) valid_loader DataLoader(valid_dataset, batch_size32, shuffleFalse, num_workers4)num_workers4表示用 4 个子进程预取数据。在 Windows 环境下如果出现 dataloader 报错可以先设为 0 排查是否是数据读取逻辑问题。Linux 服务器上可以适当调大这个值但不能超过 CPU 核心数。3.2 训练集与验证集不同的 transform 策略数据增强只在训练时使用验证和测试时只做缩放裁剪和归一化。原因很直接验证集的目标是反映真实分布加入随机噪声会让指标不稳定。项目中的训练增强策略需要考虑医学图像的特殊性。通用数据集常用的RandomHorizontalFlip和RandomRotation可以保留但ColorJitter要慎重。眼底图像的颜色通常对应血管、视网膜色素沉积等信息过度调整色相和饱和度会改变病灶外观可能导致模型学到错误颜色模式。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) valid_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop比固定CenterCrop更有利于训练每次裁剪区域不同相当于对图像位置做扰动。而ColorJitter只保留亮度和对比度的轻微调整不碰色相和饱和度。增强操作参数训练集验证集Resize256×256是是随机裁剪scale 0.8~1.0是否水平翻转0.5 概率是否旋转正负 10 度是否颜色抖动亮度/对比度 0.2是否CenterCrop224×224否是4. resnet.py 迁移学习从 ImageNet 权重到眼科病灶特征迁移学习是眼病分类项目的核心策略。眼科图像与 ImageNet 中的自然图像差异很大但底层特征如边缘、纹理、颜色块是通用的。把 ImageNet 上学习到的特征提取器保住只重新训练高层分类层比从头训练收敛快得多。4.1 ResNet50 的残差结构与参数规模ResNet50 由 4 个残差阶段组成每个阶段包含若干 Bottleneck 块。Bottleneck 先降维再升维核心是跳跃连接让梯度能直接流过深层。阶段输出尺寸块数输出通道Conv1112×112164Conv2_x56×563256Conv3_x28×284512Conv4_x14×1461024Conv5_x7×732048最后一个 Bottleneck 输出 2048 维特征经过全局平均池化后送入全连接层。默认全连接层输出 1000 个类别眼病分类任务一般只有 4 到 8 个类别所以需要替换掉这一层。4.2 加载预训练权重并替换分类头torchvision 中加载 ResNet50 有两种写法新版本推荐weights参数旧代码里常写pretrainedTrue。课程设计环境下两种都能跑通但新版 API 更清晰。import torch.nn as nn from torchvision import models num_classes 8 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) # 如果 torchvision 版本较低可以使用 pretrainedTrue # model models.resnet50(pretrainedTrue) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes)model.fc.in_features在 ResNet50 中是 2048不用硬编码避免模型结构变动后出错。替换后模型输出的 8 个 logits 传给损失函数计算。训练时是否冻结主干取决于数据规模和训练资源。眼病公开数据集样本量一般只有几千张解码全部参数容易过拟合。一个稳妥的做法是冻结前几层残差块只微调最后两个阶段for param in model.parameters(): param.requires_grad False for param in model.layer4.parameters(): param.requires_grad True for param in model.fc.parameters(): param.requires_grad True只训练layer4和全连接层参数量大幅减少显存占用降低。如果训练集样本量超过一万再考虑解冻更多层。项目源码中保留完整可训练版本实际跑的时候可以根据显存调整。4.3 损失函数中加入类别权重眼科数据里“正常”类别通常远多于病变类别直接用CrossEntropyLoss会让模型偏向多数类。为每个类别分配一个权重让少样本类别在 loss 中贡献更大class_weights torch.tensor([0.5, 1.0, 1.5, 2.0, 1.2, 1.8, 1.0, 1.0]).cuda() criterion nn.CrossEntropyLoss(weightclass_weights)权重可以按总样本数 / (类别数 × 该类样本数)计算也可以根据人工经验微调。项目文档里如果没有给出具体权重用逆频率法最可靠counts [3200, 400, 600, 800, 500, 300, 900, 700] total sum(counts) class_weights [total / (len(counts) * c) for c in counts]5. train_3d.py训练循环、学习率调度与模型保存训练脚本是整个项目的执行主体。train_3d.py 把数据集、模型、损失函数、优化器组装在一起通过命令行参数控制训练配置。这份脚本的健壮性直接影响大作业答辩时的运行演示因为导师可能现场改参数重新跑。5.1 一个完整 Epoch 的训练骨架训练循环的核心是三步前向计算、反向传播、参数更新。模型必须先调用model.train()这会启用 BatchNorm 的统计更新和 Dropout 的随机丢弃评估阶段再切换到model.eval()。def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() total_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) return total_loss / total, correct / totaloptimizer.zero_grad()必须每步调用否则梯度会在多个 batch 上累加导致参数更新方向错误。loss.item()取出的是标量用于累计 loss 值。5.2 验证阶段与早停每训练完一个 epoch立刻在验证集上计算准确率和 loss并根据验证准确率决定是否保存当前模型。这个策略在课程设计中非常实用避免训练到最后过拟合后保存了一个表现差的模型。def evaluate(model, valid_loader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in valid_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) return total_loss / total, correct / total早停逻辑如果验证准确率比历史最优值高就保存best_model.pth同时把连续不提升的计数器清零否则计数器加一超过patience就结束训练。best_acc 0.0 patience 10 bad_epochs 0 for epoch in range(num_epochs): train_loss, train_acc train_one_epoch(...) val_loss, val_acc evaluate(...) if val_acc best_acc: best_acc val_acc bad_epochs 0 torch.save(model.state_dict(), best_model.pth) else: bad_epochs 1 if bad_epochs patience: print(early stop at epoch, epoch) break5.3 优化器、学习率调度与运行参数优化器选择上迁移学习微调用 SGD 配 momentum 稳定性较好Adam 收敛更快但泛化略差。课程设计场景更看重最终指标通常 SGD 能跑到更高的验证准确率。参数名示例值说明--lr0.001初始学习率--batch_size32受显存限制--epochs50最大训练轮数--weight_decay1e-4L2 正则化--devicecuda:0无 GPU 时改为 cpu--num_classes8眼病类别数学习率调度分两种常见方案ReduceLROnPlateau在验证指标停滞时缩小学习率CosineAnnealingLR按余弦周期衰减。项目里更推荐前者因为它和早停天然配合。import torch.optim as optim optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, factor0.1, patience5 ) # 每个 epoch 结束后 scheduler.step(val_acc)运行脚本时通过命令行参数覆盖默认配置python train_3d.py --batch_size 32 --lr 0.001 --epochs 50 --device cuda:0脚本内用argparse解析这些参数训练结束后输出最优验证准确率和保存路径。答辩时可以直接展示这段终端内容说明代码具备可配置性和可复现性。6. metric.py 评估指标与高分作业的验证细节分类准确率是最直观的指标但在眼科疾病分类中不够用。助教评审时会关注模型在少数类上的表现因此项目里必须有完整的指标体系。metric.py 负责在测试集上计算准确率、精确率、召回率、F1-score 和 AUC并输出混淆矩阵。6.1 多分类指标计算与混淆矩阵生成先加载测试集预测结果再调用 sklearn 的classification_report得到每个类别的指标。同时用confusion_matrix生成一个表格直观展示哪些类别容易被混淆。import numpy as np import torch from sklearn.metrics import classification_report, confusion_matrix preds torch.load(test_predictions.pt) labels torch.load(test_labels.pt) report classification_report(labels, preds, digits4) print(report) cm confusion_matrix(labels, preds) print(cm) import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted Label) plt.ylabel(True Label) plt.savefig(confusion_matrix.png, dpi150)AUC 适合评估二分类或者多标签场景。眼病数据本身就是多分类可以按 one-vs-rest 分别计算每个类别的 AUC再取宏平均。sklearn 中roc_auc_score可以直接处理多标签概率输出前提是传入每个类别的预测概率而非硬标签。from sklearn.metrics import roc_auc_score # probs: shape (N, num_classes)每个类别的 softmax 概率 # labels_onehot: shape(N, num_classes)one-hot 编码 auc_macro roc_auc_score(labels_onehot, probs, averagemacro) print(Macro AUC:, auc_macro)6.2 提交代码和报告前值得检查的三个细节固定随机种子保证结果可复现。模型训练前在代码入口处设置 Python、NumPy 和 PyTorch 的随机种子否则每次运行结果都会波动。import random import numpy as np import torch def set_seed(seed2024): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)测试集评估时不要使用训练时的增强策略。部分同学为了“提高指标”在测试时也做翻转这会虚高结果被助教追问时很难解释。最后将错误样本可视化。从测试集中挑出预测错误且置信度较高的图像统一排列保存为error_analysis.png报告里放一张这样的图比单纯的指标表格更有说服力也能体现你对模型失败边界的理解。本文还有配套的精品资源点击获取
返回列表