☰
图像分类实战:4种庄稼害虫数据集划分与PyTorch训练全流程
2026/10/7 23:05:33 网站建设 项目流程

简介:面向图像分类与害虫识别任务的数据集资源,包含4种庄稼害虫类别的训练集与验证集,分类为蛀虫、健康无虫、螨虫等。数据按文件夹存放,可直接通过ImageFolder读取,无需额外预处理,适合用于目标检测或分类模型的训练与验证。资源包共676个文件,以673张jpg图片为主体,另含1个json分类字典文件、1个可视化py脚本及1张示例png,压缩包大小约53.89MB。其中训练集620张、验证集53张,目录结构清晰,便于划分与加载。可视化脚本可直接运行,随机抽取4张图片展示并保存结果,方便快速检查数据质量。目前已有277人学习使用,对需要现成害虫图像数据做分类或迁移学习的读者来说,是一个可快速上手的实用数据集。

1. 图像分类项目里的 4 种庄稼害虫:训练集验证集分好,项目就成功了一半

做图像分类这几年,最大的教训是:模型跑不动可以改,数据没分好只能重来。拿到一个 4 种庄稼害虫分类数据集,里面训练集、验证集已经拆好,很多人第一反应是直接开训,结果验证集指标漂亮得一塌糊涂,下地一测就翻车。这个数据集的亮点不在算法多新,而在于它把图像分类任务最容易被忽略的一步——训练集与验证集的划分,用真实图像数据帮你提前踩平了。适合三类人:做植保 AI 落地的工程师、拿图像分类练手的学生、以及想验证迁移学习效果的算法新人。下面从数据组织讲起,一路到训练脚本、参数调优和常见坑位。

2. 把 4 种害虫图片整理成可训练的分类目录:结构与标签规范

2.1 ImageNet 目录结构:为什么 train/val 分目录是最省事的组织方式

拿到数据集先看一眼目录树。凡是标注里包含训练集、验证集的分类数据,绝大多数都按 ImageNet 风格组织:顶层是 train 和 val 两个文件夹,每个文件夹下面按类别建子目录,每张图就是一个文件。这种结构最大的好处是 torchvision 的ImageFolder可以直接加载,连自定义 Dataset 都不用写。

dataset_root/ ├── train/ │ ├── rice_borer/ # 水稻螟虫 │ │ ├── img_0001.jpg │ │ └── ... │ ├── corn_armyworm/ # 玉米粘虫 │ ├── cotton_bollworm/ # 棉铃虫 │ └── aphid/ # 蚜虫 └── val/ ├── rice_borer/ ├── corn_armyworm/ ├── cotton_bollworm/ └── aphid/

这里有几个隐性约定:第一,子目录名就是类别名,ImageFolder会按字母序自动生成索引;第二,文件名尽量只用字母、数字和下划线,不要带中文或空格,否则跨平台搬运时容易出问题;第三,train 和 val 下的类别目录必须完全一致,不能出现训练集有 4 类、验证集只有 3 类的情况。

提示:不要把 val 里的图复制进 train。验证集的价值是模拟“没见过的数据”,一旦污染,后面所有指标都失去意义。

2.2 类别名与标签映射:先定 label_map.json,再写索引脚本

目录结构定好后,下一步是建立标签映射表。我一般建议直接用英文目录名做训练标签,用一份 JSON 文件维护中文名和数字索引的对应关系。这样做的好处是模型训练代码里不用出现中文字符串,部署阶段再通过 JSON 转成业务名称。

{ "0": "rice_borer", "1": "corn_armyworm", "2": "cotton_bollworm", "3": "aphid" }

注意,这份 JSON 里的数字索引和ImageFolder自动生成的索引不一定一致。ImageFolder是按子目录名的字母序排序生成class_to_idx,aphid 排最前、corn_armyworm 第二、cotton_bollworm 第三、rice_borer 第四,和我们手写的映射顺序完全不同。所以规范做法是:以ImageFolder.class_to_idx为准,把它的输出导出成新的 JSON,训练和推理都只用这一份。

下面这段脚本可以帮你做两件事:一是生成标签映射,二是扫描图片文件是否能被 PIL 正常打开,把损坏文件提前揪出来。

