聊聊深度学习实战-基于Xception的农场害虫图像分类检测模型

整理一篇学习笔记,把看到的一些要点和自己的理解都记下来。

  

🤵‍♂️ 个人主页:@艾派森的个人主页
✍🏻作者简介:Python学习者 🐋 希望大家多多支持,咱们一起进步!😄

目录

1.项目背景

2.数据集介绍

3.技术工具

4.实验过程

4.1导入数据

4.2数据可视化

4.3特征工程

4.4构建模型

4.5训练模型

4.6模型优化

4.7模型评估

4.8模型预测

5.总结

源代码


1.项目背景

        在现代农业的演进过程中,害虫侵扰始终是制约作物产量与品质的核心变量。传统的植保模式高度依赖人工巡检,不仅耗时耗力,且在面对种类繁多、形态各异的昆虫时,非专业人员极易因视觉误差导致误判,进而引发农药滥用或错过最佳防治窗口。随着精准农业概念的兴起,利用计算机视觉技术达成害虫的自动化监测与分类已成为智慧农场建设的必经之路。本项目旨在构建一套基于深度学习的高性能害虫识别系统,通过引入 Xception 这一极限卷积神经网络架构,深度挖掘昆虫在触角、翅脉、色斑及体型等维度的微观形态特征。实验针对 15 种活跃于农业生产环境中的典型昆虫,利用深度可分离卷积技术在保持模型轻量化的同时,极大提升了对复杂田间背景下细微生物特征的提取效率。从环境初始化、分层数据平衡处理,到利用 Hyperband 算法进行全参数空间的自动化搜索与微调,本实战展示了如何将先进的视觉算法转化为具备实战价值的农作物保护工具。这不仅为农民和昆虫学家提供了科学的决策支持,降低了作物受损风险,更为开发可实时预警、辅助靶向施药的智能化植保终端提供了关键的技术支撑。

2.数据集介绍

        本实验数据集来源于Kaggle,包含15种常用于农业环境中的昆虫。该数据集为研究、识别和了解这些潜在有害昆虫的特征提供了宝贵的视觉资源。每种昆虫都由多张高质量图像呈现,展现了它们独特的特征、颜色和图案。

用途和潜在应用:

1. 害虫识别与防治:该数据集为从事害虫防治的农民、昆虫学家和农业研究人员提供了宝贵的资源。图像可以准确识别危险的农作物害虫,从而制定及时有效的害虫防治策略。

2. 作物保护与增产:通过识别和了解这些有害昆虫的形态特征,农民可以采取预防措施,保护作物免受虫害侵袭。该数据集有助于制定高效的虫害防治策略,降低作物受损风险,并提高整体产量。

3. 教育与宣传:该数据集可用于教育领域,帮助学生了解昆虫识别的重要性、有害昆虫对农业的影响以及综合虫害管理措施的重要性。它有助于提高农民、学生和公众对这些昆虫带来的威胁以及可持续农业实践重要性的认识。

4. 机器学习和计算机视觉:该数据集可用于训练和评估计算机视觉模型和机器学习算法。研究人员和数据科学家可以利用该数据集开发用于昆虫识别、分类和虫害早期检测的自动化系统。

5. 研究与分析:该数据集为开展有关危险农作物害虫的行为、分布和生态影响的研究与分析提供了宝贵的资源。它有助于研究各种防治方法的有效性,并促进可持续农业创新解决方案的开发。

关键词:农场昆虫、危险昆虫、害虫管理、作物保护、农业、昆虫学、综合虫害管理、计算机视觉、机器学习、数据集。

3.技术工具

Python版本:3.9

代码编辑器:jupyter notebook

4.实验过程

4.1导入数据

在深入算法核心之前,构建一个稳健的数据管理闭环是实验成功的基石。本阶段咱们不仅集成了 TensorFlow 与 Keras 的核心组件,还引入了 Keras Tuner 用于后续的超参数调优。针对农场害虫这一特殊任务,由于不同种类的害虫受季节与采集难度影响,数据往往呈现出天然的不均衡性。于是,咱们在读取数据的同时,利用 pandas 快速构建了类别的分布矩阵。这一步并非轻松的路径索引,而是通过对 15 类害虫样本量的百分比、占比与排名进行定量分析,提前预判模型可能面临的分类偏向风险。这种“先审视数据,后训练模型”的流程,可以确保我们在进入卷积层之前,就对数据集的广度与深度拥有全局视野。

