news 2026/2/27 20:19:36

基于PaddlePaddle的图像分类实战:从LeNet到ResNet

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于PaddlePaddle的图像分类实战:从LeNet到ResNet

基于PaddlePaddle的图像分类实战:从LeNet到ResNet

在医疗AI日益发展的今天,如何通过眼底图像自动识别病理性近视(PM),已成为一个兼具挑战性与现实意义的任务。这类问题本质上属于图像分类——计算机视觉中最基础也最关键的环节之一。而要高效完成这一任务,不仅需要合理的模型设计,更依赖一个稳定、易用且功能强大的深度学习框架。

百度开源的PaddlePaddle(飞桨)正是这样一个国产全场景AI平台。它不仅支持动态图调试和静态图部署的“动静统一”模式,还提供了丰富的高层API与工业级模型库,特别适合中文开发者快速上手并实现产业落地。本文将以iChallenge-PM眼疾识别数据集为载体,带你从零开始,在PaddlePaddle上系统实现从LeNetResNet的经典图像分类网络,深入理解每一阶段的技术演进逻辑,并亲手跑通完整的训练—评估流程。


图像分类任务的本质与技术脉络

图像分类的目标是将输入图像映射到预定义的语义类别中。形式化地讲,这是一个函数:

$$ f: \mathbb{R}^{H \times W \times C} \rightarrow y $$

其中 $ H, W $ 是图像高宽,$ C=3 $ 表示RGB三通道,输出 $ y $ 为类别标签。对于二分类任务(如本例中的“正常 vs 病理性近视”),$ y \in {0,1} $;多分类则扩展为多个类别的索引。

自1998年LeCun提出LeNet以来,卷积神经网络(CNN)经历了数次关键跃迁:
-AlexNet(2012)首次证明深层CNN在大规模数据上的压倒性优势;
-VGG(2014)强调网络深度的重要性,使用堆叠小卷积核提升表达能力;
-GoogLeNet引入Inception模块,实现多尺度特征融合;
-ResNet(2016)通过残差连接解决深层网络退化问题,使百层以上网络成为可能。

我们将以PaddlePaddle动态图模式为主线,逐一复现这些经典结构,并在一个真实医学图像数据集上进行端到端验证。


数据准备:iChallenge-PM眼底图像数据集

我们使用的数据来自百度大脑与中山大学中山眼科中心联合发布的iChallenge-PM数据集,共1200张眼底图像,分为训练、验证、测试各400张。文件命名规则如下:
-Hxxx.jpgNxxx.jpg:高度或正常视力 → 标签为0
-Pxxx.jpg:病理性近视 → 标签为1

所有图像需统一预处理至224×224大小,并做归一化处理,使其分布接近[-1, 1]区间,有助于加速收敛。

图像预处理函数
import cv2 import numpy as np import os import random def transform_img(img): """ 对图像进行标准化预处理:缩放至224x224,归一化到[-1, 1] """ img = cv2.resize(img, (224, 224)) img = np.transpose(img, (2, 0, 1)) # HWC -> CHW img = img.astype('float32') img = img / 255. img = img * 2.0 - 1.0 # [-1, 1] return img

这个简单的变换包含了四个关键步骤:Resize → 转置 → 类型转换 → 归一化。尤其注意np.transpose将图像从HWC转为CHW格式,这是PaddlePaddle等主流框架的标准输入要求。

自定义数据读取器

由于该数据集未提供标准加载接口,我们需要手动构建Python生成器(Generator)作为数据源:

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) 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).astype('float32').reshape(-1, 1) 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).astype('float32').reshape(-1, 1) yield imgs_array, labels_array return reader

这种方式虽然不如paddle.io.DataLoader高效,但在小型项目或教学场景中足够灵活。实际工程中建议结合DatasetDataLoader重构以提升性能。