import os from PIL import Image def build_label_map(root): classes = sorted(os.listdir(root)) label_map = {str(idx): name for idx, name in enumerate(classes)} return label_map, classes def check_images(root): bad = [] for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((".jpg", ".jpeg", ".png")): path = os.path.join(cls_dir, fname) try: Image.open(path).verify() except Exception: bad.append(path) return bad label_map, classes = build_label_map("train") print(label_map) print(classes) print(check_images("train"))

逻辑说明:build_label_map先排序再枚举,保证索引顺序和ImageFolder完全一致;check_images用verify()只读文件头,不加载整张图,扫描几千张图也很快。如果返回的bad列表非空,这些文件需要在训练前剔除或重新导出,否则训练时会在某个 epoch 突然崩溃,报一个看不懂的图像解码错误。

2.3 样本量检查脚本:用一段 Python 数清每一类的图片数量

做分类数据,第一步永远是数样本。4 类害虫数据常见规模是每类训练集两三百张到五百张左右、验证集每类几十张。为了说明后续参数选择,下面用一组示例分布来演示,你的数据集以实际统计为准。

类别代号中文名训练集示例规模验证集示例规模
rice_borer水稻螟虫42080
corn_armyworm玉米粘虫38075
cotton_bollworm棉铃虫41585
aphid蚜虫46090

用一段代码统计实际文件数,比用眼睛看资源管理器可靠得多:

from collections import Counter def count_samples(root): counter = Counter() for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if os.path.isdir(cls_dir): counter[cls] += len([f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))]) return counter print("train:", dict(count_samples("train"))) print("val:", dict(count_samples("val")))

逻辑说明:只统计图片后缀文件,忽略目录里可能存在的Thumbs.db、.DS_Store等系统文件。参数说明:如果某一类训练样本不到 100 张,后面训练时就要靠更强的数据增强和迁移学习来补;如果训练集和验证集的类别比例差异超过 2 倍,说明划分方式可能有问题,需要回到原始采集批次重新看。

3. 训练集与验证集划分:固定随机种子,让每次实验可复现

3.1 为什么按比例随机切分会翻车:同源照片与数据泄漏

很多人会问:数据不是已经分好 train 和 val 了吗,为什么还要关心划分方法?因为昆虫图像数据有一个特殊问题:同一块田、同一天、同一个镜头连拍的照片,背景、光照、拍摄角度几乎一样。如果当初划分时直接把所有图片揉在一起随机切分,那验证集里就会出现和训练集几乎一模一样的“同源照片”。

这种问题叫数据泄漏,它的典型症状是验证集准确率高达 95% 以上,但把模型拿到另一块田、另一个时间段拍的害虫照片上测试,准确率直接掉到 70% 以下。判断方法很简单:把 train 和 val 里所有文件名导出,看有没有相同前缀的文件同时出现在两边。如果一张IMG_0421.jpg在 train,一张IMG_0422.jpg在 val,而且是同一相机同一批次拍的,那就要警惕了。

正确的划分粒度是按“拍摄批次”或“田块”来分,而不是按单张图片分。如果数据集自带文件夹批次信息,最好按批次划分;如果没有,至少要保证文件名前缀在 train 和 val 中不重复。

3.2 用 Python 做一次分层划分:按类别的 15% 划出验证集

如果你的场景是需要自己重新划分一批散图,下面这段分层抽样脚本可以直接用。它保证每个类别都按相同比例切出验证集,避免某个类在验证集中样本过少。

import os import random import shutil random.seed(42) val_ratio = 0.15 def split_by_class(src, train_dir, val_dir): for cls in sorted(os.listdir(src)): cls_path = os.path.join(src, cls) if not os.path.isdir(cls_path): continue imgs = [f for f in os.listdir(cls_path) if f.lower().endswith(".jpg")] random.shuffle(imgs) val_n = int(len(imgs) * val_ratio) os.makedirs(os.path.join(train_dir, cls), exist_ok=True) os.makedirs(os.path.join(val_dir, cls), exist_ok=True) for i, name in enumerate(imgs): if i < val_n: shutil.copy2(os.path.join(cls_path, name), os.path.join(val_dir, cls, name)) else: shutil.copy2(os.path.join(cls_path, name), os.path.join(train_dir, cls, name)) split_by_class("all_images", "train", "val")