# --- 1. 环境初始化:导入路径管理、进度监控与统计模块 ---
import os
import random
from glob import glob
from tqdm import tqdm
from collections import Counter
# --- 2. 核心计算与数据预处理模块 ---
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from sklearn.model_selection import train_test_split
from sklearn.utils.class_weight import compute_class_weight # 用于后续处理样本不均衡
# --- 3. 数据可视化:对比 Plotly 与 Matplotlib 的展示效果 ---
import plotly.express as px
import matplotlib.pyplot as plt
# --- 4. 预训练架构选择:重点引入极限卷积网络 Xception ---
from tensorflow.keras.applications import ResNet50V2, ResNet152V2, MobileNetV2, Xception
# --- 5. 网络层构建与正则化组件 ---
from tensorflow.keras import layers
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D
# --- 6. 回调机制与性能评估工具 ---
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from keras_tuner.tuners import Hyperband # 引入自动化调参器
from sklearn.metrics import classification_report, confusion_matrix
# --- 7. 全局配置:锁定随机种子以确保实验可重复性 ---
random_seed = 42
np.random.seed(random_seed)
tf.random.set_seed(random_seed)
# 指定数据集根目录及基础参数
directory_path = "/kaggle/input/dangerous-insects-dataset/farm_insects/"
N_CLASSES = 15          # 害虫类别总数
IMAGE_SIZE = (224,224,3) # 标准化输入尺寸
# --- 8. 类别统计:构建数据集全景分布表 ---
class_names = os.listdir(directory_path)
# 递归计算每个类别下的图片总数
class_sample_size = [len(os.listdir(directory_path + class_name))
                     for class_name in class_names]
# 汇总全局样本量并计算百分比
total_n_images = sum(class_sample_size)
percentage_sample_size = [value / total_n_images * 100 for value in class_sample_size]
# 构建 DataFrame 进行可视化管理与排名
class_distribution_df = pd.DataFrame({
    "Class Names": class_names,
    "Data Size": class_sample_size,
    "Percentage(%)": percentage_sample_size
})
# 根据样本量多寡进行降序排列,识别头部与尾部类别
class_distribution_df["Rank"] = class_distribution_df["Data Size"].rank(
    method="dense", ascending=False)
# 展示类别分布概览,识别数据倾斜状况
class_distribution_df

4.2数据可视化

为了更具象地审视害虫数据的结构,我们采用了基于 Plotly 的交互式柱状图。相比于静态图表,Plotly 允许我们通过悬停查看每个类别的精确占比与排名。在这张分布图中,横轴代表害虫物种,纵轴代表样本规模。通过色阶的区分与数值标注,我们可以迅速锁定那些样本量异常突出的物种(如常用的甲虫或蝗虫)以及数量稀缺的特定农业害虫。这种视觉呈现不仅是为了展示数据,更是为了在进入 Xception 深度卷积层之前,建立一套“不均衡预警”机制,确保我们在后续的训练配置中,可以为弱势类别分配更强的学习权重或数据增强强度。

# --- 1. 利用 Plotly 实现数据洞察 ---
fig = px.bar(
    data_frame=class_distribution_df,
    x="Class Names",
    y="Data Size",
    text="Data Size",
    hover_data=["Percentage(%)", "Rank"],
    color_discrete_sequence=px.colors.qualitative.D3,
    height=600,
)
# --- 2. 轴标签优化与视觉布局调整 ---
fig.update_layout(
    xaxis_title="害虫名称",
    yaxis_title="样本总量 (张)",
    yaxis_tickformat="d", # 强制以整数形式显示纵坐标
    yaxis=dict(showgrid=False), # 隐藏背景网格线,提升视觉洁净度
    showlegend=False,
    title_x=0.5 # 标题居中
)
farm_data.show_random_images(image_paths=train_images, grid=(5,5))
# --- 3. 渲染图表:在 Jupyter 环境或浏览器中展示 ---
fig.show()

4.3特征工程

本阶段我们第一步引入了类别权重计算公式,通过总样本数与各类别样本数的比例关系,为稀有类别赋予更高的损失函数增益系数。在数据集划分上,我们拒绝了轻松的随机切割,而是采用了基于标签分布的分层抽样策略。这意味着无论是在 80% 的训练集还是 20% 的验证测试集中,各类害虫的占比都与原始数据集保持严格的一致性,有效防止了因数据倾斜导致的评估失真。最终,我们通过自定义的 FarmInsects 类,将散落在磁盘中的千万张图像路径转化为结构化的张量矩阵,并同步锁定了训练所需的 class_weights_dict,为后续 Xception 模型的非对称式学习构建了稳固的数据底座。

# --- 1. 计算全局类别权重:赋予稀有类别更高的学习优先级 ---
# 权重 = 总样本数 / (类别总数 * 当前类别样本数)
class_weights = [total_n_images / (N_CLASSES * class_samples)
                 for class_samples in class_sample_size]
# 将计算结果同步至统计看板
class_distribution_df["Class Weights"] = class_weights
# --- 2. 路径搜集与标签对齐 ---
all_image_paths = []
class_labels = []
for class_name in class_names:
    # 利用 glob 模块高效抓取子目录下的所有害虫影像
    paths = glob(directory_path + f"{class_name}/*")
    all_image_paths.extend(paths)
    class_labels.extend([class_name] * len(paths))
# --- 3. 科学划分数据集:采用分层抽样 (Stratify) 确保各子集类别分布一致 ---
# 划分训练集 (80%) 与 临时集 (20%)
train_images, valid_test_images = train_test_split(
    all_image_paths, train_size=0.8, test_size=0.2, stratify=class_labels)
# 从临时集中再次切分验证集与测试集
valid_images, test_images = train_test_split(
    valid_test_images, train_size=0.9, test_size=0.1)
