
YOLO26 目标检测的训练到瑞芯微平台适配这条链路我最近刚完整跑通一遍。先说结论真正卡人的地方不在训练本身而在从 PyTorch 模型走向 RKNN 运行时的那几步——ONNX 导出、算子兼容、INT8 量化每一个都埋着不少雷。我是在给一款基于 RK3588 的智能盒子做边缘检测功能时趟完这条路的前后折腾了两周模型训到可用只花了三天剩下时间全在跟转换工具链和后处理代码较劲。这篇就把这条链路的完整过程、关键参数和踩坑经验写出来给准备做边缘端目标检测、手上正好是瑞芯微 RK3588 这类板子的朋友做个参考。我默认你已经有基本的深度学习训练经验知道标注数据是怎么回事也能跑通简单的训练脚本。下面我从动机讲起一步步拆解训练到部署的完整链路。1. 为什么是 YOLO26 加瑞芯微这套组合的真实动机1.1 模型侧的版本取舍YOLO26 这个版本号社区里一开始也有人质疑说无非是在 YOLOv8 基础上又改了个大版本号。但实际用下来它在结构上确实延续了前几代的 C2f 模块和 DFL 检测头设计训练接口也保持了 Ultralytics 系列一贯的命令行一把梭风格。对我来说最重要的是它继承了 YOLO 系列对边缘部署相对友好的特性——模型体积可控、推理速度快、检测头可以直接剥离做定制导出。选择 YOLO26 而不是更早的版本主要原因有三个一是它的 n/s 轻量型号在 INT8 量化后模型文件只有几 MB非常契合板端存储和加载速度的要求二是新版本默认集成了更多数据增强策略同样的数据集下小目标召回率通常比老版本高一截三是新版本在后续工具链兼容性上持续在跟进导出 ONNX 时踩到历史遗留算子的概率会小一些。当然这种新也有代价就是网上的部署教程普遍滞后得自己折腾。1.2 平台侧的现实约束瑞芯微的 NPU 不是一个能用 PyTorch 直接推理的设备它需要把模型转成 RKNN 格式再由 RNPU 驱动加载运行。RK3588 的 NPU 算力在 6 TOPS 左右这块算力跑 YOLO26n 这种规模的模型绰绰有余RK3568 这种 1 TOPS 级别的芯片跑轻量模型也能达到可用帧率。相比英伟达的 Jetson 系列瑞芯微的方案在功耗和成本上有明显优势因此很多工业检测、物流分拣、智慧安防类的边缘设备都选了这条路。不过代价就是工具链相对封闭。瑞芯微官方提供的 RKNN-Toolkit2 负责在 PC 端把 ONNX 转换成 RKNN 模型板端再用 RKNN Runtime 或 rknn-toolkit-lite2 做推理。整个转换链路里ONNX 算子的支持程度、量化精度控制、后处理算子的放置位置都直接影响最终效果。这套组合真正的工作量不在地模型能不能跑而在能不能转得过去、部署起来顺不顺。2. 训练阶段容易被忽视的细节很多人觉得训练是套路活按默认参数跑就行。但从部署角度看训练阶段的很多选择会直接决定后面 RKNN 转换和推理精度能不能保持住。下面几个点是我实际验证过、认为比较关键的。2.1 数据集组织与标注格式如果你是从零开始做自己的数据集目录结构最好按 YOLO 系列一贯的约定组织dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamllabels 里每个 txt 文件对应一张图片每一行是一列类别和四个归一化坐标顺序是 class_id, x_center, y_center, width, height。这个格式看似简单但有个很容易出错的地方坐标必须归一化到 0-1 之间而且是以图片宽高分别归一化不是等比缩放后共用同一个尺度。我见过有人把中心点和宽高全部除以图片宽度导致检测框在竖图上全部变形训练时 loss 还不显示异常直到上板实测才发现框的位置全偏了。data.yaml 的内容也很直白path: dataset train: images/train val: images/val nc: 3 names: [person, vehicle, package]nc 和 names 的顺序必须和标注文件里 class_id 完全对齐。这个不检查清楚训练时模型学到的类别顺序是乱的部署后输出类别含义也是错的。2.2 训练参数与监控指标训练命令我用的就是 Ultralytics 系列的标准写法yolo detect train \ datadataset/data.yaml \ modelyolo26n.pt \ epochs200 \ imgsz640 \ batch32 \ device0 \ optimizerAdamW \ lr00.001 \ cos_lrTrue \ patience30几个参数值得展开说。imgsz 我建议训练时直接用 640不要为了省显存降到 320因为边缘部署时你大概率会希望保持 640 的检测精度训练和推理分辨率一致能减少 domain gap。batch 大小根据显存调整但如果 batch 低于 16建议把 lr0 适当调低到 0.0005 附近否则容易训飞。训练过程中的监控我是这样做的每个 epoch 看 loss 曲线的三项——box_loss、cls_loss、dfl_loss以及验证集上的 metrics/precision、metrics/recall、metrics/mAP50(B)、metrics/mAP50-95(B)。其中 mAP50-95 是综合指标mAP50 更贴近实际二分类场景的直观感受。如果 box_loss 已经收敛但 cls_loss 还一直抖往往是类别不均衡可以考虑给少数类加样本或者做类别权重。2.3 训练阶段要为部署预留的伏笔这一点是最容易被忽略的训练时所用的预处理方式必须和部署端保持一致。Ultralytics 系列默认是 BGR 输入、像素值范围 0-255推理前做 letterbox 缩放。后面 RKNN 转换时你要在 config 里设置 mean_values 和 std_values如果这里和训练预处理不一致量化后的精度会掉得非常难看。我在转换阶段就吃过这个大亏后面第四章详细说。另外如果部署目标是小目标居多的场景训练时可以显式加上 small_object 相关的增强策略比如提高 mosaic 和 mixup 的概率或者在训练后期关闭 mosaic 让模型适应真实分布。YOLO26 的默认增强已经不错但我还是建议在训练最后 20 个 epoch 关闭 mosaic这个技巧在很多实际项目中都能稳涨两三个点的 mAP50。3. 导出 ONNX决定 RKNN 转换成败的第一关3.1 导出命令与关键参数训练完成后导出 ONNX 是 RKNN 转换的前置条件。这一步如果没做对后面 RKNN-Toolkit2 会报出一堆莫名其妙的算子不支持错误而且很难排查。我的推荐导出命令是yolo detect export \ modelruns/detect/train/weights/best.pt \ formatonnx \ opset12 \ simplifyTrue \ dynamicFalse \ imgsz640 \ batch1几个参数值得重点解释。opset 12 是我在 RKNN-Toolkit2 上测试下来兼容性比较好的版本opset 太高会出现某些较新算子不被解析的情况太低又可能没有 YOLO 检测头需要的算子。dynamicFalse 必须显式指定因为 RKNN 在转换阶段要求输入形状完全固定动态尺寸会直接导致转换失败或得到错误的中间表示。simplifyTrue 会调用 onnxsim 对计算图做常量折叠和冗余节点消除这能显著减少 RKNN 的解析压力。batch1 也是强制的板端推理通常一次只处理一帧没必要导出 batch 大于 1 的模型而且 RKNN 对动态 batch 的支持很差。3.2 算子兼容性排查与模型改写导出后别急着转 RKNN先看一眼 ONNX 的算子列表。YOLO26 的检测头里有几个常见算子DFLDistribution Focal Loss解码相关的卷积和 softmax以及最后的 concat 拼接输出。RKNN 对纯卷积、Sigmoid、Concat 这类算子支持很好但对 DFL 里的一些 reshape 和 softmax 组合支持不稳定尤其在不同版本的 toolkit 上表现不一致。我的处理思路是导出一个剥离检测头的版本也就是只保留 backbone 和 neck输出原始三个特征图检测头的解码逻辑全部放到板端 CPU 上用代码实现。这样既绕开了 RKNN 对 DFL 算子的兼容性问题也让 NPU 只负责真正适合并行计算的卷积部分后处理反而更灵活。具体做法是在导出前对模型结构做一点小手术把 Detect 模块拆开或者在 ONNX 图上手动删除检测头节点保留三个特征层输出。Ultralytics 系列默认导出会包含完整检测头所以我通常用脚本加载模型后替换 self.model[-1] 为一个只输出三路特征的自定义 Module。这段代码不复杂但需要你对模型类结构有基本了解。核心思路是让导出的 ONNX 输出形状为 (1, 4nc, 80, 80)、(1, 4nc, 40, 40)、(1, 4nc, 20, 20) 这样的纯张量不要包含任何 DFL 后处理。如果你实在不想改模型结构另一个办法是导出后手动把包含 DFL 的节点从计算图中剥掉用 onnx 库遍历图、删除检测头到输出之间的算子再把输出边重新接到中间特征层上。这个办法对不熟悉模型结构的人更直观但要注意保持输入输出的张量名对应关系否则 RKNN 解析时会找不到输出节点。4. RKNN 模型转换一步一个坑4.1 RKNN-Toolkit2 基本转换流程RKNN-Toolkit2 是在 PC 端运行的 Python 包我用的是 x86 环境的 UbuntuPython 3.8 左右。安装完依赖后转换脚本的核心部分如下from rknn.api import RKNN rknn RKNN(verboseTrue) rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588 ) ret rknn.load_onnx(modelbest.onnx) if ret ! 0: print(load onnx failed) exit(-1) ret rknn.build( do_quantizationTrue, datasetdataset.txt ) if ret ! 0: print(build rknn failed) exit(-1) rknn.export_rknn(best.rknn)dataset.txt 是量化校准集每一行一个图片路径。这个文件的作用是给模型喂一批真实图片统计每个激活值的分布范围从而确定量化参数。我建议收集 50 到 200 张覆盖目标所有可能出现场景的图片不用带标注但一定要代表真实分布。比如你部署的摄像头是俯视角校准集里就不该全是平视角图片否则量化后模型对实际场景的分布估计是错的。mean_values 和 std_values 这里必须和训练预处理完全一致。YOLO26 训练默认不做 0-1 归一化而是直接输入 0-255 的原始像素值所以 mean 设 0std 设 255。如果你的自定义训练流程用了别的归一化方式这里要相应调整。我见过一个项目把 mean 设成 128std 设成 128模型转出来后检测框全歪排查了一天才发现是预处理不匹配。4.2 量化精度损失常见的掉点原因INT8 量化后模型掉点是一个躲不开的话题。正常情况下mAP50 掉 1 到 3 个点是完全可以接受的但如果你的模型量化后掉点超过 5 个点问题大概率不在网络本身而在以下几个方面。第一校准集数量太少。只放 10 张图就想把激活分布统计准几乎不可能。我一般至少放 80 张类别尽量平衡。第二校准集和实际部署场景分布偏差太大。这个前面说过了不赘述。第三某些敏感层被量化后误差被放大。这种情况可以尝试用 RKNN 提供的混合量化功能把特定层的量化精度从 INT8 提升到 FP16代价是这部分算子在 NPU 上运行效率降低但精度恢复很明显。RKNN-Toolkit2 提供了一些精度分析工具可以输出每一层量化前后的输出误差对比。我当时用它的精度分析接口定位到几个对量化敏感的输出节点然后把它们单独排除在量化之外最终 mAP 恢复到接近 FP32 的水平。这一步属于进阶操作但值得投入时间因为部署精度才是最终交付标准。4.3 常见转换报错与规避手段转换过程中最常见的报错有两类。一类是op not supported说明某个 ONNX 算子在当前版本的 RKNN-Toolkit2 中不支持。这种通常通过升级 toolkit 版本或者改模型结构来解决。我遇到的倒霉算子基本集中在 DFL 和部分特殊的 reshape 上用剥离检测头的方式一劳永逸。另一类是形状不匹配或动态维度报错。这多半是导出 ONNX 时 dynamicTrue 或者模型里有依赖输入尺寸的算子。解决办法是重新导出固定尺寸的模型并且在转换前用 onnx.checker 检查模型合法性。多说一句RKNN 转换报错信息有时候非常不直观建议遇到问题先在官方示例模型上跑一遍全流程确认工具链本身是健康的再回来查自己模型的问题。这样能省下大量排查时间。转换完成后rknn.build 阶段如果报 Warning我建议不要忽略尤其和内存布局、通道顺序相关的 Warning尽量都查一遍。5. 板端推理与后处理代码实现5.1 Python 快速验证拿到 best.rknn 之后上板之前我建议先在 PC 上用 RKNN 的 Python API 做一次对比验证确认模型转换后的推理结果和原始 PyTorch 模型基本一致。PC 上推理的代码很简单from rknn.api import RKNN rknn RKNN() rknn.load_rknn(best.rknn) rknn.init_runtime(targetrk3588) import cv2 import numpy as np img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img np.expand_dims(img, axis0).astype(np.uint8) outputs rknn.inference(inputs[img])注意这里的 img 形状要严格对应导出时的输入尺寸和通道顺序。rknn.inference 返回的是检测头输出的三个特征层数组每一层的形状是 (1, 4nc, grid_h, grid_w)。拿到这些原始特征后解码逻辑完全由你自己实现。5.2 C 接口部署要点Python API 适合验证但实际产品里通常还是要用 C 接口来部署。RKNN 的 C API 核心就五个函数rknn_init 负责加载模型并初始化运行环境rknn_query 查询模型输入输出信息rknn_inputs_set 设置输入数据rknn_run 执行推理rknn_outputs_get 获取原始输出。代码骨架大致如下#include rknn_api.h rknn_context ctx; rknn_init(ctx, best.rknn, 0, 0, NULL); rknn_input inputs[1]; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size 640 * 640 * 3; inputs[0].buf img_data; inputs[0].pass_through 0; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_output outputs[3]; outputs[0].want_float 1; outputs[1].want_float 1; outputs[2].want_float 1; rknn_outputs_get(ctx, 3, outputs, NULL); float* output0 (float*)outputs[0].buf;几个细节值得注意。第一want_float 设为 1 可以让驱动帮你做反量化直接拿到 FP32 的浮点输出开发阶段方便但如果你想省掉这层转换开销也可以直接获取 INT8 原始输出在后处理里自己反量化能省几个毫秒。第二输入数据布局要和训练一致通常 RKNN 默认是 NHWC也就是内存里按 H 方向连续排列 RGB 三个通道如果你手头的数据是 CHW需要先用代码转换。第三rknn_outputs_get 返回的 buffer 生命周期只在下一次 rknn_run 之前有效需要长生命周期保存或想异步处理的话要及时拷贝走。5.3 后处理实现DFL 解码和 NMS既然模型导出时剥离了检测头板端后处理就要自己实现 DFL 解码。以输入 640x640、输出三路特征层为例stride 分别是 8、16、32。每一层的输出维度是 4 * reg_max nc其中 reg_max 通常为 16。对某个格子先把 box 分支的 64 个值分成四组每组 16 个值做 softmax然后和预设的分布中心点相乘并求和得到相对格子中心的偏移距离再乘上 stride 并配合格子中心坐标换算成 x1、y1、x2、y2。类别分支做 sigmoid 得到各类别概率。整个流程用伪代码描述就是for each output layer: for each grid point: box_prob softmax(box[0:16]), softmax(box[16:32]), ... dist sum(box_prob * [0, 1, ..., 15]) * stride x grid_x dist_left y grid_y dist_top w dist_left dist_right h dist_top dist_bottom cls_scores sigmoid(cls[0:nc]) if max(cls_scores) threshold: save box, score, class_id做完这一步会得到大量候选框再用 NMS 按类别分别抑制重叠框。NMS 在 CPU 上跑如果候选框特别多会成为性能瓶颈。我的经验是置信度阈值别设太低比如 0.25 以下会导致候选框数量爆炸NMS 耗时直接翻倍。合理阈值通常在 0.3 到 0.5 之间具体看你的召回需求。后处理代码我建议用纯 C 实现避免引入额外的运行时依赖也不要为了省事把浮点运算全换成定点精度损失在边界框上体现得比较明显。如果必须优化性能优先做循环展开和局部缓存复用而不是牺牲数值精度。6. 实测效果与后续优化空间6.1 精度和帧率实测整条链路跑通后我在后端板子上做了完整测试。实验分三组FP32 模拟推理、INT8 量化推理、INT8 加裸 DFL 解码。对比下来INT8 量化后的 mAP 相比 FP32 掉了大概 1.5 个点主要体现在小物体和遮挡严重的场景这也是 INT8 量化的普遍现象。由于这是我的目标检测模型原始需求允许这个程度的精度损失因此没有进一步做混合量化。推理耗时方面单帧 640 分辨率输入NPU 推理时间大约 40 到 50 毫秒CPU 后处理包括 DFL 解码和 NMS 合计 5 毫秒左右整体帧率能稳定在 20 帧上下。这个成绩对大多数边缘检测应用已经够用。6.2 还能继续榨的性能空间如果对帧率还不满意有几个方向可以继续试。第一个是降分辨率从 640 降到 512 或者 416推理时间几乎等比下降代价是小目标召回率降低。第二个是换更小的模型变体YOLO26n 已经是最轻量的了再往下就只能减少类别数或者裁剪通道。第三个是流水线并行利用双缓冲机制让 NPU 推理和 CPU 后处理以及图像采集同时进行而不是严格串行这个优化通常能让整体帧率再提升 30% 左右。第四个是多线程 NMS把不同类别的 NMS 拆到多个线程并行候选框多的时候效果很明显。量化这块也有优化空间。如果对精度不够满意可以尝试用 QAT量化感知训练在训练阶段把量化误差纳入优化目标这样量化后的模型能保持更好的精度。RKNN-Toolkit2 对此有配套支持但 QAT 需要重新训练模型成本较高我一般只在 PTQ 效果实在达不到要求时才用。最后分享一个我自己用的土办法把 PC 端 RKNN 的推理输出和板端 RKNN Runtime 的推理输出做逐层对比确认两边中间结果完全一致后再去调后处理。这样可以把问题边界划清楚不会出现板端跑出来的结果不对却搞不清是模型转换问题、预处理问题还是后处理问题。这条链路踩坑不少但只要每一步都做验证整体并不复杂。希望这篇能帮你少走几个弯路。