
简介基于深度学习的“水果识别系统”高分毕设资料包含完整源码、文档说明、数据集与训练好的模型面向计算机、人工智能、通信工程等专业学生、教师及企业开发者既适合毕设/课设参考也适合深度学习初学者进阶实践。项目采用迁移学习方式在ImageNet预训练权重基础上微调VGG16、ResNet50、MobileNetV2、DenseNet121四种模型于水果数据集上完成分类任务最高准确率达93.08%。压缩包共277个文件以js、gif、css等前端资源为主另有py脚本、html页面、txt说明文档及模型权重文件整体大小仅17.53MB轻量易用。目前已有319人学习可直接用于项目演示、毕业设计或二次开发。除训练代码外资源还提供可交互的Web识别界面后端由Python调用模型完成预测前端融合bootstrap、layui等框架数据集与模型均打包在内可快速复现实验也可在此基础上升级算法或扩展其他水果类别。项目源码已测试通过答辩评审平均分达96分内容完整规范符合高分毕设要求。1. 深度学习水果识别系统值不值得当毕设做每年到毕设季总有人选“水果识别系统”这类题。理由是数据集好找、效果直观、演示时放个橘子或苹果就能看到结果。但真正动手才发现光是把模型训练到95%准确率不够还要回答三个问题你的数据是从哪来的能不能讲清楚模型为什么这么选有没有对比依据最后交付的源码、文档、数据集、模型是不是一套能重复跑通的东西这个标题把四样东西捆在一起本质上是想让你在有限时间内拿到一条完整链路数据整理 → 模型训练 → 结果验证 → 文档包装。适合课设、毕设也适合想快速搭建一个视觉识别 Demo 的从业者。我的经验是这块地的难点不在准确率能到多高而在怎么把训练和交付的坑一一填平。2. 先把地基打对水果识别该用分类还是检测数据和模型怎么选2.1 分类和检测的区别单果图用分类复杂场景得用检测标题里的“水果识别”其实有歧义。它可能是图像分类这张图属于苹果/香蕉/橘子也可能是目标检测图里多个水果各自框出来并标上类别。我一般建议毕设场景优先从图像分类入手因为数据集好组织、训练资源要求低、演示也直观。如果题目要求是“识别”而非“检测”分类通常就能交差。判断标准很简单你的输入图是单个水果特写还是一整盘混合水果如果只是单个水果特写分类模型足够。如果输入图是一盘水果苹果、香蕉、橘子同时出现分类模型会把整张图归为一类这叫场景判断不叫水果识别。这种场景要换成 YOLO 这类检测模型或者先用检测框出来再逐个分类。从交付角度看分类模式更容易让“识别效果”稳定。检测模型在背景复杂时容易漏框、错框一旦掉点答辩时不好演示。检测数据的标注成本也高很多需要画框并生成坐标文件分类数据只需要按文件夹分好类别。除非任务书写了“定位”“框选”“计数”否则我建议用分类把主流程打通。开题阶段最好先跟导师确认任务类型避免做一个“识别系统”结果是分类但导师期望的是检测。2.2 公开数据集和自建数据怎么权衡类别数、光线、背景都要控制数据集是水果识别系统的命根子。公开数据集的好处是量大、类别统一、有标准划分适合快速验证模型选型。但公开集普遍在实验室条件下采集光线和背景干净模型学习到的是“无菌环境”下的水果特征。一旦换成食堂、宿舍的实拍图光线一偏准确率可能掉 5 到 10 个点。所以公开数据集适合做预选型自建数据才是让系统真正能落地的关键。自建数据集的常见做法是手机拍摄加网上找图。手机拍摄会让模型更贴近真实部署场景但拍摄角度、距离、光线、背景都要覆盖。每类至少收集 300 到 500 张否则类别一多模型很容易过拟合。划分比例我一般用 60% 训练、20% 验证、20% 测试。测试集必须和训练集、验证集完全独立最好按拍摄时间或来源区分避免同一张图被切进两个集合。类别平衡也要控制。如果苹果 10 类中有 1000 张香蕉只有 200 张训练时模型会把香蕉当成稀有类别收敛不稳。解决办法是给每个类别设置逆频率采样权重或者直接用 PyTorch 的WeightedRandomSampler。数据增强能辅助比如随机旋转、亮度抖动、裁剪但不能把增强当作解决数据量不足的主要手段。训练前记得人工浏览一遍每个类别确认没有混图、没有重复图、没有明显低像素的小图这一步能避免后面大面积返工。2.3 模型选型清单ResNet、MobileNet、EfficientNet 怎么挑模型选型影响训练速度、精度和部署难度。在深度学习 CNN 方案里我通常优先试 ResNet 系列。ResNet 的残差连接解决了深层网络梯度消失问题预训练权重也丰富作为毕设基线不会出错。MobileNet 参数少、速度快适合最后放到笔记本 CPU 上做实时识别EfficientNet 精度更高但训练和推理都更慢对显存要求也更高。下表是我常用的选型对照模型优点缺点推荐场景ResNet18/50训练稳定预训练权重多参数量相对大毕设首选MobileNetV3轻量CPU 可跑精度略低低配置演示EfficientNet精度高显存占用大有较好 GPU 时选用YOLOv8检测分类一体标注成本高题目要求检测时选用选择步骤也不复杂先看自己的显卡显存小于 4G 就选 MobileNetV3 或 ResNet18显存 6G 以上可以试试 ResNet50。再看数据量每类只有两三百张时ResNet18 比 ResNet50 更不容易过拟合。最后看部署目标如果要输出 Web 界面或桌面程序模型文件越小越好MobileNet 的权重文件通常只有几十 MB打包更方便。不要盲目加载最大的模型。我见过不少同学把 ResNet152 拉起来跑结果单张推理接近一秒完全不适合演示。毕设要的是“能跑、能讲、能演示”不是刷榜。如果题目没指定ResNet18 是投入产出比最稳的选择。数据量小就优先考虑迁移学习用在 ImageNet 上预训练好的权重做初始化比从零训练收敛快得多。2.4 训练参数预设学习率、batch size、Epochs 怎么定训练参数是新手最容易照抄然后翻车的地方。常见预设是优化器用 Adam学习率初始设为 0.001batch size 根据显存选 32 或 64Epochs 先定 30。听起来没问题但如果数据集很小0.001 偏大会导致 loss 震荡如果数据增强很猛训练到 20 轮还没收敛也正常。下面这张参数表是我实际训练时常用的起点参数推荐起始值调整依据学习率1e-3 到 1e-4小数据集用 1e-4迁移学习微调用 1e-4Batch size16 / 32 / 64显存内尽量大但过大收敛会变慢Epochs30 到 50用早停策略验证集 loss 不再下降就停优化器Adam默认 beta 参数即可SGD 需要更多 epochWeight decay1e-4数据量小时防止过拟合迁移学习时要分层设置学习率。常见做法是先冻结骨干网络只让最后的全连接层学习学习率可以稍高比如 1e-3跑 5 到 10 轮后再解冻全部层用 1e-4 微调。这样既保留预训练特征又让分类头适应新类别。很多人忽略的是输入分辨率水果识别统一缩放到 224x224 或 256x256 都可以但训练和推理必须一致。如果训练时用了RandomResizedCrop推理时直接用CenterCrop效果会有肉眼可见的差别。调参别靠玄学每次实验都要记录。我的习惯是给每次训练写一行备注比如“加了亮度抖动验证集提升 1.2%”。这个实验记录在答辩时非常好用比口头解释有说服力得多。3. 用 ResNet 在本地跑通第一版水果识别环境搭建、数据准备与训练命令3.1 用 conda 建干净环境安装 PyTorch 和 torchvision这一步的目标是让所有人都能按命令复现环境。我一般用 conda 创建虚拟环境避免系统 Python 被不同项目的依赖污染。下面是在本地搭建环境的常用命令# 创建并激活 Python 3.10 的虚拟环境 conda create -n fruit python3.10 conda activate fruit # 安装 PyTorch 与 torchvisioncu118 对应 CUDA 11.8 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 训练和可视化需要的辅助库 pip install pandas matplotlib tqdm opencv-pythoncu118是常见的 CUDA 版本代号如果你的机器没有 NVIDIA 显卡可以换成cpu版本训练慢一些但功能完整。Python 建议用 3.10 或 3.11兼容性比较好。需求明确后可以把这些依赖写进requirements.txt方便答辩时在别的机器上复现。参数说明--index-url指定 PyTorch 官方 whl 源避免默认源装错版本。torch 和 torchvision 版本需要对齐否则加载预训练权重时经常报错。装好之后可以用python -c import torch; print(torch.__version__)验证。3.2 把数据集整理成 ImageFolder 目录结构PyTorch 的ImageFolder会自动按目录名生成类别标签这是分类任务最省事的方案。目录结构一般是data/fruits/ ├── train/ │ ├── apple/ │ ├── banana/ │ └── orange/ ├── val/ │ ├── apple/ │ ├── banana/ │ └── orange/ └── test/ ├── apple/ ├── banana/ └── orange/训练前先用一段小脚本统计每个类别的数量发现不均衡问题import os data_root data/fruits train_dir os.path.join(data_root, train) classes os.listdir(train_dir) print(类别列表:, classes) for cls in classes: img_dir os.path.join(train_dir, cls) images os.listdir(img_dir) print(f{cls}: {len(images)} 张)这段代码的作用是快速检查每类图片数量。如果发现某类只有几十张要么补数据要么在训练时做类别加权。图片格式尽量统一为 jpg文件命名不要包含中文和空格否则在跨平台复制时容易出问题。3.3 训练脚本ResNet18 迁移学习与数据增强下面是一个可以直接保存为train.py的最小训练脚本。它包含了数据增强、冻结骨干、只训练分类头的完整流程import torch from torch import nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models # 训练数据增强随机裁剪、水平翻转、亮度抖动、归一化 train_transforms transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # ImageFolder 直接按目录分配标签 train_data datasets.ImageFolder(data/fruits/train, train_transforms) train_loader DataLoader(train_data, batch_size32, shuffleTrue, num_workers2) # 加载预训练 ResNet18替换最后全连接层 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) num_classes len(train_data.classes) model.fc nn.Linear(model.fc.in_features, num_classes) # 第一步冻结骨干只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr0.001) # 先跑 10 轮分类头 for epoch in range(10): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() out model(images) loss criterion(out, labels) loss.backward() optimizer.step() running_loss loss.item() print(fepoch {epoch1}, loss{running_loss/len(train_loader):.4f})几个关键点的说明RandomResizedCrop(224)每次从原图随机裁剪一块区域并缩放到 224x224相当于模拟不同尺寸和遮挡ColorJitter调整亮度和对比度让模型对光线变化更鲁棒Normalize使用 ImageNet 的均值和标准差因为加载的是 ImageNet 预训练权重输入分布要匹配。batch_size32需要约 2 到 4 G 显存不够就改成 16。CrossEntropyLoss是多分类标准损失Adam适合训练不过深的迁移学习任务。完成分类头训练后要继续微调全部层这样才能让模型真正适应水果细节# 解冻全部层降低学习率微调 for param in model.parameters(): param.requires_grad True optimizer torch.optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4)解冻后学习率必须降到1e-4甚至更低否则预训练权重会被快速打乱。weight_decay1e-4是 L2 正则数据量不大时能压低过拟合。微调轮数可以继续 20 轮配合验证集 loss 做早停。3.4 训练完成后怎么导出和验证模型训练结束后保存模型不能只存一个model.state_dict()还要把类别映射一起存进去。下面是我常用的保存方式torch.save({ model_state_dict: model.state_dict(), class_to_idx: train_data.class_to_idx, num_classes: num_classes }, fruit_resnet18.pth)加载模型做推理时需要先重建模型结构再读入权重同时把索引映射反转成类别名# 新建模型并加载权重 model models.resnet18(weightsNone) model.fc nn.Linear(model.fc.in_features, num_classes) state_dict torch.load(fruit_resnet18.pth, map_locationcpu) model.load_state_dict(state_dict[model_state_dict]) model.eval() # 反转映射{0: apple, 1: banana, ...} idx_to_class {v: k for k, v in state_dict[class_to_idx].items()}验证集评估时算一下 top-1 准确率和每类准确率。如果某类准确率明显低于其他类大概率是数据量不足或特征太像。保存模型时建议把验证集准确率写进文件名比如resnet18_0421_acc93.2.pth后续调版本时不会搞混。4. 避坑从训练到真实场景这 5 个问题最容易让识别系统翻车4.1 数据泄露让验证集虚高换个场景就崩现象训练时验证集准确率 95%一到自己拍的测试图只有 70%。这个翻车场景在毕设里太常见了。原因数据划分前没有按“来源”独立随机打乱导致同一批连续采样的图片同时进了训练和验证。比如用手机连拍一串苹果画面几乎一样模型其实是在记忆背景不是真正认苹果。解决按拍摄时间、拍摄批次来划分数据集。简单做法是先按文件夹批次划分再把批次中的图片分到训练和验证而不是把所有文件打散后再随机分。测试集更要独立最好和训练集来自完全不同的来源。4.2 类别不均衡让 Accuracy 变成虚高指标现象数据集中香蕉占 80%模型把所有图都猜香蕉准确率照样有 80%可苹果和橘子几乎全错。原因CrossEntropyLoss在多类不均衡时会被多数类主导模型不需要学习少数类特征也能拿到低 loss。解决统计每类图片数量给少数类增加采样概率。常用WeightedRandomSampler权重可以设为样本数的倒数from torch.utils.data import WeightedRandomSampler class_counts [800, 200, 250] # 每类图片数 weights [1/c for c in class_counts] sampler WeightedRandomSampler(weights, num_samplessum(class_counts), replacementTrue)评估指标换成 Macro F1不看全局 Accuracy防止被多数类带偏。答辩时这时候老师大概率会问“准确率是不是被大类带起来的”这个指标能帮你顶住。4.3 训练曲线震荡别急着加 Dropout现象loss 先降到 0.8 之后开始上下弹不继续降很多人第一反应加 Dropout结果模型欠拟合。原因最常见的是学习率太高或 batch size 太小导致梯度噪声大。Dropout 只能处理过拟合解决不了优化器在错误参数空间里震荡的问题。解决先把学习率降到1e-4如果 batch size 只有 8试着提到 32。如果验证集 loss 开始上升而训练集 loss 还在下降那才是过拟合再考虑加 Dropout 或数据增强。把训练曲线截图存档答辩时能说清楚判断依据。4.4 模型学到的是桌子而不是水果现象把没有水果的空桌面误判成苹果或者换一块桌布后水果识别率直线下降。原因训练集所有图片背景都是同一个桌面、同一块布卷积网络会把背景纹理一起当成判别特征。模型不一定会关注果实区域。解决采集数据时换 3 到 5 种背景训练时做随机裁剪让水果在画面中的占比变化。另一个办法是训练后用热力图看看模型到底在看哪里——如果高亮区域集中在桌子纹理而不是果实说明数据采集方式有问题要重采。这个习惯能提前暴露两次背景污染问题。4.5 模型权重文件保存和加载版本不匹配现象在服务器上训练好的.pth文件拷到本地加载报Missing key(s)或size mismatch有时训练时用 GPU加载到 CPU 又报错。原因PyTorch 版本不一致、模型定义结构不一致或者保存时把整个model对象而不是state_dict存了。torch.save(model)会在换环境时尝试重建完整的类路径一旦类没在 import 范围内就失败。解决统一保存state_dict并同时记录class_to_idx。加载时先手动重建模型结构再用load_state_dict。跨设备加载要加map_locationcpu。另外把 PyTorch 版本写进文档重装环境时尽量保持一致。5. 把零散文件变成完整交付物源码、文档、数据集、模型怎么组织5.1 源码目录结构train / eval / inference / utils 的职责划分一套能拿去答辩的源码不是一个大文件夹里堆一堆.py文件。我习惯按职责分目录结构如下fruit_recognition/ ├── data/ │ ├── train/ │ ├── val/ │ └── test/ ├── models/ │ ├── resnet18_0421_acc93.2.pth │ └── class_to_idx.json ├── docs/ │ ├── 需求分析.md │ ├── 系统设计.md │ └── 测试报告.md ├── train.py ├── evaluate.py ├── inference.py └── utils/ ├── datasets.py └── metrics.py职责划分很清楚train.py负责读数据、训练、保存模型evaluate.py在测试集上计算准确率和 F1并输出混淆矩阵inference.py加载模型对单张图或一个文件夹做预测utils/datasets.py放数据增强、加载逻辑utils/metrics.py放指标计算。models目录只放权重文件和类别映射不写代码。train.py最好支持命令行参数方便现场调参。常见做法是用argparse接收--data、--epochs、--lr、--batch_size这样答辩演示时可以现场换个数据集或调一个参数证明这是工程化了的脚本而不是写死的。5.2 毕设文档怎么写需求分析、系统设计、测试章节放什么表格文档说明不是把代码注释抄一遍。需求分析部分要写清楚功能需求和非功能需求。功能需求包括能识别水果类别、显示置信度、支持单张图片输入非功能需求包括准确率目标、单张识别时间上限、运行环境。系统设计部分要写模型结构、数据集组成、训练流程和评估指标。测试章节最有说服力的是模型对比表格。我建议至少做两个对照不同模型ResNet18 和 MobileNetV3在同一数据集上的表现以及同一模型在不同数据增强策略下的表现。模型准确率Macro F1CPU 推理时间结论ResNet1893.2%0.9245ms推荐MobileNetV390.1%0.8928ms轻量场景表格里的数值按你实际跑的结果填关键是让导师看到你做了实验对比而不是只交一个脚本。测试报告里再加一组混淆矩阵把易混类别整理成文字说明这一项在答辩时非常加分。5.3 数据集清单与标注规范说明交付数据集时需要一个清单文件说清楚类别数、图片总数、每类张数、图片格式、来源和清洗方式。不要直接把整个数据集压缩包丢给老师里面有大量重复图和小图反而显得不专业。分类数据集的标注就是“目录即标签”但要在文档里写清楚这个约定。如果你做的是检测方向还需要补充标注规范。YOLO 格式是每个图片对应一个.txt每行内容为类别ID x_center y_center width height坐标需要归一化到 0 到 1 之间。这个格式在训练时不会被ImageFolder读取需要用专门的数据加载器。写文档时把坐标含义和转换公式讲清楚能避免答辩被问倒。5.4 模型文件版本管理命名规则和评估记录表模型文件不能只叫final.pth。我的命名规则是模型名_日期_验证集指标例如resnet18_0421_acc93.2.pth。这样看一眼文件就知道是哪个版本什么精度。旧版本不要删保留在models/archive目录里方便回溯。评估记录表用表格维护版本模型学习率BatchEpochs验证集准确率备注v1ResNet181e-3321090.5%只训练分类头v2ResNet181e-4322593.2%解冻微调这个表和源码文档放在一起构成完整的交付物。导师拿到之后可以直接按文档跑通流程而不是需要你本人现场操作。6. 更进一步用 Grad-CAM 和混淆矩阵证明模型真的认的是水果6.1 Grad-CAM 可视化热力图准确率再高也可能学歪。Grad-CAM 能生成热力图红色区域就是模型决策时重点看的位置。常见做法是用pytorch_grad_cam这个第三方工具核心代码很简洁from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget cam GradCAM(modelmodel, target_layers[model.layer4[-1]]) target ClassifierOutputTarget(类别索引) grayscale_cam cam(input_tensor, targets[target])验证标准很简单苹果图上高亮区域应该落在果皮和果蒂而不是旁边桌面。如果高亮区域一直在背景说明数据污染已经发生需要重新采集或增强样本多样性。6.2 混淆矩阵找到易混类别混淆矩阵能直接告诉你哪两类互相分不清。用sklearn.metrics.confusion_matrix生成矩阵后重点关注对角线以外的集中错误。比如“青苹果”和“香梨”因为颜色形状接近被反复混那就针对这两类补充不同光线和角度的照片或者考虑把它们合并成“绿果”类别减少模型负担。6.3 最后一个技巧预处理一致性推理时最容易犯的错是训练用随机裁剪推理时直接resize(224x224)导致输入分布不一致准确率掉几个点。推理阶段的正确做法只有两步先resize到 224x224再按训练用的均值和标准差做Normalize不要加任何随机增强。这个细节我每次换项目都会检查一遍已经变成肌肉记忆了。我每做完一个模型都会跑一遍热力图看看它到底在看什么。这个习惯帮我提前暴露过两次背景干扰问题。希望帮到你。本文还有配套的精品资源点击获取