尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于深度学习的水果识别系统:从数据清洗到模型部署的完整实战

基于深度学习的水果识别系统:从数据清洗到模型部署的完整实战 简介这是一套面向计算机相关专业学生与项目实战学习者的深度学习水果识别完整项目可直接用于课程大作业、毕业设计或算法练手。项目以Python为核心实现配套数据集与训练模型源码均经本地编译调试可正常运行难度适中评审得分98分适合需要快速搭建可演示识别系统、又缺乏完整工程参考的读者。压缩包共277个文件约17.53MB其中8个py文件承载模型训练与识别主逻辑114个js、26个css与7个html构成前端交互与展示页面另有gif、jpg、png等图像素材及字体、图标资源整体结构清晰便于按模块阅读与二次修改。目前已有141人学习下载。通过该资源读者可掌握从数据准备、模型训练到界面展示的完整流程理解水果分类项目的目录组织与关键代码并在此基础上替换数据集或调整网络结构完成自己的课题与答辩演示。1. 水果识别系统到底难在哪从一张误判的苹果照片说起把一张红富士的照片丢给刚训完的模型它信心十足地输出「番茄」置信度 0.91。这不是段子是我帮学弟调毕业设计时真实翻车的现场。基于深度学习的水果识别系统表面看是个烂大街的入门题目实际上它同时踩中了细粒度分类、类间相似、数据长尾三个坑。苹果和番茄、柠檬和青梨、不同成熟度的香蕉这些类别在像素层面高度重叠模型很容易学到「红色番茄」这种捷径特征。这套 Python 源代码加数据集的完整项目要解决的就是让模型真正区分这些长得像但语义不同的水果而不是靠背景颜色蒙答案。适合正在做毕业设计、想找一个能跑通又有话可说的题目的同学也适合想练手图像分类全流程的 Python 开发者。下面我按自己实际搭过一遍的顺序把选型、数据、训练、部署和踩坑讲清楚。2. 水果识别系统的技术选型为什么是 CNN 而不是上 Transformer2.1 从数据集规模反推模型复杂度毕业设计的数据集通常不会太大。常见做法是自己爬或者用公开水果数据集规模大多在几千到两三万张之间类别数在 10 到 30 类。这个量级下直接上 ViT 这类 Transformer 架构基本是自找麻烦注意力机制缺少归纳偏置小数据上极容易过拟合训练轮次拉长后验证集准确率反而往下掉。我一般会推荐 ResNet18 或 MobileNetV3 作为 backbone前者结构规整、残差连接成熟后者参数量小、推理快答辩演示时用普通笔记本 CPU 也能跑出实时效果。选 ResNet18 的另一个理由是迁移学习友好。ImageNet 预训练权重里已经包含大量纹理和边缘特征水果识别需要的颜色、形状、表面斑点这些低级特征可以直接复用只需要微调后面的层。相比之下从头训练一个 CNN 在几千张图上很难收敛到理想精度。这里有个参数要特别注意如果冻结全部卷积层只训练全连接精度往往卡在 70% 上下解冻最后两个 stage 一起微调通常能到 90% 以上。2.2 数据增强策略与水果类别的特殊性水果图像的增强不能照搬通用配方。水平翻转、随机裁剪、颜色抖动是标配但颜色抖动幅度要控制。水果分类高度依赖颜色把饱和度调过头会让青苹果变成红苹果的分布模型学到的特征就乱了。我一般把 brightness、contrast、saturation 的抖动系数设在 0.2 以内hue 干脆不动。旋转增强也要谨慎。香蕉、黄瓜这类长条水果旋转 90 度后语义虽然没变但和自然拍摄角度差异大适度旋转±15 度比任意角度旋转更稳。另外建议加 RandomResizedCrop让模型适应不同拍摄距离这对手机拍照场景的泛化很有帮助。MixUp 和 CutMix 在小数据集上能提点但会让训练曲线变得难读毕业设计答辩时不好解释看情况用。2.3 用 Python 搭出可复现的训练骨架下面这段是数据加载和增强的核心代码基于 torchvision 实现直接可跑。注意 normalize 的均值和方差用的是 ImageNet 统计值因为我们要加载预训练权重。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集增强翻转、裁剪、轻度颜色抖动 train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 验证集只做缩放和中心裁剪保证评估稳定 val_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]), ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) print(类别:, train_ds.classes) print(训练样本数:, len(train_ds))逻辑说明ImageFolder 要求数据按类别分文件夹存放每个子文件夹名就是类别标签这是最省事的组织方式。训练集和验证集用不同的 transform训练集做增强验证集保持确定性否则评估指标会抖动。batch_size 设 32 是显存和梯度稳定性的折中显存不够就降到 16同时把学习率按比例调小。num_workers 在 Windows 上如果报错就改成 0这是 DataLoader 多进程在 Windows 下的老问题。参数说明RandomResizedCrop 的 scale 下限设 0.7意味着最多裁掉 30% 的画面再小可能把水果主体裁没。RandomRotation 限 15 度避免长条水果出现不自然姿态。Normalize 的三个通道值必须和预训练模型一致写错会导致精度莫名其妙掉十几个点这个坑我踩过。3. 数据集准备与划分别让数据泄漏毁掉你的评估3.1 数据采集与清洗的实操要点数据集来源无非三种公开数据集、网络爬取、自己拍摄。公开数据集省事但类别可能和你的选题对不上爬取要注意图片质量和版权自己拍最可控但费时间。不管哪种来源清洗这一步不能省。常见脏数据包括同一张图重复出现、标签错标、非水果图片混入、严重模糊或遮挡的废图。我一般写个脚本先做去重用感知哈希pHash计算图片指纹汉明距离小于 5 的判为重复。然后人工过一遍标签重点看那些容易混的类别。清洗完的数据量如果每类少于 200 张建议要么补数据要么用更强的增强否则模型对这类水果的召回率会很难看。3.2 训练验证测试三分与类别平衡划分比例常见 7:2:1 或 8:1:1。关键是划分要在类别层面分层抽样保证每个集合里各类别比例接近。如果随机划分导致某个类别在验证集里只有两三张评估结果就没有统计意义。用 sklearn 的 train_test_split 加 stratify 参数可以一步到位。类别不平衡是水果数据集的常态苹果可能上千张榴莲只有几十张。处理方式有两种一是对少数类做过采样加增强二是在损失函数里加类别权重。我倾向后者改动小且不引入重复样本。CrossEntropyLoss 的 weight 参数传入各类别样本数的倒数归一化即可。import os import shutil from sklearn.model_selection import train_test_split root data/raw classes os.listdir(root) train_files, val_files, test_files [], [], [] for c in classes: files [os.path.join(c, f) for f in os.listdir(os.path.join(root, c))] # 先分训练验证 和 测试 tv, te train_test_split(files, test_size0.1, random_state42) # 再从训练验证里分验证 tr, va train_test_split(tv, test_size0.22, random_state42) train_files [(f, c) for f in tr] val_files [(f, c) for f in va] test_files [(f, c) for f in te] def copy_to(split_files, split_name): for rel, c in split_files: dst_dir os.path.join(data, split_name, c) os.makedirs(dst_dir, exist_okTrue) shutil.copy(os.path.join(root, rel), dst_dir) copy_to(train_files, train) copy_to(val_files, val) copy_to(test_files, test) print(划分完成)逻辑说明对每个类别单独做划分保证分层。先切出 10% 做测试集剩下的再按 22% 切验证集最终比例约为 7:2:1。random_state 固定住保证每次运行划分一致这对复现实验很关键。复制而不是移动保留原始数据改错了还能重来。参数说明test_size 和验证集比例可以根据数据量调整数据少的时候测试集别低于 10%否则评估方差太大。如果某个类别样本极少train_test_split 可能报错需要先检查每类数量。3.3 数据集目录结构规范训练代码依赖固定的目录结构整理清楚能省很多调试时间。标准结构如下data/ ├── train/ │ ├── apple/ │ ├── banana/ │ └── orange/ ├── val/ │ ├── apple/ │ └── ... └── test/ └── ...每个类别一个文件夹文件夹名用英文小写避免中文路径在部分环境下读取报错。图片格式统一成 jpg 或 png混用虽然 ImageFolder 也能读但统一格式能减少解码异常。4. 模型训练与调参让验证集准确率真正涨上去4.1 迁移学习的两阶段训练法前面提过冻结全部卷积层精度上不去全解冻又容易过拟合。我一般用两阶段第一阶段冻结 backbone只训练最后的全连接层学习率设 1e-3跑 5 到 10 个 epoch让分类头先适应水果类别。第二阶段解冻最后两个 stage学习率降到 1e-4继续训练 20 到 30 个 epoch。这样既利用了预训练特征又让高层特征适配了水果数据。优化器用 AdamW 比 SGD 省心weight_decay 设 1e-4 抑制过拟合。学习率调度用 CosineAnnealingLR比 StepLR 更平滑末期学习率趋近于零收敛更稳。import torch.nn as nn import torch.optim as optim from torchvision import models from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) num_classes len(train_ds.classes) # 加载预训练 ResNet18 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) # 第一阶段冻结 backbone for name, param in model.named_parameters(): if fc not in name: param.requires_grad False criterion nn.CrossEntropyLoss() optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max10) def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (out.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total for epoch in range(10): loss, acc train_one_epoch(model, train_loader, optimizer, criterion) scheduler.step() print(fEpoch {epoch1} loss{loss:.4f} acc{acc:.4f})逻辑说明第一阶段只让 fc 层参与梯度更新filter 过滤出 requires_grad 为 True 的参数传给优化器。train_one_epoch 里累加 loss 时乘了 batch 大小最后除以总样本数得到的是样本级平均损失比 batch 级平均更准确。准确率同理按样本统计。参数说明lr1e-3 是冻结阶段的常用值因为只训练分类头可以大一点。weight_decay 抑制权重过大。T_max 对应总 epoch 数CosineAnnealingLR 会在 T_max 轮内把学习率从初始值降到接近零。第二阶段把解冻层的 lr 改成 1e-4重新建优化器因为不同参数组需要不同学习率。4.2 第二阶段微调与早停第二阶段解冻 layer3 和 layer4学习率降到 1e-4。这时候要盯着验证集损失如果连续 5 个 epoch 不下降就早停保存验证集准确率最高的权重。早停能有效防止过拟合也省训练时间。# 解冻 layer3 和 layer4 for name, param in model.named_parameters(): if layer3 in name or layer4 in name or fc in name: param.requires_grad True else: param.requires_grad False optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) best_acc, patience, wait 0, 5, 0 for epoch in range(30): tr_loss, tr_acc train_one_epoch(model, train_loader, optimizer, criterion) # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) out model(imgs) correct (out.argmax(1) labels).sum().item() total imgs.size(0) val_acc correct / total scheduler.step() print(fEpoch {epoch1} val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best.pth) wait 0 else: wait 1 if wait patience: print(早停触发) break逻辑说明解冻策略只放开高层特征低层的边缘纹理特征保持冻结避免小数据上把通用特征带偏。验证阶段用 model.eval() 关闭 dropout 和 batchnorm 的训练行为torch.no_grad() 省显存。保存 best.pth 而不是最后一个 epoch 的权重因为末期可能已经过拟合。参数说明patience5 是经验值数据噪声大可以设 8数据干净设 3 也行。T_max 要和最大 epoch 数一致否则学习率调度和训练轮次对不上。4.3 评估指标不只看准确率准确率在类别不平衡时会骗人。如果苹果占 80%模型全预测苹果也有 80% 准确率。所以要同时看每类的 precision、recall、F1以及混淆矩阵。sklearn 的 classification_report 一行就能输出。重点看哪些类别被混淆比如柠檬和青梨如果互相误判多说明特征区分度不够可以考虑加数据或换更强的 backbone。5. 避坑与排查水果识别项目里最容易翻车的五件事5.1 验证集准确率远高于测试集现象验证集 95%测试集只有 70%。原因通常是数据泄漏同一张图或高度相似的图同时出现在训练和验证里。解决用感知哈希去重后再划分划分时按类别分层确保三个集合互不重叠。5.2 训练损失不下降或震荡现象loss 在 2.3 附近不动或者上下大幅震荡。原因可能是学习率太大、数据标签错乱、normalize 参数写错。解决先把学习率降一个数量级试再检查标签文件夹名和图片是否对应最后核对 normalize 的均值和方差是否和预训练模型一致。5.3 模型对某几类水果几乎全错现象混淆矩阵里某两类互相误判严重。原因这两类视觉特征太接近或者其中一类样本太少。解决针对性补充这两类的数据尤其是容易混的样本或者在损失函数里给这两类更高权重。5.4 推理时预测结果和训练时不一致现象训练时验证准确率很高部署后单张图片预测乱跳。原因推理时的预处理和验证集不一致比如忘了 normalize或者 resize 方式不同。解决把验证集的 transform 封装成函数训练和推理共用同一套。5.5 显存溢出或训练速度异常慢现象CUDA out of memory或者每个 epoch 要跑很久。原因batch_size 太大、num_workers 设置不当、图片分辨率过高。解决降 batch_sizeWindows 下 num_workers 设 0图片统一缩到 256 以内再送入网络。6. 从能跑到好用把水果识别系统做成可演示的完整项目训练出权重只是半成品毕业设计答辩要的是能演示、能交互的完整系统。我一般会加一个推理脚本和一个简单的界面。推理脚本负责加载 best.pth对单张图片输出类别和置信度界面用 Gradio 或 Streamlit 搭几十行代码就能做出上传图片即时识别的效果答辩时比命令行有说服力。import torch from PIL import Image from torchvision import transforms, models import torch.nn as nn def load_model(ckpt_path, num_classes): model models.resnet18(weightsNone) model.fc nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(ckpt_path, map_locationcpu)) model.eval() return model infer_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]), ]) def predict(model, img_path, classes): img Image.open(img_path).convert(RGB) x infer_tf(img).unsqueeze(0) with torch.no_grad(): out model(x) prob torch.softmax(out, dim1) conf, idx prob.max(1) return classes[idx.item()], conf.item() model load_model(best.pth, num_classeslen(train_ds.classes)) label, conf predict(model, test.jpg, train_ds.classes) print(f预测: {label}, 置信度: {conf:.4f})逻辑说明推理时的 transform 必须和验证集完全一致这是保证线上线下一一致的关键。load_state_dict 用 map_locationcpu 是为了在没有 GPU 的机器上也能加载。softmax 把 logits 转成概率取最大值对应的类别。参数说明num_classes 必须和训练时一致否则 fc 层维度对不上会报错。classes 列表的顺序要和训练时 ImageFolder 的 classes 顺序一致这个顺序是按文件夹名排序的改文件夹名会导致标签错位。进阶一点的做法是加一个置信度阈值低于阈值就输出「不确定」避免模型在没见过的水果上硬答。还可以用 Grad-CAM 可视化模型关注的区域答辩时展示热力图能直观说明模型确实在看水果本身而不是背景。这个技巧在解释模型行为时特别有用也是区分「跑通」和「做好」的分水岭。我自己做这类项目最大的教训是别一上来就调模型结构先把数据清洗和划分做扎实八成的精度问题都出在数据上。模型换来换去不如把标签看一遍。希望帮到你。本文还有配套的精品资源点击获取
返回列表