
简介这份资源面向计算机、人工智能、通信工程等专业的在校学生与教师提供一套可直接运行的高分毕设方案——基于深度学习的水果识别系统。项目采用迁移学习思路在ImageNet预训练权重基础上对VGG16、ResNet50、MobileNetV2、DenseNet121四种骨干网络进行微调并迁移到水果数据集上完成分类任务最终最高准确率达到93.08%可作为毕设、课程设计或项目立项的参考实现。压缩包共277个文件约17.53MB其中8个py脚本承载模型训练与推理逻辑js、css、html等前端文件构成可视化交互页面gif、jpg、png等图片资源用于界面展示与效果演示另附md说明文档与txt配置记录便于快速理解项目结构。目前已有319人学习下载。读者可获得完整源码、数据集、模型权重与文档说明既能直接复现训练与识别流程也能在此基础上修改网络结构或更换数据集拓展为其他分类任务。1. 从一份水果识别毕设说起深度学习落地到底卡在哪每年毕业季做「基于深度学习的水果识别系统」的人特别多但真正能把源码、数据集、模型、文档四样东西串成一条完整链路的少之又少。大部分人卡在三个地方数据集找不到合适的、模型训完不知道准不准、源码跑起来一堆环境报错。这篇笔记就围绕这个标题把一套可复现的水果识别系统从数据准备到模型推理的完整路径拆开讲清楚包括源码结构怎么组织、数据集怎么处理、模型怎么选、文档说明该写什么。适合正在做毕设的本科生、刚入门深度学习想找个完整项目练手的开发者以及需要快速搭一个图像分类 demo 的工程师。读完你至少能自己跑通一套水果识别流程知道每一步的参数为什么这么设以及哪些坑是前人踩过的。2. 水果识别系统的技术选型为什么是 CNN 而不是别的2.1 图像分类任务的常见方案对比做水果识别本质上是一个图像分类问题。常见的技术路线有三条传统机器学习加手工特征、经典 CNN 网络、以及基于预训练模型的迁移学习。三条路各有适用场景但毕设场景下选择逻辑很明确。传统方案比如 HOG 特征加 SVM 分类器在水果类别少、背景干净的情况下能跑出不错的结果但它对光照、角度、遮挡非常敏感换一个数据集基本就要重新调特征。经典 CNN 比如自己搭一个五六层的卷积网络参数量可控训练速度快适合数据量在几千张左右的场景。迁移学习则是拿 ResNet、MobileNet、EfficientNet 这类在 ImageNet 上预训练好的模型冻结部分层或者全部微调通常在小数据集上表现最好。我一般会推荐毕设用迁移学习打底原因很实际水果数据集通常不大自己从零训一个 CNN 很容易过拟合而预训练模型已经学到了通用的边缘、纹理、颜色特征微调几十个 epoch 就能到很高的准确率。如果导师要求「体现网络设计能力」可以在预训练模型基础上加自定义的分类头或者注意力模块既有了创新点又不会因为从零训练导致效果翻车。2.2 数据集的选择与处理数据集是水果识别系统的地基。公开的水果数据集有几个常见来源Kaggle 上的 Fruits-360、Fruit Recognition 数据集以及自己用爬虫或者手机拍摄采集的图片。Fruits-360 包含一百多种水果、十几万张图片但它的图片背景非常干净全是白色背景训出来的模型换到真实场景容易崩。自己采集的数据更贴近实际但标注成本高。常见做法是混合使用用公开数据集做预训练或者数据增强的底库再补充一部分自己拍摄的图片做微调。数据集的目录结构建议按类别分文件夹每个类别下面放对应的图片这样 PyTorch 的 ImageFolder 或者 TensorFlow 的 image_dataset_from_directory 可以直接读取。# 推荐的数据集目录结构 dataset/ ├── train/ │ ├── apple/ │ │ ├── 001.jpg │ │ └── 002.jpg │ ├── banana/ │ └── orange/ ├── val/ │ ├── apple/ │ ├── banana/ │ └── orange/ └── test/ ├── apple/ ├── banana/ └── orange/这个结构的好处是训练集、验证集、测试集物理隔离避免数据泄露。验证集用来调超参数和早停测试集只在最后评估时用一次。很多人把验证集和测试集混用导致最终报告的准确率虚高答辩时被问住。数据增强是另一个关键环节。水果识别常见的增强手段包括随机旋转、水平翻转、颜色抖动、随机裁剪。注意颜色抖动对水果识别要慎用因为颜色是区分水果的重要特征过度抖动可能把苹果的颜色改成橘子的颜色反而降低模型判别力。我一般会把颜色抖动的幅度控制在 0.1 到 0.2 之间。2.3 模型选型MobileNetV3 还是 ResNet50选模型要看两个约束部署环境和精度要求。如果是毕设演示跑在笔记本或者树莓派上MobileNetV3 是首选参数量只有几百万推理速度快精度也够用。如果追求更高的准确率ResNet50 或者 EfficientNet-B0 是更稳的选择但模型体积和推理时间会明显增加。模型参数量输入尺寸适用场景MobileNetV3-Small2.5M224x224移动端/嵌入式部署ResNet1811M224x224桌面端快速实验ResNet5025M224x224追求精度EfficientNet-B05.3M224x224精度与速度平衡选型时还有一个容易被忽略的点输入尺寸。大部分预训练模型默认 224x224但水果图片的细节比如表面的纹理、瑕疵在缩小后可能丢失。如果数据集图片分辨率高可以尝试 320x320 或 448x448但要注意显存占用会成倍增加。我一般先用 224 跑一版 baseline再根据验证集表现决定要不要放大输入。3. 从零跑通训练流程源码结构与关键代码3.1 项目源码的目录组织一套完整的水果识别系统源码目录结构应该清晰到别人拿到就能跑。我习惯按功能模块划分而不是把所有代码堆在一个文件里。fruit-recognition/ ├── configs/ │ └── config.yaml # 超参数、路径配置 ├── data/ │ └── dataset.py # 数据加载与增强 ├── models/ │ └── model.py # 模型定义 ├── utils/ │ ├── metrics.py # 评估指标 │ └── logger.py # 日志记录 ├── train.py # 训练入口 ├── evaluate.py # 评估脚本 ├── predict.py # 单张图片推理 ├── requirements.txt # 依赖清单 └── README.md # 文档说明这个结构的好处是配置和代码分离换数据集或者调参只需要改 config.yaml不用动训练逻辑。requirements.txt 要写清楚版本号比如 torch2.0.1、torchvision0.15.2避免别人装最新版跑不起来。3.2 数据加载与增强的代码实现import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集增强随机裁剪、翻转、轻微颜色抖动 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), 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]) ]) train_dataset datasets.ImageFolder(dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4)这段代码里几个参数值得说明。RandomResizedCrop 的 scale 设成 (0.8, 1.0) 而不是默认的 (0.08, 1.0)是因为水果通常占据图片主体裁剪太狠会把水果裁掉一半反而引入噪声。Normalize 的均值和标准差用的是 ImageNet 的统计值因为预训练模型就是在这个分布上训的保持一致能让微调更稳定。num_workers 设成 4 是经验值太大在 Windows 上容易出问题太小则数据加载成为瓶颈。3.3 模型定义与迁移学习微调import torch.nn as nn from torchvision import models def build_model(num_classes, model_nameresnet18, pretrainedTrue): if model_name resnet18: model models.resnet18(pretrainedpretrained) # 冻结前面的卷积层只训练最后的分类头 for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 in_features model.fc.in_features model.fc nn.Sequential( nn.Linear(in_features, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) elif model_name mobilenet_v3: model models.mobilenet_v3_small(pretrainedpretrained) for param in model.parameters(): param.requires_grad False in_features model.classifier[3].in_features model.classifier[3] nn.Linear(in_features, num_classes) return model冻结卷积层的策略适合数据量小的场景通常几千张图片以下。如果数据量超过一万张可以考虑解冻最后几个卷积块一起微调效果会更好但训练时间更长。分类头里加 Dropout 是为了防止过拟合0.3 是一个比较安全的起点如果验证集 loss 明显高于训练集 loss可以加到 0.5。3.4 训练循环与关键参数设置import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes10).to(device) criterion nn.CrossEntropyLoss() # 只优化分类头的参数 optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) best_acc 0.0 for epoch in range(30): model.train() 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() scheduler.step() # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch1}, Val Acc: {acc:.4f})学习率设 1e-3 是因为只训练分类头参数随机初始化需要较大的学习率快速收敛。weight_decay 用 1e-4 做正则化。CosineAnnealingLR 让学习率按余弦曲线下降比 StepLR 更平滑后期不容易震荡。保存 best_model.pth 而不是最后一个 epoch 的模型是因为验证集准确率最高的那个 checkpoint 通常泛化最好。4. 避坑指南水果识别系统最常见的五个翻车点4.1 数据集类别不平衡导致模型偏科现象训练完发现模型对苹果识别很准对猕猴桃几乎全错。原因苹果的图片有 2000 张猕猴桃只有 200 张模型倾向于预测多数类。解决用 WeightedRandomSampler 给少数类更高的采样权重或者在 loss 里给少数类更大的权重。也可以对少数类做数据增强把数量补到和多数类接近。4.2 验证集准确率虚高但实际推理一塌糊涂现象验证集准确率 98%拿手机拍一张真实的水果照片去测结果乱猜。原因训练集和验证集来自同一个数据源背景、光照、角度分布一致模型学到了背景特征而不是水果特征。解决验证集里必须包含一部分真实场景拍摄的图片或者用完全独立的数据源做测试。如果做不到至少做一次跨数据集的评估。4.3 环境依赖版本冲突导致源码跑不起来现象别人拿到源码pip install -r requirements.txt 之后报一堆错torch 和 torchvision 版本不匹配是最常见的。原因requirements.txt 里只写了包名没写版本号或者写的是本地环境的版本但和别人系统不兼容。解决用 pip freeze 导出精确版本并且在 README 里写清楚 Python 版本要求比如 Python 3.8 到 3.10。CUDA 版本也要注明CPU 和 GPU 版本的 torch 安装命令不一样。4.4 图片预处理不一致导致推理结果异常现象训练时用了 Normalize推理时忘了做同样的归一化模型输出完全不对。原因训练和推理的预处理流程没有统一。解决把预处理逻辑封装成一个函数或者类训练和推理都调用同一个。在 predict.py 里显式写出和训练时一致的 transform不要凭记忆手写。4.5 模型保存与加载的 key 不匹配现象torch.load 加载模型时报 KeyError提示缺少某些层或者多了某些层。原因保存的是整个模型还是 state_dict加载时结构是否一致这些细节容易搞混。解决统一用 torch.save(model.state_dict()) 保存加载时先实例化模型结构再 load_state_dict。如果改了分类头的结构加载旧权重时需要 strictFalse但要注意这样会忽略不匹配的层可能引入隐藏 bug。5. 进阶技巧用 Gradio 快速搭一个可交互的演示界面毕设答辩时光有准确率数字不够直观评委更想看到实际效果。用 Gradio 可以在十几行代码内搭一个网页界面上传图片就能看到识别结果和置信度。import gradio as gr import torch from PIL import Image from torchvision import transforms model build_model(num_classes10, pretrainedFalse) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() class_names [apple, banana, orange, grape, kiwi, mango, pear, peach, strawberry, watermelon] 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]) ]) def predict(image): img Image.fromarray(image).convert(RGB) tensor transform(img).unsqueeze(0) with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1)[0] return {class_names[i]: float(probs[i]) for i in range(len(class_names))} demo gr.Interface(fnpredict, inputsgr.Image(), outputsgr.Label(num_top_classes3), title水果识别系统) demo.launch()这段代码的关键点load_state_dict 时用 map_locationcpu这样即使在没有 GPU 的机器上也能加载。softmax 之后的概率值转成字典返回给 Gradio 的 Label 组件它会自动按概率排序显示前三名。num_top_classes3 让界面显示置信度最高的三个类别比只显示一个结果更有说服力。如果想让演示更稳定可以在 predict 函数里加一个异常捕获当输入不是有效图片时返回友好提示而不是直接报错。另外Gradio 默认启动在本地 7860 端口答辩时提前在演示电脑上跑一遍确认端口没被占用。我自己做这类项目最大的教训是不要等到答辩前一天才跑通全流程。数据集清洗、模型训练、界面调试每一步都可能出意外。提前两周把整个链路走一遍留出时间处理环境问题和效果调优。希望帮到你。本文还有配套的精品资源点击获取