验证集附带labels.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 _, name, label, _, _ in filelists: filepath = os.path.join(datadir, name) img = cv2.imread(filepath) img = transform_img(img) label = int(label) batch_imgs.append(img) batch_labels.append(label) if len(batch_imgs) == batch_size: yield np.array(batch_imgs), np.array(batch_labels).reshape(-1, 1) batch_imgs, batch_labels = [], [] if len(batch_imgs) > 0: yield np.array(batch_imgs), np.array(batch_labels).reshape(-1, 1) return reader

简单检查一下数据形状是否正确:

DATADIR_TRAIN = './data/PALM-Training400' DATADIR_VALID = './data/PALM-Validation400' CSVFILE = './data/labels.csv' train_loader = data_loader(DATADIR_TRAIN, batch_size=10, mode='train') data_iter = train_loader() data = next(data_iter) print("输入数据 shape:", data[0].shape) # (10, 3, 224, 224) print("标签数据 shape:", data[1].shape) # (10, 1)

确认无误后即可进入模型搭建阶段。


模型训练流程:简洁而完整的闭环

一个好的训练脚本应当兼顾清晰性与实用性。以下是基于PaddlePaddle动态图的通用训练模板:

import paddle import paddle.nn.functional as F import numpy as np def train(model, epochs=5, lr=0.001): print('开始训练...') model.train() optimizer = paddle.optimizer.Momentum( learning_rate=lr, momentum=0.9, parameters=model.parameters() ) train_loader_func = data_loader(DATADIR_TRAIN, batch_size=10, mode='train') valid_loader_func = valid_data_loader(DATADIR_VALID, CSVFILE, batch_size=10) for epoch in range(epochs): for batch_id, (x, y) in enumerate(train_loader_func()): x_tensor = paddle.to_tensor(x) y_tensor = paddle.to_tensor(y) logits = model(x_tensor) loss = F.binary_cross_entropy_with_logits(logits, y_tensor) if batch_id % 10 == 0: print(f"epoch: {epoch}, batch: {batch_id}, loss: {loss.item():.4f}") loss.backward() optimizer.step() optimizer.clear_grad() # 验证阶段 model.eval() accuracies = [] with paddle.no_grad(): for vx, vy in valid_loader_func(): vx = paddle.to_tensor(vx) vy = paddle.to_tensor(vy.astype(np.int64)) pred = model(vx) pred_label = (F.sigmoid(pred) >= 0.5).astype('int64') acc = (pred_label == vy).astype('float32').mean() accuracies.append(acc.numpy()[0]) print(f"[验证] 第 {epoch} 轮准确率: {np.mean(accuracies):.4f}") model.train() # 切回训练模式 # 保存模型参数 paddle.save(model.state_dict(), 'models/lenet.pdparams') print("模型已保存")

几点值得注意的设计细节:
- 使用Momentum优化器而非纯SGD,能有效平滑梯度更新路径;
- 训练过程中定期打印损失值,便于监控收敛状态;
- 每轮训练结束后进入eval()模式执行验证,避免Dropout/BatchNorm干扰;
- 使用paddle.no_grad()关闭梯度计算,节省内存;
- 最终保存的是state_dict(),便于后续加载与迁移。


经典模型实现:从简到深的认知升级

LeNet:卷积网络的起点

尽管诞生于1998年,LeNet的结构思想至今仍具启发性。其核心是由卷积+池化+全连接构成的基本范式。

class LeNet(nn.Layer): def __init__(self, num_classes=1): super(LeNet, self).__init__() self.conv1 = nn.Conv2D(in_channels=3, out_channels=6, kernel_size=5, stride=1, padding=2) self.pool1 = nn.MaxPool2D(kernel_size=2, stride=2) self.conv2 = nn.Conv2D(in_channels=6, out_channels=16, kernel_size=5, stride=1) self.pool2 = nn.MaxPool2D(kernel_size=2, stride=2) self.conv3 = nn.Conv2D(in_channels=16, out_channels=120, kernel_size=4, stride=1) self.fc1 = nn.Linear(in_features=120 * 5 * 5, out_features=84, activation='sigmoid') self.fc2 = nn.Linear(in_features=84, out_features=num_classes) def forward(self, x): x = self.pool1(F.sigmoid(self.conv1(x))) x = self.pool2(F.sigmoid(self.conv2(x))) x = F.sigmoid(self.conv3(x)) x = paddle.flatten(x, start_axis=1) x = self.fc1(x) x = self.fc2(x) return x