# --- 4. 动态计算训练集权重:基于实际切分后的样本分布 ---
trained_class_labels = [os.path.split(path)[0].split("/")[-1] for path in train_images]
class_distribution = dict(Counter(trained_class_labels))
total_trained_images = len(train_images)
train_class_weights = {class_label: total_trained_images /
                       (N_CLASSES * class_samples) for class_label, class_samples in class_distribution.items()}
# --- 5. 数据加载与张量化:将路径转化为模型可解析的数值矩阵 ---
# 初始化自定义加载类 (需配合 FarmInsects 预定义类使用)
farm_data = FarmInsects(class_names=class_names, n_classes=N_CLASSES, image_size=IMAGE_SIZE)
# 依次加载三部分数据流
trainXs, trainYs = farm_data.load_data(image_paths=train_images, desc="读取训练集图像")
testXs, testYs = farm_data.load_data(image_paths=test_images, desc="读取测试集图像")
validXs, validYs = farm_data.load_data(image_paths=valid_images, desc="读取验证集图像")
# 格式化权重字典,用于后续 model.fit 的 class_weight 参数
class_weights_dict = dict(zip(range(N_CLASSES), class_distribution_df["Class Weights"]))

经过这一轮精密的特征处理,我们已经从混乱的文件目录中提取出了三套高质量的“标准化试卷”。特别值得注意的是 class_weights_dict 的生成,它像是一个调节阀,在模型训练时,如果模型错认了一只样本量极少的害虫,产生的损失值会被成倍放大,从而强制卷积核修正其对该物种的特征记忆。这种针对农业实际环境设计的“非均衡对抗”策略,是 Xception 能够在 15 类复杂害虫中达成高召回率的工程秘诀。

4.4构建模型

为了达成高效的对比实验与模型部署,我们通过 Model 类对主流的深度学习架构进行了封装。该类集成了从 ResNet 到 Xception 的多种经典 Backbone。在初始化时,我们通过 include_top=False 剥离了原有的 ImageNet 分类层,仅保留核心特征提取能力。在 train 方法中,我们构建了一个典型的混合架构:第一步是一个全局平均池化层(GlobalAveragePooling2D)用于降维,随后通过 Dropout(0.4) 引入随机失活机制以对抗过拟合,最终连接两层密集的 ReLU 神经元与一个 Softmax 输出层。这种设计既能利用预训练权重的稳定性,又通过 class_weight 的注入,强制模型关注那些容易被忽略的稀有物种。

# --- 1. 构建多架构统一管理类 ---
class Model:
    # 预载入主流的迁移学习骨架网络
    models = [
        ResNet50V2(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE),
        ResNet152V2(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE),
        MobileNetV2(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE),
        Xception(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE), # 重点推荐:深度可分离卷积架构
    ]
    model_names = ["ResNet50V2", "ResNet152V2", "MobileNetV2", "Xception"]

    def __init__(self, model_name: str):
        """
        初始化:根据名称定位骨架网络并锁定其作为模型底座
        """
        self.model_name = model_name
        self.index = self.model_names.index(model_name)
        self.backbone = self.models[self.index]

    def train(self, trainXs, trainYs, validation_data, epochs=10, batch_size=32, verbose=1):
        """
        训练流程:冻结底座,训练自定义分类头
        """
        # 冻结预训练层,防止破坏已学到的通用视觉特征
        self.backbone.trainable = False
        # 构建完整的网络流水线
        self.model = Sequential([
            self.backbone,               # 预训练特征提取层
            GlobalAveragePooling2D(),    # 空间维度压缩
            Dropout(0.4),                # 强力正则化,防止农业背景干扰导致的过拟合
            Dense(256, activation='relu'), # 非线性特征融合
            Dense(128, activation='relu'),
            Dense(N_CLASSES, activation='softmax') # 15类害虫概率输出
        ])
        # 编译模型:采用经典的 Adam 优化器与交叉熵损失
        self.model.compile(
            loss="categorical_crossentropy",
            optimizer="adam",
            metrics=['accuracy']
        )
        # 执行训练:注入类别权重以平衡不同害虫的贡献度
        self.history = self.model.fit(
            trainXs, trainYs,
            validation_data=validation_data,
            epochs=epochs,
            batch_size=batch_size,
            class_weight=class_weights_dict, # 关键:处理害虫样本不平衡
            steps_per_epoch=len(trainXs)//batch_size,
            callbacks=[EarlyStopping(patience=3, restore_best_weights=True)], # 自动早停保护
            verbose=verbose
        )

    def visualize_training(self):
        """
        绘制训练曲线:直观监控 Loss 与 Accuracy 的演进趋势
        """
        plt.figure(figsize=(15, 5))
        plt.suptitle(f"{self.model_name} 训练全周期监控曲线")
        # 绘制损失曲线
        plt.subplot(1, 2, 1)
        plt.plot(self.history.history['loss'], label="训练损失", color="g")
        plt.plot(self.history.history['val_loss'], label="验证损失", color="b")
        plt.xlabel("迭代轮次 (Epochs)")
        plt.ylabel("Loss (Crossentropy)")
        plt.grid()
        plt.legend()

        # 绘制准确率曲线
        plt.subplot(1, 2, 2)
        plt.plot(self.history.history['accuracy'], label="训练准确率", color="g")
        plt.plot(self.history.history['val_accuracy'], label="验证准确率", color="b")
        plt.xlabel("迭代轮次 (Epochs)")
        plt.ylabel("准确率 (Accuracy)")
        plt.grid()
        plt.legend()
        plt.show()

    def evaluate(self, testXs, testYs, verbose=1, visualize=False):
        """
        性能评估:在未见过的测试集上验证实战能力
        """
        test_loss, test_acc = self.model.evaluate(testXs, testYs, verbose=verbose)
        if visualize:
            # 可视化评估:将测试指标与训练轨迹对比,判定泛化能力
            plt.figure(figsize=(15, 5))
            plt.suptitle(f"{self.model_name} 测试集性能核验")

            plt.subplot(1, 2, 1)
            plt.plot(self.history.history['loss'], label="训练轨迹")
            plt.plot(self.history.history['val_loss'], label="验证轨迹")
            plt.axhline(test_loss, color='black', alpha=0.8, linestyle="--", label="测试集表现")
            plt.ylim([0, 1.5]); plt.grid(); plt.legend()
            plt.subplot(1, 2, 2)
            plt.plot(self.history.history['accuracy'], label="训练轨迹")
            plt.plot(self.history.history['val_accuracy'], label="验证轨迹")
            plt.axhline(test_acc, color='black', alpha=0.8, linestyle="--", label="测试集表现")
            plt.ylim([0.4, 1.0]); plt.grid(); plt.legend()
            plt.show()

