基于CNN的蝴蝶识别:从数据增强到迁移学习实战
简介这是一份基于PyTorch的CNN蝴蝶识别实战项目包含完整的Python源码与图片数据集适合正在学习深度学习图像分类、希望上手PyTorch完整流程的开发者。压缩包内共408个文件核心为3个Python脚本数据集文本生成、模型训练、PyQt5界面推理3个txt文本含环境依赖说明requirements.txt以及402张jpg蝴蝶样本图整体大小约32.77MB。项目提供了清晰的三步式运行逻辑先通过01脚本读取各类别图片并生成标签文本再运行02脚本完成训练集与验证集的加载和模型训练最后用03脚本启动可视化界面进行识别。数据集预处理环节加入了短边灰边填充和随机旋转增强能有效扩充样本、提升模型泛化能力。当前已有96人学习下载适合需要快速搭建图像分类Demo、理解数据预处理与PyTorch训练流程的入门及进阶学习者。1. 从“花分类”到“蝴蝶识别”CNN在细粒度图像分类上的第一课很多人第一次做图像分类会选择猫狗识别或花卉识别但蝴蝶识别更贴近真实业务种类差异往往只体现在翅膀纹理的走向、边缘缺口和颜色块分布上姿态、光照、背景变化都会让同一种蝴蝶呈现完全不同的像素。所以标题强调“CNN识别”而不是目标检测任务是对整张图打类别标签让卷积层自动提取判别特征。这篇文章基于Python CNN深度学习项目数据以图片类别目录组织。你会看到数据划分、图像增强、卷积网络搭建、训练评估最后用预训练模型微调提升准确率。代码使用PyTorch新手能按步骤复现老手可对照参数边界和踩坑点。2. 蝴蝶识别要先管好数据图片数据集的组织与增强拿到“蝴蝶数据集.zip”后先别急着写模型。解压后通常是一个个文件夹文件夹名就是类别名里面是该类别的图片。常见做法是先按8:1:1随机分成train/val/test三个目录而不是直接全量训练。CNN需要验证集来监控训练过程并决定是否保存模型也需要测试集来评估在没见过的图片上的真实泛化能力。2.1 数据集目录结构训练集、验证集、测试集怎么分我一般把输出目录设成data/train/类别名/、data/val/类别名/、data/test/类别名/。这样不需要额外标注文件PyTorch的ImageFolder可以直接读取类别。划分脚本如下import os import random import shutil root butterfly_dataset out data val_ratio 0.1 test_ratio 0.1 random.seed(42) for cls in os.listdir(root): src os.path.join(root, cls) if not os.path.isdir(src): continue imgs [f for f in os.listdir(src) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) n_test int(len(imgs) * test_ratio) n_val int(len(imgs) * val_ratio) n_train len(imgs) - n_val - n_test for split in (train, val, test): os.makedirs(os.path.join(out, split, cls), exist_okTrue) for i, img in enumerate(imgs): if i n_train: split train elif i n_train n_val: split val else: split test shutil.copy(os.path.join(src, img), os.path.join(out, split, cls, img))代码先把每类图片随机打乱再按样本数切片给三个集合。这里用copy而不是move避免原始数据被误删。如果你的原始数据结构不是文件夹而是一张CSV表也可以按文件名前缀做映射但目录结构更简洁后面直接用ImageFolder不会出错。划分比例不是固定的。如果数据集只有几百张10%的测试集就太小可以改成train 85%、val 10%、test 5%。更极端的情况会砍掉测试集直接用val当测试但那个做法会让模型选择用掉评估机会最后报告的准确率有偏乐观的风险。蝴蝶数据集通常包含几十类每类几百到几千张按8:1:1一般够用。还要留意类别列表排序。ImageFolder在加载train目录时会按文件夹名的字母顺序生成类别索引如果之后又手动给test目录新增了类类别索引就会错位。所以训练和评估必须使用同一个datasets.ImageFolder实例或先把类别列表取出来保存。2.2 图像增强让CNN看到更多“蝴蝶family”CNN虽然有平移不变性作为归纳偏置但真实图片里的蝴蝶会在画面中处于不同位置、姿态、光线和遮挡状态。如果只用原始图片训练模型很容易学到捷径比如根据背景颜色判断种类而不是根据蝴蝶本身的纹理。图像增强通过实时制造合理变体强迫CNN去关注蝴蝶本身的特征。下表是我在蝴蝶识别任务里比较常用的增强初值增强操作推荐参数作用RandomResizedCropsize224, scale(0.7, 1.0)模拟远近和构图变化RandomHorizontalFlipp0.5避免左右偏置ColorJitterbrightness0.3, contrast0.3, saturation0.3, hue0.05模拟晨昏光照和色彩漂移RandomRotation通常不用蝴蝶朝向有语义不宜强转随机旋转看起来是万能增强但蝴蝶的头部朝向在人类视觉中是有先验的如果测试集都是正着拍的训练时大规模旋转反而会引入分布偏差。同理超大角度的RandomResizedCrop会把蝴蝶切掉一半。增强参数应保持“合理困难”而不是让模型学会在拼图碎片里猜类别。from torchvision import transforms normalize transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.05), transforms.ToTensor(), normalize]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), normalize])验证集和测试集只做固定Resize和CenterCrop不做随机增强否则每次评估结果都会因为随机性产生波动。这里的Normalize使用ImageNet统计值因为很多预训练模型的权重都是在ImageNet上训练出来的后续如果要用ResNet这个参数就不要改。若从头训练CNN也可以自己算数据集的mean和std但差别通常不大。2.3 数据加载器与归一化参数目录结构和transform准备好后用ImageFolder和DataLoader接起来。ImageFolder会自动把data/train下的子目录名当成类别名按字母顺序映射为0,1,2。from torchvision import datasets from torch.utils.data import DataLoader train_loader DataLoader( datasets.ImageFolder(data/train, transformtrain_transform), batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader( datasets.ImageFolder(data/val, transformval_transform), batch_size32, shuffleFalse, num_workers4) test_loader DataLoader( datasets.ImageFolder(data/test, transformval_transform), batch_size32, shuffleFalse, num_workers4)batch_size取决于GPU显存16或32都是常用值。shuffle只在训练集打开验证和测试集关闭。num_workers在Windows上建议设0否则可能因为多进程启动问题卡住Linux服务器设4到8能明显提升加载速度。pin_memoryTrue在数据拷贝到GPU时更快但会占用固定内存机器内存紧张时可以不设。数据读到这里还差一个关键的类别数。在每个epoch之前打印len(train_loader.dataset.classes)确认它和你的蝴蝶种类数一致。以前遇到过zip包内多了一个隐藏的__MACOSX目录被当成一个类别导致模型输出维度错误。数据部分就绪后下一章搭建卷积网络。3. 用Python搭建CNN模型从卷积核到全连接层3.1 为什么蝴蝶识别用CNN而不是全连接网络把图片拉成一维向量丢给全连接网络空间结构会立刻被打乱。128x128的彩色图输入就有49152个值第一层隐藏层哪怕只有256个神经元参数量也超过1200万训练慢且极容易过拟合。CNN通过局部连接和权重共享解决这个问题一个卷积核在整张图上滑动所有空间位置共享同一套权重既减少参数又保留“相邻像素才相关”的图像先验。蝴蝶识别属于细粒度图像分类需要区分的特征经常是局部的翅脉的走向、斑点排列、外缘缺口。浅层卷积核负责边缘和色块深层卷积核把局部组合成完整的翅膀纹样。因此设计卷积网络时通道数通常逐层增加空间尺寸逐层缩小让语义一步步抽象。3.2 一个可复现的卷积网络结构下面这个SimpleCNN是三层卷积加三层池化最后接两层全连接。输入是224x224的RGB图输出是num_classes类别。这个模型在CPU上也能完成训练适合先跑通整体流程。import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))第一层卷积输入3个通道输出32个通道kernel_size3且padding1会让特征图宽高保持不变之后每经过一次MaxPool2d宽高减半。224经过三次池化变成28因此Flatten后是1282828。如果输入尺寸改为256这里的线性层维度要同步改为1283232或换成AdaptiveAvgPool2d固定输出。卷积层后面接ReLU已经是标配池化层可以选MaxPool2d或AvgPool2d。MaxPool保留最强响应适合边缘和纹理检测AvgPool更适合全局信息。这里交替使用没有严格规定只要记住池化会丢失位置信息而分类任务本来就不需要精确位置所以很安全。下表对比从头训练小网络和迁移学习常用配置方便在不同阶段切换配置项从头训练小网络迁移学习微调网络深度3层卷积ResNet18首层卷积通道3264Dropout0.50.2初始学习率1e-31e-3仅fc/1e-4解冻weight_decay1e-41e-43.3 损失函数、优化器与学习率参数多分类任务默认用CrossEntropyLoss。PyTorch的CrossEntropyLoss把LogSoftmax和NLLLoss合在一起所以模型输出层直接输出类别logits就好不需要手动加Softmax。训练时加Softmax不仅多余还会让梯度在反向传播中出现数值不稳定。优化器在小型CNN上我习惯用Adam初始学习率1e-3。它对学习率的敏感度低于SGD但并非完全免疫。如果loss迟迟不降把lr除以10如果loss震荡不收敛用0.0003再试。weight_decay是L2正则的一种实现给大权重加惩罚抑制过拟合。import torch num_classes len(train_loader.dataset.classes) model SimpleCNN(num_classesnum_classes) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam( model.parameters(), lr1e-3, weight_decay1e-4)如果数据集中某些类别样本很少可以在CrossEntropyLoss里传入torch.tensor的权重向量。计算权重时通常是样本数越多权重越低例如每个类别的权重设为总样本数除以该类别样本数这样小类别的Loss贡献会被放大。Adam配合weight_decay也能用但适合SGD的weight_decay取值范围和Adam场景并不完全一致试出来的经验值多是在1e-4到3e-4之间。如果想快速确认模型规模运行下面一行会打印总参数量print(sum(p.numel() for p in model.parameters()))SimpleCNN参数量大约在250万左右大头集中在最后两个全连接层。如果希望更轻量把中间的256改成128如果训练准确率不足把128改成256或增加一层卷积。卷积神经网络的调整不是一次到位的你要结合下一个epoch的日志再决定方向。4. 训练与评估跑通训练循环看懂准确率和loss4.1 训练循环与模型保存训练循环本身不复杂但模式容易写错。每个epoch分成训练阶段和验证阶段。训练阶段需要model.train()打开Dropout和BatchNorm的随机行为验证阶段需要model.eval()关闭它们。每次迭代的关键顺序是清零梯度、前向传播、计算损失、反向传播、更新参数。device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) best_acc 0.0 epochs 50 for epoch in range(epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) out model(images) loss criterion(out, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() * images.size(0) train_loss running_loss / len(train_loader.dataset) model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) out model(images) _, pred torch.max(out, 1) total labels.size(0) correct (pred labels).sum().item() val_acc correct / total print(fEpoch {epoch1:3d} | train_loss {train_loss:.4f} | fval_acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)running_loss在累加时乘上batch大小最后除以总样本数得到每个样本的平均损失。torch.max(out, 1)的第二个返回值是预测类别索引pred labels得到布尔张量sum()统计正确数。验证过程包在torch.no_grad()里避免为验证数据建立计算图浪费显存。判断是否保存模型看的是val_acc而不是train_loss因为训练损失持续下降并不代表泛化能力变好验证集准确率才说明模型对没见过图片的表现。实际训练中常见这种曲线训练损失一路走低验证准确率到达某个峰值后不再上涨后续epoch只在拟合训练集噪声。保存峰值权重之后即使后面训练中断也可以从best_model.pth恢复。如果想把训练过程可视化简单做法是每个epoch记录数值后用matplotlib画两条曲线train_loss和val_acc。train_loss持续下降但val_acc平滑不涨说明模型容量不足或增强过度两条曲线同步走低又上升则可能是学习率太大。4.2 评估指标分类报告与混淆矩阵准确率在蝴蝶识别里容易被多数类带偏。如果数据集中最常见的类别占30%一个永远预测该类的模型也有30%的准确率。为了看清每个类别的表现需要在测试集上生成classification_report和confusion_matrix。from sklearn.metrics import classification_report, confusion_matrix model.load_state_dict(torch.load( best_model.pth, map_locationdevice)) all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: images images.to(device) out model(images) _, pred torch.max(out, 1) all_preds.extend(pred.cpu().numpy()) all_labels.extend(labels.numpy()) names test_loader.dataset.classes print(classification_report(all_labels, all_preds, target_namesnames)) print(confusion_matrix(all_labels, all_preds))classification_report会逐类输出precision、recall、F1。recall低表示这种蝴蝶经常被漏判原因通常是训练样本太少或该类与另一个类外观太接近。confusion_matrix非对角线上数值大的位置就是模型最常搞混的类别对。如果两个类反复互相误判优先回去看数据有没有标注错误、拍摄角度是否极端、两个类在数据集里的图片是否已经非常相似。下表总结了训练中常见现象和优先处理方向现象原因先试操作train_acc高val_acc低过拟合增大Dropout或weight_decaytrain_loss一直不降学习率过大/过小容量不足调低lr或增加通道数val_acc剧烈震荡学习率偏大或batch太小降低lr尝试增大batchtrain/val都很低欠拟合加深网络减少增强4.3 过拟合判断与调整策略蝴蝶数据集通常几千张SimpleCNN约250万参数训练中过拟合几乎是必然的。最直接的信号是训练集准确率接近100%验证集准确率停住甚至回落。此时验证loss已经从低点反弹训练loss还在降两条曲线开始拉大距离。我的处理顺序是先把Dropout从0.5升到0.6同时把weight_decay从1e-4提到3e-4如果依旧过拟合减小模型容量例如把三个卷积层的通道数从32/64/128改为24/48/96再不行就降低增强强度尤其检查RandomResizedCrop的scale下限是不是太小。假如以上都不理想就不要再揪着小网络调直接换预训练模型微调通常更省时间。5. 进阶用预训练模型微调把蝴蝶识别准确率提上去5.1 替换ResNet18的全连接层SimpleCNN在蝴蝶数据上能拿到80%左右的验证准确率已经不错。想再往上提用PyTorch的预训练ResNet18是命令行选手和调参老手都会首选的路线。ResNet18在ImageNet上已经学到了边缘、纹理、形状这些通用特征蝴蝶识别只是在这个底座上做一次小规模适配。import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.fc torch.nn.Linear(model.fc.in_features, num_classes)新版torchvision推荐weightsDEFAULT比pretrainedTrue更显式。替换fc层时用model.fc.in_features取值而不是写死512这样可以无脑切换到ResNet50等模型。权重首次下载可能需要一段时间缓存路径由TORCH_HOME环境变量指定默认在用户目录下的.cache/torch/hub/checkpoints。5.2 两段式冻结微调参数迁移学习最稳妥的参数策略是“先冻结再解冻”。第一段把所有卷积层参数requires_grad设为False只训练fc层学习率1e-3跑10个epoch第二段解冻layer4的参数学习率降到1e-4再跑10个epoch。layer4是ResNet最靠近输出的残差块语义层次最高解冻它能帮助模型适应蝴蝶的视觉分布又不会让底层基础特征被破坏。for p in model.parameters(): p.requires_grad False for p in model.fc.parameters(): p.requires_grad True optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 第一段训练10个epoch后再解冻 layer4 for p in model.layer4.parameters(): p.requires_grad True optimizer torch.optim.Adam( [p for p in model.parameters() if p.requires_grad], lr1e-4)第二段的optimizer这一步很关键如果只传model.parameters()会把requires_gradFalse的参数也放进优化器虽然梯度为零不会更新但浪费内存。用列表推导只挑requires_grad为真的参数才是“解冻到哪就优化到哪”的正确写法。5.3 用测试集验证提升幅度跑第4.2节里同一套评估代码把加载的权重换成微调后的best_model.pth重新打印classification_report。对比双方的准确率、macro F1和个别难类的recall。提升一般是明显的如果没提升先检查两件事第一数据划分时随机种子是否一致两次训练用的是不是同一份test集第二微调过程中是否不小心把训练集和验证集transform混用了。模型文件的命名加上时间戳方便回滚到表现最好的版本。到这里从zip数据集到可用的蝴蝶识别模型已经形成完整闭环。本文还有配套的精品资源点击获取