☰
迁移学习微调四模型实现水果分类,准确率93.08%
2026/9/26 14:46:21 网站建设 项目流程

简介:这是一份基于深度学习的水果识别系统Python毕设资源,面向计算机相关专业学生、教师及企业开发者,也可作为毕业设计、课程设计或入门迁移学习实战的项目范例。资源完整包含可运行源码、配套文档说明、水果数据集以及训练好的模型,采用VGG16、ResNet50、MobileNetV2、DenseNet121四种预训练模型进行微调,在水果数据集上最高准确率达到93.08%。压缩包共277个文件,大小17.53MB,以Python脚本、HTML页面为核心,附带大量JS/CSS前端交互资源、GIF操作演示图及jpg/png数据集图片,另有txt和markdown说明文档方便对照查阅。项目代码经过完整测试运行,答辩评审平均分96分,目前已吸引319人学习浏览。对于希望快速搭建深度学习毕设或系统了解迁移学习分类任务的开发者,这份资源提供了从数据处理、模型训练、结果评估到界面展示的完整参考。

1. 一份能直接复现的水果识别毕设:迁移学习微调四个模型,最高 93.08%

答辩时评委最常问的一句是:数据集就几千张图,你怎么把准确率做到 90% 以上的?项目里那套迁移学习做法就是答案。这份 Python 高分毕设资源把 VGG16、ResNet50、MobileNetV2、DenseNet121 四个带 ImageNet 预训练权重的模型全部微调跑通,水果分类最高准确率达到 93.08%,源码、文档说明、数据集、训练好的模型一应俱全,代码测试没问题才打包上传,答辩评分 96 分。它适合计科、人工智能、通信、自动化、电子信息方向做毕设或课设的在校生,也适合第一次想完整走一遍深度学习图像分类全流程的初学者。下面按我复盘这套资源的顺序展开:先讲为什么微调在这里是必选项,再逐步落到数据、训练、评估和踩坑。

2. 为什么选迁移学习:四个预训练模型的选型逻辑

2.1 小数据集上从零训练会翻车,微调才是常规解

图像分类毕设最常见的现象是:数据量只有几千张、十几个类别,从随机初始化开始训练一个深层 CNN,训练集准确率能冲到接近 100%,验证集却卡在 70% 上下不来。原因是网络参数量动辄几千万,数据量根本喂不满,模型把训练集中的背景、光照、叶片纹理都背了下来。

迁移学习解决的是这个样本量矛盾。ImageNet 上有 130 万张图片、1000 个类别,预训练模型已经学会了提取边缘、纹理、形状这些通用特征,这些特征对水果识别同样适用。微调的做法是保留预训练卷积层的权重,替换掉最后的 1000 类分类器,接上自己的水果类别分类头,再用小学习率做少量训练。

常见做法是先冻结全部卷积层,只训练新接的分类头,等分类头收敛后再解冻最后几个卷积块做二次微调。答辩被问“为什么用预训练模型”时,这一套逻辑能直接说清楚:不是因为我们数据多,恰恰是因为数据少才要借力。

2.2 VGG16、ResNet50、MobileNetV2、DenseNet121:四套方案的取舍

这份资源把四个预训练模型都跑了一遍,不是随意堆叠,而是四种结构路线各代表一类思路,放进论文对比实验里说服力很强。我的理解是:

  • VGG16 结构规整,全部由 3×3 卷积叠加,参数量约 1.38 亿,特征提取直白、调参空间大,适合当 baseline。代价是训练和推理都偏慢,显存占用高。
  • ResNet50 引入了残差连接,解决了网络加深后的退化问题,训练收敛稳,准确率通常高于 VGG16,是毕设里最高频的默认选项。
  • MobileNetV2 用深度可分离卷积替代普通卷积,参数量和计算量远小于前两者,训练和推理都快,适合做部署演示。
  • DenseNet121 让每一层与后续所有层建立密集连接,特征是重用的而不是重复学的,在中等规模数据集上表现往往不差。

