简介:面向YOLO系列算法目标检测的开发者与研究人员,这份作物杂草数据集包含200张已标注图像,可直接用于训练、验证与测试,并附带data.yaml配置文件,适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流版本。标注提供YOLO格式(txt)与VOC格式(xml)两套,分别存放于不同文件夹,便于切换训练框架;txt记录类别索引、归一化中心点坐标及宽高,xml保留传统检测标注信息,适应不同工作流。压缩包共601个文件,含200个jpg原图、200个txt标签、200个xml标签及1个yaml配置,整体约185.56MB,数据划分清晰,可直接用于模型训练和测试。目前已有106人学习下载,适合需要快速验证模型效果、开展农业视觉识别实验或入门YOLO数据格式的读者。
1. 先回答最直接的问题:200 张带标签图像做作物杂草检测,够不够用
很多人拿到“yolo算法-作物杂草数据集-200张图像带标签-.zip”这种压缩包,第一反应就是“200 张?这能训出什么?”但作物杂草检测和你平时刷的 COCO 类通用目标检测完全是两码事——场景固定、目标类别少、拍摄设备基本不变。200 张干净、对准标签、和真实田间状况对得上的图,足够你把 YOLO 的完整链路走一遍:解包、核对标签、转换格式、训练、验证、导出模型,最后得到一个能下地试跑的最小可用版本。这篇文章就围绕这个小数据集展开,告诉你怎么一步步把它变成能用的检测模型,以及中途一定会踩到的几个坑。
2. 拆开 .zip 之后先别训练:把标签格式、转换脚本和目录结构一次理清
2.1 200 张图能撑起什么任务:小数据集的边界和可能性
先给结论:200 张图能撑起一个“固定场景下的单类或多类杂草检测模型”,撑不起“任意地块、任意光照、任意生长期的通用杂草识别”。这里面的差距不在算法,而在数据分布。如果你手里的 200 张图是在同一个地块、同一种相机高度、同一时间段拍的,那训练集和验证集之间的分布差距很小,训练出来的模型在相似条件下表现会不错。但如果你指望它到了另一块地、换个光照角度还能保持同样的效果,那基本是玄学,不可能。
所以拿到数据集的第一件事不是急着训练,而是确认这些图的覆盖范围:包含了哪些杂草类别、作物是什么、拍摄角度是俯视还是侧视、图像分辨率多大。这些信息决定了你后面怎么划分验证集、怎么调参数、怎么设置盲测标准。200 张图本身的边界就在那里,你要做的是在这个边界内把模型做扎实,而不是幻想小数据创造奇迹。
2.2 先核对压缩包里是什么格式:VOC/XML、COCO/JSON、YOLO/TXT 三选一
解压之前先用脚本看一眼压缩包里的文件结构。常见的数据集打包方式有三种:每张图对应一个 XML 的 VOC 格式、一个 JSON 文件的 COCO 格式、或者每张图对应一个 TXT 的 YOLO 格式。这三种格式的标签结构完全不同,直接决定了你接下来要写什么样的转换脚本。
import zipfile from pathlib import Path zip_path = Path("作物杂草数据集-200张图像带标签.zip") with zipfile.ZipFile(zip_path) as zf: names = zf.namelist() # 按扩展名统计,快速判断是哪一种标注格式 stats = {} for n in names: ext = Path(n).suffix.lower() stats[ext] = stats.get(ext, 0) + 1 for ext, count in sorted(stats.items(), key=lambda x: -x[1]): if ext == "": print(f"{'目录':8s} {count} 个") else: print(f"{ext:8s} {count} 个文件") # 打印前5个非目录文件的具体路径,确认目录层级 files = [n for n in names if Path(n).suffix] for n in files[:5]: print(n)这段代码的逻辑很简单:把压缩包里的文件按扩展名归类,统计每个扩展名有多少个文件。看到.jpg配.xml,那基本就是 VOC 格式;看到.jpg配.txt,可能是 YOLO 格式,也可能只是没标签的占位文件;看到单个.json加一堆.jpg,大概率是 COCO 格式。打印前几个文件的路径,是为了确认压缩包内部有没有嵌套目录,后面写 data.yaml 时路径需要对应上。
三种格式的核心差异如下,转换前心里要有数:
| 格式 | 标签文件 | 坐标形式 | 类别标识 |
|---|---|---|---|
| VOC (XML) | 每图一个 .xml | bndbox 内 4 个绝对像素坐标 xmin/ymin/xmax/ymax | object 标签下的 name 文本 |
| COCO (JSON) | 单文件或每图一个 .json | 像素坐标或归一化坐标,annotation 内数组 | category_id 数字 |
| YOLO (TXT) | 每图一个 .txt | 一行一个目标,x_center y_center w h 归一化 0~1 | 每行开头的数字类别 ID |
其中 VOC 和 COCO 都要先转换成 YOLO 的 txt 格式才能训练。别省这一步,YOLO 原生训练器只认 txt。
2.3 把 VOC 转成 YOLO 归一化 txt:转换脚本与四个边界坑
作物杂草数据集里最常见的标签格式是 VOC XML,因为很多标注工具默认导出的就是 XML。下面这段是转换脚本,能直接跑:
import xml.etree.ElementTree as ET from pathlib import Path # 类别顺序必须和压缩包内标签的 name 字段一致 class_names = ["weed", "crop"] def voc_to_yolo(xml_path: Path, out_dir: Path, class_names: list[str]): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") if size is None: print(f"跳过 {xml_path.name}: 缺少 size 信息") return img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: print(f"{xml_path.name}: 忽略未知类别 {name}") continue cls_id = class_names.index(name) box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # 边界坑一:XML 里偶尔出现越界坐标,先裁剪到图像范围内 x1 = max(0, min(x1, img_w)) x2 = max(0, min(x2, img_w)) y1 = max(0, min(y1, img_h)) y2 = max(0, min(y2, img_h)) w = x2 - x1 h = y2 - y1 # 边界坑二:过滤掉宽高为 0 的坏框 if w <= 0 or h <= 0: print(f"{xml_path.name}: 跳过大小为 0 的框") continue # YOLO 需要的是中心点坐标和宽高,并且全部归一化到 0~1 x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h w_norm = w / img_w h_norm = h / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}") out_path = out_dir / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-8")这段代码把 XML 里的绝对像素坐标转成中心点加宽高的归一化坐标。为什么 YOLO 要中心点格式?因为同一张图在不同尺度下缩放,归一化坐标不需要跟着变,模型训练时做各种尺度增强就不用再回算坐标了。
实际转换时会碰到几个坑:一是 XML 里的坐标偶尔会超出图像宽高,不裁剪的话生成的 txt 里会出现大于 1 的坐标值,训练时轻则警告、重则 loss 异常;二是标注工具可能把某个目标标成 0 宽或 0 高,这种框要直接过滤掉,不然损失函数里会出现 NaN;三是 XML 里的类别 name 和你的 class_names 列表必须完全匹配,差一个字母就会把目标标成“未识别”;四是图像是 JPG 但 XML 文件名后缀是 .jpeg,匹配图像和标签时要用 Path.stem 而不是完整文件名。
2.4 划分 train/val:按类别分层,不能随机抽完事
200 张图,最忌惮的就是随便random.shuffle之后按 8:2 切一刀。作物杂草数据集的类别分布往往极不均衡,可能杂草出现 180 张图里,作物只出现 30 张图里。如果随机切,很有可能验证集里一种类别一张都没有,训练出来的模型在这类目标上就是瞎猜。
我一般用按类别分层的办法来切分,保证每种类别在验证集里至少出现一次:
import random from pathlib import Path random.seed(42) img_dir = Path("images") label_dir = Path("labels") imgs = sorted(img_dir.glob("*.jpg")) # 统计每张图里包含哪些类别 img_classes = {} for img in imgs: txt = label_dir / (img.stem + ".txt") if not txt.exists(): continue cls_ids = set() for line in txt.read_text(encoding="utf-8").strip().splitlines(): if line.strip(): cls_ids.add(int(line.split()[0])) img_classes[img] = cls_ids # 先保证每个类别在 val 里至少有一张 val = set() for cls_id in sorted({c for cs in img_classes.values() for c in cs}): candidates = [i for i in imgs if cls_id in img_classes[i] and i not in val] if candidates: val.add(random.choice(candidates)) # 再补齐到 20% 总量 target = max(1, int(len(imgs) * 0.2)) while len(val) < target: c = random.choice(imgs) if c not in val: val.add(c) train = [i for i in imgs if i not in val] print(f"train: {len(train)} 张, val: {len(val)} 张")这段代码的核心逻辑是先做“保类别”再做“补数量”。先遍历所有类别 ID,从包含该类的图片里抽一张进验证集;然后再随机补到总张数的 20%。这样即使某个类别只有两张图,验证集里也一定能看到它。代价是训练集里这类只留一张,训练效果会打折,但至少验证指标不会失真到骗你自己。
目录结构也按 YOLO 惯例来组织,这是 Ultralytics 训练器默认认可的布局:
weed_dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/图片和标签分开存放,train 和 val 各自对应。注意 labels 里的 txt 文件名必须和 images 里的 jpg 文件名完全一致(不包括扩展名),否则训练时找不到匹配标签。
提示:压缩包里如果有中文文件名,在 Linux 下解压可能出现乱码。用
unzip -O gbk 文件名.zip可以指定编码解压,避免后面路径匹配翻车。
3. 用 YOLOv8 训练杂草检测模型:选型理由、data.yaml 配置和训练参数设置
3.1 为什么选 YOLOv8 而不是 YOLOv5/YOLO11:小数据集的实际选型逻辑
现在 YOLO 家族成员很多,但做作物杂草检测这个小方向,我首推 YOLOv8,理由有三个。第一,YOLOv8 的预训练权重下载和加载做得最省心,一条命令自动拉取 COCO 预训练模型,不需要手动去翻各种镜像站,这对初学者最友好。第二,Ultralytics 的训练器把数据增强、早停、日志可视化都集成好了,200 张小数据集最需要的恰恰是这些内置机制,不需要你自己搭一堆外围脚本。第三,YOLOv8 的模型规模梯度很清晰,从 n 到 x 都有,小数据集先用 nano 级模型起步,非常合适。
YOLOv5 虽然生态成熟,但对新用户来说接口不如 v8 统一;YOLO11 更偏向最新的算法特性,社区里踩坑记录还没沉淀够。做项目讲究的是稳定复现,不是追新。
提示:用 YOLOv8 训练自己的数据集,最省事的方式是从 COCO 预训练权重开始做迁移学习。200 张图从头训练基本不可能收敛,但从预训练权重继续微调,效果会好得多。这就是热词里说的“yolo预训练模型下载”的意义所在。
3.2 data.yaml 配置:路径、类别数 nc、类别名 names 三个坑
训练前先写好 data.yaml,这是 YOLO 读取数据集配置的唯一入口:
# data.yaml path: /home/yourname/weed_dataset # 改成你解压后的绝对路径 train: images/train val: images/val nc: 2 # 类别数量 names: 0: weed 1: crop这里三个参数最容易出问题。path必须是绝对路径,虽然相对路径也能写,但在不同机器上跑容易因为当前工作目录不对而找不到数据,浪费半天排查时间。nc必须和类别实际数量严格一致,写多了会在训练日志里看到莫名其妙的空类,写少了类别 ID 直接越界。names的顺序不是随便写的,它决定了预测结果里“第 0 类叫什么”,如果你在标签转换脚本里把 weed 设为 0,这里就必须让 0 对应 weed。
3.3 训练命令和参数:batch、epochs、imgsz、patience 怎么定
安装 Ultralytics 然后跑训练,一条命令就能开始:
pip install ultralytics yolo train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=30 \ device=0参数解释很直白,但每个都有讲究。model=yolov8n.pt表示基于 COCO 预训练的 nano 版继续训练,这次训练会自动从官网下载权重文件到本地缓存。epochs=100是最大训练轮数,不是必然跑满,配合patience=30做早停。因为 200 张图很小,模型很容易在 30 到 50 轮之间达到最优,patience 设成 30 表示连续 30 轮验证指标不再提升就自动停,节省时间也避免过拟合。
imgsz=640是训练时缩放到 640x640。如果原图分辨率在 1920 以上且杂草目标很小,可以考虑提到 768 或 1024,但显存占用会显著增加。batch=16在 200 张图的情境下意味着每个 epoch 只有 12 个 batch,梯度更新次数很少,所以小数据集不要用特别大的 batch,8 到 16 比较合理。如果显存报 OOM,优先把 batch 降到 8,而不是改 imgsz。device=0是使用第一块 GPU,纯 CPU 训练时改成device=cpu,但训练速度会慢一个数量级,200 张图也要等一阵子。
YOLOv8 的损失函数由三部分组成:box_loss 负责预测框和真实框的重合度,cls_loss 负责分类是否正确,dfl_loss 负责边界框分布的细节拟合。训练日志里这三项都下降,说明模型在正常学习。
3.4 训练日志怎么看:四个关键字段判断模型真的在学
训练开始后,终端会输出这样的日志:
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 30/100 3.1G 1.21 0.583 1.102 7 640 Class Images Instances P R mAP50 all 40 102 0.851 0.79 0.831这里要看四个关键字段。第一是box_loss,它下降的速度代表模型定位能力在提升,如果这个值一直横盘不降,大概率是标签坐标有错。第二是cls_loss,它代表分类能力,如果它降了但 box_loss 不动,问题出在标注框的位置不准确。第三是验证集里的R(召回率),杂草检测场景里漏检比误检更致命,召回率上不去说明模型漏掉了很多目标。第四是mAP50,这个指标最容易骗人,后面单独说。
训练产生的所有中间权重保存在runs/detect/train/下,best.pt是验证集上表现最好的模型,last.pt是最后一轮的模型。有些新手不看日志直接拿 last.pt 去用,结果发现效果很差,因为最后几轮可能已经过拟合了。记住只用 best.pt。
4. 避坑:用 200 张作物杂草图训 YOLO 的 5 个高频翻车现场
4.1 mAP 全程为 0:类别编号和标签对不上
现象:训练 loss 正常下降,但验证集 mAP 从第一个 epoch 到最后一个 epoch 始终是 0,precision 和 recall 也接近 0。
原因:这是 200 张小数据集最经典的翻车现场。压缩包里的标签类别编号和你在 data.yaml 里定义的类别顺序不一致。比如数据集的标签文件里 0 代表 crop、1 代表 weed,但你的 data.yaml 里 0 写了 weed、1 写了 crop。模型在训练时看到的目标类别和真实类别完全错位,验证时自然一个都认不出来。
解决:不要靠肉眼猜。训练前先写一段 5 行的脚本,把标签 txt 里实际出现的类别 ID 全部打印出来,再和 data.yaml 的 names 一一对照:
from pathlib import Path label_dir = Path("labels/train") all_cls = set() for txt in label_dir.glob("*.txt"): for line in txt.read_text(encoding="utf-8").strip().splitlines(): if line.strip(): all_cls.add(int(line.split()[0])) print("标签中实际存在的类别 ID:", sorted(all_cls))如果打印结果是[0, 1],但你觉得 weed 应该是第 0 类,那就核对转换脚本里的 class_names 顺序。这个脚本应该是你整个流程的第一步,每次换新数据集都要跑一遍,别嫌麻烦。
4.2 验证 loss 第 30 轮抬头:过拟合比预想来得早
现象:训练集 loss 一直在降,验证集 loss 在第 20 到 30 轮左右开始掉头向上,同时验证集 mAP 不再上涨甚至下滑。
原因:200 张图本来就少,模型在训练集上转了两三轮就能把标注目标背下来,从第 20 轮开始就进入死记硬背阶段。这和模型大小也有关,如果用 YOLOv8s 甚至更大的模型,过拟合会来得更快。
解决:三个手段配合使用。第一,模型先用 nano 级,YOLOv8n 对 200 张小数据集是更安全的选择,参数少意味着记忆能力弱、泛化压力大。第二,patience=30配合早停,看到验证 loss 掉头后自动停止,不用手工盯。第三,把验证集比例从 20% 提到 25%,200 张里分出 50 张做验证,能更早观测到过拟合信号。
4.3 杂草框老是偏大偏小:标注质量和 imgsz 的锅
现象:训练完看预测结果,框住杂草的边界框明显偏大,一个框里装了土和相邻的作物叶子;或者框偏小,只框住了杂草的局部。
原因:两个来源。一是原始标注就不准——作物杂草这类密集场景,标注员很难精确框住每一株的外观,特别是叶片交叉部分,很多人会随手把整个叶片簇框进去;二是模型推理时用的 imgsz 和训练时差距太大导致目标尺度失配,训练 640 推理 1280 虽然能让小目标更清晰,但边界框更倾向于放大。
解决:先检查标注质量。用yolo predict跑几张验证集图片,把预测框和真实框叠加看,如果普遍偏大,回到标注文件检查。如果标注本身没问题,只是推理时想提高小目标检测,可以试试在训练阶段就把 imgsz 提到 768,而不是训练 640 推理 1280 这种割裂做法。
4.4 绿草里找不到杂草:对比度不足时调增强参数
现象:模型在验证集 mAP 还行,但实际检测时杂草检出率很低,特别是背阴处和土壤背景里的杂草几乎全漏。
原因:这是作物杂草场景最典型的视觉难题——杂草和背景都是绿的,对比度天然低。如果原图是在阴天或逆光下拍的,细节对比更差。模型在低对比度图像上学到的特征不够锐利,推理时自然漏检。
解决:不换算法,先调数据增强。Ultralytics 训练器内置了 HSV 增强参数,在 data.yaml 同级可以建一个 hyp.yaml 覆盖默认值:
# hyp.yaml hsv_h: 0.02 hsv_s: 0.8 hsv_v: 0.5把hsv_s从默认的 0.7 提到 0.9、hsv_v从 0.4 提到 0.6,让模型在训练中看到更多色彩饱和度和明暗变化,强迫它去学形状和纹理特征而不是依赖颜色。如果你的图像普遍偏暗,可以在数据预处理阶段加一个直方图均衡化步骤,但这需要在训练前把图像统一处理,不要混着来。
4.5 指标很好看但田间就废:分布偏移和盲测集
现象:验证集 mAP50 达到 0.9,precision 和 recall 都过 0.8,但拿到另一个地块或不同天候下实拍的照片上跑,检测结果惨不忍睹。
原因:这就是机器学习里说的分布偏移。200 张训练图如果都来自同一块地和同一天的光照条件,模型学到的其实是“这个地块的杂草长这样、这个光照下的颜色是这样”,而不是“杂草长什么样”。验证集也是从同一个分布里切的,指标自然虚高。
解决:在训练之前就预留盲测集。从 200 张之外找 20 到 30 张完全不同地块或不同时间拍摄的图,不打进训练也不打进验证,等训练结束后再跑一遍。指标以盲测结果为准,验证集 mAP 只是用来选模型的参考,不是用来向别人证明效果的素材。
5. 验证模型能不能下地:混淆矩阵、PR 曲线和盲测脚本
5.1 混淆矩阵读法:草被认成作物和漏检要分开看
训练结束后的第一步不是急着部署,而是先跑一遍验证:
yolo val model=runs/detect/train/weights/best.pt data=data.yaml这个命令会在runs/detect/val/下生成 confusion_matrix.png、PR_curve.png、F1_curve.png 等一系列图表。很多人只看 mAP 就结束了,但混淆矩阵才是判断模型能否下地的关键。
在作物杂草场景里,混淆矩阵里最危险的不是“把杂草认成作物”,而是“杂草那一行的召回率过低”。把杂草认成作物,后果是漏喷,杂草继续生长;把作物认成杂草,后果是误喷,整片作物受损。两种错误的代价完全不同。所以看混淆矩阵时,先看杂草类别的召回率,再看作物类别的精确率,最后才看整体准确率。
5.2 PR 曲线怎么用:把置信度阈值调到实际场景的平衡点
PR_curve.png 展示的是在不同置信度阈值下 precision 和 recall 的折中关系。默认推理阈值是 0.25,这个值对杂草检测来说可能偏低或偏高,取决于你的场景偏误检还是偏漏检。
如果下地后发现杂草漏检多,把置信度阈值降到 0.15 或 0.1,多框出来一些无所谓,后面可以人工筛选。如果发现误检太多,把阈值提到 0.4,减少误报的代价是牺牲一部分召回率。调阈值不需要重新训练,推理时加一个conf参数就行:
yolo predict model=runs/detect/train/weights/best.pt source=../field_test/ conf=0.15实际使用中,我习惯先用 0.25 跑一遍盲测,数一下漏检和误检数量,再根据代价方向决定阈值往哪个方向调。
5.3 用没见过的田间照片做 blind test:一段能直接跑的脚本
盲测集是验证模型能不能用的最后一道关口。从不同地块或不同时间拍的图里找 20 张左右,手工标好每张图里杂草和作物的数量,然后跑一段推理脚本统计检测结果:
from ultralytics import YOLO from pathlib import Path model = YOLO("runs/detect/train/weights/best.pt") test_dir = Path("field_test") # 放盲测图片的目录 for img_path in sorted(test_dir.glob("*.jpg")): result = model(img_path, conf=0.25, verbose=False)[0] boxes = result.boxes if boxes is None or len(boxes) == 0: print(f"{img_path.name}: 无检测") continue for box in boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) # 输出文件名、类别名和置信度 print(f"{img_path.name}: {result.names[cls_id]} conf={conf:.2f}") # 打印每张图检测到的各类别数量 cls_count = {} for box in boxes: name = result.names[int(box.cls[0])] cls_count[name] = cls_count.get(name, 0) + 1 print(cls_count)这段脚本对每张盲测图输出检测到的类别和数量,拿这个和手工统计的“真值表”对比,算一下实际召回率。判断标准很简单:杂草漏检率必须低于 5%,因为漏检意味着漏喷;作物误检率需要根据你的喷洒代价来决定,如果误喷直接造成经济损失,那阈值还要再往上调。
盲测时有一个很容易犯的错误:把置信度调得极低来追求召回率,结果一张图里输出几十个框。这种模型的精确率已经崩了,下地实际使用会被大量误检淹没。所以盲测的结论要同时给出“在这个阈值下的漏检数和误检数”,而不是只看一个指标。
6. 从 200 张到能长期用的模型:数据增强、伪标签和半监督的落地顺序
200 张图训练出的模型只能算一个能用的起点,离“长期稳定地服务于农业生产”还有距离。接下来的路径也很明确,按顺序做就可以。
第一步是数据增强。不要一上来就拍脑袋加旋转、翻转,先看模型在盲测集上的失败模式。如果漏检发生在光照暗的区域,重点加 HSV 亮度扰动;如果漏检发生在密集的杂草丛,重点加 Mosaic 增强。Ultralytics 默认开了 Mosaic,200 张图配 Mosaic 是够用的,但不要太激进,增强过头会让模型学到一堆不真实的纹理。
第二步是伪标签。用 best.pt 在更多未标注图像上跑推理,把置信度高于 0.7 的检测结果转成 YOLO 标签,人工抽查其中一部分,然后把这些伪标签数据混进训练集。这个做法能快速扩数据,前提是人工抽查比例足够高,不能直接把模型自己的输出喂回去训练,否则错误会在迭代中被不断放大。
第三步才是考虑更多标注数据。优先补充盲测里漏检最多的那些场景,而不是随机拍一堆同样地块的照片。数据质量比数量重要得多,50 张覆盖了不同光照、不同土壤背景的标注图,效果可能好过 500 张同一地块凑数的图。
我自己的习惯是,200 张图的模型永远只当基线来看。每次换地块或换设备,都要重新收集一小批当地图像做盲测,用失败样例决定下一步动作。这套打法的核心不是追求一次训练直接达标,而是让数据和模型迭代形成一个能收敛的闭环。希望帮到你。
本文还有配套的精品资源,点击获取