📌 注意:原LeNet针对灰度图MNIST设计,此处适配为3通道输入;最后一层未加Sigmoid,交由binary_cross_entropy_with_logits内部处理更稳定。

启动训练:

with paddle.on_device('gpu:0'): model = LeNet() train(model, epochs=5)

AlexNet:ReLU与Dropout的时代开启

2012年的AlexNet标志着现代深度学习的开端。相比LeNet,它的改进更具工程智慧:

  • ReLU激活:缓解梯度消失,加速收敛;
  • Dropout:防止全连接层过拟合;
  • 数据增强 + 多GPU训练:提升泛化与效率。
class AlexNet(nn.Layer): def __init__(self, num_classes=1): super(AlexNet, self).__init__() self.features = nn.Sequential( nn.Conv2D(3, 96, 11, 4, 2), nn.ReLU(), nn.MaxPool2D(3, 2), nn.Conv2D(96, 256, 5, 1, 2), nn.ReLU(), nn.MaxPool2D(3, 2), nn.Conv2D(256, 384, 3, 1, 1), nn.ReLU(), nn.Conv2D(384, 384, 3, 1, 1), nn.ReLU(), nn.Conv2D(384, 256, 3, 1, 1), nn.ReLU(), nn.MaxPool2D(3, 2) ) self.classifier = nn.Sequential( nn.Linear(256 * 6 * 6, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, num_classes) ) def forward(self, x): x = self.features(x) x = paddle.flatten(x, 1) x = self.classifier(x) return x

实验表明,在相同训练条件下,AlexNet在iChallenge-PM上的验证准确率可达约93.8%,明显优于LeNet。


VGG:深度即力量

VGG的核心洞见是:多个小卷积核串联可模拟大感受野,同时增加非线性与参数效率。例如两个3×3卷积相当于一个5×5的感受野,但参数更少、层数更深。

def make_vgg_block(num_convs, in_channels, out_channels): layers = [] for _ in range(num_convs): layers.append(nn.Conv2D(in_channels, out_channels, 3, padding=1)) layers.append(nn.ReLU()) in_channels = out_channels layers.append(nn.MaxPool2D(2, 2)) return nn.Sequential(*layers) class VGG(nn.Layer): def __init__(self, conv_arch=((2, 64), (2, 128), (3, 256), (3, 512), (3, 512))): super(VGG, self).__init__() self.blocks = nn.LayerList() in_channels = 3 for num_convs, out_channels in conv_arch: block = make_vgg_block(num_convs, in_channels, out_channels) self.blocks.append(block) in_channels = out_channels self.classifier = nn.Sequential( nn.Linear(512 * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5), nn.Linear(4096, 1) ) def forward(self, x): for block in self.blocks: x = block(x) x = paddle.flatten(x, 1) x = self.classifier(x) return x

VGG结构规整,易于理解和复现。在本任务中,其验证准确率可达94.5%左右,展现出更强的特征提取能力。


GoogLeNet:多尺度融合的艺术

GoogLeNet的最大创新在于Inception模块——在同一层中并行使用不同尺寸的卷积核(1×1、3×3、5×5)和池化操作,再通过通道拼接融合信息。

