• 正文
  • 相关推荐
申请入驻 产业图谱

深度学习实战-基于ResNet50的葡萄叶图像分类识别模型

07/14 14:19
391
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

 

1.项目背景

在现代葡萄产业的精准化管理中,品种的快速且准确识别是确保果园分级采收、提升酿酒品质以及保护种质资源的核心环节。传统的品种鉴定高度依赖农业专家的经验,不仅效率受限,且在面对 Ak、Dimnit 或 Nazli 等形态极为接近的葡萄品种时,叶缘锯齿的微小差异、叶脉的分布走向以及裂片的深浅极易造成视觉误判。随着智慧农业的深入发展,如何利用计算机视觉技术替代繁琐的人工鉴定,并在不同光照及生长环境下实现稳健的品种识别,已成为数字化果园建设中亟待解决的实战课题。

本项目针对葡萄叶片形态的多样性与复杂性,构建了一套基于 ResNet50 残差网络的深度学习识别方案。通过引入“迁移学习”策略,模型能够借用预训练架构在通用视觉领域的感知本能,并结合针对性的局部微调(Fine-tuning),精准捕捉葡萄叶片特有的生物学性状。实验过程不仅涵盖了从自照明系统采集的高质量影像预处理,还深入探讨了通过自定义监控回调与动态学习率调节来优化模型收敛轨迹的技巧。

本实战不仅展示了深度学习在 5 类典型葡萄品种识别中的优异性能,更通过多维度的分类报告解析了模型在相似特征间的判别边界,为开发便携式葡萄品种智能鉴定设备提供了可落地的算法参考。

2.数据集介绍

本实验数据集来源于Kaggle,葡萄叶图像数据集,包括Ak、Ala Idris、Büzgülü、Dimnit 和 Nazli 葡萄藤叶子图片。使用特殊的自照明系统拍摄了5个品种共500张葡萄叶图像。

3.技术工具

Python版本:3.9

代码编辑器:jupyter notebook

4.实验过程

4.1导入数据

在开启深度学习流水线之前,首要任务是构建一个稳健的数据索引环境。我们首先集成了数值计算核心库 NumPy 与深度学习框架 TensorFlow,并引入了用于数据探索性分析的 Pandas 和 Seaborn。针对葡萄叶片数据集的存储结构,我们通过 Python 的文件系统接口对目标路径进行扫描。在初步的目录检索中,我们排除了非图像的说明文档,从而精准锁定五个核心品种的存储文件夹。通过这种自动化的路径解析,我们不仅确认了数据集的类别完整性,也为后续将原始图像转化为张量流奠定了逻辑基础,确保每一类品种都能在预处理阶段被准确地打上语义标签。

import numpy as np import os import tensorflow as tf import matplotlib.pyplot as plt import seaborn as sns import pandas as pdfrom sklearn.metrics import classification_report# 定义数据集根路径DATA_DIR = '../input/grapevine-leaves-image-dataset/Grapevine_Leaves_Image_Dataset'# 获取目录下所有子文件夹(即品种分类)folder_elements = os.listdir(DATA_DIR)# 移除与图像无关的引用说明文件folder_elements.remove('Grapevine_Leaves_Image_Dataset_Citation_Request.txt')# 输出并核验最终参与训练的类别名称print(f' - Classes name : {folder_elements}')

4.2数据可视化

为了全面覆盖五个葡萄品种的视觉特性,我们构建了一套随机抽样与矩阵排布的展示系统。通过 choice_random_images_for_each_classe 函数,我们从每个品种的文件夹中随机提取 4 张具有代表性的原始影像,并利用 Matplotlib 搭建起一个 5 x 4 的对比画布。这种排布方式允许我们横向观察同类品种内部的形态一致性,纵向对比不同品种间的显著差异。通过 tf.keras.utils.load_img 加载的图像保留了叶片的细微纹理,这对于评估模型在处理诸如 Ak(阿克)、Bg(博兹加)等品种时,是否能有效识别其叶缘锯齿密度及叶面粗糙度至关重要。

