资讯详情

基于Paddle与ResNet的蝴蝶种类识别实战:从训练到推理全流程

📅 2026/9/28 23:35:04 | 华诺云谱 👁 阅读
基于Paddle与ResNet的蝴蝶种类识别实战:从训练到推理全流程
简介这份资源是面向高校学生与深度学习入门者的蝴蝶种类识别与分类项目源码基于PaddlePaddle框架与ResNet残差网络实现可直接用于课程设计、期末大作业或图像分类练手场景。压缩包共14个文件以10张png训练过程与结果可视化图、2个md说明文档、1个txt数据集说明和1个py核心脚本为主整体约3.29MB结构清晰、便于快速定位代码与实验记录。项目已获导师指导并通过97分评审下载即用无需修改完整确保可运行。读者可从中获得完整的图像分类建模流程、ResNet残差结构在蝴蝶细粒度识别上的落地代码、数据集组织方式以及训练输出图所反映的调参参考适合作为深度学习课程实践与项目复现的参考模板。目前已有176人学习关注。1. 蝴蝶种类识别项目从 Paddle 到 ResNet 的落地路径蝴蝶种类识别听起来像是一个偏门的小众课题但真正动手做过的人会知道它几乎把细粒度图像分类的所有难点都踩了一遍类间差异极小、类内差异极大、背景干扰强、样本分布长尾。用 Python 加 Paddle 框架配合 ResNet 残差网络来做这个任务是一条被反复验证过的可靠路线。这个方案适合两类人一类是刚学完卷积网络基础、想找一个完整项目把训练到推理全流程跑通的学生另一类是需要快速搭建细粒度分类基线、再往上叠加注意力或特征融合模块的工程师。读完你应该能自己搭出一套可训练、可评估、可导出推理的蝴蝶分类流程并且知道哪些参数一动就会翻车。2. Paddle 环境搭建与蝴蝶数据集准备把地基打牢2.1 安装 Paddle 并验证 GPU 是否真正可用环境这一步是后面所有工作的前提。很多人装完 Paddle 直接开始写模型结果训练时发现用的是 CPU一个 epoch 跑半小时血泪经验告诉我们先验证 GPU再写一行模型代码。# 创建独立环境避免和系统 Python 冲突 conda create -n butterfly python3.9 -y conda activate butterfly # 安装 Paddle GPU 版本具体 CUDA 版本按自己驱动选 # 这里以 CUDA 11.2 为例pip 源用官方推荐 python -m pip install paddlepaddle-gpu2.5.2.post112 \ -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html # 验证安装 python -c import paddle; print(paddle.__version__)安装完成后不要急着跑模型先做一次 GPU 可用性检查import paddle # 打印版本和编译信息 print(Paddle 版本:, paddle.__version__) print(是否支持 GPU:, paddle.is_compiled_with_cuda()) # 真正把张量放到 GPU 上跑一次才算验证成功 if paddle.is_compiled_with_cuda(): paddle.set_device(gpu) x paddle.randn([2, 3]) y paddle.randn([3, 4]) z paddle.matmul(x, y) print(GPU 矩阵乘法结果形状:, z.shape) print(当前设备:, paddle.get_device()) else: print(当前是 CPU 版本训练会很慢建议重装 GPU 版)逻辑说明paddle.is_compiled_with_cuda()只说明这个包编译时带了 CUDA不代表运行时一定能用。真正靠谱的验证是paddle.set_device(gpu)之后做一次实际运算如果这一步不报错才说明驱动、CUDA、cuDNN 和 Paddle 版本是匹配的。参数上paddle.set_device可以传gpu:0指定某一块卡多卡场景下这个细节很关键。提示如果is_compiled_with_cuda()返回 False不要试图用pip install paddlepaddle的 CPU 包硬跑直接换对应 CUDA 版本的 GPU 包否则后面调 batch size 和学习率都是白费功夫。2.2 蝴蝶数据集的目录结构与划分策略蝴蝶数据集通常来自公开的细粒度分类数据常见做法是按物种分文件夹。Paddle 的paddle.vision.datasets.ImageFolder对目录结构有固定要求每个类别一个子目录图片放在对应目录下。butterfly_dataset/ ├── train/ │ ├── Papilio_bianor/ │ │ ├── img_0001.jpg │ │ └── ... │ ├── Papilio_machaon/ │ └── ... ├── val/ │ ├── Papilio_bianor/ │ └── ... └── test/ └── ...划分时有两个坑要提前避开。第一同一个体的多张照片不能同时出现在训练集和验证集否则验证准确率会虚高这是细粒度任务里最常见的评估失真来源。第二长尾类别要做处理要么在采样时给少数类更高权重要么在损失函数里加类别权重否则模型会偏向样本多的物种。import os import random import shutil def split_dataset(src_dir, dst_dir, train_ratio0.7, val_ratio0.15): 按类别划分数据集保证每个类别内部随机划分 src_dir: 原始按类别分好的目录 dst_dir: 输出目录会生成 train/val/test random.seed(42) # 固定随机种子保证可复现 classes os.listdir(src_dir) for cls in classes: cls_path os.path.join(src_dir, cls) if not os.path.isdir(cls_path): continue imgs [f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .png, .jpeg))] random.shuffle(imgs) n len(imgs) n_train int(n * train_ratio) n_val int(n * val_ratio) splits { train: imgs[:n_train], val: imgs[n_train:n_train n_val], test: imgs[n_train n_val:] } for split, files in splits.items(): out_dir os.path.join(dst_dir, split, cls) os.makedirs(out_dir, exist_okTrue) for f in files: shutil.copy(os.path.join(cls_path, f), os.path.join(out_dir, f)) print(划分完成) split_dataset(./raw_butterfly, ./butterfly_dataset)逻辑说明这个脚本按类别内部随机划分避免某个类别整体落到某一个 split 里。random.seed(42)是为了让每次划分结果一致方便复现实验。参数上train_ratio和val_ratio按数据量调整数据量少于每类 100 张时验证集比例可以降到 0.1把更多样本留给训练。注意如果原始数据里存在类别名称大小写不一致、空格或特殊字符ImageFolder会把它们当成不同类别训练前先用脚本统一清洗一遍目录名。3. ResNet 残差网络在 Paddle 中的实现与改造3.1 残差结构为什么适合蝴蝶细粒度分类残差网络的核心是跳跃连接它让梯度可以绕过若干卷积层直接回传从而把网络做深而不退化。蝴蝶识别属于细粒度分类浅层网络提取的边缘和纹理不够判别必须靠深层特征去区分翅膀斑纹的细微差异。ResNet 的 bottleneck 结构在深层仍然保持较好的梯度流这是它比普通 VGG 堆叠更适合这个任务的根本原因。Paddle 的paddle.vision.models里已经内置了 ResNet 系列常见做法是直接调用预训练权重再把最后的全连接层换成蝴蝶类别数。预训练模型在 ImageNet 上学到的通用纹理特征对蝴蝶翅膀这种高频细节有不错的迁移效果比从零训练收敛快很多。import paddle import paddle.nn as nn from paddle.vision.models import resnet50 class ButterflyResNet(nn.Layer): def __init__(self, num_classes20, pretrainedTrue): super().__init__() # 加载预训练 ResNet50去掉原始分类头 self.backbone resnet50(pretrainedpretrained) in_features self.backbone.fc.weight.shape[0] # 替换全连接层输出蝴蝶类别数 self.backbone.fc nn.Linear(in_features, num_classes) # 加一层 dropout缓解细粒度任务过拟合 self.dropout nn.Dropout(p0.3) def forward(self, x): # 取 backbone 除 fc 外的特征 feat self.backbone(x) feat self.dropout(feat) return feat # 实例化假设有 20 个蝴蝶物种 model ButterflyResNet(num_classes20, pretrainedTrue) print(model)逻辑说明resnet50(pretrainedTrue)会下载 ImageNet 预训练权重这一步需要网络能访问 Paddle 的权重服务器。in_features从原始 fc 层读取避免硬编码 2048 这个数字换 ResNet18 或 101 时不用改代码。Dropout(0.3)是细粒度任务的常用配置因为蝴蝶数据集通常每类只有几十到几百张过拟合风险高。参数说明num_classes必须和实际物种数一致多一个少一个都会在损失计算时报错。pretrained在调试阶段可以先设 False 加快启动正式训练时再打开。3.2 数据增强与 DataLoader 配置细粒度分类对数据增强很敏感。水平翻转、随机裁剪、颜色抖动是标配但垂直翻转要慎用因为蝴蝶翅膀的上下斑纹分布有方向性垂直翻转可能破坏判别特征。from paddle.vision.transforms import Compose, RandomResizedCrop, RandomHorizontalFlip, ColorJitter, Normalize, ToTensor from paddle.io import DataLoader from paddle.vision.datasets import ImageFolder train_transform Compose([ RandomResizedCrop(224, scale(0.7, 1.0)), # 随机裁剪并缩放到 224 RandomHorizontalFlip(p0.5), # 水平翻转 ColorJitter(brightness0.2, contrast0.2, saturation0.2), # 颜色扰动 Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), # ImageNet 统计量 ToTensor() ]) val_transform Compose([ RandomResizedCrop(224, scale(0.8, 1.0)), Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensor() ]) train_ds ImageFolder(./butterfly_dataset/train, transformtrain_transform) val_ds ImageFolder(./butterfly_dataset/val, transformval_transform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, drop_lastTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4)逻辑说明RandomResizedCrop的scale下限设 0.7是防止裁得太狠把整只蝴蝶裁没。Normalize用的均值和方差是 ImageNet 的统计量因为 backbone 是 ImageNet 预训练的输入分布要匹配。drop_lastTrue在训练集上避免最后一个不完整 batch 影响 BatchNorm 统计。参数说明batch_size32在 8G 显存上跑 ResNet50 加 224 输入基本是上限显存不够就降到 16 并同步调小学习率。num_workers在 Linux 上设 4 到 8Windows 上设 0 或 2否则容易卡在数据加载。4. 训练循环、学习率调度与模型评估4.1 损失函数与优化器的选择蝴蝶识别是单标签多分类交叉熵是默认选择。但前面提到长尾问题如果某些物种样本特别少可以在交叉熵里加类别权重。import paddle import paddle.nn as nn from paddle.optimizer import Adam from paddle.optimizer.lr import CosineAnnealingDecay # 假设已经统计出每个类别的样本数 class_counts [120, 95, 80, 60, 45, 30, 25, 20, 18, 15, 12, 10, 9, 8, 7, 6, 5, 4, 3, 2] total sum(class_counts) # 权重与样本数成反比样本越少权重越大 weights [total / (len(class_counts) * c) for c in class_counts] weights paddle.to_tensor(weights, dtypefloat32) criterion nn.CrossEntropyLoss(weightweights) # 余弦退火初始学习率 1e-3 scheduler CosineAnnealingDecay(learning_rate1e-3, T_max50) optimizer Adam(learning_ratescheduler, parametersmodel.parameters(), weight_decay1e-4)逻辑说明类别权重让少数类的损失贡献被放大模型不敢完全忽略它们。CosineAnnealingDecay的T_max设成总 epoch 数学习率从 1e-3 平滑降到接近 0比固定学习率更容易收敛到好的局部解。weight_decay1e-4是 Adam 上的常用正则太大欠拟合太小没效果。参数说明如果显存允许batch_size翻倍时学习率可以乘 1.5 到 2但不要直接乘 2容易震荡。T_max要和实际训练 epoch 对齐设小了学习率提前降到 0后面等于没在学。4.2 完整训练循环与验证指标训练循环里要同时记录训练损失和验证准确率验证集准确率才是判断模型好坏的标准。import paddle from paddle.metric import Accuracy def train_one_epoch(model, loader, optimizer, criterion, epoch): model.train() total_loss 0.0 acc Accuracy() for batch_id, (imgs, labels) in enumerate(loader): preds model(imgs) loss criterion(preds, labels) loss.backward() optimizer.step() optimizer.clear_grad() total_loss loss.item() acc.update(preds, labels) if batch_id % 20 0: print(fEpoch {epoch} batch {batch_id} loss {loss.item():.4f} acc {acc.accumulate():.4f}) return total_loss / len(loader), acc.accumulate() def evaluate(model, loader, criterion): model.eval() total_loss 0.0 acc Accuracy() with paddle.no_grad(): for imgs, labels in loader: preds model(imgs) loss criterion(preds, labels) total_loss loss.item() acc.update(preds, labels) return total_loss / len(loader), acc.accumulate() best_acc 0.0 for epoch in range(50): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, epoch) val_loss, val_acc evaluate(model, val_loader, criterion) scheduler.step() print(fEpoch {epoch}: train_loss {train_loss:.4f} train_acc {train_acc:.4f} fval_loss {val_loss:.4f} val_acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc paddle.save(model.state_dict(), best_butterfly_resnet.pdparams) print(f保存最佳模型val_acc {best_acc:.4f})逻辑说明model.train()和model.eval()切换会影响 Dropout 和 BatchNorm 的行为验证时必须切到 eval。paddle.no_grad()关闭梯度计算省显存也加速。scheduler.step()放在 epoch 末尾配合余弦退火按 epoch 更新。保存最佳模型而不是最后一个是因为过拟合后验证准确率会掉。参数说明Accuracy默认取 top-1细粒度任务也可以看 top-5把topk(1, 5)传进去。如果验证准确率震荡厉害先把学习率降一半再看。5. 避坑与排查蝴蝶识别项目里最容易翻车的几件事5.1 验证准确率虚高测试集一跑就崩现象验证集准确率能到 95%换测试集只有 60% 多。原因同一个体的多张照片被分到了训练和验证两边模型其实在背个体特征而不是学物种特征。解决划分时按个体 ID 分组确保同一个体的所有照片只出现在一个 split 里。如果数据没有个体 ID至少按拍摄时间或文件名前缀做分组。5.2 训练损失不降准确率卡在随机水平现象loss 一直在 3.0 附近准确率等于 1/类别数。原因最常见的是标签和输出维度对不上或者Normalize的均值和预训练模型不匹配。解决先打印一个 batch 的 labels 范围和 preds 形状确认num_classes一致再检查 transform 里的 mean/std 是不是 ImageNet 的统计量。如果用的是自定义预训练权重统计量要换成对应的。5.3 GPU 显存溢出batch size 一降就报错现象Out of memory报错把 batch size 从 32 降到 16 后仍然报错。原因Paddle 默认会缓存显存上一次的残留没释放。解决在训练脚本开头加paddle.device.cuda.empty_cache()或者用paddle.set_flags({FLAGS_allocator_strategy: auto_growth})让显存按需增长。另外检查是不是在验证时忘了no_grad验证阶段也会建计算图。5.4 数据增强把关键特征裁掉了现象训练准确率上不去可视化发现很多图只剩翅膀一角。原因RandomResizedCrop的 scale 下限设得太低或者用了过于激进的旋转。解决把 scale 下限提到 0.7 以上旋转角度限制在 ±15 度以内。蝴蝶的对称斑纹对旋转敏感大角度旋转会引入错误标签。5.5 预训练权重下载失败导致训练中断现象pretrainedTrue时卡在下载或者报连接错误。原因权重服务器访问不稳定。解决先手动下载权重文件放到~/.cache/paddle/weights/下再把pretrained设成权重文件的本地路径。如果实在下不下来先用pretrainedFalse跑通流程再换网络环境补下载。6. 从训练到推理导出模型与单张图片预测的完整技巧训练完拿到best_butterfly_resnet.pdparams只是半成品真正要用起来还得会导出和推理。Paddle 提供了动态图转静态图的接口导出后的模型可以脱离训练代码独立运行部署时更轻量。import paddle from paddle.static import InputSpec # 重新实例化模型结构加载训练好的权重 model ButterflyResNet(num_classes20, pretrainedFalse) state_dict paddle.load(best_butterfly_resnet.pdparams) model.set_state_dict(state_dict) model.eval() # 指定输入形状batch 维度用 None 表示可变 input_spec [InputSpec(shape[None, 3, 224, 224], dtypefloat32, nameimage)] paddle.jit.save(model, butterfly_inference, input_specinput_spec) print(静态图模型已导出)逻辑说明InputSpec的 batch 维度设 None导出后的模型可以接受任意 batch size。paddle.jit.save会生成.pdmodel和.pdiparams两个文件前者是网络结构后者是权重。导出前必须model.eval()否则 Dropout 和 BatchNorm 的行为会被固化进静态图。推理阶段最容易忽略的是预处理必须和训练时一致。很多人训练用了一套 transform推理时随手写了个 resize结果预测全错。import paddle import numpy as np from PIL import Image from paddle.vision.transforms import Compose, Resize, CenterCrop, Normalize, ToTensor # 推理用的 transform必须和验证集一致 infer_transform Compose([ Resize(256), CenterCrop(224), Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensor() ]) # 加载导出的静态图模型 infer_model paddle.jit.load(butterfly_inference) infer_model.eval() def predict(image_path, class_names): img Image.open(image_path).convert(RGB) tensor infer_transform(img).unsqueeze(0) # 加 batch 维度 with paddle.no_grad(): logits infer_model(tensor) probs paddle.nn.functional.softmax(logits, axis1) pred_idx paddle.argmax(probs, axis1).item() confidence probs[0][pred_idx].item() return class_names[pred_idx], confidence class_names [fspecies_{i} for i in range(20)] # 替换成真实物种名 name, conf predict(./test.jpg, class_names) print(f预测物种: {name}, 置信度: {conf:.4f})逻辑说明Resize(256)加CenterCrop(224)是验证和推理的标准组合先缩放到短边 256 再中心裁剪保证主体不被裁掉。unsqueeze(0)补上 batch 维度因为模型输入是四维。softmax把 logits 转成概率置信度低于 0.5 时建议人工复核细粒度任务里低置信度往往意味着图片质量差或物种不在训练集里。参数说明class_names的顺序必须和训练时ImageFolder的类别顺序一致这个顺序是按文件夹名字母序排的可以用train_ds.classes打印出来核对。如果推理时发现置信度普遍偏低检查一下Normalize的统计量是不是和训练时一致这是最常见的翻车点。我自己在这个项目上踩过最深的坑是第一次导出静态图时忘了切eval()导致推理结果和动态图对不上排查了大半天才发现是 Dropout 在作怪。后来养成的习惯是任何导出操作前先跑一遍动态图推理把结果存下来导出后再跑一遍两边对得上才继续往下走。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