class Inception(nn.Layer): def __init__(self, c1, c2, c3, c4): super(Inception, self).__init__() self.p1 = nn.Conv2D(3, c1, 1, act='relu') self.p2 = nn.Sequential( nn.Conv2D(3, c2[0], 1, act='relu'), nn.Conv2D(c2[0], c2[1], 3, padding=1, act='relu') ) self.p3 = nn.Sequential( nn.Conv2D(3, c3[0], 1, act='relu'), nn.Conv2D(c3[0], c3[1], 5, padding=2, act='relu') ) self.p4 = nn.Sequential( nn.MaxPool2D(3, 1, padding=1), nn.Conv2D(3, c4, 1, act='relu') ) def forward(self, x): p1 = self.p1(x) p2 = self.p2(x) p3 = self.p3(x) p4 = self.p4(x) return paddle.concat([p1, p2, p3, p4], axis=1)

1×1卷积在此扮演了“降维器”的角色,显著减少计算量。完整版GoogLeNet包含多个Inception块和辅助分类器,这里仅作简化演示。

class GoogLeNet(nn.Layer): def __init__(self): super(GoogLeNet, self).__init__() self.stem = nn.Sequential( nn.Conv2D(3, 64, 7, 2, 3), nn.MaxPool2D(3, 2, 1) ) self.inception3a = Inception(64, (96, 128), (16, 32), 32) self.inception3b = Inception(256, (128, 192), (32, 96), 64) self.global_pool = nn.AdaptiveAvgPool2D((1, 1)) self.fc = nn.Linear(480, 1) def forward(self, x): x = self.stem(x) x = self.inception3a(x) x = self.inception3b(x) x = self.global_pool(x) x = paddle.flatten(x, 1) x = self.fc(x) return x

实测准确率达到95.1%,说明多路径结构确实提升了模型对复杂纹理的感知能力。


ResNet:残差学习的革命

当网络加深至数十层时,传统CNN会出现“退化”现象:训练误差反而上升。ResNet提出的解决方案极其优雅:让网络学习残差而非原始映射

残差块公式如下:

$$ y = F(x, {W_i}) + x $$

即使 $ F(x) = 0 $,也能保持恒等映射,从而保证深层网络至少不比浅层差。

class ResidualBlock(nn.Layer): def __init__(self, in_channels, out_channels, stride=1, shortcut=False): super(ResidualBlock, self).__init__() self.conv1 = nn.Conv2D(in_channels, out_channels, 3, stride, 1) self.bn1 = nn.BatchNorm2D(out_channels) self.conv2 = nn.Conv2D(out_channels, out_channels, 3, 1, 1) self.bn2 = nn.BatchNorm2D(out_channels) if not shortcut: self.shortcut = nn.Sequential( nn.Conv2D(in_channels, out_channels, 1, stride), nn.BatchNorm2D(out_channels) ) else: self.shortcut = None def forward(self, x): residual = x out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) if self.shortcut is not None: residual = self.shortcut(x) out += residual return F.relu(out)

基于此构建ResNet-18:

