简介:面向需要构建农业病虫害检测训练集的算法工程师与科研人员,这份大型植物叶片病害缺陷检测数据集覆盖29类常见叶片病害,包含葡萄叶黑腐病、番茄叶菌斑、苹果锈叶病、马铃薯晚疫病等典型类别,并按VOC格式组织训练集与验证集。训练集含2345张图片及对应XML标注,验证集含239张图片及对应XML标注,边界框信息完整,可直接接入YOLO、Faster R-CNN等目标检测模型训练;压缩包共2000个文件,以XML标注文件为主,另附类别标签JSON字典和可视化Python脚本,整体约935MB。JSON字典便于类别名与标签ID映射,可视化脚本无需修改,随机传入一张图片即可绘制边界框并保存结果,方便快速核查标注质量。数据目录已按训练集、验证集分目录存放,已有255人学习下载,适合作为叶片病害检测实验的基准数据集或预训练数据。
1. 大型植物叶片病害检测数据集:29类VOC标注到底能解决什么问题
做农业视觉的人看到"29类病害缺陷检测"这个规模,第一反应通常是:终于不用自己攒数据了。植物叶片病害检测在落地时最大的障碍不是模型选型,而是标注数据的获取成本。这个数据集把VOC标注格式、训练集和验证集划分、类别json文件以及可视化脚本一次性配齐,意味着你拿到手之后可以直接开始验证标注质量、跑通目标检测训练链路,而不是先花两周清洗标注文件。它适合三类人:第一次做目标检测、想用现成数据跑通全流程的新手;需要微调模型做农业病害识别的算法工程师;以及团队在投入标注预算前,想先评估数据质量和类别分布是否值得做的评估人员。这篇文章顺着数据集的结构、转换步骤、可视化验证和训练踩坑展开,最后给出一套能落地的提点方案。
2. 拆解VOC标注格式:29类叶片病害数据集里到底有什么
2.1 VOC格式的XML标注文件:核心字段与读取方法
VOC标注格式的核心是一个图像对应一个同名的XML文件,XML里记录图像尺寸、文件名、以及每个目标物体的类别和边界框坐标。很多新手拿到数据集后第一件事就是转YOLO格式,结果转换脚本报错,最后发现是没搞清楚XML字段结构。XML里的字段看起来多,但真正参与训练的只有三个部分:size节点里的图像宽高、object节点里的name(类别名)和bndbox(四个坐标值)。
建议在转换之前先写一段脚本把全部标注文件扫描一遍,输出每个XML的类别列表、框数和图像尺寸,这样能提前发现数据层面的问题。下面是我常用的快速盘点脚本:
import xml.etree.ElementTree as ET from pathlib import Path def inspect_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) objects = [] for obj in root.iter('object'): name = obj.find('name').text.strip() bbox = obj.find('bndbox') xmin = int(float(bbox.find('xmin').text)) ymin = int(float(bbox.find('ymin').text)) xmax = int(float(bbox.find('xmax').text)) ymax = int(float(bbox.find('ymax').text)) objects.append((name, xmin, ymin, xmax, ymax)) return width, height, objects xml_root = Path('Annotations') for xml_file in list(xml_root.glob('*.xml'))[:8]: w, h, objs = inspect_voc_xml(xml_file) class_names = set(obj[0] for obj in objs) print(f'{xml_file.name}: {w}x{h}, {len(objs)}个目标, 类别: {class_names}')逻辑说明:这段脚本用ET.parse加载XML文件,逐层取size和object节点。坐标转换前先过float()再转int,是因为部分标注工具会把坐标写成带小数的形式,直接int('123.0')会抛出异常。参数说明:w和h是原始图像尺寸,在后续转YOLO格式时作为归一化分母使用,一旦读错,所有框的坐标都会偏移。len(objs)可以帮你快速判断是否存在空标注的XML。
2.2 类别JSON文件:29类的ID映射规则与方向确认
类别JSON文件在这个数据集里承担着"翻译官"的角色:XML里存的是类别名称字符串(比如"早疫病"),而YOLO等检测框架训练时需要的是整数类别ID。不过JSON文件的组织方式并不统一,常见有两种:一种是{"1": "类名1", "2": "类名2"}的数字到名称映射,另一种是{"类名1": 0, "类名2": 1}的名称到数字映射。在动手转换之前,必须先确认是哪种方向,否则后患无穷。
import json with open('classes.json', 'r', encoding='utf-8') as f: class_map = json.load(f) # 判断JSON映射方向 if all(isinstance(k, str) and isinstance(v, int) for k, v in class_map.items()): name2id = class_map id2name = {v: k for k, v in class_map.items()} elif all(isinstance(k, str) and isinstance(v, str) for k, v in class_map.items()): id2name = class_map name2id = {v: k for k, v in class_map.items()} else: raise ValueError('无法识别的类别JSON格式,需要人工确认') print(f'共 {len(name2id)} 个类别') print('前3个映射示例:', list(name2id.items())[:3])逻辑说明:这段脚本通过检查value的类型来判断JSON的映射方向。第一种{"类名": 0}是YOLO生态最常见的格式,键是类别名,值是整数ID;第二种{"1": "类名"}在COCO风格工具链中较多见。参数说明:如果JSON里的ID从1开始,而YOLO训练要求从0开始,转换时必须统一减1。29类病害中如果ID偏移一位,训练出来的模型会在混淆矩阵上表现出"整体错位一个类别"的诡异现象,而且很难排查。
2.3 训练集与验证集的划分逻辑:ImageSets/Main目录
VOC格式有一个容易被忽略的特点:训练集和验证集不是用目录区分的,而是通过ImageSets/Main目录下的train.txt和val.txt来指定。每个txt文件里每一行是图像文件名(不带扩展名),框架读取这两个文件构建训练和验证列表。拿到数据集之后,第一件事应该检查这两个文件是否有重叠,以及文件名是否都能在JPEGImages目录里找到对应图片。
# 检查train和val是否有重叠的文件名 sort ImageSets/Main/train.txt ImageSets/Main/val.txt | uniq -d # 检查train.txt中的名字是否有对应的jpg图片 while read name; do if [ ! -f "JPEGImages/${name}.jpg" ]; then echo "缺失图片: ${name}" fi done < ImageSets/Main/train.txt # 统计训练集和验证集样本数 wc -l ImageSets/Main/train.txt ImageSets/Main/val.txt命令说明:第一条用sort加uniq -d找出同时出现在训练集和验证集中的文件名,一旦有输出说明数据划分不干净,必须剔除重复项;第二条逐行检查训练集文件名是否能在JPEGImages里找到对应jpg,防止训练时出现"图片不存在"的随机中断;第三条统计样本量,评估数据规模是否支持29类训练。参数说明:如果你的图片扩展名不是jpg而是png或jpeg,把${name}.jpg改成对应的扩展名即可。
3. 把VOC标注转换为YOLO训练格式:坐标换算与边界处理
3.1 坐标换算公式:从像素绝对坐标到归一化相对坐标
VOC的bndbox记录的是像素绝对坐标,而YOLO系框架要求的是归一化后的相对坐标。换算公式如下:
x_center = ((xmin + xmax) / 2) / img_width y_center = ((ymin + ymax) / 2) / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height公式本身不难,但实际操作中有一个高频问题:标注框越界。当标注框的一部分超出了图像边界,比如xmax > img_width,归一化后宽度可能大于1.0,YOLO训练时会导致loss出现NaN或者模型无法收敛。因此在转换时,必须先对坐标做一次裁剪(clip),把框拉回图像有效范围内。
3.2 一个可直接复用的VOC转YOLO脚本
import xml.etree.ElementTree as ET from pathlib import Path import json def voc2yolo(xml_file, class_name2id, output_txt_path, img_width, img_height): tree = ET.parse(xml_file) root = tree.getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in class_name2id: print(f'警告: {xml_file.name} 中存在未知类别 {name}') continue bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) # 关键一步: 把越界坐标裁剪回图像范围内 xmin = max(0, min(xmin, img_width - 1)) ymin = max(0, min(ymin, img_height - 1)) xmax = max(0, min(xmax, img_width - 1)) ymax = max(0, min(ymax, img_height - 1)) # 裁剪后如果是空框则跳过 if xmax <= xmin or ymax <= ymin: print(f'警告: {xml_file.name} 中 {name} 裁剪后为空框,跳过') continue x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height class_id = class_name2id[name] lines.append(f'{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') if lines: with open(output_txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) # 使用示例 with open('classes.json', 'r', encoding='utf-8') as f: class_map = json.load(f) # 假设classes.json格式是 {"类名": 0} class_name2id = class_map xml_dir = Path('Annotations') txt_dir = Path('labels') txt_dir.mkdir(exist_ok=True) from PIL import Image for xml_file in xml_dir.glob('*.xml'): img_name = xml_file.stem + '.jpg' img_path = Path('JPEGImages') / img_name with Image.open(img_path) as im: img_w, img_h = im.size output_txt = txt_dir / (xml_file.stem + '.txt') voc2yolo(xml_file, class_name2id, output_txt, img_w, img_h)逻辑说明:脚本按XML逐个object解析,先裁剪越界坐标,再做归一化转换。裁剪的目的是防止YOLO训练出现NaN loss;空框过滤是为了避免极小目标在裁剪后变成零面积框,这类框会让损失函数无法计算。参数说明:class_name2id必须和训练时的类别配置完全一致,尤其注意ID从0开始。保留6位小数足够,更多的精度不会带来训练收益,反而让txt文件变大。PIL.Image读取尺寸是最稳妥的做法,比读取XML里的size节点更可靠,因为有时XML记录的尺寸和实际图片不一致。
3.3 转换后的人工抽检:用可视化确认没有转错
转换脚本跑完千万别直接进训练。先随机抽5到10张图,把YOLO格式的txt读回来画到原图上,肉眼确认框的位置、大小和类别标签是否正确。这一步能拦住多数转换事故,比如坐标顺序写反、类别ID整体偏移、宽高互换等。
import cv2 def draw_yolo_boxes(img_path, txt_path, id2name, conf_threshold=0.0): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path, 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f'跳过异常行: {line}') continue cls_id, xc, yc, bw, bh = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) color = (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) label = id2name.get(cls_id, f'ID_{cls_id}') cv2.putText(img, label, (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) return img sample_img = draw_yolo_boxes('JPEGImages/leaf_001.jpg', 'labels/leaf_001.txt', id2name) cv2.imwrite('check_leaf_001.jpg', sample_img) print('抽检图已保存为 check_leaf_001.jpg')逻辑说明:这段脚本读取YOLO txt里的归一化坐标,反算回像素坐标后画框。反算公式和正向转换互逆,任何一处出错都会在这步暴露。参数说明:id2name是第2.2节里得到的反向映射;如果画出的框全部偏到图像角落或尺寸异常,优先检查归一化时分母用的是不是原图宽高,而不是模型输入尺寸(比如640×640)。
4. 可视化脚本的三种用法:不只看画框效果
4.1 全面检查标注质量:类别分布与空标注文件
数据集自带的可视化脚本通常只是画几个框让你"看个样子",但真正的标注质量检查需要更多维度。我通常会改造脚本,让它输出类别分布统计和空标注文件清单。这两项数据直接决定后续训练策略——类别分布是否长尾,空标注文件是否需要在训练时踢掉。
import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter xml_dir = Path('Annotations') class_counter = Counter() empty_xml_list = [] for xml_file in xml_dir.glob('*.xml'): tree = ET.parse(xml_file) root = tree.getroot() objs = list(root.iter('object')) if len(objs) == 0: empty_xml_list.append(xml_file.name) continue for obj in objs: name = obj.find('name').text.strip() class_counter[name] += 1 print(f'标注文件总数: {len(list(xml_dir.glob("*.xml")))}') print(f'空标注文件数: {len(empty_xml_list)}') if empty_xml_list[:5]: print('空标注文件示例:', empty_xml_list[:5]) print(f'类别总数: {len(class_counter)}') print('类别分布Top10:', class_counter.most_common(10)) print('类别最少5个:', class_counter.most_common()[-5:])逻辑说明:脚本统计了类别实例数、空标注文件数和类别分布。29类叶片病害数据集通常呈现明显的长尾分布——少数几个常见病种占了大半标注,罕见病种只有几十个框。参数说明:空标注文件如果不剔除,训练时读入一张没有目标的图片会导致loss异常波动。class_counter.most_common()[-5:]取的是分布中最少的5个类别,用来评估数据均衡性。
4.2 按类别抽检图像:肉眼检查同类别内部一致性
叶片病害的类别之间视觉差异很小,比如早疫病和晚疫病在早期阶段几乎难以区分。即便标注工作做得再仔细,也难免出现少量标错、标串的样本。一个实用的做法是:按类别从训练集中各抽9张图片拼成网格图,集中检查同一病害类别内部的视觉一致性。
import cv2 import random from pathlib import Path import numpy as np # 构建类别 -> 图片路径 的映射 class_to_images = {} with open('ImageSets/Main/train.txt') as f: train_names = [line.strip() for line in f if line.strip()] # 用XML读取每个训练图片的类别 import xml.etree.ElementTree as ET for name in train_names: xml_path = Path('Annotations') / f'{name}.xml' if not xml_path.exists(): continue tree = ET.parse(xml_path) root = tree.getroot() classes_in_img = set() for obj in root.iter('object'): classes_in_img.add(obj.find('name').text.strip()) for cls in classes_in_img: class_to_images.setdefault(cls, []).append(str(Path('JPEGImages') / f'{name}.jpg')) def build_class_grid(class_name, img_list, grid_cols=3, cell_size=224): rows = (len(img_list) + grid_cols - 1) // grid_cols canvas = np.ones((rows * cell_size, grid_cols * cell_size, 3), dtype=np.uint8) * 255 for idx, img_path in enumerate(img_list[:rows * grid_cols]): img = cv2.imread(img_path) img = cv2.resize(img, (cell_size, cell_size)) r, c = idx // grid_cols, idx % grid_cols y1, x1 = r * cell_size, c * cell_size canvas[y1:y1 + cell_size, x1:x1 + cell_size] = img cv2.putText(canvas, f'{class_name}_{idx}', (x1 + 5, y1 + 18), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 1) cv2.imwrite(f'grid_{class_name}.jpg', canvas) # 对每个类别生成一张抽检网格图 for cls_name, img_list in class_to_images.items(): build_class_grid(cls_name, img_list) print(f'{cls_name}: 生成网格图,样本数 {len(img_list)}') print('所有类别的网格抽检图已生成')逻辑说明:脚本按类别收集训练图片,每个类别最多取9张拼成一张网格图。通过肉眼对比同一类别的多张图,可以发现标注错误和样本分布问题。参数说明:cell_size控制每张缩略图的边长,默认224像素足够看清病斑纹理;如果类别图片少于3张,网格图会留有空白区域,这本身就是一个信号——该类别样本量不足,训练时需要处理长尾问题。
4.3 检查图像与标注文件是否一一对应
数据量一大,偶尔会有几张图片没有标注文件、或者标注文件指向的图片缺失。训练时这类问题表现为"随机中断",并且不是每次都能复现,排查成本极高。建议在训练前用脚本做一次全量配对检查,把问题一次性暴露出来。
# 找出没有标注文件的图片 for img in JPEGImages/*.jpg; do name=$(basename "$img" .jpg) [ -f "Annotations/${name}.xml" ] || echo "缺标注: ${name}" done # 找出没有对应图片的标注文件 for xml in Annotations/*.xml; do name=$(basename "$xml" .xml) [ -f "JPEGImages/${name}.jpg" ] || echo "缺图片: ${name}" done命令说明:第一段遍历JPEGImages下所有jpg,检查同名的XML是否存在,缺失的输出到终端;第二段反向检查。参数说明:如果你发现缺失的只是个别几个文件,手动补或删都行;如果缺失几十个,建议写脚本批量处理,避免误删正常文件。这个检查在每次拿到新数据集时都要做一遍,不要跳过。
5. 避坑指南:29类叶片病害数据集训练的7个常见陷阱
5.1 坑一:XML里存在difficult和truncated标注
现象:训练时Loss曲线正常下降,但mAP始终在某个低值徘徊,验证集上频繁出现"预测框被忽略"的情况。
原因:VOC标准里除了正常的目标框,还有difficult和truncated两个字段。difficult=1表示该目标极难辨认,truncated=1表示目标被图像边界截断。如果你的转换脚本没有处理这两个字段,会把难以辨认或被截断的目标也当正样本喂进去。在叶片病害数据集里,早期病斑在叶片边缘被截断很常见,这条坑基本必踩。
解决:转换脚本里加过滤——遇到difficult=1或truncated=1的object直接跳过,不生成训练样本。具体在XML解析时增加一个判断:
# 在遍历object时增加过滤 difficult = obj.find('difficult') if difficult is not None and difficult.text == '1': continue truncated = obj.find('truncated') if truncated is not None and truncated.text == '1': continue5.2 坑二:27类还是29类?类别ID对不齐导致静默出错
现象:训练日志显示29类,但验证时mAP只有预期的一半,混淆矩阵中出现"某个类预测成另一个完全不相似的类"的均匀混淆块。
原因:类别JSON的映射方向判断错误,或者ID从1开始时没有统一减1。比如JSON里写{"1": "早疫病", "2": "晚疫病"},但脚本读入后直接用enumerate重建,导致早疫病变成ID=0,晚疫病变成ID=1,而原始定义是早疫病=1、晚疫病=2,整体错位一位。29类病害中不少是近缘病害,错位后混淆矩阵看起来像"模型没学好",其实是标签本身错位了。
解决:不要用enumerate重建映射,直接以原始JSON为准。如果JSON的ID从1开始,在构建name2id时统一减1。转换后务必打印前5个类别的ID和名称对,核对ID=0对应的是哪个类别。叶片病害数据集里,通常ID=0是健康叶片或者最常见的病害,如果ID=0对应的是个冷门病害,大概率映射反了。
5.3 坑三:训练集和验证集存在同一植株的重复图片
现象:训练mAP在20轮后超过0.9,验证mAP却连0.5都上不去。反复调参无效,甚至调大训练集也没变化。
原因:叶片病害数据采集时,通常对同一株植物的多片叶子、同一片叶子的多个角度拍多张照片。这些照片被随机分到了训练集和验证集,导致验证集与训练集高度相似——模型在训练时"记住"了纹理细节,验证时遇到同一片叶子的又一个角度,反而表现不佳。这属于数据泄漏的一种形式,在植物病害场景比通用目标检测更常见。
解决:按"采集批次"分组划分数据,而不是按单张图片随机划分。具体做法:先按图片文件名的前缀聚类(比如同一片叶子编号leaf_023下有多张不同角度的图片),把同组图片全部放入同一个集合后再划分。需要你去看文件名规律来决定分组粒度。
5.4 坑四:长尾分布导致小类目被模型"放弃"
现象:29类中频次最高的类有几千样本,最低的类别可能只有几十个框。训练后期低样本类别的precision趋近于0,且Loss曲线在尾部反复震荡。
原因:默认的损失函数对各类别一视同仁。当某类样本极少时,模型每轮只能看到几个该类的正样本,梯度更新次数严重不足,效果自然差。叶片病害数据集中,常见病种和不常见病种的样本量可能相差两个数量级,这是农业数据的常态。
解决:最简单的做法是给低样本类别更高的损失权重。在YOLO的配置文件中,把loss权重按类别频率倒数归一化后分配;或者更直接地在训练时对低样本类别做过采样,把它们的图片重复读取几次,人为提升曝光度。先试过采样,因为它不需要改训练代码,只在数据集层面操作。
5.5 坑五:小目标占比高导致漏检严重
现象:验证结果中大斑病害检测得很好,但早期小斑点类别的召回率只有0.2左右。
原因:叶片病害的早期症状是直径只有几个像素到十几像素的小斑点,经过检测模型的下采样后,在特征图上可能只占极少的网格区域。29类里有相当一部分属于"小斑型"病害(比如细菌性斑点、锈病早期),它们尺度小、对比度低、和叶脉纹理难以区分。
解决:把输入分辨率从默认的640×640提高到960×960或1280×1280,并在训练配置里开启多尺度训练(比如scale=0.5, 1.5)。如果计算资源允许,在模型配置中增加小目标检测头。实测在叶片小斑类病害上,输入分辨率从640提高到960,小目标类别的mAP能提升3到5个点,代价是训练时间变长。
5.6 坑六:可视化脚本掩盖了标注错位
现象:脚本画出来的框看起来都在目标上,训练效果却差。仔细看才发现框的位置没问题,但类别文字标注和框不匹配。
原因:部分标注工具生成的XML里,name字段自带特殊字符,比如全角空格或者换行符。转换脚本在strip()之前就把原始字符串写进txt,导致类别匹配失败。可视化画框时颜色按ID分配,错位的类别显示成不同颜色的文字,肉眼如果不逐字核对,根本不会注意。
解决:转换脚本里统一做name = obj.find('name').text.strip(),并且在构建name2id时同样处理。转换完用可视化脚本抽查至少20张图,重点看类别文字是否与框内病斑一致。
5.7 坑七:直接拿VOC格式训练,忘了做数据集格式转换
现象:网上找到的代码仓库明明是YOLO训练脚本,自己却看着VOC格式的数据集不知所措;或者干脆把VOC的XML路径直接填进训练配置,报错后一脸懵。
原因:VOC和YOLO是两套不同的标注体系。VOC用XML存像素坐标,YOLO用txt存归一化坐标。如果训练脚本只认YOLO格式,你必须先把数据转换过去。有人会觉得"框架里不是有自动转换功能吗",但多数框架的自动转换只针对标准COCO数据集,自定义的VOC数据还是要自己写转换脚本。
解决:用第3.2节的脚本把数据转成YOLO格式,这是最普遍、最稳妥的做法。转换后把图片和txt整理成如下结构:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── classes.json └── data.yamldata.yaml内容示例:
path: ./dataset train: images/train val: images/val nc: 29 names: ['类名0', '类名1', '类名2', ..., '类名28']参数说明:nc必须与names列表长度一致,顺序必须与转换脚本里的class_name2id一致。这一步是整个训练前最值得花时间核对的地方。
6. 进阶技巧:用这个VOC数据集把mAP稳定拉高
6.1 训练完第一版后,用混淆矩阵反向定位难分病害对
训练完第一版模型后,做一次验证集混淆矩阵分析。29类叶片病害中,真正难分的通常集中在2到3对病害上,比如早疫病和晚疫病、锈病和叶枯病。把混淆矩阵按行归一化后,找到互相错误分类最高的类别对,单独抽取它们的样本重新审视标注一致性——有时候是模型不够强,但更多时候是标注本身就不稳定。这一步花的排查时间不多,但通常能找到数据层面的硬伤。
6.2 针对29类病害做分组的级联检测
工程落地时,一个值得试的技巧:不要一次训29类,改成级联结构。第一级检测器负责判断"叶片上有没有病斑"并输出叶片区域,第二级检测器在病斑区域上做细粒度病害分类。两级级联在样本不均衡的农业数据上往往比单模型直接训29类更稳,缺点是推理时间翻倍,因为每张图跑两遍网络。如果你的场景是离线分析或者对实时性要求不高,优先试级联;如果在线推理且延迟敏感,先保单模型。
6.3 微调时冻结backbone前期层
在这个数据集上微调YOLO系模型时,一个比较实用的训练策略:前30轮冻结backbone前80%的参数,只训练neck和head部分。叶片病害特征在ImageNet预训练权重里没有对应的纹理概念,前期全量训练容易震荡;冻结backbone可以促使检测头先学会"找叶子",后期解冻再让backbone学"辨病害"。具体做法是在训练代码里给backbone参数的requires_grad设为False,30轮后恢复为True并把学习率降到之前的1/10,再训20轮。
结合第5.4节的过采样处理,这套流程在当前叶片病害数据上能够稳定提升mAP。但我还是想强调最基础的一步——每次改动数据或模型结构,都用第4章的可视化脚本重新检查一遍样本和标注。我早期几次训练翻车,基本都栽在"觉得数据肯定没问题,直接训"的侥幸上。先花半小时盘清XML和JSON的关系,再谈mAP,这一步做到了,后面就是相对平稳的调参过程。希望帮到你。
本文还有配套的精品资源,点击获取