ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于迁移学习和ResNet50的花卉识别系统实战

基于迁移学习和ResNet50的花卉识别系统实战 简介这是一套基于Python深度学习的花卉识别系统完整源码包面向想入门图像分类、需要参考完整工程结构的开发者和学习者也可作为课程设计、毕业设计或企业原型验证的基础工程。系统利用TensorFlow或PyTorch搭建神经网络实现花卉图像特征提取与分类适用于植物研究、花卉市场、景区导览等场景。压缩包共1048个文件约37.71MB包含456个Python源代码、450个pyc字节码以及dll/pyd/exe运行库、模型权重h5、xml配置和虚拟环境脚本等类型覆盖从训练推理到部署运行的完整环节。包内保留的venv虚拟环境、依赖配置和工程目录结构清晰便于快速复现运行环境跟踪预处理、模型构建、训练评估等关键模块。目前已有665人学习下载适合希望从工程角度掌握深度学习识别项目落地方法的读者。1. 拿到花卉识别系统源码这个标题先别急着跑训练基于Python深度学习的花卉识别系统设计源码是课程设计、毕业设计和花店/园林类小项目里出现频率最高的方向之一。看起来就是图像分类但真正动手时会发现102个花种里玫瑰和月季长得几乎一样数据集的图片尺寸五花八门训练完的模型在真实照片上准确率直接掉20%。这个标题要解决的不是识别本身而是如何用预训练模型做迁移学习在有限数据下把分类精度做到能用的水平。适合的人是有Python基础、了解CNN基本概念、想在一到两周内跑通完整流程的开发者。本文按数据准备 → 训练调参 → 踩坑排查 → 部署验证的顺序展开每个环节都给了可直接复制的脚本和参数边界。2. 花卉数据集准备好了吗目录结构与按类划分的训练/验证集2.1 公开花卉数据集的常见形态与标签读取方式做花卉识别最常用的公开数据集是Oxford 102 Flower共102个类别、约8000多张图片每类图片数量在40到250张之间类别不均衡问题很突出。也有17 Flower Dataset类别少、每类图片更少适合做快速demo。如果做中文场景下的月季/玫瑰等细分品种识别通常需要自己采集。拿到数据集后第一步不是写模型而是把数据读进来。我见过太多人上来就写ResNet结果数据加载时报错——因为标签不在文件名里而是在一个单独的labels.txt里。Oxford 102的图片文件名是image_00001.jpg这样的序号labels.txt里每行是图片序号 类别编号而类别名称又放在另一个文件里。第一步要把这三者对上。import os from PIL import Image from torch.utils.data import Dataset class Flower102Dataset(Dataset): def __init__(self, img_dir, label_file, transformNone): self.img_dir img_dir self.transform transform self.samples [] with open(label_file, r) as f: for line in f.readlines(): parts line.strip().split() img_name fimage_{parts[0]}.jpg label int(parts[1]) - 1 # 标签从1开始转成0基 self.samples.append((img_name, label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_name, label self.samples[idx] path os.path.join(self.img_dir, img_name) image Image.open(path).convert(RGB) if self.transform: image self.transform(image) return image, label这里有个关键点int(parts[1]) - 1。很多数据集的标签文件编号从1开始但PyTorch的CrossEntropyLoss要求标签从0开始忘了减1会导致训练时损失一直不降而且报错都不一定出现。另一个细节是convert(RGB)——数据集里偶尔混着灰度图或带透明通道的PNG不转RGB会在归一化时维度对不上。2.2 按类别划分数据集别用随机乱序训练集和验证集的划分方式直接影响最终精度的真实性。常见错误是直接调用random_split把全部数据随机打乱再按比例切分。这在多类数据上会造成数据泄露同一类别、甚至同一植株的相似照片同时出现在训练集和验证集里。花卉数据集往往同一物种照片来自同一场景序列随机划分会让验证集看起来精度很高换到真实照片就露馅。我一般按类别分组后每类取70%训练、20%验证、10%测试来切。代码逻辑是先按类别编号聚合所有样本再对每个类内部打乱、切分最后合并成三份。import random from collections import defaultdict def split_by_class(samples, train_ratio0.7, val_ratio0.2, seed42): random.seed(seed) class_to_samples defaultdict(list) for img_name, label in samples: class_to_samples[label].append((img_name, label)) train, val, test [], [], [] for label, items in class_to_samples.items(): random.shuffle(items) n len(items) n_train int(n * train_ratio) n_val int(n * val_ratio) train.extend(items[:n_train]) val.extend(items[n_train:n_train n_val]) test.extend(items[n_train n_val:]) return train, val, test这个函数的核心是defaultdict按类别聚合再逐类切分。参数上seed必须固定否则每次运行数据划分不同后续对比实验就没有意义。比例上如果总数据量只有8000张70%训练集约5600张对102类来说平均每类55张勉强够微调。如果数据量更少可以提高到80%训练、10%验证、10%测试。验证集的作用是早停和调参不能太小否则验证损失抖动太厉害。2.3 数据增强配置参数这么设才不玄学数据增强的目的是让模型对拍摄角度、光照、遮挡鲁棒。花卉场景的特殊性在于花朵颜色是重要特征所以颜色增强要做但幅度不能太猛。我常用的组合是RandomResizedCrop加RandomHorizontalFlip其中RandomResizedCrop的scale参数设为(0.6, 1.0)保证裁掉的面积不至于把主要花朵切掉。水平翻转对花卉有效因为花的左右对称性不强不会像文字识别那样翻转后语义崩溃。垂直翻转基本不用。花的位置虽然无所谓上下但真实场景中拍摄角度很少会上下颠倒这个增强会引入训练分布外的样本。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0), ratio(0.75, 1.33)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_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]) ])RandomResizedCrop本身会输出224尺寸后面的Resize((224, 224))是保险操作防止某些版本输入尺寸不匹配。验证集不随机裁剪先Resize(256)再CenterCrop(224)是ImageNet系列模型的标配——这么做是为了评估时保证结果可复现不会因为随机裁剪导致同张图两次推理结果不一样。归一化的mean和std用的是ImageNet的统计值。这里必须强调只要用ImageNet预训练权重归一化就必须用ImageNet的均值和方差不能自己算数据集的均值替换。否则预训练权重的特征分布直接被打破效果会断崖式下跌。3. 用ResNet50做迁移学习训练脚本与关键参数调优3.1 为什么不从头训练数据量撑不起随机初始化花卉识别的数据量一般在几千到几万张102类平均每类只有几十张从头训练一个ResNet50至少需要百万级数据才能收敛。随机初始化的卷积网络在前几十个epoch内学到的只是边缘和纹理特征根本来不及收敛到花的语义特征训练集loss还没降下去验证集早就过拟合了。所以实际工程里几乎都是用ImageNet预训练权重做微调只改最后一层全连接输出的类别数。预训练模型从torchvision.models里加载关键是weights参数在新版本里的写法。老代码写pretrainedTrue的方式在新版torchvision里已经被标记弃用建议直接用weightsResNet50_Weights.IMAGENET1K_V1。改最后一层时把model.fc替换成输出102维的全连接前面所有层的参数默认保留预训练权重。至于是否冻结前面的卷积层取决于数据量几千张图且和ImageNet物种分布差距不大时冻结前几层只训练后面几层即可数据集稍大且想追求更高精度就解冻全部层用较小的学习率微调。import torch import torch.nn as nn from torchvision import models, transforms def create_model(num_classes102, freeze_backboneFalse): weights models.ResNet50_Weights.IMAGENET1K_V1 model models.resnet50(weightsweights) for param in model.parameters(): param.requires_grad True if freeze_backbone: for param in model.parameters(): param.requires_grad False in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model参数说明freeze_backboneTrue时Backbone所有参数不计算梯度训练时只更新新增的Dropout和全连接层。这种方式适合训练集极小每类少于30张或者只是想快速出demo的场景训练速度也快很多。缺点是精度上限有限因为前面的卷积特征是为ImageNet品类设计的花的细粒度特征没有被充分适配。Dropout(0.3)是防止最后一层过拟合的常用值0.3在102类任务上不算激进如果验证集损失明显高于训练集损失可以调到0.5试一试。3.2 优化器、损失函数和学习率调度直接给一套能跑的组合花卉分类是标准的单标签多分类任务损失函数用CrossEntropyLoss即可不需要加权——除非类别极度不均衡比如某一类只有10张而另一类有250张那就需要在损失里加类别权重。优化器上我优先推SGD加动量的组合而不是Adam。原因在于微调预训练模型时SGD配合小学习率的收敛曲线更平缓最终精度通常比Adam高1到2个百分点。Adam收敛快但容易卡在次优解尤其在Fine-tune场景下。如果追求省事用AdamW学习率要降到SGD的十分之一。import torch.optim as optim from torch.optim import lr_scheduler model create_model(num_classes102, freeze_backboneFalse) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.005, momentum0.9, weight_decay1e-4) scheduler lr_scheduler.CosineAnnealingLR(optimizer, T_max30, eta_min1e-6)这里lr0.005是解冻全部层时的安全起点。如果freeze_backboneTrue只训练最后两层lr可以放到0.01因为需要更新的参数量小、不容易振荡。weight_decay1e-4是L2正则项的系数太小起不到约束作用太大会让权重收缩过度、特征表达力下降。CosineAnnealingLR的T_max设成总epoch数让学习率在30个epoch内从0.005余弦下降到1e-6最后几个epoch用极小学习率精修权重。3.3 完整训练循环验证损失早停与模型保存策略训练循环本身不复杂但模型保存策略值得想清楚。不要每个epoch都保存也不要只在训练集loss最低时保存。我习惯在每个epoch结束后算验证集loss和准确率只在验证准确率创新高时保存模型。这样最终拿到的是验证集上最优的模型而不是训练后期已经过拟合的模型。def train_one_epoch(model, train_loader, optimizer, criterion, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / total def validate(model, val_loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / total这段代码里有三个细节值得注意。optimizer.zero_grad()必须在每次前向传播之前调用否则梯度会在多个batch间累加导致参数更新方向飘掉。model.train()和model.eval()的切换不能省略——它们影响BatchNorm层的运行时统计和Dropout是否生效忘了切换验证时指标会异常波动。torch.no_grad()块内推理能省显存和加速验证阶段不需要梯度。早停和保存代码best_val_acc 0.0 for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc validate(model, val_loader, criterion, device) scheduler.step() if val_acc best_val_acc: best_val_acc val_acc torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), val_acc: val_acc, }, best_flower_model.pth) print(fEpoch {epoch1}: train_acc{train_acc:.4f}, val_acc{val_acc:.4f})保存为字典格式含epoch、优化器状态、val_acc比只保存model.state_dict()更实用——恢复训练时能把优化器状态也恢复进来验证指标的信息也能作为后续对比依据。如果显存不够把batch_size从32降到16或8同时把输入图片的resize改为160x160可以大幅降低显存占用但精度会有损失。4. 训练收敛的三个信号与五组必查参数4.1 收敛信号怎么判断训练状态是正常的训练时盯着loss曲线看是基本功但很多人看不出正常收敛和虚胖收敛的区别。这里给出三个可用信号。第一信号训练loss在前5个epoch内明显下降。如果用了ImageNet预训练权重且lr设置合理第一轮epoch结束训练准确率就应该在30%以上而不是接近1/102的随机水平。如果训练loss迟迟不降优先怀疑lr过小或数据读取有问题。第二信号验证集准确率随训练过程稳步上升且与训练集准确率差距维持在5到10个百分点以内。差距过小不一定好——说明模型几乎没有学到类别间的判别边界只是简单记忆差距超过15个百分点则说明过拟合明显。第三信号验证loss曲线呈现先降后升趋势。如果验证loss在某个epoch后开始持续上升而训练loss还在降说明过拟合已开始此时保存的最佳模型大概率出现在验证loss拐点前这也是早停要发挥作用的位置。4.2 五组必查参数按优先级排查参数组一学习率与优化器。SGD动量为0.9lr在1e-3到1e-2之间AdamW则建议1e-4到3e-4。如果loss剧烈振荡不下降八成是lr大了如果下降缓慢像蜗牛则lr小了。参数组二batch_size。batch越大梯度越平滑但过大会让模型陷入尖锐极小值、泛化变差。batch_size32是102类花卉任务的标准参考值显存不足时优先降分辨率而不是降batch效果损失更小。参数组三权重衰减weight_decay。这个参数对最终精度的影响常被忽略默认1e-4是个平衡点如果验证集loss曲线比训练集更早抬头可以增大到1e-3试试。参数组四冻结层数。freeze_backboneTrue时训练只需几分钟就能达到80%左右但想冲击更高精度必须解冻全部层做微调此时lr按前面说的降到0.005。参数组五数据增强幅度。如果验证准确率很高但测试集真实场景照片准确率掉得多不是模型问题是训练时数据增强不够。增强的scale从(0.6, 1.0)调到(0.4, 1.0)并增加RandomRotation(degrees15)能有效提升泛化。注意修改任何参数后都要保持数据集划分seed不变否则无法判断是参数导致的差异还是数据划分运气导致的差异。5. 花识别系统最常见的五个翻车点与排查顺序5.1 翻车点一数据泄露验证集准确率虚高现象训练集准确率只有85%验证集却有95%怎么看都不正常。原因数据集按文件名乱序后随机划分同一类的照片同时出现在训练集和验证集。尤其花卉数据集经常包含同一位置连续拍摄的序列这些图片高度相似模型记住了训练样本的细节特征后验证集里高度相似的图片自然也被分对。解决按类别分组后划分即本文2.2节的方法。另外检查验证集里是否出现文件名前缀相同的图片同时存在于训练集的情况如果存在进一步做去重处理——同一类内相似度高的图片只保留一条放进同一边。5.2 翻车点二类别不均衡小众花种识别率接近零现象整体准确率75%但有个别类别准确率是0%预测结果完全偏向样本量大的类别。原因102类中每类图片数从40到250不等。模型在训练时看到某类样本多、某类样本少决策边界被样本量大的类别主导。解决有两个方案。方案一是给CrossEntropyLoss传入weight参数按类别样本数的倒数加权让小类别的loss贡献更大。方案二是做数据增强的过采样——对小类别每epoch多重复几次等价于增加其曝光次数。import torch.nn as nn class_counts [len([s for s in all_samples if s[1] i]) for i in range(102)] total_count sum(class_counts) weights [total_count / (102 * c) for c in class_counts] weights torch.tensor(weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightweights)5.3 翻车点三相似品种混淆玫瑰与月季、雏菊与菊花现象验证集里玫瑰和月季互相误判的比例特别高模型输出的置信度两个类别都接近0.5。原因花卉细粒度识别中相似物种的差异在花瓣纹理、叶片形状等局部区域。ResNet50最后一层输出的102维向量在这些类别上区分不明显。解决提升输入分辨率到320x320让模型看到更多局部细节另外在训练好的模型上做类别合并——把容易混淆的类别合并成一个大类等模型预测出大类后再用第二个小模型细分两级结构在真实系统里更能落地。如果不想做两级模型可以增加RandomResizedCrop的裁剪次数让模型更关注局部。5.4 翻车点四显存不足训练中断现象训练到一半报CUDA out of memory。原因batch_size过大或输入分辨率太高。常见误区是一上来设batch_size64还开RandomResizedCrop显存直接爆掉。解决先降batch_size到16或8还不够就降低分辨率到160再不行就把backbone冻结只训练分类层。注意torch.cuda.empty_cache()只能清理缓存碎片不能解决显存溢出的本质问题。5.5 翻车点五验证集上好的模型部署后推理慢现象模型验证准确率90%但放到CPU环境跑一张图要1秒以上根本没法实用。原因ResNet50本身有2500万参数如果部署目标是手机或低配CPU直接用原模型不现实。解决部署前做知识蒸馏——把ResNet50作为教师模型训练一个MobileNetV3或EfficientNet-Lite作为学生模型学生模型的精度损失通常在2到3个百分点以内但推理速度能快3到5倍。如果不想引入蒸馏流程直接用torchvision的mobilenet_v3_small从头微调也能在速度和精度之间取得平衡。6. 导出TorchScript模型并写一个本地识别接口训练完成后的最后一步是把模型部署成本地可调用接口。我习惯用TorchScript做模型固化它不依赖Python解释器和torchvision的模型类定义在Flask里加载时只用torch.jit.load就能完成。导出前先确认模型处于eval模式否则BatchNorm层的行为不一致固化的模型在推理时结果会漂移。import torch model.eval() example_input torch.randn(1, 3, 224, 224) traced_model torch.jit.trace(model, example_input) traced_model.save(flower_traced.pt)这里用torch.jit.trace而不是torch.jit.script。trace是按实际输入张量走一遍前向适合确定性网络结构script适合含大量控制流的模型但对于ResNet这种纯卷积堆叠结构trace更简单稳定。torch.randn生成的输入只用于trace时的形状探测不是需要固定的真实数据。推理接口用Flask写就够了不需要FastAPI。Flask的同步阻塞模型在这种低并发场景更直观代码量也更少。接口接收上传图片先用训练时一致的预处理流程转成张量再做推理。这里最容易踩的坑是忘了做Resize(256)和CenterCrop(224)直接把原图resize到224导致推理精度比验证时掉好几个点。from flask import Flask, request, jsonify from PIL import Image import torch import torchvision.transforms as transforms app Flask(__name__) model torch.jit.load(flower_traced.pt, map_locationcpu) model.eval() normalize transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) preprocess transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), normalize ]) CLASS_NAMES [fflower_{i:03d} for i in range(102)] # 替换为真实类别名 app.route(/predict, methods[POST]) def predict(): file request.files[image] image Image.open(file.stream).convert(RGB) tensor preprocess(image).unsqueeze(0) with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1) top3 torch.topk(probs, 3) result [{ class: CLASS_NAMES[idx], confidence: round(prob.item(), 4) } for idx, prob in zip(top3.indices[0].tolist(), top3.values[0].tolist())] return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000)启动后用curl -X POST -F imagetest.jpg http://localhost:5000/predict验证。返回Top-3结果比单返回Top-1实用得多——花卉识别场景中相似物种本就容易混淆Top-3里包含正确类别的概率远高于Top-1这也是我在真实项目里一直沿用的做法。验证时重点看三件事同一张图多次推理结果是否稳定、不同图片的置信度分布是否合理、CPU下单张延迟是否在可接受范围。最后一件事验证时还要检查模型的置信度校准。很多分类模型的置信度偏高实际只有60%把握时输出98%的置信度。写接口时可以加一个阈值判断低于阈值的返回无法识别这个阈值通过验证集统计每类最小置信度来确定。我在第一个花卉项目里没做这步结果接口对模糊图片总是自信地给出错误答案后来把置信度阈值调到0.5才把误判率降下来。希望帮到你。本文还有配套的精品资源点击获取
返回列表