逻辑说明:先对每个类别的图片列表做shuffle,再取前val_ratio比例的样本进验证集。这种分层抽样的好处是类别比例在训练集和验证集中保持一致,不会出现蚜虫在训练集占 40%、在验证集只占 10% 的偏差。

参数说明:random.seed(42)是后悔药,固定住随机种子才能保证每次划分结果完全一致,哪怕代码跑一百遍也是同一个验证集。val_ratio=0.15对每类几百张的规模来说是合适的,如果每类只有 50 张,建议提到 0.2。

3.3 验证集比例怎么选:5%、10%、还是 20%

验证集大小直接影响两个东西:指标稳定性和训练数据量。验证集太小,准确率曲线会像心电图一样剧烈波动,一个 batch 的预测结果就能把整体准确率拉高或拉低 3 个百分点;验证集太大,训练数据不够,模型欠拟合。下面是我常用的经验区间:

训练集每类样本量验证集比例说明
小于 100 张20%验证集样本太少方差太大,需要留足
100 到 500 张15%本文这类数据的常见区间,平衡稳定性和训练量
大于 1000 张10%训练数据充足,验证集可以小一些

这里有个关键细节:验证集一旦固定,训练过程中就不要反复往里加数据。我见过有人训练到一半发现验证集准确率不理想,就把几张 train 里的图挪到 val 里再跑一次,这相当于把验试卷答案提前看了,后面所有对比实验全部失去可信度。

3.4 划分后必做的三件事:一致性、重名、比例

划分完成后不要急着训练,先跑三个快速检查。第一,确认 train 和 val 的类别目录一致;第二,确认两边没有相同文件名的图片;第三,确认每类的 train-val 比例没有严重偏离。

find train -name "*.jpg" -printf "%f\n" | sort > train_files.txt find val -name "*.jpg" -printf "%f\n" | sort > val_files.txt comm -12 train_files.txt val_files.txt | head

逻辑说明:第一条命令把所有训练集文件名按字典序输出,第二条做同样操作,第三条comm -12找出两边重复的行。如果第三条有输出,说明存在同名图片同时出现在 train 和 val,这时要检查是同一张图被复制了两份,还是不同批次的图片恰好同名。遇到后者,建议用批量重命名脚本加批次前缀,避免后续推理时混淆。

4. 用 PyTorch 跑通 4 类害虫分类:最小训练脚本与必调参数

4.1 用 ImageFolder 加载数据:归一化参数不能抄错

数据准备好之后,训练脚本最核心的部分就是数据加载。ImageFolder会自动扫描 train 目录下的子文件夹,把每个子文件夹名当作类别标签,省去手写 dataset 的麻烦。关键是 transform 的配置:训练集和验证集必须用不同的策略。

