
简介在智慧农业和作物病害识别场景中水稻叶病虫害的准确分类是农情监测与精准防控的重要基础。面向需要训练图像分类模型的开发者数据集包含5000张真实场景高质量水稻叶片图片按照细菌性叶枯病(bacterial_leaf_blight)、褐斑病(brown_spot)、健康叶片(healthy)、叶瘟病(leaf_blast)、叶鞘腐病(leaf_scald)、窄褐斑病(narrow_brown_spot)、穗颈瘟(neck_blast)、稻飞虱(rice_hispa)、纹枯病(sheath_blight)、钨黄病毒病(tungro)共10个类别进行文件夹整理分类结构清晰文件夹命名即类别标签免去额外标注成本可直接用于YOLO11cls等图像分类算法训练。资源额外提供YOLO11cls一键训练脚本与博主训练结果日志可帮助读者快速复现训练流程并对照分析模型效果。由于数据集体量较大资源以PDF形式发布于百度网盘内含数据集基本情况介绍与获取方式整体仅1个PDF文件约2.46MB。目前已有115人学习适合作为水稻叶病虫害分类项目的数据基础也可作为通用分类数据集场景的补充资料。1. 水稻叶病虫害分类5000张图配YOLO11cls为什么说这是最快能落地的方案做农业视觉的同学大概率经历过这个场景在田里拍了一下午叶片回办公室对着几千张照片分不清稻瘟病和胡麻叶斑病的边界想训练一个分类模型又卡在数据集格式和训练脚本上。这个标题给的正是这条路的最短闭环5000张水稻叶病虫害图片、按类别整理好的分类文件夹、YOLO11cls一键训练脚本。它解决的是“图有了但模型出不来”的典型问题适合农业科研课题组、植保站技术员以及做智慧农业落地项目的工程师。需要注意这里讲的是图像分类不是目标检测模型输出的是“这张叶片属于哪类病”而不是“病斑框在哪里”。2. 数据集先过关5000张图整理成分类文件夹的标准流程2.1 分类文件夹该长什么样从采集目录到ImageNet风格目录训练一个分类模型数据集的目录结构决定了后面所有脚本的写法。YOLO11cls以及所有基于ultralytics仓库的分类任务默认支持两种数据组织方式一种是ImageFolder风格也就是每个类别一个文件夹文件夹名就是类别名另一种是txt文件列表方式每行写一张图片的绝对路径配合一个记录类别顺序的元数据文件。我一般强烈建议用分类文件夹作为原始存储格式原因很实际采集回来的照片往往是按日期、按田块存放的混杂着大量无效图而分类文件夹是人类可读、可复查的。整理完毕后再用脚本生成txt列表这样原始文件夹不动训练时用的划分结果可以随时重新生成。分类文件夹的标准结构长这样rice_leaf_dataset/ ├── blast/ # 稻瘟病 │ ├── blast_0001.jpg │ ├── blast_0002.jpg │ └── ... ├── sheath_blight/ # 纹枯病 ├── bacterial_blight/ # 白叶枯病 ├── brown_spot/ # 胡麻叶斑病 └── healthy/ # 健康叶片每个文件夹对应一个类别内部图片统一命名为“类别英文名_序号.jpg”。这里有两个硬性要求文件夹名和文件名都不能出现中文、空格和特殊符号否则在Linux服务器和Windows之间拷贝时经常出编码问题图片统一转成jpg格式遇到png和bmp混存的情况先用脚本统一转换。很多人在这个环节图省事直接用相机原始文件结果训练脚本里PIL读图报错一大片得不偿失。2.2 水稻叶病虫害的类别命名与样本画像5000张图能支撑多少个类别按我的经验单类最少50张勉强能训单类200张以上才谈得上稳定。如果5000张平铺到5个类每类1000张这个量在分类任务里属于“够用但不算富裕”需要通过数据增强和迁移学习来补。常见的水稻叶病虫害分类数据集通常包含稻瘟病blast、纹枯病sheath blight、白叶枯病bacterial blight、胡麻叶斑病brown spot以及健康叶片healthy这五个类在视觉特征上有明显区分度但病斑形态在早期阶段高度相似——稻瘟病的梭形病斑和胡麻叶斑病的圆形小斑在远拍图里容易混。整理样本时有三个原则需要贯彻。第一每类的图片要覆盖不同生育期、不同光照条件、不同拍摄距离不要全部来自同一块田同一天。第二健康的叶片也要拍足很多分类模型在田测时把枯黄老叶误判为病害就是因为训练集里健康类只有嫩绿叶片模型没见过生理性黄化。第三每张图只保留单张叶片或小范围叶丛如果一张图里有五六片叶子且分属不同状态分类标签就会互相污染。如果原图是大田拍摄的整株照片建议先用工具裁剪成单叶区域再做分类这比直接拿整株图训练可靠得多。2.3 图片完整性检查与相似去重两个必做的前置脚本采集设备不同、传输过程不同数据集里混进损坏文件几乎是必然事件。训练时遇到损坏图片轻则跳过该样本影响精度重则直接中断训练流程。所以在划分训练集之前先跑一遍全量检查。下面的脚本用PIL逐个验证图片能否正确解码同时过滤掉尺寸过小的图片比如小于64x64的基本是缩略图残留import os from pathlib import Path from PIL import Image dataset_root Path(rice_leaf_dataset) broken [] tiny [] count 0 for img_path in dataset_root.rglob(*.jpg): count 1 try: with Image.open(img_path) as im: im.load() w, h im.size if min(w, h) 64: tiny.append(str(img_path)) except Exception as e: broken.append(str(img_path)) print(f扫描图片总数: {count}) print(f损坏图片: {len(broken)}) for p in broken: print( BROKEN:, p) print(f尺寸过小图片: {len(tiny)}) for p in tiny: print( TINY:, p)逻辑说明用rglob(*.jpg)递归扫描所有子目录PIL的load()会真正解码像素数据能捕获“文件存在但数据截断”的假jpgmin(w,h) 64过滤掉无意义的微小图。这两个过滤条件建议保留在脚本里不要删后续每次新增图片重跑一遍即可。参数上最小尺寸阈值可以根据你图片的实际分辨率调整如果采集原图普遍是4000x3000阈值提到128也不会误伤正常样本。去重是很多人忽略的一步。田间拍摄经常连拍同一个病斑会有多张几乎一样的照片这些相似样本如果同时进了训练集和验证集会虚高模型评估指标。常见做法是用感知哈希pHash计算每张图的指纹两两之间汉明距离小于阈值就判定为重复图保留其中一张。5000张图的规模用pHash全量比对大概几分钟值得做。2.4 训练/验证/测试划分固定随机种子比你想的更重要划分比例我习惯用80%训练、10%验证、10%测试测试集只用于最终评估训练过程中完全不碰。划分时要按类别分层抽样保证每个类在三个集合中的比例一致否则某类恰好全进了训练集验证时该类准确率就会被低估或高估。下面这段代码按分层抽样的方式生成train/val/test三个集合的图片路径清单并固定随机种子保证可复现import random from pathlib import Path from collections import defaultdict random.seed(42) dataset_root Path(rice_leaf_dataset) splits {train: [], val: [], test: []} class_names sorted([d.name for d in dataset_root.iterdir() if d.is_dir()]) for cls in class_names: cls_dir dataset_root / cls images list(cls_dir.glob(*.jpg)) random.shuffle(images) n len(images) n_train int(n * 0.8) n_val int(n * 0.1) splits[train] [(str(p), cls) for p in images[:n_train]] splits[val] [(str(p), cls) for p in images[n_train:n_train n_val]] splits[test] [(str(p), cls) for p in images[n_train n_val:]] for split_name, items in splits.items(): print(f{split_name}: {len(items)} 张) cls_counter defaultdict(int) for _, cls in items: cls_counter[cls] 1 print( , dict(cls_counter))逻辑说明按类别目录名排序得到类别列表保证每次运行类别顺序一致对每类图片shuffle后按8:1:1切分random.seed(42)是关键不固定种子的话每次运行划分结果都不同后面做过任何数据清洗都得重新划分实验之间没法对比。参数可以按需调整如果总数偏少把验证集比例降到5%但测试集不要少于每类10张否则评估结果的置信区间太大。3. YOLO11cls 选型与参数分类模型不是拿过来就训3.1 搞清 YOLO11cls 是分类任务而不是检测任务YOLO这个家族名字太响导致很多人看到YOLO11cls就默认它和YOLO11det是同一套东西。实际上cls后缀代表classification head输出的是类别概率分布没有边界框回归分支而det后缀才是目标检测。做水稻叶病虫害分类标签是“整张图属于哪一类”不需要框出每个病斑所以YOLO11cls是正确选择。如果误用检测模型你还得先把每张图的病斑标注成矩形框5000张图的标注成本会直接劝退项目。YOLO11cls在ultralytics仓库里和YOLOv8分类任务的接口基本兼容训练入口、data.yaml写法、评估指标都是一套逻辑。这意味着你之前用YOLOv8-cls踩过的坑、调过的参数迁移过来大部分有效。它的模型结构相比v8在骨干网络上做了优化但对5000张这种量级的数据集结构差异带来的精度提升远不如数据清洗和数据增强带来的提升明显所以不必纠结版本号直接用最新稳定版即可。3.2 模型尺度与输入分辨率5000张图该用哪个型号YOLO11cls提供n、s、m、l、x五个尺度计算量和参数量递增。5000张图、5个类别这个规模完全用不上l和x强行上大模型只会过拟合训练时间还长好几倍。我的建议是优先试yolo11n-cls.pt跑通全流程再说如果验证集准确率在85%左右上不去再换yolo11s-cls.pt对比。n到s的提升在分类任务上通常有2到4个点但训练时间也会翻倍。在只有CPU的机器上n是唯一现实的选择。输入分辨率方面分类任务默认是224x224这个值对叶片病斑识别够用。有人觉得病斑小把分辨率调到448或640这不是不行但显存占用会变为原来的4倍训练速度明显下降而精度提升经常只有1个点以内。除非你的测试集里大量图片是小病斑远距离拍摄否则224是性价比最高的起点。真要提高细粒度识别能力优先做数据增强里的随机裁剪而不是整体抬分辨率。3.3 学习率、批次与超参数小数据集的标配参数YOLO11cls继承了一套默认超参数直接训也不是不行但针对5000张的小数据集做几处调整收敛速度和最终精度都有改善。我的常用配置如下表参数推荐值说明epochs60~1005000张图50轮基本收敛100轮是上限多了必过拟合batch32或16显存不够就降batch别降分辨率imgsz224细粒度需求再考虑320lr00.01~0.005用预训练权重时取0.01从零训练取0.005optimizerAdamWSGD在小数据集上收敛偏慢weight_decay5e-4防过拟合比默认值略高augment默认开启保留随机翻转、旋转、色彩抖动值得特别说明的是lr0。很多人习惯直接沿用检测任务的0.01在分类任务上这通常偏高尤其是用预训练权重做微调时前几个epoch loss会剧烈震荡。我一般把初始学习率设在0.005配合cosine学习率调度验证集准确率曲线会比固定步长下降平滑很多。另外weight_decay不要低于1e-4水稻叶图片的背景纹理相似度高正则化不足时模型容易记住背景特征而不是病斑特征。3.4 预训练权重什么时候用什么时候不用YOLO11cls官方提供的预训练权重是在ImageNet-1K上训出来的类别是通用物体没有水稻病害。但它学到了丰富的低级视觉特征比如纹理、边缘、颜色分布这些对叶片病斑识别非常有用。5000张图的数据量不足以让模型从头学透这些特征所以结论很明确用预训练权重不要从零训练。使用方式是在训练命令里指定modelyolo11n-cls.ptultralytics会自动下载权重并加载。这里有一个坑预训练模型的分类头是1000类你的数据集是5类加载时ultralytics会替换最后一层全连接层前几轮训练里分类头收敛较慢是正常现象不要看到前几个epoch准确率低就觉得模型坏了。如果数据集类别恰好也是1000左右且和ImageNet类别高度重叠那保留完整分类头微调也可以但水稻病害不属于这种情况不需要额外处理。4. 一键训练脚本落地数据检查、自动划分、训练与产物导出4.1 脚本整体设计与调用方式所谓“一键训练”我的理解是不需要手动去改数据集路径、不需要自己拼训练命令、不需要记得上次用了什么参数拿到脚本改几个配置就能跑。所以脚本里包含四个阶段扫描目录并清洗非法样本、按分类分层划分数据、生成YOLO11cls需要的data.yaml、启动训练并保存类别映射文件。整条链路收敛到一个python train_rice.py --data rice_leaf_dataset --epochs 80命令里。4.2 第一步扫描目录并校验数据训练启动前先全量检查图片完整性发现损坏图片直接报错退出而不是等训练中途崩。这里把第2章的检查逻辑收进函数并补充一个类别数量校验任何一类少于30张直接拒绝训练因为这种数据量训出来没有实用价值。import sys from pathlib import Path from PIL import Image MIN_IMAGES_PER_CLASS 30 def validate_dataset(root: Path): categories sorted([d.name for d in root.iterdir() if d.is_dir()]) stats {} for cls in categories: imgs list((root / cls).glob(*.jpg)) stats[cls] len(imgs) if len(imgs) MIN_IMAGES_PER_CLASS: raise RuntimeError(f类别 {cls} 只有 {len(imgs)} 张图少于下限 {MIN_IMAGES_PER_CLASS}) for img_path in imgs: with Image.open(img_path) as im: im.load() print(f数据校验通过共 {len(categories)} 个类别{stats}) return categories逻辑说明先用glob(*.jpg)限定图片格式校验数量下限再逐张解码验证完整性。MIN_IMAGES_PER_CLASS是硬编码阈值实际使用可以改成命令行参数。这个脚本故意不做“自动剔除损坏图片”因为自动剔除会掩盖数据采集环节的问题报错退出让你去查原始数据这才是正确姿势。4.3 第二步按分类分层划分并生成 data.yaml划分逻辑见第2.4节这里在划分完成后直接生成YOLO11cls需要的data.yaml文件。YOLO11cls的data.yaml格式和检测任务类似核心字段是train、val、names。三种写法train/val指向文件夹、指向txt文件、指向yaml里的类别描述。这里用文件夹路径因为YOLO11cls会自动按ImageFolder方式读取省去维护txt文件列表的麻烦。import random import yaml from pathlib import Path def split_and_write_yaml(root: Path, val_ratio: float 0.1, seed: int 42): random.seed(seed) categories sorted([d.name for d in root.iterdir() if d.is_dir()]) train_root root / train val_root root / val train_root.mkdir(exist_okTrue) val_root.mkdir(exist_okTrue) for cls in categories: cls_dir root / cls images list(cls_dir.glob(*.jpg)) random.shuffle(images) n_val int(len(images) * val_ratio) for img in images[:n_val]: val_root.mkdir(cls, exist_okTrue) img.replace(val_root / cls / img.name) for img in images[n_val:]: train_root.mkdir(cls, exist_okTrue) img.replace(train_root / cls / img.name) data { path: str(root.resolve()), train: train, val: val, names: {i: name for i, name in enumerate(categories)}, } with open(root / data.yaml, w, encodingutf-8) as f: yaml.dump(data, f, allow_unicodeTrue, sort_keysFalse) print(f划分完成data.yaml 已写入 {root / data.yaml})这里有一个重要的取舍需要说明这个脚本用的是img.replace()会把原始图片移动到train和val子目录原始分类文件夹结构会被改变。如果你不希望动原始图片改成复制shutil.copy2或者在原地生成txt列表都行。移动的好处是后续训练时数据集路径干净坏处是一旦划分比例需要调整你得重新整理原始数据。我的习惯是保留一份原始目录做备份划分脚本操作副本。4.4 第三步启动 YOLO11cls 训练并保存类别映射划分完成后直接调用ultralytics的API启动训练不通过命令行是为了能在同一个脚本里做更多逻辑控制比如训练结束后导出混淆矩阵、把类别映射表保存下来供推理时使用。from ultralytics import YOLO def train(data_yaml: Path, model_name: str yolo11n-cls.pt, epochs: int 80, batch: int 32, imgsz: int 224, lr0: float 0.005): model YOLO(model_name) results model.train( datastr(data_yaml), epochsepochs, batchbatch, imgszimgsz, lr0lr0, optimizerAdamW, weight_decay5e-4, patience20, projectstr(data_yaml.parent / runs), namerice_cls, ) # 保存类别映射文件 import yaml as yml with open(data_yaml, r, encodingutf-8) as f: cfg yml.safe_load(f) name_map cfg[names] with open(data_yaml.parent / class_names.txt, w, encodingutf-8) as f: for idx in sorted(name_map.keys()): f.write(f{idx}\t{name_map[idx]}\n) return results参数说明patience20是早停参数验证集指标连续20轮不提升就提前停止防止无效训练时间浪费project和name指定训练输出目录便于多轮实验对比。类别映射写到class_names.txt每行是“索引 类名”推理时加载这个文件把模型输出索引还原成病害名称。这个文件是部署环节的必需品很多人训练完找不到类别和索引的对应关系推理脚本里硬编码类别顺序换数据集就翻车。4.5 输出产物说明训练结束后runs/rice_cls/目录下会有weights/best.pt和weights/last.pt。best.pt是验证集指标最优的权重部署用它last.pt只是最后一轮的状态用于从断点继续训练。此外会有results.png和confusion_matrix.png前者是训练过程曲线后者是验证集混淆矩阵。生产环境里我只会把best.pt、class_names.txt和一份data.yaml拷贝到部署目录其他训练中间产物不带走体积大且对推理无用。5. 训练与部署避坑实录翻车最多的五个地方5.1 现象训练到一半报“OSError: image file is truncated”且日志里没有具体文件名原因数据集中混入了网络传输截断的jpg文件PIL解码读到一半发现文件不完整。YOLO11cls的dataloader是多线程预读的报错信息经常不关联具体文件路径你甚至不知道是哪张图的问题。解决在训练前用第2.3节的校验脚本全量扫一遍把损坏图片找出来重新导出或者直接删除。这里有一个血泪经验千万不要把校验脚本里的except Exception写得过于宽泛然后continue会漏掉那些“能打开但像素缺失”的半损坏图。用im.load()强制解码像素是底线。5.2 现象验证集准确率95%拿到田间新拍的图片一测直接掉到60%原因数据泄露。最常见的是同一次拍摄的连拍图被分到了训练集和验证集或者划分时按文件名排序而不是随机导致相同病斑的不同角度照片天然聚集在一起。另一个常见原因是背景泄露某个类别的图片全部在某块田拍摄模型学到的是泥土颜色和光线方向不是病斑特征。解决按第2.4节的方式用随机种子做分层划分并做pHash去重阈值设在汉明距离小于10就判重。另外把每类图片的拍摄时间戳打出来看分布如果某类图片集中在同一天说明样本多样性不足需要补拍或者做更强的色彩增强。5.3 现象训练损失不断下降验证集准确率却剧烈震荡原因学习率偏高加上batch太小。用预训练权重微调时分类头替换成随机初始化前几个epoch梯度较大学习率0.01会让权重在最优解附近来回弹跳。验证集准确率震荡的另一个原因是batch太小导致BN统计量不稳定。解决把lr0降到0.003~0.005batch提到32以上如果显存不够就降低imgsz而不是降batch。再不行就把优化器从AdamW换成SGDmomentum虽然收敛慢但震荡会小很多。5.4 现象训练完成但某一类别的召回率接近0原因类别不平衡。比如“健康叶片”有1500张“胡麻叶斑病”只有200张模型倾向于把所有样本判成高频类因为这样整体准确率最高。混淆矩阵里表现为某一行几乎全是背景色。解决两个手段配合使用。一是给每个类设置权重YOLO11cls的分类任务接口里可以通过class_weights参数配置二是在数据层面做类别增广对少数类多做一些随机旋转、平移、mixup增强。5000张图规模不建议用过采样复制粘贴模型会过拟合少数类的特定样本。5.5 现象同一份数据在不同机器上训练准确率差异超过3个点原因随机种子没有全局固定。PyTorch的dataloader、CUDA的卷积算子都存在随机性只固定Python的random.seed远远不够。另外不同机器上的cuDNN版本差异会导致卷积实现不同即使代码完全一致结果也可能有细微差异。解决在训练脚本入口处固定三处种子random.seed()、numpy.random.seed()、torch.manual_seed()并设置torch.backends.cudnn.deterministic True。注意开启deterministic后训练速度会下降10%~20%这是为了可复现付出的合理代价。我在实验阶段会开正式批量训练时关掉。6. 更进一步的验证与部署混淆矩阵、ONNX导出与单图推理6.1 混淆矩阵决定模型是否真的可用只看准确率训练分类模型十有八九会在部署时翻车。准确率高只能说明整体判断正确但水稻叶病虫害分类最怕的是把“稻瘟病”判成“健康叶片”因为这会直接耽误防治。训练结束后第一件事是打开confusion_matrix.png看哪些类别之间互相混淆。稻瘟病和胡麻叶斑病病斑都是褐色斑点早期形态接近混淆是常态如果混淆集中在它们之间说明数据还可以接受需要补拍更多早期症状样本如果健康叶片被大量误判为病害说明训练集里的健康叶片太干净没有覆盖生理性黄化、机械损伤这些非病害状态。6.2 导出ONNX并写一个单图推理脚本部署到服务器或边缘设备通常不需要完整的ultralytics环境。把best.pt导出成ONNX格式推理依赖就只剩onnxruntime和PIL。导出命令很简单yolo export modelruns/rice_cls/weights/best.pt formatonnx imgsz224导出后用下面的脚本对单张图片做推理并对照class_names.txt输出病害名称import onnxruntime as ort from PIL import Image import numpy as np sess ort.InferenceSession(best.onnx) input_name sess.get_inputs()[0].name img Image.open(test.jpg).convert(RGB).resize((224, 224)) img_array np.array(img).astype(np.float32) / 255.0 img_array np.transpose(img_array, (2, 0, 1))[None, ...] probs sess.run(None, {input_name: img_array})[0].squeeze() pred_idx int(np.argmax(probs)) name_map {} with open(class_names.txt, encodingutf-8) as f: for line in f: idx, name line.strip().split(\t) name_map[int(idx)] name print(f预测类别: {name_map[pred_idx]}, 置信度: {probs[pred_idx]:.4f})参数说明onnxruntime输入格式是NCHW所以要用np.transpose((2, 0, 1))把HWC转成CHW[None, ...]是加一个batch维度归一化直接除以255.0这是YOLO11cls导出的ONNX默认输入格式不需要再做ImageNet的mean/std归一化。导出时有个容易踩的坑导出的ONNX对输入尺寸是固定的训练时用的224导出也要用224部署时如果resize成其他尺寸会直接报错。做这个项目第一版的时候我的习惯是用yolo predict命令直接看输出准确率挺高就没继续做部署。直到真正接到田间识别需求才发现没有类别映射文件、没有ONNX模型所有推理逻辑都耦合在ultralytics环境里换了台没装YOLO的机器就跑不起来。后来养成的习惯是训练脚本里固定存放class_names.txt训练完立刻导出ONNX这样模型产物和类别定义永远绑定在一起换个环境也不慌。这套流程跑顺之后从拿到整理好的数据集到产出一个可部署的模型一个下午就能完成。希望帮到你。本文还有配套的精品资源点击获取