四个模型在同一套数据、同一套超参下分别训练,最后把验证集准确率列成表,谁高选谁做最终模型。这就是这套资源里 93.08% 这个数字的由来。下面先解决数据这一关。

3. 数据整理:目录结构、标签读取与数据增强

3.1 一份能被直接读入的目录结构

这套资源里的数据集是如何组织的?常见毕设做法是按类别建子目录,训练集和验证集分开,目录名就是类别名。例如data/fruit/train/apple下放苹果训练图,data/fruit/val/apple下放苹果验证图。

用 Keras 的ImageDataGenerator配合flow_from_directory读取时,目录名自动映射成标签,不需要手写标注文件。这是绝大多数毕设项目的标准读取方式:

from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1.0 / 255, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True ) val_datagen = ImageDataGenerator(rescale=1.0 / 255) train_generator = train_datagen.flow_from_directory( 'data/fruit/train', target_size=(224, 224), batch_size=32, class_mode='categorical' ) val_generator = val_datagen.flow_from_directory( 'data/fruit/val', target_size=(224, 224), batch_size=32, class_mode='categorical', shuffle=False )

这套代码里值得注意的有三点。flow_from_directory会自动扫描子目录名生成类别索引,所以目录结构一旦建成,标签就固定下来了;class_mode='categorical'意味着标签会转成 one-hot 编码,配合输出层的 softmax 使用;shuffle=False要刻意对验证集关闭随机打乱。后面画混淆矩阵、逐类统计准确率时,预测顺序必须和标签顺序一致,这个参数一旦漏掉,评估结果全是错位的。

3.2 数据增强参数怎么设

上面开了六项增强,幅度是针对这个水果数据集调的。rotation_range=20表示图片在 ±20 度范围内随机旋转;width_shift_range=0.2和height_shift_range=0.2表示水平、垂直方向最多平移 20% 的图片宽度或高度;zoom_range=0.2是缩放范围;horizontal_flip=True打开水平翻转。

需要提醒的是垂直翻转vertical_flip尽量别开。水果图片虽然不容易出错,但像带果柄、带蒂的样本,一旦上下翻转,果柄方向就和真实采集情况相反,等于向训练集里注入噪声。增强幅度也不是越大越好,旋转超过 45 度、缩放超过 30% 会产生大量严重失真的样本,反而拉低验证集准确率。

另外rescale=1.0/255把像素值从 0-255 压缩到 0-1,这是毕设里最常见的预处理。如果想让输入分布更接近 ImageNet 上训练时的分布,可以在加载预训练模型后用preprocess_input做均值-方差归一化,这一项在评估和预测时必须保持一致,否则模型会表现异常。

3.3 动手前先核对类别数和每类数量

flow_from_directory执行后会打印类似Found 6000 images belonging to 15 classes的提示,多看一眼这个数字能省很多事。许多翻车案例是train目录里混进了.ipynb_checkpoints文件夹、缓存缩略图或重复拷贝,导致类别数凭空多出几个、某个类图片数直接偏少。

动手训练前的检查习惯是:打印train_generator.class_indices看类别名和索引映射,再按类别统计图片数量。类别数必须和class_mode='categorical'的输出层神经元数对得上,各类图片数量差距也不要超过一倍。水果这类数据通常比较均衡,如果发现某个类只有几十张,先补齐数据再训,而不是指望增强把它救回来。

4. 训练与评估:从预训练权重到 93.08% 准确率

4.1 搭建微调模型的主干代码

训练脚本的核心是封装一个build_model(model_name)函数,根据传入的名字加载对应预训练网络的卷积部分,再接全局平均池化和自己的分类头。精简后可复用的是:

# 加载预训练权重,替换分类头 from tensorflow.keras.applications import VGG16, ResNet50, MobileNetV2, DenseNet121 from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout from tensorflow.keras.models import Model from tensorflow.keras.optimizers import Adam def build_model(base_name='resnet50', num_classes=15, fc_size=256): base_models = { 'vgg16': (VGG16, (224, 224, 3)), 'resnet50': (ResNet50, (224, 224, 3)), 'mobilenetv2': (MobileNetV2, (224, 224, 3)), 'densenet121': (DenseNet121, (224, 224, 3)) } builder, input_shape = base_models[base_name] # weights='imagenet' 加载预训练权重,include_top=False 去掉 1000 类分类器 base_model = builder(weights='imagenet', include_top=False, input_shape=input_shape) base_model.trainable = False x = base_model.output x = GlobalAveragePooling2D()(x) x = Dropout(0.5)(x) x = Dense(fc_size, activation='relu')(x) x = Dropout(0.3)(x) outputs = Dense(num_classes, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=outputs) return model, base_model

这里有两个关键设计。include_top=False把 ImageNet 上训练好的 1000 类分类器整个去掉,只保留卷积特征提取部分;用GlobalAveragePooling2D而不是Flatten来过渡,能把全连接层的参数量压缩一个量级,是这类小数据集项目防过拟合最有效的结构手段。两个 Dropout 分别设置在中间层和输出层前,前者值取 0.5 效果通常不错,后者 0.3 是为了避免分类头输出过于自信。

4.2 先冻结后解冻:两阶段训练策略

分类头刚初始化的权重数值很大,如果一上来就解冻全部卷积层,梯度回传会把预训练权重冲坏。常见做法是分两个阶段,先只训练新接的分类头,等它收敛后再解冻部分底层做微调:

model.compile( optimizer=Adam(learning_rate=1e-4), loss='categorical_crossentropy', metrics=['accuracy'] ) # 第一阶段:只训分类头 history = model.fit( train_generator, validation_data=val_generator, epochs=10, steps_per_epoch=train_generator.samples // train_generator.batch_size, validation_steps=val_generator.samples // val_generator.batch_size, verbose=1 ) # 第二阶段:解冻后半段卷积块,低学习率微调 base_model.trainable = True for layer in base_model.layers: if 'block5_conv' not in layer.name and 'conv5_block3' not in layer.name: layer.trainable = False model.compile( optimizer=Adam(learning_rate=1e-5), loss='categorical_crossentropy', metrics=['accuracy'] ) history_finetune = model.fit( train_generator, validation_data=val_generator, epochs=10, steps_per_epoch=train_generator.samples // train_generator.batch_size, validation_steps=val_generator.samples // val_generator.batch_size, verbose=1 )

学习率从 1e-4 降到 1e-5 不是玄学。第一阶段新分类头需要学得激进一点;第二阶段解冻的卷积层权重在 ImageNet 上已经很合适,学习率太大会把通用特征破坏掉,调小让更新保持温和。解冻层名的写法需要注意:VGG16 的最后一段是block5_conv开头,ResNet50 是conv5_block3开头,MobileNetV2 和 DenseNet121 对应的层名不同,换成别的模型时这个过滤条件必须跟着改。

训练过程中再加两个回调就完整了:

from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping checkpoint = ModelCheckpoint( 'best_model.h5', monitor='val_accuracy', mode='max', save_best_only=True, verbose=1 ) early = EarlyStopping( monitor='val_accuracy', patience=8, restore_best_weights=True )

save_best_only=True只保存验证集准确率最高的那一份权重,避免最后几个 epoch 过拟合时覆盖掉最优模型;restore_best_weights=True会在早停时把权重回滚到最优状态。这两个回调配合使用,基本不用担心训练完手里的模型反而比中途差。

4.3 训练日志怎么盯

训练时终端里每一轮会打印accuracy和val_accuracy。第一阶段的正常形态是:前 2-3 轮val_accuracy从零点几快速爬到 0.8 以上,这是新分类头在快速学习特征到类别的映射关系;之后进入缓慢爬升期,每轮提升一两个百分点,最终在第一阶段结束时稳定在 0.85-0.9 附近。