import torch from torchvision import datasets, transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder("train", transform=train_transform) val_ds = datasets.ImageFolder("val", transform=val_transform) train_loader = torch.utils.data.DataLoader( train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = torch.utils.data.DataLoader( val_ds, batch_size=32, shuffle=False, num_workers=4) print(train_ds.classes) print(train_ds.class_to_idx)

逻辑说明:训练集用了RandomResizedCrop和ColorJitter做轻量增强,让模型见过不同尺度和亮度的害虫图像;验证集只做Resize和CenterCrop,不做任何随机变换,保证评估结果稳定。class_to_idx打印出来看一眼,确认和之前生成 label_map 的顺序一致。

参数说明:RandomResizedCrop的scale=(0.8, 1.0)表示裁剪区域占原图的 80% 到 100%,范围别太大,否则会把害虫身体截掉一半。num_workers=4在 Windows 上如果报多进程错误,改成 0 先用单进程跑通,再逐步提高。

4.2 模型选型:4 类小规模数据为什么首选 ResNet18

网上最新的图像分类模型层出不穷,某些大模型在千万级数据上刷榜很猛,但在 4 类害虫、每类几百张的数据规模下,大模型反而容易过拟合,训练时间却翻好几倍。我的建议是:第一版基线直接用 ResNet18 预训练权重。

理由有三个。第一,ResNet18 只有约 1100 万参数,一个普通显卡甚至 CPU 都能在 30 个 epoch 内跑完;第二,PhotoNet 等大规模预训练权重已经学到了通用纹理、边缘和形状特征,迁移到害虫识别只需要微调最后的分类头;第三,它的结构简单,出了问题容易排查。如果 ResNet18 的验证集准确率跑到 90% 以上,满足业务需求,那就不需要换更大的模型;如果差一些,再往 ResNet50 升级。

4.3 最小训练循环:30 行代码跑通全流程

有了数据加载和模型,训练循环本身并不复杂。下面这段代码是一个完整的微调流程,假设类别数是 4。

import torch import torch.nn as nn from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = nn.Linear(model.fc.in_features, 4) model.train() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) criterion = nn.CrossEntropyLoss() for epoch in range(30): total_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() logits = model(images) loss = criterion(logits, labels) loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() if (epoch + 1) % 5 == 0: avg_loss = total_loss / len(train_loader) print(f"epoch {epoch+1} loss {avg_loss:.4f}")

逻辑说明:每一轮先把梯度清零,前向传播拿到logits,算交叉熵损失,反向传播,再更新权重。model.fc被替换成输出 4 类的新全连接层,原来的预训练特征提取层保留。scheduler.step()放 epoch 循环里,每轮调整一次学习率。

参数说明:lr=1e-4是微调场景的起点,因为整个模型都有预训练权重,学习率太大容易把学到的特征冲掉。weight_decay=1e-4给权重加 L2 正则,对小数据能压住一部分过拟合。T_max=30必须和 epoch 总数一致,否则余弦退火曲线会在中途跳变。

4.4 三个必调参数:学习率、batch size、权重衰减

很多人跑完一遍发现 loss 不降或者验证集指标不行,第一反应是换模型,其实先把三个参数调对更重要。

参数推荐值不合适的表现
batch size16 或 32太大显存不够且收敛慢,太小 loss 震荡剧烈
学习率1e-4 微调 / 1e-3 从头训太大 loss 不降或直接爆掉,太小收敛极慢
weight decay1e-4过拟合时验证 acc 停滞、训练 acc 继续涨

有一个经验性的检验方法:训练一段时间后,如果训练 loss 持续下降,验证集准确率却原地踏步,说明欠拟合和过拟合的边界没找对,优先调大 weight decay,其次降低学习率;如果训练 loss 都在震荡,先检查 batch size 是不是太小,再确认数据集路径是不是加载了错误的文件夹。

5. 避坑排查:害虫分类数据集训练时最常见的 5 个翻车现场

5.1 验证集准确率 95%,拿到真实田间照片直接掉到 70%

现象:训练过程一切正常,验证集准确率稳定在 95% 左右,你以为模型已经能干活了,结果换了一批新拍的害虫照片一测,准确率惨不忍睹。

原因:训练集和验证集来自同一拍摄批次,同源照片过多,模型实际记住了背景和光照,而不是害虫本身的纹理特征。这是图像分类算法在农业数据上最常见的翻车现场。

解决:回到原始数据,按拍摄时间或田块重新分组,确保验证集和训练集来自不同批次。如果原数据集已经按 train/val 分好且无法回溯批次信息,就检查文件名前缀,把疑似同一批次的图从训练集或验证集中挑出来。

5.2 预测时蚜虫被认成水稻螟虫,而且错得很有规律

现象:模型训练准确率不低,但推理单张图片时,输出类别总是对不上业务名称,比如蚜虫的图永远被分到水稻螟虫。

原因:训练时ImageFolder按目录名字母序生成索引,aphid 索引是 0,rice_borer 索引是 3;推理脚本里如果用手写的映射表,把 0 对应成了 rice_borer,所有预测结果就会整体错位。

解决:训练完把train_ds.class_to_idx保存下来,推理时直接读取同一份映射。不要手写数字索引,不要用 JSON 里自己定义的顺序。

