Paddle+ResNet蝴蝶识别实战:从数据加载到分类优化全解析
发布时间:2026/10/10 12:56:19 锦皓数字建站

简介这份基于Paddle框架与ResNet残差网络的蝴蝶种类识别与分类项目是面向高校课程设计和期末大作业的完整源码包。项目经导师指导并通过拿下97分高分代码完整、即下即用能够直接运行复现蝴蝶图像的分类识别流程特别适合深度学习初学者作为实战参考。压缩包中一共有14个文件一个Python脚本承担数据读取、模型构建、训练评估和分类预测等主流程两个Markdown文档分别提供项目说明与使用指引一个TXT文件对数据集结构做了说明另附十张PNG图片展示不同类别蝴蝶的识别结果以及训练过程中的可视化输出方便对照检查模型效果。整个资源包仅3.29MB体积轻巧、目录清晰便于快速下载、运行和二次开发。目前已有177人学习使用对希望从零搭建图像分类项目或熟悉Paddle与残差网络实操的学习者来说这是一份值得参考的高分完整样例。1. 一套能跑的蝴蝶识别源码不是让你看懂的是让你改得动的如果你手头正卡在一个图像分类课程设计或毕设上又不想从零搭网络、不想看着精度停在百分之六十几干瞪眼那这个「python基于Paddle框架ResNet残差网络的蝴蝶种类识别和分类项目源码」大概率就是你想要的那包东西。说白了它做的是这样一件事输入一张蝴蝶照片模型输出它属于哪个种类。听起来像入门级的图像分类但真正上手做过的人都知道蝴蝶识别是个特别能暴露问题的任务——类间差异小、背景干扰大、样本数量还不均衡拿个随手写的CNN去训验证集loss能给你抖成心电图。选Paddle而不是PyTorch对国内学生和开发者来说最大的理由是省心安装一条命令搞定GPU版本对国产显卡的适配也做得早代码风格跟PyTorch极像几乎可以零成本平移过去。而ResNet在这类项目里的地位也很明确——它不是最花哨的网络但它是残差结构里最经得起折腾的。源码项目一般不会只丢一个训练脚本通常会把数据处理、模型定义、训练验证、预测脚本都拆开你所需要做的不是把它跑通就交差而是把它改成你能讲清楚每一步逻辑的作品。这套东西能不能高分取决于你投入了多少理解和调整。2. 蝴蝶识别的数据到底要怎么喂给ResNet从图片目录到DataLoader2.1 先看你手里的数据是什么形态分类项目最常用的两种组织方式拿到源码第一件事不是急着改模型而是先看清楚数据集是怎么摆的。绝大多数Paddle分类项目的源码默认支持两种数据组织方式。第一种是txt格式的标签文件每一行左边是图片路径右边是类别编号这种格式在PaddleClas的老项目里特别常见第二种是ImageNet风格的一级目录按类别名分好每个类别的图片放在对应文件夹里。# 第一种txt标注格式常见于PaddleClas系列源码 dataset/ train_list.txt # 内容形如: images/001.jpg 0 val_list.txt # 内容形如: images/002.jpg 1 # 第二种文件夹分类格式适合自己收集图片 dataset/ train/ banded_peacock/ img_1.jpg monarch/ img_2.jpg val/ banded_peacock/源码里一般会有一个data_reader.py或utils.py负责把这两种格式统一转成Paddle的Dataset对象。如果你手里只有按文件夹分好类的图片我一般会建议直接写一个三行脚本把类别文件夹扫一遍生成txt标注而不要去改写源码的Dataset逻辑——因为改写Dataset意味着你同时要动数据增强、shuffle、num_workers这些后续逻辑很容易把训练节奏搞乱。2.2 用Paddle的Dataset子类封装蝴蝶图片核心代码与参数含义不管源码怎么组织你最终都要面对这段核心逻辑定义一个继承paddle.io.Dataset的类实现__init__、__getitem__和__len__三个方法。这是Paddle训练流程的地基也是你改数据增强、改输入尺寸、改归一化参数唯一需要动的地方。import paddle from paddle.io import Dataset from PIL import Image import numpy as np import os class ButterflyDataset(Dataset): def __init__(self, data_dir, label_file, transformNone, is_trainTrue): super().__init__() self.data_dir data_dir self.transform transform self.is_train is_train self.samples [] with open(label_file, r) as f: for line in f.readlines(): img_path, label line.strip().split() self.samples.append((os.path.join(data_dir, img_path), int(label))) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] img Image.open(img_path).convert(RGB) # 注意统一转RGB避免灰度图通道报错 if self.transform: img self.transform(img) # 归一化到[0,1]Paddle默认的ToTensor会做这件事 img np.array(img).transpose([2, 0, 1]) / 255.0 img img.astype(float32) label np.array([label], dtypeint64) return img, label逻辑说明这段代码的核心其实只有两件事——把图片路径和标签读进内存然后在__getitem__这一步把PIL对象变成Paddle训练的Tensor格式。注意transpose那一步PIL读出来的图片shape是HWC而Paddle卷积层默认的输入格式是CHW不转的话后面模型直接报维度错误。label包了一层np.array([label])是为了让label保持一个带有shape的Tensor避免标量和Paddle计算图之间出现shape不匹配。参数说明里有三个地方要盯紧第一convert(RGB)很重要蝴蝶数据集中偶尔会混入灰度图或RGBA图不统一处理会在训练中段突然崩掉第二除以255是对像素做归一化有些源码会写mean/std归一化那个要看你用的预训练模型是什么规格第三astype(float32)能强制统一数据类型避免CPU端int和float混用触发警告。你自己改的时候最常动的一个参数是输入尺寸也就是后面transform里的resize/crop目标尺寸ResNet系列的常见选择是224x224。2.3 数据增强蝴蝶识别能不能上70%准确率一半看这里很多新手拿着源码直接跑发现val准确率卡在60%出头就以为是模型不行其实问题大概率出在数据增强太弱或者太强。蝴蝶图片有个典型特征训练集和验证集往往来自不同拍摄环境如果只做简单的resize模型会死记背景而不是蝴蝶本身。常见的做法是在训练集上开RandomHorizontalFlip、RandomCrop和ColorJitter但幅度不能过大。import paddle.vision.transforms as T train_transform T.Compose([ T.RandomResizedCrop(224, scale(0.6, 1.0)), # 随机裁剪缩放区域模拟不同距离拍摄 T.RandomHorizontalFlip(0.5), # 水平翻转蝴蝶姿态左右对称天然适合 T.ColorJitter(brightness0.2, contrast0.2, saturation0.2), # 光线抖动 ]) val_transform T.Compose([ T.Resize(256), T.CenterCrop(224), # 验证集不做随机增强只做等比缩放后中心裁剪 ])这段transform的设计逻辑是训练集用RandomResizedCrop而不是普通Resize因为蝴蝶在照片里的占比变化很大固定尺寸裁剪会让模型对目标的尺度变化非常敏感。ColorJitter的三个值我一般控制在0.2以内调得太大等于给数据加噪声模型在真实图片上反而会掉点。验证集始终只用CenterCrop这样才能保证每次评估的输入是一致的指标才可对比。你在源码里大概率会看到一个train_transform和一个val_transform的区分如果你发现源码两个transform写得一样这本身就是一个值得改的优化点。3. ResNet残差网络在Paddle里的正确打开方式预训练权重与模型组网3.1 为什么是ResNet而不是VGG或EfficientNet残差结构对细分类的隐性价值蝴蝶种类识别属于细粒度图像分类类间差异集中在翅膀纹理、斑点和边缘结构上。这种任务对网络深度很敏感但单纯堆层数会导致梯度消失训练集loss降不下去、验证集更是一塌糊涂。ResNet的残差连接核心操作是让某一层的输入直接跨层加到输出上这样梯度可以绕过中间层直接回传网络做到50层以上依然能被正常训练。你在源码里看到ResNet50这种命名数字代表的是卷积层的总深度50层的ResNet在蝴蝶这种中等规模数据集上通常已经是性价比最高的配置——比ResNet18表达能力强又不像ResNet101那么容易过拟合。Paddle里加载ResNet模型的方式和PyTorch极为相似源码通常会直接用paddle.vision.resnet50这个接口获取一个预训练模型。预训练权重是在ImageNet上训出来的虽然ImageNet里没有蝴蝶的专门类别但这个权重已经学到了大量关于纹理、边缘、颜色分布的通用特征。直接拿这些特征做初始化可比你从零开始训收敛快得多这也是热词里「预训练模型」频繁出现的原因。3.2 替换分类头把ImageNet的1000类输出改成你的蝴蝶种类数这是整个源码里最核心的改动之一也是最容易被忽略的坑。预训练ResNet50的输出层是1000维对应ImageNet的1000个类别而你的蝴蝶数据集可能只有10类或20类。如果不替换最后一层全连接模型输出维度不匹配直接报错如果只是简单删掉最后一层而不重新初始化模型前面所有层的特征都浪费了。import paddle import paddle.nn as nn from paddle.vision.models import resnet50 def build_model(num_classes, use_pretrainedTrue): # 获取预训练模型pretrainedTrue会下载ImageNet权重并加载 model resnet50(pretraineduse_pretrained) # 删除原有分类头 model.fc nn.Sequential( nn.Linear(2048, 512), # ResNet50的倒数第二层特征维度是2048 nn.ReLU(), nn.Dropout(0.5), # 防止小数据集过拟合Dropout比例可调 nn.Linear(512, num_classes) ) return model逻辑说明model.fc是ResNet50在Paddle里的分类头属性名它的输入维度是2048也就是前面卷积部分输出的特征通道数。我把原来的单层全连接换成了一个小型多层感知器加上ReLU和Dropout这样做的目的是在特征和最终分类之间加入一层非线性映射。蝴蝶种类少则几类、多则几十类两层全连接已经足够拟合不需要再加更深的分类头。参数说明Dropout(0.5)是中等强度如果你发现训练集准确率接近100%、验证集却上不去可以把Dropout调到0.6或0.7反过来如果欠拟合就降到0.3。512这个中间维度也可以调但一般没有太大必要。num_classes是你数据集里的蝴蝶类别总数这个一定要拿标签文件里的最大值1来算或者数一下文件夹数量改错的话训练时Paddle不会报错但准确率会一直停在很低的水平。3.3 训练脚本的核心配置学习率、batch大小和epoch之间的配合搭建好模型之后训练脚本里最需要关注的是优化器配置和训练循环。源码里大概率已经有一套默认配置但默认值往往不适合你的具体数据。蝴蝶数据集如果只有几千张图batch大小最好不要超过32learning rate从0.001开始是比较稳妥的。很多人直接上了PyTorch默认的0.01或者0.1然后发现loss爆炸这是最常见的翻车现场。import paddle.nn.functional as F # 配置优化器、损失函数与评估指标 optimizer paddle.optimizer.Adam( parametersmodel.parameters(), learning_rate0.001, weight_decay1e-4 ) criterion nn.CrossEntropyLoss() # 训练循环 model.train() for epoch in range(epochs): total_loss 0.0 for batch_id, (images, labels) in enumerate(train_loader): logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() optimizer.clear_grad() total_loss loss.numpy()[0] if batch_id % 20 0: avg_loss total_loss / (batch_id 1) print(fEpoch [{epoch1}/{epochs}], Batch [{batch_id}], Loss: {avg_loss:.4f})逻辑说明这段循环就是Paddle训练的标准架子loss.backward()计算梯度optimizer.step()更新参数optimizer.clear_grad()把梯度清零。注意Paddle和PyTorch的一个小差别PyTorch是backward之后在下一轮step之前需要手动zero_grad而Paddle的clear_grad要放在step之后写顺序写反会导致梯度累积训练曲线出现周期性抖动。参数说明Adam优化器在分类任务里是默认选择相比SGD它不需要手动调momentum对学习率的容忍范围更大。learning_rate0.001是ResNet配合Adam最常用的起点如果训练三轮之后loss还在0.5以上波动可以先把学习率降到0.0003weight_decay1e-4起到轻微的L2正则作用数值调大会让模型参数被压得太小特征表达能力下降。epochs要看你的数据量和验证集表现通常30~50轮足够一个中等规模的蝴蝶数据集收敛。4. 从训练到落地模型评估、预测脚本与源码项目交付4.1 评估阶段不只盯着准确率混淆矩阵才是蝴蝶分类的验金石大部分源码项目的评估脚本只会输出一个top-1准确率也就是预测的类别和真实类别一致的样本占比。但对蝴蝶这种类间相似度极高的任务top-1准确率很容易给你虚假的信心——可能整体准确率有80%但某两个近似种类之间几乎全部误判。正确做法是生成混淆矩阵看看错误到底集中在哪几类上。from sklearn.metrics import confusion_matrix import numpy as np all_preds [] all_labels [] model.eval() with paddle.no_grad(): for images, labels in val_loader: logits model(images) preds paddle.argmax(logits, axis-1).numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) np.save(confusion_matrix.npy, cm)逻辑说明paddle.no_grad()在这个阶段是必须的它告诉Paddle不需要构建计算图能省下大量的显存和计算时间推理速度会有肉眼可见的提升。argmax取logits在类别维度上最大的索引值作为预测结果。生成的混淆矩阵是numpy数组保存成.npy文件后可以在本地用matplotlib画成热力图。实际项目里我会先看混淆矩阵的主对角线数值再关注非对角线上的大数值。如果你发现monarch和viceroy这两类经常互相混淆那说明这两类蝴蝶本身的视觉差异集中在非常细微的翅膀纹理上ResNet50用的224x224输入可能丢掉了这些细节。此时有两个思路一是把输入尺寸从224升到288甚至320让网络「看」到更多纹理细节代价是训练时间变长二是给这两类单独补训练样本或者用数据增强针对性地模拟翅膀形变。这些调整做下去项目答辩时能讲的内容就完全不一样了。4.2 单张图片预测脚本让源码项目从训练走向能演示的完整闭环训练完模型、评估完指标之后如果项目交上去只能跑训练脚本那这个源码的价值就大打折扣了。一个像样的蝴蝶识别源码至少应该再提供一个predict.py接收一张图片路径输出类别名和置信度。下面的脚本你可以直接照着改import paddle import numpy as np from PIL import Image import paddle.vision.transforms as T def predict_image(model, img_path, label_map, use_gpuTrue): # 设备设置检测到GPU才用GPU否则走CPU paddle.set_device(gpu if use_gpu and paddle.device.is_compiled_with_cuda() else cpu) model.eval() # 预测时的预处理必须和验证集一致不能使用训练时的随机增强 transform T.Compose([ T.Resize(256), T.CenterCrop(224), ]) img Image.open(img_path).convert(RGB) img transform(img) img np.array(img).transpose([2, 0, 1]) / 255.0 img np.expand_dims(img.astype(float32), axis0) # 加batch维度 img_tensor paddle.to_tensor(img) with paddle.no_grad(): logits model(img_tensor) probs paddle.nn.functional.softmax(logits, axis-1).numpy()[0] pred_idx int(np.argmax(probs)) confidence float(probs[pred_idx]) return label_map[pred_idx], confidence逻辑说明这段脚本的关键是和验证集保持完全相同的预处理流水线。训练时用的RandomResizedCrop不能出现在预测阶段因为推理时只需要从一张完整图片里稳定地提取中心区域特征。np.expand_dims是为了给单张图片加上batch这个维度因为Paddle模型默认接受四维输入(N, C, H, W)。softmax把输出logits转成概率分布取最大概率对应的索引再通过label_map映射回蝴蝶种类名。参数说明label_map是一个字典键是类别编号值是类别名称字符串。这个字典可以从训练集的类别文件夹顺序生成也可以手动维护。在AiStudio等环境里跑预测时注意把use_gpu参数和实际情况对上如果你的环境没有GPU训练时一样能跑但这个参数决定了Paddle是否启用CUDA。写完之后用三张训练集里的图和三张网上下载的蝴蝶图各测一次如果训练集里能识别对的图网上图识别不对那大概率是模型过拟合了需要回到增强和正则化那一章去调整。4.3 保存与导出什么时候用Layer.save什么时候只需要checkpoint源码项目通常会在训练每个epoch结束之后保存模型但新手经常会困惑保存的文件怎么用。Paddle里有两套保存体系一套是动态图模型权重用paddle.save(model.state_dict(), xxx.pdparams)这套保存的只是参数加载时要求你先把模型结构建出来另一套是训练checkpoint用paddle.save({model: model.state_dict(), optimizer: optimizer.state_dict()}, xxx.pdopt)这种保存了优化器状态适合中断后恢复训练。# 每个epoch结束后保存 if (epoch 1) % 5 0: paddle.save(model.state_dict(), fcheckpoints/butterfly_resnet50_epoch{epoch1}.pdparams)推断流程则是构建模型 → load权重 → 送入一张预处理后的图片 → 得到分类。如果你要把项目打包给别人直接用通常再走一步Paddle Inference导出生成推理模型和参数文件这样对方不需要安装Paddle也能用Paddle Lite或Paddle Inference跑识别。但对课程设计和毕设来说保存pdparams已经足够答辩现场不会要求你部署成服务。5. 蝴蝶识别项目的常见排查清单训练翻车、评估掉点这些坑我都踩过5.1 训练loss不降反升验证集一直是随机水平现象训练过程中loss在2.0~3.0之间震荡完全没有下降趋势验证集准确率稳定在1/类别数的水平和随机猜没区别。这个问题在新手项目里出现的概率极高。原因第一种是learning rate太大特别是用了SGD优化器时默认的0.01在ResNet50这种深网络上很容易让loss直接飞掉第二种是数据预处理出了问题最常见的错误是图片没有归一化就送进网络或者标签从0开始还是从1开始的约定没对齐第三种是预训练权重没有实际加载成功pretrained参数设了True但Paddle网络下载失败时静默使用了随机初始化权重这种情况下模型需要从头学习全部特征训练几十轮当然收敛不了。解决先检查数据预处理确认图片数值范围在0~1之间而不是0~255把这个写一个小断言打印出来。然后尝试把learning rate降到0.0005跑5个epoch如果loss还是纹丝不动就去检查预训练权重是否真的下载成功了看看默认缓存目录里有没有对应的.pdparams文件。5.2 训练集准确率接近100%验证集却只有60%现象训练进行到中间阶段时训练集上的准确率已经飙升到95%但每轮验证集准确率始终在60%~65%徘徊甚至出现验证集准确率下降。这是典型的小数据集过拟合信号。原因蝴蝶数据集通常只有几千张图片ResNet50的参数量高达两千多万模型完全可以靠硬背训练集图片来获取高训练准确率。导致过拟合的原因可能来自几个方面数据增强太弱、Dropout比例太低、或者训练轮数太多。很多源码默认训练50甚至100轮对于小数据集这属于过度训练。解决优先增加数据增强强度把RandomResizedCrop的scale下限从0.6调整到0.4同时把ColorJitter的saturation上限从0.2提高到0.3。然后把Dropout从0.5调到0.6最后用early stopping策略——每5轮看一次验证集准确率连续3次没有提升就停止训练或者直接减小epoch总数到30轮。我在项目里通常的做法是保留每个epoch的验证集快照最后回滚到验证集准确率最高的那个checkpoint而不是用最后一个epoch的权重。5.3 验证时预测结果和训练时相差巨大现象训练日志里验证集准确率有85%单独写了一个预测脚本拿同一张验证集图片丢进去预测结果直接错了。这个现象非常反直觉容易让人怀疑模型保存环节出了问题。原因预测脚本的预处理和训练时的验证集预处理不一致。最常见的情况是训练时验证集用了T.Resize(256)T.CenterCrop(224)预测脚本里却写成了T.Resize(224)或者漏了CenterCrop导致输入图片的尺寸或内容分布和模型期望的完全不同。还有一种情况是预测时忘了加batch维度Paddle把单张图片当成batch_size1来算但如果你的输入数据是HWC格式没有转成CHW视觉特征就会被彻底破坏。解决严格复用验证集里的transform定义不要重新写一遍。最好的办法是把transform定义单独放在一个函数里训练、验证、预测三个模块共用同一个函数。另外在预测脚本里打印一下预处理后Tensor的shape确认是(1, 3, 224, 224)再检查数值范围是否还是0~1。5.4 显存不够但想用ResNet50现象GPU显存只有4GB或6GBbatch_size设成32直接报OOMbatch_size降到8又感觉训练太慢。原因ResNet50在224x224输入下batch_size32大约需要8~9GB显存这是常见的显存瓶颈。但这个问题有时候不是单纯显存不够而是在你之前训练的环境里没有清空显存或者是用了默认的paddle.set_device(gpu)导致Paddle在GPU上分配了过多的缓存。解决先用nvidia-smi确认显存占用情况清掉残留进程。如果确实不够用有两个方向一是把batch_size降到16或8同时把学习率按比例调到0.0005二是换输入尺寸把训练分辨率从224降到192显存消耗会下降接近三分之一而蝴蝶识别的精度损失通常只在1~2个百分点。不要一开始就换ResNet18很多情况下减少batch_size配合梯度累积就可以解决问题。6. 往高分改用验证集反馈闭环和混淆矩阵把准确率再推上去作为一个完整能跑的源码项目跑通只是及格线高分的关键在于你如何从验证集反馈里找到模型的视觉盲区并针对性地调整。我惯用的方法是先记录验证集里被误判的样本把图片按「真实类别-预测类别」聚类然后可视化地查看这些样本的共性和模型给出的置信度分布。在蝴蝶识别项目里最容易出现的问题是两类蝴蝶的翅膀花纹在颜色上高度相似模型在这种样本上会给出接近50%的均匀概率分布。做这类细粒度优化的技巧是使用Top-2投票法对同一张验证集图片做五次不同的随机裁剪分别预测统计五个结果中类别出现频率最高者获胜。这个策略可以直接写进预测脚本让模型对同一张图给出多个视角的预测能有效降低单次裁剪带来的偶发误判。以下是这个技巧的最小实现def predict_with_voting(model, img, label_map, num_crops5): preds [] with paddle.no_grad(): for _ in range(num_crops): # 每次做随机裁剪模拟不同的视角 crop T.RandomResizedCrop(224, scale(0.7, 1.0))(img) crop_tensor paddle.to_tensor( np.array(crop).transpose([2, 0, 1]) / 255.0 ).unsqueeze(0) logits model(crop_tensor) pred int(paddle.argmax(logits, axis-1).numpy()[0]) preds.append(pred) final_pred max(set(preds), keypreds.count) return label_map[final_pred]这段代码是在现有模型基础上做推理期增强不需要重新训练成本极低。它背后的逻辑是把单次中心裁剪替换成多次随机裁剪让模型对同一张图片的不同局部区域分别判断最终按投票结果取最高频类别。对于笔记本摄像头拍的模糊蝴蝶图或者背景杂乱的照片这个技巧通常能把识别稳定性提升2~4个百分点。最后再强调一次我的血泪经验永远不要只保留训练结束时的那个checkpoint一定要每个epoch都存一份然后按验证集准确率挑最优权重。蝴蝶数据集本身就小训练后期验证集准确率往往会有几个点的波动最后一个epoch根本不一定是最优解。这个坏习惯我改了很久才改过来也是我所有Paddle项目里最想让人早点知道的一件事。希望这些经验能帮你把这个源码项目从「跑通」推到「高分」做完之后你会发现蝴蝶识别这件事远远没有看上去那么「玄学」。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。