# --- 1. 定义随机路径选择逻辑 ---def choice_random_images_for_each_classe(list_dir_data, nbr_random_images=4):     random_dir_class_images = []    # 遍历每个品种的目录    for c in list_dir_data:         class_images = os.listdir(c)        # 从该品种中随机挑选指定数量的图片文件名        random_class_images = np.random.choice(class_images, nbr_random_images)        # 拼接成完整的物理路径        random_dir_class_images.append([os.path.join(c, img) for img in random_class_images])      # 重新整理数组维度,适配后续的循环显示    list_dir_images = np.array(random_dir_class_images).reshape(len(list_dir_data), nbr_random_images)    return list_dir_images    # --- 2. 批量展示随机样本 ---def display_random_images(list_dir_data, list_classes_name, n_classes=2, nbr_random_images=4):     # 初始化子图画布    fig, ax = plt.subplots(ncols=nbr_random_images, nrows=n_classes)    fig.set_size_inches(16, 12) # 调整画布高度以容纳 5 个类别    fig.suptitle('nRandom images for each classen', c='gray', fontsize=20)        # 获取随机抽取的路径矩阵    random_dir_class_images = choice_random_images_for_each_classe(list_dir_data)    # 嵌套循环:外层控制品种,内层控制样本数量    for i, cn in enumerate(list_classes_name):         for j, images_dir in enumerate(random_dir_class_images[i]):             # 实时加载并渲染图像            img = tf.keras.utils.load_img(images_dir)            ax[i, j].imshow(img, aspect='equal')            ax[i, j].set_title(f'{cn} image {j+1}')            ax[i, j].axis('off') # 移除坐标轴,突出叶片主体                plt.tight_layout()    plt.show()# 执行可视化展示(针对 5 个品种,每类显示 4 张)display_random_images(dir_classes_list, folder_elements, n_classes=5, nbr_random_images=4)

4.3特征工程

本环节我们通过封装 split_data 函数,将葡萄叶片数据集严谨地切分为训练集、验证集与测试集。我们采用了 tf.keras.utils.image_dataset_from_directory 接口,将图像统一调整为 224 x 224 的标准分辨率,并以 Categorical 模式处理 5 类品种标签。为了榨干硬件性能,我们引入了 cache() 内存缓存技术与 prefetch(tf.data.AUTOTUNE) 预取机制,这使得 GPU 在进行当前批次计算时,CPU 能同步准备下一组数据,极大地减少了 I/O 阻塞。通过这种流式处理,我们确保了模型在 70% 数据的驱动下进行学习,同时在剩余的 15% 验证集上进行参数微调,最终由独立的 15% 测试集给出客观评价。

# --- 1. 定义数据三分法与性能优化函数 ---def split_data(dir_data, image_size, label_model='binary', batch_size=32, percentage_val_data=0.15, percentage_test_data=0.15):    """    将原始目录数据划分为训练、验证与测试集,并开启流水线优化    """    total_val_test_split = percentage_val_data + percentage_test_data    if total_val_test_split >= 1.0:        raise ValueError("验证集与测试集占比之和必须小于 1.0")    # 提取训练集主体    train_data = tf.keras.utils.image_dataset_from_directory(        directory=dir_data,        label_mode=label_model,        color_mode='rgb',        batch_size=batch_size,        image_size=image_size,        shuffle=True,        seed=101,        validation_split=total_val_test_split,        subset='training',    )    # 提取临时集合(后续再切分为验证与测试)    temp_data = tf.keras.utils.image_dataset_from_directory(        directory=dir_data,        label_mode=label_model,        color_mode='rgb',        batch_size=batch_size,        image_size=image_size,        shuffle=True,        seed=101,        validation_split=total_val_test_split,        subset='validation',    )    # 计算批次数量并执行逻辑切分    total_temp_batches = tf.data.experimental.cardinality(temp_data).numpy()    validation_batches = int(total_temp_batches * (percentage_val_data / total_val_test_split))        validation_data = temp_data.take(validation_batches)    test_data = temp_data.skip(validation_batches)    # --- 数据流水线性能炼金术 ---    # cache: 缓存数据至内存;shuffle: 打乱顺序防止偏差;prefetch: 异步预取加速计算    train_data = train_data.cache().shuffle(1000).prefetch(buffer_size=tf.data.AUTOTUNE)    validation_data = validation_data.cache().prefetch(buffer_size=tf.data.AUTOTUNE)    test_data = test_data.cache().prefetch(buffer_size=tf.data.AUTOTUNE)    return train_data, validation_data, test_data# 执行 224x224 分辨率下的五分类数据划分,设定 20% 为非训练数据(测试与验证各占 10% 左右)training_data, validation_data, testing_data = split_data(DATA_DIR, (224, 224), 'categorical', 32, 0.2)