4.5训练模型

作为残差网络的基准,ResNet50V2 通过跳跃连接解决了深层网络中的梯度消失问题。我们第一步将其作为对照组进行训练,通过 visualize=True 实时绘制其在验证集与测试集上的表现。ResNet 架构在处理中型数据集时通常表现得非常稳健,它为我们提供了一个可靠的性能基准线,让我们能够直观评估后续更复杂模型所带来的收益是否值得其对应的计算开销。

# --- 1. 初始化并训练 ResNet50V2 基础模型 ---
resnet50_model = Model(model_name="ResNet50V2")
# 启动训练流:verbose=0 保持输出界面简洁,聚焦核心指标
resnet50_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)
# 性能核验:自动对比训练/验证轨迹,并在测试集上画出水平基准线
resnet50_model.evaluate(testXs, testYs, visualize=True, verbose=0)

为了探究更深的模型层次是否能带来更细腻的虫害特征描述,我们引入了层数更多的 ResNet152V2。在农业影像中,害虫与其背景(如叶片、泥土)颜色高度接近,更深的网络理论上能够提取更高阶的非线性特征。然而,我们也需要警惕:过深的模型在样本量有限的情况下,是否会更容易在细微的背景噪声上产生过拟合。

# --- 2. 初始化并训练深度残差网络 ResNet152V2 ---
resnet152_model = Model(model_name="ResNet152V2")
# 观察更深层网络在相同迭代轮次下的收敛速度
resnet152_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)
# 评估其在未见过的测试集上的泛化精度
resnet152_model.evaluate(testXs, testYs, visualize=True, verbose=0)

针对未来可能部署在农户手机 App 或嵌入式巡检机器人上的需求,轻量级的 MobileNetV2 是必测选项。它采用了倒残差结构和线性瓶颈设计,虽然参数量远小于 ResNet 系列,但在迁移学习的加持下,其对害虫轮廓的捕捉能力往往令人惊喜。这一步的对比重点在于评估:在损失极小准确率的前提下,我们能否换取更高的推理速度?

# --- 3. 初始化并训练轻量化架构 MobileNetV2 ---
mobilenetv2_model = Model(model_name="MobileNetV2")
# 针对端侧部署潜力进行模型训练
mobilenetv2_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)
# 验证轻量级模型是否能维持足够的分类灵敏度
mobilenetv2_model.evaluate(testXs, testYs, visualize=True, verbose=0)

最终登场的是本项目的主角——Xception。它通过将 Inception 模块替换为深度可分离卷积,实现了对空间特征和通道特征的解耦学习。在处理害虫复杂的翅膀脉络或节肢结构时,Xception 理论上具有更强的细粒度辨析力。通过观察其训练曲线,我们可以验证其收敛过程是否比 ResNet 更加丝滑,以及在处理 15 类害虫分类时是否展现出了明显的代差优势。

# --- 4. 初始化并训练核心架构 Xception ---
xception_model = Model(model_name="Xception")
# 验证深度可分离卷积在虫害识别任务中的特征提取上限
xception_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)
# 最终性能对冲:确认 Xception 是否为当前数据集下的最优方案
xception_model.evaluate(testXs, testYs, visualize=True, verbose=0)

通过对 ResNet 和 MobileNet 等架构的横向测评,Xception 在测试集上的卓越表现使其脱颖而出,正式成为本项目害虫识别任务的核心骨架。尽管受限于样本规模,模型在捕捉某些微小昆虫特征时仍存在挑战,但 Xception 凭借其特有的深度可分离卷积结构,在泛化性能上展现出了巨大的潜力。为了进一步挖掘该架构在现实农场复杂环境下的实战能力,我们需要打破预训练权重的束缚,通过全参数微调(Fine-tuning)与超参数自动化搜索,精准定制最适合本数据集的网络拓扑与学习策略。

