前阵子手上接了个边缘设备视觉检测的需求,要把目标检测模型从服务器端搬到嵌入式板子上跑。研究了一圈,最终选了 YOLOv8 训练、瑞芯微 RK3588 平台配合 RKNN 工具链做推理落地。这条链路走下来,踩了不少坑,也攒了不少经验。这套流程我会拆成几篇文章来讲,第一篇先解决最基础的部分:环境搭建、数据集准备和模型训练。只有这一步走稳了,后面的 RKNN 转换和板端部署才有得玩。
要说清楚这套流程的适用对象:手里有或者准备入手 RK3588 系列开发板(RK3588S、RK3588J 同理),打算在板子上跑 YOLOv8 目标检测,但又没接触过 RKNN 工具链的朋友。如果你习惯于在 GPU 服务器上训练,从没碰过嵌入式部署,这篇文章也很适合做切入点。我默认你的训练机器是 Ubuntu 系统,因为后面 RKNN-Toolkit2 在 Linux 下最省心,Windows 虽然也能用但是幺蛾子多。
1. 为什么是 YOLOv8 + RK3588:先搞清楚这套组合的定位
动手之前,我建议先想明白一个事:为什么要选 YOLOv8,为什么要选 RK3588?很多人的思路是"别人用这个我也用",结果后面转换模型时各种踩坑,才开始后悔当初没多花点时间做选型。
1.1 RK3588 的算力画像与部署场景
RK3588 瑞芯微的旗舰 SoC,8 核 CPU(4×A76 + 4×A55),集成了 6 TOPS 算力的 NPU(INT8 精度)。这个算力水平在国产边缘设备里算比较能打的,关键是对比 NVIDIA Jetson 系列,RK3588 在成本上有明显优势。Jetson Orin Nano 8GB 的板子要两千多,二手都要一千七八,而 RK3588 的核心板整套下来几百块就能搞定(视具体配置),这对做量产设备的团队来说是个很大的考虑因素。
6 TOPS 的算力能跑什么模型?以 YOLOv8 为例,实测下来模型精度选 INT8 量化后的 YOLOv8s,输入分辨率 640×640,在 RK3588 的 NPU 上能跑到 30 FPS 左右;换成 YOLOv8n 能到 60 FPS 以上,实时性完全够用。这个性能覆盖了工业质检、安防监控、农业巡检、交通流量统计等绝大多数边缘检测场景。
1.2 YOLOv8 相比其他版本在端侧部署上的优势
YOLOv8 是 Ultralytics 在 2023 年推出的版本,相比 YOLOv5 做了几个关键改动:Anchor-Free 检测头(去掉 anchor 机制,减少后处理复杂度)、C2f 模块替代 C3(增强梯度流)、Decoupled Head(解耦分类与回归分支)。这些改动让模型精度和收敛速度都有提升。
那为什么不用更新的 YOLOv9、YOLOv10、YOLOv11?说实话我试过几个,在 RKNN 工具链上的支持成熟度是关键问题。YOLOv8 的结构相对规整,导出 ONNX 时基本不用改代码就能转;YOLOv9 用了可逆分支结构,YOLOv10 去掉了 NMS 但引入了 one-to-many 头,这些在 RKNN 转换时都可能出现算子不支持或需要额外处理的情况。对部署来说,"稳定能跑"比"理论精度高"重要得多。YOLOv8 在 RKNN 生态里几乎是"标准答案",参考资料多、踩坑记录全,出了问题基本能搜到解决方案。
1.3 整套流程的技术链路概览
整个部署链路可以概括为四个阶段:
- 阶段一:在 PC(GPU)上进行 YOLOv8 模型训练,得到 PyTorch 权重(.pt)
- 阶段二:将模型导出为 ONNX 格式,做算子检查和优化
- 阶段三:使用 RKNN-Toolkit2 将 ONNX 转换为 RKNN 格式,同时做 INT8 量化
- 阶段四:在 RK3588 板端使用 RKNN Runtime(Lite)加载模型完成推理
本文对应的是阶段一,也是整个链条的地基。后续文章会详细展开阶段二到四的操作细节。这里先给一个整体图景,让你清楚自己在哪个位置。
2. 训练环境搭建:动手前的版本匹配,少走三天弯路
环境搭建这个环节,看别人教程总觉得简单,自己做的时候才会发现版本兼容性就是个坑。我印象最深的一次踩坑:Ubuntu 系统装好了,NVIDIA 驱动也装好了,结果因为 CUDA 版本和 PyTorch 不匹配,import torch 之后 GPU 根本不可用,搞了一整天。
2.1 硬件与系统选型:别小看这个基础
先说硬件。训练 YOLOv8 不一定需要多高端的显卡,但最好不要用 CPU 硬扛——YOLOv8s 在 CPU 上训练一个 epoch 要几分钟到十几分钟,一百个 epoch 就是十几个小时甚至更久。实测下来,一张 8GB 显存的显卡(比如 RTX 3060Ti、RTX 3070)就能很舒服地训练 YOLOv8s,batch size 开到 16 毫无压力;如果只有 6GB 显存,YOLOv8n 也能跑,只是 batch 会小一些。
系统方面,推荐 Ubuntu 20.04 或 22.04。这里有个很实际的原因:瑞芯微官方的 RKNN-Toolkit2 在 Ubuntu 18.04/20.04/22.04 上都有完善的安装支持,但 20.04 的案例最多、最稳。后面转换模型时你会发现,折腾系统的成本远比安装几个包要高。
2.2 CUDA、PyTorch、Python 的版本匹配
这是环境搭建的核心,也是最容易出问题的地方。PyTorch 官方安装命令里通常会标注对应的 CUDA 版本,而 CUDA 版本又受限于 NVIDIA 驱动的版本号。驱动版本和 CUDA 版本的关系是:驱动向下兼容,高版本的驱动支持低版本的 CUDA。
我的建议是直接按这个组合来配:
| 组件 | 推荐版本 | 原因 |
|---|---|---|
| 操作系统 | Ubuntu 20.04 | RKNN-Toolkit2 支持最完善 |
| Python | 3.9 | 兼容 PyTorch 2.x 和 RKNN-Toolkit2 |
| CUDA | 11.8 | PyTorch 和 TensorRT 支持稳定 |
| PyTorch | 2.0.1 | 与 CUDA 11.8 匹配,bug 修复充分 |
| 显卡驱动 | ≥ 525 | 支持 CUDA 11.8 |
安装驱动有两种路径:一是通过 Ubuntu 的"软件和更新"里的附加驱动选项卡选专有驱动(适合新手和日常使用);二是从 NVIDIA 官网下 .run 文件安装(适合对版本有精确要求的情况)。我推荐第一种,简单可靠,一般不会把系统搞坏。装完驱动后执行nvidia-smi,能看到显卡型号和驱动版本就说明驱动 OK。
CUDA 装的时候有个容易忽略的细节:不要用apt install cuda,因为系统源里的版本可能和你 PyTorch 需要的对不上。建议直接从 NVIDIA 官网下载 CUDA Toolkit 11.8 的 runfile,只装 Toolkit 组件,不装驱动(驱动已经装过了),也不装 Samples。
2.3 conda 虚拟环境创建与验证
Python 环境管理这块,在原版系统 Python 里直接 pip 装包会把系统环境搞得很乱。用 conda 或 virtualenv 都行,我更推荐 Miniconda,因为它体积小、创建环境快,而且后面的 RKNN-Toolkit2 在 conda 环境里安装最顺。
# 安装 Miniconda(官网下载对应版本或直接用命令) wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh # 创建 YOLOv8 训练环境 conda create -n yolov8 python=3.9 -y conda activate yolov8 # 安装 PyTorch(注意指定 CUDA 版本,用清华源或官方源) pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics(YOLOv8 的官方训练库) pip install ultralytics # 验证 GPU 可用性 python -c "import torch; print(torch.cuda.is_available()); print(torch.__version__)"如果上面最后一步输出True,说明你已经在用 GPU 训练了。注意torch.cuda.is_available()返回 False 的原因排查顺序:驱动是否正常(nvidia-smi)→ CUDA 版本是否匹配(nvidia-smi 显示的最高 CUDA 版本要 ≥ PyTorch 需要的)→ PyTorch 是否是 cu118 版本(而不是默认的 CPU 版本)。
到这里训练环境就算搭完了。但我建议先别急着训练,顺手安装一下后面一定会用到的 RKNN-Toolkit2,这样后面转换模型时不会手忙脚乱。
# 在另一个 conda 环境安装 RKNN-Toolkit2,避免污染训练环境 conda create -n rknn python=3.9 -y conda activate rknn pip install --user rknn-toolkit2==2.0.0b0 -i http://pypi.douban.com/simple --trusted-host pypi.douban.com这里有个经验:训练环境和 RKNN 转换环境最好分离。因为训练环境可能需要频繁升级(PyTorch 版本更新会影响模型训练行为),而 RKNN-Toolkit2 对环境依赖比较敏感,升级某个包可能导致它直接罢工。分开之后各自维护,互不干扰。
3. 数据集准备:标注格式转换,细节决定训练上限
数据集的准备是最花时间、也最容易被低估的一个环节。很多初学者第一次接触,都是随便找些图片标注,跑出来的模型精度一塌糊涂,还以为是训练参数的问题,其实问题出在数据本身。
3.1 数据采集与清洗的规范
先说采集。训练数据要尽量贴近实际部署场景:光线条件、拍摄角度、目标尺寸范围、背景复杂度,这些都要覆盖到。如果部署场景是厂区白天的监控画面,你拿网上下载的夜景图片来训练,效果一定好不到哪儿去。我见过一个里项目,客户提供的视频素材是静止机位拍的,训练结果在相似机位下很好用,换个角度直接废了——这是数据分布没对齐的典型表现。
采集之后是清洗,这一步很容易被忽略。主要做三件事:
- 剔除严重模糊、过曝、遮挡目标的图片(这类图片会让模型学到错误的特征)
- 删除重复图片(避免训练集和验证集里出现同一张图片,否则评估结果虚高)
- 检查图片分辨率,建议统一到 640×640 或相近尺寸(YOLOv8 默认输入就是 640,图像太大或太小都会影响训练效率)
区分训练集和验证集也有讲究。很多人写个脚本把所有图片打乱后按比例随机分成两份,这个做法在大部分场景没什么问题,但如果数据是从视频里抽帧来的,同一条视频前后相邻的帧内容高度相似,会导致验证集的指标虚高。正确的做法是按视频或按场景分组划分,比如一段视频的帧全部归入训练集,另一段视频的帧全部归入验证集,这样验证指标才真实反映模型的泛化能力。
3.2 labelme 标注实操与格式理解
标注工具方面,LabelImg 是老牌工具,但它是基于 Python 2 的,在 Ubuntu 20.04 上安装比较麻烦。labelme 是更现代的选择,pip 直接装就能用,标注结果保存为 JSON 文件。
pip install labelme labelmelabelme 的界面操作虽然简单,但有两个容易忽略的设置:一是打开图片前先在 Edit 菜单里选择 Create Polygons 而非 Create Rectangles 或 Create Circle,因为 YOLOv8 的标注只需要矩形框(bounding box);二是标注完每个框之后会弹出对话框让你填写类别名,一定要提前定义好类别列表,避免同一个类别出现两种写法(比如 "person" 和 "Person" 是两种情况)。
labelme 产出的 JSON 文件内容大致是:
{ "version": "5.2.1", "flags": {}, "shapes": [ { "label": "cat", "points": [[132.0, 48.0], [318.0, 320.0]], "group_id": null, "shape_type": "rectangle", "flags": {} }, { "label": "dog", "points": [[190.0, 210.0], [400.0, 480.0]], "group_id": null, "shape_type": "rectangle", "flags": {} } ], "imagePath": "cat_dog.jpg", "imageData": null }这个格式里points是两个顶点坐标,左上角和右下角。这个 JSON 文件不能直接被 YOLOv8 使用,YOLO 格式要求的是每个目标一行,格式为:类别ID x_center y_center width height,其中 x_center、y_center、width、height 都是归一化到 0~1 的浮点数(相对于图片宽度和高度)。
3.3 格式转换脚本:JSON 到 YOLO txt 的每一步
这个转换脚本我通常直接写,不依赖第三方库,因为逻辑很简单,自己写反而最可控。脚本的核心逻辑是读取 labelme JSON,把像素坐标换算为归一化坐标。
import json import os # 类别映射表,注意 ID 从 0 开始! CLASS_MAP = {"cat": 0, "dog": 1} def convert_labelme_json(json_path, txt_path, image_width, image_height): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) lines = [] for shape in data['shapes']: label = shape['label'] if label not in CLASS_MAP: continue # 矩形框的两个顶点 pt1, pt2 = shape['points'] x1, y1 = pt1 x2, y2 = pt2 # 确保 x1<x2, y1<y2(有些工具标注时可能反过来) x_left = min(x1, x2) x_right = max(x1, x2) y_top = min(y1, y2) y_bottom = max(y1, y2) # 计算中心点、宽度、高度并归一化 box_width = x_right - x_left box_height = y_bottom - y_top x_center = x_left + box_width / 2.0 y_center = y_top + box_height / 2.0 x_center_n = x_center / image_width y_center_n = y_center / image_height width_n = box_width / image_width height_n = box_height / image_height lines.append(f"{CLASS_MAP[label]} {x_center_n:.6f} {y_center_n:.6f} {width_n:.6f} {height_n:.6f}") with open(txt_path, 'w') as f: f.write('\n'.join(lines) + '\n') print(f"Converted: {json_path} -> {txt_path}")关于图片尺寸,脚本里的image_width和image_height必须要和实际图片保持一致。我一度因为偷懒直接在脚本里写死尺寸,换了一批图片之后转换出的 txt 坐标全部错位,训练出来模型对目标框的定位完全乱掉。
还有一类坐标要注意:labelme 的points用的坐标系原点在图片左上角,X 轴向右,Y 轴向下。如果你的图片有 EXIF 旋转信息,labelme 读取时可能会自动旋转显示,但保存的 JSON 坐标仍然是原图坐标系,这会导致标注框和图片内容不一致。处理方式是先用脚本把所有图片统一转为无 EXIF 旋转的 JPEG,再开始标注。
3.4 数据集划分:不要忽略时序相关性
转换完格式后,就到了数据集划分这一步。YOLOv8 需要的数据目录结构如下:
datasets/ my_data/ images/ train/ img_0001.jpg img_0002.jpg val/ img_0101.jpg img_0102.jpg labels/ train/ img_0001.txt img_0002.txt val/ img_0101.txt img_0102.txt这里有个很容易犯的错误:图片集和标注 txt 文件命名必须完全一致才能被 ultralytics 正确匹配,包括扩展名。我建议在转换脚本里直接保持原始文件名,不要做任何改名操作。如果确实需要重命名,务必同时修改对应的 txt 文件名。
目录结构准备好之后,我们再统计一下各类别的样本数。这一步很关键:如果某个类别的标注框数量显著少于其他类别,训练时模型会倾向于把该类别预测为其他类别,或者干脆预测不到。处理方法有三个:一是采集更多该类别图片;二是对该类别的图片做离线增强(旋转、翻转、亮度变化)以扩充数量;三是适当提高"损失函数"中该类别对应的权重,但 ultralytics 默认没有暴露这个参数,实操中多数靠前两种方式解决。
按照我的经验,数据准备的完整顺序是:采集原始图片 → 清洗整理 → 统一尺寸和格式 → labelme 标注 → 脚本转换为 YOLO 格式 → 检查各类别数量 → 划分数据集。每一步都做完再开始训练,后面能省下大量的排查时间。
4. 模型训练:跑通只是第一步,看懂结果才算数
环境搭好了,数据也备齐了,现在进入训练环节。很多人的想法是"命令行跑起来就完事了",但实际训练过程的监控和结果的解读,直接影响后续整个部署流程的成败。
4.1 ultralytics 环境与自定义 yaml 配置
上面我们已经通过pip install ultralytics装好了训练库。训练自定义数据集需要准备一个描述数据集信息的 YAML 配置文件。在项目目录下创建my_data.yaml:
# 数据集配置文件 path: /home/user/datasets/my_data # 数据集根目录的绝对路径 train: images/train # 训练集图片目录(相对于 path) val: images/val # 验证集图片目录 names: 0: cat 1: dog这里的names必须和转换脚本里的CLASS_MAP完全一致,ID 从 0 开始递增。如果 YAML 里定义的类别名和标注 txt 里的 ID 对不上,训练时模型会静默跳过对不上的标注框,导致有效样本数变少,训练结果指标很低——这个坑非常隐蔽,我当时排查了很久才发现是 YAML 里漏了一个类别。
4.2 训练参数设置:从 epochs 到 batch size
ultralytics 提供了一组合理的默认参数,但实际训练时还是要根据自己的数据和硬件做一些调整。我常用的完整训练命令是这样的:
yolo detect train \ --model yolov8s.pt \ --data my_data.yaml \ --epochs 100 \ --batch 16 \ --imgsz 640 \ --device 0 \ --workers 8 \ --project runs/train \ --name my_yolov8s_exp1 \ --pretrained参数的含义和选择逻辑我逐个说一下:
model选择yolov8s.pt还是yolov8n.pt,本质是精度和速度的折中。如果你的部署设备是 RK3588,我建议直接用yolov8s,因为s在量化后精度损失通常还可以接受,速度也不会拉胯;n虽然更快但精度下降明显。当然如果数据非常简单(只有两三类目标),n也够用。epochs默认是 100。数据量小的时候 100 轮可能早就收敛了,可以看训练曲线提前终止;数据量大(几千张图)的时候 100 轮往往不够,可以加到 200 甚至 300。我习惯先用 100 轮跑通,再根据 loss 曲线的趋势决定是否加跑。batch设 16 怎么样取决于显存。--device 0指定用第一张 GPU。imgsz设为 640,这是 YOLOv8 的默认输入尺寸,也是后续 RKNN 量化时推理分辨率改起来最方便的设置——部署阶段模型输入尺寸会严格绑定训练尺寸,换尺寸可能精度下降。pretrained使用 COCO 预训练权重做迁移学习。这是个非常重要的加速前提:预训练模型已经学会各种底层特征(边缘、纹理、颜色),只需要微调上层即可,收敛速度远快于从零训练。
4.3 训练过程监控与常见问题
训练启动后,终端会实时打印每个 epoch 的 loss 和 metric。这里有几个应该关注的指标:
box_loss(边界框回归损失)、cls_loss(分类损失)、dfl_loss(分布焦点损失)这三个 loss 应该整体呈下降趋势。如果 loss 在训练后期出现反弹(先降后升),说明学习率可能太大,或者模型过拟合了,需要调低学习率或加早停(ultralytics 默认有早停机制,patience=50 会默认开启)。如果 loss 卡在一个平台期怎么都降不下去,常见原因有两个:一是学习率太小,二是数据集里有大面积标注错误(标签噪声),建议抽样检查数据。
GPU 显存不足是新手最容易遇到报错,OutOfMemoryError或者CUDA out of memory。解决办法按优先级排序:
- 调小 batch size,从 16 降到 8 或 4;
- 调小
imgsz,从 640 降到 512(注意后面部署时也要用对应尺寸); - 开启梯度累积,ultralytics 支持
--batch 16 --device 0之外加--fraction 0.5(使用 50% 显存)。
训练刚开始的几分钟是最容易慌的时候:mAP 先是 0,过几个 epoch 才开始涨,很多新手以为模型训练失败了。其实这是正常的——模型还没学会任何东西,指标自然为零。按我的经验,mAP 一般从第 10 个 epoch 左右开始明显上升,如果 20 个 epoch 之后还是 0,才需要停下来检查问题。
4.4 结果评估指标解读
训练完成之后,ultralytics 会在runs/train/目录下生成一组结果文件,包括results.png、confusion_matrix.png、PR_curve.png等。理解这些指标比直接看 mAP 数字重要得多。
mAP50和mAP50-95是核心指标。mAP50表示 IoU 阈值为 0.5 时的平均精度,容易达到较高的数值;mAP50-95是多个 IoU 阈值的平均,更能反映定位精度。目标检测模型训练,通常 mAP50 会先上去,mAP50-95 慢慢跟着涨。如果 mAP50 很高(0.95+)但 mAP50-95 很低(0.5以下),说明模型定位不够精细,通常和目标框标注不够准确有关。
混淆矩阵(confusion_matrix.png)能直观看出模型容易在哪些类别之间搞混。比如猫狗分类,如果混淆矩阵显示大量"猫被预测为狗",说明训练集中这两个类别的特征不够可分,或者狗的照片里混入了大量猫的背景。
PR_curve.png(Precision-Recall 曲线)反映的是置信度阈值变化时精确率和召回率的权衡。部署时你会选一个置信度阈值,比如 0.5 或 0.3。如果想提高检出率(减少漏检),就要用低一点的阈值(比如 0.25),代价是误检率会上升;如果误检代价高(比如报警系统),阈值设高一些更稳妥。
4.5 导出工作:为 RKNN 转换做第一步准备
训练完成,精度也符合预期之后,就要做导出 ONNX 这个动作了。这时你训练好的模型还是 PyTorch 的.pt权重,RKNN 工具链不认识它,需要先转成 ONNX,RKNN-Toolkit2 才能在此基础上转换成 RKNN 格式。
yolo export model=runs/train/my_yolov8s_exp1/weights/best.pt format=onnx opset=12几个细节注意一下:
opset建议设为 12,RKNN-Toolkit2 对 opset 12 的 ONNX 模型支持最完整。- 导出完成后检查一下 ONNX 文件里的算子列表,确认没有 RKNN 不支持的算子。常用的
Conv、Relu、Add、Concat、Resize都没问题,如果发现奇怪的自定义算子(用onnx.checker可以检查模型结构),多半是训练时有特殊模块,需要在导出时删掉。部署时只保留模型前向推理需要的部分,后处理(NMS)由 RKNN Runtime 实现或自己在板端写。 - 如果你的模型里用了自定义的预处理(比如归一化到特定范围),记得在配置文件里记录清楚,后面 RKNN 量化时要用到。YOLOv8 默认预处理是
(x / 255)归一化到[0, 1],如果没做特殊处理,保持默认就好。
到这里,训练阶段的工作就完成了:有了一个训练好的.pt文件,又有了一个导出的.onnx文件,下一篇文章我会继续讲如何在 PC 上使用 RKNN-Toolkit2 把 ONNX 模型量化和转换成 RKNN 格式,然后部署到 RK3588 板端完成推理。
回到整个流程,我现在越来越觉得,边缘部署这件事,模型的训练精度固然重要,但更关键的是从第一天就带着"最终要在 NPU 上跑"的意识去选模型、定参数、设计数据流程。数据库采集时想清楚部署场景的光线、角度、目标尺度,训练时固定好输入尺寸,导出时保证算子和 RKNN 对齐——每一步都为下一步留好接口,整条链路才会顺畅。