资讯动态

Paddle+ResNet蝴蝶分类实战:从数据清洗到90%+精度

发布时间:2026/9/28 23:34:19 来源:尧图企业网站定制
简介这份资源是面向高校学生与深度学习入门者的蝴蝶种类识别与分类项目源码基于PaddlePaddle框架与ResNet残差网络实现可直接用于课程设计、期末大作业或图像分类练手场景。压缩包共14个文件约3.29MB以10张png运行结果图、2个md说明文档、1个txt数据集说明和1个py主程序为主图片展示训练与预测效果文档交代环境与数据组织脚本则承载模型搭建、训练与推理的完整流程。目前已有176人学习下载说明其在同类作业中具备一定参考价值。项目已获导师指导并通过97分评审下载即用无需修改读者可据此掌握残差网络结构、数据读取与分类评估的完整链路并对照输出图快速验证运行结果适合作为图像分类方向的实战起点。1. 蝴蝶分类项目为什么值得用 Paddle ResNet 重做一遍蝴蝶种类识别这个题目很多人在课程设计或毕设里都碰过。常见的做法是拿一个现成的图像分类脚本换掉数据集路径跑通就交差。但真正动手之后你会发现蝴蝶数据集的类间差异极小——有些品种的差别只在翅脉走向和眼斑数量上用浅层 CNN 或者直接调sklearn里的分类器准确率往往卡在 60% 上下上不去。这个项目的核心价值就是用 Paddle 框架配合 ResNet 残差网络把「深网络训不动」这个老问题绕过去让蝴蝶这种细粒度分类任务能稳定跑到 90% 以上。它适合三类人正在找 Python 图像分类实战项目的学生、想从 TensorFlow 或 PyTorch 转到 Paddle 的工程师、以及需要一套可复现细粒度分类基线的人。整条链路不复杂——数据整理、残差网络搭建、迁移学习微调、推理验证每一步都有明确的参数和排错点。下面按我实际搭这套流程的顺序把能抄的代码和容易翻车的地方都摊开讲。2. 蝴蝶数据集怎么整理成 Paddle 能直接吃的格式2.1 先搞清楚蝴蝶分类的数据长什么样蝴蝶识别属于典型的细粒度图像分类。和猫狗分类不同它的难点不在「是不是蝴蝶」而在「是哪一种蝴蝶」。公开的蝴蝶数据集一般按物种分文件夹每个文件夹里几十到上百张图拍摄角度、光照、背景差异很大。我见过最多的坑是有人直接把原始图片一股脑丢进ImageFolder结果训练集里混进了同一张图的裁剪版本验证集准确率虚高到 99%换一批真实照片立刻掉到 50%。所以第一步不是写模型是清洗数据。常见做法是每个类别至少保留 80 张有效图剔除模糊、遮挡超过三分之一的样本然后按 7:2:1 切分训练、验证、测试。切分时要注意同一只蝴蝶的多角度照片不能跨集合分布否则就是数据泄漏。Paddle 侧读取数据用paddle.io.Dataset配合paddle.vision.transforms。下面是我常用的数据集类骨架import os import paddle from paddle.io import Dataset from paddle.vision import transforms from PIL import Image class ButterflyDataset(Dataset): def __init__(self, root_dir, modetrain): self.samples [] self.mode mode # 按类别文件夹收集路径类别名即标签 self.classes sorted(os.listdir(root_dir)) self.class_to_idx {c: i for i, c in enumerate(self.classes)} for cls in self.classes: cls_dir os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): self.samples.append( (os.path.join(cls_dir, fname), self.class_to_idx[cls]) ) # 训练集做增强验证测试只做归一化 if mode train: self.transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) else: self.transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) return self.transform(img), label这段代码的逻辑很直白初始化时把路径和标签配对存进列表取数据时按模式走不同的 transform。参数上有两个点值得说。RandomResizedCrop的scale(0.7, 1.0)是蝴蝶任务里比较稳的区间裁得太狠会把翅膀关键区域切掉Normalize用的均值方差是 ImageNet 的统计值因为后面要加载预训练权重这一步必须对齐否则预训练模型的特征分布对不上微调会变慢。2.2 用 DataLoader 把 batch 和 shuffle 配好数据集类写完后用paddle.io.DataLoader包一层。训练集要shuffleTrue验证集不用。num_workers在 Windows 上建议设 0Linux 上可以设 4 到 8设太高反而会因为进程通信拖慢速度。train_ds ButterflyDataset(data/train, modetrain) val_ds ButterflyDataset(data/val, modeval) train_loader paddle.io.DataLoader( train_ds, batch_size32, shuffleTrue, num_workers4, drop_lastTrue ) val_loader paddle.io.DataLoader( val_ds, batch_size32, shuffleFalse, num_workers4 )batch_size32是 8GB 显存下的安全值显存够可以上 64。drop_lastTrue是为了避免最后一个不满 batch 的样本干扰 BatchNorm 的统计这个细节在小数据集上影响不小。提示整理完数据后先跑一遍len(train_ds)和train_ds.classes确认类别数和样本数对得上别等训练完才发现少了一个类。3. ResNet 残差结构在 Paddle 里怎么搭和怎么改3.1 残差连接到底解决了蝴蝶分类的什么问题蝴蝶分类要区分的是翅脉、斑点这类局部细节网络必须足够深才能提取到这些高层语义。但普通 CNN 堆到几十层后会出现退化训练误差不降反升。这不是过拟合是梯度在反向传播中被逐层稀释浅层参数几乎收不到有效更新。ResNet 的做法是加一条恒等映射的捷径让某一层的输出变成F(x) x。反向传播时梯度可以通过这条捷径直接回传不用穿过所有中间层。对蝴蝶任务来说这意味着网络可以放心堆深浅层学到的边缘和纹理特征不会被丢掉深层又能专注组合出物种判别性的模式。残差块有两种BasicBlock 用于 ResNet18/34Bottleneck 用于 ResNet50 及以上。蝴蝶数据集通常几千到几万张ResNet50 是精度和速度的平衡点我一般首选它。3.2 手写一个 Bottleneck 残差块Paddle 的paddle.vision.models里已经有现成的 ResNet但理解结构才能改。下面是一个标准 Bottleneck 的实现import paddle import paddle.nn as nn class Bottleneck(nn.Layer): expansion 4 # 输出通道是中间通道的4倍 def __init__(self, in_channels, mid_channels, stride1, downsampleNone): super().__init__() # 1x1 降维 self.conv1 nn.Conv2D(in_channels, mid_channels, 1, bias_attrFalse) self.bn1 nn.BatchNorm2D(mid_channels) # 3x3 提取空间特征 self.conv2 nn.Conv2D(mid_channels, mid_channels, 3, stridestride, padding1, bias_attrFalse) self.bn2 nn.BatchNorm2D(mid_channels) # 1x1 升维 self.conv3 nn.Conv2D(mid_channels, mid_channels * self.expansion, 1, bias_attrFalse) self.bn3 nn.BatchNorm2D(mid_channels * self.expansion) self.relu nn.ReLU() self.downsample downsample # 捷径分支维度不匹配时用 def forward(self, x): identity x out self.relu(self.bn1(self.conv1(x))) out self.relu(self.bn2(self.conv2(out))) out self.bn3(self.conv3(out)) if self.downsample is not None: identity self.downsample(x) out identity # 残差连接核心 return self.relu(out)三个卷积的分工是第一个压通道数减少计算量第二个在低维空间做 3x3 卷积第三个恢复通道。downsample处理的是输入输出维度不一致的情况用 1x1 卷积加 BN 把捷径分支的通道数对齐否则out identity会直接报维度错误。这是手写残差块最常见的翻车点。3.3 加载预训练权重并替换分类头从零训练 ResNet50 在蝴蝶数据集上收敛很慢迁移学习是标配。Paddle 加载预训练权重的写法from paddle.vision.models import resnet50 # 加载 ImageNet 预训练权重 model resnet50(pretrainedTrue) # 替换最后的全连接层输出改为蝴蝶类别数 num_classes 20 # 按你的数据集实际类别数改 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes)pretrainedTrue会自动下载 ImageNet 权重第一次运行需要联网。替换fc层之后新加的全连接层是随机初始化的所以训练初期学习率不能太大否则会把预训练好的卷积特征冲垮。我一般对卷积层用1e-4对fc层用1e-3分组设置学习率。注意如果你在离线环境跑pretrainedTrue会卡在下载。提前把权重文件放到~/.cache/paddle/weights/下或者用paddle.utils.download手动拉。4. 训练脚本怎么写损失、优化器和学习率调度4.1 损失函数和优化器的参数怎么定蝴蝶分类是单标签多分类损失用交叉熵。Paddle 里是nn.CrossEntropyLoss它内部已经包含了 softmax所以模型输出直接给 logits不要再加 softmax 层否则等于做了两次梯度会异常。优化器选paddle.optimizer.AdamW它比 SGD 在小数据集上更容易调。权重衰减设1e-4这个值对 ResNet 微调比较稳。学习率用分组策略import paddle num_classes 20 model resnet50(pretrainedTrue) model.fc paddle.nn.Linear(model.fc.in_features, num_classes) # 卷积层和全连接层分组学习率不同 param_groups [ {params: [p for n, p in model.named_parameters() if fc not in n], learning_rate: 1e-4}, {params: [p for n, p in model.named_parameters() if fc in n], learning_rate: 1e-3}, ] optimizer paddle.optimizer.AdamW( learning_rate1e-4, parametersparam_groups, weight_decay1e-4 ) criterion paddle.nn.CrossEntropyLoss()分组学习率的逻辑是预训练卷积层已经学到了通用特征只需要小步微调新的分类头是随机初始化的需要大步快跑。如果两组用同一个学习率要么卷积层被破坏要么分类头学得太慢。4.2 学习率调度和训练循环学习率调度用余弦退火让学习率从初始值平滑降到接近 0避免后期在最优解附近震荡。epochs 30 scheduler paddle.optimizer.lr.CosineAnnealingDecay( learning_rate1e-4, T_maxepochs ) optimizer paddle.optimizer.AdamW( learning_ratescheduler, parametersparam_groups, weight_decay1e-4 ) for epoch in range(epochs): model.train() for batch_id, (images, labels) in enumerate(train_loader): logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() optimizer.clear_grad() if batch_id % 20 0: print(fepoch {epoch} step {batch_id} loss {loss.numpy()[0]:.4f}) scheduler.step() # 每个 epoch 结束做一次验证 model.eval() correct, total 0, 0 for images, labels in val_loader: logits model(images) preds paddle.argmax(logits, axis1) correct (preds labels).astype(int64).sum().numpy()[0] total labels.shape[0] print(fepoch {epoch} val_acc {correct / total:.4f})训练循环里有几个关键动作optimizer.clear_grad()必须在step()之后调用清掉上一轮的梯度验证前切model.eval()让 BatchNorm 用滑动统计量而不是当前 batch 的统计量这个切换不做的话验证准确率会明显偏低。scheduler.step()放在 epoch 末尾配合CosineAnnealingDecay的T_maxepochs学习率会在 30 个 epoch 内走完一个余弦周期。4.3 训练过程中的监控指标光看 loss 不够蝴蝶分类要同时盯三个数训练 loss、验证准确率、以及每类的召回率。如果整体准确率上去了但某一类召回率极低说明类间不平衡或者该类样本太少。Paddle 没有内置的分类报告可以自己用混淆矩阵算import numpy as np def evaluate(model, loader, num_classes): model.eval() matrix np.zeros((num_classes, num_classes), dtypenp.int64) for images, labels in loader: logits model(images) preds paddle.argmax(logits, axis1).numpy() for t, p in zip(labels.numpy(), preds): matrix[t][p] 1 acc np.trace(matrix) / matrix.sum() return acc, matrix混淆矩阵能直接看出哪些类别被互相误判。蝴蝶里有些近缘种本来就难分如果混淆集中在某两类之间可以考虑加细粒度注意力模块或者对这两类做针对性数据增强。5. 避坑与排查蝴蝶分类训练里最容易翻车的五件事5.1 验证准确率远高于测试准确率现象验证集跑到 95%换测试集只有 60%。原因切分时同一只蝴蝶的多张照片被分到了不同集合模型记住了背景而不是蝴蝶本身。解决按拍摄个体或拍摄批次切分确保同一来源的图片只出现在一个集合里。这个坑我踩过不止一次血泪经验是切分脚本里加一个按文件名前缀分组的逻辑。5.2 训练 loss 不降一直卡在 4.6 左右现象loss 从第一个 epoch 到第十个 epoch 几乎不动。原因CrossEntropyLoss前面多加了 softmax或者标签类型不对。Paddle 的交叉熵要求标签是int64如果从 DataLoader 出来是int32不会报错但梯度会算错。解决在 Dataset 的__getitem__里把 label 转成np.int64并检查模型输出没有经过 softmax。5.3 显存溢出batch_size 降到 8 还是 OOM现象报Out of memory调小 batch 也没用。原因验证阶段没有用paddle.no_grad()中间激活值一直在累积。解决验证和推理循环外面套with paddle.no_grad():显存占用能降一半以上。另外num_workers设太高也会占额外内存Windows 上直接设 0。5.4 预训练权重加载后准确率反而下降现象pretrainedTrue之后第一个 epoch 的验证准确率比随机初始化还低。原因归一化参数和预训练权重不匹配。ImageNet 预训练用的是mean[0.485, 0.456, 0.406]如果你用了[0.5, 0.5, 0.5]输入分布偏移预训练特征失效。解决transform 里的 Normalize 必须和预训练权重训练时用的统计值一致。5.5 模型保存后重新加载推理结果全乱现象训练完保存参数重新加载做推理输出全是同一类。原因保存的是model.state_dict()加载时没有先构建相同结构再set_state_dict或者分类头类别数对不上。解决保存时同时存类别映射表加载时先实例化同结构模型再model.set_state_dict(paddle.load(path))最后切model.eval()。6. 把训练好的蝴蝶分类模型跑成可验证的推理脚本训练完只是半成品能稳定推理才算闭环。我习惯写一个独立的推理脚本输入单张图片路径输出 top-3 类别和置信度。这样验证模型时不用每次都跑整个验证集。import paddle import numpy as np from PIL import Image from paddle.vision import transforms def predict(img_path, model, class_names, topk3): model.eval() tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(img_path).convert(RGB) tensor tf(img).unsqueeze(0) # 加 batch 维度 with paddle.no_grad(): logits model(tensor) probs paddle.nn.functional.softmax(logits, axis1) values, indices paddle.topk(probs, ktopk, axis1) results [(class_names[i], float(v)) for v, i in zip(values[0], indices[0])] return results # 加载模型 model resnet50(pretrainedFalse) model.fc paddle.nn.Linear(model.fc.in_features, len(class_names)) model.set_state_dict(paddle.load(best_model.pdparams)) model.eval() for name, score in predict(test.jpg, model, class_names): print(f{name}: {score:.4f})这段脚本的关键在unsqueeze(0)推理时模型要求输入是四维[N, C, H, W]单张图只有三维不加 batch 维度会报错。paddle.topk返回的是值和索引配合类别名列表就能输出可读结果。验证模型是否真的学到了东西我有个习惯拿一张训练集里没见过的蝴蝶照片先看 top-1 是不是对的再看 top-2 和 top-3 里有没有正确答案。如果正确答案经常掉到第三名开外说明模型对细粒度特征的区分力还不够可以考虑把输入分辨率从 224 提到 320或者换 ResNet101 再试一轮。另一个技巧是冻结前两个 stage 的卷积层只微调后面三层加分类头在小数据集上这样比全量微调更稳收敛也更快。这套流程我从数据整理到推理验证完整跑过几轮最深的体会是蝴蝶分类的精度瓶颈往往不在模型结构而在数据清洗和归一化对齐这两个看起来最不起眼的地方。把这两步做扎实ResNet50 加迁移学习足够应付大多数蝴蝶数据集。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