针对葡萄叶片在实际拍摄中可能出现的倾斜、远近不一或光照过强等问题,我们构建了一个基于 tf.keras.Sequential 的增强层。这套系统集成了水平/垂直翻转、20% 比例的随机缩放、旋转以及对比度调整。通过可视化增强前后的样本对比可以看出,同一张叶片被赋予了多样的“生命力”:它可能在画面中发生偏移,也可能呈现出不同的色彩对比。这种动态增强技术本质上是在不增加物理采集成本的前提下,人为制造了数倍的特征扰动,强迫模型去学习叶片最本质的脉络结构而非死记硬背像素位置,从而显著提升了分类器在复杂多变果园环境下的稳健性。

# --- 2. 构建数据增强序列 ---def data_augmentation():     # 将增强算子封装为可调用的模型层    data_aug = tf.keras.Sequential([        tf.keras.layers.RandomFlip("horizontal_and_vertical"), # 模拟多角度叶片分布        tf.keras.layers.RandomZoom(0.2, fill_mode='nearest'),   # 模拟拍摄距离变化        tf.keras.layers.RandomRotation(0.2),                   # 模拟自然摆放角度        tf.keras.layers.RandomContrast(0.2)                    # 模拟不同天气下的光照强度    ])    return data_augdata_augmentor = data_augmentation()# --- 3. 增强效果可视化验证 ---# 选取样本批次中的一张图,连续施加 10 次随机增强变换sample_image = tf.keras.utils.array_to_img(example_batch_images[2])images = [sample_image]for i in range(5):     # 对单张图像执行动态增量变换    image_aug = data_augmentor(tf.expand_dims(sample_image, axis=0))    image_aug = tf.keras.utils.array_to_img(tf.squeeze(image_aug))    images.append(image_aug)# 绘制原始图与增强效果的对比阵列fig, axs = plt.subplots(1, 6, figsize=(16, 7))for ax, image, title in zip(axs, images, ['Original', 'Aug 1', 'Aug 2', 'Aug 3', 'Aug 4', 'Aug 5']):     ax.imshow(image)    ax.set_title(title)    ax.axis('off')plt.show()

4.4构建模型

本阶段我们构建了一个高度模块化的深度学习架构。首先,通过 features_extractor_resNet50 函数加载预训练的 ResNet50 权重,并采取了“阶梯式冻结”方案:我们冻结了前 125 层的基础特征提取算子,确保模型底层对边缘和色块的敏感度不被破坏,同时释放后续的高级卷积层(直至 conv4_block3_out)参与训练,以适配葡萄叶片特有的品种特征。在分类头(Classifier Head)的设计上,我们采用了由 1024、512 到 256 个神经元组成的递减式全连接网络,并高频嵌入了 BatchNormalization 层以稳定由于微调带来的参数波动。最后的 Dropout(0.3) 层则作为正则化屏障,确保 5 类品种的预测概率分布既精准又具备良好的泛化性能。