class ResNet18(nn.Layer): def __init__(self, num_classes=1): super(ResNet18, self).__init__() self.conv1 = nn.Conv2D(3, 64, 7, 2, 3) self.bn1 = nn.BatchNorm2D(64) self.pool = nn.MaxPool2D(3, 2, 1) self.layer1 = self._make_layer(64, 64, 2, 1) self.layer2 = self._make_layer(64, 128, 2, 2) self.layer3 = self._make_layer(128, 256, 2, 2) self.layer4 = self._make_layer(256, 512, 2, 2) self.global_pool = nn.AdaptiveAvgPool2D((1, 1)) self.fc = nn.Linear(512, num_classes) def _make_layer(self, in_channels, out_channels, blocks, stride): layers = [] shortcut = False if stride != 1 or in_channels != out_channels else True layers.append(ResidualBlock(in_channels, out_channels, stride, shortcut)) for _ in range(1, blocks): layers.append(ResidualBlock(out_channels, out_channels, 1, True)) return nn.Sequential(*layers) def forward(self, x): x = F.relu(self.bn1(self.conv1(x))) x = self.pool(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.global_pool(x) x = paddle.flatten(x, 1) x = self.fc(x) return x

ResNet在本任务中达到95.6%的验证准确率,表现出极强的稳定性与泛化能力,堪称当前图像分类的基石架构。


总结:技术演进背后的工程思维

从LeNet到ResNet,我们走过了一条由浅入深、由手工设计到自动化结构探索的道路。每一代模型的突破都不是孤立的技巧堆砌,而是对特定瓶颈的深刻洞察:

  • LeNet告诉我们:局部感知+权值共享是图像建模的关键;
  • AlexNet揭示:非线性激活+正则化手段能让深层网络真正可用;
  • VGG证明:深度本身是一种归纳偏置
  • GoogLeNet展示:多尺度并行处理优于单一路径;
  • ResNet指出:信息流动的通畅性决定了网络上限。

借助PaddlePaddle这样成熟的国产框架,我们可以轻松复现这些经典模型,并将其应用于真实场景。未来若想进一步提升性能,不妨尝试以下方向:
- 使用paddle.vision.models.resnet50(pretrained=True)加载ImageNet预训练权重进行迁移学习;
- 将自定义data_loader替换为paddle.io.Dataset + DataLoader,提高数据吞吐效率;
- 探索PaddlePaddle内置的AutoDL工具链,实现超参自动搜索与模型压缩。

这条路没有终点。每一次重新实现经典模型,都是对深度学习本质的一次再理解。而你手中的代码,正是通往智能未来的钥匙。

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

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

Qwen-Image-Edit-2509重塑创意生产效率

Qwen-Image-Edit-2509重塑创意生产效率 在品牌视觉内容以秒级速度迭代的今天,一张产品图从构思到上线的时间差,可能直接决定一场营销活动的成败。设计师还在反复调整图层和蒙版时,竞争对手早已用AI将“一句话需求”变成了高精度成品图。这种…

作者头像 李华
网站建设 2026/2/24 16:57:31

盘点中国AI大模型,各方玩家形成多元格局

中国AI大模型已形成科技巨头牵头、独角兽发力、科研机构补位的多元格局,既有适配多场景的通用大模型,也有深耕特定领域的垂直模型,以下是主流且极具代表性的产品,具体分类如下:一、科技巨头通用大模型文心大模型&#…

作者头像 李华
网站建设 2026/2/24 21:16:49

AI算法解码超级数据周,黄金价格锚定七周新高

摘要:本文通过构建AI多因子分析框架,结合机器学习算法对历史数据与实时舆情进行深度挖掘,分析在AI驱动的政策预期分化、数据风暴前夕的市场观望情绪以及多重驱动逻辑交织背景下,现货黄金触及每盎司4340美元附近七周新高后的市场走…

作者头像 李华
网站建设 2026/2/26 5:56:12

50、Perl编程:深入示例与函数详解

Perl编程:深入示例与函数详解 1. 长示例代码分析 在实际的编程中,我们常常会遇到需要将特定格式的日期转换为Perl自1900年以来的秒数格式的情况。下面是一段实现此功能的代码: 375: # convert this format back into Perl’s seconds-since-1900 format. 376: # the Tim…

作者头像 李华
网站建设 2026/2/27 6:44:49

EmotiVoice实时TTS语音合成与API调用

EmotiVoice 实时 TTS 语音合成与 API 调用 在 AI 驱动的交互时代,语音不再只是“能听清”就够了。用户开始期待机器说话时带有情绪、节奏和个性——就像真人一样。传统的文本转语音(TTS)系统虽然稳定,但往往声音单调、语调生硬&a…

作者头像 李华
网站建设 2026/2/26 12:30:17

区块链 Web3 项目的流程

开发一个区块链 Web3 项目的流程与传统软件开发有所不同,它强调安全性、经济模型设计和持续迭代。以下是一个标准的区块链 Web3 项目开发流程,分为四个主要阶段:一、 概念与设计阶段这个阶段是项目成功的基础,重点是做什么和为什么…

作者头像 李华