ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv8自定义数据集训练全流程:以罐头和瓶子检测为例

YOLOv8自定义数据集训练全流程:以罐头和瓶子检测为例 简介面向罐头和瓶子检测场景的目标检测数据集含一千五百三十一张图像的标注信息覆盖鲜奶、罐头、瓶子等对象适合目标检测入门学习及模型效果验证。数据已完成训练集与验证集划分附带的配置文件可直接用于各类主流YOLO系列算法的模型训练无需再手动整理标签。压缩包内共有两千个文件其中一千零七十三份为可扩展标记语言格式的注释文件九百二十七份为文本格式的标签文件两者分别对应VOC与YOLO注释规范YOLO格式标签采用归一化的中心点坐标和宽高比例符合常见训练框架的输入要求。文件总量约六十八点五九兆命名规律清晰可按名称快速定位图片编号和类别。已有五十四人浏览学习特别适用于刚接触目标检测的初学者熟悉注释格式也适合有经验的开发者快速评估罐头与瓶子检测的模型性能。1. 拿到「罐头和瓶子数据集」先搞清楚它是不是 YOLO 能直接吃的格式如果你下载的是这份「罐头和瓶子数据集-1531张图像带标签-鲜奶-瓶子.zip」多半不是想拿它欣赏而是想训一个能认货架或产线上鲜奶盒、饮料瓶的 YOLO 检测模型。1531 张图不算大但对一个场景相对固定的检测任务来说已经够用真正的风险不在图片数量而在解压之后 labels 目录里那一个个 txt 文件——类别 id 有没有错位、坐标是不是归一化、train/val 有没有切干净。这篇文章按我平时处理自定义数据集的顺序来解压、核对标签、清洗切分、跑通 YOLOv8 训练再把最容易翻车的几个坑单独挑出来讲。2. 拆包核对目录与标签格式把 1531 张图对应的 txt 标注理清楚2.1 先用 tree 和 find 核对 images/labels 数量顺手检查文件名是否同源解压之后先别急着训练。常见做法是先在解压根目录跑一遍目录树和计数确认 images 和 labels 数量对得上。这种数据集压缩包通常按images/和labels/平铺存放train/val 可能已经切好也可能没有切。路径结构不一样后面 data.yaml 的写法就不一样所以这一步省不掉。# 解压并进入目录 unzip 罐头和瓶子数据集*.zip -d cans_bottles cd cans_bottles # 统计图片和标签数量, 预期应各为 1531 find images -type f | wc -l find labels -type f | wc -l # 各看前三个文件名, 确认同名配对 ls images | head -3 ls labels | head -3如果图片数是 1531、标签数少了十几问题不大但必须记下来后面第 3 章清洗环节会统一处理。如果数量差了上百那就不是漏发而是目录结构或命名规则不一致比如图片是.jpg、标签是.png读出来的缓存文件混在里面。YOLO 系列对标签的要求很朴素一张图片对应一个同名.txt文件里每行描述一个目标框。Ultralytics YOLO 在训练时会自动把路径里的/images/替换成/labels/去找标签所以两个目录名必须按这个约定来否则训练阶段会报「label 文件不存在」。2.2 标签 txt 里那五个数字类别、中心点、宽高全是比例值打开任意一个 txt 文件你会看到类似0 0.5234 0.3812 0.1823 0.4217的一行。这五个数字的含义依次是类别 id、目标中心点 x 坐标、目标中心点 y 坐标、目标宽度、目标高度。注意后四个值全部是相对图片宽高的比例取值在 0 到 1 之间不是像素坐标。很多人第一次拿到数据集就写脚本转成 VOC 格式结果画出来的框全偏多半就是把「归一化坐标」当成了像素值。一个实用的抽查办法写几行 Python把标签画回原图上看一眼。1531 张图不用全看随机抽 20 张就能发现大多数标注问题。# check_labels.py: 把标注框画回图上, 检查坐标和类别是否正常 import cv2 from pathlib import Path CLASSES [fresh_milk, bottle] # 以包内 classes.txt 的实际顺序为准 vis_dir Path(vis) vis_dir.mkdir(exist_okTrue) for img_path in sorted(Path(images).glob(*.jpg))[:20]: img cv2.imread(str(img_path)) h, w img.shape[:2] txt_path Path(labels) / (img_path.stem .txt) if not txt_path.exists(): print(f{img_path.name} 缺少标签) continue with open(txt_path) as f: for line in f: parts line.split() if len(parts) ! 5: print(f{txt_path.name} 行数不对: {line}) continue cls, xc, yc, bw, bh parts # 转回像素坐标 x1 int((float(xc) - float(bw) / 2) * w) y1 int((float(yc) - float(bh) / 2) * h) x2 int((float(xc) float(bw) / 2) * w) y2 int((float(yc) float(bh) / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, CLASSES[int(cls)], (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(str(vis_dir / img_path.name), img)这个脚本的逻辑很直接读取每行五个数字把中心点坐标和宽高换算成左上角、右下角像素坐标再画框。如果画出来的框明显偏离目标、框住了两个目标的一半或者类别名和框体对不上那就要么手工修复要么考虑这个数据集的标签质量够不够支撑训练。还要顺手看一眼有没有xc 1.0的行——一旦出现说明这份标签是用像素坐标写的需要先做单位换算再喂给 YOLO。2.3 统计各类目标框数量再生成一份能进 YOLOv8 的 data.yaml确认单张标签没大问题之后把整个 labels 目录扫一遍统计每个类别的目标框总数。类别分布决定后面训练要不要做类别加权或补充样本。假设包内classes.txt给的是fresh_milk和bottle两类脚本如下# count_boxes.py: 统计每个类别的框数, 同时找出空标签 from collections import Counter from pathlib import Path counts Counter() empty [] for txt in Path(labels).glob(*.txt): lines [line.strip() for line in txt.read_text().splitlines() if line.strip()] if not lines: empty.append(txt.name) for line in lines: counts[int(line.split()[0])] 1 print(各类别框数:, dict(sorted(counts.items()))) print(空标签数量:, len(empty)) print(空标签示例:, empty[:10])跑完看一眼输出如果某类只有几十个框后面 mAP 大概率会在这一类上拉胯因为正样本太少。如果空标签文件很多训练时会触发 ultralytics 的检查逻辑轻则警告重则把该样本跳过。处理办法是直接删掉空 txt 对应的图片或者给空 txt 写一个默认背景标签——实际操作中前者更省事。接下来写 data.yaml。这是 YOLOv8 训练时唯一要手动准备的数据描述文件结构非常简单但有一个高频错误names的顺序必须和标签里的类别 id 一一对应不是按字母排也不是按你觉得好看的顺序排。# data.yaml: path 写解压后的实际绝对路径 path: /home/yang/datasets/cans_bottles train: images/train val: images/val test: images/test # 顺序必须与 classes.txt 保持一致; 通常 0 是鲜奶, 1 是瓶子 names: 0: fresh_milk 1: bottle如果这个包本身没有切分 train/val先把 train/val/test 指向原目录会让训练跑起来但验证结果不可信。正确做法是回到第 3 章先用切分脚本把目录整理好再回头填这个 yaml。路径尽量写绝对路径尤其当你要换机器、换用户跑训练时相对路径经常是「跑一天才发现缓存读错目录」的坑。3. 清洗、切分与增强把 1531 张图改造成能直接训的目录结构3.1 清掉孤儿标签有 txt 没图、有图没 txt 的都要查下载的数据集不一定干净。所谓孤儿标签就是 labels 里存在、但 images 里找不到对应图片的 txt 文件。反过来有些图片没有标签。前者会让训练日志里出现一堆找不到图片的警告后者会让一张没标注的图混进训练集相当于给模型喂了一帧背景当作正样本的一部分干扰很小但让人心里没底。# 找孤儿标签: labels 下存在, images 下没有同名 jpg/png for f in labels/*.txt; do base${f##*/}; base${base%.txt} if [ ! -f images/${base}.jpg ] [ ! -f images/${base}.png ]; then echo 孤儿标签: $f fi done # 找缺标签的图片: 反过来查一遍 for f in images/*.jpg; do base${f##*/}; base${base%.jpg} if [ ! -f labels/${base}.txt ]; then echo 缺失标签: $f fi done两段脚本都是纯 bash 字符串拼接1531 张的量级直接跑没有问题不需要 xargs 并行。跑出来如果有输出先确认是不是因为图片后缀混用——有些图是.jpg有些是.png上面第二段只查了.jpg所以会误报。更稳妥的做法是两条命令都写全后缀或者干脆用find先把手头的后缀枚举出来。对于确认的孤儿文件我的习惯是先移到.orphan后缀而不是直接删万一下载包里本来就少文件后面还能人工判定要不要补标签。3.2 8:1:1 切分脚本先打乱再切避免连续帧泄漏进验证集如果数据集是从视频抽帧得到的连续几十帧场景几乎一样按文件名排序直接切分会把同一个画面的不同帧同时放进 train 和 val验证集的 mAP 虚高部署时原形毕露。所以切分之前必须先打乱然后再按比例分配。# split_dataset.py: 打乱后按 8:1:1 切分到 images/train 等目录 import random import shutil from pathlib import Path random.seed(2024) src_img Path(images) src_lab Path(labels) imgs sorted(src_img.glob(*)) random.shuffle(imgs) n len(imgs) n_train int(n * 0.8) n_val int(n * 0.1) splits { train: imgs[:n_train], val: imgs[n_train:n_train n_val], test: imgs[n_train n_val:], } for split, items in splits.items(): dest_img src_img.parent / images / split dest_lab src_lab.parent / labels / split dest_img.mkdir(parentsTrue, exist_okTrue) dest_lab.mkdir(parentsTrue, exist_okTrue) for img in items: # 复制而不是移动, 原目录留作备份; 1 万张以内直接复制最省心 shutil.copy2(img, dest_img / img.name) lab src_lab / (img.stem .txt) if lab.exists(): shutil.copy2(lab, dest_lab / lab.name) print(train:, len(splits[train])) print(val:, len(splits[val])) print(test:, len(splits[test]))脚本做的事情把 images 下所有图片读进来随机打乱按 8:1:1 切成三份再把图片和同名标签一起复制到对应子目录。注意random.seed(2024)这一行固定随机种子才可复现切分结果。如果你下次发现模型差在数据上靠这个 seed 能原样切回同一份训练集方便定位问题。复制而不是移动是因为原目录保留后后续想换切分比例或重新抽样不需要重新解压。3.3 数据增强别乱上瓶子罐头是刚体翻转和 HSV 扰动是首选增强策略在目标检测里常常被玄学化一上来就堆一堆随机擦除、旋转 90 度、Cutout结果训练 loss 波动巨大mAP 还降了。罐头和瓶子是刚体目标形状规则语义不依赖方向但瓶身上的商标文字和罐头的印刷图案是模型真正在学的纹理特征。把瓶子横过来、把罐头倒过来尺寸比例变得不常见检测器反而会困惑。所以我一般只用几项增强原则是「改变光照和模糊不改变物体拓扑」增强项建议值对这个数据集的作用fliplr0.5镜像不改变物体语义能缓解商标文字方向过拟合hsv_h0.015轻微色相变化接近不同批次包装的色差hsv_s0.5增强暗光货架场景的饱和度差异hsv_v0.4模拟不同灯光下的亮度变化scale/translate0.2 / 0.1目标尺度扰动让模型对远近更鲁棒不需要离线把增强后的图片存出来YOLOv8 在训练时内置了这些变换直接在命令行参数里传或者写到配置文件中即可。如果你用 Albumentations 做离线增强记得 Compose 里要带bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels])否则框不会跟着图一起变等于给模型喂了错标签。这个错误很隐蔽训练时 loss 也不炸就是 mAP 上不去。4. 用 YOLOv8 训练自己的数据集最小命令与超参数调整4.1 最小训练命令先跑 20 个 epoch 验证管道再谈精度第一次训练的目标不是拿高分而是确认整条链路通图片读得进来、标签配得上、损失函数能正常下降、验证集指标能算出来。用最小的 YOLOv8n 模型先跑 20 个 epoch几分钟就能看出问题。# 最小训练命令: 先验证数据管道, 模型用最小的 n yolo detect train \ modelyolov8n.pt \ datadata.yaml \ epochs20 \ imgsz640 \ batch16 \ patience10 \ projectruns/detect \ namefresh_bottle_smokeyolov8n.pt是预训练权重拿它做初始化比从随机权重开始收敛快得多epochs20是验证性训练够看出 loss 趋势但不追求收敛imgsz640是 YOLO 系列的默认输入分辨率也是后面做 TensorRT 部署时最常用的分辨率batch16在 1531 张的小数据集上完全够用显存吃紧就降到 8。patience10的意思是连续 10 个 epoch 验证指标没提升就提前停止对冒烟测试刚好。跑完后看runs/detect/fresh_bottle_smoke/目录里的results.png如果 box_loss 和 cls_loss 曲线是下降趋势说明数据管道没问题可以开始正式训练。4.2 超参数表和损失函数读数小数据集不怕慢怕瞎调正式训练时我通常会把训练轮数放到 100 到 150模型从 YOLOv8n 换到 YOLOv8s其他参数先用默认值跑一版再根据损失曲线做小幅度调整。小数据集最怕的是频繁改超参数每次改动都让模型从头学最后你根本不知道是数据问题还是参数问题。参数我的建议值理由modelyolov8s.pt1531 张用 n 精度不够用 m 又容易过拟合s 是甜点位epochs120小数据集 100 轮上下足够收敛再多就背背景了imgsz640如果不吃显存训练用 960、测试用 640 也能涨点batch8 或 16看显存batch 太大在小数据集上会提前过拟合lr00.01从预训练权重续训不需要更大的初始学习率patience20小数据集验证指标波动大太早停看不到真实趋势训练日志里那个 loss 是 box_loss、cls_loss、dfl_loss 三项的加权和不是某个单一指标。clas_loss 长期横盘不动先怀疑标签里有类别标错的框box_loss 降到底但 dfl_loss 还很高说明边界框的分布预测不够准通常和标注框边界不齐有关。有人说「loss 降了 mAP 不涨」时我会先跑一遍标签统计脚本八成是背景相似度太高或某个类样本太少。4.3 中断恢复与 epochs 选择用 last.pt 续训别硬跑几百轮训练中断是常事显存不够、机器重启、You 关错终端窗口都会打断训练。YOLOv8 的好处是每个 epoch 结束都会写一个last.pt续训直接用这个权重文件恢复即可。# 从中断处恢复训练 yolo detect train \ modelruns/detect/fresh_bottle_v1/weights/last.pt \ datadata.yaml \ resumeTrue注意 two 个细节。第一data.yaml的内容必须和中断前完全一致尤其是 path 路径否则恢复时校验数据缓存会失败或读到另一份数据集。第二patience 的计时从恢复时刻重新开始这意味着中断一次早停判断就宽松一段所以不要频繁手动中断否则模型会在过拟合边缘多跑好几轮。epochs 这个参数在小数据集上很容易被调大过头。有人觉得 1531 张图不够直接跑 300 个 epoch结果后面几十轮验证指标不断下滑。原因很简单模型已经把训练图里的背景纹理记住了测试时换个场景就识别不出来。我一般以验证集 mAP50 不再上升且连续 10 个 epoch 波动不超过 1 个点为基准回看训练日志基本都在 100 到 150 之间收敛。5. 避坑类别 id 错位、路径写死、背景过拟合的排查套路5.1 现象loss 在降、mAP 恒为 0原因类别 id 与 names 错位这是拿到自己数据集训练时最常踩的坑。现象是训练日志里 loss 曲线很漂亮地下降但每个 epoch 后的验证 mAP50 始终是 0或者metrics/precision偶尔跳到一个数又掉回 0。原因基本出在类别映射上。数据集的classes.txt里如果写着0 fresh_milk, 1 bottle但下载包里实际标签文件第一条写的类别 id 是1而你在 data.yaml 里把0写成了bottle那模型就把鲜奶当成瓶子学验证时一个都认不对。更隐蔽的是有些标签混入了2这类越界 id训练时分类损失在学一堆不存在的类mAP 自然上不去。排查命令很简单把 labels 里所有类别 id 的种类拉出来看一眼# 检查所有标签文件中的类别 id 是否都在 0/1 范围内 awk {print $1} labels/*.txt | sort -n -u如果输出里出现了大于等于2的值就用脚本批量修正。按实际 classes.txt 重新映射# fix_cls.py: 把 id 从 1 开始映射回 0 开始 from pathlib import Path for txt in Path(labels).glob(*.txt): lines [] for line in txt.read_text().splitlines(): parts line.split() if not parts: continue cls int(parts[0]) - 1 # 如果原 id 从 1 开始 parts[0] str(cls) lines.append( .join(parts)) if lines: txt.write_text(\n.join(lines) \n)这段代码把每个 txt 里的类别 id 减 1适合「classes.txt 用 0 开始、标注文件却用 1 开始」的情况。改完重新跑一遍第 2.2 节的画框脚本确认类别名和物体对得上再进训练。5.2 现象Dataset not found 或 label 文件不存在原因images 与 labels 目录名不配对训练一启动日志里报Dataset not found或assertion failed: label file ... does not exist这通常是目录名不匹配。YOLOv8 找标签的逻辑是从 data.yaml 里读 train 路径把路径字符串中的/images/替换成/labels/。比如你的训练图像在images/train它就自动去labels/train找同名 txt。如果你的目录叫annots、annotations、labels_train无论 data.yaml 怎么写都找不到。处理办法是建符号链接不要为了迁就一个压缩包的原始命名去改代码# 解压后标签目录如果叫 annots, 直接软链成 labels ln -s annots labels # 确认最终的目录布局必须长这样 # images/train/xxx.jpg - labels/train/xxx.txt find labels -type f | wc -l如果你的包目录是imgs和gt同样ln -s imgs images、ln -s gt labels。这个坑高发在「数据集发布者自己改写目录名」的场景一旦报错先检查软链和 data.yaml不要先去改模型配置。5.3 现象mAP 不低但实拍漏检原因背景过拟合与暗光样本缺失训练集 mAP50 到了 0.9你觉得稳了结果拿手机对着超市货架拍一段视频测试透明塑料瓶在暗光下漏了一半。这个现象我见过不止一次根因是这 1531 张图的背景高度单一——很可能全部来自同一个机位、同一批灯光。模型真正学到的不只是「瓶子长这样」还包括「瓶子出现时背景长这样」。测试环境一换背景变了置信度就掉到阈值以下。解决思路有两个。第一补困难样本把实拍漏检的帧截出来人工标上标签不用标得很精细哪怕每张只标漏掉的瓶子加入训练集重新训练 50 轮。几十张暗光样本就能把实拍漏检率拉下来一大截。第二加固增强把hsv_v从默认的 0.4 调到 0.6同时给训练图加一点随机亮度扰动让模型对整体变暗不敏感。如果你能把训练集和验证集都做一次「亮度随机化」的预处理再训练暗光鲁棒性会更好但这相当于离线增强生成的数据量得翻倍。判断一个数据集值不值得继续投入最直接的方法是看验证集混淆矩阵。训练完成后在runs/detect/目录下找confusion_matrix.png如果两类目标互相误检严重说明标注边界或类别定义本身有歧义这时候补数据不如先把标签改清楚。6. 用最小验证流程判断这 1531 张图的边界在哪里6.1 三步验证与一个判定基准训练结束、拿到best.pt之后我不会直接看精度数字激动而是按一个固定流程跑三件事在 test 集上验证、在真实视频上推理、抽查二十张带标签可视化图。三步都过了这包数据才算真的吃透。# 第一步: 在 test 集上验证, 输出每个类别的 mAP50/mAP50-95 yolo detect val \ modelruns/detect/fresh_bottle_v1/weights/best.pt \ datadata.yaml \ splittest \ conf0.35 \ iou0.5 # 第二步: 拿一段没参与训练的真实货架视频做推理 yolo detect predict \ modelruns/detect/fresh_bottle_v1/weights/best.pt \ sourcerefrigerator_phone.mp4 \ conf0.3 \ imgsz640 \ save_txtTrue看yolo detect val输出的 results 表重点不是平均 mAP而是每一类的 AP。两类目标如果 AP 差超过 0.1说明弱势类样本量不足后面要补那一类的数据。再打开第二步生成的视频一帧一帧看漏检和误检出现在哪种场景。我的经验准则是如果 test 集 mAP50 超过 0.85且视频里连续 100 帧没有明显漏检这批数据就可以作为一版基线模型上线如果 mAP50 超过 0.85 但视频漏检频繁那说明训练集和真实场景的域差距太大继续调超参已经没意义直接去补实拍数据。我自己在类似项目上吃过一次亏当时训练集 mAP50 到了 0.92觉得稳了结果一到亮面反光的饮料柜透明瓶十次漏四次。后来把反光样本抽了 40 帧手工补标重新训 80 轮漏检率才降到可接受范围。从那以后我养成了一个习惯——拿到任何数据集先跑冒烟训练再追指标模型指标再好看也不如一段手机实拍视频有说服力。这一步跳过后面上线就得拿客户现场翻车的代价来补。希望这套验证流程能帮你少走这段弯路。本文还有配套的精品资源点击获取
返回列表