基于迁移学习的水果识别:VGG16、ResNet50等四模型对比实战
简介这份资源面向计算机、人工智能、通信工程等专业的在校学生与教师提供一套可直接用于毕业设计或课程设计的水果识别系统完整方案。项目基于迁移学习思路对ImageNet上预训练的VGG16、ResNet50、MobileNetV2、DenseNet121四种模型进行微调并迁移到水果数据集上完成分类任务最高准确率达到93.08%源码均经过实际运行测试答辩评审平均分约96分。压缩包共277个文件约17.53MB其中包含8个Python脚本负责模型训练与推理另有大量js、css、html等前端页面文件用于搭建可视化交互界面以及gif、jpg、png等图片素材和字体文件整体结构清晰、便于二次修改。目前已有319人学习下载适合希望快速上手深度学习图像分类、需要完整毕设参考或在此基础上扩展功能的读者。1. 从一份 93.08% 准确率的水果识别毕设说起带过几届毕设之后我对水果识别这个题目是有偏见的——太常见常见到很多同学直接拿个 MNIST 改改就交差答辩时被问一句为什么用这个模型就卡壳。但这份资源有点不一样它没有从零搭网络而是走迁移学习路线把 ImageNet 上预训练好的 VGG16、ResNet50、MobileNetV2、DenseNet121 四个骨干分别微调最后在水果数据集上跑到 93.08% 的准确率。这个数字不算惊艳但胜在真实、可复现而且四个模型横向对比的思路恰好是答辩老师最爱追问的点。这份资源包含源码、文档说明、数据集和训练好的模型权重适合计算机、人工智能、通信、自动化等专业的同学做毕设、课程设计或立项演示。如果你基础还行想在这个框架上改分类类别、换骨干网络、加个 Web 界面它也能当个不错的起点。下面我按这套东西怎么跑起来、参数怎么调、坑在哪的顺序拆一遍。2. 迁移学习选型四个骨干网络到底怎么选2.1 为什么是微调而不是从零训练水果数据集通常只有几千到上万张图类别也就几十种。这种规模下从零训练一个 CNN过拟合几乎是必然的——训练集准确率能冲到 99%验证集卡在 70% 上下不动。迁移学习的逻辑是ImageNet 上预训练的卷积层已经学会了边缘、纹理、颜色块这些通用特征水果识别需要的底层特征和它高度重合你只需要替换最后的全连接分类头再解冻部分高层做微调。常见做法是冻结前面的卷积基只训练新加的分类层等 loss 稳定后再解冻最后几个 block 一起微调。这份资源里四个模型都是按这个思路走的区别在于解冻的层数和学习率的设置。2.2 四个骨干的取舍模型参数量输入尺寸特点适合场景VGG16约 138M224×224结构简单全连接层占参数大头教学演示理解迁移学习流程ResNet50约 25M224×224残差连接训练稳定追求准确率显存够用MobileNetV2约 3.4M224×224深度可分离卷积轻量部署到移动端或边缘设备DenseNet121约 8M224×224密集连接特征复用强数据量偏少时表现好选型上没有绝对优劣。VGG16 参数量大但结构直白适合第一次跑通流程ResNet50 是稳妥的基线MobileNetV2 如果你后面要往手机或树莓派上搬优先考虑它DenseNet121 在小数据集上往往能比 ResNet 高出一两个点。资源里四个都给了建议先跑 MobileNetV2 快速验证流程再用 ResNet50 冲准确率。2.3 环境搭建与依赖安装先确认 Python 版本建议 3.8 到 3.10太新的版本有些深度学习库轮子还没跟上。# 创建虚拟环境避免和系统包冲突 python -m venv fruit_env # Windows 激活 fruit_env\Scripts\activate # Linux/Mac 激活 source fruit_env/bin/activate # 安装核心依赖版本按资源文档里的来 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy pandas matplotlib opencv-python pillow tqdm scikit-learn这里torch的安装命令带了 CUDA 11.8 的索引地址如果你没有 NVIDIA 显卡把--index-url那行去掉装 CPU 版本即可。opencv-python用于图像预处理scikit-learn用来生成分类报告和混淆矩阵。装完之后用python -c import torch; print(torch.cuda.is_available())验证一下 GPU 是否可用返回True说明环境没问题。2.4 数据集的目录结构资源里的数据集一般按类别分文件夹存放这是torchvision.datasets.ImageFolder能直接读取的标准格式fruit_dataset/ ├── train/ │ ├── apple/ │ ├── banana/ │ ├── orange/ │ └── ... ├── val/ │ ├── apple/ │ └── ... └── test/ └── ...如果你的数据是混在一起的一堆图片加一个标签 CSV需要先写个脚本按类别拆分。常见做法是训练集、验证集、测试集按 7:2:1 划分划分时注意每个类别都要有代表别出现某个类别验证集里一张图都没有的情况。3. 训练流程拆解从数据加载到模型保存3.1 数据增强与 DataLoader 配置水果图片的拍摄角度、光照、背景差异很大数据增强是防过拟合的第一道防线。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强随机裁剪、翻转、颜色抖动 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), # 随机裁剪缩放 transforms.RandomHorizontalFlip(p0.5), # 水平翻转 transforms.RandomRotation(15), # 随机旋转±15度 transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1), # 颜色抖动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet 均值 std[0.229, 0.224, 0.225]) # ImageNet 标准差 ]) # 验证集和测试集只做缩放和归一化不做随机增强 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(fruit_dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(fruit_dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4)Normalize里的均值和标准差必须和预训练模型一致因为骨干网络是在 ImageNet 上用这套参数训练的你换了参数底层特征的分布就对不上微调效果会打折扣。batch_size设 32 是显存和训练速度的折中显存不够就降到 16 或 8同时把学习率相应调小。num_workers在 Windows 上有时会报错改成 0 用主进程加载即可。3.2 替换分类头与冻结策略以 ResNet50 为例把原来的 1000 类输出换成你的水果类别数import torch.nn as nn from torchvision import models num_classes len(train_dataset.classes) # 自动获取类别数 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) # 冻结所有卷积层参数 for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 model.fc nn.Sequential( nn.Linear(model.fc.in_features, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) # 只训练新加的分类头 optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3)冻结之后只有model.fc的参数会被更新训练速度快也不容易过拟合。等分类头收敛后可以解冻最后一个残差 block 做精细微调学习率降到 1e-4 甚至 1e-5。这一步是准确率能不能从 90% 推到 93% 的关键但解冻太多层反而会破坏预训练特征血泪经验是先从最后一层开始试。3.3 训练循环与学习率调度import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() # 余弦退火学习率从 1e-3 平滑降到 1e-6 scheduler CosineAnnealingLR(optimizer, T_max30) best_acc 0.0 for epoch in range(30): model.train() running_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() running_loss loss.item() # 验证阶段 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 scheduler.step() print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {acc:.4f}) # 保存最佳模型 if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth)CosineAnnealingLR让学习率按余弦曲线下降前期大步走、后期小步收敛比固定学习率稳。T_max设成总 epoch 数这里 30 轮对应 30。保存模型时只存state_dict而不是整个模型对象文件更小加载时也更灵活。训练过程中如果验证准确率连续几轮不涨可以提前停别硬跑满。3.4 四个模型的对比实验资源里四个骨干都跑了如果你想复现对比把上面的models.resnet50换成对应的即可# VGG16 model models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) model.classifier[6] nn.Linear(4096, num_classes) # MobileNetV2 model models.mobilenet_v2(weightsmodels.MobileNet_V2_Weights.IMAGENET1K_V1) model.classifier[1] nn.Linear(model.last_channel, num_classes) # DenseNet121 model models.densenet121(weightsmodels.DenseNet121_Weights.IMAGENET1K_V1) model.classifier nn.Linear(model.classifier.in_features, num_classes)注意每个模型替换分类头的位置不一样VGG16 在classifier[6]MobileNetV2 在classifier[1]DenseNet121 直接换classifier。改错了不会报错但训练时分类头没被替换输出还是 1000 类loss 会一直降不下去。这是新手最容易翻车的地方改完打印一下模型结构确认。4. 推理与部署模型怎么用起来4.1 单张图片推理训练完拿到best_model.pth写个推理脚本验证效果from PIL import Image import torch from torchvision import transforms, models # 加载模型结构必须和训练时一致 model models.resnet50(weightsNone) model.fc nn.Sequential( nn.Linear(model.fc.in_features, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 预处理和验证集保持一致 preprocess 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]) ]) img Image.open(test_apple.jpg).convert(RGB) input_tensor preprocess(img).unsqueeze(0) # 加 batch 维度 with torch.no_grad(): output model(input_tensor) prob torch.softmax(output, dim1) conf, pred torch.max(prob, 1) print(f预测类别: {train_dataset.classes[pred.item()]}, 置信度: {conf.item():.4f})load_state_dict时如果报 key 不匹配八成是模型结构没对上检查分类头的层数和顺序。map_locationcpu让你在没有 GPU 的机器上也能加载。unsqueeze(0)是给单张图补上 batch 维度模型默认接受四维输入。4.2 批量测试与混淆矩阵答辩时老师常问哪类容易分错提前跑个混淆矩阵心里有数from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namestest_dataset.classes)) cm confusion_matrix(all_labels, all_preds) print(cm)classification_report会给出每个类别的精确率、召回率和 F1比一个总准确率有说服力得多。混淆矩阵能看出哪些类别互相混淆比如青苹果和青柠、橙子和橘子这些在特征上确实接近可以在文档里主动说明并给出改进思路。4.3 模型导出与轻量化如果要把模型部署到 Web 或移动端可以考虑导出 ONNXdummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, fruit_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}})dynamic_axes让 batch 维度可变推理时可以一次传多张图。ONNX 格式能被 ONNX Runtime、TensorRT 等推理引擎加载速度比原生 PyTorch 快不少。MobileNetV2 导出的 ONNX 模型通常只有十几 MB塞进小程序或 App 后端都够用。5. 避坑与排查那些文档里不会写的问题5.1 显存不够训练中途崩现象是跑几个 batch 后报CUDA out of memory。原因通常是 batch_size 太大或者验证阶段没加torch.no_grad()导致计算图一直累积。解决办法把 batch_size 降到 16 或 8验证和测试代码块外面套with torch.no_grad():训练循环里每个 batch 结束后optimizer.zero_grad()别漏。如果还不行用torch.cuda.empty_cache()手动清一下缓存。5.2 验证准确率不动loss 不降现象是训练十几轮验证准确率一直在随机水平比如 10 类就是 10% 左右。原因多半是分类头没替换成功模型还在输出 1000 类或者标签和输出对不上。解决办法打印model结构确认最后一层输出维度等于类别数检查ImageFolder读到的classes列表和实际文件夹是否一致确认criterion用的是CrossEntropyLoss而不是BCELoss。5.3 训练准确率高但测试低现象是训练集 99%测试集 70%。这是典型过拟合。原因可能是数据增强太弱、模型参数量相对数据量太大、或者训练轮数过多。解决办法加强数据增强加 RandAugment 或 MixUp换更轻量的骨干如 MobileNetV2加 Dropout 和权重衰减早停。别一味追求训练准确率验证集才是真实水平。5.4 推理时类别对不上现象是预测结果总是某几个类别或者置信度普遍很低。原因是推理时的预处理和训练时不一致比如忘了Normalize或者Resize的尺寸不对。解决办法把训练时的val_transform原封不动复制到推理脚本里别凭记忆重写。Image.open之后记得.convert(RGB)有些 PNG 带透明通道直接转 Tensor 会报错。5.5 换数据集后准确率暴跌现象是拿自己的水果照片测试准确率远低于测试集。原因是训练数据和你的实际场景分布不一致比如训练集都是白底商品图你拍的是果园实景。解决办法要么在训练集里加入实景图做微调要么在推理前做背景分割。这个问题没有银弹只能靠数据覆盖。6. 进阶技巧把 93% 再往上推一推如果你已经跑通了基线想让准确率再好看一点可以试试这几个方向。第一个是解冻更多层做渐进式微调先只训分类头 10 轮再解冻最后一个 block 训 10 轮学习率降到 1e-4最后解冻倒数第二个 block 训 5 轮学习率 1e-5。这个策略在多个数据集上都能稳定涨 1 到 2 个点代价是训练时间翻倍。第二个是测试时增强TTA对同一张测试图做多次不同变换翻转、裁剪把预测概率平均。代码不复杂在推理脚本里加个循环就行def predict_with_tta(model, img, n5): probs [] for _ in range(n): aug transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) tensor aug(img).unsqueeze(0) with torch.no_grad(): out torch.softmax(model(tensor), dim1) probs.append(out) return torch.mean(torch.stack(probs), dim0)TTA 的代价是推理时间变成 n 倍但准确率通常能涨 0.5 到 1 个点答辩演示时值得开。第三个是类别不平衡处理如果某些水果样本特别少用WeightedRandomSampler给少样本类别更高采样权重或者在 loss 里加类别权重。这个改动小但对小类别召回率提升明显。最后一个习惯每次改完超参数别只看最终准确率把训练曲线、混淆矩阵、分类报告都存下来。答辩时老师问你怎么调的你能拿出对比图比空口说我试了很多参数有说服力得多。从那以后我每次跑实验都强制走一遍保存日志 画曲线 存最佳权重的流程后悔药这东西能提前备就提前备。希望这份拆解帮到你资源里的文档和代码配合着看跑通不难。本文还有配套的精品资源点击获取