尧图精选

基于CNN的果蔬识别系统:完整源码与工程实践指南

🕒 发布时间:2026/10/2 1:23:28 📁 来源:尧图网络
简介这是一份面向计算机相关专业学生的深度学习实战资源围绕卷积神经网络CNN实现果蔬图像识别系统适合用作毕业设计、课程设计或期末大作业。项目难度适中源码经导师指导与评审本地编译可运行并配有完整文档说明帮助读者理解从数据预处理、模型搭建到训练评估的全流程。压缩包共38个文件约2.54MB包含8个Python脚本负责数据划分、增强、训练与测试等核心逻辑、20张png与3张jpeg图像、1个jpg、2个pyc、2个txt、1个md及1份pdf设计文档结构清晰便于按模块查阅。目前已有61人学习下载。读者可获得可运行的CNN果蔬识别源码、数据集处理脚本、模型训练与测试代码以及一份讲解设计思路与关键代码的PDF文档既能直接运行复现也能在此基础上进行二次开发与创新是深度学习图像识别方向较为完整的学习与实践素材。1. 从一份能跑通的 CNN 果蔬识别源码说起很多同学做深度学习课程设计时卡住的地方往往不是模型本身而是从数据集到可运行 demo 之间那一大段没人讲清楚的工程细节。这份「利用深度学习 CNN 技术的水果蔬菜识别系统」资源包含完整项目源码与配套文档说明解决的正是这个问题它把数据预处理、CNN 网络搭建、训练脚本、推理接口和一份能照着走的说明文档打包在一起适合正在做 Python 期末大作业、深度学习毕设或者想找一个图像分类实战项目入门 CNN 的人。你拿到手后能直接看到一份分类任务从零到推理的完整链路而不是只对着一段孤立的模型定义发呆。2. 拆开这份源码CNN 图像分类的工程骨架长什么样2.1 为什么果蔬识别适合用 CNN 而不是传统方法水果蔬菜识别本质上是一个细粒度图像分类问题。同一类水果在不同光照、角度、成熟度下差异很大而不同类之间又可能长得很像比如青苹果和青椒、柠檬和黄色甜椒。传统方法靠人工设计特征比如颜色直方图、HOG 梯度方向直方图、SIFT 尺度不变特征再喂给 SVM 做分类。这套流程在背景干净、类别少的时候能跑但一旦图片来自真实场景特征设计就变得非常吃力换一批数据就得重新调参。CNN 卷积神经网络的优势在于它把特征提取和分类器训练放在同一个可微分的框架里。卷积核在图像上滑动自动学习从边缘、纹理到局部形状、整体语义的层级特征。浅层卷积关注颜色斑块和边缘深层卷积关注更抽象的部件组合。对于果蔬这种类间差异体现在形状和纹理上的任务CNN 的归纳偏置刚好匹配。常见做法是用一个轻量级骨干网络比如自己搭 4 到 5 层卷积或者用 ResNet18、MobileNetV2 做迁移学习。这份源码走的是自建小型 CNN 的路线参数量可控适合在单卡甚至 CPU 上跑通全流程对课程设计来说足够说明问题。2.2 源码目录结构与各模块职责拿到一份项目源码第一件事不是急着跑而是先看清楚目录怎么分的。这份资源的工程结构大致如下fruit_veg_cnn/ ├── data/ │ ├── train/ # 训练集按类别分子文件夹 │ ├── val/ # 验证集同样按类别分 │ └── test/ # 测试集用于最终评估 ├── src/ │ ├── dataset.py # 自定义 Dataset负责读图、增强、归一化 │ ├── model.py # CNN 网络定义 │ ├── train.py # 训练主循环含损失、优化器、日志 │ ├── evaluate.py # 在测试集上算准确率、混淆矩阵 │ └── predict.py # 单张图片推理接口 ├── configs/ │ └── default.yaml # 超参数、路径、类别数配置 ├── requirements.txt # 依赖清单 └── README.md # 文档说明含环境配置和运行步骤这个结构的好处是职责清晰。dataset.py只管数据怎么读、怎么增强model.py只管网络结构train.py把两者串起来。你想换网络只改model.py想换数据增强策略只改dataset.py。很多同学写课程设计时把所有逻辑塞进一个文件改一处就牵一发而动全身这份源码的分层方式值得借鉴。2.3 环境配置与依赖安装在动手之前先把环境理顺。这份源码基于 PyTorch常见做法是用 conda 建一个独立环境避免和系统里的包打架。步骤如下# 创建并激活虚拟环境Python 版本建议 3.8 到 3.10 conda create -n fruit_cnn python3.9 -y conda activate fruit_cnn # 安装 PyTorch根据是否有 GPU 选择对应命令 # 有 NVIDIA 显卡且装了 CUDA 11.8 的情况 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 没有 GPU 或不想折腾 CUDA直接装 CPU 版 # pip install torch torchvision # 安装其余依赖 pip install -r requirements.txtrequirements.txt里通常包含numpy、pillow、matplotlib、scikit-learn、pyyaml、tqdm这些。torchvision负责提供图像变换和预训练权重。如果你用的是较新的 PyTorch 版本注意torchvision的版本要匹配否则 import 时会报错。装完之后跑一句python -c import torch; print(torch.__version__, torch.cuda.is_available())确认版本号和 GPU 是否可用。提示如果torch.cuda.is_available()返回 False但你有显卡先检查驱动版本和 CUDA 版本是否匹配。不匹配的话要么升级驱动要么换对应的 PyTorch 安装命令。2.4 数据集的准备与目录规范这份源码默认使用按类别分文件夹的图像数据集这是 PyTorchImageFolder的标准格式。你需要把图片整理成data/train/apple/xxx.jpg data/train/banana/xxx.jpg data/train/carrot/xxx.jpg ... data/val/apple/xxx.jpg ...训练集和验证集的类别文件夹名称必须完全一致。常见做法是按 8:1:1 划分训练、验证、测试。如果你的原始数据没有划分可以写个小脚本按比例拷贝import os import random import shutil # 原始数据目录每个类别一个子文件夹 src_dir raw_data # 输出目录 dst_dir data # 划分比例 ratios {train: 0.8, val: 0.1, test: 0.1} for cls in os.listdir(src_dir): cls_path os.path.join(src_dir, cls) if not os.path.isdir(cls_path): continue imgs [f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .png, .jpeg))] random.shuffle(imgs) n len(imgs) n_train int(n * ratios[train]) n_val int(n * ratios[val]) splits { train: imgs[:n_train], val: imgs[n_train:n_train n_val], test: imgs[n_train n_val:] } for split, files in splits.items(): out_dir os.path.join(dst_dir, split, cls) os.makedirs(out_dir, exist_okTrue) for f in files: shutil.copy(os.path.join(cls_path, f), os.path.join(out_dir, f))这段脚本的逻辑很直接遍历每个类别文件夹打乱图片顺序按比例切分然后拷贝到对应的train/val/test目录下。random.shuffle之前最好固定随机种子比如random.seed(42)保证每次划分结果一致方便复现。注意类别文件夹名称不要有空格和特殊字符否则ImageFolder读取时可能出问题。3. 训练脚本怎么读从数据增强到损失下降的完整链路3.1 Dataset 与 DataLoader图像增强的四个关键参数dataset.py里定义了一个继承自torch.utils.data.Dataset的类核心是__getitem__方法。它负责把图片路径读成 PIL Image然后做一系列变换最后返回张量和标签。训练集和验证集的变换策略通常不一样from torchvision import transforms # 训练集变换带随机增强 train_transform transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸 transforms.RandomHorizontalFlip(p0.5), # 随机水平翻转 transforms.RandomRotation(15), # 随机旋转 ±15 度 transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), # 颜色抖动 transforms.ToTensor(), # 转成张量像素值归一化到 [0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet 统计量 ]) # 验证集变换只做确定性的缩放和归一化 val_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]) ])四个关键参数值得展开说。Resize((224, 224))把图片统一到固定尺寸因为全连接层要求输入维度一致。RandomHorizontalFlip和RandomRotation属于几何增强让模型对位置和角度不那么敏感。ColorJitter调整亮度、对比度、饱和度模拟不同光照条件。Normalize用的均值和标准差是 ImageNet 上的统计量如果你从零训练且数据量不大也可以自己算一遍数据集的均值和方差但用 ImageNet 的值通常没问题。注意验证集和测试集绝对不能加随机增强否则每次评估结果都在变没法比较。这是新手很容易翻车的地方。DataLoader负责把 Dataset 包装成可迭代的批次from torch.utils.data import DataLoader train_loader DataLoader( train_dataset, batch_size32, # 每批 32 张显存不够就调小 shuffleTrue, # 训练集打乱顺序 num_workers4, # 读取数据的子进程数Windows 下建议设 0 pin_memoryTrue # 有 GPU 时加速数据传输 ) val_loader DataLoader( val_dataset, batch_size32, shuffleFalse, # 验证集不打乱 num_workers4, pin_memoryTrue )batch_size是最需要根据显存调整的参数。8GB 显存跑 224×224 的图32 通常没问题如果报 CUDA out of memory先降到 16 或 8。num_workers在 Linux 下设 4 能加快数据读取Windows 下有时会报多进程错误设 0 最稳。3.2 CNN 网络结构卷积层、池化层与全连接层的参数含义model.py里定义了一个典型的卷积神经网络。结构大致是卷积 → ReLU → 池化重复若干次最后展平接全连接层输出类别数。下面是一个可参考的实现import torch.nn as nn import torch.nn.functional as F class FruitVegCNN(nn.Module): def __init__(self, num_classes10): super().__init__() # 第一组卷积输入 3 通道输出 32 通道卷积核 3x3padding 1 保持尺寸 self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(32) # 第二组卷积32 到 64 通道 self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(64) # 第三组卷积64 到 128 通道 self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) self.bn3 nn.BatchNorm2d(128) # 池化层2x2 窗口步长 2尺寸减半 self.pool nn.MaxPool2d(2, 2) # 全连接层经过三次池化224 - 112 - 56 - 28128 通道 self.fc1 nn.Linear(128 * 28 * 28, 256) self.fc2 nn.Linear(256, num_classes) self.dropout nn.Dropout(0.5) def forward(self, x): # 第一组卷积 - BN - ReLU - 池化 x self.pool(F.relu(self.bn1(self.conv1(x)))) # 第二组 x self.pool(F.relu(self.bn2(self.conv2(x)))) # 第三组 x self.pool(F.relu(self.bn3(self.conv3(x)))) # 展平 x x.view(x.size(0), -1) # 全连接 Dropout x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x逐层看参数。nn.Conv2d(3, 32, kernel_size3, padding1)表示输入 3 通道RGB输出 32 个特征图卷积核 3×3padding 为 1 使得输出空间尺寸和输入一致。BatchNorm2d对每个通道做归一化加速收敛、缓解梯度问题。MaxPool2d(2, 2)把特征图宽高各减半。三次池化后224 变成 28。view(x.size(0), -1)把每个样本的特征图展平成一维向量x.size(0)是 batch 维度。Dropout(0.5)在训练时随机丢弃一半神经元防止过拟合。最后fc2输出num_classes个 logits交给交叉熵损失。num_classes必须和你的数据集类别数一致。如果你有 15 类果蔬就传 15。类别数写错是训练时最常见的报错来源之一表现为 loss 计算时维度不匹配。3.3 训练循环优化器、学习率与早停策略train.py的主循环负责把数据喂给模型、算损失、反向传播、更新参数。核心代码框架如下import torch import torch.nn as nn import torch.optim as optim from tqdm import tqdm device torch.device(cuda if torch.cuda.is_available() else cpu) model FruitVegCNN(num_classes10).to(device) # 交叉熵损失内部自带 softmax criterion nn.CrossEntropyLoss() # Adam 优化器学习率 1e-3权重衰减 1e-4 做 L2 正则 optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) # 学习率调度验证集损失 5 个 epoch 不降就乘 0.1 scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience5, factor0.1) best_val_acc 0.0 patience_counter 0 early_stop_patience 10 for epoch in range(50): # 训练阶段 model.train() train_loss 0.0 for imgs, labels in tqdm(train_loader, descfEpoch {epoch1} train): imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * imgs.size(0) train_loss / len(train_dataset) # 验证阶段 model.eval() val_loss 0.0 correct 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) val_loss loss.item() * imgs.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() val_loss / len(val_dataset) val_acc correct / len(val_dataset) scheduler.step(val_loss) print(fEpoch {epoch1}: train_loss{train_loss:.4f}, val_loss{val_loss:.4f}, val_acc{val_acc:.4f}) # 保存最佳模型 if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) patience_counter 0 else: patience_counter 1 if patience_counter early_stop_patience: print(Early stopping triggered.) break几个参数需要根据实际情况调。lr1e-3是 Adam 的常用起点如果 loss 震荡厉害就降到 1e-4。weight_decay1e-4是 L2 正则系数数据量小的时候可以适当加大到 1e-3。ReduceLROnPlateau在验证损失停滞时降低学习率patience5表示等 5 个 epoch。早停的early_stop_patience10表示验证准确率连续 10 个 epoch 没提升就停防止过拟合和浪费时间。model.train()和model.eval()的切换很关键前者启用 Dropout 和 BatchNorm 的训练行为后者固定它们。3.4 推理与评估混淆矩阵看出哪些类别容易混训练完之后用evaluate.py在测试集上算准确率和混淆矩阵。混淆矩阵能告诉你模型把哪些类别搞混了比如苹果和番茄、柠檬和香蕉。代码片段from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds [] all_labels [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namesclass_names))classification_report会输出每个类别的精确率、召回率和 F1 分数。如果某个类别召回率特别低说明很多该类的图被分到别的类去了可以针对性补充该类训练数据或调整增强策略。混淆矩阵用matplotlib画成热力图更直观文档说明里通常有示例图。4. 避坑与排查训练不收敛、显存爆炸、准确率虚高的真实原因4.1 现象loss 一直不降或变成 NaN原因通常有三个。一是学习率太大Adam 虽然自适应但 1e-2 以上仍可能发散。二是数据没有归一化像素值直接 0 到 255 输入网络梯度爆炸。三是标签越界比如num_classes10但标签里出现了 10 或更大的值交叉熵计算时索引越界loss 直接变 NaN。解决办法先把学习率降到 1e-4 试确认ToTensor()和Normalize都在变换里打印一下标签的最大值labels.max()看是否小于类别数。4.2 现象CUDA out of memory原因可能是batch_size太大、图片尺寸太大、模型参数量太多或者没有用torch.no_grad()包住验证阶段。解决办法先把batch_size减半再把Resize从 224 降到 128 试验证和推理阶段务必加with torch.no_grad():。如果还不行检查是不是在训练循环里累积了计算图比如把 loss 存进列表却没 detach。4.3 现象训练准确率很高但测试准确率很低这是典型过拟合。原因可能是训练集太小、模型太复杂、增强不够。解决办法加大数据增强力度加 Dropout 和权重衰减或者用迁移学习换更小的模型。另一个容易被忽略的原因是训练集和测试集划分时没有打乱比如按文件夹顺序切分导致某一类全在训练集。用前面给的划分脚本时记得random.shuffle。4.4 现象验证集准确率波动大原因通常是验证集太小或者 BatchNorm 在验证时统计量不稳定。解决办法增大验证集比例或者用交叉验证。另外检查model.eval()是否在验证前调用了如果忘了切 eval 模式Dropout 还在随机丢弃神经元准确率自然忽高忽低。4.5 现象推理时单张图片预测结果离谱原因可能是推理时的预处理和训练时不一致。比如训练用了Normalize推理时只做了ToTensor像素分布对不上。解决办法把验证集的变换单独抽出来推理时复用同一套。另外确认图片的通道顺序PIL 读出来是 RGBOpenCV 读出来是 BGR用 OpenCV 读图的话要手动转一下。5. 把模型用起来从单张推理到批量预测的进阶技巧训练出一个准确率不错的模型只是第一步真正让这份资源发挥价值的是把它接到实际使用场景里。predict.py通常提供一个命令行接口输入图片路径输出类别和置信度。但实际用的时候你可能会遇到需要批量处理一个文件夹、或者把模型导出成 ONNX 格式部署到其他环境的情况。这里分享几个我常用的进阶操作。先说批量预测。把predict.py改造成接受文件夹路径遍历所有图片输出结果到 CSVimport os import csv import torch from PIL import Image from torchvision import transforms def batch_predict(model, folder, class_names, device, output_csvpredictions.csv): 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]) ]) model.eval() results [] with torch.no_grad(): for fname in os.listdir(folder): if not fname.lower().endswith((.jpg, .png, .jpeg)): continue img Image.open(os.path.join(folder, fname)).convert(RGB) tensor transform(img).unsqueeze(0).to(device) # 增加 batch 维度 outputs model(tensor) probs torch.softmax(outputs, dim1) conf, pred probs.max(dim1) results.append([fname, class_names[pred.item()], f{conf.item():.4f}]) with open(output_csv, w, newline) as f: writer csv.writer(f) writer.writerow([filename, predicted_class, confidence]) writer.writerows(results) print(fSaved {len(results)} predictions to {output_csv})unsqueeze(0)是把单张图的[3, 224, 224]变成[1, 3, 224, 224]因为模型 forward 期望有 batch 维度。torch.softmax把 logits 转成概率max(dim1)同时拿到置信度和类别索引。输出 CSV 方便后续用 Excel 或 pandas 分析。再说模型导出。如果你想把模型放到没有 PyTorch 的环境里跑可以导出成 ONNXdummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, fruit_veg_cnn.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )dynamic_axes让导出的模型支持可变 batch 大小。opset_version11兼容性较好。导出后用onnxruntime加载推理速度通常比原生 PyTorch 快一些尤其是在 CPU 上。最后说一个验证模型是否真正学到东西的小技巧用 Grad-CAM 可视化热力图看模型关注图片的哪个区域。如果它盯着背景而不是果蔬本身说明数据有偏或者模型走了捷径。常见做法是用pytorch-grad-cam库几行代码就能出图。我一般会在测试集上随机抽 20 张跑一遍确认关注区域合理再决定要不要继续调。从那以后我每次拿到一个图像分类项目都强制先跑一遍混淆矩阵和 Grad-CAM确认模型没在偷懒再去看准确率数字。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →