news 2026/10/1 6:18:26

YOLOv5实战:苹果橘子梨三类别数据集标注与训练全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv5实战:苹果橘子梨三类别数据集标注与训练全攻略

简介:苹果、橘子、梨三种水果目标检测数据集,按YOLOv5目录格式整理,内含训练集与验证集,可直接用于YOLOv5系列模型训练,无需额外格式转换,适合目标检测入门练习和实际项目部署。数据集共2000个文件,主要由1397个txt标签文件、602个jpg图像文件及1个py可视化脚本组成,压缩包约226.87MB;图像为1080×810的RGB彩色图片,每张包含多个目标,边界框标注完整,可作为小型水果检测训练数据。训练集有1117张图片及对应标签,验证集有279张图片及对应标签,类别信息通过txt文件说明,目录结构清晰,图片与标签文件名一一对应,方便直接读取。附带可视化脚本可直接运行,随机传入一张图片即可绘制边界框并保存到当前目录,无需修改任何参数,便于快速检查标注质量。平台已有518人学习下载,资源上手门槛低,适合目标检测入门与实战。

1. 苹果橘子梨三类别数据集:把目标检测从黑匣子拉回地面

目标检测入门最崩溃的时刻,不是模型训练不起来,而是你下载了一个数据集,却不知道图片和标注该怎么组织才能喂给 YOLOV5。这个三种水果检测数据集就是能把这件事拉通的最小样例:类别只有 apple、orange、pear 三个,目标大、边缘清晰、互相遮挡少,训练集验证集已经按 YOLOV5 的约定分好。新手可以用它跑通「数据集 → 训练 → 验证 → 部署」的完整链路,老手可以拿它当试验田,验证标注转换脚本、超参数改动和迁移学习策略。下面按我平时拿到这种数据集后动手的顺序,从目录结构、标注格式、划分逻辑到踩坑记录一次讲清楚。

2. 先搭YOLOV5目录结构:把三种水果的图片和标签放进该放的位置

2.1 images 与 labels 双轨目录:YOLOV5 数据集的标准骨架

YOLOv5 训练自己的数据集时,对目录结构的要求其实很死板,但也因此很省心。标准骨架是把图片和标签放在两个互相平行的目录树里,图片归图片,标签归标签,靠文件名一一对应。

datasets/ └── fruit/ ├── images/ │ ├── train/ │ │ ├── apple_001.jpg │ │ ├── orange_002.jpg │ │ └── pear_003.jpg │ └── val/ │ ├── apple_011.jpg │ └── ... └── labels/ ├── train/ │ ├── apple_001.txt │ ├── orange_002.txt │ └── pear_003.txt └── val/ ├── apple_011.txt └── ...

注意两个关键点。第一,images 和 labels 下的训练目录名不必完全一致,YOLOv5 实际上只认 data.yaml 里配置的路径,但社区惯例是 train 对 train、val 对 val,你按这个惯例组织最不容易把自己绕晕。第二,同名图片和同名 txt 是一一对应的,apple_001.jpg 必须配 apple_001.txt,扩展名不同、前缀完全一致,YOLOv5 在加载时就是按这个前缀去匹配标签的。如果有人给你的压缩包里 images 和 labels 目录对不上,训练时会出现大量「WARNING: image without labels」日志,这就是形态没组织好的典型翻车。先建目录再放文件,是规避这类问题最省事的办法。

2.2 labels/train 里的 txt 长什么样:一行一个目标

YOLOv5 的标签文件不是 XML,也不是 JSON,而是纯文本 txt,每行代表一个目标框。拿 apple_001.jpg 对应的 apple_001.txt 来说,内容大致是:

0 0.6230 0.4521 0.1802 0.2345 1 0.1209 0.7811 0.1504 0.2600 2 0.5012 0.3333 0.2015 0.1789

这一真实格式里,每行五个数字分别是:类别编号、目标中心点 x 坐标、目标中心点 y 坐标、目标框宽度、目标框高度。注意三点。类别编号从 0 开始计数,这里的 0 对应 data.yaml 里 names 列表的第一个名称,1 对应第二个,2 对应第三个,顺序由你在 names 里定义的顺序决定;坐标全部是归一化到 0~1 之间的浮点数,也就是用像素坐标除以图片宽高,不是原始像素值;坐标是框中心点加宽高,不是左上角加右下角。这套约定的好处是图片无论缩放到多大,标签都不用改,训练时 YOLOv5 内部会统一做 scale 处理。

验证标签是否合法有个土办法:随便挑一张训练图,读一下图片宽高,把 txt 里的五个数乘回去,看能不能还原成框在图上正常范围内的合理位置。如果还原出来的 x、y 在 0~1 之外,或者宽高为负数,说明标注或转换脚本出了问题,直接拿这种标签去训练,loss 大概率发散。

2.3 用 Python 脚本把图片与标签同步拆进 train 和 val

很多三类别小数据集下载下来是图片和标签全混在一个目录里的,需要你自己划分。我一般会用按文件名散列、可复现的脚本拆,而不是纯随机抽,这样同一批数据无论跑几次,train/val 的成员都不会变。

import os import random import shutil random.seed(42) # 固定种子,保证每次划分结果一致 src_images = "fruit_source/images" src_labels = "fruit_source/labels" dst_root = "fruit" split_ratio = 0.85 # 85% 训练,15% 验证 img_files = [f for f in os.listdir(src_images) if f.lower().endswith((".jpg", ".png", ".jpeg"))] random.shuffle(img_files) split_idx = int(len(img_files) * split_ratio) for i, img_name in enumerate(img_files): prefix = img_name.rsplit(".", 1)[0] label_name = prefix + ".txt" is_train = i < split_idx split_name = "train" if is_train else "val" # 同步移动图片和标签 img_src = os.path.join(src_images, img_name) label_src = os.path.join(src_labels, label_name) img_dst = os.path.join(dst_root, "images", split_name, img_name) label_dst = os.path.join(dst_root, "labels", split_name, label_name) shutil.copy(img_src, img_dst) shutil.copy(label_src, label_dst) print(f"train: {split_idx}, val: {len(img_files) - split_idx}")

这个脚本看起来简单,但有两个细节很容易忽略。一是 label_src 必须用和图片相同的前缀去拼,所以图片是 PNG、标签 txt 的基名也要一致,常见翻车是图片叫 apple_001.JPG 大写后缀,txt 按小写拼出来,匹配不上;二是这里用了 copy 而不是 move,是因为我不想在划分阶段就把源数据破坏掉,等确认训练跑通了再统一删源目录也来得及。split_ratio按你的实际图片量调,图片总量超过 2000 张时用 0.9 也不怕,低于 300 张时不要分太低,验证集太少会导致 mAP 波动大得没法看。

2.4 data.yaml:train、val、nc、names 四行把数据连起来

YOLOv5 不靠硬编码扫描所有图片,而是通过 data.yaml 告诉训练程序去哪里找图、有几个类别、类别叫什么。fruit.yaml 最简版本是这样:

# data/fruit.yaml train: fruit/images/train val: fruit/images/val nc: 3 names: 0: apple 1: orange 2: pear

路径这里最容易有歧义。train和val的路径,是相对于你执行 train.py 时的工作目录来解析的。如果你在 yolov5 仓库根目录下执行python train.py --data data/fruit.yaml,那fruit/images/train就是仓库根目录下的fruit/images/train。如果你在别的位置执行,建议直接写绝对路径,或者写成../datasets/fruit/images/train这种相对路径。我见过太多人把数据集放在 D 盘,然后写train: E:/fruit/images/train,一旦换机器就全部失效,这类路径问题很玄学但几乎人人遇到过。nc必须和 names 列表长度一致,names 的默认顺序就是训练时类别编号的顺序,你有三种水果,那 0、1、2 的顺序就写死为 apple、orange、pear,千万不能中途换顺序。这套目录结构和 data.yaml 说明对 YOLOv8、YOLOv11 同样成立,新框架几乎都保留了这套数据约定。

3. 把标注转成YOLO格式:VOC、COCO与手工标注的落地转换

3.1 三种标注来源怎么选

拿到三类别水果数据集,标注形式不一定直接是 YOLOv5 txt。常见来源有三种:LabelImg 手工标注输出 VOC XML 格式;LabelMe 输出 JSON;公开检测数据集或 CVAT 导出常用 COCO json 格式。目标检测常用的标注工具对比大致如下:

标注工具输出格式转 YOLO 难度适用场景
LabelImgVOC XML低少量数据手工标注
LabelMeJSON(多边形)中需要分割或复杂形状
CVAT 导出COCO json中团队协作、批量标注
现成公开数据集COCO json低直接复用已有数据

对三种水果这种小目标、边界规则的数据集,LabelImg 人手标几百张是最快的。但无论哪种来源,最后你都得有一个统一的转换脚本。下面两节给出两种最常见格式的转换代码,我每次换数据集都是直接改类别名列表复用这两段。

3.2 从 COCO json 转 YOLO txt:类别顺序是第一个坑

COCO 格式里,每个标注是一个 bbox,形如[x, y, width, height],单位是像素,坐标是框左上角。转成 YOLO 需要换算成中心点加宽高、再除以图片宽高归一化。转换脚本里最大的坑是类别编号映射。

import json import os def coco_to_yolo(coco_json, img_dir, label_dir, class_names): with open(coco_json, "r", encoding="utf-8") as f: coco = json.load(f) # 关键:按类别名称排序,把 COCO 的 category_id 映射成连续的 0,1,2 cat_id_to_new = {} sorted_names = sorted(class_names) # 例如按苹果、橘子、梨的字母序 for new_id, name in enumerate(sorted_names): for cat in coco["categories"]: if cat["name"] == name: cat_id_to_new[cat["id"]] = new_id break img_id_to_info = {img["id"]: img for img in coco["images"]} anns_by_img = {} for ann in coco["annotations"]: anns_by_img.setdefault(ann["image_id"], []).append(ann) for img_id, anns in anns_by_img.items(): img_info = img_id_to_info[img_id] img_w = img_info["width"] img_h = img_info["height"] prefix = img_info["file_name"].rsplit(".", 1)[0] label_path = os.path.join(label_dir, prefix + ".txt") with open(label_path, "w", encoding="utf-8") as out: for ann in anns: new_id = cat_id_to_new[ann["category_id"]] x, y, w, h = ann["bbox"] # 左上角+宽高 -> 中心点+宽高,并归一化 x_center = (x + w / 2) / img_w y_center = (y + h / 2) / img_h w_norm = w / img_w h_norm = h / img_h out.write(f"{new_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n") coco_to_yolo("fruit_annotations.json", "fruit_images", "fruit_labels", ["apple", "orange", "pear"])

这里最重要的参数是class_names列表,它决定了你最终 YOLO 标签里每个数字代表什么。很多公开数据集的 category_id 不是从 0 开始,可能出现 apple 的 id 是 3、orange 的 id 是 1、梨的 id 是 7,YOLOv5 完全不认这种跳号,必须映射成 0、1、2。我按sorted(class_names)来做映射是为了让函数脱离具体数据集写死,但你也可以直接写死为一个固定字典,比如{"apple":0, "orange":1, "pear":2},效果一样。要注意如果 COCO 里的命名和你的 class_names 不完全一致,cat["name"] == name匹配不上,会导致部分标注被丢弃,转换后统计一下每个类别的框数量是最快的检查办法。

3.3 从 VOC XML 转 YOLO:归一化坐标的四个换算

LabelImg 默认输出 VOC 格式 XML,里面记录的是xmin, ymin, xmax, ymax,同样是绝对像素。转换的四个换算公式是固定的,我用 xml.etree.ElementTree 解析,不依赖额外库。

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_dir, label_dir, class_names): for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) prefix = xml_name.rsplit(".", 1)[0] label_path = os.path.join(label_dir, prefix + ".txt") lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue class_id = class_names.index(name) 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) # 四个核心换算公式 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w_norm = (xmax - xmin) / img_w h_norm = (ymax - ymin) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n") with open(label_path, "w", encoding="utf-8") as f: f.writelines(lines) voc_to_yolo("fruit_xmls", "fruit_labels", ["apple", "orange", "pear"])

这段代码里容易出问题的是 bbox 解析。部分标注工具的节点名不是bndbox,而是boundingBox或直接叫box,obj.find("bndbox")会返回 None,程序直接报错。另一个边界情况是xmin, xmax是像素小数,但 XML 里存成整数,除法没问题,唯独要注意个别工具把坐标存成相对宽高的百分比,这种情况下就不能再除以 img_w。转完后我习惯顺手跑一个循环,读取 txt 每行检查五个数是否都在 0~1 以内,有过界的立即定位到对应 XML 手查。

3.4 越界修正与空标签兜底:标注框不该落在图外

手工标注很容易把框拖到图片边缘外几个像素,或者因为误操作画出一个宽度为 0 的框,转换脚本如果不做兜底,后期训练会随机出 NAN。我一般在转换脚本末尾统一做一次清洗,代码不长但能省很多调试时间。

import os def clean_yolo_labels(label_dir, img_dir): for txt_name in os.listdir(label_dir): if not txt_name.endswith(".txt"): continue txt_path = os.path.join(label_dir, txt_name) with open(txt_path, "r") as f: lines = f.readlines() clean_lines = [] for line in lines: parts = line.strip().split() if len(parts) != 5: continue # 丢掉格式损坏的行 cls = int(parts[0]) xc, yc, w, h = map(float, parts[1:]) # 宽高必须为正,否则丢弃 if w <= 1e-6 or h <= 1e-6: continue # 将中心点和宽高裁剪到合法范围,并重新限制尺寸 xc = min(max(xc, 0.0), 1.0) yc = min(max(yc, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) clean_lines.append(f"{cls} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}\n") with open(txt_path, "w") as f: f.writelines(clean_lines) clean_yolo_labels("fruit_labels", "fruit_images")

这里的裁剪逻辑要说明一下:直接粗暴 clip 到 0~1 并不是最严谨的,因为中心点在 0~1 外但框本身有一大部分在图内的目标,clip 后框位置会失真。更好的做法是先判断中心点是否在图内,中心点越界但宽高很大的框直接丢弃;中心点在 0~1 内而框边略微越界,才用 clip 修正。上面这个脚本是按大多数三类别水果图写的「保底方案」,你先存下修改前的 txt 再跑,对比一下被修了哪些框,能帮你反过来发现标注工具的惯性问题。空标签文件也同样要兜底,如果一个 txt 清洗后一行都不剩,会造成 YOLOv5 加载时报空标签警告,这种图片最好直接从训练集里剔除,不要留着凑数量。

4. 划分训练集验证集:三种水果该怎么分才不翻车

4.1 按文件散列划分而不是按随机抽

第 2 章提到了用固定 random 种子的划分方式,实际项目里我更推荐按文件名散列来划分,因为不依赖random.shuffle对列表顺序的改动,哪怕未来往目录里新增图片,已经划出去的文件不会变。

import hashlib import os import shutil def hash_split(src_images, src_labels, dst_root, train_ratio=0.85): for img_name in os.listdir(src_images): if not img_name.lower().endswith((".jpg", ".png", ".jpeg")): continue prefix = img_name.rsplit(".", 1)[0] # 用 md5 散列的前两位来决定归属,稳定且可复现 digest = hashlib.md5(prefix.encode("utf-8")).hexdigest() is_train = int(digest[:2], 16) / 255 < train_ratio split_name = "train" if is_train else "val" src_img = os.path.join(src_images, img_name) src_lbl = os.path.join(src_labels, prefix + ".txt") shutil.copy(src_img, os.path.join(dst_root, "images", split_name, img_name)) shutil.copy(src_lbl, os.path.join(dst_root, "labels", split_name, prefix + ".txt")) hash_split("fruit_source/images", "fruit_source/labels", "fruit", 0.85)

散列划分的好处是,验证集不会因为哪次 shuffle 顺序变了而变化。但用md5[:2]这种方式时,样本量小到 100 张以内,划分比例可能偏离 85% 比较多,因为散列值的分布并不保证每 100 个文件均匀 0~255。样本量超过 500 张时基本稳定。如果你想严格保证比例,那还是回到第 2 章的 random.seed 版本,两者各有侧重:散列法侧重增量可复现,固定种子法侧重比例精确。三类别水果这种小数据集,我用固定种子法更多。

4.2 按图片抽还是按场景抽:采样泄漏是 mAP 虚高的元凶

划分训练集和验证集时,最常见误区是单独把每一张图随机分边,而没有考虑图片之间的相似度。三种水果数据集如果是连续拍摄的,比如同一堆苹果在传送带上连续拍了 200 张,这些图背景几乎相同、目标位姿几乎相同,如果一部分进训练集一部分进验证集,模型在验证集上的 mAP 会虚高,因为它已经提前见过几乎一样的背景。

正确的做法是按「场景批次」划分。如果你知道哪些图来自同一个视频或同一次拍摄,把整个批次划到一边。如果没有批次信息,保守做法是把图片按时间戳前缀或采集批次命名分组,再把整个组放进 train 或 val。对苹果橘子梨这类目标检测小数据集,我还会额外检查验证集里是否有跟训练集一模一样的重复文件,用文件 md5 对比一遍,重复的图必须从一边删掉。这一步成本极低,但对结果可信度影响极大。

4.3 类别分布检查:梨只有 200 张时别硬按 85% 分

三类别数据集经常出现类别不均衡,比如苹果 5000 张、橘子 4500 张、梨只有 800 张。这时候如果整体按 85% 随机切,验证集里梨可能只有 100 张出头,mAP 对梨的评估会非常不稳定。我会按类别分别统计后再决定是否分层采样。

import os from collections import Counter def count_class_instances(label_dir): counter = Counter() for txt_name in os.listdir(label_dir): with open(os.path.join(label_dir, txt_name), "r") as f: for line in f: parts = line.strip().split() if len(parts) == 5: counter[int(parts[0])] += 1 return counter train_counter = count_class_instances("fruit/labels/train") val_counter = count_class_instances("fruit/labels/val") print("train:", train_counter) print("val:", val_counter)

这个脚本输出每个类别在训练集和验证集里的目标框总数。我要强调的是,划分时看的是「目标框数」不是「图片数」,因为一张图里可能同时有苹果和橘子。如果发现验证集某一类占比明显低于训练集该类占比,比如训练集里梨占 8%,验证集里梨只占 3%,那就应该手动从梨比较多的剩余图片里再抽补一些进验证集,或者干脆对梨的图片做一次复制粘贴增强后再划分。类别不均衡直接决定最终模型对梨的检出率,这个锅不能甩给模型结构,多半是划分阶段就埋下的。

4.4 训练命令与五条快速验证命令:先跑 1 个 epoch 再决定要不要全量训

目录结构、划分、data.yaml 都就绪后,我建议不要直接--epochs 300开训,先跑一个 epoch 做冒烟验证。

# 冒烟测试:1 个 epoch,确认数据加载无报错 python train.py --data data/fruit.yaml --weights yolov5s.pt --epochs 1 --img 640 --batch-size 16 # 训练完成后,验证模型质量 python val.py --data data/fruit.yaml --weights runs/train/exp/weights/best.pt --plots # 用验证集图片做实际推理可视化 python detect.py --source fruit/images/val --weights runs/train/exp/weights/best.pt --conf 0.25 --save-txt

这三个命令里,第一个命令如果顺利跑完,说明 images 和 labels 能对上、data.yaml 路径正确、类别配置无误。--epochs 1不是为了出效果,是为了让数据加载、loss 计算、反向传播全链路快速过一遍,任何标注格式问题都会在这个环节爆出来。第二个命令会输出 mAP@0.5 和 mAP@0.5:0.95,同时--plots会生成混淆矩阵和 PR 曲线,验证集能不能用一眼就清楚。第三个命令把验证集真实跑一遍,输出的 txt 和图片能直接看到漏检和误检。五条命令里最重要的其实是第一条和第二条,一个保证能训,一个保证能评。

5. 常见问题与避坑:三类别小数据集最容易踩的五个坑

5.1 能训练但 mAP 始终低于 0.5:类别编号整体错位

现象:训练 loss 能正常下降,但验证集 mAP@0.5 一直在 0.3~0.4 徘徊,无论训多久都上不去。看检测结果图,发现模型把橘子框出来但类别写成 apple,或者梨写成了 orange。

原因:标注 txt 里的类别编号和 data.yaml 的 names 顺序不一致。常见于从 COCO json 转换时没有重新映射 category_id,导致原本的 id 1、2、3 直接当作 YOLO 的 0、1、2 写进去,和 names 列表错位。

解决:先用统计脚本遍历 labels 目录,确认每个 txt 里类编号的最大值必须小于 nc。然后随机挑 3~5 个 txt,手动打开看第一列编号,再对照原图做一次人眼复核。修改映射关系后重新转换标注,不要试图在 data.yaml 里通过调整 names 顺序去迁就地错位的编号,那是给后面挖更大的坑。

5.2 验证集 loss 正常而 mAP 波动剧烈:验证集混入了训练图片

现象:每次训练完跑验证,mAP 一会儿 0.7、一会儿 0.4,同一份训练权重两次验证结果差异巨大。更诡异的是训练 loss 很平稳,完全看不出异常。

原因:验证集里混入了几张和训练集重复或几乎相同的图片。第一次验证时这些重复图出现在检测正确的区间,mAP 被抬高;第二次模型对同类图稍微没检出,mAP 又被打回去。三类水果的小数据集,如果划分时按无序的目录直接切,极容易出现同批连拍的相似图分到两边。

解决:对 train 和 val 的图片各算一次 md5,找出两边重复的文件,从验证集里删除。再按第 4 章说的场景分组检查一次,连续帧图片原则上不能跨边。验证集数量不足时,不要降低验证集比例硬填,而是收集更多新场景数据补上,验证集脏了比验证集小更可怕。

5.3 训练到一半 loss 突然变 NAN:标注坐标越界或出现负数

现象:训练前几十个 epoch 正常,某个 epoch 开始 loss 输出为nan,模型权重基本报废,中断后无法继续。查看本 epoch 的日志,正常 epoch 的box_loss通常在 0.05 左右,出现 nan 的那一轮直接变成nan或者大数。

原因:标签 txt 里存在越界坐标或非法的极大值。平时这些坏标签的梯度贡献不明显,等某次数据增强恰好把对应区域放大,坏坐标的 loss 瞬间爆炸。多数情况下是转换脚本没有对归一化坐标做范围检查,比如把像素值除以了错误的宽高,得到 2.5 或 -0.3 这种值。

解决:按第 3 章的clean_yolo_labels统一清洗一遍全部标签,把五列数值都在0~1区间的行留下,其余删除。清洗后再跑一次python val.py,如果数据加载阶段报出没有标签或标签异常,说明还有漏网之鱼。为了让问题提前暴露而不是训到一半才发现,训练时加上--cache ram有时候会触发更早的数据校验,但最根本的做法是清洗脚本每次转换后必须跑。

5.4 梨的类别总是检测不到:小类别样本不足不是模型问题

现象:混淆矩阵里 apple 和 orange 的正确率都在 0.9 以上,但 pear 那一行的召回率在 0.2 以下。检测验证集图片,梨子要么被漏掉,要么被标成 apple。

原因:梨的标注数量明显少于另外两类,模型在训练时对梨的特征贡献权重很低。三类别水果数据集里,这往往不是算法问题,而是样本数量分布问题。如果梨只有几十张,再好的结构也学不出稳定的类别特征。

解决:先做类别统计,确认不均衡程度。梨严重偏少时,先对梨的图片做随机旋转、亮度变化、水平翻转的数据增强,在增强后再重新划分 train/val。更稳的做法是修改训练超参数,把--hyp data/hyps/ hyp.scratch.yaml里的cls损失权重调高一点,但最根本的还是去补拍箱子里不同成熟度、不同光照的梨,增加样本覆盖。超参数只是把模型注意力往梨上拨,不能从无中生有变出特征。

5.5 验证集为空或路径报错:data.yaml 的相对路径与 labels.cache 缓存

现象:训练开始时日志输出train: ... images/train... val: ...但后面跟了0 images,或直接报AssertionError: train: No labels in...。另一种情况是修改了目录结构后,再次训练仍然读取旧数据。

原因:第一,data.yaml 里 val 路径写错或写成了valid而目录名是val;第二,YOLOv5 首次加载数据会生成labels.cache文件,之后直接读缓存,如果你删掉或调整了部分标注文件,缓存里还是旧记录;第三,路径相对执行目录解析出错。

解决:先删除数据集目录下所有.cache文件,再检查 data.yaml 的train和val是否指向真实存在的目录。在 yolov5 根目录下用ls fruit/images/val确认能看到图片。最后把 data.yaml 里的路径改成绝对路径试一次,如果绝对路径正常而相对路径异常,那就是执行位置不对,把命令固定到 yolov5 根目录或在 data.yaml 里写死绝对路径即可。这个坑和标注格式无关,纯粹是路径解析,但出现频率非常高,尤其是从压缩包解压后目录层级变化时。

6. 训练前必做:用可视化把三种水果的每一框看一遍

6.1 先跑一次短训练,用 --plots 生成带框预览图

