news 2026/4/4 16:23:26

基于PaddlePaddle实现眼疾图像分类

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于PaddlePaddle实现眼疾图像分类

基于PaddlePaddle实现眼疾图像分类

在医疗AI领域,一个微小的像素变化可能意味着重大疾病的早期征兆。尤其是在眼科诊断中,病理性近视(Pathologic Myopia, PM)这类隐匿性强、进展迅速的眼底病变,若能在影像阶段被及时识别,将极大降低失明风险。然而,依赖人工阅片不仅耗时耗力,还容易因疲劳或经验差异导致漏诊。这正是深度学习可以大显身手的地方。

我们今天要构建的,就是一个基于国产深度学习框架PaddlePaddle的眼底图像分类系统,目标是让模型自动判断一张眼底照片是否属于“病理性近视”——这个任务看似简单,但背后涉及数据处理、模型设计、训练调优和部署落地的一整套工程实践。

项目采用由百度与中山大学中山眼科中心联合发布的iChallenge-PM 数据集,共1200张高质量眼底图像,训练/验证/测试各400张。标签规则清晰:
- 文件名以HN开头:非病理性(正常或高度近视),标记为负样本(label=0)
- 以P开头:病理性近视,正样本(label=1)

数据下载地址:https://aistudio.baidu.com/aistudio/datasetdetail/19065

数据预处理:从原始图像到可训练张量

医学图像往往存在尺寸不一、光照不均、边缘模糊等问题。直接喂给模型效果很差,必须进行标准化处理。我们的策略如下:

  1. 统一缩放到224×224——这是大多数视觉模型的标准输入尺寸;
  2. 调整通道顺序为[C, H, W],即先通道再高宽,符合主流框架要求;
  3. 归一化至[-1.0, 1.0]区间,有助于梯度稳定;
  4. 训练时打乱顺序,提升泛化能力。
import cv2 import os import random import numpy as np def transform_img(img): """图像预处理函数""" img = cv2.resize(img, (224, 224)) # 缩放 img = np.transpose(img, (2, 0, 1)) # HWC -> CHW img = img.astype('float32') # 类型转换 img = img / 255. # 归一化到 [0,1] img = img * 2.0 - 1.0 # 映射到 [-1,1] return img

为了高效加载数据,避免内存溢出,我们使用 Python 生成器实现流式读取。训练集通过文件名首字母判断标签,而验证集则从labels.csv中读取真实标注:

def data_loader(datadir, batch_size=10, mode='train'): filenames = os.listdir(datadir) def reader(): if mode == 'train': random.shuffle(filenames) # 训练时打乱顺序 batch_imgs = [] batch_labels = [] for name in filenames: filepath = os.path.join(datadir, name) img = cv2.imread(filepath) if img is None: continue img = transform_img(img) # 根据文件名首字母确定标签 if name[0] in ['H', 'N']: label = 0 elif name[0] == 'P': label = 1 else: raise ValueError(f"Unexpected filename: {name}") batch_imgs.append(img) batch_labels.append(label) if len(batch_imgs) == batch_size: imgs_array = np.array(batch_imgs).astype('float32') labels_array = np.array(batch_labels).reshape(-1, 1).astype('float32') yield imgs_array, labels_array batch_imgs, batch_labels = [], [] # 处理最后不足一个batch的数据 if len(batch_imgs) > 0: imgs_array = np.array(batch_imgs).astype('float32') labels_array = np.array(batch_labels).reshape(-1, 1).astype('float32') yield imgs_array, labels_array return reader

验证集读取器稍有不同,需解析 CSV 文件中的标签信息:

def valid_data_loader(datadir, csvfile, batch_size=10): lines = open(csvfile).readlines()[1:] # 跳过表头 filelists = [line.strip().split(',') for line in lines] def reader(): batch_imgs = [] batch_labels = [] for item in filelists: img_name = item[1] label = int(item[2]) filepath = os.path.join(datadir, img_name) img = cv2.imread(filepath) if img is None: continue img = transform_img(img) batch_imgs.append(img) batch_labels.append(label) if len(batch_imgs) == batch_size: imgs_array = np.array(batch_imgs).astype('float32') labels_array = np.array(batch_labels).reshape(-1, 1).astype('float32') yield imgs_array, labels_array batch_imgs, batch_labels = [], [] if len(batch_imgs) > 0: imgs_array = np.array(batch_imgs).astype('float32') labels_array = np.array(batch_labels).reshape(-1, 1).astype('float32') yield imgs_array, labels_array return reader