4.6模型优化

本阶段我们引入了 Keras Tuner 中的 Hyperband 搜索算法,对 Xception 模型进行全方位的“基因重组”。不同于此前的冻结训练,我们通过 model_base.trainable = True 释放了整个骨架网络的权重,允许卷积核针对害虫的纹理细节进行二次进化。搜索空间涵盖了关键的学习率、Dropout 丢弃率以及全连接层的深度与神经元密度。Hyperband 算法会像“锦标赛”一样,在有限的算力预算下快速筛选出表现最优的参数组合,从而在抑制过拟合的同时,将模型的识别精度推向实战级的巅峰。

# --- 1. 定义动态模型构建函数:为自动化调优设定搜索空间 ---
def build_model(hp):
    """
    配置 Xception 模型的微调结构,搜索最优的学习率与层级组合
    """
    # 载入 Xception 骨架并解除权重冻结,开启全参数微调模式
    model_base = Xception(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE)
    model_base.trainable = True
    # 构建序列模型
    model = Sequential([
        model_base,
        GlobalAveragePooling2D(),
        # 搜索最佳的 Dropout 比例,增强模型在复杂农场背景下的鲁棒性
        layers.Dropout(hp.Choice("rate", [0.4, 0.5])),
    ])
    # 动态搜索全连接层的层数 (1-3层)
    for _ in range(hp.Choice("n_layers", [1, 2, 3])):
        model.add(
            # 在 128 到 1024 之间寻找最合适的神经元密度
            Dense(hp.Choice("Units", [128, 256, 1024]), activation="relu"),
        )
    # 输出层:匹配 15 类害虫标签
    model.add(Dense(N_CLASSES, activation="softmax"))
    # 编译模型:针对微调阶段,提供三个量级的学习率进行筛选
    model.compile(
        loss="categorical_crossentropy",
        optimizer=keras.optimizers.Adam(learning_rate=hp.Choice('lr', [1e-2, 1e-4, 1e-6])),
        metrics=['accuracy']
    )
    return model
# --- 2. 初始化 Hyperband 调优器 ---
kt = Hyperband(
    build_model,
    max_epochs=10,             # 每组参数的最大尝试轮次
    objective='val_accuracy',  # 以验证集准确率为优化目标
    project_name="XceptionHS", # 定义项目存放路径
    overwrite=True,            # 覆盖旧的搜索记录
    factor=5                   # 每一轮筛选的资源削减因子
)
# --- 3. 启动自动化参数搜索 ---
print("开始执行 Xception 超参数空间搜索...")
kt.search(trainXs, trainYs, validation_data=(validXs, validYs))

4.7模型评估

为了全面量化最佳模型(best_model)的实战性能,我们采用了多维条形图进行直观对冲。在损失函数(Model Loss)方面,我们追求的是三者之间的数值尽可能接近且处于低位,这标志着模型在不同数据分布下表现出了极强的鲁棒性。而在准确率(Model Accuracy)分布上,如果测试集的性能紧咬训练集,则说明 Xception 成功习得了害虫本质的形态特征(如触角比例、甲壳纹理),而非记住了背景中的特定杂草或光影。这种严谨的评估闭环,为模型从实验室走向田间应用提供了最终的数据背书。

# --- 1. 多维度性能采样:获取训练、验证与测试三端的量化指标 ---
# 利用微调后筛选出的最佳模型进行全量评估
train_loss, train_acc = best_model.evaluate(trainXs, trainYs, verbose=0)
valid_loss, valid_acc = best_model.evaluate(validXs, validYs, verbose=0)
test_loss, test_acc = best_model.evaluate(testXs, testYs, verbose=0)
# --- 2. 可视化性能看板:构建损失与准确率的对比矩阵 ---
plt.figure(figsize=(10, 5))
plt.suptitle("微调后 Xception 模型的实战性能评估", fontsize=15)
# 子图1:模型损失值对比
# 较低且平稳的 Loss 是模型在复杂农场环境下抗干扰能力的体现
plt.subplot(1,2,1)
plt.title("Model Loss")
plt.bar(["Train", "Valid", "Test"], [train_loss, valid_loss, test_loss])
plt.grid()
# 子图2:模型准确率对比
# 高一致性的准确率标志着模型具备优秀的泛化性能,未发生严重过拟合
plt.subplot(1,2,2)
plt.title("Model Accuracy")
plt.bar(["Train", "Valid", "Test"], [train_acc, valid_acc, test_acc])
plt.grid()
plt.tight_layout()
plt.show()

4.8模型预测

本环节我们通过 5x5 的矩阵布局,对测试集进行了随机采样展示。在推理过程中,每一张害虫图像都会被转化为符合 Xception 输入规范的张量,通过 best_model.predict 得到各类别概率分布。为了客观评估性能,我们将预测标签(Pred)与真实标签(True)进行并列显示。在这种高强度的随机抽检下,如果模型能够准确区分长相极其相似的甲虫或飞蛾,则说明我们的微调策略成功避开了“局部最优解”,真正赋予了模型在现实农业场景中识别 15 类害虫的“慧眼”。

