
简介面向计算机相关专业学生与从业者的毕业设计级水果识别系统资源包可应用于课程设计、大作业与毕设参考帮助快速构建完整的图像分类项目。项目采用迁移学习思路在ImageNet预训练的VGG16、ResNet50、MobileNetV2、DenseNet121四种经典模型上进行微调并在水果数据集上完成训练、验证与测试最高准确率达到93.08%训练好的模型可直接对输入图片分类源码经过严格调试可运行。压缩包共277个文件、约17.5MB包括Python训练与推理代码、文档说明、水果数据集、模型权重以及带前端展示页面的HTML、CSS、JavaScript、图片等素材目录结构清晰便于定位代码、数据和模型文件。文档部分对项目背景、环境搭建、运行方法和实验结果进行了说明适合学习者理解迁移学习、模型对比与分类流程目前已有342人学习下载可作为答辩演示与二次开发的基础。1. 把水果识别做成迁移学习项目而不是从零训练CNN水果识别这个题目如果从零训练一个CNN几千张图片跑一天准确率可能还不到80%。我拆这个评审97分的毕设项目时发现作者把重心放在了迁移学习上直接加载ImageNet预训练权重用VGG16、ResNet50、MobileNetV2、DenseNet121四个模型在水果数据集上微调最终最高准确率达到93.08%。这个项目包里除了训练好的模型还有完整Python源码、文档说明和分好类的图片数据集。压缩包里那一堆bootstrap.css、layui.css其实是管理后端的界面样式不影响训练但说明系统带了可视化页面。真正核心的目录是data、models、src和docs。和直接搭CNN相比迁移学习的数据需求更少收敛更快代码结构也更适合毕业设计答辩讲清。2. 项目结构与数据准备解压后先让训练数据按类别排队2.1 解压后的目录结构拿到zip先不要急着跑训练脚本先看目录。常见解压后结构如下fruit_classify/ ├── data/ │ ├── train/ │ │ ├── apple/ │ │ ├── banana/ │ │ └── ... │ ├── val/ │ └── test/ ├── models/ │ └── fruit_model.h5 ├── src/ │ ├── train.py │ ├── predict.py │ └── data_utils.py ├── docs/ │ ├── 开题报告.docx │ └── 毕业论文.docx └── web/ ├── static/ │ ├── bootstrap.css │ ├── layui.css │ └── ... └── index.html这个结构是资源包最常见的组织方式。static里那堆css拿来做Web页面时才会用到。训练入口在src/train.py数据集在data目录已经处理好可以直接喂给模型。如果解压后发现所有图片混在一个文件夹就先按下面的方式整理再开始训练。2.2 训练集/验证集/测试集怎么划分图片分类任务要求每个类别的图片放在一个以类名命名的子目录里目录名就是标签。划分比例常用7:2:1或8:1:1。写一个脚本把原始图片随机分到训练、验证、测试三个目录import os, random, shutil src_dir data/full/apple train_dir data/train/apple val_dir data/val/apple test_dir data/test/apple os.makedirs(train_dir, exist_okTrue) os.makedirs(val_dir, exist_okTrue) os.makedirs(test_dir, exist_okTrue) files [f for f in os.listdir(src_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(files) train_n int(len(files) * 0.8) val_n int(len(files) * 0.1) for f in files[:train_n]: shutil.copy(os.path.join(src_dir, f), os.path.join(train_dir, f)) for f in files[train_n: train_n val_n]: shutil.copy(os.path.join(src_dir, f), os.path.join(val_dir, f)) for f in files[train_n val_n:]: shutil.copy(os.path.join(src_dir, f), os.path.join(test_dir, f))这里用shutil.copy而不是shutil.move保留原始图片避免划分错了没法回退。random.shuffle必须做因为爬虫连续抓取的图片往往在光线、背景上高度相似不随机打乱会让验证集和测试集和训练集分布重叠导致评估结果虚高。划分要按类别做不是把所有图片混合后再全局划分。2.3 用ImageDataGenerator加载图片并做数据增强TensorFlow/Keras下最省事的方式是ImageDataGeneratorfrom tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./255, rotation_range20, width_shift_range0.2, height_shift_range0.2, shear_range0.2, zoom_range0.2, horizontal_flipTrue ) val_datagen ImageDataGenerator(rescale1./255) train_generator train_datagen.flow_from_directory( data/train, target_size(224, 224), batch_size32, class_modecategorical ) val_generator val_datagen.flow_from_directory( data/val, target_size(224, 224), batch_size32, class_modecategorical, shuffleFalse )rescale1./255把像素归一化到[0,1]这是预训练模型的通用要求。旋转、平移、缩放、翻转都是随机数据增强目的是让模型对拍摄角度和位置变化更鲁棒。验证集不做增强只做缩放否则评估结果不能反映真实分布。四个预训练模型VGG16、ResNet50、DenseNet121都接受224×224输入MobileNetV2也推荐224所以统一用224不用改适配代码。2.4 常用参数速查参数设定值作用rescale1./255像素归一化rotation_range20随机旋转±20度width_shift_range0.2水平方向平移20%height_shift_range0.2垂直方向平移20%shear_range0.2错切变换强度zoom_range0.2随机缩放20%horizontal_flipTrue水平翻转如果某些类别图片特别少可以单独给那一类加强增强强度但不要动验证集。一般每类有300到500张图片微调就能有不错的效果只有几十张时优先增强和降低学习率。3. 迁移学习选型四个预训练模型在水果分类上的对比与微调3.1 为什么用预训练模型而不是自己搭网络水果分类属于细粒度图像识别苹果和蛇果、香蕉和芭蕉之间纹理差异很小。从零训练的CNN第一层卷积只能学到边缘和颜色块要堆很深才能区分这些品种。预训练模型在ImageNet上已经看过160万张图片底层特征已经通用比如边缘、纹理、形状。迁移学习保留这些通用特征只训练后面的分类层。这样冻结卷积部分需要的样本量大幅下降训练时间也能缩短到几十分钟到一两个小时。3.2 四个模型的特点对比模型参数量特点项目里的定位VGG16约138M结构简单感受野小推理慢做基线对比曲线稳定ResNet50约25.6M残差结构解决梯度消失ImageNet表现好最终最高准确率93.08%落在它身上MobileNetV2约3.4M深度可分离卷积体积小速度快适合部署到低端设备DenseNet121约8M密集连接特征复用参数量可控用来和ResNet50做对照从项目自带的训练日志看四个模型在水果数据集上都能跑到90%左右。ResNet50微调后测试集最高到了93.08%VGG16慢一些但更稳MobileNetV2准确率略低但推理速度快很多。选型原则毕设答辩通常选ResNet50因为效果好且结构好讲如果做实时Web演示再训练一个MobileNetV2版本放后台。3.3 Keras下ResNet50微调实现from tensorflow.keras.applications import ResNet50 from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout from tensorflow.keras.models import Model from tensorflow.keras.optimizers import Adam num_classes len(train_generator.class_indices) base_model ResNet50(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) base_model.trainable False x base_model.output x GlobalAveragePooling2D()(x) x Dense(256, activationrelu)(x) x Dropout(0.5)(x) x Dense(num_classes, activationsoftmax)(x) model Model(inputsbase_model.input, outputsx) model.compile( optimizerAdam(learning_rate1e-4), losscategorical_crossentropy, metrics[accuracy] ) model.summary()include_topFalse去掉ImageNet的1000分类全连接层只保留卷积主干。GlobalAveragePooling2D把每个特征图压成一个值比直接Flatten参数量小也更适合最后一层卷积。Dropout(0.5)防止分类层过拟合。迁移学习不要用大学习率新加层是随机初始化学习率太大会把预训练特征冲坏所以取1e-4。冻结整个backbone训练几个epoch后可以解冻最后10层做二次微调for layer in base_model.layers[-10:]: layer.trainable True model.compile( optimizerAdam(learning_rate1e-5), losscategorical_crossentropy, metrics[accuracy] )解冻后学习率降到1e-5让模型以微小步长调整高层特征避免破坏已经收敛好的权重大量被覆盖。3.4 训练参数与保存epochs设为30batch_size设为32。训练结束后保存模型model.save(models/fruit_model.h5)也可以保存成TensorFlow SavedModel格式方便后续转TFLitemodel.save(models/fruit_model_savedmodel, save_formattf)SavedModel目录下会有saved_model.pb和variables文件夹部署时很多后端直接读这个目录。3.5 微调时最常见的坑include_topTrue时强行替换分类层输入尺寸和类别都对不上。num_classes写死和class_indices长度不一致训练时直接报错。输入图片尺寸小于224×224预训练模型在卷积池化阶段报维度错误。数据没有归一化模型的收敛速度和最终精度都会明显变差。4. 训练与评估从loss曲线到93.08%准确率的调参路径4.1 训练脚本的关键结构一个完整的train.py除了模型定义还要有数据生成器和回调函数。新版Keras里model.fit(generator)已经兼容生成器输入不需要再用旧的fit_generator。常见写法from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint early_stop EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ) checkpoint ModelCheckpoint( models/best_model.h5, monitorval_accuracy, save_best_onlyTrue ) history model.fit( train_generator, steps_per_epochtrain_generator.samples // train_generator.batch_size, validation_dataval_generator, validation_stepsval_generator.samples // val_generator.batch_size, epochs30, callbacks[early_stop, reduce_lr, checkpoint] )steps_per_epoch等于样本数除以batch如果生成器数据量大不写也行Keras会自动估算。EarlyStopping的patience设为8连续8轮验证loss不下降就停止避免训练末期长时间震荡。ReduceLROnPlateau在loss卡住时把学习率减半从1e-4降到1e-5再到1e-6逐步收敛到更优区域。ModelCheckpoint只保存验证准确率最高的权重防止训练后期过拟合覆盖了最优模型。4.2 怎么判断模型真的收敛训练完看history.history里的loss曲线。正常曲线应该是训练loss单调下降验证loss先降后升这就是过拟合信号。如果训练准确率很高、验证准确率却差很多优先加Dropout、增强数据增强或提前停止。拆这个项目的日志时可以看到前10轮验证准确率涨得很快之后开始小幅波动打开ReduceLROnPlateau后曲线又平滑了一段最终停在93.08%。所以不要盲目加大epochs回调函数才是调参的核心。4.3 评估混淆矩阵和分类报告只报一个准确率不够还要知道哪些水果被模型搞混了。用测试集评估import numpy as np from sklearn.metrics import classification_report, confusion_matrix test_generator val_datagen.flow_from_directory( data/test, target_size(224, 224), batch_size32, class_modecategorical, shuffleFalse ) y_true test_generator.classes y_pred np.argmax(model.predict(test_generator), axis1) print(classification_report(y_true, y_pred, target_nameslist(test_generator.class_indices.keys()))) print(confusion_matrix(y_true, y_pred))shuffleFalse很关键只有测试集顺序不打乱y_pred才能和test_generator.classes一一对应。class_indices保存了类名和数字标签的映射训练时生成一次预测时也要用同一个映射否则输出标签对不上。classification_report里的f1-score能直观反映每个类别的分离度某类f1明显低就去数据集里看是不是图片太少或背景干扰太大。4.4 准确率上不去时的检查清单现象可能原因对策train loss不降学习率太高或数据没归一化降到1e-5检查rescaleval loss震荡学习率反复穿过最优区域开ReduceLROnPlateaupatience设2~3验证集准确率低类别样本不均衡按类别加权采样或加强数据增强测试集远低于验证集数据划分有同源图片重新按类别打乱并过滤相似图片解冻后模型崩了解冻层数太多或学习率太大只解冻最后1~2个残差块学习率保持1e-5正常项目里最典型的坑是测试集图片和验证集图片来自同一次爬取背景几乎一样测试准确率看上去很高换个真实场景立刻掉点。拆这个项目时我特意看了数据划分脚本发现作者在类别内随机分发所以评估结果相对可靠。5. 部署与二次开发用训练好的模型写一个水果分类HTTP接口5.1 加载模型做单张预测训练好之后predict.py承担推理。核心逻辑import numpy as np from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing import image model load_model(models/fruit_model.h5) class_names [apple, banana, orange, pear, grape] # 替换成你的class_indices def predict_fruit(img_path): img image.load_img(img_path, target_size(224, 224)) x image.img_to_array(img) x np.expand_dims(x, axis0) x x / 255.0 pred model.predict(x)[0] idx int(np.argmax(pred)) return class_names[idx], float(pred[idx])np.expand_dims把单张图片变成(1, 224, 224, 3)的批次输入除以255.0必须和训练时一致。推理时不要加随机增强否则输出会波动。class_names的顺序必须和训练时class_indices一致建议在训练结束后把映射存成JSON预测时直接加载避免手写顺序出错。5.2 置信度阈值与未知类别用户上传一张不属于数据集的水果照片时模型也会硬给出一个最高概率的类别。在predict_fruit里加一个阈值判断if pred[idx] 0.75: return unknown, float(pred[idx])阈值取0.7~0.85之间具体值在测试集上画出错分样本的置信度分布再定。调低阈值更多模糊样本会被接受调高阈值系统更保守。5.3 封装Flask接口后端用Flask写一个最简接口from flask import Flask, request, jsonify app Flask(__name__) app.route(/predict, methods[POST]) def handle_predict(): f request.files[image] f.save(/tmp/tmp.jpg) label, confidence predict_fruit(/tmp/tmp.jpg) return jsonify({label: label, confidence: confidence}) if __name__ __main__: app.run(host0.0.0.0, port5000)测试方式pip install flask curl -X POST -F imagetest_apple.jpg http://127.0.0.1:5000/predictFlask接口和项目里的Layui页面配合时前端上传图片后通过fetch请求/predict把返回的JSON渲染到页面表格里。这个部署形态在课程设计答辩里很加分评委可以直接用浏览器看到识别过程。5.4 换新数据集的复用技巧如果要把这套系统用在其他图像分类任务上不需要改模型结构只需要改三件事把data/train、data/val、data/test下的子目录改成新类别名训练时num_classes用len(train_generator.class_indices)动态获取预测时的class_names从保存的class_indices.json加载。新数据集每类只有几十张时加入更强的数据增强并把Dropout从0.5提高到0.6同时优先选MobileNetV2这种小模型不容易过拟合。本文还有配套的精品资源点击获取