import json with open("class_to_idx.json", "w", encoding="utf-8") as f: json.dump(train_ds.class_to_idx, f, indent=2) print(train_ds.class_to_idx)

5.3 训练 loss 前 5 个 epoch 完全不下降

现象:训练脚本跑起来,loss 一直维持在某个高位附近,前 5 个 epoch 几乎没动静。

原因:最常见的是学习率太大,损失函数在最优点附近来回跳跃;其次是归一化参数写错,忘了用transforms.Normalize,模型输入分布和预训练权重完全不匹配。

解决:先把学习率降到 1e-5 试跑 3 个 epoch,如果 loss 开始下降,说明原学习率不合适;再确认 transform 里Normalize的 mean/std 用的是 ImageNet 标准值。打印一次训练集的 batch 数据,看像素分布是否在 0 附近,能快速定位问题。

5.4 验证集 loss 和 acc 曲线像心电图,每轮结果完全不同

现象:训练 loss 平滑下降,但验证集准确率每轮都在 70% 到 92% 之间来回跳,完全没法判断模型真实水平。

原因:验证集太小,每类只有二三十张图,一个 batch 里的随机构成就能显著影响整体指标。4 类害虫数据如果验证集每类低于 30 张,这个问题几乎必然出现。

解决:把验证集合并到每类至少 50 张;如果原始数据不够,先用 20% 的验证集比例重新划分,而不是用 5%。验证时关闭shuffle,固定遍历顺序,也能减少一部分指标波动。

5.5 训练集增强过头,验证集也跟着用随机裁剪,指标突然崩掉

现象:训练集用了很强的数据增强,验证集也顺手复制了同一套 transform,结果训练准确率还在爬,验证准确率突然下跌。

原因:验证集不应该有任何随机性。RandomResizedCrop和RandomHorizontalFlip每轮都会让模型看到不同版本的同一张图,验证集指标就变成了“随机抖动的噪声”。

解决:验证集只用Resize、CenterCrop、ToTensor、Normalize四个变换。这是分类任务里最不应该省的一条规则。

提示:训练集的增强强度也不要一开始就拉满。先只用随机裁剪和水平翻转跑一个基线,确认正常后,再逐步加ColorJitter和旋转。

6. 用混淆矩阵验收 4 类害虫模型:从整体准确率到每类召回

6.1 保存模型时把 class_to_idx 一起存,别只存权重

训练结束后的第一件事不是测试,而是把模型状态和标签映射打包保存。只存state_dict的模型在推理阶段很容易被标签顺序坑到。

torch.save({ "state_dict": model.state_dict(), "class_to_idx": train_ds.class_to_idx, }, "pest_classifier.pt")

推理时先加载映射,再加载权重,保证预测输出的类别编号和业务名称严格对应。

6.2 每类单独看 precision 和 recall,而不是只看 acc

4 类害虫中,水稻螟虫和玉米粘虫的幼虫在形态上很像,模型可能整体准确率不错,但这两类互相混淆得厉害。用classification_report一眼就能看清。

from sklearn.metrics import classification_report, confusion_matrix preds, gts = [], [] for images, labels in val_loader: with torch.no_grad(): out = model(images).argmax(dim=1) preds += out.tolist() gts += labels.tolist() print(classification_report(gts, preds, target_names=train_ds.classes)) print(confusion_matrix(gts, preds))

如果发现某一类的 recall 明显低,说明模型对这类害虫的特征学习不足,优先补充不同龄期、不同背景的训练样本,而不是急着换更大的模型。数据层面补一张有代表性的图,往往比调三天模型参数更有效。

6.3 扩展新类别时的做法:保留旧验证集,再做增量微调

4 类模型上线后,业务方可能会要求加入新害虫类别。这时把旧的验证集完整保留一份,在新增类别数据上微调后,用旧验证集重新评估一遍,防止模型为了学新类别而遗忘旧类别的特征。

我习惯在每个项目跑完以后,把训练曲线、混淆矩阵和标签映射三样东西一起存档。下次拿到类似作物分类数据,直接按这套模板先跑基线,再谈调优。这个习惯帮我省下了大量重复排错的时间,希望帮到你。

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

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

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

立即咨询