第二阶段解冻后,val_accuracy一般会先小幅波动,再慢慢突破第一阶段的高点。如果出现train_loss持续下降、val_loss不降反升,说明模型开始过拟合了,此时即使准确率还在涨,也应该依赖EarlyStopping停下来。资源里的 93.08% 就是用四组模型里验证集表现最好的那一组跑出来的,训练脚本会把训练曲线和评估结果导出成图和 CSV,直接能放进论文做对比实验。

4.4 用独立的测试集重算准确率

训练完成后,严谨的做法是用一份独立的测试集重算,而不是只报验证集结果。这份资源里数据是按类别拆好 train、val、test 三套目录的,评估脚本大致是这样:

from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.image import ImageDataGenerator model = load_model('best_model.h5') test_datagen = ImageDataGenerator(rescale=1.0 / 255) test_generator = test_datagen.flow_from_directory( 'data/fruit/test', target_size=(224, 224), batch_size=32, class_mode='categorical', shuffle=False ) loss, acc = model.evaluate(test_generator) print(f'test_loss={loss:.4f}, test_acc={acc:.4f}')

model.evaluate会按 batch 跑完全部测试图片,返回全局准确率。如果答辩时想展示逐类的薄弱环节,需要predict得到每一类的概率矩阵,再argmax转成预测类别,与目录名映射的真实标签做逐条比对。这一步做出来,论文的实验结果表就不只有一行的总准确率,还能写出每个水果类别的准召率,评委观感会好很多。

5. 避坑与排查:训练不收敛、内存爆掉、精度虚高

5.1 验证集准确率很高,换一批图预测却拉胯

现象:训练完在验证集上准确率 90% 以上,随手拍一张相似角度、相似光照的水果照片喂进去,预测结果完全不对。

原因:数据划分泄漏。训练集和验证集来自同一批水果的相邻帧或同一次拍摄,只差一点角度和亮度,验证集等于在测训练分布的近邻;或者增强幅度太小,验证图片和训练图片过于接近,模型实际没有学到泛化特征。

解决:严格按拍摄批次或场景划分数据,保证测试集图片和训练集不同源;评估时用独立的 test 目录而不是 val 目录,val 只用来做训练中的早停和调参。如果一份数据拆不出独立测试集,至少把验证集图片挑选的时间点或设备错开。

5.2 训练到一半 CUDA 内存爆掉

现象:训练跑到第几个 epoch,终端直接报ResourceExhaustedError,或者对应进程被系统杀掉,GPU 显存占用拉满。

原因:没有做 resize 的把原图直接喂进网络,原图尺寸可能是 3000×4000 级别的;或者 batch_size 开得太大,比如单卡直接上 128;又或者用np.stack把全部图片一次性读进内存再分批训练。

解决:输入尺寸统一走target_size=(224, 224),batch_size 从 16 或 32 开始,训练中显存峰值如果接近上限就降到 8。用flow_from_directory或tf.data按需读图,不要一次性把所有图片 load 进一个数组。

5.3 MobileNetV2 微调后精度反而下降

现象:VGG16、ResNet50 结果都不错,轮到 MobileNetV2,解冻微调后val_accuracy反而比第一阶段还低,或者全程上不去。

原因:MobileNetV2 是深度可分离卷积结构,还带 BN 层,卷积核容量小于普通模型,对学习率极其敏感。微调时如果保持 1e-4 的学习率,梯度更新幅度相对过大,预训练特征被冲掉了。

解决:MobileNetV2 解冻后把学习率降到 1e-5 甚至 5e-6,只解冻最后 1-2 个 block,不要整个网络一起解冻。另一个容易忽略的检查点:预训练权重的输入尺寸必须和训练一致,MobileNetV2 在 192×192 或 224×224 下都能跑,但如果你换了尺寸而权重还是原来尺寸的,特征图会错配,识别效果就会异常。

5.4 单张图片预测输出概率分布很奇怪

现象:加载best_model.h5预测单张图片,输出的概率接近均匀分布,每个类别都差不多;或者预测结果和肉眼判断完全不符。

