最近把 yolov4 在 KV260 上完整跑通了一遍,从训练到量化、编译再到部署,整个链路走下来最大的感受是:边缘部署真正的难点不在模型本身,而在每个环节之间的衔接。网上关于 KV260 的 DPU 部署资料不少,但大多只讲某一段,比如只讲量化或者只讲编译,很少有人把“训练完的 .weights 怎么一步步变成板子上能跑的目标检测程序”这件事完整串起来。这篇文章就按我实际操作的过程,把 yolov4 模型在 KV260 上的训练、量化、编译、部署全流程拆开讲清楚,包括每个环节的产出物、关键参数、踩过的坑和最终的实测效果。适合手里有 KV260 开发板、准备跑目标检测任务,或者正在做边缘端 AI 部署的开发者参考。
1. 项目概述与整体流程
1.1 KV260 平台与 yolov4 的适配关系
KV260 是面向边缘视觉应用的设计平台,核心优势在于集成了可编程逻辑资源和一个专门做深度学习推理的 DPU 计算单元。DPU 不是一个通用的 GPU,它有自己支持的算子集合和网络结构约束。这就带来了一个非常实际的问题:训练好的模型不能直接拿到板子上跑,必须经过一层和硬件绑定的“翻译”过程。yolov4 作为目标检测领域非常经典的模型,结构相对规整,主干 CSPDarknet53、颈部 SPP+PANet、头部 YOLO Head 三大部分都是常规卷积、残差、拼接、上采样操作的组合,恰好落在 DPU 的算子支持范围内。这也是我选择 yolov4 而不是其他更新、更复杂的检测模型的原因之一——模型结构和 DPU 算子的匹配度直接决定了部署工作量。
官方工具链对 yolov4 的支持已经比较成熟,提供了从 Darknet 训练权重到 DPU 可执行文件的完整转换路径。但要注意的是,这里的“成熟”指的是转换流程本身,并不是说你随便训练一个 yolov4 就能顺利部署。训练时的一些细节,比如输入分辨率、归一化方式、anchors 尺寸、是否使用 mish 激活等,都会影响后续量化和编译的顺利程度。KV260 上的 DPU 版本不同,指令集和算子支持也有差异,编译时选错 DPU 配置会直接报错,这些我在后面都会详细展开。
1.2 全流程链路拆解:训练、量化、编译、部署
从训练到部署,整个链路可以分成四个清晰的阶段,每个阶段的产出物就是下一个阶段的输入:
- 训练阶段:用标注好的数据集训练 yolov4,产出权重文件,格式通常是 .weights(Darknet 训练)。
- 量化阶段:把浮点模型转换成 INT8 量化模型,需要准备校准图片集,产出量化后的模型文件。
- 编译阶段:把量化后的模型编译成 DPU 可执行的指令集文件,产出 .xmodel 文件。
- 部署阶段:在 KV260 上加载 .xmodel 文件,写推理代码,把检测结果输出出来。
这四个阶段并不是完全独立的,前面的任何一步犯懒,后面都要加倍偿还。比如训练时如果图省事不改默认输入分辨率,量化时压缩得厉害,编译时算子不匹配报错,最后部署时精度垮掉,你还得回头去找是哪一步出了问题。我自己的建议是把整个链路的关键参数提前定好,尤其是输入分辨率、训练数据格式、量化校准集数量,后面每一步都保持一致性,能省掉大量排查时间。
2. 模型训练环节
2.1 数据集准备与标注要点
yolov4 的训练需要标注好的目标检测数据集,标注格式采用标准 Darknet 格式:每张图片对应一个文本文件,每一行是class_id x_center y_center width height,其中坐标和宽高都是相对于图片尺寸归一化后的值。不管你是自己标注还是用现成数据集,在开始训练之前都要把这个格式确认好。我实测过的最容易出问题的地方有两个:一是坐标归一化时用像素值除以图片宽高时很容易算错,导致训练时 loss 异常;二是类别编号和配置文件里的 classes 数量、名字顺序一定要对齐,否则检测结果张冠李戴。
标注工具方面,LabelImg 或者 更专业的标注平台都可以,导出为 YOLO 格式是最省事的路径。我自己用的标注数量不大,大概两千多张,标注目标以车辆和行人为例,类别就只定了两类,所以训练难度不算高。如果你的目标类别很多或者目标尺寸差异很大,建议在标注完成后统计一下所有目标的宽高分布,后面计算 anchors 时要用到。
另外还要提一个训练集和验证集的划分问题。很多人图省事直接随机切分,但检测任务里同一张场景的相似图片如果同时出现在训练集和验证集里,验证集的意义就大打折扣。我习惯按场景或者时间段来划分,确保验证集里的图片在内容上和训练集有明显差异,这样后期评估模型真实泛化能力才有参考价值。
2.2 网络结构与 anchors 参数计算
yolov4 的检测头有三个尺度,分别对应 13×13、26×26、52×52 的特征图。每个尺度上每个格子预设三个 anchors,总共九组。默认配置里的 anchors 是 COCO 数据集上计算出来的,适用于 80 类通用目标。但因为我自己的目标是小样本场景,车辆和行人的尺度和 COCO 差异不大,所以我重新基于自己的数据集计算了一组 anchors。计算方式用 K-means 聚类,距离度量用 1 - IoU,对标注框的宽高做聚类。我用 Python 脚本跑聚类的时候,选择的聚类中心数量 k=9,迭代过程中发现不同初始化对结果有一定影响,所以多跑了几次取最稳定的结果。
计算 anchors 时有一个细节:聚类输入是归一化后的宽高,还是像素值,结果会有细微差别。我的建议是直接用像素值聚类,然后在配置里换算成相对于输入尺寸的像素值,因为 anchors 本身的含义就是在原生输入分辨率下的框尺寸。我最终使用的配置如下:
- 输入分辨率:416×416
- 聚类得到的 anchors(以像素为单位):小尺度(52×52 特征图)对应 10×14, 23×27, 37×58;中尺度对应 81×82, 135×169, 217×137;大尺度对应 301×275, 375×222, 442×426
这个 anchors 用在自己的数据集上比默认值收敛更快,前几十个 epoch 的 loss 下降趋势明显更顺。
2.3 训练策略与关键参数
训练阶段,我用的是与 yolov4 原生配套的训练框架,硬件上就一块普通消费级显卡。因为数据集规模不大,网络结构也相对成熟,所以训练策略比较常规:输入尺寸先以 416×416 训练,后面如果还想提升精度可以考虑微调到 608×608,但考虑部署时 KV260 的 DPU 算力,最终就锁定在 416×416。
关键参数给出一份可复现的配置参考:
- batch size:64(如果显存不够就用 32,但需要同步调整学习率)
- subdivisions:8 或 16,用来把一个大 batch 拆成多个小 batch 前向反向
- learning rate:初始 0.001,采用 step 策略在迭代次数到 60%、90% 的时候各衰减一次,衰减系数 0.1
- momentum:0.949
- decay:0.0005
- max_batches:根据数据集大小和类别数量设置,类别数×2000 通常是一个合理起点,我对 2 类目标设置了 12000 轮
- burn_in:1000,让学习率在前 1000 轮慢慢爬升,避免初期 loss 爆炸
还有一个容易被忽略的参数是online或者数据增强相关的开关。yolov4 在训练时默认使用 mosaic、random affine 等数据增强策略,这些策略对最终模型的鲁棒性提升很大,但在量化部署阶段,训练时用的增强和预处理方式必须和推理时对齐。特别是归一化方式,Darknet 训练里图像的归一化是除以 255 做的,这个在量化时也要保持一致,否则 INT8 校准会失真。
训练过程中我自己会盯两个指标:一个是 avg loss 的下降曲线,另一个是验证集上的 mAP。avg loss 降到 0.05 以下基本可以停掉。因为数据集小,训练时间不长,大约几个小时就完成了。训练完成后会得到yolov4_final.weights,这个就是后续量化阶段的输入。顺便说一句,不要只留最后一步的权重,训练过程中定期保存的权重建议都留着,后面如果量化精度不理想,可以回溯到不同 epoch 的权重重新尝试量化。
2.4 训练结果评估与模型导出
训练完成后,先用验证集跑一波 mAP 评估。我用的工具是官方脚本,输入模型权重和配置文件,输出每个类别的 AP 和整体 mAP。实测下来两个类别的 AP 分别到了 78.6 和 82.3,整体 mAP 80.5 左右,对于小数据集来说这个结果是可以接受的。评估的目的不仅仅是确认模型效果,更重要的是筛选出量化精度最高的候选权重。我有过一次经历,训练后期 val loss 略微回升,但最后一次保存的权重反而在验证集上 AP 最高,所以不能只看 loss 曲线,还是要在验证集上实际跑一遍。
接着把权重文件整理好,放在工作目录下等待量化。此时需要记录几个信息,这些在后面配置量化流程时要用到:网络输入分辨率(416×416)、类别数量(2)、anchors 列表、训练时的归一化方式。信息缺失是很多人在量化阶段报错的主要原因,尤其是 anchors 填错导致的编译后输出框严重偏移。
3. 模型量化
3.1 为什么要量化:FP32 到 INT8 的转化逻辑
KV260 的 DPU 为了在有限的硬件资源里跑出足够的算力,普遍采用 INT8 定点运算代替原先的 FP32 浮点运算。这和手机端的 NPU 思路是一样的:参数从 FP32 变成 INT8,存储带宽需求减到四分之一,算力利用率大幅提高。代价是数值精度下降,毕竟 FP32 的动态范围和 INT8 完全不在一个量级,所以量化的核心就是在“压缩”和“保精度”之间找平衡。
但量化不是简单地做四舍五入。直接对权重做线性缩放会带来很大的精度损失,实际流程是:把训练好的浮点模型在大量输入数据上做一次前向推理,统计每一层激活值的动态范围,然后为每个张量选择合适的浮点缩放因子,把动态范围映射到 INT8 的 [-128, 127] 区间。这个过程需要一组有代表性的输入图片,也就是校准集。校准集不需要带标注,但需要尽量覆盖训练数据的分布,数量不需要太多,几百张到一千张都有。我实测下来 500 张效果和 1000 张差别不大,但少于 100 张时精度会明显下滑。
量化对 yolo 系列模型算比较友好的。卷积层、全连接层的权重和激活值分布相对集中,量化后精度损失通常能控制在可接受范围内。但 yolov4 里有 concat 操作、上采样操作和 shortcut 操作,DPU 对这些算子的量化策略各有不同,容易成为精度的短板。如果量化后 mAP 掉得太多,优先从这些算子入手排查。
3.2 校准集准备与量化配置
在开始量化前,先准备好一个文件夹,里面放校准图片。注意几点:第一,图片内容要和你实际部署时面对的输入分布一致,也就是说你部署后要检测车,就不要拿一堆风景图做校准集;第二,图片分辨率不需要和训练输入一致,量化工具会自动做尺寸调整,但建议直接用接近部署分辨率的图片,减少前处理差异;第三,图片数量不用贪多,我最终选了 500 张,均匀覆盖了光照、角度、目标大小等不同情况。
量化工具本身需要读取训练好的权重文件,同时要提供一个包含网络结构信息的数据集描述文件。对于 yolov4,最简单的方式是直接用训练时的 cfg 文件作为网络结构描述。量化流程开始后,工具会先用浮点模型跑一遍校准集,收集每层的激活分布,然后生成阈值参数。量化完成后会输出两个东西:量化后的模型文件和一份精度报告。精度报告里能看到每一层的量化误差,但这个报告在 yolov4 场景下参考意义有限,我一般只看最终的聚类精度或者直接做一次验证集推理比对。
配置里有一个比较隐蔽但极其关键的参数是量化输入尺寸。有的版本会默认使用 cfg 文件里的输入尺寸,有的版本需要单独指定。我踩过一次坑:cfg 文件里写的是 608×608 训练尺寸,但我准备部署成 416×416,量化时工具按 608 建模,编译后到板子上跑 416 的输入就报 shape mismatch。解决方法是先在 cfg 文件里把 width 和 height 改成最终目标尺寸,再拿去量化和编译。这个顺序别搞反。
3.3 量化精度验证与反复迭代策略
量化完成后不代表流程就结束了,必须做一次精度验证。我用的方法是直接用量化后的模型在验证集上做一轮前向推理,计算 mAP,和之前浮点模型的 mAP 对比。两者的差距就是量化损失。我的第一次量化结果 mAP 从 80.5 掉到了 76.2,掉了 4.3 个点,这个幅度对目标检测来说偏高,不能直接接受。
排查后发现两个问题:一是校准集里包含了大量完全没有目标的大背景图片,这些图片对激活值统计产生了很多低响应值,拉宽了动态范围;二是校准集数量太少,只有 120 张,导致统计不够稳定。换了一批目标更密集的校准图片、数量增到 500 张之后,重新量化,再测 mAP 到了 78.9,损失缩小到 1.6 个点,这个精度损失就可以接受了。
所以我的建议是:量化是一个迭代过程,不是跑一次就完事。当你发现精度损失超标,优先调整校准集的构成和数量,再考虑调整模型本身的参数,比如把 mish 换成 relu(如果 DPU 对 mish 支持不好,这一步在量化阶段甚至会更早暴露,编译阶段会直接报算子不支持)。实测下来,DPU 对 mish 的支持取决于编译器的算子库版本,如果编译时报 mish 算子不支持,需要在训练阶段就把激活函数换掉重新训练。
4. 编译与 DPU 部署
4.1 编译工具与命令解析
量化完成后得到的是一个中间格式模型,还不能被 DPU 直接执行,需要经过编译生成 DPU 的可执行文件(.xmodel)。编译工具做的事情可以理解成“把网络结构翻译成 DPU 的机器指令”。编译过程有几个重要输入:量化后的模型文件、DPU 配置(包括 DPU 架构型号、 DPU 核数)、编译选项(比如输入尺寸、节省内存模式等)。
我使用的编译命令大致是这样的:
vai_c_xir --prototxt quantized_model.xmodel \ --dpu_arch DPUCZDX8G_ISA1 \ --cpu_arch arm64 \ --output_dir ./output \ --net_name yolov4_416这里--dpu_arch指定 DPU 架构类型,不同的 KV260 上可能烧录了不同版本的 DPU IP,架构名要对上,否则编译出来的模型在板子上加载不了。你可以用官方提供的工具查询板子上的 DPU 架构名,再填进来。--cpu_arch arm64表示部署端是 ARM 处理器,这个参数影响编译后的模型组织方式。
编译后输出的 .xmodel 文件就是板子上要用的最终模型。除了 .xmodel 之外,通常还会输出一个 meta.json 文件,里面有模型的输入输出节点信息,部署时读取这个文件可以避免手写节点名出错。
4.2 DPU 配置与 JSON 解析文件
部署环节除了 .xmodel 之外,还需要一个描述运行时参数的 JSON 文件,内容包括模型路径、输入分辨率、类别数量、anchors 等。DPU 本身并不直接解析 yolov4 的输出,它只负责把所有卷积层算完,输出的是三个特征图。你要在 ARM 端写 C++ 或者 Python 程序,把特征图取出,做解码、候选框筛选、NMS,最终得到检测结果。这个解码逻辑和训练框架里的 decode 逻辑要保持一致。
JSON 文件大致长这样:
{ "model_path": "/usr/share/vitis_ai_library/models/yolov4_416/yolov4_416.xmodel", "input_width": 416, "input_height": 416, "num_classes": 2, "anchors": [10,14, 23,27, 37,58, 81,82, 135,169, 217,137, 301,275, 375,222, 442,426], "score_threshold": 0.5, "nms_threshold": 0.45 }这个文件我自己写了一个解析器,不依赖官方的推理库,这样整个部署流程更可控。注意 anchors 的顺序和训练时 cfg 文件里的顺序一致,否则三个尺度上分配的 anchors 错位,检测框会完全没有意义。
4.3 部署应用与运行时推理
部署时我在 KV260 上跑的是 Python 推理程序,底层用官方运行时库加载 .xmodel。运行时流程是这样的:
- 从摄像头或图片文件夹读取帧。
- 将图像缩放填充到 416×416,保持宽高比不变,空白区用灰色像素填充。这里有一个极其容易被忽视的问题:训练时如果用了 letterbox,部署时也必须用完全相同的 letterbox 方式,包括填充值。填充值不同会影响推理结果,尤其是在 INT8 量化模型上,误差会被放大。
- 将预处理后的图像送入 DPU,执行推理。
- 从输出特征图解码出边界框、置信度和类别。
- 做 NMS 去除重叠框,绘制结果。
需要注意的一点是,DPU 执行推理是异步的,和 CPU 端数据搬运同时进行。代码中不能每次推理都同步等结果,那样会让 DPU 空转。最好的方式是用双缓冲,一个 buffer 在搬运数据,另一个 buffer 在执行推理,交替进行。我实测过,改成双缓冲之后整体帧率提升了大约 30%。
我自己写的是一个简单的循环,核心代码大概是这样的:
import cv2 import numpy as np from runtime_loader import RuntimeLoader model = RuntimeLoader("yolov4_416.xmodel") cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() img, scale, pad = letterbox(frame, (416, 416)) img = img / 255.0 img = np.transpose(img, (2, 0, 1)) img = np.expand_dims(img, 0).astype(np.float32) outputs = model.run(img) # 返回三个尺度的特征图 boxes = decode_outputs(outputs, anchors, num_classes) boxes = nms(boxes, score_threshold=0.5, nms_threshold=0.45) draw_boxes(frame, boxes)这只是一个示意,实际工程里decode_outputs这一步要很仔细,尤其是特征图的排列顺序和维度,不同版本的工具导出的输出布局会有差异,必须以实际打印出来的 shape 为准。
4.4 部署性能调优
部署后我测了一下性能:在 DPU 上推理一帧 416×416 的输入,耗时大约 15 毫秒,加上前后处理之后整体帧率在 30 到 35 帧之间。纯 DPU 算力其实很充裕,瓶颈主要出在数据搬运和前后处理上。几个优化方向:
- 把图像缩放用硬件加速单元完成,而不是 CPU 做双线性插值。
- 把输出解码和 NMS 写成 C 扩展,Python 循环解码三个尺度的特征图确实很慢。
- 如果只是固定分辨率的摄像头输入,提前把 letterbox 的缩放系数和填充偏移算好,传入解码函数,省去每次计算。
- DPU 频率和功耗模式可以在系统层面配置,性能优先模式下推理时延会更低,但发热量会上去,需要在散热条件允许的情况下操作。
经过这些优化之后,整体帧率能稳定到接近 40 帧,对实时检测场景已经可以用了。记住一个基本原则:DPU 很快,但配套工程要跟上,瓶颈往往在你自己写的代码里。
5. 常见问题与排查技巧实录
整个流程跑下来,我把遇到过的典型问题和排查思路整理出来,做成一个速查表,希望能帮你少走一些弯路。
表格:常见问题对照表
| 问题现象 | 可能原因 | 排查思路与解决方法 |
|---|---|---|
| 训练时 loss 不下降 | 学习率过高或过低、anchors 与数据集不匹配、数据标注格式错误 | 先检查标注文件的坐标是否有 0 或超出 1 的值;尝试降低初始学习率或调长 burn_in;用 K-means 重新计算 anchors |
| 量化后 mAP 大幅下降 | 校准集不具代表性、校准集数量太少、输入尺寸和训练时不一致 | 更换校准集图片构成,增加目标密集的图片;将校准集数量提升到 500 张左右;统一训练、量化、部署三者的输入分辨率 |
| 编译时报错算子不支持 | DPU 架构版本较旧或编译工具版本较旧 | 确认板子上的 DPU 架构名称是否正确;某些激活函数如 mish 可能不被旧版工具链支持,考虑在训练阶段替换成 relu |
| 编译成功但是部署后输出结果全为 0 或者乱码 | feather map 输出解码错误、anchors 顺序不对、节点名称不匹配 | 先打印出模型输出的 shape,核对每个尺度的输出顺序;对照 meta.json 里的节点名;重新确认 anchors 和 cfg 文件的顺序一致 |
| 部署后检测框严重偏移 | letterbox 填充方式不一致、归一化方式不一致 | 对比训练时和部署时图片预处理代码,填充值和缩放方式必须完全一致 |
| 推理帧率低 | DPU 数据搬运没有流水线化、前后处理太慢 | 用双缓冲方式重叠数据搬运和推理;把解码 NMS 逻辑改成 C 扩展;避免在循环内做耗时操作 |
5.1 编译报错算子不支持的完整解决过程
这是我最想单独拿出来讲的一个问题。第一次量化编译时,工具直接报错说 mish 算子不支持。原因是我训练时用的是 yolov4 默认的 mish 激活函数,但当前版本的编译工具对 mish 的算子支持还不完整。mish 这个函数不是不能用,而是要看 DPU 镜像里有没有对应的指令支持。遇到这种情况有几个处理方向:
- 升级编译工具和相关 runtime 到最新版本,新版本通常会增加算子支持。
- 如果升级后还是不支持,就在训练阶段把 yolov4 的激活函数从 mish 全部替换成 relu,然后重新训练。很多人担心换了 relu 精度会下降,实测对于中小规模数据集,用 relu 替代 mish 的 mAP 损失通常只有 0.5 到 1 个点,但部署顺畅度提升非常明显。
- 还有一种折中办法是只在主干网络用 relu,neck 和 head 保留 mish,但这种混用需要自己改配置文件,一旦遇到算子不支持还是要继续改,不如一开始就统一用 relu 省心。
5.2 量化精度异常波动的排查心得
另一个反复出现的问题是多轮量化后精度差异很大。我试过两次完全相同的量化流程,结果 mAP 差了 2 个点。后来发现是校准集的加载顺序问题,板子内存紧张导致某些图片加载失败被跳过,实际用到的校准图片变少了。所以在量化之前先把校准图片全部解码成 raw 格式或者提前加载到内存中管理,避免量化过程中因 I/O 失败跳过图片。还有一个做法是固定随机种子,让量化过程中的随机采样可复现,这样每次量化出来的结果一致,排查问题时就容易定位到底是哪一步引入了新的变化。
5.3 一个容易被忽视的部署细节:DPU 温度与推理稳定性
KV260 在长时间运行时发热比较明显,尤其是性能优先模式下。温度升高之后,DPU 的推理时延会出现轻微波动。我在一次连续运行两个小时的测试里发现,前 30 分钟帧率很稳定,后面偶尔出现单帧耗时突然翻倍的情况。排查下来是散热片接触不良导致温度过高,DPU 触发了降频保护。这个问题不影响模型精度,但如果你跑的是实时应用,最好给开发板加一个主动散热风扇,并在软件层面对推理耗时做监控,超过阈值时自动提醒。
从我个人的角度来看,KV260 上跑 yolov4 的整体体验比预想中顺畅不少,最耗时间的部分是前期的数据准备和训练参数调整,真正量化和编译环节只要环境配好,速度很快。整个流程里最需要用心的是每个环节之间的参数一致性,从输入尺寸、anchors 到预处理方式,任何一个参数在某个环节发生了变化,最后表现出的问题都会非常诡异且难以排查。建议你在开始动手之前先写一份配置文件,把输入尺寸、anchors、类别数、归一化方式、letterbox 填充值全部固定下来,后续所有环节都从这份配置取参,不要到处复制粘贴参数,这样能省下大量的排查时间。