我们可以快速检查一下数据形状是否正确:

DATADIR_TRAIN = '/home/aistudio/work/palm/PALM-Training400' DATADIR_VALID = '/home/aistudio/work/palm/PALM-Validation400' CSVFILE = '/home/aistudio/labels.csv' train_loader = data_loader(DATADIR_TRAIN, batch_size=10, mode='train') data_iter = train_loader() data = next(data_iter) print("Input shape:", data[0].shape) # (10, 3, 224, 224) print("Label shape:", data[1].shape) # (10, 1)

输出应为:

Input shape: (10, 3, 224, 224) Label shape: (10, 1)

一切就绪,接下来进入核心环节。

模型构建:ResNet50 的 PaddlePaddle 实现

选择 ResNet50 并非偶然。它在 ImageNet 上表现优异,且其残差结构能有效缓解深层网络的梯度消失问题,特别适合提取复杂医学图像中的细微特征。

以下是使用 PaddlePaddle 2.x 风格编写的完整实现。注意其 API 设计非常接近 PyTorch,对新手友好,同时保留了底层灵活性。

import paddle import paddle.nn as nn class ResNetBlock(nn.Layer): expansion = 4 def __init__(self, in_channels, out_channels, stride=1, downsample=None): super(ResNetBlock, self).__init__() self.conv1 = nn.Conv2D(in_channels, out_channels, kernel_size=1, bias_attr=False) self.bn1 = nn.BatchNorm2D(out_channels) self.relu = nn.ReLU() self.conv2 = nn.Conv2D(out_channels, out_channels, kernel_size=3, padding=1, stride=stride, bias_attr=False) self.bn2 = nn.BatchNorm2D(out_channels) self.conv3 = nn.Conv2D(out_channels, out_channels * self.expansion, kernel_size=1, bias_attr=False) self.bn3 = nn.BatchNorm2D(out_channels * self.expansion) self.downsample = downsample def forward(self, x): identity = x out = self.conv1(x) out = self.bn1(out) out = self.relu(out) out = self.conv2(out) out = self.bn2(out) out = self.relu(out) out = self.conv3(out) out = self.bn3(out) if self.downsample is not None: identity = self.downsample(x) out += identity out = self.relu(out) return out class ResNet50(nn.Layer): def __init__(self, num_classes=1): super(ResNet50, self).__init__() self.in_channels = 64 self.conv1 = nn.Conv2D(3, 64, kernel_size=7, stride=2, padding=3, bias_attr=False) self.bn1 = nn.BatchNorm2D(64) self.relu = nn.ReLU() self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1) self.layer1 = self._make_layer(64, 3) self.layer2 = self._make_layer(128, 4, stride=2) self.layer3 = self._make_layer(256, 6, stride=2) self.layer4 = self._make_layer(512, 3, stride=2) self.avgpool = nn.AdaptiveAvgPool2D((1, 1)) self.fc = nn.Linear(512 * ResNetBlock.expansion, num_classes) def _make_layer(self, out_channels, blocks, stride=1): downsample = None if stride != 1 or self.in_channels != out_channels * ResNetBlock.expansion: downsample = nn.Sequential( nn.Conv2D(self.in_channels, out_channels * ResNetBlock.expansion, kernel_size=1, stride=stride, bias_attr=False), nn.BatchNorm2D(out_channels * ResNetBlock.expansion) ) layers = [] layers.append(ResNetBlock(self.in_channels, out_channels, stride, downsample)) self.in_channels = out_channels * ResNetBlock.expansion for _ in range(1, blocks): layers.append(ResNetBlock(self.in_channels, out_channels)) return nn.Sequential(*layers) def forward(self, x): x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.maxpool(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.avgpool(x) x = paddle.flatten(x, 1) x = self.fc(x) return x

💡 小贴士:虽然这里手动实现了 ResNet50,但在实际项目中更推荐使用paddle.vision.models.resnet50(pretrained=True)直接调用预训练模型,配合迁移学习可显著提升收敛速度和最终精度。

训练流程:动态图下的端到端训练

PaddlePaddle 默认启用动态图模式(dygraph),调试方便,逻辑直观。我们使用 Adam 优化器 + BCEWithLogitsLoss(二分类交叉熵损失,内置 Sigmoid),避免数值不稳定。

def train_model(model, epochs=5, lr=0.001, save_path='resnet50_pm'): optim = paddle.optimizer.Adam(parameters=model.parameters(), learning_rate=lr) bce_loss = nn.BCEWithLogitsLoss() # 自动包含sigmoid train_loader = data_loader(DATADIR_TRAIN, batch_size=10, mode='train') valid_loader = valid_data_loader(DATADIR_VALID, CSVFILE, batch_size=10) print("Start training...") for epoch in range(epochs): model.train() total_loss = 0. count = 0 for batch_id, (img, label) in enumerate(train_loader()): x = paddle.to_tensor(img) y = paddle.to_tensor(label) logits = model(x) loss = bce_loss(logits, y) loss.backward() optim.step() optim.clear_grad() total_loss += loss.numpy()[0] count += 1 if batch_id % 10 == 0: print(f"Epoch[{epoch}] Batch[{batch_id}], Loss: {loss.numpy()[0]:.4f}") avg_train_loss = total_loss / count # Validation model.eval() accuracies = [] val_losses = [] with paddle.no_grad(): for val_batch in valid_loader(): val_x, val_y = val_batch x = paddle.to_tensor(val_x) y = paddle.to_tensor(val_y) logits = model(x) pred = paddle.sigmoid(logits) acc = (pred.round() == y).astype('float32').mean() val_loss = bce_loss(logits, y) accuracies.append(acc.numpy()[0]) val_losses.append(val_loss.numpy()[0]) avg_val_acc = np.mean(accuracies) avg_val_loss = np.mean(val_losses) print(f"[Epoch {epoch+1}/{epochs}] " f"Train Loss: {avg_train_loss:.4f}, " f"Val Loss: {avg_val_loss:.4f}, " f"Val Acc: {avg_val_acc:.4f}") # 保存模型参数 paddle.save(model.state_dict(), save_path + '.pdparams') paddle.save(optim.state_dict(), save_path + '.pdopt') print(f"Model saved to {save_path}.pdparams")

启动训练只需几行代码:

with paddle.fluid.dygraph.guard(): model = ResNet50(num_classes=1) train_model(model, epochs=5)

典型输出如下:

[Epoch 1/5] Train Loss: 0.3124, Val Loss: 0.1876, Val Acc: 0.9125 ... [Epoch 5/5] Train Loss: 0.0832, Val Loss: 0.1342, Val Acc: 0.9525

可以看到,仅用5个epoch,验证准确率已突破95%,说明模型具备较强判别能力。

模型评估与生产部署

训练完成后,我们需要独立评估模型性能,并准备部署方案。

性能评估函数

def evaluate_model(model_path): model = ResNet50(num_classes=1) model_state_dict = paddle.load(model_path) model.set_state_dict(model_state_dict) model.eval() valid_loader = valid_data_loader(DATADIR_VALID, CSVFILE, batch_size=10) accuracies = [] losses = [] bce_loss = nn.BCEWithLogitsLoss() with paddle.no_grad(): for batch in valid_loader(): x, y = batch x = paddle.to_tensor(x) y = paddle.to_tensor(y) logits = model(x) pred = paddle.sigmoid(logits) acc = (pred.round() == y).astype('float32').mean() loss = bce_loss(logits, y) accuracies.append(acc.numpy()[0]) losses.append(loss.numpy()[0]) print(f"Evaluation Result: " f"Accuracy={np.mean(accuracies):.4f}, " f"Average Loss={np.mean(losses):.4f}")

运行:

evaluate_model('resnet50_pm.pdparams')

预期结果:

Evaluation Result: Accuracy=0.9525, Average Loss=0.1342

模型保存与加载

PaddlePaddle 支持完整的参数序列化:

# 保存 paddle.save(model.state_dict(), 'model.pdparams') paddle.save(optimizer.state_dict(), 'opt.pdopt') # 加载 state_dict = paddle.load('model.pdparams') model.set_state_dict(state_dict)

导出为推理模型

若要在生产环境部署(如 Web 服务、移动端、嵌入式设备),建议导出为静态图格式,利用 Paddle Inference 提升推理效率:

from paddle.static import InputSpec net = ResNet50(num_classes=1) net.eval() x_spec = InputSpec(shape=[None, 3, 224, 224], dtype='float32', name='input') paddle.jit.save(net, 'inference/resnet50_pm', input_spec=[x_spec])

导出后会生成三个文件:
-__model__:网络结构
-__params__:模型权重
-infer_cfg.yml:配置信息

这些文件可通过 C++、Java、Python 等多种语言调用,支持 TensorRT、OpenVINO 等加速后端,在服务器、手机甚至边缘设备上高效运行。

写在最后

这套基于 PaddlePaddle 的眼疾图像分类方案,展示了如何从零开始完成一个完整的 AI 医疗项目:从数据清洗、模型搭建、训练监控到最终部署。整个过程流畅自然,得益于飞桨出色的 API 设计和中文生态支持。

当然,这只是一个起点。要真正达到临床可用水平,还需进一步优化:
- 使用 ImageNet 或更大规模医学图像上预训练的权重进行迁移学习;
- 引入 RandAugment、MixUp 等数据增强策略防止过拟合;
- 添加 Grad-CAM 可视化热力图,帮助医生理解模型决策依据;
- 集成 Paddle Serving 构建 RESTful API,供前端系统调用;
- 结合半监督学习,利用未标注数据进一步提升性能。

PaddlePaddle 不仅是一个工具,更是一套面向产业落地的解决方案体系。对于医疗AI这类高门槛、强合规性的场景,它的本土化优势、文档完备性和社区活跃度,使其成为极具竞争力的选择。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/1 0:11:31

LangFlow在CRM系统智能化升级中的价值

LangFlow在CRM系统智能化升级中的价值 在客户体验成为企业竞争核心的今天,如何让CRM系统真正“懂”客户,而不是仅仅记录客户信息,已成为数字化转型的关键命题。传统CRM依赖预设规则和人工介入处理客户请求,面对复杂多变的服务场景…

作者头像 李华
网站建设 2026/4/4 2:20:40

用Qwen3-VL-8B实现低成本视频理解

用Qwen3-VL-8B实现低成本视频理解 你有没有遇到过这种情况:用户上传了一段操作录屏,你想快速知道“他卡在哪个步骤了”;或者品牌方给了一条60秒的产品视频,你希望自动提炼出卖点文案,而不是逐帧看、手动记?…

作者头像 李华
网站建设 2026/3/26 12:10:53

Langchain-Chatchat 0.3.0保姆级部署指南

Langchain-Chatchat 0.3.0 部署实战:从零构建私有化知识问答系统 在企业级 AI 应用中,如何安全、高效地将大模型与内部知识库结合,已成为技术选型的关键。Langchain-Chatchat 自开源以来,凭借其对中文场景的深度优化和灵活的架构…

作者头像 李华
网站建设 2026/3/24 7:18:50

ComfyUI常用节点及安装避坑指南

ComfyUI常用节点及安装避坑指南 在AI图像生成的工具版图中,WebUI(A1111)像是一台功能齐全的“傻瓜相机”——点一下就能出图;而 ComfyUI 更像是专业摄影师手中的模块化单反系统:每一个组件都可拆卸、组合、精确调控。…

作者头像 李华
网站建设 2026/4/3 14:30:18

vLLM-Ascend部署Qwen3-Next大模型指南

vLLM-Ascend 部署 Qwen3-Next 大模型实战指南 在当前企业级大模型推理场景中,如何在保证高吞吐、低延迟的同时充分利用国产算力平台的性能潜力,已成为AI基础设施建设的关键挑战。华为 Ascend 910B(Atlas A2/A3 系列)凭借其强大的N…

作者头像 李华
网站建设 2026/4/2 18:50:23

Dify智能体平台部署全攻略:快速搭建企业级AI应用

Dify智能体平台部署全攻略:快速搭建企业级AI应用 在企业纷纷拥抱大模型的今天,一个现实问题摆在面前:如何让非算法背景的团队也能高效构建稳定、可维护的AI应用?很多公司尝试从零开始用LangChain或LlamaIndex写代码搭建RAG系统&am…

作者头像 李华