# --- 1. 配置 5x5 推理矩阵:随机选取 25 个测试样本 ---
n_rows, n_cols = [5, 5]
n_images = n_rows * n_cols
# 从测试集中随机抽取索引,模拟实地抽检流程
image_indexes = random.sample(range(len(testXs)), 25)
# 设定大尺寸画布,确保每张图片的虫体细节与标签清晰可见
plt.figure(figsize=(25, 20))
plt.suptitle("Xception 模型:农场害虫测试集随机预测实战", fontsize=25, y=1.02)
# --- 2. 迭代执行前向传播与可视化渲染 ---
for plot_index, index in enumerate(image_indexes):
    # 提取测试图片及其对应的 One-hot 编码真值
    image = testXs[index]
    true_label = class_names[np.argmax(testYs[index])]
    # 维度扩充:将 (H, W, C) 提升为 (1, H, W, C) 以适配模型批次输入
    # 执行推理:关闭 verbose 日志以提升渲染速度
    prediction = best_model.predict(tf.expand_dims(image, axis=0), verbose=0)[0]
    # 提取概率最大的索引作为预测结果
    predicted_label = class_names[tf.argmax(prediction)]
    # 动态构建子图
    plt.subplot(n_rows, n_cols, plot_index + 1)
    # 展示原始图像
    plt.imshow(image)
    # 设置标题对比预测与真值
    # 如果预测错误,可以在实际应用中通过颜色标记(如红/绿)进行更直观的区分
    plt.title(f"真实类别 (True): {true_label}\nAI 预测 (Pred): {predicted_label}",
             fontsize=12, pad=10)
    # 隐藏坐标轴,聚焦害虫形态特征
    plt.axis('off')
# --- 3. 渲染结果看板 ---
plt.tight_layout()
plt.show()

5.总结

        本实验基于 Kaggle 提供的包含 15 种常用农业害虫的高质量图像数据集,通过深度学习技术成功构建并优化了一个自动化昆虫识别系统。实验核心采用了 Xception 架构,利用其深度可分离卷积的特性,在处理害虫复杂的颜色、图案及形态特征方面展现出了卓越的分类性能。在经历 Hyperband 算法的自动化超参数搜索与全参数微调后,模型克服了原始数据分布不均及样本规模有限的挑战,将验证集准确率从初期的低位大幅提升至 0.7631 的高水准,证明了该方案在识别危险农作物害虫方面的实战潜力。这一研究成果不仅为农民和昆虫学家提供了精准的数字化防治工具,有效辅助制定害虫管理策略以保护作物产量,更在计算机视觉与机器学习领域探索了轻量化架构在农业细分场景下的泛化边界。该模型的成功开发,标志着从传统人工巡检向自动化、智能化虫害监测迈出了关键一步,为未来部署可实时预警、离线运行的智慧农业植保应用奠定了坚实的算法基础。

源代码

# Management Modules
import os
import random
from glob import glob
from tqdm import tqdm
from collections import Counter

# Data loading and transformation
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from sklearn.model_selection import train_test_split
from sklearn.utils.class_weight import compute_class_weight

# Data Visualization
import plotly.express as px
import matplotlib.pyplot as plt

# Pre-trained models
from tensorflow.keras.applications import ResNet50V2, ResNet152V2, MobileNetV2, Xception

# Model Layers
from tensorflow.keras import layers
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D

# Model Checkpoints
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.callbacks import ModelCheckpoint

# Model Hyertunig and Metrices
from keras_tuner.tuners import Hyperband
from sklearn.metrics import classification_report, confusion_matrix
# Set up random seed
random_seed = 42
np.random.seed(random_seed)
tf.random.set_seed(random_seed)

# Specify the root file path
directory_path = "/kaggle/input/dangerous-insects-dataset/farm_insects/"
N_CLASSES = 15
IMAGE_SIZE = (224,224,3)

# Collect the class names
class_names = os.listdir(directory_path)

# Compute the sample size for each class
class_sample_size = [len(os.listdir(directory_path + class_name))
                     for class_name in class_names]

# Calculate the total number of images
total_n_images = sum(class_sample_size)

# Calculate the percentage of sample size for each class
percentage_sample_size = [value / total_n_images *
                          100 for value in class_sample_size]

# Sort the class sample sizes in descending order
sorted_class_sample_size = sorted(class_sample_size, reverse=True)

# Display the class names, data size, percentage, and rank
class_distribution_df = pd.DataFrame({
    "Class Names": class_names,
    "Data Size": class_sample_size,
    "Percentage(%)": percentage_sample_size
})

# Calculate the rank of each class based on data size
class_distribution_df["Rank"] = class_distribution_df["Data Size"].rank(
    method="dense", ascending=False)

# Show the data
class_distribution_df
# Visualize the class distribution
fig = px.bar(
    data_frame=class_distribution_df,
    x="Class Names",
    y="Data Size",
    text="Data Size",
    hover_data=["Percentage(%)", "Rank"],
    title="Class Distribution of Insects in the Dataset",
    color_discrete_sequence=px.colors.qualitative.D3,
    height=600,
)

