简介:面向使用YOLO11cls开展农作物病虫害图像分类的开发者与学习者,这份PDF资源提供了真实场景图片数据集的结构说明与获取渠道。包内虽仅1个PDF文件(5.63MB),但它对应1000张已按分类文件夹整理的作物图片,涵盖腰果、木薯、玉米、番茄四大类作物下共22种状态,包含炭疽病、细菌性枯萎病、草地贪夜蛾、叶斑病、花叶病等典型病虫害样本;所有图像按类别文件夹存放,标注质量较高,可直接用于YOLO11cls等分类任务。文档同步介绍了配套的YOLO11cls一键训练脚本及博主训练结果日志,便于快速搭建训练流程、对比实际效果。由于图片数据集体量较大,整体托管在百度网盘,PDF内附有数据集基本介绍与获取凭证,下载后先读说明即可按指引取得完整图片及脚本。目前已有63人浏览学习,适合正在做农作物病虫害检测项目或希望补充通用分类数据集场景的读者。
1. 只有1000张图的病虫害分类项目,凭什么能训出能用的模型
如果你做过农作物病虫害识别,一定体会过那种「找图两小时,标注一整天」的滋味。这份项目标题给的是个非常务实的组合:目标分类——农作物病虫害分类数据集,1000张图,按分类文件夹整理好,外加一个YOLO11cls一键训练脚本。它解决的并不是「从零研发识别算法」这种大问题,而是很多入门者和农业从业者真正卡住的地方:手上只有少量图片、不知道该按什么结构放、不知道怎么把数据喂给模型。1000张图放在深度学习里确实不算多,但只要你用对目录结构、用对预训练权重、把训练参数调到符合小样本的节奏,这套流程足够让你在半天内拿到一个能区分几种常见病虫害的可用模型,而不是模型代码和数据集各躺各的、根本跑不起来。
这套方案适合谁?适合刚走通YOLO系列目标检测、现在想转图像分类的开发者;也适合农学背景、有几千张田间照片但不会写训练流程的研究生。它的核心思路很朴素:数据集按分类文件夹整理,YOLO11cls负责从文件夹直接学类别特征,一键训练脚本把环境确认、数据校验、参数配置、训练启动和结果落盘串成一条命令。后文我会把目录整理、脚本参数、小样本调参和常见翻车点逐项拆开,每一段你都能直接照着做。
2. 把1000张图变成YOLO11cls认识的目录:文件夹整理与数据校验
2.1 ImageNet风格目录是YOLO11cls的默认食物:目录树与data.yaml
YOLO11cls虽然是YOLO系列的新成员,但它的分类模式沿用的是最经典的ImageNet目录组织方式:数据根目录下分train和val两个文件夹,每个类别一个子文件夹,图片直接放在类别文件夹里。模型训练时不是读一个标注文件,而是根据图片落在哪个文件夹来认定它的类别。这意味着「分类文件夹整理」不是可有可无的步骤,而是YOLO11cls能否工作的前提条件。
我建议最终目录结构固定成下面这样,类名用英文小写加下划线,避免中文路径和空格带来的编码问题。
crop_pest_dataset/ ├── data.yaml ├── train/ │ ├── healthy/ │ │ ├── 001.jpg │ │ └── ... │ ├── leaf_spot/ │ │ ├── 001.jpg │ │ └── ... │ ├── powdery_mildew/ │ │ ├── 001.jpg │ │ └── ... │ └── leaf_curl/ │ ├── 001.jpg │ └── ... └── val/ ├── healthy/ ├── leaf_spot/ ├── powdery_mildew/ └── leaf_curl/data.yaml是给YOLO11cls喂数据路径的配置文件,内容很简单,但path字段要特别注意:最好写相对路径,也就是相对你执行训练命令时所在目录的路径。很多人在自己电脑上跑通了,把数据集拷到服务器上就报错Dataset not found,八成就是path写死了绝对路径。
path: crop_pest_dataset train: train val: val names: 0: healthy 1: leaf_spot 2: powdery_mildew 3: leaf_curl这里names的序号其实不需要严格对应文件夹名,模型训练时ultralytics会扫描train目录下的子文件夹,按名称排序自动生成类别索引。但建议手动写names并保持顺序一致,一方面是为了后面推理时能直观对应类别名,另一方面是防止某些环境下系统排序规则不同导致类别顺序漂移。
2.2 从散图到分类文件夹:两个脚本快速落地
大多数人手上的图片不是规规矩矩按类别分好的,而是散在一堆文件夹里,文件名也千奇百怪。那么第一步就是把散图按类别归拢。常见做法是维护一个CSV清单,每一行写图片路径,类别,然后用脚本批量拷贝。
import csv import shutil from pathlib import Path csv_file = "label_list.csv" src_root = Path("./raw_images") dst_root = Path("./crop_pest_dataset") with open(csv_file, encoding="utf-8") as f: for row in csv.reader(f): img_rel, label = row[0].strip(), row[1].strip() src = src_root / img_rel dst = dst_root / "train" / label / src.name dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst)这段脚本做的事情很直接:从CSV里读图片相对路径和类别名,按类别创建目录,再把图片复制进去。我故意用copy2而不是move,因为原始图可能还有别的用途,复制一份训练集更安全。如果你确定原始图不再需要,改成shutil.move能省一半磁盘空间。
类别怎么定、定几个,是一个需要你提前想清楚的问题。1000张图如果分10个类,每个类只有100张,很多类会很难训;如果你先只做二分类「有病/无病」,或者聚焦在你们当地最常见的3~4种病虫害,效果会稳定得多。整理数据时顺手跑一下数量统计,让心里有数:
find crop_pest_dataset/train -type f | cut -d/ -f3 | sort | uniq -c这就是个标准的管道命令:先列出train下所有文件,取第三个字段也就是类别文件夹名,再排序去重统计。看到某个类别只有二三十张图,你就该考虑是不是要把这个类合并到「其他」里,或者去补数据。
2.3 切块扩样暴增训练集:让1000张图变成3000张图
1000张图做分类并不算充裕,尤其是当原始图片是1920×1080甚至更高分辨率的田间照片时。病虫害的区域往往只占画面的一小部分,如果整张图直接缩到imgsz=224,病斑特征会被压缩得几乎看不见。这时候有两个选择:一是先做目标检测把叶片框出来,再对叶片做分类,这是两步方案的思路;二是在数据集整理阶段把大图按网格切块,只保留包含叶片或病斑的小块,变相扩充样本量。
from PIL import Image from pathlib import Path img_path = Path("./raw_images/leaf_spot_001.jpg") img = Image.open(img_path) w, h = img.size crop_size = 512 overlap = 64 out_dir = Path("./cropped") step = crop_size - overlap for i, top in enumerate(range(0, h - crop_size + 1, step)): for j, left in enumerate(range(0, w - crop_size + 1, step)): box = (left, top, left + crop_size, top + crop_size) crop = img.crop(box) crop.save(out_dir / f"leaf_spot_001_{i}_{j}.jpg")切块时我用了一个overlap=64的滑窗步长,这样相邻图块之间有重叠,一张大图能产出几十个训练块。切完之后记得把明显没有叶片内容的纯土块、纯天空块删掉,这个筛选动作如果人工做太累,可以写个简单脚本按颜色方差过滤——方差太低的块基本是纯色背景,直接丢弃。1000张原图按这种方式切到3000~4000个训练块很常见,训练数据的有效信息量会明显提升。
2.4 数据分布检查:在你跑训练之前先让脚本自己体检
图片整理完成不代表可以直接开训。我见过太多人第一次训练失败,是因为数据集里混入了损坏图片、或者某些图其实是PNG带透明通道、有些是灰度图。YOLO11cls底层用的图像库大多能处理这些情况,但一旦某张图解码失败,训练进程会在跑了几个epoch之后突然崩溃,报错信息还很抽象。所以在一键训练脚本里,第一步应该先做数据体检。
from PIL import Image from pathlib import Path def check_images(root_dir): bad_files = [] count = 0 for p in Path(root_dir).rglob("*.jpg"): count += 1 try: with Image.open(p) as im: im.load() except Exception: bad_files.append(str(p)) print(f"共扫描 {count} 张图片,损坏 {len(bad_files)} 张") for b in bad_files: print("损坏:", b) return bad_files这个检查脚本逐个打开图片并真正加载像素数据,能过滤掉「后缀是.jpg但内容已经损坏」的文件。训练之前跑一遍,发现损坏图直接删掉或者重新导出,比训练中途崩溃要省心得多。如果你用的是PNG,把rglob("*.jpg")改成rglob("*.[jp][pn]g")这类通配就行。
3. 用YOLO11cls一键训练脚本跑通第一个模型:环境准备与命令参数
3.1 数据集下载、放置与Path参数:换机器不翻车
标题里的数据集是随项目附带的,放到你自己的工作目录后,第一件事不是写训练脚本,而是确认ultralytics环境装好了。常见的安装方式如下:
pip install ultralytics --index-url https://pypi.org/simple装完以后,可以用一行命令验证YOLO11cls能不能正常加载:
python -c "from ultralytics import YOLO; model = YOLO('yolo11n-cls.pt'); print(model.model.__class__.__name__)"能打印出模型类名说明环境没问题。注意yolo11n-cls.pt是YOLO11分类模式的轻量预训练权重,后缀-cls表示classification,和做目标检测的yolo11n.pt不是同一个权重。第一次运行会自动把权重文件下载到用户目录下的~/.cache/ultralytics里,后续训练就离线了。
之前提到的data.yaml中path字段,强烈建议写成相对路径。因为一键训练脚本很可能在多个机器间移动——今天在笔记本上调参,明天丢到服务器上跑整夜。如果path写死成C:/Users/xxx/crop_pest_dataset或者/home/ubuntu/crop_pest_dataset,换机器必报错。相对路径的值是相对于你执行yolo命令时所在的目录,所以训练脚本和执行命令要放在数据集根目录的上一级,保持固定相对关系。
3.2 一键训练脚本本体:先跑通再谈优化
很多所谓「一键训练脚本」其实就是把官网命令行抄了一遍。那不是脚本,那叫命令记录。真正的一键脚本至少要做三件事:检查数据集是否完整、自动推导关键路径、启动训练并保存产物。下面这个脚本是我常用的一种结构:
#!/usr/bin/env python # train_cls.py import argparse from pathlib import Path from ultralytics import YOLO def main(): parser = argparse.ArgumentParser() parser.add_argument("--data", type=str, default="crop_pest_dataset/data.yaml") parser.add_argument("--model", type=str, default="yolo11s-cls.pt") parser.add_argument("--epochs", type=int, default=50) parser.add_argument("--imgsz", type=int, default=224) parser.add_argument("--batch", type=int, default=32) parser.add_argument("--lr0", type=float, default=0.001) parser.add_argument("--device", type=str, default="0") parser.add_argument("--patience", type=int, default=15) args = parser.parse_args() data_yaml = Path(args.data) if not data_yaml.exists(): raise FileNotFoundError(f"数据集配置文件不存在: {data_yaml.resolve()}") model = YOLO(args.model) model.train( data=str(data_yaml), epochs=args.epochs, imgsz=args.imgsz, batch=args.batch, lr0=args.lr0, device=args.device, patience=args.patience, project="runs/classify", name=f"crop_pest_{args.imgsz}_{args.epochs}ep", ) if __name__ == "__main__": main()然后在命令行里一行启动:
python train_cls.py --data crop_pest_dataset/data.yaml --epochs 50 --batch 32这段代码的逻辑并不复杂,但有几个设计考量。第一,model.train的project和name参数把训练产物统一放到了runs/classify/下,不会散落在当前目录里,后面查看best.pt、混淆矩阵图都方便。第二,把patience暴露成参数而不是写死在代码里,因为早停轮数在小样本和大样本上的最佳值差异很大,1000张图的数据集建议patience=15,loss容易波动,给足耐心。第三,YOLO(args.model)加载的是预训练权重,如果你传"yolo11s-cls.yaml"这种结构文件,就变成了从零训练,在小数据集上效果会打折。
3.3 脚本逻辑说明与5个必调参数
上面的脚本核心只有一处调用:model.train(...),但ultralytics的train方法内部帮你处理了学习率调度、数据加载、验证集评估、早停、权重保存这一整套流程。你不需要自己写训练循环,但需要知道每个参数控制什么。
下面这5个参数是1000张图这个规模下最值得调的:
| 参数 | 默认值 | 小样本建议 | 调参理由 |
|---|---|---|---|
lr0 | 0.01 | 0.001 | 数据量少时梯度噪声大,偏大的学习率容易反复震荡,减半再试是常规操作 |
batch | 16 | 32左右 | 显存允许的情况下适当加大batch,能压低BN层的统计噪声 |
imgsz | 224 | 224或256 | 分类任务不需要为了检测任务把图放大到640,256在小目标细节和显存之间更平衡 |
freeze | 0 | 8~10 | 冻结前若干层主干参数,让模型只微调高层特征,强约束适合小数据 |
patience | 100 | 15 | 小数据集过拟合来得快,早停太晚,val/top1_acc掉到谷底才停就太迟了 |
其中freeze参数的坑比较隐蔽:它只在加载预训练权重时生效,如果你给model传的是yaml结构文件而不是.pt权重文件,freeze会被静默忽略。另外,freeze=10的意思是冻结网络前10层,对YOLO11s-cls来说大约冻结了主干网络前三分之二的卷积层,剩下的层继续更新。这个小技巧能让1000张图的训练稳定性明显提升,但也别冻得太多,否则新类别的特征完全学不出来。
4. 小样本调参不玄学:损失、学习率、冻结与早停的配合
4.1 从预训练权重迁移学习,不要从零开始
1000张图从零训练一个ResNet或者YOLO11分类头,结果基本可以预料:训练集准确率跳得飞快,验证集一路躺平,这是典型的欠拟合加过拟合并存。根本原因是数据量不足以让网络从头学到「叶片纹理」这种通用视觉特征。常见做法是加载在ImageNet上预训练好的权重,把前几层当作固定的特征提取器,只微调后面几层去适配病虫害分类任务。
具体到YOLO11cls,加载预训练权重的方法就是YOLO("yolo11s-cls.pt")。这个权重内部包含ImageNet分类任务学到的通用特征,比如边缘、纹理、颜色分布。你要做的不是重新发明这些特征,而是让模型知道「叶斑」和「健康叶片」在特征空间里的分界在哪里。用freeze参数冻结前几个stage,相当于告诉模型:通用特征你学得很好了,别再动了,帮我专心把最后几层分类层调好。实际操作中freeze=8和freeze=10差异不大,但freeze=0和freeze=10在小样本上的收敛速度差异肉眼可见。
4.2 学习率、batch与epochs三者怎么联动
YOLO11cls的train方法内部自带学习率warmup和余弦退火调度,理论上不需要你手动调学习率策略,但初始学习率lr0的选择仍然直接影响小样本训练成败。batch越小,每个step的梯度越不稳定,学习率就要相应调低;反过来,如果显存充足把batch从32提到64,学习率可以稍微放大一点点。
一个我常用的参考比值是lr0 * batch / 64。也就是说,如果你在batch=64时用lr0=0.001能稳定收敛,那么改成batch=32时,lr0=0.0005是一个合理的起跳点。1000张图片按batch=32算,每个epoch才30个step左右,整个训练过程非常短。50个epoch看起来很多,但实际训练时间可能只有几分钟到十几分钟,这取决于你用CPU还是GPU。
epochs的选择在小样本场景下比较纠结。训练集小意味着模型很容易在10个epoch内就把训练集背下来,但你真正关心的是验证集准确率。我的建议是epochs设一个看起来偏大的值比如80,然后靠patience=15让训练自动早停,最后拿best.pt而不是last.pt。这样你不需要手工反复试「到底多少epoch合适」,训练到验证集指标不再提升时自己就停了。
4.3 用早停和时间戳续训保护你的训练过程
早停机制是ultralytics内置的:每训练完一个epoch就在验证集上评估一次,如果patience个epoch内最优指标没有刷新,训练提前终止。对小数据集这几乎是最重要的护身符,因为过拟合通常在训练中期突然出现,没有早停的话最后保存的last.pt很可能已经烂掉。
训练过程中途中断是常态。可能是服务器被重启,也可能你手动停掉去调参。中断后不需要重新开始,一行命令续训:
python train_cls.py --data crop_pest_dataset/data.yaml --model runs/classify/crop_pest_224_80ep/weights/last.pt续训时把model参数从预训练权重换成上次训练的last.pt,脚本其余逻辑不变。ultralytics会检测到这个权重文件的训练状态,自动读取之前保存的epoch、优化器状态和学习率调度位置。要注意的是,续训之后脚本里project和name最好改成新的目录名,避免把旧的结果文件覆盖掉。我习惯在name里加时间戳:
import time run_name = f"crop_pest_{time.strftime('%m%d_%H%M')}"这样每次续训或者调参后的结果都不会互相覆盖,横向对比best.pt时也有依据。
4.4 类别不平衡时,权重和指标都要改
1000张图如果平均分配到4个类,每个类250张,那是理想情况。现实往往是「健康叶片」占了600张,「稻瘟病」只有100张。类别不平衡在分类任务里最直接的影响是:模型倾向于把不确定的样本全部判成大类,因为这种偷懒策略的总体准确率并不低。但你要的显然不是这种模型。
ultralytics分类任务没有像目标检测那么完善的类别权重参数,常见做法是给类别设定sample weight,或者更实用一点:在数据层面给少样本类别做复制扩增。比如稻瘟病只有100张,可以通过水平翻转、轻度旋转、颜色抖动把它扩到300张。PIL和OpenCV都能做,但要注意扩增只作用于训练集,验证集保持原图。
from PIL import Image, ImageEnhance src = Image.open("train/rice_blast/012.jpg") dst = src.transpose(Image.FLIP_LEFT_RIGHT) dst.save("train/rice_blast/012_flip.jpg") enhancer = ImageEnhance.Color(src) dst = enhancer.enhance(1.3) dst.save("train/rice_blast/012_color.jpg")类别不平衡还要影响你的评估方式。只看top1_acc是不够的,必须看每类的precision、recall和F1。ultralytics在训练结束后会输出混淆矩阵图,如果看到少样本类别的召回率特别低,就说明模型在这个类上学到的特征不足,扩增数据或者增加该类样本是最直接的改进方向。
5. 1000张图训练翻车实录:5条避坑笔记
5.1 训练acc 99%但换张照片直接错:模型只认了背景
这是小样本分类项目里最经典的现象。某个类别的训练图片全部在水泥地上拍摄,另一类全部在泥土地上拍摄,模型根本不需要学叶片特征,直接看背景颜色就能分类。训练准确率接近100%,验证集也是高得离谱,但你把模型拿到田间对新拍的叶子一测,立刻现出原形。
原因在于模型「抄了近道」:背景特征比病斑特征更明显,计算loss时更容易降低误差。解决这个问题的思路是提前掐断近道。第一种办法是在数据整理时,把叶片区域尽量裁剪出来,减少背景面积。第二种是加入随机背景替换或随机擦除的数据增强,人为破坏背景与类别的强相关性。ultralytics里可以设置hsv_h、hsv_s、hsv_v等颜色增强参数,也可以打开mixup让模型看到混合背景的样本。
5.2 train和val「串图」,指标虚高而不自知
有些人是从视频里抽帧做数据集的,相邻帧几乎一模一样。如果随机划分train和val时没有考虑图片来源,很可能同一段视频的相邻帧分别跑到了训练集和验证集。验证集里全是「见过的画面」,指标自然好看,但模型没有任何泛化能力。
实践中的解决办法是:按来源分组划分,而不是按单张图片随机划分。比如你有10段不同地块的视频,每个视频的帧放同一个集合里去划分。如果是不同人拍摄的独立照片,也要先按拍摄批次分组再划分。理想划分比例是训练集占80%~85%,验证集占15%~20%,并且确认验证集的每个类别都有足够数量,避免某类只有两三张验证图导致评估结果剧烈波动。
5.3 训练到一半显存炸了,从头再来
1000张图做分类,按道理对显存要求不高,但YOLO11cls默认的深度训练模式下,如果你把imgsz调到640以上、batch也拉得很高,6GB显存的卡照样能秒炸。最常见的情况是用户看网上的目标检测教程把imgsz=640抄过来,分类任务根本用不到这么大的输入。分类模型的输入一般224就够了,你是在训练模型识别「叶子上有什么病」,不是在找「叶子的边界框坐标」。
解决手段有梯度累积,但ultralytics的分类模式并不直接暴露accumulate参数;更实用的做法是把batch减小、imgsz恢复到224。如果显存还是不够,确认一下是不是开了大量线程加载图片导致CPU内存不足,workers=0有时候比workers=8在小数据集上跑得更稳定,因为省掉了进程间通信的开销。
5.4 精度指标全绿,单个类别却烂到底
只看平均精度会掩盖很多问题。你的模型可能有四个类别,其中三个类别识别得很好,但最需要关注的那个病害类别识别率只有不到五成。平均准确率看起来还有八十多分,实际用起来却不敢依赖。
检查办法是训练结束后直接看混淆矩阵,哪个类被误判成了哪个类一目了然。出现这种情况,先看该类别的训练样本数是不是远少于其他类;再看该类别的图片是否拍摄角度、光线条件和其他类差异过大,导致模型把「光照条件」当成了类别特征。解决路径通常是扩充该类样本、增加针对性数据增强、或者干脆降低你对该类别的上线预期,改为在推理时加入额外的置信度阈值过滤。
5.5 resume之后loss暴涨,反而白训了
续训last.pt时出现loss比上一次训练结束时高出很多,通常不是优化器状态丢了,而是你的训练配置变了。比如上次用imgsz=224,续训时不小心改成imgsz=256,输入尺寸变化会导致特征分布漂移;或者你换了batch大小,学习率没跟着调,优化器重新适应新batch时产生剧烈波动。
遇到这种情况我的习惯做法是:如果上次训练的最后的best.pt已经足够好,直接加载best.pt在新的配置下从零开始继续微调,而不是用last.pt续训。best.pt保存的是早停前验证集指标最优的权重,它的泛化能力比最后一个epoch的状态可靠得多。续训场景里,last.pt并不总是最佳选择。
6. 训完不是终点:用混淆矩阵、单图推理和ONNX导出做模型体检
训练结束后的产物在runs/classify/目录下,重点关注weights/best.pt、confusion_matrix.png和results.png。其中confusion_matrix.png是你判断模型是否可用的第一份证据:对角线上的颜色越深越好,如果某个列对应的色块明显发亮,说明有大量其他类别的样本被误判到了这一类。
from ultralytics import YOLO from PIL import Image import numpy as np model = YOLO("runs/classify/crop_pest_224_50ep/weights/best.pt") img = np.array(Image.open("test_images/wheat_leaf_spot.jpg")) results = model.predict(img, imgsz=224)[0] probs = results.probs.data.cpu().numpy() top5 = probs.argsort()[::-1][:5] print("Top5 索引:", top5) print("Top5 置信度:", probs[top5]) names = model.names print("预测类别:", names[int(top5[0])], f"置信度: {probs[top5[0]]:.4f}")这段推理代码把best.pt加载进来,对一张新图输出Top5预测。实际部署时如果你的分类项目已训练完成,建议把权重导出成ONNX格式,推理速度和部署便利性都更好:
yolo export model=runs/classify/crop_pest_224_50ep/weights/best.pt format=onnx imgsz=224导出成ONNX之后,可以脱离ultralytics环境,用ONNX Runtime在任意平台加载推理。最后再说一个体检技巧:把验证集里预测错误的所有图片单独列出来检查一遍,看它们是「模型判断错了」还是「样本本身标注错了」。有时候1000张图手工整理,难免有几张类别放错。把这些错误图片抽出来手工看一轮,纠正标注,效果往往比盲目调一次参数提升更多。这是我跑小样本分类时最实用的习惯。
图像分类是个反馈极快的领域,1000张图从整理到训练出结果,半天完全够用。你不需要一上来就追求SOTA精度,先跑通流程、拿到一套能用的脚本和数据组织规范,后续补数据、换模型结构都是顺水推舟的事。希望你从这套流程里建立自己的数据整理习惯和调参手感,再遇到更大规模的数据集时,也能心中有数。
本文还有配套的精品资源,点击获取