拿到任何新数据集,我第一步都不是直接调参,而是用最小代价跑几个 epoch 后生成可视化标注图。具体做法是在正常训练命令里加--plots,训练过程会在runs/train/exp/下生成val_batch0_labels.jpg和val_batch0_pred.jpg这类文件,前者是真实标注画在图上,后者是模型预测结果。

python train.py --data data/fruit.yaml --weights yolov5s.pt --epochs 50 --img 640 --batch-size 16 --plots

val_batch0_labels.jpg会随机挑一批验证集图,把每个真实框和类别标签画上去。我一般盯这个文件看三件事:框有没有明显偏移目标,类别名和物体是否对得上,有没有漏标或误标的目标。比看训练曲线快得多,数据问题在这一步暴露,三分钟就能看明白,而不是等训练完才发现。

6.2 小数据集迁移学习:从 yolov5s.pt 起步更省心

三类别水果这种小规模数据集,不要从随机权重开始训,直接拿yolov5s.pt做迁移学习。水果检测和 COCO 预训练特征高度相关,预训练权重能提供骨干网络的通用特征,训练几轮后收敛速度肉眼可见地快。常见做法是保持默认超参数先把模型训起来,等验证 mAP 稳定后再针对低召回的那个类别去调cls损失权重,而不是一上来就改一堆超参数。我自己拿到三类别水果或类似小数据集时,习惯先跑一遍默认配置把底线摸清楚,再看混淆矩阵决定下一步到底补数据还是调参数,这个过程几乎不会翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 6:18:23

马德拉酒全解析:揭秘最强“不死”葡萄酒的氧化工艺与选酒指南

在酒圈里泡了十几年&#xff0c;我早就过了见什么喝什么的阶段&#xff0c;但有一类酒每次碰上都还是会让我停下来——马德拉&#xff08;Madeira&#xff09;。说白了&#xff0c;它是一种来自葡萄牙马德拉群岛的强化葡萄酒&#xff0c;发酵到一半加入中性葡萄烈酒&#xff0c…

作者头像 李华
网站建设 2026/10/1 6:18:06

SDIO -110排查:Linux内核超时与嵌入式WiFi初始化

新板子第一次上电&#xff0c;串口 log 滚到最后停在一行字上&#xff1a;mmc1: error -110 whilst initialising SDIO card。另一台已经跑起来的产品&#xff0c;用户反馈 WiFi 偶尔掉线&#xff0c;抓内核日志能看到brcmf_sdio_bus_txctl: dongle is not responding: err-110…

作者头像 李华
网站建设 2026/10/1 6:17:34

AI Agent核心架构拆解:从认知澄清到工程落地全景指南

这两年只要聊到大模型&#xff0c;AI Agent 几乎是绕不开的话题。但我在一线搭建过几个智能体项目之后&#xff0c;发现一个很扎心的现实&#xff1a;真正能讲清楚"核心架构"的人&#xff0c;远没有喊着"Agent 元年"的人多。很多人觉得 Agent 就是"大…

作者头像 李华
网站建设 2026/10/1 6:17:31

平稳随机过程遍历性详解:从时间平均到工程应用

学随机过程的时候&#xff0c;很多人把“平稳随机过程遍历性”当成一个必须背下来的数学定理&#xff0c;考完试就忘了。但真正开始处理实测信号、做时间序列分析之后&#xff0c;我才意识到这一章可能是全书最实用的一节。原因很朴素&#xff1a;你做实验、采数据&#xff0c;…

作者头像 李华
网站建设 2026/10/1 6:17:26

Multi-Agent系统实战:任务拆解、上下文隔离与协作机制

1. 为什么单Agent不够用&#xff1a;从一次真实翻车说起去年下半年我接手了一个内部工具链的改造项目&#xff0c;需求说起来不复杂&#xff1a;把散落在各个仓库里的技术文档做一次结构化整理&#xff0c;提取出接口定义、参数说明和调用示例&#xff0c;最后生成一份统一的AP…

作者头像 李华
网站建设 2026/10/1 6:16:34

从零搭建AI工程:数据、Prompt与Agent工作流实战

说实话&#xff0c;这两年AI这波浪潮起来之后&#xff0c;最不缺的就是各种“一句话生成应用”的Demo&#xff0c;但真正到了自己手上要搭一个能跑、能维护、能迭代的AI工程时&#xff0c;很多人还是会被一堆问题卡住。我自己从零开始折腾AI工程已经有一段时间了&#xff0c;从…

作者头像 李华