# --- 1. 定义 ResNet50 特征提取器(含微调逻辑) ---def features_extractor_resNet50(input_images_size, name_last_layer, fine_tune_at=150):    # 载入 ImageNet 预训练权重,不包含原始千分类顶层    pre_trained_resNet50 = tf.keras.applications.resnet.ResNet50(        input_shape=input_images_size,         include_top=False,         weights='imagenet')    # 默认开启可训练模式,后续通过循环局部锁定    pre_trained_resNet50.trainable = True    # 冻结前 fine_tune_at 层,保护基础通用特征    for layer in pre_trained_resNet50.layers[:fine_tune_at]:        layer.trainable = False    # 截取指定卷积层的输出作为特征图    last_layer = pre_trained_resNet50.get_layer(name_last_layer)    last_output = last_layer.output    return pre_trained_resNet50, last_output# --- 2. 构建自定义深度分类头 ---def classifier(input):    # 全局平均池化配合平坦化,大幅降低参数冗余    x = tf.keras.layers.GlobalAveragePooling2D()(input)    x = tf.keras.layers.Flatten()(x)        # 逐层递减的全连接层:整合高阶语义特征    x = tf.keras.layers.Dense(1024, activation='relu')(x)    x = tf.keras.layers.BatchNormalization()(x)        x = tf.keras.layers.Dense(512, activation='relu')(x)    x = tf.keras.layers.BatchNormalization()(x)        x = tf.keras.layers.Dense(256, activation='relu')(x)    x = tf.keras.layers.BatchNormalization()(x)        # 引入 30% 比例的失活层,防止模型产生训练依赖    x = tf.keras.layers.Dropout(0.3)(x)        # 输出层:对应 5 个葡萄品种的分类概率    x = tf.keras.layers.Dense(5, activation='softmax')(x)      return x# --- 3. 封装最终端到端模型 ---def final_model(input_images_size, name_last_layer, fine_tune_at=150):    inputs = tf.keras.Input(shape=input_images_size)    # 集成之前定义的数据增强模块,实现实时训练增强    augmented_inputs = data_augmentation()(inputs)    pre_trained_model, output = features_extractor_resNet50(input_images_size, name_last_layer, fine_tune_at)    # 提取特征流:注意在微调时 training 设置为 False,保持 BN 层的均值方差稳定    features = pre_trained_model(augmented_inputs, training=False)    # 挂载分类头    x = classifier(features)    model = tf.keras.Model(inputs, x)    # 配置优化算法:使用较小的学习率(0.0001)以配合微调过程    model.compile(        optimizer=tf.keras.optimizers.Adam(            learning_rate=0.0001,            beta_1=0.9,            beta_2=0.999,            epsilon=1e-08        ),        loss=tf.keras.losses.CategoricalCrossentropy(),        metrics=['accuracy']    )    return model# 实例化模型:输入尺寸 224x224,截取 conv4 块输出,微调后 75 层左右INPUT_IMAGE_SIZE = (224,224,3)NAME_LAST_LAYER = 'conv4_block3_out'model = final_model(INPUT_IMAGE_SIZE, NAME_LAST_LAYER, 125)# 输出模型拓扑结构model.summary()

通过 model.summary() 的反馈可以观察到,模型在 final_model 中巧妙地将数据增强层直接嵌套在输入端,这意味着增强操作完全在计算图中完成,具有极高的并行效率。我们选择截断在 conv4_block3_out 层而非网络最末端,是因为 ResNet 后期的特征图尺度过小,对于葡萄叶片这种依靠细微边缘锯齿来判别的任务,保留稍大尺寸的特征图有助于捕捉更丰富的空间细节。此时的模型已经整装待发,准备在接下来的拟合过程中,将预训练的通用视觉经验转化为对葡萄品种的精准洞察。

4.5训练模型

本环节的训练策略核心在于“稳健增益”。我们首先构建了一个名为 EarlyStoppingCallback_90 的自定义监听器,它在每轮迭代结束时自动复核模型的双向精度——一旦训练集与验证集的准确率双双突破 90%,系统将果断触发保护性停机,锁定当前最优权重。与此同时,为了应对训练后期的梯度平原问题,我们部署了 ReduceLROnPlateau 机制:当发现验证集损失值在连续 3 轮内未能进一步下降时,系统会自动将学习率压缩至原先的 10%。这种降速策略能让优化器在损失函数的波谷处进行更细微的探索,从而精准定位 5 类葡萄品种之间的最窄分类边界。

# --- 1. 定义自定义“双达标”早期停止回调 ---class EarlyStoppingCallback_90(tf.keras.callbacks.Callback):    """    当训练准确率与验证准确率同时达到 90% 时自动终止训练    """    def on_epoch_end(self, epoch, logs={}):        # 获取当前轮次的监控指标        if logs['accuracy'] >= 0.90 and logs['val_accuracy'] >= 0.90:            self.model.stop_training = True            print(f"n[预判终止] 第 {epoch+1} 轮:训练与验证精度均已达标(>=90%),停止迭代以防止过拟合!")# --- 2. 配置动态学习率衰减策略 ---# 监控验证集损失,若连续 3 轮无改善则降低学习率reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(    monitor='val_loss',     factor=0.1,         # 学习率缩减倍数    patience=3,         # 容忍轮次    min_lr=1e-10        # 学习率下限)# --- 3. 启动模型拟合程序 ---EPOCHS = 50hist = model.fit(    training_data,    epochs=EPOCHS,    # 集成自定义逻辑与标准优化回调    callbacks=[EarlyStoppingCallback_90(), reduce_lr],    validation_data=validation_data)

在 50 轮的最大预期迭代中,这种“双保险”机制显著提升了训练质量。由于葡萄叶片的品种识别极易受到实验光照背景的干扰,单纯观察训练精度(Accuracy)是具有欺骗性的;唯有当验证精度同步跟进,才意味着 ResNet50 的残差模块真正消化了叶缘轮廓与叶脉布局的视觉逻辑。随着学习率的适时下调,我们可以观察到 val_loss 呈现出更加平滑的收敛轨迹。

