ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于深度学习的图像分类实战:水果识别系统从训练到部署

基于深度学习的图像分类实战:水果识别系统从训练到部署 简介这是一份面向计算机相关专业学生与初学者的深度学习实战项目资源围绕水果图像分类任务基于VGG16、ResNet50、MobileNetV2、DenseNet121四种预训练模型进行迁移学习微调最终模型最高准确率达93.08%可有效解决毕设选题、课程设计及入门实践中的完整流程参考问题。压缩包共277个文件约17.53MB包含Python源码、数据集、模型及文档说明另有网页前端展示所需的js、css、html与图片素材结构完整便于直接运行与二次开发。目前已有319人学习下载项目代码均测试通过答辩评审平均分达96分适合需要快速搭建演示系统或借鉴工程实现思路的读者。同时提供清晰的目录组织便于定位数据预处理、模型训练与评估等关键模块可在此基础上快速扩展新功能。1. 基于深度学习的水果识别系统难的不是模型而是四件套对齐当一个毕设标题里同时出现“源码文档说明数据集模型”真正要交付的不只是一个跑得通的train.py。答辩评委通常不会先问准确率而是问三件事数据怎么来的、标签怎么定的、换一台电脑能不能把模型训练过程复制出来。水果识别本身是典型的图像分类问题用 PyTorch 或 TensorFlow 都能做但“高分”的差异往往体现在数据划分、模型选择、训练复现性和评估完整性这些容易被一眼带过的地方。这篇文章会从模型选型开始把数据集整理、训练脚本、评估指标、文档结构和演示 Demo 串成一条可以在本地跑的链路。重点是让 Python 环境里能直接执行也让答辩时每个文件都有存在的理由。适合正在做深度学习方向毕业设计、或者想把分类项目工程化落地的开发者。2. 水果识别模型选型从分类难点到迁移学习水果识别在公开数据集里通常是 10 到 100 多个类别颜色、纹理、形状差异明显但仍有几个让准确率上不去的典型场景青苹果和青梨颜色相近、柠檬和青柠形状相似、葡萄和樱桃在低分辨率下容易互相污染。数据规模通常不大单类几百张到几千张直接从头训练深层网络很容易过拟合所以迁移学习是首选方案。2.1 为什么不用手工特征而是直接端到端学习早期水果识别常用颜色直方图、灰度共生矩阵、Hu 矩这些手工特征配合 SVM 或随机森林。这类方法在固定光照、单一背景的商品分选线上有效但毕设场景里往往使用网络图片背景复杂、光照变化大、水果互相遮挡。手工特征只能描述局部统计信息很难兼顾“颜色相似但纹理不同”和“纹理相似但形状不同”这两类判别需求。深度学习 CNN 的好处是把特征提取和分类器放在同一个损失函数里优化。浅层卷积学边缘和颜色块深层卷积学语义结构最终的全连接层再组合这些特征做决策。对于水果这样类别间有重叠特征的任务端到端训练能自动找到判别区域比如苹果的高光和苹果梗的形态。2.2 主干网络参数对比ResNet、MobileNet 还是 EfficientNet选主线网络时不需要追新重点看训练时间和显存。下面这张表是我在做分类毕设项目时常用的对比维度模型参数量单张推理相对速度在中小数据集上的表现适合场景ResNet1811.7M快够用易收敛CPU 也能勉强推理ResNet5025.6M中等精度高于 ResNet18GPU 训练通用默认MobileNetV32.5M很快精度略低但省显存需要部署到本机演示EfficientNetB05.3M中等参数效率高数据量偏小时优先表中的参数量是标准 ImageNet 版本实际会随分类数微调。对水果识别数据量通常不到 2 万张ResNet18 完全够用ResNet50 可以余量更大一些。EfficientNetB0 的输入分辨率要求 224和 ResNet 一致迁移方便。2.3 迁移学习的冻结与解冻策略预训练权重来自 ImageNet通用边缘和纹理特征可以直接复用。常见做法是先冻住前面大部分层只训练最后全连接层等 loss 不再下降再解冻部分深层卷积一起微调。下面这段 PyTorch 代码展示了冻结逻辑import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False num_features model.fc.in_features model.fc torch.nn.Linear(num_features, 10)这段代码把所有不在layer4和fc里的参数都冻结。第一次前向时只计算梯度更新最后一层显存占用小训练速度快。等基本收敛再把layer4的requires_grad打开用较小学习率更新。要注意的是 PyTorch 旧代码里常见的pretrainedTrue参数现在会提示改成weights接口建议直接使用带后缀的权重枚举避免未来升级后无法加载。冻结层数不是越多越好如果数据集中水果背景和 ImageNet 差异很大比如统一白底冻结住layer1和layer2反而能保留边缘特征减少过拟合。3. 源码工程化数据加载、训练循环与预测脚本的组织一个能得高分的源码包不是只有一个 Jupyter Notebook。导师打开目录后五秒内应该能看出哪里是数据、哪里是模型、哪里是训练入口。工程结构清晰也方便后续写进文档说明。3.1 一个可复现的目录结构推荐按功能拆分而不是把所有东西塞进一个文件fruit_recognition/ ├── data/ │ ├── raw/ # 原始下载的图片 │ ├── processed/ # 按类别归好后的目录 │ └── split/ # train/val/test 划分结果 ├── src/ │ ├── dataset.py # 自定义 Dataset │ ├── model.py # 模型定义和加载 │ ├── train.py # 训练入口 │ ├── predict.py # 单张图片预测 │ └── evaluate.py # 混淆矩阵和分类报告 ├── checkpoints/ │ ├── best_model.pth │ └── last_model.pth ├── docs/ │ └── 实验报告.md └── requirements.txtdata/raw放原始下载文件不直接修改data/processed是清洗后按类别建的文件夹ImageFolder可以直接读取checkpoints负责模型保存避免和源码混在一起。requirements.txt里至少包含torch、torchvision、pillow、matplotlib、scikit-learn顺便固定主版本号。3.2 自定义 Dataset 与标签读取虽然ImageFolder能直接读文件夹名作为标签但有时需要从 CSV 文件加载标签或者对图片做灰度、裁剪后再进模型。自定义 Dataset 更灵活import torch from torch.utils.data import Dataset from PIL import Image from pathlib import Path class FruitDataset(Dataset): def __init__(self, img_dir, labels_file, transformNone): self.img_paths [] self.labels [] for line in Path(labels_file).read_text().strip().splitlines(): rel_path, label_id line.split(,) self.img_paths.append(Path(img_dir) / rel_path) self.labels.append(int(label_id)) self.transform transform def __len__(self): return len(self.img_paths) def __getitem__(self, idx): image Image.open(self.img_paths[idx]).convert(RGB) if self.transform: image self.transform(image) return image, torch.tensor(self.labels[idx], dtypetorch.long)这段把标签文件设计成图片相对路径,类别编号两列避免只依赖中文文件夹名。__getitem__里强制convert(RGB)否则个别灰度图会让张量维度不匹配。如果图片损坏建议在读取时用try-except跳过坏图而不是让整个训练崩溃。3.3 训练循环最小实现与参数说明训练脚本是源码包的核心。下面这段是精简过但能直接运行的版本import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import transforms from torchvision.models import resnet18 from torch.optim import AdamW transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_loader DataLoader(dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) model resnet18(weightsresnet.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) criterion nn.CrossEntropyLoss() optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-4) for epoch in range(30): model.train() total_loss 0.0 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() total_loss loss.item() print(fepoch {epoch1}, loss {total_loss / len(train_loader):.4f})参数说明batch_size32在 ResNet18 且 224 分辨率时占用约 4GB 显存如果显存不足优先减到 16num_workers4让数据加载并行Windows 下如果报BrokenPipeError就改成 0 最稳妥weight_decay1e-4对防止过拟合帮助很大。学习率1e-4是微调阶段的常见起点如果从头训练可以用1e-3但水果数据规模不建议从头训。3.4 预测脚本与模型加载训练完成后预测脚本必须和训练时使用同一套 transformdef predict(image_path): image Image.open(image_path).convert(RGB) image test_transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): output model(image) prob, idx torch.max(torch.softmax(output, dim1), dim1) return class_names[idx.item()], prob.item()这里最容易踩的坑是把model.eval()写在循环外面或者忘记torch.no_grad()。这两步不写推理时行为不稳定而且显存会越积越多。class_names建议保存成 JSON在文档里说明映射关系不要用不同数字顺序的硬编码。4. 数据集清洗、增强与类别均衡处理公开的水果数据集很多但下载后的原始目录不一定适合直接训练。常见问题是类别名有空格、大小写不统一、样本数量差距悬殊、还有少量损坏图片。如果直接ImageFolder读取最终会得到错误标签或模型崩溃。4.1 从原始下载到正常训练的数据处理流程从开源社区下载的数据集通常以类别名作为顶层目录例如apple/、banana/。处理顺序是先统计每个类别的图片数去掉少于 5 张的类别再统一所有图片到 RGB 格式最后按train:val:test 7:2:1划分。不要在拿到数据后立刻训练先写个统计脚本看分布更稳。4.2 目录转成 ImageFolder 的 Python 脚本下面脚本把原始目录复制到新目录并同时生成 train/val 两个文件夹import os import shutil import random from pathlib import Path src_root Path(data/raw) dst_root Path(data/split) for class_dir in src_root.iterdir(): if not class_dir.is_dir(): continue images list(class_dir.glob(*.jpg)) list(class_dir.glob(*.jpeg)) random.seed(42) random.shuffle(images) split_idx int(len(images) * 0.8) for phase in [train, val]: subset images[:split_idx] if phase train else images[split_idx:] target_dir dst_root / phase / class_dir.name target_dir.mkdir(parentsTrue, exist_okTrue) for img in subset: shutil.copy(img, target_dir / img.name)用random.seed(42)固定划分保证文档里写的“80% 训练”能复现。glob(*.jpg)只匹配 jpg如果数据集里混有 png 或 bmp要额外补扩展名或者直接用class_dir.iterdir()配合后缀白名单。4.3 针对水果图像的增强策略水果识别不能只做水平翻转因为颜色是重要特征而ColorJitter会把红苹果改成类似青色反而引入噪声。更合理的是小范围调整亮度、饱和度并加上随机旋转和裁剪train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomResizedCrop从 256 分辨率中裁剪出 224模拟不同距离拍摄RandomRotation需要注意背景填充色使用默认的黑色填充有时会在最终预测时产生奇怪边缘。如果类别本身旋转角度固定比如超市称重摄像头固定俯拍那就不要用大角度旋转最多degrees5。4.4 处理类别不平衡加权采样与损失函数如果数据集中苹果有 3000 张榴莲只有 80 张直接训练会让小类别准确率几乎为 0。常见做法是使用WeightedRandomSampler让每个类别在每轮迭代中被采样到的概率均衡from torch.utils.data.sampler import WeightedRandomSampler import numpy as np labels np.array([sample[1] for sample in dataset]) class_counts np.bincount(labels) weight 1.0 / class_counts[labels] sampler WeightedRandomSampler(weight, num_sampleslen(labels), replacementTrue) train_loader DataLoader(dataset, batch_size32, samplersampler, num_workers4)使用WeightedRandomSampler后DataLoader里的shuffle必须设置成False否则会冲突。另一个方案是给CrossEntropyLoss传入weight参数比如weighttorch.tensor([0.5, 2.0, ...])但加权损失只改变梯度大小采样器能更稳定地保证每个 batch 内部类别均衡。两种可以同时用但不要一开始就加先跑一版普通 loss 作为基线。不平衡程度推荐手段何时停用最大类别 / 最小类别 10普通损失无需处理比例在 10~50加权损失小类别准确率开始升高后可以继续比例 50采样器 加权损失如果出现过拟合再减弱5. 文档说明、评估指标与 checkpoint 管理源码包里“文档说明”不是把代码注释抄一遍而是要解释数据来源、模型结构、实验环境、评价方法和复现步骤。评阅老师会按文档去跑一次所以文档里的命令必须和代码真实路径一致。5.1 毕设文档中的系统设计章节应包含什么文档至少要有这几块硬件和软件环境表写明 GPU 型号、Python 版本、PyTorch 版本数据集描述写明类别数、每类图片数量、训练验证划分比例模型结构说明不能只写“使用 ResNet18”而要说明为什么选择它训练超参数包括学习率、batch size、epoch 和优化器。这些内容用表格呈现会比大段文字更清晰答辩时也能快速定位。5.2 混淆矩阵与分类报告的计算代码只报准确率太单薄高分毕设还需要每个类别的精确率、召回率和 F1。scikit-learn可以直接计算from sklearn.metrics import confusion_matrix, classification_report y_true [] y_pred [] model.eval() with torch.no_grad(): for images, labels in val_loader: outputs model(images.to(device)) _, predicted torch.max(outputs, 1) y_true.extend(labels.cpu().numpy()) y_pred.extend(predicted.cpu().numpy()) cm confusion_matrix(y_true, y_pred) report classification_report(y_true, y_pred, target_namesclass_names) print(report)classification_report的输出里包含了每个类别的 P/R/F1以及加权平均。混淆矩阵最好用matplotlib画成热力图保存到docs/文件夹作为答辩 PPT 里的证据。如果某两个类别互相混淆严重就说明它们视觉相似度太高需要单独补充数据或提升输入分辨率。5.3 模型保存best checkpoint 与 last model训练循环中不能只保存最后一轮模型要同时保存验证集准确率最高的一次。常见做法是best_acc 0.0 for epoch in range(epochs): # 训练代码 val_acc evaluate(model, val_loader) if val_acc best_acc: best_acc val_acc torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_acc: best_acc, }, checkpoints/best_model.pth)保存 optimizer 的状态后精调或断点续训才有意义。last_model.pth可以只保存model_state_dict两者命名上要区分。加载时注意模型结构必须先初始化再加载字典否则会报出 key 不匹配。5.4 训练过程中的典型报错与解决办法torch版本更新很快代码在电脑上跑不起来是最高频的问题。ModuleNotFoundError: No module named torch说明环境没配好需要先按 Python 安装教程把虚拟环境建好。CUDA out of memory优先降低 batch size其次降低num_workers。size mismatch几乎都来自全连接层类别数不一致检查model.fc的输出数量和数据集classes数量。另一个隐蔽问题是不同图片尺寸被Resize成不一致时DataLoader最后一步报错设置transforms.Resize((224, 224))能解决。6. 把水果识别系统做成可演示的 Web 应用Gradio 与推理细节毕设答辩现场最加分的环节是打开一个网页上传图片立刻看到识别结果。用 Flask 写太啰嗦Gradio 几行就能完成界面和交互。这个过程也是检验模型是否有实际可用性的关键。6.1 用 Gradio 写一个最小推理界面import gradio as gr def recognize(image): img Image.fromarray(image).convert(RGB) img test_transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs model(img) probs torch.softmax(outputs, dim1)[0] top_prob, top_idx torch.topk(probs, k3) return {class_names[i]: float(p) for i, p in zip(top_idx, top_prob)} demo gr.Interface(fnrecognize, inputsimage, outputslabel) demo.launch()inputsimage会让 Gradio 使用 PIL 处理上传图片所以在recognize里先转换成 PIL再走同一套测试 transform。这样写的好处是不用自己处理 base64 编码和 HTTP 请求。6.2 推理 transform 与训练保持一致有相当多项目在训练时用了RandomResizedCrop在推理时也直接使用包含随机增强的完整 transform导致每次预测结果不稳定。推理端应该使用去随机化的版本test_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])这里把RandomResizedCrop换成固定比例的中心裁剪保证了不同上传图片都从中心取同样的区域结果可复现。答辩时如果演示同一张图两次结果不同大概率就是这里写错了。6.3 三个值得调的参数学习率、dropout 和输入分辨率想让精度再上一个台阶优先调这三个地方。学习率在微调阶段建议从3e-5到5e-4之间做三组实验而不是直接用1e-3。如果 ResNet 最后一层出现过拟合可以在全连接前插入torch.nn.Dropout(p0.3)但注意 ResNet18 本身没有 dropout 可调位置需要自己包一层。输入分辨率从 224 升到 320 通常会对小水果类别有明显改善代价是显存占用增加约一倍。实测时先在小验证集上抽查错误案例看是背景干扰还是目标过小再决定要不要动这三个参数。本文还有配套的精品资源点击获取
返回列表