☰
基于深度学习的水果识别系统:从CNN训练到Gradio部署的完整毕业设计实战
2026/10/4 7:22:18 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生与项目实战学习者的深度学习水果识别系统完整项目,可直接用于毕业设计、课程大作业或算法练手。项目经导师指导并通过评审,源码在本地编译运行无误,难度适中,适合作为入门到进阶的深度学习实践案例。压缩包共277个文件,约17.53MB,其中8个Python文件承载模型训练与识别核心逻辑,114个js、26个css与7个html构成前端交互界面,另有84个gif、14个jpg、10个png等图片素材及字体文件,配套数据集与模型一并提供,目录结构清晰,便于按模块查阅与二次开发。目前已有141人学习下载。读者可从中获得一套可复现的完整方案,涵盖数据准备、模型构建、训练调参与界面展示全流程,既能对照源码理解深度学习落地细节,也能在此基础上替换数据集或调整网络结构,快速完成自己的设计任务。

1. 从零搭一套水果识别系统:毕业设计里最容易被低估的工程细节

很多人做水果识别毕业设计,第一反应是打开 Python 装个深度学习框架,找一份水果数据集,跑一个 CNN 就交差。真到答辩现场被问「模型部署在哪、推理一张图要多久、换一种水果还能不能认」,往往答不上来。这套「基于深度学习的水果识别系统 Python 源代码 + 数据集模型完整项目」真正要解决的,不是训练一个分类器,而是把数据采集、标注、训练、评估、推理封装成一条能跑通、能演示、能讲清楚的链路。它适合正在做毕业设计的学生,也适合想用一个小而完整的视觉项目入门深度学习 CNN 的开发者。下面我按自己实际搭过几套的经验,把这条链路拆开讲,重点放在能复现的命令、参数和踩过的坑上。

2. 水果识别系统的技术选型:为什么是 CNN 而不是传统特征

2.1 从 HOG+SVM 到 CNN,差在哪

早期水果识别常用手工特征加分类器,比如提取颜色直方图、HOG 梯度特征,再喂给 SVM。这条路在背景干净、光照固定的数据集上能到 80% 左右,但一旦水果有遮挡、旋转、光照变化,准确率掉得很快。原因是手工特征描述的是「人认为重要」的边缘和颜色分布,而水果的判别信息往往藏在纹理和局部形状的组合里,人很难穷举。

CNN 的优势在于卷积核自动学习局部特征,浅层学边缘和颜色,深层学形状和语义。对水果这种类间差异中等、类内差异较大的目标,CNN 的泛化能力明显更强。常见做法是用迁移学习:拿在 ImageNet 上预训练过的骨干网络,替换最后的全连接层,用水果数据集微调。这样即使你的数据集只有几千张,也能拿到 95% 以上的验证准确率。

选型上,毕业设计推荐两条路线:一是轻量级自定义 CNN,参数量小、训练快、代码透明,适合讲清楚每一层在干什么;二是迁移学习,用 ResNet18 或 MobileNetV3,精度高、训练轮数少,适合追求指标。我一般会两个都跑,用自定义 CNN 讲原理,用迁移学习出最终指标。

2.2 数据集怎么组织才不返工

数据集是这类项目最容易翻车的地方。常见的水果数据集有 Fruit-360、Kaggle 上的 Fruits-360,类别从几十到上百不等。但直接下载的数据集往往目录结构混乱,有的按类别分文件夹,有的所有图混在一起配一个 CSV 标签。训练前必须统一成ImageFolder能读的结构:

dataset/ ├── train/ │ ├── apple/ │ │ ├── 001.jpg │ │ └── ... │ ├── banana/ │ └── ... ├── val/ │ ├── apple/ │ └── ...

划分比例建议 train:val = 8:2,如果类别样本不均衡,按类别分层抽样,别直接随机切。我见过有人随机切完,验证集里某个类别只有两张图,评估指标完全不可信。

提示:划分脚本要固定随机种子,否则每次运行切分结果不同,实验无法复现。

import os, shutil, random from pathlib import Path random.seed(42) # 固定种子,保证切分可复现 src = Path("raw_dataset") dst = Path("dataset") classes = [d.name for d in src.iterdir() if d.is_dir()] for cls in classes: imgs = list((src / cls).glob("*.jpg")) random.shuffle(imgs) split = int(len(imgs) * 0.8) # 8:2 划分 for phase, subset in [("train", imgs[:split]), ("val", imgs[split:])]: out = dst / phase / cls out.mkdir(parents=True, exist_ok=True) for img in subset: shutil.copy(img, out / img.name)

这段脚本做三件事:遍历原始类别目录、按类别打乱、按 8:2 复制到 train 和 val。random.seed(42)是关键,保证每次切分一致。split用整数截断,样本少的类别要检查 val 是否为空。复制而不是移动,是为了保留原始数据,方便换划分比例重跑。

2.3 环境依赖与版本对齐

Python 环境建议用 conda 单独建一个,避免和系统包冲突。核心依赖就四个:torch、torchvision、numpy、pillow。安装命令:

conda create -n fruit python=3.9 -y conda activate fruit pip install torch torchvision numpy pillow matplotlib

版本上,torch 1.13 到 2.x 都能跑,torchvision 要和 torch 对应。如果要用 GPU,先确认 CUDA 版本再装对应 wheel。CPU 也能训,只是慢,小数据集 20 轮大概十几分钟。matplotlib用来画训练曲线,答辩时放 loss 和 accuracy 曲线很加分。

3. 训练一个能用的水果分类模型:代码、参数与评估

3.1 自定义 CNN 的最小实现

先给一个结构清晰的自定义 CNN,适合讲原理。三层卷积,每层后接 ReLU 和最大池化,最后全连接输出类别数。

import torch import torch.nn as nn class FruitCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 224->112 nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 112->56 nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 56->28 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))

padding=1保证卷积后尺寸不变,池化负责降采样。三次池化把 224 降到 28,特征图 128 通道。全连接前加 Dropout 0.5 抑制过拟合,这是小数据集上的常规操作。num_classes按你的数据集类别数传,比如 10 类水果就传 10。

3.2 数据增强与 DataLoader 参数

小数据集必须做增强,否则训练集准确率 99%、验证集 70% 的过拟合很常见。用 torchvision 的 transforms:

from torchvision import transforms, datasets from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), # 随机水平翻转 transforms.RandomRotation(15), # 随机旋转 ±15 度 transforms.ColorJitter(0.2, 0.2, 0.2), # 亮度/对比度/饱和度扰动 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder("dataset/train", transform=train_tf) val_ds = datasets.ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4)

Normalize 用的均值方差是 ImageNet 的统计值,迁移学习和从头训练都建议沿用,保证输入分布一致。batch_size=32是显存和稳定性的折中,显存小就降到 16。num_workers=4在 Windows 上有时会报错,改成 0 即可。验证集不做增强,只做 Resize 和归一化,否则评估指标会失真。

3.3 训练循环与关键超参

训练循环要记录每轮 loss 和 accuracy,方便画曲线。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = FruitCNN(num_classes=len(train_ds.classes)).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5) for epoch in range(30): model.train() for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() scheduler.step() # 每 10 轮学习率减半 # 验证阶段省略,见完整代码

lr=1e-3是 Adam 的常用起点,weight_decay=1e-4做 L2 正则。StepLR每 10 轮把学习率乘 0.5,后期收敛更稳。轮数 30 对小数据集够用,如果验证 loss 连续 5 轮不降,可以早停。训练时把模型切到model.train(),验证时切model.eval()并加torch.no_grad(),这是新手最容易漏的两步。

3.4 评估指标别只看准确率

准确率在类别均衡时够用,但水果数据集往往某几类样本多。要补一个混淆矩阵和每类 precision/recall。

from sklearn.metrics import classification_report, confusion_matrix model.eval() preds, labels = [], [] with torch.no_grad(): for x, y in val_loader: out = model(x.to(device)) preds.extend(out.argmax(1).cpu().numpy()) labels.extend(y.numpy()) print(classification_report(labels, preds, target_names=val_ds.classes)) print(confusion_matrix(labels, preds))

classification_report直接给出每类的 precision、recall、f1。如果某一类 recall 特别低,说明模型把它和别的类混了,看混淆矩阵定位是哪两类。常见原因是这两类外观接近,比如青苹果和梨,需要更多样本或更强增强。

4. 推理封装与界面演示:让答辩现场能跑起来

4.1 单张图片推理函数

训练完要能对任意一张图输出类别和置信度。

from PIL import Image import torch.nn.functional as F def predict(img_path, model, class_names, device): model.eval() img = Image.open(img_path).convert("RGB") x = val_tf(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): prob = F.softmax(model(x), dim=1) conf, idx = prob.max(1) return class_names[idx.item()], conf.item() # 用法 name, conf = predict("test.jpg", model, train_ds.classes, device) print(f"{name} {conf:.2%}")

