深度学习植物叶片识别实战:基于TensorFlow与MobileNetV2构建分类模型

发布时间:2026/10/2 14:46:05
深度学习植物叶片识别实战:基于TensorFlow与MobileNetV2构建分类模型 简介这是一套基于Python与深度学习的植物叶片识别系统源码面向有Python基础、想入门计算机视觉与图像分类的开发者也适合在植物分类研究、高校教学演示及个人CNN实战中直接参考。项目以Deep-Leafsnap为骨架围绕leafsnap叶片图像数据实现从数据读取、图像预处理、模型搭建到训练评估与预测的完整管线。压缩包共13个文件主体是9个Python脚本分别对应resnet/densenet/vgg等模型定义、数据加载、训练、测试和工具函数另含csv格式叶片标注数据、依赖清单及说明文档整包仅327KB结构紧凑且便于快速定位代码模块。目前已有364人学习下载。通过阅读和二次开发读者可掌握叶片图像分类的典型流程理解迁移学习在小型数据集上的应用并能将这套思路迁移到其他植物或图像识别任务中。1. 植物叶片识别项目 Deep-Leafsnap从压缩包到能用的分类模型拿到一包叶片照片想快速知道它们分别属于什么植物这是农业、园艺和植物爱好者最常见的诉求。Deep-Leafsnap 正是一个以 Python 为核心、面向植物叶片识别的开源实现标题里的 master.zip 说明它是以源码压缩包形式分发的完整工程。和那些给一张图就出结果的在线识别站点不同这类项目把采集、训练、评估和预测整个流程都放在你本地好处是数据可控、识别种类可自定义坏处是你得自己把环境搭起来。这套方案适合三类人想给毕设或课程设计找一份可复现代码的学生有固定植物种类需要分类但不想依赖第三方 API 的业务方以及刚入门深度学习、想拿真实数据集练手的人。你要做的不是解压之后立刻运行一个脚本而是先明白它依赖什么框架、数据怎么组织、模型怎么训练再动手把精度做上去。接下来我按自己调通这类叶片识别项目的顺序把关键步骤和那些容易翻车的地方讲清楚。2. 环境准备与依赖安装先把 Python 侧的工具链理清楚2.1 Python 版本和虚拟环境为什么我建议你用 3.8 到 3.10Deep-Leafsnap 这类项目的依赖通常写在 requirements.txt 里涵盖 TensorFlow、Keras、OpenCV、NumPy、Matplotlib 和 scikit-learn。如果你直接拿最新版 Python 3.12 或 3.13 去装大概率会在编译依赖时碰到兼容性问题因为部分图像处理库和深度学习框架的预编译轮子还没有跟上最新 Python 版本。我一般会先用 conda 或 venv 创建一个干净的虚拟环境把 Python 版本锁定在 3.9。创建虚拟环境的原因不只是隔离依赖更是为了给后面安装 TensorFlow 留一条退路。项目里如果同时用到 TensorFlow 2.x 和 OpenCV它们对 NumPy 的版本要求是互相牵制的装错版本经常会在 import 阶段报奇怪的错误比如 OpenCV 提示找不到 libGL.so.1。虚拟环境让你可以快速销毁重建不用动系统 Python。# 创建虚拟环境指定 Python 3.9 conda create -n leafsnap python3.9 -y conda activate leafsnap # 如果你更喜欢原生的 venv # python3.9 -m venv leafsnap_env # source leafsnap_env/bin/activate参数说明conda create 的-n指定环境名python3.9是版本锁定。激活之后所有 pip 安装都落在独立环境里不会污染系统 Python。这一步看似多余但我见过太多人图省事在全局环境里装依赖结果升级某个包之后另一个项目的模型直接跑不起来。2.2 安装依赖用 requirements.txt 还是手动 pip install解压 Deep-Leafsnap-master.zip 之后先别急着运行训练脚本。第一件事是看根目录下有没有 requirements.txt有的话直接一条命令装完。没有看到这个文件也别慌你可以在 README 或脚本头部的 import 语句里推断出依赖清单。# 进入项目目录 cd Deep-Leafsnap-master # 用 requirements 安装如果存在 pip install -r requirements.txt # 如果缺少该文件按最小依赖手动安装 pip install tensorflow2.13.0 opencv-python numpy matplotlib scikit-learn pandas参数说明tensorflow 版本建议选 2.13 或 2.10这两个版本对 Python 3.9 的支持最稳。opencv-python 是图像读取和预处理的主力。pandas 不一定在原始依赖里但后面做结果汇总和 CSV 输出时几乎必用。如果你的机器有 NVIDIA GPU可以把 tensorflow 换成 tensorflow-gpu但要注意 CUDA 和 cuDNN 版本匹配否则会像我在 CUDA 11.8 上遇到的那样检测到 GPU 却用不了。装完后跑一段检查代码确认核心库能否正常 import这一步能过滤掉大半环境问题。2.3 验证环境用一段短代码确认图片读写和模型框架可用依赖装完不验证就直接训练是新手最容易犯的错。一个简单办法是写一个五六行的脚本读取项目里任意一张叶片图片做一次缩放和维度转换再确认 TensorFlow 能创建一个小模型。这样可以提前暴露图片路径中文乱码、图像数组维度错误、libGL 缺失这类问题。import cv2 import numpy as np import tensorflow as tf # 读取一张项目里的样本图路径按实际修改 img cv2.imread(dataset/sample.jpg) print(图像形状:, img.shape) # 正常输出 (高, 宽, 3) # 缩放到模型常见输入尺寸 224x224 img_resized cv2.resize(img, (224, 224)) img_array np.expand_dims(img_resized, axis0) print(输入张量形状:, img_array.shape) # 创建一个极小的卷积网络验证 TensorFlow 计算正常 model tf.keras.Sequential([ tf.keras.layers.Conv2D(16, 3, activationrelu, input_shape(224, 224, 3)), tf.keras.layers.GlobalAveragePooling2D(), tf.keras.layers.Dense(10, activationsoftmax) ]) print(模型参数量:, model.count_params())代码说明cv2.imread 返回的是 BGR 格式的三维数组shape 的第三个维度是通道数 3这代表彩色图片成功读入。cv2.resize 把尺寸统一到 224×224这是许多叶片识别模型默认的输入分辨率。tf.keras.Sequential 里第一个 Conv2D 的 input_shape 必须和图片维度一致最后一个 Dense 的神经元数量对应你要分类的植物种类数这里先用 10 验证流程后面按实际类别数改。运行这段脚本两个输出都正常环境这块就稳了大半。如果 TensorFlow 报了 DLL 加载失败或找不到设备优先检查 Python 位数和 CUDA 驱动如果 OpenCV 报错多半是系统缺共享库在 Ubuntu 上执行 apt install libgl1 就能解决。3. 数据集与预处理叶片识别模型的命根子3.1 数据目录怎么组织从散装图片到 keras 可直接读取的结构叶片识别项目的精度上限其实在数据组织阶段就定了。Deep-Leafsnap 这类工程通常要求数据目录按类别分文件夹每一类植物的所有叶片照片放在同一个子目录里。TensorFlow 的 image_dataset_from_directory 和 torchvision 的 ImageFolder 都直接支持这种结构省去了写数据加载器的功夫。解压项目后你最好先按下面的方式把数据归位。dataset/ train/ apple/ apple_001.jpg apple_002.jpg cherry/ cherry_001.jpg peach/ peach_001.jpg validation/ apple/ apple_010.jpg cherry/ cherry_005.jpg文件结构说明train 和 validation 下分别是类别文件夹类别名就是标签最好用英文小写。训练集每类至少 30 张图片验证集每类 5 到 10 张。如果原始数据是 CSV 格式或一张大图里切出来的叶片你需要先写个脚本把它们拆开按类别存到这个目录结构里。标签数量直接影响模型最后一个 Dense 层的神经元个数所以这一步要和后面训练脚本里的参数保持一致。3.2 数据增强参数让几十张叶片图变成几百张有效样本叶片识别的训练数据往往不多尤其是冷门植物种类可能只有几十张。直接拿这么少的数据训练深网络过拟合几乎是必然的。数据增强就是解决的常用手段通过对原图随机旋转、翻转、缩放、改变亮度让模型认为每次看到的都是新样本。Keras 里用 ImageDataGenerator 或者 tf.keras.layers.RandomFlip 都能实现我推荐后者因为它在图上直接操作且不占额外存储。from tensorflow.keras import layers # 定义训练时的数据增强层 data_augmentation tf.keras.Sequential([ layers.RandomFlip(horizontal), # 水平翻转防止模型记住叶片朝向 layers.RandomRotation(0.2), # 随机旋转最多 20% layers.RandomZoom(0.1), # 随机缩放 10% layers.RandomBrightness(0.15), # 亮度扰动适应光照变化 ]) # 读取训练集 train_ds tf.keras.utils.image_dataset_from_directory( dataset/train, image_size(224, 224), batch_size32, shuffleTrue, ) # 把增强层接到数据集上 train_ds train_ds.map(lambda x, y: (data_augmentation(x), y))参数说明RandomFlip(horizontal)只做水平翻转不做垂直翻转因为叶片正反面区分明显垂直翻转会制造错误样本。RandomRotation(0.2)表示旋转角度范围在 -72 到 72 度设置太小起不到增强作用太大又会让叶片特征变形0.15 到 0.25 是比较稳的经验区间。RandomBrightness的 0.15 表示亮度在 ±15% 内波动。这段代码先定义增强序列再读取数据集最后把增强映射到每个 batch 上。验证集不需要增强直接读原图即可。3.3 划分数据集别让数据泄漏毁了你的验证精度很多人解压项目后不管三七二十一就把所有图片都拿去训练结果验证精度虚高部署到真实场景直接翻车。原因是同一株植物的多张照片太相似训练时见过的叶子在验证时又出现了一次模型相当于开卷考试。数据划分必须保证训练集、验证集、测试集互不包含同一来源的叶片。import os import shutil import random random.seed(42) source_root dataset/all_images # 假设所有图片按类别存放在这里 train_root dataset/train val_root dataset/validation # 为每个类别创建目标目录 for cls in os.listdir(source_root): os.makedirs(f{train_root}/{cls}, exist_okTrue) os.makedirs(f{val_root}/{cls}, exist_okTrue) images os.listdir(f{source_root}/{cls}) random.shuffle(images) # 先打乱再划分避免按文件名排序造成偏差 split_idx int(len(images) * 0.8) # 80% 训练20% 验证 for img in images[:split_idx]: shutil.copy( f{source_root}/{cls}/{img}, f{train_root}/{cls}/{img} ) for img in images[split_idx:]: shutil.copy( f{source_root}/{cls}/{img}, f{val_root}/{cls}/{img} )逻辑说明代码先读源目录里的所有类别对每个类别单独划分这样能保证每个类的训练验证比例一致不会出现某个类别全被分到验证集的情况。random.seed(42)让随机结果可复现方便你对比不同预处理策略的效果。80/20 这个比例对叶片识别这种小样本场景比较合适如果图片总量特别少可以把比例调到 85/15但验证集每类至少保留 5 张。4. 训练一个叶片识别模型核心流程与参数调优4.1 模型选型为什么从预训练卷积网络开始而不是自己设计叶片识别的关键特征是叶脉走向、边缘轮廓和叶片内部的斑纹纹理这些都可以被卷积神经网络有效地捕捉。但如果你从零训练一个大网络叶片数据量通常不够模型很难收敛。一个省力又可靠的做法是迁移学习用 ImageNet 上预训练好的权重作为特征提取器只重新训练最后的分类层。Deep-Leafsnap 这类项目最常见的模型结构就是 MobileNetV2、ResNet50 或 EfficientNet 加一个全连接头。MobileNetV2 对我来说是最稳妥的选择模型体积小、推理速度快精度在叶片识别这种中低分辨率任务上和 ResNet50 差距很小但在 CPU 上跑推理能快出一倍。如果你有 GPU 且数据量过万张可以换用 EfficientNetB3 换来 1% 到 2% 的精度提升。下面的代码用 MobileNetV2 做特征提取并接上适合自己数据集的分类层。import tensorflow as tf from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models # 加载预训练模型不要包含最顶层的分类层 base_model MobileNetV2( input_shape(224, 224, 3), include_topFalse, weightsimagenet ) # 冻结卷积主干参数只训练后面的分类层 base_model.trainable False model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.3), # 随机丢弃部分神经元降低过拟合 layers.Dense(num_classes, activationsoftmax) # num_classes 你自己数据集的类别数 ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losssparse_categorical_crossentropy, metrics[accuracy] )参数说明weightsimagenet加载在 ImageNet 上训好的权重省去从头训练的时间。base_model.trainable False冻结主干前几轮训练只更新分类层等分类层收敛后再解冻主干做微调。Dropout(0.3)是叶片数据量少时对抗过拟合的重要参数值太小没效果值太大会欠拟合。learning_rate0.001是 Adam 优化器在迁移学习阶段的标准选择微调时改成 0.0001。4.2 训练脚本从回调函数到早停策略完整落地训练阶段最影响结果的两个细节是学习率衰减和早停。训练到后期还在用原始学习率会导致损失值震荡随便跑几十个 epoch 又可能白白浪费算力。我在训练脚本里会加入 ReduceLROnPlateau 和 EarlyStopping 两个回调前者在损失不下降时自动降低学习率后者在验证精度连续若干个 epoch 不提升时提前结束训练省时间也防过拟合。from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping # 读取验证集 val_ds tf.keras.utils.image_dataset_from_directory( dataset/validation, image_size(224, 224), batch_size32, shuffleFalse ) # 定义回调 callbacks [ ReduceLROnPlateau( monitorval_loss, factor0.5, # 触发后学习率减半 patience3, # val_loss 连续 3 个 epoch 不降就减学习率 verbose1 ), EarlyStopping( monitorval_accuracy, patience8, # 连续 8 个 epoch 精度不提升就停止 restore_best_weightsTrue # 回到验证精度最高的权重 ) ] history model.fit( train_ds, validation_dataval_ds, epochs50, callbackscallbacks )参数说明monitorval_loss决定依据哪个指标调整学习率对分类任务我更喜欢监控损失而不是精度因为精度变化是离散的在边界点上可能来回跳动。factor0.5表示触发后学习率乘以 0.5这个衰减力度适中。patience的取值影响训练节奏太小导致规则太敏感比如验证损失在第 2 个 epoch 小幅上升就触发提前减学习率有可能打断正常收敛。restore_best_weightsTrue 这个参数很重要如果不开启EarlyStopping 触发时保存的是最后一步的权重而不是精度最高的那个等于白训了几十轮。4.3 评估模型用混淆矩阵找短板而不只看总精度训练完成后大多数人看一眼验证集精度就结束了。总精度在各类别分布不均时很有欺骗性比如你的数据里苹果叶片占 60%桃树叶占 10%即便桃树叶全部识别错误总精度仍然可以维持在一个好看的数值上。叶片识别项目里不同物种的形态可能非常接近比如两种蔷薇科植物的叶片差异仅在于锯齿边缘的疏密这时候混淆矩阵能直观告诉你模型到底在哪些类之间犹豫。import numpy as np from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt # 收集所有验证集的预测结果 y_true [] y_pred [] for images, labels in val_ds: preds model.predict(images) y_pred.extend(np.argmax(preds, axis1)) y_true.extend(labels.numpy()) # 输出每一类的精确率、召回率 class_names val_ds.class_names print(classification_report(y_true, y_pred, target_namesclass_names)) # 绘制混淆矩阵 cm confusion_matrix(y_true, y_pred) plt.figure(figsize(10, 8)) plt.imshow(cm, cmapBlues) plt.colorbar() plt.xticks(range(len(class_names)), class_names, rotation45) plt.yticks(range(len(class_names)), class_names) plt.xlabel(预测类别) plt.ylabel(真实类别) plt.show()代码说明对每个 batch 做 predict得到每个类别的概率分布np.argmax取出概率最大的类别作为预测结果。classification_report 输出的每一类精确率和召回率才是更真实的模型能力画像精确率低意味着模型总把别的叶片认成这一类召回率低意味着这一类叶片总被漏掉。混淆矩阵对角线越亮代表识别越准非对角线上的亮块就是模型容易混淆的类别组合你之后扩充数据时应优先补充这些类别的样本。5. 叶片识别常见问题排查训练不收敛和过拟合的 4 个典型坑5.1 现象一损失函数一直不下降训练精度徘徊在随机水平训练了好几个 epochloss 几乎没动训练集上的精度一直停留在类别数的倒数附近比如 5 类就卡在 0.2 左右。这种情况最直接的原因是学习率设置不合理过大导致梯度在损失曲面边缘来回震荡过小又让更新幅度接近零。这时不要急着改网络结构先看一眼训练日志里 loss 的数值量级。如果第一轮 loss 就异常大比如超过 10通常说明标签有问题确认一下是不是所有图片都被读成了同一个类别。我的排查顺序是先打印一个 batch 的数据形状和标签分布再用极小的学习率如 0.00001 试跑 5 个 epoch能下降就说明问题出在学习率不能下降再查数据。5.2 现象二训练精度达到 95%验证精度却只有 70%这是典型的过拟合表现。叶片数据集量小模型拿训练集里的重复特征投机取巧把叶片背景里的土壤颜色、拍摄环境的灯光当成了分类依据。解决路径有三条增强数据增强的力度把 RandomRotation 从 0.2 提到 0.3加上 RandomContrast 对比度扰动增加 Dropout 的比例到 0.5更有效的是把 MobileNetV2 的部分层解冻做微调让预训练权重适应叶片数据的纹理分布。我通常会在这三个方向上分别试一组实验记录对应的验证精度而不是一次性改三个参数否则哪个起作用了你都不知道。数据增强力度增大后训练精度会下降一些这反而是正常现象。5.3 现象三预测阶段报错提示维度不匹配或输入尺寸不一致训练时一切正常一到跑测试脚本就报维度相关的错误最常见的错误是模型期望输入形状是 (None, 224, 224, 3)但实际传入的是 (None, 224, 224, 4)也就是带 Alpha 通道的 PNG 图片。另一个常见原因是用 cv2.imread 读灰度图得到二维数组模型期望三维输入。解决办法是在预处理阶段统一检查图片通道数。def load_and_preprocess(image_path): img cv2.imread(image_path) if img is None: raise ValueError(f无法读取图片: {image_path}) if len(img.shape) 2: # 灰度图转三通道 img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) if img.shape[2] 4: # RGBA 图去掉透明通道 img cv2.cvtColor(img, cv2.COLOR_RGBA2BGR) img cv2.resize(img, (224, 224)) return img逻辑说明代码依次处理两类特殊图片灰度图只有二维形状通过 cvtColor 复制成三通道带有透明通道的 PNG 图直接把 Alpha 通道去掉只保留 RGB。这个预处理函数可以统一放在预测脚本里调用避免训练和预测阶段因图片格式差异而得到不同的输入分布这一点在真实场景中非常关键因为网络上下载的叶片图片经常混合 jpg 和 png 两种格式。5.4 现象四叶片旋转 90 度后识别结果发生改变有时直接判错叶片识别比一般的物体识别对方向更敏感有些植物的叶形是明显的长椭圆形旋转 90 度之后特征分布完全不同。MobileNetV2 本身不是旋转等变的模型的训练数据如果几乎没有旋转变化模型天然没有学习到旋转不变性。很多人在期望模型能识别各种角度的叶片但训练时却只用了自然方向贴着的图片。之前数据增强里加了 RandomRotation但只对训练集有效。如果你模拟真实场景把测试图片旋转后精度明显下降就说明旋转增强的力度还不够可以单独针对旋转角度做一轮实验把 RandomRotation 增大到 0.4 试试即允许 ±144 度旋转。更极端的方案是让模型检测叶片主轴方向先旋转归一化再预测这个方案实现成本高适合在叶片方向随机且背景复杂的真实部署场景中使用。6. 把模型落到实际使用批量预测和轻量部署的进阶技巧训练出精度满意的模型之后离真正能用还有一步。你现在手里是一个 keras 模型对象但实际需求往往是给一个装满照片的目录自动输出每张图的植物名称和置信度。我习惯写一个批量预测脚本用脚本扫描目录下所有图片逐张预测并把结果写入 CSV。import os import csv import cv2 import numpy as np import tensorflow as tf # 加载训练好的模型 model tf.keras.models.load_model(leafsnap_model.h5) class_names [apple, cherry, peach, tomato, grape] # 读取预测目录下的所有图片 test_dir test_images results [] for file_name in os.listdir(test_dir): if not file_name.lower().endswith((.jpg, .jpeg, .png)): continue image_path os.path.join(test_dir, file_name) img load_and_preprocess(image_path) # 使用上面定义的统一预处理函数 img_array np.expand_dims(img, axis0) / 255.0 # 归一化到 [0,1] probs model.predict(img_array, verbose0)[0] pred_idx int(np.argmax(probs)) confidence float(probs[pred_idx]) results.append([file_name, class_names[pred_idx], round(confidence, 4)]) with open(prediction_results.csv, w, newline) as f: writer csv.writer(f) writer.writerow([file_name, predicted_class, confidence]) writer.writerows(results) print(f预测完成共 {len(results)} 张图片结果已保存到 prediction_results.csv)这段代码的真正常用价值在于对置信度做了保留。识别结果不只是给一个名字而是给出概率值。当 confidence 低于 0.6 时说明模型不太确定这类结果值得人工复核。实际使用中把低于阈值的图片单独拷贝到 unknown 文件夹里只把高置信度的结果直接用于生产可以把误判率压到很低。到我这里为止脚本的阈值我一般设 0.7叶片这种类间相似度高的任务低于 0.7 的预测结果可信度确实不够。如果要把模型集成到网页或移动端TensorFlow 官方提供了 TFLite 转换器把 h5 模型转成轻量的 tflite 文件推理速度比原模型快 3 到 5 倍体积也会压缩到原来的三分之一左右。转换后注意检查输入输出的张量名称不同转换器的 API 版本之间经常因为张量名不匹配报错这类问题用 netron 工具打开模型文件就能看清结构。叶片识别项目的终点不是训练出高精度而是顺畅地在真实图片上流动起来。回头看我做过的几个植物识别项目最大的习惯就是每次训练前先花十分钟把数据分布看明白哪些类别样本少、哪些类别背景差异大都会直接影响识别结果。模型结构反而是最不需要花心思的部分MobileNetV2 加微调已经能覆盖绝大多数场景。数据清理增强和阈值校准才是反复迭代最多的地方这些才是一套叶片识别方案真正值钱的部分。希望帮到你。本文还有配套的精品资源点击获取