# Set axis labels and adjust layout
fig.update_layout(
    xaxis_title="Class Names",
    yaxis_title="Data Size",
    yaxis_tickformat="d",
    yaxis=dict(showgrid=False),
    showlegend=False,
)

# Show the plot
fig.show()
# Computer class weights.
class_weights = [total_n_images / (N_CLASSES * class_samples)
                 for class_samples in class_sample_size]

# Add this data into the data frame
class_distribution_df["Class Weights"] = class_weights
# Initialize empty lists to collect image paths and class labels
all_image_paths = []
class_labels = []

# Collect all image paths and corresponding class labels
for class_name in class_names:
    paths = glob(directory_path + f"{class_name}/*")
    all_image_paths.extend(paths)
    class_labels.extend([class_name] * len(paths))

# Perform stratified data split
train_images, valid_test_images = train_test_split(
    all_image_paths, train_size=0.8, test_size=0.2, stratify=class_labels)
valid_images, test_images = train_test_split(
    valid_test_images, train_size=0.9, test_size=0.1)
    # Extract class labels for trained images
trained_class_labels = [os.path.split(path)[0].split("/")[-1] for path in train_images]

# Compute class distribution
class_distribution = dict(Counter(trained_class_labels))

# Compute class weights
total_trained_images = len(train_images)
train_class_weights = {class_label: total_trained_images /
                       (N_CLASSES * class_samples) for class_label, class_samples in class_distribution.items()}
                       # Initialize dataset
farm_data = FarmInsects(class_names=class_names, n_classes=N_CLASSES, image_size=IMAGE_SIZE)
# Load Training Data
trainXs, trainYs = farm_data.load_data(image_paths=train_images, desc="Training Data")
# Load testing Data
testXs, testYs = farm_data.load_data(image_paths=test_images, desc="Testing Data")
# Load validation Data
validXs, validYs = farm_data.load_data(image_paths=valid_images, desc="Validation Data")
class_weights_dict = dict(zip(range(N_CLASSES), class_distribution_df["Class Weights"]))
class Model:
    models = [
        ResNet50V2(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE),
        ResNet152V2(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE),
        MobileNetV2(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE),
        Xception(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE),
    ]

    model_names = [
        "ResNet50V2",
        "ResNet152V2",
        "MobileNetV2",
        "Xception"
    ]

    def __init__(self, model_name: str):
        """
        Initialize the Model class with the specified pre-trained model.

        Args:
            model_name (str): Name of the pre-trained model to be used.
        """
        self.model_name = model_name
        self.index = self.model_names.index(model_name)
        self.backbone = self.models[self.index]

    def train(self, trainXs, trainYs, validation_data, epochs=10, batch_size=32, verbose=1):
        """
        Train the model using the specified training and validation data.

        Args:
            trainXs (numpy.ndarray): Training images.
            trainYs (numpy.ndarray): Training labels.
            validation_data: Validation data as a tuple (valXs, valYs).
            epochs (int): Number of epochs to train the model.
            batch_size (int): Batch size for training.
            verbose (int): Verbosity level for training logs (0, 1, or 2).
        """
        self.backbone.trainable = False

        self.model = Sequential([
            self.backbone,
            GlobalAveragePooling2D(),
            Dropout(0.4),
            Dense(256, activation='relu'),
            Dense(128, activation='relu'),
            Dense(N_CLASSES, activation='softmax')
        ])

        self.model.compile(
            loss="categorical_crossentropy",
            optimizer="adam",
            metrics=['accuracy']
        )

        self.history = self.model.fit(
            trainXs, trainYs,
            validation_data=validation_data,
            epochs=epochs,
            batch_size=batch_size,
            class_weight=class_weights_dict,
            steps_per_epoch=len(trainXs)//batch_size,
            callbacks=[EarlyStopping(patience=3, restore_best_weights=True)],
            verbose=verbose
        )

    def visualize_training(self):
        """
        Visualize the training history.
        """
        plt.figure(figsize=(15, 5))
        plt.suptitle(f"{self.model_name} Training Curve")

        plt.subplot(1, 2, 1)
        plt.plot(self.history.history['loss'], label="Loss", color="g")
        plt.plot(self.history.history['val_loss'], label="Val Loss", color="b")
        plt.xlabel("Epochs")
        plt.ylabel("Loss (crossentropy)")
        plt.grid()
        plt.legend()

        plt.subplot(1, 2, 2)
        plt.plot(self.history.history['accuracy'], label="Accuracy", color="g")
        plt.plot(self.history.history['val_accuracy'], label="Val Accuracy", color="b")
        plt.xlabel("Epochs")
        plt.ylabel("Accuracy")
        plt.grid()
        plt.legend()

        plt.show()

    def evaluate(self, testXs, testYs, verbose=1, visualize=False):

        test_loss, test_acc = self.model.evaluate(testXs, testYs, verbose=verbose)

        if visualize:
            plt.figure(figsize=(15, 5))
            plt.suptitle(f"{self.model_name} Performance Check")
            plt.subplot(1, 2, 1)
            plt.plot(self.history.history['loss'], label="Loss", color="g")
            plt.plot(self.history.history['val_loss'], label="Val Loss", color="b")
            plt.axhline(test_loss, color='black', alpha=0.8, linestyle="--", label="Test Loss")
            plt.xlabel("Epochs")
            plt.ylabel("Loss (crossentropy)")
            plt.grid()
            plt.ylim([0, 1.5])
            plt.legend()

            plt.subplot(1, 2, 2)
            plt.plot(self.history.history['accuracy'], label="Accuracy", color="g")
            plt.plot(self.history.history['val_accuracy'], label="Val Accuracy", color="b")
            plt.axhline(test_acc, color='black', alpha=0.8, linestyle="--", label="Test Accuracy")
            plt.xlabel("Epochs")
            plt.ylabel("Accuracy")
            plt.ylim([0.4, 1.0])
            plt.grid()
            plt.legend()

            plt.show()
        else:
            return test_loss, test_acc