unsqueeze(0)把单张图变成[1,3,224,224],因为模型 forward 期望有 batch 维。softmax把 logits 转成概率,max取最大概率和对应类别。置信度低于 0.6 时建议提示「不确定」,避免硬答。

4.2 用 Gradio 快速搭演示界面

答辩现场用命令行不够直观,Gradio 几行代码就能出网页界面。

import gradio as gr def classify(img): name, conf = predict(img, model, train_ds.classes, device) return {name: conf} gr.Interface(fn=classify, inputs=gr.Image(type="filepath"), outputs=gr.Label(num_top_classes=3), title="水果识别系统").launch()

gr.Image(type="filepath")传文件路径给 predict,gr.Label显示 top3 类别和概率。launch()默认起本地服务,答辩时浏览器打开就能演示。注意模型和 class_names 要在全局加载一次,别每次推理都重新加载。

4.3 模型保存与加载的坑

保存用torch.save(model.state_dict(), "fruit.pth"),只存参数不存结构。加载时要先实例化同结构模型再load_state_dict。

torch.save(model.state_dict(), "fruit.pth") # 加载 model = FruitCNN(num_classes=len(train_ds.classes)).to(device) model.load_state_dict(torch.load("fruit.pth", map_location=device)) model.eval()

map_location很重要,GPU 上训的模型在 CPU 机器上加载不加这个参数会报错。类别数必须和训练时一致,否则最后一层维度对不上。建议把 class_names 一起存成 json,加载时读回来,避免顺序错乱。

5. 避坑与排查:水果识别项目里最常见的 5 个翻车点

5.1 验证准确率远低于训练准确率

现象:训练集 99%,验证集 70% 上下。原因:过拟合,数据增强太弱或模型太大。解决:加强增强(加 RandomResizedCrop、ColorJitter),加 Dropout 和 weight_decay,或者换更小的模型。如果数据集本身很小,考虑迁移学习而不是从头训。

5.2 损失变成 nan

现象:训练几轮后 loss 显示 nan。原因:学习率太大,或者输入没归一化。解决:把 lr 降到 1e-4,确认 transforms 里有 Normalize,检查数据里有没有损坏图片。损坏图片用 PIL 打开会抛异常,写个脚本遍历一遍过滤掉。

5.3 类别索引和名称对不上

现象:预测结果张冠李戴,明明输入苹果输出香蕉。原因:ImageFolder按文件夹名排序生成类别索引,推理时 class_names 顺序和训练时不一致。解决:训练完把train_ds.classes存成 json,推理时读同一个文件,别手写类别列表。

5.4 DataLoader 在 Windows 上报 BrokenPipe

现象:num_workers>0时 Windows 报错或卡死。原因:Windows 多进程 spawn 机制和 Linux 不同。解决:把num_workers设为 0,或者把训练代码放在if __name__ == "__main__":里。这是血泪经验,调半天以为是代码问题,其实是平台差异。

5.5 推理速度慢到无法演示

现象:单张图推理要好几秒。原因:每次推理都重新加载模型,或者没切 eval 模式。解决:模型全局加载一次,推理前model.eval()并包torch.no_grad()。如果还慢,检查是不是在 CPU 上跑了大模型,换 MobileNetV3 会快很多。

6. 把项目做扎实的进阶技巧:迁移学习与可复现实验

自定义 CNN 跑通后,想让指标更好看,迁移学习是最划算的一步。用 torchvision 自带的预训练 ResNet18,只改最后一层:

from torchvision import models import torch.nn as nn model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = nn.Linear(model.fc.in_features, len(train_ds.classes)) model = model.to(device) # 只微调最后几层,前面冻结 for name, param in model.named_parameters(): if "fc" not in name: param.requires_grad = False

weights=DEFAULT加载 ImageNet 预训练权重,model.fc.in_features是 ResNet18 最后一层输入维度 512。冻结前面层只训 fc,第一轮就能到 90% 以上,再解冻全部微调几轮,通常能到 97% 以上。学习率要调小,微调阶段用 1e-4,别用 1e-3 把预训练权重冲掉。

可复现实验的习惯:每次训练把超参、数据集划分、随机种子写进一个 config 字典,连同最终指标存成 json。答辩被问「你这个结果怎么来的」,直接翻记录。我一般还会固定torch.manual_seed(42)和torch.cuda.manual_seed_all(42),保证同一份代码两次运行结果一致。这些细节看起来琐碎,但正是毕业设计里区分「跑通」和「做扎实」的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询