2026/10/11 13:34:46

YOLOv8自定义数据集训练实战:从环境配置到部署的避坑指南

YOLOv8自定义数据集训练实战:从环境配置到部署的避坑指南 简介这是一份面向深度学习开发者的YOLOv8实战指南完整覆盖从零搭建GPU训练环境到用自定义数据集训练实例分割模型的流程。文档以Ubuntu 22.04为演示平台详细说明NVIDIA驱动安装与验证、CUDA 11.7及cuDNN 8.9的配置方法包括环境变量写入、GPG密钥处理等常见坑点在PyTorch部分给出Anaconda虚拟环境创建、GPU版torch及torchvision的安装指令并提示如何配置默认Python路径等细节。此外还介绍了数据集标注工具选择、YOLOv8目录结构整理、预训练权重下载以及基础训练脚本的运行方式即使首次接触YOLO系列也能按图索骥完成从环境搭建到模型训练的整套流程。整个资源打包为一个PDF文档大小约1.23MB便于查阅与保存。目前已有6477人学习使用适合需要在Ubuntu平台快速落地YOLOv8项目、或想系统了解实例分割训练全流程的工程师与研究者。1. 当检测目标变成「你自己的库存」为什么绕不开YOLOv8与自建数据集COCO 预训练权重再强也只会识别那 80 类常见物体。你真正要检测的往往是「产线上某一种铝件划痕」「停车场里某一类车牌」「仓库里某个特定型号的箱子」——这些东西没有任何公开权重见过训练自己数据集就成了绕不开的一步而 YOLOv8 是目前被问得最多的起点。这篇笔记把「从零到能部署」的最小路径拆开讲环境怎么装、图片怎么标注和转格式、训练命令每项参数代表什么、翻车时先查哪里。适合刚接触目标检测的工程师照着一路做下来也适合手里已经攒了一批图片、想快速验证能不能出模型的团队参考。读完你至少能跑通一次完整训练并知道下一步该往哪调。2. 先装环境再跑通推理ultralytics 安装、结构速览与三行代码验证 GPU2.1 安装与版本锁定的 3 个选择装环境是最先卡住人的一步也是最玄学的一步。YOLOv8 本身不复杂复杂的是 torch、CUDA、ultralytics 之间的版本组合。我一般会单独建一个 conda 环境避免把其他项目搅乱conda create -n yolo python3.10 -y conda activate yolo pip install ultralytics8.1.0,8.2.0 python -c import ultralytics; print(ultralytics.__version__)Python 3.10 是兼容性最稳的选择网上绝大多数教程也基于这个版本跑通。把 ultralytics 锁在 8.1.x 系列是因为这个时期的 API 和命令行参数最稳定随手搜到的代码基本都能直接抄如果你直接装最新版某些函数签名变了照着老教程写会莫名其妙报错。训练框架这件事上稳定比追新重要。接下来装 PyTorch。不要用 pip 默认源装 torch默认源拿到的通常是 CPU 版本训练慢到你想放弃。从 PyTorch 官网挑对应 CUDA 版本的安装命令复制过来装装完用这一句验证而不是直接开训练python -c import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))如果输出里有True和你的显卡型号说明 GPU 版本生效了如果是False先去看驱动版本和 CUDA 版本有没有对上这一步没查清楚后面每跑一个模型都会怀疑人生。机器只有 CPU 也不是不能跑yolov8n 推理一张图也就一两秒但训练几百张图会非常煎熬建议还是先解决显卡。2.2 yolov8 网络结构图里的三个关键改动网上搜「yolov8 网络结构图」会看到一条清晰的路线输入图片先过 backbone 提取特征中间经过 C2f 模块逐步下采样neck 用 PAN-FPN 做多尺度特征融合最后 head 输出框位置和类别。相比 YOLOv5YOLOv8 有三处关键改动理解了这三处后面看训练日志才不会懵C2f 模块替换了 YOLOv5 的 C3。C2f 把特征图拆成两支再拼接梯度回传路径更丰富在差不多的算力下特征表达更好。这是 YOLOv8 精度比 v5 同体积模型高的主要原因之一。Decoupled Head 把分类和回归分成两个分支。v5 是耦合头分类和回归共享同一组卷积v8 拆开之后分类分支不用被迫适应回归任务的需求收敛更稳定。从 anchor-based 变成 anchor-free。不再需要预先聚类 anchor 尺寸模型直接预测中心点和宽高后处理逻辑也简化了。模型体积从 n 到 x 分了五档选型直接决定你后面训练是否顺利档位参数量典型用途yolov8n约 300 万快速验证流程、CPU 推理、嵌入式设备yolov8s约 1100 万6G 显存起步的主流训练选择yolov8m约 2600 万有 10G 以上显存追求更高精度yolov8l / x约 4400 万 / 6800 万服务器级显卡数据量大且标注质量高如果你只是训练自己数据集别一上来就选 x。数据量、标注质量、显存这三样跟不上大模型只会让你更快过拟合不会让你 mAP 更高。2.3 用 yolov8n.pt 跑通一张图的推理环境装好后先别碰自己的数据拿官方权重跑通一张图。这一步的任务只有一个证明 torch、ultralytics、模型文件下载这条链路是通的。yolo predict modelyolov8n.pt sourcebus.jpg等价地在 Python 里这样写from ultralytics import YOLO model YOLO(yolov8n.pt) # 首次运行会自动下载权重到当前目录 results model(bus.jpg, conf0.4) # conf 是置信度阈值 results[0].save(output.jpg)model加载时如果本地没有权重文件会自动从官方仓库拉取保存后下次复用不再下载。conf参数控制保留哪些框默认 0.25如果你想让结果干净一点调高到 0.4代价是可能漏掉一些置信度偏低的目标。results[0].save(output.jpg)会把画好框的图存下来打开一眼就能看出检测是否正常。如果这一步能出框你的环境就稳了可以进入下一节准备数据。如果连官方权重都跑不通先回头查 CUDA 版本和 torch 版本不要急着碰数据集。3. 把图片变成训练语料标注、VOC/COCO 转 YOLO 与数据集 YAML3.1 标注的目录结构与 YOLO txt 格式YOLOv8 训练时对数据目录有固定约定图片和标签分开放各自再按 train / val 拆开。以头盔检测为例datasets/helmet/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── helmet.yaml一张images/train/001.jpg对应一份labels/train/001.txt文件名必须完全一致只是扩展名不同。txt 里每一行代表一个目标框0 0.5234 0.5813 0.1867 0.2145 1 0.2110 0.3909 0.1533 0.1812五个字段分别是类别编号、框中心点 x、框中心点 y、框宽、框高。注意所有坐标都是归一化到 0 到 1 之间的小数用像素宽度和高度分别除过。类别编号从 0 开始具体哪个数字代表哪个类别完全由你后面写的 YAML 决定所以标注时就要定好顺序。标注工具我常用 LabelImg界面简单能直接导出 YOLO 格式的 txt。标注时有一个细节容易被忽略框要尽量贴边。做过 CCPD 这类车牌检测的人应该都有体会车牌在画面里占比不大如果框四面留白训练出来的预测框会比车牌大一圈后接手的人看着就想骂人。框贴边归一化坐标才稳定。3.2 VOC XML 转 YOLO txt脚本与坐标归一化陷阱很多老项目和公开数据集给的是 Pascal VOC 的 XML 标注里面存的是像素坐标。转成 YOLO txt 是必踩的第一道坎核心是归一化和换坐标表达方式import xml.etree.ElementTree as ET from pathlib import Path def clip01(v): return max(0.0, min(1.0, v)) def convert_voc(xml_file, out_txt, class_names): tree ET.parse(xml_file) root tree.getroot() img_w int(root.findtext(size/width)) # 图片原始像素宽 img_h int(root.findtext(size/height)) # 图片原始像素高 lines [] for obj in root.iter(object): name obj.findtext(name) if name not in class_names: # 跳过没在类别表里的目标 continue if obj.findtext(difficult) 1: # 难例样本建议直接跳过 continue cls_id class_names.index(name) bnd obj.find(bndbox) xmin float(bnd.findtext(xmin)) ymin float(bnd.findtext(ymin)) xmax float(bnd.findtext(xmax)) ymax float(bnd.findtext(ymax)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append( f{cls_id} {clip01(x_center):.6f} {clip01(y_center):.6f} f{clip01(box_w):.6f} {clip01(box_h):.6f} ) Path(out_txt).write_text(\n.join(lines), encodingutf-8) if __name__ __main__: class_names [helmet, head] convert_voc(000001.xml, 000001.txt, class_names)这段脚本把 XML 里的bndbox四个角点换算成中心点加宽高再除以图片实际宽高完成归一化。有几个坑是实战中真实遇到的一是 XML 里存在difficult标记这类目标不参与训练更稳妥二是标注软件偶尔会给出超出图片边界的框换算后坐标可能大于 1 或小于 0必须 clip否则训练时数据增强会直接报错三是写文件强制 utf-8避免 Windows 中文路径下编码错乱。转换完不要急着训练。随机抽十个 txt把数值乘回图片宽高画框出来肉眼看一遍比对原图有没有错位。这一步花十分钟能省掉后面排查一晚上的时间。3.3 COCO JSON 转 YOLO txt 与数据集 YAML 的写法如果你的数据来自 CCPD、HRSC2016 这类公开数据集的 COCO 版本或者团队用 CVAT 导出拿到的是一份大 JSON。COCO 的组织方式是images和annotations两个数组转换时要按 image_id 把标注归到每一张图上import json from pathlib import Path from collections import defaultdict def coco_to_yolo(json_path, out_dir): with open(json_path, encodingutf-8) as f: data json.load(f) cat_id_to_cls {c[id]: i for i, c in enumerate(data[categories])} img_id_to_info {im[id]: im for im in data[images]} anns_by_img defaultdict(list) for ann in data[annotations]: anns_by_img[ann[image_id]].append(ann) out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) for img_id, anns in anns_by_img.items(): img img_id_to_info[img_id] img_w, img_h img[width], img[height] lines [] for ann in anns: x, y, w, h ann[bbox] # COCO 的 bbox 是左上角 宽高 cx (x w / 2) / img_w cy (y h / 2) / img_h cls_id cat_id_to_cls[ann[category_id]] lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w / img_w:.6f} {h / img_h:.6f}) stem Path(img[file_name]).stem (out_dir / f{stem}.txt).write_text(\n.join(lines), encodingutf-8)这里最常见的翻车点是把 COCO 的 bbox 当成中心点坐标直接用。COCO 的bbox字段是[x, y, width, height]表示左上角坐标和宽高而 YOLO 要的是中心点加宽高。必须先把 x 加上宽的一半换算成中心点不能直接照搬。另外注意 txt 文件名要和图片名一致COCO 的file_name往往带路径前缀取stem时先确认目录层级。数据整理好之后写一个 YAML 描述数据集path: ../datasets/helmet train: images/train val: images/val names: 0: helmet 1: headpath写数据集根目录train和val相对它定位图片目录names必须从 0 开始连续编号顺序和标注 txt 里的类别编号一一对应。如果手头只有一个图片目录常见做法是写一个小的 Python 脚本用固定随机种子把 20% 图片和同名 txt 一起移进 val 目录保证训练集和验证集完全不重叠这也直接关系到第 5 章要讲的验证集虚高问题。4. 训练自己的数据集命令逐项拆解、参数表与 loss 曲线判读4.1 最小训练命令与 6 个必调参数数据和 YAML 都就绪后训练命令比想象中短yolo detect train \ datahelmet.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ patience15 \ device0 \ projectruns \ namehelmet_v1这些参数里最值得花时间理解的是下面几个参数默认值建议modelyolov8s.pt用预训练权重做迁移学习收敛速度远快于随机初始化epochs100先跑 100看 loss 不再降就用 best.pt 续训imgsz640通用选择目标很小再考虑 1280显存占用接近翻倍batch16由显存决定6G 卡用 8 更稳patience15验证指标连续 15 轮不涨就早停省时间device00 表示第一张显卡CPU 才写 -1model参数不只是一个初始化文件它决定了你训练的网络体量。用yolov8s.pt作为起点时模型结构就是 s 档不会因为数据少就自动变小。如果你的显存只有 6G数据量又在几千张以内我一般直接推荐 s 档起步n 档也不是不行但精度上限低测试阶段够用真上线容易不够看。训练启动后所有产物会落在runs/detect/helmet_v1/下。weights文件夹里会有best.pt和last.pt前者是验证集指标最好的时刻后者是训练最后一步的状态。last.pt 就是训练过程中的后悔药——断点续训靠它临时想改参数接着跑也靠它。4.2 显存不够先改 batch从 1660Ti 出发的参数组合用 GTX 1660Ti 这类 6G 显存的卡跑 YOLOv8一上来就撞 OOM 是常态。先说结论改参数的顺序是模型体量 → imgsz → batch而不是一上来就疯狂减 batch。因为 batch 太小会让梯度估计不稳loss 震荡精度反而受影响。显存推荐组合6G1660Ti 等yolov8s imgsz 640 batch 8或 yolov8n imgsz 640 batch 168G ~ 10Gyolov8m imgsz 640 batch 812G 以上yolov8l / yolov8xbatch 8 ~ 16 自行尝试训练日志第一行会打印gpu_mem如果你看到显存占用在 5.8G 以上贴着上限说明已经在爆的边缘。这个时候把 batch 从 16 减到 8一般立刻缓解。ultralytics 内部会按接近 64 的等效总 batch 自动分配梯度累积倍数所以小 batch 不会让总样本量损失太多但单张 batch 仍然要完整放进显存做前向传播OOM 不会因此消失。另外一个容易被忽略的点Windows 下workers默认值可能引发数据加载器崩溃。如果你看到训练卡在第一个 epoch 之前或者报dataloader worker exited unexpectedly把训练命令加上workers2再试比反复调 batch 有效。4.3 训练日志与损失函数曲线图怎么看训练开始后终端会滚动类似这样的日志Epoch gpu_mem box_loss cls_loss dfl_loss mAP50 mAP50-95 12 4.83G 1.21 0.52 0.94 0.621 0.412三行 loss 对应网络三个输出头box_loss是回归分支的框误差cls_loss是分类分支的类别误差dfl_loss是分布聚焦损失负责让框边界更精确。只要三条曲线整体下行哪怕有小幅波动都不用慌。训练结束后ultralytics 会在结果目录自动生成results.png这就是大家常说的损失函数曲线图不用自己写脚本。如果想把曲线放到自己的报告里可以读results.csv重新画import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/helmet_v1/results.csv) df[train/box_loss].plot(labeltrain box_loss) df[val/box_loss].plot(labelval box_loss) plt.legend() plt.show()看曲线时盯着两个关系train loss 和 val loss 的差距。train 一路降、val 不降甚至反弹就是过拟合信号两者同步降但都很慢可能是学习率偏小或数据量不够。还有一个新手常犯的错误以为 loss 必须单调下降。早期震荡是正常现象关键看整体趋势和mAP50-95的爬升斜率。如果训练中途断了不用重来yolo detect train modelruns/detect/helmet_v1/weights/last.pt resumeTrue它会从last.pt的状态接着跑已完成的 epoch 不会重复计算。我习惯每次训练前先确认一下这个命令可用毕竟没人知道半夜显卡会不会掉驱动。5. 避坑实录训练与数据集最常见的 5 个翻车现场5.1 类别编号对不上mAP 永远是 0现象训练时 loss 正常下降验证集 mAP 却是一条直线死活停在 0 附近。原因标注 txt 里的类别编号和 YAML 的names顺序对不上最常见的是从 1 开始编号或者 YAML 里少写了某个类别导致部分框的cls_id超出 names 长度训练时被当作无效目标。解决训练前先扫一遍所有标签文件from pathlib import Path num_classes 2 # 改成你自己的类别数 labels_dir Path(datasets/helmet/labels/train) for txt in labels_dir.rglob(*.txt): for i, line in enumerate(txt.read_text(encodingutf-8).splitlines()): parts line.strip().split() if not parts: continue cls_id int(parts[0]) coords [float(v) for v in parts[1:]] if cls_id 0 or cls_id num_classes: print(f{txt}: 第{i}行 cls_id{cls_id} 越界) if any(c 0 or c 1 for c in coords): print(f{txt}: 第{i}行 坐标超出 0~1)这段代码逐行检查两个硬性条件类别编号是否在合法范围坐标是否都在归一化区间内。正常标注不会触发任何输出一旦有输出对应的就是训练时会被忽略或报错的样本。脚本逻辑很简单但它能挡住你训练一晚上后才发现数据有问题的翻车。5.2 6G 显存 OOM换 batch 不如先换模型体量现象训练第一个 epoch 跑到一半终端刷出CUDA out of memory然后进程退出。原因6G 显存跑yolov8s imgsz 640 batch 16在某些数据上并不安全。目标多、图片大、背景复杂时中间特征图的临时张量会显著增大再加上显存里还有系统其它程序占用贴着上限跑迟早爆。解决优先把模型从 s 换到 n或者把imgsz从 640 降到 512最后才考虑减 batch。同时可以用nvidia-smi -l 1实时盯着显存变化确认是不是有别的进程在抢卡。1660Ti 这类卡最省心的组合是yolov8s imgsz 640 batch 8训练速度不会太慢精度也够用。另外Windows 下把workers从默认值降到 2能避免数据加载线程把内存和显存同时拖垮。5.3 loss 不降或震荡先查数据再查学习率现象box_loss跑了几十个 epoch 还在 1.2 附近不动或者 loss 曲线像锯齿一样剧烈上下跳。原因数据问题的概率远大于超参数问题。常见三类标注框错位、类别严重失衡90% 的框都是同一类、图片里存在大量没标注的目标。以 HRSC2016 这类遥感舰船数据集为例目标在整张图里占比很小如果imgsz只给 640小目标在下采样过程中几乎被抹掉loss 自然降不下去。解决先统计数据别急着调参。算一下每个类别的实例数、每张图的平均框数、框的宽高占图片比例。如果小目标占比高把imgsz提到 1280或者把大图切块检测。数据本身没问题后再考虑把lr0从默认 0.01 降到 0.005 或 0.001。数据量只有两三百张的时候loss 不降先别调参去补数据或者用同领域的公开集做预训练比折腾任何超参数都有效。5.4 验证集虚高数据泄露比过拟合更隐蔽现象训练时 mAP50 能到 90% 以上一换到实拍环境mAP 直接掉到 50%前后判若两模。原因训练集和验证集由同一批图片随机 20% 切分而来同一目标实例甚至同一视频帧同时出现在两边。自动驾驶数据集 BDD100K 这类连续帧数据尤其危险前后两帧几乎一样随机切分等于让模型把验证集背下来了。解决按视频、时间段或场景划分数据集而不是按文件路径随机划分。手动拍照采集时把不同时段、不同角度、不同目标个体分开存放划分时保证同一物体不会同时出现在 train 和 val。更稳的做法是额外留出一份完全独立的测试集训练全程不碰它。这一步能让你后面部署时少掉一层皮。5.5 中文路径与损坏图片加载阶段就静默失败的坑现象训练莫名卡住或者报Found 0 images in ...或者某个 epoch 跑完正要保存时崩掉。原因Windows 下数据集路径包含中文或空格ultralytics 在解析时偶发失败或者某张 jpg 其实是 PNG 改了扩展名甚至是个 0 字节空文件读取时直接异常中断。解决数据集和工程路径全用英文不要放在桌面/新文件夹这种路径下。训练前先跑一遍图片完整性检查from PIL import Image from pathlib import Path def verify(img_dir): bad [] for p in Path(img_dir).rglob(*): if p.suffix.lower() not in {.jpg, .jpeg, .png, .bmp}: continue try: with Image.open(p) as im: im.load() # 真正解码像素比仅打开文件更严格 except Exception: bad.append(str(p)) return bad print(verify(datasets/helmet/images/train))这里的im.load()会触发真实解码能骗过它的损坏图片很少。坏图单独移出目录不要用 OpenCV 的imread检查——它遇到空图会静默返回 None训练时读成黑图也查不出来。另外训练日志里的WARNING不要跳过比如标签文件为空、框坐标全零这类警告往往是数据问题的第一信号。6. 从 best.pt 到部署选权重、导出 ONNX 与后续验证训练跑完结果目录下的weights文件夹里有两个权重best.pt按验证集指标保存last.pt是训练最后一步。部署用 best续训用 last这个区分我一开始总记反直到有一次把 last 部署出去性能差了一大截才长记性。顺手再看一眼results.png和confusion_matrix.png混淆矩阵能看到哪些类别互相搞混这比 mAP 数字更接近实际问题。要往外部署第一步通常是导 ONNXyolo export modelruns/detect/helmet_v1/weights/best.pt formatonnx opset12导出后用 ultralytics 自带的加载能力做一致性验证from ultralytics import YOLO pt_model YOLO(best.pt) onnx_model YOLO(best.onnx) a pt_model(test.jpg, conf0.25)[0].boxes.xyxy b onnx_model(test.jpg, conf0.25)[0].boxes.xyxy print(a - b) # 数值差距应该非常小两份权重对同一张图的输出框坐标如果基本一致说明 ONNX 导出没有问题。后面如果要把模型跑到 RK3588 这类带 NPU 的板子上常见路径是把 ONNX 再转成 rknn 格式。我的习惯是转 rknn 之前先把 ONNX 在 onnxruntime 里的输出和 PyTorch 对到一致这样后面遇到精度损失时能快速定位问题出在量化还是转换步骤而不是在一堆玄学里瞎猜。转 rknn 前也建议把输入尺寸固定、batch 设成 1动态 shape 会让板端推理框架处理起来平白多出很多限制。这套流程走完一个「新场景 — 图片采集 — 标注 — 训练 — 导出」的闭环就跑通了。我自己吃过最大的亏是习惯一上来就上大模型结果显存、时间、标注质量全部跟不上最后还得退回小模型重跑。现在做任何新检测任务都先用 n 跑通、再慢慢放大这个顺序帮我省掉了好多个烧显卡的晚上希望帮到你。本文还有配套的精品资源点击获取