4.6模型评估

为了客观呈现 ResNet50 在微调过程中的演进状态,我们构建了 performance_avaluation 函数,将训练集与验证集的准确率(Accuracy)与损失值(Loss)进行同步对比。理想的评估图表应当显示出两条曲线稳步上升(或下降)且间隔较小,这标志着模型在学习过程中成功保持了泛化性。通过红色标记的训练曲线与蓝色的验证曲线,我们可以清晰地观察到在触发 ReduceLROnPlateau 后,模型在损失平面上的进一步下潜。这种数字化的轨迹复盘,能够帮助我们确认模型是在“理解特征”还是在“背诵数据”。

# --- 1. 定义性能轨迹可视化函数 ---def performance_avaluation(train_accuracy, train_loss, validation_accuracy, validation_loss, size_fig=(16, 4)):     # 绘制准确率随轮次演进的对比图    plt.figure(figsize=size_fig)    plt.plot(train_accuracy, label='训练准确率', c='red', marker='o')    plt.plot(validation_accuracy, label='验证准确率', marker='o')    plt.xlabel('迭代轮次 (Epoch)')    plt.ylabel('准确率')    plt.title('n模型准确率变化轨迹n', c='red', fontsize=14)    plt.legend()    plt.grid(True, alpha=0.3)    # 绘制损失函数随轮次演进的对比图    plt.figure(figsize=(16, 4))    plt.plot(train_loss, label='训练损失', c='red', marker='o')    plt.plot(validation_loss, label='验证损失', marker='o')    plt.xlabel('迭代轮次 (Epoch)')    plt.ylabel('损失值')    plt.title('n损失函数收敛轨迹n', c='red', fontsize=14)    plt.legend()    plt.grid(True, alpha=0.3)# 提取训练历史数据train_accuracy = hist.history['accuracy']train_loss = hist.history['loss']validation_accuracy = hist.history['val_accuracy']validation_loss = hist.history['val_loss']# 执行可视化绘图performance_avaluation(train_accuracy, train_loss, validation_accuracy, validation_loss)

当模型的训练轨迹趋于平稳后,我们使用从未参与训练的独立测试批次来检验其真正的实战水平。通过 model.predict 获取预测概率分布,并利用 np.argmax 锁定置信度最高的类别,我们得以构建出一份详尽的分类报告(Classification Report)。这份报告涵盖了针对 5 个葡萄品种的 精确率(Precision)召回率(Recall) 以及 F1-Score。对于阿克(Ak)或博兹加(Bg)等特定品种,观察其召回率能反映模型是否存在“漏诊”现象。这种基于真实样本的统计学复盘,为模型从实验室环境走向自动化果园管理提供了最核心的技术背书。

# --- 2. 独立批次预测与多指标性能复盘 ---# 使用测试集样本进行预测(此处 images 与 labels 为预先提取的测试批次)predict = model.predict(images)# 将 One-hot 编码的预测值与真实标签转换为类别索引predict_idx = np.argmax(predict, axis=1)true_labels_idx = np.argmax(labels, axis=1)# 生成并打印按品种分类的详细报告report = classification_report(    true_labels_idx,     predict_idx,     target_names=[i for i in folder_elements])print(report)

5.总结

本实验围绕葡萄品种的自动化识别,利用采集自专业自照明系统的葡萄叶影像数据集,成功构建并验证了基于 ResNet50 的深度学习分类模型。该数据集涵盖了 Ak、Ala Idris、Büzgülü、Dimnit 及 Nazli 五个核心品种,通过迁移学习与局部微调策略,模型在训练集上表现出极强的特征捕获能力,准确率达到 95%,且损失值有效收敛至 0.1559

在独立测试集的评估中,模型展现了科学的判别逻辑,尤其是对 Dimnit 品种实现了 100% 的精准识别,而加权平均 F1 分数达到 0.82,证明了残差网络在处理具有细微形态差异的农学图像时具有显著优势。尽管受限于 500 张原始样本的规模,部分品种在召回率上仍有波动,但整体 81% 的测试准确率已充分论证了计算机视觉技术在智慧果园管理中进行品种鉴定的实战价值。

本研究不仅为葡萄种质资源的数字化保护提供了技术路径,也为未来在复杂自然光照下实现端侧设备的实时监测奠定了坚实的算法基础。

相关推荐