简介:面向Python深度学习初学者的番茄叶片病害目标检测完整项目,基于YOLO与PyTorch实现,涵盖数据集制作、模型训练与PyQt可视化识别全流程。压缩包共1470个文件、约40.55MB,包含730张jpg叶片图像、363个txt标签、358个xml标注文件、3个YOLO配置文件、3个Python脚本及3个已训练模型pt权重;其中数据划分脚本可自动将原始标注转为YOLO格式并生成训练/验证集,训练脚本输出results.csv、验证集预测图与TensorBoard事件文件,便于直观评估模型效果。已有110人学习下载,适合需要快速搭建植株病害识别实验、熟悉YOLO训练流程或开展毕业设计/课程项目的读者。通过运行代码,既能了解数据标注格式转换与训练参数调优方法,也能直接使用PyQt界面加载图片完成病害类别检测,是一份从理论到部署的综合性参考样例。
1. 番茄叶片病害目标检测:别急着解压跑模型,先搞清楚这个方案在解决什么
拿到“基于python深度学习对番茄叶片病害目标检测-含数据集和代码.zip”这个压缩包,我劝你别急着解压敲训练命令。农业视觉项目里,决定模型能不能用的,往往不是深度学习算法本身,而是数据集里那些框得歪歪扭扭的标注。这个方案本身很直白:输入番茄叶片照片,用 python 做目标检测,框出早疫病、晚疫病、细菌性斑点等病害的位置和类别,输出带预测框和置信度的结果图。
它对应的是农业巡检里的真实痛点:人工逐叶翻看效率低,病害特征又非常相似,肉眼很容易把早疫病和晚疫病看混。适合谁的场景?本科或研究生阶段做农业AI课题的学生,想给温室或大棚配一个低成本病害筛查工具的工程师,以及刚接触深度学习、恰好拿到一份带数据集和代码的初学者。在动手训练之前,先想清楚模型选型、数据格式、训练参数、问题排错、结果验证这五步,后面才能少走弯路。
2. 选型决定省力程度:目标检测模型、框架与标注工具怎么配
很多人拿到包含数据集和代码的项目包,第一反应是“用什么模型更牛”。真实经验恰恰相反,选型阶段省下来的力气,最后都会在数据清洗阶段加倍还回来。番茄叶片病害检测有两个明显特征:一是目标在整张图里占比小,叶片边缘还有卷曲、阴影和露水反光;二是病害类别之间的视觉差异极小,早疫病和晚疫病都呈现为叶片上的同心环纹,连人都容易看错。这种场景下,模型选型要优先考虑标注成本低、迭代速度快、单卡能训练,而不是单纯追求 mAP 刷高多少。
2.1 YOLO 为什么是番茄叶片的默认答案:和 Faster R-CNN、SSD 放在一起看
先给结论:这类项目我一般会把 YOLO 系列作为默认选项,只有特殊需求才回头用两阶段检测器。原因是病害检测对“框得准”的要求远低于“找得到、分得对”,而 YOLO 把定位和分类放进同一个网络里一次推理完成,训练只需要一份标注框,不用为候选区域生成额外写采样逻辑。Faster R-CNN 在极端小目标上有理论精度优势,但训练时要跑 RPN,显存占用和单 epoch 耗时明显更高;SSD 处于两者之间,工程生态这些年几乎不再更新,遇到问题连可抄的教程都少。
| 算法 | 定位与分类方式 | 单卡可训性 | 工程生态 | 适合场景 |
|---|---|---|---|---|
| Faster R-CNN | 两阶段,先提候选框再分类 | 一般,显存和耗时高 | 成熟但配置繁琐 | 对精度极致敏感、机器资源充足 |
| SSD | 单阶段,多尺度预测 | 较好 | 更新停滞 | 已有旧代码要维护 |
| YOLO 系列 | 单阶段,anchor-free 演进 | 好,显存可控 | 活跃,教程和预训练权重最全 | 病害检测这类快速迭代项目 |
选 YOLO 而不是更花哨的模型,还有个务实原因:预训练权重。COCO 上训出来的 YOLO 权重对叶片纹理、茎秆背景已经有不错的底层特征提取能力,迁移到番茄病害上只需微调高层,几百张图也能训出能看的初版。相比之下,从零训练一个商业级模型,同样数据量往往连收敛方向都找不到。这项“借用预训练权重”的能力,也是标题里“深度学习”三个字价值最大的地方。不过这里有个边界要认清:如果你最终要做的不只是框出病害,还要统计病斑面积占比,那就得换成 YOLO-seg 这类实例分割模型,检测框给不了像素级面积。
2.2 框架、标注工具、硬件搭配:给纯小白的选型清单
框架上,我默认 PyTorch。不否认 TensorFlow 在工业部署里也有一席之地,但 YOLO 系工程几乎全部以 PyTorch 实现,网上能被搜到的“超详细配置教程”大多也是这条路线。PyTorch 的调试手感也更适合入门:print 一个张量的 shape 立刻就知道数据流到哪一步出了错,断点打在 loss.backward() 前后能直接看到梯度是否消失,这份直觉对第一次做目标检测的人非常重要。
标注工具方面,常见做法是先用 LabelImg 这类轻量工具把最初的 200 张图标完,再换 X-AnyLabeling 这类带自动辅助标注的工具做增量扩充。工具选型不用纠结,能导出 YOLO 格式 txt 就行。关键在导出后必须看一遍标注文件:类别 ID 有没有和类别名错位、框的坐标是不是负数、叶片重叠区域有没有漏标。如果数据集是多批次采集的,还要注意批次间的光照和色温差异,最好在训练前做一次全局的亮度归一化,否则模型会把“偏暗环境”当成一个隐性类别。
硬件上,一张 6GB 显存的显卡就够完成训练,没有 GPU 就用 CPU 先把流程跑通,只是速度会慢几十倍。真到调参阶段再租卡也不迟。配置环境时如果遇到 Windows 报缺 msvcp140.dll,别急着重装系统,装一下 VC++ 运行库就能解决,具体在 4.1 小节细说。
2.3 拿到的压缩包先别训:解压自检三步
第一步,看目录结构。常见做法是数据集解压后出现两个顶层目录:images 放图片,labels 放同名 txt 标签,各自里面再分 train 和 val。如果压缩包里的图片按病害类别分文件夹,说明它还是分类数据集格式,得先转成检测格式,第三章会给转换脚本。
第二步,看标签文件的数值。YOLO 格式的每一行是“类别ID 中心点x 中心点y 框宽 框高”,坐标全部归一化到 0~1。用文本编辑器随便开一个 txt,如果看到 x_center 大于 1.5、宽度是 0、或者行里的数字超过 6 列,这份数据基本不能直接进训练。第三步,检查标签和图片是否能对上。统计每个类别有多少目标,顺便看图片尺寸是否统一。快速命令:
# 统计训练集图片数量 find dataset/images/train -name "*.jpg" | wc -l # 抽查前 20 个标签文件里各有多少个标注框 for f in dataset/labels/train/*.txt; do echo "$(basename "$f"): $(wc -l < "$f")"; done | head -20 # 按标签文件的第一列聚类,统计每个类别目标总数 cat dataset/labels/train/*.txt | awk '{print $1}' | sort | uniq -c第一行命令看图片总量,第二行抽查标签框数量,第三行直接用 awk 按第一列聚类,得到每个病害类别在训练集里的目标总数。如果某个类别只有个位数目标,要尽早决定是扩充数据还是调 loss 权重,等到训练结束才发现类别不平衡,前面几十个 epoch 等于白跑。另外建议用 file 命令抽查几张图片的实际格式,防止扩展名是 jpg 内容却是 png 的混入情况,这类文件在统一缩放的训练流程里会造成偶发的解码报错。
3. 把数据集喂给训练脚本:目录重组、格式转换与可视化自查
数据准备这个环节最花时间,也最容易被跳过。我见过太多人拿到数据集后直接开训,第一次跑通后才发现标注框画错了方位,回头改数据等于把整个训练流程重做一遍。与其赌运气,不如先花半小时做三件事:把目录按 YOLO 规范重组、把非 YOLO 格式的标注转成统一 txt、把标注可视化逐张看一遍。这三步做完,模型训练才算有了一个可信的输入。
3.1 目录重组与 train/val 划分:一个能直接跑的 Python 脚本
如果压缩包里的图片和标注文件在同一目录、共用同一文件名前缀,还要按 8:2 划分训练集和验证集。下面这份脚本可以直接放到数据集根目录运行,它会把图片和标签同步复制到 YOLO 要求的布局里。
# split_dataset.py import os import shutil import random random.seed(42) # 固定随机种子,保证多次运行划分一致 src_images = "raw_data/images" # 原始图片目录 src_labels = "raw_data/labels" # 原始标签目录 out_images = "dataset/images" # 输出图片目录 out_labels = "dataset/labels" # 输出标签目录 for sub in ["train", "val"]: os.makedirs(f"{out_images}/{sub}", exist_ok=True) os.makedirs(f"{out_labels}/{sub}", exist_ok=True) imgs = [f for f in os.listdir(src_images) if f.endswith((".jpg", ".jpeg", ".png"))] imgs.sort() random.shuffle(imgs) val_count = max(1, int(len(imgs) * 0.2)) val_imgs = set(imgs[:val_count]) for img in imgs: sub = "val" if img in val_imgs else "train" stem = os.path.splitext(img)[0] src_txt = os.path.join(src_labels, stem + ".txt") if not os.path.exists(src_txt): continue # 图片没有对应标签时跳过,避免把脏数据带进训练 shutil.copy(os.path.join(src_images, img), os.path.join(out_images, sub, img)) shutil.copy(src_txt, os.path.join(out_labels, sub, stem + ".txt"))逻辑说明:脚本先按扩展名筛出图片,通过 sort 和 random.seed 保证划分可复现,再按 20% 比例取前 n 张作验证集,最后把图片和同名 txt 一起复制过去。这里最重要的一点是“同步”:图片和标签文件共用文件名前缀,只要不同步,训练时会报“找不到标签”或者把两张图的标签错配到一张图上。
参数说明:src_labels 没有对应 txt 时直接跳过,避免把未标注图片带进训练;想改成 9:1,把 0.2 改成 0.1 即可;shutil.copy 是复制而不是移动,原目录文件还在,跑错了可以删掉 dataset 重来,这算一个低成本后悔药。
3.2 把 VOC/COCO 标注转成 YOLO 格式:转换脚本与四个边界坑
很多公开数据集给的是 VOC 的 xml 或 COCO 的 json 标注,YOLO 训练读不了这两种格式,必须先转换。VOC 格式里每个目标对应一个 bndbox 节点,里面是 xmin、ymin、xmax、ymax 四个像素坐标;YOLO 要的是相对图片宽高的中心点和宽高比例。核心换算只有几行,但真正的坑全在换算之外。
# voc2yolo.py import os import xml.etree.ElementTree as ET class_map = {"healthy": 0, "early_blight": 1, "late_blight": 2, "bacterial_spot": 3} def convert_one(xml_path, img_w, img_h, out_txt): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: continue # 类别名不在映射表里,直接跳过 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) # 越界裁剪:坐标小于 0 按 0,大于宽高按宽高 x1 = max(0, x1); y1 = max(0, y1) x2 = min(img_w, x2); y2 = min(img_h, y2) if x2 <= x1 or y2 <= y1: continue # 裁剪后框为空,说明原始标注有问题 x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt, "w", encoding="utf-8") as f: f.write("\n".join(lines)) # 使用时传入图片实际宽高,不要用 xml 里的假设值 convert_one("annotations/tomato_001.xml", 1280, 960, "labels/tomato_001.txt")逻辑说明:代码遍历每个 object 节点,从 bndbox 拿四角像素坐标,裁剪到图片边界内再归一化。裁剪很重要,人工标注经常把框画出图片边缘,不裁会让后续数据增强在越界坐标上产生空 box。类别 ID 由 class_map 映射,不在表里的名称直接跳过,便于反向排查数据里有没有未知病害类别。
参数说明:输出精度保留 6 位小数,够 YOLO 使用;如果手里是 COCO 的 json 格式,需要换 pycocotools 读取,换算公式一样。四个边界坑提前说清楚:一是 xml 里的图片尺寸可能是 0 或错误值,转换前先查实际尺寸;二是坐标越界要裁剪而不是直接丢弃,丢弃会丢目标;三是不同数据集里 bndbox 字段名可能写错,写成 xmin、ymax 之外的名字会直接 KeyError;四是一张图可能包含多个同名 object,代码里的 for 循环天然支持多目标,别只取第一个。
3.3 标注可视化自查:把 txt 画到图上,60 秒内发现问题
格式正确不等于标注正确。最有效的自查方法就是把标签框画回原图,逐张翻看。下面这段代码读一张图和同名 txt,用 OpenCV 画出目标框和类别 ID,输出到检查目录,跑一遍就能看出坐标是否错位、框是否框住了整片叶、类别是否标错。
# check_annotations.py import os import cv2 img_dir = "dataset/images/train" label_dir = "dataset/labels/train" out_dir = "check_output" os.makedirs(out_dir, exist_ok=True) for img_name in os.listdir(img_dir): stem = os.path.splitext(img_name)[0] label_path = os.path.join(label_dir, stem + ".txt") if not os.path.exists(label_path): continue img = cv2.imread(os.path.join(img_dir, img_name)) img_h, img_w = img.shape[:2] with open(label_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue # 行格式不对,直接跳过 cls_id, xc, yc, w, h = parts cls_id = int(cls_id); xc = float(xc); yc = float(yc) w = float(w); h = float(h) x1 = int((xc - w / 2) * img_w) y1 = int((yc - h / 2) * img_h) x2 = int((xc + w / 2) * img_w) y2 = int((yc + h / 2) * img_h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, str(cls_id), (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(os.path.join(out_dir, img_name), img)逻辑说明:脚本按文件名前缀找对应标签,把归一化坐标还原成像素坐标后用 cv2.rectangle 画框,cv2.putText 在框上方写类别 ID,处理完保存到 check_output。如果发现大量框偏到叶片外侧,通常是标签归一化公式错了;如果框位置基本正确但类别 ID 对不上病害,说明 class_map 映射写歪了。
参数说明:建议把检查图片放到统一目录里用看图工具快速翻页。一次性检查几百张图时,可以把图片先缩到 640 宽度再保存,减少翻页体量。这一步不产生任何模型收益,但能省掉后面无数次调试时瞎猜的时间,是整条流程里性价比最高的一步。
4. 训练落地:用 PyTorch 把训练跑顺,记住这组参数
数据准备好了,接下来是把训练脚本跑起来。这里最大的风险不是模型不懂番茄叶片,而是环境装不好、参数设置不合理、训练中断后不知道怎么续上。建议严格按“环境 → 配置 → 监控”三步走,每一步只做最小必要的事,不要一上来就去调花里胡哨的训练技巧。
4.1 环境准备:conda 虚拟环境与依赖安装
为了不让系统里已有的 Python 包污染项目,我一般先建独立的 conda 环境。下面这段命令在终端里按顺序执行。
conda create -n tomato python=3.10 -y conda activate tomato pip install torch torchvision pip install ultralytics逻辑说明:第一行创建名为 tomato 的 Python 3.10 虚拟环境,第二行激活,第三行安装 PyTorch 和配套 torchvision,第四行安装 ultralytics——这个库封装了 YOLO 系列的训练、验证、导出接口。torch 和 torchvision 需要配套版本,直接 pip 安装通常会自动匹配,但要确认 torch 能用 GPU,方法是在 Python 里执行 import torch; print(torch.cuda.is_available()),输出 True 才说明 CUDA 可用。
参数说明:Python 3.10 是这几年 PyTorch 兼容性最稳的版本之一,纯小白照抄即可;如果机器是纯 CPU,不装 CUDA 版 torch 也能跑,只是慢很多。Windows 上最容易翻车的点有两个:一个是缺 VC++ 运行库,报错文字里出现 msvcp140.dll,装一下运行库就好;另一个是显卡驱动太旧,torch 报“CUDA driver version is insufficient”,去显卡厂商官网更新驱动即可,不用自己乱改 torch 版本。装完后建议跑一下 yolo --version 确认命令行可用,很多环境问题在这一步就能暴露。
4.2 数据集配置与训练命令:参数表里哪些是安全区,哪些是玄学
ultralytics 的训练接口要求先用一个 yaml 文件描述数据路径和类别信息。文件名随意,结构固定。假设只做四类:健康叶片、早疫病、晚疫病、细菌性斑点,内容如下。
# tomato.yaml path: dataset train: images/train val: images/val nc: 4 names: 0: healthy 1: early_blight 2: late_blight 3: bacterial_spot逻辑说明:path 是数据集根目录,train 和 val 是相对路径,nc 是类别总数,names 给每个类别 ID 一个名字。训练脚本读这个文件就知道去哪里找图片、验证集用哪份数据、输出多少个类别。最容易翻车的配置是 path 写成绝对路径,换机器后路径失效;相对路径则要求执行训练命令时的工作目录在数据集上层,我会在命令前用 cd 进入项目根目录来规避。
训练命令如下:
cd tomato_project yolo detect train model=yolov8n.pt data=tomato.yaml \ epochs=80 imgsz=640 batch=8 device=0 workers=4 \ lr0=0.01 patience=15逻辑说明:model=yolov8n.pt 表示加载官方预训练权重做迁移学习,n 是 nano 版本,显存占用小、适合入门;data=tomato.yaml 指向刚才的配置;epochs=80 是完整遍历训练集 80 次;imgsz=640 让图片缩放成 640x640 再进网络;batch=8 是每次送入 GPU 的图片数量;patience=15 表示验证指标连续 15 个 epoch 不提升就提前停止,防过拟合还能省时间。
| 参数 | 我的默认值 | 影响 | 说明 |
|---|---|---|---|
| model | yolov8n.pt | 模型容量与速度 | 显存小用 n,数据多再换 s 或 m |
| epochs | 80 | 训练轮数 | 几百张图时 80 够用 |
| imgsz | 640 | 输入分辨率 | 分辨率越高越吃显存,小目标多再调 960 |
| batch | 8 | 单轮显存占用 | 6GB 显存安全区是 8,溢出就减半 |
| lr0 | 0.01 | 初始学习率 | 迁移学习时 0.01 是保守安全值 |
| patience | 15 | 早停耐心值 | 验证指标不升就停,防无效等待 |
参数说明:imgsz 和 batch 的乘积直接决定显存占用,6GB 显存跑 640+8 是比较稳的搭配;如果数据里叶片只占很小一块,可以试 imgsz=960,但 batch 要降一半。lr0 是最容易踩的玄学参数:调大了 loss 早期爆炸,调小了 40 轮还在原地晃,迁移学习场景下 0.01 基本不会出错。显存溢出时优先把 batch 减半而不是换更小的模型,因为 batch 减半不影响模型表达能力,只是梯度估计噪声变大。
4.3 训练过程怎么监控:从 loss 数值到 best.pt 的完整闭环
训练开始后,终端会打印每个 epoch 的进度条和一堆指标,要有取舍地看。第一优先看 box_loss 和 cls_loss,前者是定位损失,后者是分类损失,两者都应随 epoch 数增加而下降,下降速度变缓是正常现象。第二看 val 行的 mAP50,这是最能说明模型真实水平的数字,不要只看最终值,要关注它在训练中期的爬升趋势。
训练结束时,runs/detect/train 目录下会出现两个权重文件:best.pt 是验证指标最好的权重,last.pt 是最后一轮权重。默认用 best.pt,不要只拿 last.pt 做推理,因为训练后期可能出现轻微过拟合,best.pt 才是泛化能力最强的快照。中断恢复也有后悔药:训练中途崩了,在命令最后加 resume=True,脚本会从 last.pt 断点继续,不用从头再来。
如果 mAP50 到某个 epoch 后突然掉一截,别急着加正则化,先回第三章把可视化自查再做一遍,看是不是训练集里混进了贴错标签的图。数据层面干净了,指标才会恢复正常。另外值得记的一个经验是:batch 小的时候 loss 曲线更抖,这是正常的,不用过度紧张;真正要警惕的是 loss 徘徊在高位完全不降,那种情况多半是标签坐标越界,回 3.2 小节的转换脚本检查边界裁剪逻辑。
5. 番茄叶片病害检测的典型翻车现场排查与避坑
训练和推理跑通之后,真正的疑难杂症才开始浮出来。下面五条是从实际项目里反复遇到、且在新手身上出现频率最高的问题,按现象、原因、解决的顺序写,每一条都尽量给一个能直接抄的判断路径,方便你在训练失败时按图索骥。
5.1 显存溢出:不只是换显卡就能解决
现象:训练跑到十几个 epoch 后,进程突然报 CUDA error: out of memory,前面十几个 epoch 的进度全部作废。
原因:最典型根源是 batch 和 imgsz 的乘积超过显存上限,尤其是带数据增强的 YOLO 训练,显存峰值往往出现在增强后的大图堆叠阶段,而不是推理阶段。有时候 6GB 显存跑 batch=8 能启动、却在某 epoch 中途溢出,是因为增强操作随机生成了比平时更大的输入图,瞬时峰值比平均值高出一截。
解决:先把 batch 减半到 4,同时把 imgsz 降到 512,大多数 6GB 卡能稳定跑完;如果还溢出,再看 workers 是否太大,workers=4 会让数据加载线程同时准备多批数据,显存和内存都被推高。改完参数重新训练,因为 best.pt 还没生成,没有断点可以利用。这条经验的关键是:显存溢出不是换显卡的信号,先把参数调回安全区再说。
5.2 标注框口径不统一:模型学的到底是叶片还是病斑
现象:训练完的模型在真实照片上表现尚可,但预测框总是只框住病斑区域而不是整片叶;或者反过来,该框病斑却框了整片叶子。
原因:这是标注阶段埋下的锅。同一批数据里,一部分人标整片叶,另一部分人只标病斑,模型在矛盾中学会了“最小化损失”的折中方案,输出框忽大忽小。整片叶占图面积大、病斑占图面积小,两者混合会让置信度分布非常奇怪。
解决:没有捷径,只能回标注环节统一口径。建议定成“目标是叶片,类别是病害”;如果真正关心病斑面积占比,就定成“目标是病斑,类别是病害类型”。定好之后,把不符合的标注批量改成统一规则,再跑一次 3.3 小节的可视化自查,确认每个框都符合新口径再重新训练。这个问题越早发现代价越小,训练完才发现,等于前几十个 epoch 全部作废。
5.3 类别严重不平衡:整体指标还行,细看某个类别几乎不检
现象:训练完看验证结果,mAP50 有 0.7,但按类别拆开统计,晚疫病这一类精确率和召回率双双接近 0,模型把所有相似目标都判成了早疫病。
原因:数据集中晚疫病样本只有早疫病的十分之一。目标检测的损失函数在加权求和时,样本多的类别贡献的梯度更大,模型很容易倾向把所有相似纹理都判成大类。病害之间的视觉相似性会放大这个问题,早疫病和晚疫病的同心环差几层纹理细节,样本不足时模型根本学不到区分特征。
解决:优先做数据扩充,找更多晚疫病照片或做旋转、亮度、雨露等增广,把数量拉到一个相对均衡的水平;确实找不到素材,再调整损失权重,比如给 cls loss 按类别占比反比加权,或者换 focal loss 让模型更关注难分的少数类。我的经验是:调 loss 权重只能把指标从几乎为零拉回“勉强能用”,真正有效的还是数据量本身。
5.4 loss 不降:先查标签再调学习率
现象:训练到第 30 轮,box_loss 和 cls_loss 都停在一个较高水平不再下降,验证集 mAP50 也在低位震荡,调 epochs 也没用。
原因:排在前列的三类原因分别是:标签坐标越界或归一化错误、学习率过高导致 loss 反复震荡、模型容量不足。最常见的是第一种,因为转换脚本如果没做边界裁剪,x_center 和 w 会偶尔出现大于 1 或小于 0 的数值,模型看到的标签和实际目标框对不上,自然学不下去。
解决:不要先动模型,先执行第三章的可视化自查脚本,逐张看标注框是否贴合叶片。框全部正常,再检查 lr0,把 0.01 降到 0.002 重跑;数据量大且模型是 yolov8n,可以换 yolov8s 提高容量。这三步按顺序排查,能覆盖九成“loss 不降”的现场。很多人一上来就换高级损失函数或加注意力模块,但在这类项目里,数据问题占比远高于模型能力问题,先把数据钉死再谈模型。
5.5 推理阶段误检满天飞:把 conf 阈值调高只是治标
现象:模型在训练集上的指标很好看,但把训练时没见过的真实照片喂进去,预测结果里到处是框,连土壤、茎秆甚至背景纹理都被标注成病害。
原因:训练正样本里几乎全是“叶片居中、背景干净”的照片,模型没见过不带叶子的负样本,于是在推理阶段对一切有纹理的区域都打了高分。这是数据分布和真实场景不匹配造成的问题,不是单纯过拟合。
解决:治本的手段是往训练集里加入背景图片作为负样本,再配合 mosaic 和 mixup 增强,让模型学会忽略无关区域。治标的手段是把推理时的 conf 参数调高,例如从默认的 0.25 提到 0.5,误检数量会明显下降,但会牺牲一部分真目标召回。建议一起做:先用 conf=0.5 把眼前误检压下去,同时补一批带背景的负样本重新训练,双管齐下才算真正解决。
提示:这五条经验的共同点是,现象在训练或推理中暴露,根源却都在数据或参数上。遇到问题按“数据 → 参数 → 模型”的顺序排查,不要一上来就换网络结构。
6. 把模型从“能跑”变成“能用”:验证指标与落地前的最后一步
训练完成不代表项目结束,模型要在没见过的真实照片上证明自己。验证阶段最有参考价值的数字是 mAP50 和混淆矩阵,比单独看训练 loss 诚实得多。
yolo detect val model=runs/detect/train/best.pt data=tomato.yaml这条命令会在验证集上跑一遍最优权重,打印 box_loss、mAP50、mAP50-95 等指标,同时生成混淆矩阵图。混淆矩阵的阅读方法是看对角线,对角线数值越高,说明每个类别分得越清楚;如果早疫病和晚疫病之间出现大片非对角区域,说明这两个类别在特征空间里确实太接近,优先回 5.3 小节补数据。
推理阶段的验证要换一批照片,最好用手机在真实温室拍几张,分别覆盖单叶片、多叶片、背景杂乱、光线不均四种情况。下面这段代码可以快速做批量预测并保存结果。
from ultralytics import YOLO model = YOLO("runs/detect/train/best.pt") results = model.predict( source="real_photos/", conf=0.45, save=True, imgsz=640, project="predict_output" )逻辑说明:model 加载训练好的 best.pt,source 指向真实照片目录,conf=0.45 比默认值略高以过滤误检,save=True 保存画好框的结果图,project 指定输出目录。参数说明:如果真实场景里叶片占整图比例很小,imgsz 可调到 960 再试一次,代价是推理变慢,但小目标检出率通常会提高。这里有一个我从第一次做叶片检测就记住的习惯:永远同时保存两份验证输出,一份按默认参数,一份按高 conf,对比着看模型的边界在哪里。如果两份输出差异极大,说明模型对置信度非常敏感,部署时要仔细标定阈值。
这套流程走完,这个基于 python 深度学习的番茄叶片病害检测项目才真正从“代码能跑”变成了“检测能信”。我早年第一次做完类似项目时,花在换模型上的时间远超花在查数据上的时间,结果每个模型的指标都类似地差。后来养成先做数据自查、再看混淆矩阵、最后用真实照片兜底的顺序,返工次数骤降。希望这个顺序也能帮你在番茄叶片病害检测上少走几步弯路。
本文还有配套的精品资源,点击获取