拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于ShuffleNet的菠萝成熟度分类:轻量级CNN实战

简介面向菠萝成熟度识别场景的轻量级卷积神经网络实战项目基于ShuffleNet模型对没熟、半熟、成熟等8个阶段进行分类适合希望完整掌握图像分类训练、评估与推理流程的学习者。7Z压缩包约201MB共2000个文件包括1992张样本图片、4个Python脚本、2个TXT说明、1个readme文档及1个JSON配置训练集4808张、测试集806张均按类别子文件夹组织结构清晰。该项目已有124人学习使用。网络参数量约一百万采用余弦学习率自动衰减策略训练50个回合测试集最高精度达87%并附有最优权重、训练日志及损失/精度曲线。运行预测脚本可自动读取inference目录下的全部图片将概率前三的类别绘制在左上角按照readme还能训练自定义数据代码会自动生成分类类别个数等配置便于快速迁移到其他分类任务。1. 把菠萝成熟度分类交给轻量级CNN为什么选 ShuffleNet 而不是 ResNet一份 201MB 的资源包包含完整的菠萝成熟度分类项目基于 ShuffleNet 对 8 种不同阶段的成熟度做图像分类。这听起来是个小众场景实际上菠萝的成熟度判断直接影响采摘时间、仓储周转和损耗率农业视觉里这种细粒度分类需求非常普遍。项目里的 4808 张训练图和 806 张测试图覆盖了从没熟、半熟到成熟的全过程已经按文件夹分好类解压就能直接用。选 ShuffleNet 而不是 ResNet、VGG核心原因是参数量只有一百万左右在嵌入式设备和边缘计算场景下推理速度快、内存占用低。对于菠萝这种类间差异不明显、类内差异很大的对象同一成熟度的菠萝颜色、纹理可能差别不小轻量网络配合合理的训练策略完全够用。项目训练了 50 个 epoch测试集最好精度到 87%这对农业场景已经具备实用价值。下面从数据组织、训练细节到预测推理逐步拆解这个项目最后会写清楚几个最容易翻车的坑。2. 数据集落位与训练脚本准备从下载到开始训练前必做的三件操作拿到项目包后先别急着跑训练。解压后确认目录结构是否符合预期这是后面所有操作的基石。这个数据集的组织方式很直观训练集和测试集各自是一个大文件夹里面按类别分子文件夹子文件夹的名字就是类别名同一类的图片放在同一个子文件夹里。这种做法是图像分类任务最常见的数据组织方式PyTorch 的 ImageFolder 机制原生支持这种结构。2.1 第一步把训练脚本跑通前先确认三类文件都在位项目要能正常启动需要确认三个部分齐全训练用的数据文件夹、模型定义文件、以及预训练权重。用下面的命令检查数据目录结构# 查看数据目录结构确认类别文件夹名称和图片数量 tree>from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), # 统一缩放为 ShuffleNet 标准输入尺寸 transforms.RandomRotation(15), # 随机旋转15度模拟不同拍摄角度 transforms.RandomHorizontalFlip(p0.5), # 随机水平翻转数据量等效翻倍 transforms.ColorJitter(brightness0.3, contrast0.3, # 调节亮度与对比度适应不同光照条件 saturation0.2), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], # ImageNet 预训练模型的标准化参数 std[0.229, 0.224, 0.225] # 与前三个数配套使用直接用即可 ) ]) test_transform transforms.Compose([ transforms.Resize((224, 224)), # 测试集只做缩放不做增强 transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ])两套 transform 的区别在于训练集加了旋转、翻转和色彩变化让模型在有限的 4808 张图中见到更多样化的输入测试集保持原始状态这样才能用统一的评估标准衡量模型的真实表现。颜色增强的幅度不要过大过大反而会让成熟度更高的菠萝颜色失真模型会把“颜色异常”和“成熟度高”混淆起来。3. 训练阶段参数配置学习率策略、损失函数与训练轮数的搭配逻辑模型选型定下来后训练策略就是决定最终精度的关键变量。项目采用了常见做法中的余弦退火调度器来逐步衰减学习率并训练 50 个 epoch最终测试集精度为 87%。在理解这个策略之前先明确一个前提轻量网络的特征表达能力有限训练策略必须精细化才能逼近其性能上限。3.1 余弦退火学习率为什么它比固定学习率更适应轻量网络固定学习率在训练后期容易遇到两个问题学习率大导致在损失曲面底部来回震荡无法收敛到最优解或者学习率过小导致收敛速度太慢。余弦退火策略的理念是让学习率在整个训练过程中按照余弦函数的曲线逐渐下降到接近 0。这个设计既能让模型在早期以较快速度探索损失曲面又能在后期以精细的步长稳定收敛到更好的局部最优点。在这种细粒度分类任务中成熟度相邻类别的特征差异很小最后几个 epoch 的小学习率阶段往往就是精度提升的关键区间。训练过程本质上是先用较大学习率快速锁定大致区域再用小学习率精细调整类别边界。在代码里常见的实现方式是使用 PyTorch 的 LambdaLR 或 CosineAnnealingLR 调度器两者在效果上对训练后期收敛的影响有差异。下面给出一个可以嵌入训练流程的余弦退火配置片段和项目采用的训练策略一致import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR # 训练超参数 num_epochs 50 init_lr 0.01 # 初始学习率可根据数据量适当调整到 0.005~0.02 weight_decay 1e-4 # 权重衰减防止模型在少量数据上过拟合 # 以当前项目的 8 分类为例最后一层输出维度改为 8 model.fc nn.Linear(model.fc.in_features, 8) # 优化器SGD 搭配动量在轻量网络上效果稳定Adam 收敛快但容易错过精调区域 optimizer optim.SGD(model.parameters(), lrinit_lr, momentum0.9, weight_decayweight_decay) # 每训练完一个 epoch 更新一次学习率 scheduler CosineAnnealingLR(optimizer, T_maxnum_epochs) for epoch in range(num_epochs): # training_loop 示意每个 epoch 跑一遍训练数据 train_one_epoch(model, train_loader, optimizer, criterion) # 更新学习率 scheduler.step() # 验证集精度记录保存最优权重 accuracy evaluate(model, test_loader) print(fEpoch [{epoch1}/{num_epochs}] lr{scheduler.get_last_lr()[0]:.6f} acc{accuracy:.4f})T_max 设成 num_epochs 的效果是学习率在 50 个 epoch 内从初始值平滑降到接近 0完成一个完整的余弦周期。SGD 加动量的组合在轻量网络上通常比 Adam 更容易精调出更高的精度因为它在训练后期能维持更稳定的更新方向。权重衰减设为 1e-4 是图像分类任务的标准取值能有效抑制模型权重过大提升泛化能力。如果训练中发现精度上升得太慢可以检查前几个 epoch 的学习率变化曲线。余弦退火在早期阶段的下降速度较快如果初始学习率设置偏小模型可能还没充分探索就到了低学习率阶段。另一种常见做法是配合 warmup 策略让学习率在前五个 epoch 线性上升这可以避免训练早期震动过大。3.2 训练轮数与损失函数的选择理解 50 个 epoch 的上限和下限50 个 epoch 在这个数据规模下是一个经验值。数据量 4808 张类别数 8 类平均每类约 600 张。用轻量网络在这样的数据规模下训练,通常 30 到 50 个 epoch 就能达到收敛状态继续训练收益有限。如果项目简介没有提到加大 epoch 能提升精度那说明数据集本身已能够支撑模型的充分学习训练轮数并非影响精度的关键瓶颈。损失函数方面8 分类任务直接用交叉熵损失这是合理的默认选择。模型输出的 logits 经过 Softmax 转换为各类别的概率分布交叉熵通过计算预测分布与真实标签分布的差异来指导模型优化。对于成熟度这种有序类别有些情况下可以考虑使用回归损失把类别映射为连续值但前提是任务本身对连续度的要求明确否则贸然改变任务定义可能破坏数据组织方式。3.3 训练过程中的监控指标除了 accuracy还应该盯哪些量只看测试集精度会漏掉很多信号。训练时会保存日志和曲线主要应该关注 loss 曲线、学习率曲线和验证集精度三条曲线。Loss 曲线的下降形态能反映整体收敛状态学习率曲线确认退火进度验证集精度曲线判断最优权重出现的位置。如果验证集精度曲线在训练后期还保持上升趋势说明 50 个 epoch 不够应该加大训练轮数如果曲线在某个点达到峰值后开始波动下降说明模型开始过拟合最优权重应该取峰值时刻。另外一个容易遗漏的点是类别的混淆情况。8 种成熟度之间的边界模糊模型容易在相邻成熟度类别之间犯错。训练日志保存的是整体精度和平均 loss这些指标无法反映具体是哪两个类别最容易混淆。最佳实践是在训练完成后对测试集做一次完整的混淆矩阵分析找出模型系统的错误模式后续迭代优化时才有明确方向。4. 八类菠萝成熟度数据集的特殊性处理类别不均衡与过拟合的应对方案八类成熟度的标注天然带有细粒度属性相邻类别比如“半熟”和“接近成熟”的图像差异可能非常微小。这种数据集落到实际训练中会引出一系列实际问题和相应的解决经验。只有在训练前理解数据本身的特殊性才能看懂训练过程中的各类现象是怎么发生的。4.1 类别不均衡问题先做统计再决定要不要上加权损失多数真实分类数据集都存在类别分布不均衡的问题这个项目也不例外。对收集到的菠萝图像做统计可能发现某些成熟度阶段的样本量明显少于其他阶段例如“完全成熟”的样本很容易拍而“刚成熟”阶段的样本因为时间窗口短采集难度大数量可能跌到 200 张以下。模型对训练样本量少的类别特征学习不充分在测试时容易被误判到样本量大的类别。判断是否不均衡先用代码统计各类别样本数量计算最大值与最小值的比值。如果比值超过 3 倍就值得采取措施from collections import Counter import os import numpy as np # 统计训练集中每个类别的样本数评估不均衡程度 train_root data-train class_counts Counter({d: len(os.listdir(os.path.join(train_root, d))) for d in os.listdir(train_root)}) counts np.array(list(class_counts.values())) print(各类别样本数:, dict(class_counts)) print(f最大/最小比值: {counts.max() / counts.min():.2f}) print(f总样本数: {counts.sum()}) # 如果比值超过3倍使用类别权重来平衡损失对少数类的过小贡献 # 方案1在损失函数中按样本量反比加权 if counts.max() / counts.min() 3.0: weights 1.0 / counts weights weights / weights.sum() * len(class_counts) # 归一化到类别数 class_weights torch.tensor(weights.tolist(), dtypetorch.float32).cuda() criterion nn.CrossEntropyLoss(weightclass_weights) else: criterion nn.CrossEntropyLoss()类别加权损失的原理是给样本量少的类别更高的损失权重强制模型在这些类别上多花精力学习。值得权衡的是给少数类大幅加权虽然能提高少数类的准确率但也会导致多数类性能下滑需要观察整体精度变化来决定是否使用。从本项目的简介来看训练脚本默认使用普通交叉熵说明数据分布大概率是相对均衡的轮不到这个问题。但放到自己的数据上就必须提前验证这一点。4.2 成熟度类别边界模糊的本质图内相似度高而图间差异小菠萝的成熟度是一个连续变化过程切分成 8 个离散阶段后相邻阶段的图像特征在很多情况下只是颜色饱和度的微小差异。这和猫狗分类类间差异大不同成熟度分类更像细粒度识别模型需要捕捉非常细微的模式差异。轻量网络受限于参数量特征提取能力无法与大型网络比肩因此对输入图像的质量和预处理方式更加敏感。在两者间取得平衡最有效的操作是统一输入分辨率。项目的 resize 目标为 224×224这对应 ImageNet 预训练模型的标准输入。如果你的图大部分是手机竖屏拍摄直接拉伸到 224×224 会让长宽比严重变形但实际测试中绝大多数预训练模型并不在乎这一点反而比保持长宽比后加 padding 效果更稳定。一个值得尝试的方向是微调输入分辨率到 256×256 或 192×192观察精度变化看模型对细节感知是否有所改善。4.3 权重初始化和预训练选择这决定你的训练能走多远很多人忽略的一环用不用 ImageNet 预训练权重在轻量网络上差距巨大。ShuffleNet 在 ImageNet 上预训练后已经学会了基础的颜色、纹理、边缘特征迁移到菠萝数据集只需要微调全连接层和部分卷积层。从零训练则要求模型在 4808 张图上重新学习这些基础特征以轻量网络的表达能力和数据规模来看很难达到理想效果。实际执行时在训练脚本中用torchvision.models.shufflenet_v2_x1_0(pretrainedTrue)加载预训练权重再把最后一层全连接替换为输出维度等于类别数的新全连接层。对新加的层可以采用稍大的学习率加速收敛对预训练层保持较小的学习率防止灾难性遗忘。这通常有两种实现方式一是整体统一更新最简单效果也可接受二是分组设置不同学习率精细可控但需要多一步参数分组# 只训练新加的全连接层和最后两个 stage前面的层尽量冻结以保留通用特征 # 这是轻量网络在小数据集上兼顾“收敛速度”和“精度”的常见做法 optimizer optim.SGD([ {params: model.features[:12].parameters(), lr: 0.001}, # 低学习率微调底层 {params: model.features[12:].parameters(), lr: 0.005}, # 较高学习率微调高层 {params: model.fc.parameters(), lr: 0.01} # 全连接层从头学学习率最高 ], momentum0.9, weight_decay1e-4)这种分组策略既保留了预训练模型前期提取通用特征的能力又给了高层特征和分类层足够的学习空间来适配菠萝图像。如果训练资源有限或数据集规模更小可以适当增大大规模冻结的范围只微调最后一级的卷积层和全连接层。5. 预测推理的自动化流程图片目录遍历与 Top-3 类别输出训练完成后模型的实际应用价值体现在预测推理阶段。项目在预测脚本中实现了一个自动化机制代码自动将 inference 目录下所有图片逐张推理并将概率最高的前三个类别的名称和概率值绘制在图片左上角。这种做法的实战意义在于批量处理多张图片时无需逐张手动指定直接把待分类图片丢进目录就能得到结构化结果。5.1 推理脚本的执行逻辑与输出结构预测流程的完整链路从上到下依次是加载训练好的最佳权重、遍历 inference 目录所有图片、逐张预处理→前向传播→概率排序、画框并标注 Top-3 类别、保存结果图片、输出文本日志。关键代码结构如下import torch import torch.nn as nn from torchvision import transforms, models from PIL import Image, ImageDraw, ImageFont import os import json # 1. 加载模型结构并载入训练权重 model models.shufflenet_v2_x1_0(pretrainedFalse) model.fc nn.Linear(model.fc.in_features, 8) checkpoint torch.load(run_results/best_model.pth, map_locationcpu) model.load_state_dict(checkpoint[state_dict]) model.eval() # 2. 类别名称需要与训练时保持一致建议通过读取训练集的子目录名自动生成 class_names sorted([d for d in os.listdir(data-train) if os.path.isdir(os.path.join(data-train, d))]) # 3. 遍历推理目录下的所有图片并逐个预测 inference_dir inference results [] for img_name in sorted(os.listdir(inference_dir)): img_path os.path.join(inference_dir, img_name) if not img_name.lower().endswith((.jpg, .jpeg, .png)): continue img Image.open(img_path).convert(RGB) input_tensor test_transform(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): outputs model(input_tensor) probs torch.softmax(outputs, dim1)[0] # 取概率最大的前3个类别 top3_probs, top3_indices torch.topk(probs, 3) # 4. 把类别名和概率绘制在图片左上角结果保存到输出目录 draw ImageDraw.Draw(img) y_offset 10 for i in range(3): label class_names[top3_indices[i]] prob top3_probs[i].item() text f{label}: {prob:.3f} draw.text((10, y_offset), text, fill(255, 0, 0)) y_offset 20 img.save(os.path.join(output, img_name)) results.append({image: img_name, predictions: top3_indices.tolist()}) # 5. 输出结构化结果方便后续程序化处理 with open(output/predict_results.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)这段代码把“目录遍历、前向推理、Top-3 绘制”三件事一次性做完了。常规的工程实践还会把预测结果同时输出为 JSON 文件方便后续接质检系统或机器人分选链路时直接解析数据而不是依赖人眼查看图片。5.2 推理阶段的两个关键细节类别顺序与设备兼容类别顺序问题是最容易在推理时翻车的点。ImageFolder 生成标签时按字母序排列子目录名称比如[mature, overripe, ripe, ...]而推理脚本如果硬编码一个不同的顺序预测结果就会错位。防止这种情况的有效做法是让推理脚本在运行时动态读取训练集目录的名称并排序保证脚本端与训练端的类别索引始终保持一致。设备兼容问题同样值得提前处理。训练时用 GPU 加速推理时可能切换到 CPU 环境尤其是部署到边缘设备时。加载权重文件时加map_locationcpu能确保在无 GPU 的环境下也能顺利运行。如果未来要部署到实际产线建议转成 ONNX 用 TensorRT 加速或直接量化成 INT8 模型部署收益比继续改进 PyTorch 推理快得多。5.3 推理脚本易踩的坑图像读取模式与 batch 维度两个高频错误值得特别留意。第一PIL 读取图片时如果图像本身是 RGBA 四通道或灰度图直接送入模型会报形状错误。正确做法是先调用img.convert(RGB)统一转换为三通道标准格式。这看似是基础操作但用户自定义数据时经常遗漏。第二to_tensor得到的张量 shape 是[C, H, W]模型要求输入带 batch 维度[B, C, H, W]手动unsqueeze(0)是常见解法。另一种可选方案是直接用torchvision.transforms.functional.to_tensor(img).unsqueeze(0)效果相同且代码更简洁。遇到 shape mismatch 的报错优先排查这两处。6. 精度上限验证与可迁移改造把这份资源用出更大的价值如果你让模型跑了几个 epoch发现精度确实到了 85% 左右这时候有价值的工作已经不是继续调参而是确认这个 8 分类任务的上限在哪里同时评估能不能把这个方案复用到别的细粒度分类场景里。一个很实际的方法是刻意压测模型的边界。把测试集里模型预测错误但置信度很高的样本单独挑出来统计它们的特征和标签你会看到大量误判集中在相邻成熟度之间。反过来想如果业务上允许把 8 类合并成“未熟 / 可采 / 过熟”三档模型精度通常会极大幅度提升因为类间距离变大后边界模糊问题被天然规避了。总结我的经验做农业视觉分类时先确认业务到底需要几档不要盲目迁就数据集的标注粒度。标注 8 类往往只是为了采集时能区分实际业务里可能不需要。换个维度看这套方案迁移到其他类似的物品成熟度分类场景是完全可行的。比如香蕉成熟度、牛油果成熟度甚至工业检测场景中的表面缺陷等级分类——只要数据组织成文件夹目录结构、类别差异用肉眼可分辨这个项目的主干代码就能直接复用需要改的只有三个数类别数、输入分辨率、epoch 数量。要特别注意冻结层的问题。换到新数据集时预训练模型的前几层可以继续冻结但最后几层卷积必须解冻微调。我遇到不少初学者直接冻结所有层只改全连接输出导致新数据集上怎么训精度都上不去。实践总结出的合理判断是和 ImageNet 分布差异越大的数据需要解冻的层数越多甚至全网络解冻重训效果好。也可以尝试在边缘端部署。把 PyTorch 模型转成 ONNX再用 TensorRT 优化在 Jetson Nano 这类设备上推理一张图能达到毫秒级。这里顺带提一句之前的教训转 ONNX 时如果模型里用了动态控制流或自定义算子中途很容易卡住报错最好先用测试图验证输出一致性再部署。如果你打算继续提升精度有两个更明确的推进方向优先级从高到低排序。一是在目前 87% 的基础上继续训练 20 到 30 个 epoch但要配合更强的正则化手段如 label smoothing防止过拟合二是换成 ShuffleNet V2 的 1.5 倍宽度版本参数量增加约 50%精度通常可以有 1 到 2 个百分点的提升代价是推理速度略降。从那以后我每拿到一个预训练模型都会先做一次单 epoch 的测试训练确认数据加载、模型前向、损失计算、权重保存这一整套链路没问题再开完整训练流程。因为训练到第 40 个 epoch 才发现数据预处理写错了那才是真正的灾难这个习惯帮我避免了很多次重跑 50 个 epoch 的尴尬希望也能帮你少踩一些坑。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门