原因:预测时的预处理 pipeline 和训练时不一致。最常见的是用cv2.imread读图——OpenCV 读出来是 BGR 通道顺序,直接喂给在 RGB 上训练的模型,颜色特征全反了;还有一种是用 PIL 读图但忘了.convert('RGB'),灰度图被直接送进 3 通道输入。

解决:预测前强制统一预处理,读图后显式转成 RGB,再 resize、归一化。稳妥写法是:

from PIL import Image import numpy as np def preprocess_single_image(img_path, target_size=(224, 224)): img = Image.open(img_path).convert('RGB') # 显式转 RGB img = img.resize(target_size) arr = np.array(img, dtype=np.float32) / 255.0 arr = np.expand_dims(arr, axis=0) # 变成 (1, 224, 224, 3) return arr arr = preprocess_single_image('test_apple.jpg') prob = model.predict(arr)[0]

这段代码先转 RGB、再 resize、再做数值归一化,三个步骤缺一不可。model.predict返回的是形状为(1, num_classes)的概率数组,取[0]后argmax就是预测类别。

5.5 加载模型报 Unknown layer 错误

现象:训练保存了best_model.h5,换一台机器或换一个 Python 环境后load_model直接报Unknown layer: Functional或其它反序列化错误。

原因:训练端和预测端的 Keras/TensorFlow 版本不一致,保存的 H5 文件在旧版本下解析不了函数式 API 构建的模型结构。

解决:要么两端环境统一版本后再load_model;要么存权重而不是存整模型,预测端用同一个build_model函数重建结构,再load_weights导入权重。这套资源同时保留了两种保存方式,我的习惯是始终走权重路线,结构由代码保证,版本迁移更稳。

6. 进阶用法:把这套流程复用到自己的分类数据上

毕设做完水果识别,下一步十有八九是换一套自己的数据。这套流程的迁移成本很低,真正要改的只有三个位置:目录结构、类别数、输入尺寸。

目录结构只要保证train和test下面每个类别一个子目录即可,目录名就是标签;num_classes改成自己的类别数,输出层的神经元数必须同步;target_size和你选的预训练模型输入尺寸保持一致,换数据的时候最容易漏的是这里,图片尺寸不匹配会导致流程在不知道哪里报错。

换数据后第一个要做的是导出一份可供论文使用的评估图。训练脚本里通常已有训练曲线绘制,但混淆矩阵值得单独写一段:

import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix import numpy as np # test_generator 必须 shuffle=False test_generator.reset() y_true = test_generator.classes y_pred = np.argmax(model.predict(test_generator), axis=1) cm = confusion_matrix(y_true, y_pred) plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt='d', xticklabels=test_generator.class_indices.keys(), yticklabels=test_generator.class_indices.keys()) plt.xlabel('Predicted') plt.ylabel('True') plt.savefig('confusion_matrix.png', dpi=200, bbox_inches='tight')

这段代码先取验证集的真实标签,再对预测概率做argmax,最后用 seaborn 画热力图。混淆矩阵能一眼看出哪些类别被互相混淆,答辩被问“模型哪类最容易错”时,直接指这张图比背数据有力得多。例如香蕉和芒果都是黄色系,如果两者混淆严重,说明模型主要依赖颜色而不是纹理和形状,这本身就是一个值得写进分析的点。

部署或答辩演示时,还可以把模型导出成 TensorFlow SavedModel 格式,避免 H5 在不同 Keras 版本间的兼容性问题:

model.export('saved_model')

SavedModel 目录下的saved_model.pb是跨版本通用的,加载方式也简单,tf.keras.models.load_model('saved_model')即可。从那以后我每次跑这类迁移学习项目,都强制自己先过一遍数据目录树和预处理 pipeline,确认验证集shuffle=False、确认读图是 RGB、确认增强参数和模型输入尺寸对得上,再点训练。这套流程下来踩过的坑基本都在前面那五条里,希望帮到你。

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

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

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

立即咨询