# Initialize the ResNet50V2 model
resnet50_model = Model(model_name="ResNet50V2")

# Train the model
resnet50_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)

# Evaluate model performance on testing data.
resnet50_model.evaluate(testXs, testYs, visualize=True, verbose=0)
# Initialize the ResNet152V2 model
resnet152_model = Model(model_name="ResNet152V2")

# Train the model
resnet152_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)

# Evaluate model performance on testing data.
resnet152_model.evaluate(testXs, testYs, visualize=True, verbose=0)
# Initialize the MobileNetV2 model
mobilenetv2_model = Model(model_name="MobileNetV2")

# Train the model
mobilenetv2_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)

# Evaluate model performance on testing data.
mobilenetv2_model.evaluate(testXs, testYs, visualize=True, verbose=0)
# Initialize the Xception model
xception_model = Model(model_name="Xception")

# Train the model
xception_model.train(
    trainXs, trainYs,
    validation_data=(validXs, validYs),
    verbose=0,
)

# Evaluate model performance on testing data.
xception_model.evaluate(testXs, testYs, visualize=True, verbose=0)
def build_model(hp):

    model_base = Xception(include_top=False, weights="imagenet", input_shape=IMAGE_SIZE)
    model_base.trainable = True

    model = Sequential([
        model_base,
        GlobalAveragePooling2D(),
        Dropout(hp.Choice("rate", [0.4, 0.5])),
    ])

    for _ in range(hp.Choice("n_layers", [1, 2, 3])):
        model.add(
            Dense(hp.Choice("Units", [128, 256, 1024]), activation="relu"),
        )
    model.add(Dense(N_CLASSES, activation="softmax"))

    model.compile(
        loss="categorical_crossentropy",
        optimizer=keras.optimizers.Adam(learning_rate=hp.Choice('lr', [1e-2, 1e-4, 1e-6])),
        metrics=['accuracy']
    )

    return model

kt = Hyperband(
    build_model,
    max_epochs=10,
    objective='val_accuracy',
    project_name="XceptionHS",
    overwrite=True,
    factor=5
)
kt.search(trainXs, trainYs, validation_data=(validXs, validYs))
# Extract the Best Model
best_model = kt.get_best_models()[0]

# The best model architecture
best_model.summary()
# Model evaluation
train_loss, train_acc = best_model.evaluate(trainXs, trainYs, verbose=0)
valid_loss, valid_acc = best_model.evaluate(validXs, validYs, verbose=0)
test_loss, test_acc = best_model.evaluate(testXs, testYs, verbose=0)

# Visualizing Model Performance
plt.figure(figsize=(10,5))

plt.subplot(1,2,1)
plt.title("Model Loss")
plt.bar(["Train", "Valid", "Test"], [train_loss, valid_loss, test_loss])
plt.grid()

plt.subplot(1,2,2)
plt.title("Model Accuracy")
plt.bar(["Train", "Valid", "Test"], [train_acc, valid_acc, test_acc])
plt.grid()

plt.show()
# Define the number of rows and columns for the grid of images
n_rows, n_cols = [5, 5]
n_images = n_rows * n_cols

# Generate random image indexes from the test set
image_indexes = random.sample(range(len(testXs)), 25)

# Create a larger figure for displaying the grid of images
plt.figure(figsize=(25, 20))

# Iterate over the selected image indexes and plot each image with labels
for plot_index, index in enumerate(image_indexes):

    # Get the image and its corresponding true label
    image = testXs[index]
    true_label = class_names[np.argmax(testYs[index])]

    # Make a prediction using the trained model and get the predicted label
    prediction = best_model.predict(tf.expand_dims(image, axis=0), verbose=0)[0]
    predicted_label = class_names[tf.argmax(prediction)]

    # Create a subplot within the grid
    plt.subplot(n_rows, n_cols, plot_index + 1)

    # Display the image
    plt.imshow(image)

    # Set the title to show the true and predicted labels
    plt.title(f"True: {true_label}\nPred: {predicted_label}")

    # Turn off the axis ticks for cleaner presentation
    plt.axis('off')

# Display the grid of images
plt.show()

资料获取,更多粉丝福利,关注下方公众号获取


本次分享就到这里。技术这东西越研究越有意思,后续有新的收获我也会继续更新。

评论 (0)

暂无评论