十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

YOLOv5转RKNN部署实战:置信度异常与乱框问题排查指南

YOLOv5转RKNN部署实战:置信度异常与乱框问题排查指南 1. 从一次模型转换翻车说起把YOLOv5的权重从PyTorch的.pt文件一路转到RKNN、部署到瑞芯微的NPU上跑推理这件事听起来是一条非常标准的流水线pt → onnx → rknn三步走完板上跑起来就完事。但真正动过手的人都知道这条链路里埋的坑远比想象中多尤其是置信度异常和乱框这两个问题几乎是每个做端侧部署的人都会撞上的墙。我最近就完整地踩了一遍。模型在PC上用PyTorch推理检测结果干净利落框准、分数高导出ONNX之后用onnxruntime验证也基本正常可一旦转成RKNN放到板子上跑画面就彻底变了样——要么满屏都是低置信度的碎框要么本该检测到的目标一个都不出要么框的位置整体偏移、大小离谱。更让人抓狂的是这些现象在不同输入图片上表现还不一样有的图正常有的图崩完全没有规律可循。这篇文章就是把这套翻车现场完整拆开讲清楚。我会从为什么PT到RKNN这条链路容易出问题讲起把置信度异常和乱框这两类症状的根因逐个定位再给出可复现的排查步骤和修复方案。内容面向的是已经跑通过YOLOv5训练、准备往瑞芯微平台部署的开发者也适合任何在做ONNX到NPU转换时遇到类似问题的朋友参考。核心关键词会围绕YOLOv5、RKNN、PT、ONNX、置信度异常这几个点展开但重点不在概念科普而在于把为什么错和怎么修讲透。先说一个反直觉的结论绝大多数PT转RKNN后的置信度异常和乱框根因不在RKNN工具链本身而在ONNX导出环节和前后处理的对齐上。很多人一看到板上结果不对第一反应是RKNN量化把精度搞坏了然后拼命去调量化参数、换量化数据集结果折腾半天没效果。实际上问题往往在更早的环节就已经埋下了。2. PT到RKNN这条链路到底经过了什么2.1 三个阶段的职责边界要定位问题先得搞清楚这条链路上每一步到底做了什么、改变了什么。PT阶段是PyTorch的原生权重它保存的是完整的模型结构和参数。YOLOv5的.pt文件里不仅有网络权重还带着一些训练时的元信息比如类别数、锚框配置、输入尺寸等。这个阶段模型是活的前向传播、后处理NMS、坐标解码都在PyTorch框架内完成一切自洽。ONNX阶段是中间表示。导出ONNX的本质是把PyTorch的计算图冻结成一张静态图。这里有个关键点导出时你选择导出哪些输出节点直接决定了后续所有环节的对齐基准。YOLOv5官方仓库的export.py默认会导出三个检测头的原始输出shape类似[1, 25200, 85]这种而不是经过NMS之后的最终结果。这个选择本身没错但它意味着后处理逻辑必须由你自己在推理端实现而这一步恰恰是最容易出错的地方。RKNN阶段是把ONNX图进一步转换成瑞芯微NPU能执行的格式中间通常还会做量化INT8。量化会引入精度损失但正常的量化损失是分数略微下降、框略微偏移绝不会造成满屏乱框这种量级的崩坏。如果症状是灾难性的那基本可以排除是量化本身的锅。2.2 为什么能导出不等于能对齐很多人验证ONNX是否正确的做法是用onnxruntime跑一遍看看输出shape对不对或者随便看一张图的结果感觉差不多就过了。这种做法非常危险。原因在于ONNX导出正确性和后处理对齐正确性是两件独立的事。你可能导出了一张完全正确的ONNX图但推理端的后处理写错了结果照样崩。反过来后处理写得没问题但导出时输出节点选错了比如把sigmoid之前的输出导出来了结果也会崩。这两种崩坏在表象上都是置信度异常或乱框很难一眼区分。我建议的验证方式是在ONNX阶段就把后处理完整跑通并且和PyTorch的结果做逐框对比。具体做法是拿同一张图分别用PyTorch和onnxruntime自写后处理跑把两者的检测框坐标和置信度打印出来对比。如果这一步就能对上允许小数点后几位的误差那问题一定出在RKNN转换或板端推理如果这一步就对不上那根本不用往下走先把ONNX和后处理修好。2.3 一个容易被忽略的细节输入预处理在讲后处理之前必须先说输入预处理因为乱框问题里有相当一部分其实是预处理不一致导致的。YOLOv5的标准预处理是letterbox缩放保持长宽比短边补灰边、BGR转RGB、归一化到0-1、HWC转CHW。这一套在PyTorch训练和推理时是固定的。但到了部署端很多人会图省事直接用resize强行拉伸到640x640或者忘了做letterbox的padding或者颜色通道顺序搞反。这些预处理差异会造成什么后果框的位置会系统性偏移而且偏移量随图片原始长宽比变化。这正好解释了为什么有的图正常有的图崩——长宽比接近1:1的图resize和letterbox差别不大看起来正常长宽比悬殊的图差别就巨大。所以排查乱框的第一步永远是确认预处理和训练时完全一致。这个后面会给出具体的对齐方法。3. 置信度异常从分数全低到分数全乱3.1 置信度异常的三类典型表现置信度异常不是一个单一症状它至少分三种情况而每种情况的根因完全不同。第一类所有框的置信度都异常低比如全在0.1以下但框的位置大致正确。这种情况通常是量化损失过大或者后处理里的sigmoid/softmax被重复应用或漏应用。YOLOv5的输出在objectness和类别分数上都需要经过sigmoid如果你在导出时已经包含了sigmoid后处理又做了一次分数就会被压得很低。第二类置信度数值完全离谱比如出现负数、超过1、或者量级完全不对几千几万。这基本可以断定是输出节点选错了。比如你把sigmoid之前的raw logits导出来了那数值范围就是负无穷到正无穷后处理再一折腾就彻底乱了。第三类置信度分布正常但该高的不高、该低的不低导致NMS后要么全被滤掉要么全留下。这种情况往往是量化校准集选择不当或者输出张量的通道顺序被搞乱了。通道顺序错乱是个隐蔽的坑因为shape可能还是对的但每个通道代表的含义变了。3.2 输出节点选择一个决定成败的分叉口YOLOv5导出ONNX时输出节点的选择是第一个关键决策点。官方export.py在不同版本里行为略有差异但核心逻辑是导出的应该是三个检测头的输出每个头的shape是[batch, anchors, 5num_classes]其中5是xywh objectness。这里有个非常容易踩的坑不同版本的YOLOv5导出的输出是否已经包含了sigmoid和坐标解码是不一样的。早期版本导出的是raw output需要后处理自己做sigmoid和anchor解码后来有些版本或者你自己改过的export脚本可能已经把sigmoid做进去了。如果你拿到的ONNX和你的后处理假设不匹配结果必然崩。我的建议是导出后立刻用Netron打开ONNX文件看清楚最后一个节点的类型和输出shape。如果最后是Sigmoid节点那后处理里就不要再做sigmoid如果是Conv或Add那后处理必须补上sigmoid。这一步花五分钟能省掉后面几小时的瞎折腾。3.3 量化校准为什么你的INT8模型分数全崩如果确认ONNX和后处理都对得上但转成INT8 RKNN后置信度还是崩那就要看量化校准了。RKNN的量化需要一批校准图片通常几十到几百张用来统计每层激活值的动态范围。校准集的选择直接决定量化质量。常见的错误包括校准集用了和实际场景分布差异很大的图片比如用COCO的图去校准一个工业检测模型校准集数量太少少于20张统计不充分校准集里全是简单背景没有覆盖目标的多样性。校准不当的后果是某些层的量化scale偏得离谱导致激活值被截断或压缩最终表现为置信度整体偏低或分布异常。提示如果怀疑是量化问题最快的验证方法是先转一个FP16不量化的RKNN跑一遍看结果。如果FP16正常、INT8崩那问题就在量化如果FP16也崩那问题在ONNX或后处理跟量化无关。这个二分法能帮你快速缩小范围。3.4 一个真实案例sigmoid被应用了两次我遇到过一个特别典型的案例。模型转RKNN后所有框的置信度都在0.05到0.15之间框的位置基本正确但分数低到NMS阈值一卡就全没了。排查过程是这样的先看ONNX用Netron打开发现最后确实有Sigmoid节点。再看后处理代码发现代码里对objectness和类别分数又做了一次sigmoid。两次sigmoid叠加原本0.9的分数变成了sigmoid(sigmoid(logit))数值被严重压缩。把后处理里的sigmoid去掉之后分数立刻恢复正常。这个坑的隐蔽性在于如果你只看ONNX的输入输出shape是发现不了这个问题的因为shape完全正确。必须看节点类型才能发现。所以再强调一遍Netron看节点类型这一步不能省。4. 乱框问题坐标解码与anchor的那些事4.1 乱框的本质坐标解码错了如果说置信度异常是分数不对那乱框就是位置不对。乱框的表现形式很多框整体偏移、框大小离谱、框集中在图像某个角落、框完全随机分布。这些不同的表现对应的是坐标解码环节的不同错误。YOLOv5的坐标解码逻辑是网络输出的是相对于anchor的偏移量tx, ty, tw, th需要经过一系列变换才能得到实际的xywh。标准公式是bx (sigmoid(tx) * 2 - 0.5 cx) * stride by (sigmoid(ty) * 2 - 0.5 cy) * stride bw (sigmoid(tw) * 2) ** 2 * anchor_w bh (sigmoid(th) * 2) ** 2 * anchor_h这里面涉及sigmoid、grid坐标、stride、anchor尺寸等多个要素。任何一个要素搞错框就会乱。而且不同版本的YOLOv5这个公式的细节比如是否减0.5、是否乘2可能略有差异必须和你训练时用的版本严格对应。4.2 anchor配置版本差异带来的隐形陷阱anchor是YOLOv5里一个特别容易出问题的地方因为不同版本的YOLOv5默认anchor不一样而且anchor是和输入尺寸绑定的。YOLOv5在640x640输入下的默认anchor是[10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326]但如果你训练时改了输入尺寸比如改成416或1280或者用了自定义anchor用--noautoanchor关掉自动anchor后手动设的那部署时必须用你训练时实际用的那套anchor。我见过太多人部署时直接抄了网上的后处理代码里面写死了默认anchor结果自己训练时用的是自动anchork-means聚类出来的两者对不上框自然全乱。确认anchor的方法很简单看训练时的hyp.yaml或者训练日志或者直接从.pt文件里读出来。4.3 stride与grid的对应关系YOLOv5有三个检测头分别对应stride 8、16、32。每个头的输出会reshape成[batch, 3, grid_h, grid_w, 5num_classes]的形式其中grid_h和grid_w分别是输入尺寸除以stride。在写后处理时必须正确地为每个头分配对应的stride和grid尺寸。常见的错误是把三个头的stride搞混或者grid尺寸算错比如输入640stride 8对应的grid应该是80x80但有人写成了40x40。这种错误会导致框的位置整体缩放错位。一个实用的技巧是在reshape输出时把每个头的stride和grid尺寸打印出来和理论值对一遍。640输入下三个头的grid应该是80x80、40x40、20x20anchor数量各3个。对不上就说明reshape逻辑有问题。4.4 letterbox的padding乱框的另一个高频来源前面提过letterbox这里展开说。letterbox的核心是把原图按比例缩放到目标尺寸短边用灰色114,114,114填充。推理得到框之后需要把框的坐标从letterbox坐标系映射回原图坐标系这个映射涉及减去padding和除以缩放比例。如果这一步漏了或者算错了框的位置就会整体偏移。偏移量等于padding的量而padding量又取决于原图长宽比。这就是为什么有的图正常有的图崩——长宽比接近1的图padding小偏移不明显长宽比悬殊的图padding大偏移就明显。正确的映射逻辑是# 假设letterbox后图像尺寸为640x640原图尺寸为orig_w x orig_h # 缩放比例 r min(640/orig_w, 640/orig_h) # padding: pad_w (640 - orig_w * r) / 2, pad_h (640 - orig_h * r) / 2 # 框映射回原图 x_orig (x_letterbox - pad_w) / r y_orig (y_letterbox - pad_h) / r这段逻辑看起来简单但实际写的时候很容易把pad_w和pad_h搞反或者忘了除以r。建议把这段单独封装成函数并且用一张已知结果的图验证。5. 一套可复现的排查流程5.1 第一步锁定问题发生在哪个环节排查的核心思路是二分法。不要一上来就盯着板子看而是从链路中间切开逐段验证。具体做法是准备一张测试图然后依次在四个节点上跑推理并记录结果环节工具验证内容PTPyTorch基准结果框和分数都正常ONNXonnxruntime和PT结果逐框对比误差应在小数点后2-3位RKNN (FP16)rknn_toolkit / 板端和ONNX结果对比允许小幅精度损失RKNN (INT8)板端和FP16对比允许分数略降、框略偏只要某一环节和上一环节对不上问题就在这一环节。比如ONNX和PT对不上那问题在导出或后处理ONNX对得上但FP16 RKNN对不上那问题在RKNN转换配置FP16对得上但INT8对不上那问题在量化。这个表格看起来简单但能帮你省掉大量盲目试错的时间。我见过太多人跳过中间环节直接从PT跳到板端结果出了问题完全不知道从哪查。5.2 第二步ONNX阶段的逐框对比脚本在ONNX阶段做逐框对比是整套流程里最关键的一步。下面是一个简化的对比思路伪代码import torch import onnxruntime as ort import numpy as np # 1. PyTorch推理 model torch.load(yolov5.pt)[model].float().eval() img preprocess(test.jpg) # letterbox normalize toTensor with torch.no_grad(): pt_out model(img)[0].numpy() # 2. ONNX推理 sess ort.InferenceSession(yolov5.onnx) onnx_out sess.run(None, {images: img.numpy()})[0] # 3. 对比原始输出 print(PT output shape:, pt_out.shape) print(ONNX output shape:, onnx_out.shape) print(Max diff:, np.abs(pt_out - onnx_out).max()) print(Mean diff:, np.abs(pt_out - onnx_out).mean())如果Max diff在1e-3量级说明导出没问题如果diff很大比如几十几百说明输出节点选错了或者导出配置有问题。这一步的diff值是最直接的诊断指标。5.3 第三步后处理的独立验证确认ONNX输出正确后下一步是验证后处理。方法是用ONNX的原始输出手动跑一遍后处理看能不能得到合理的框。这里的关键是把后处理拆成独立的、可单步调试的模块先做sigmoid再做坐标解码再做置信度过滤最后做NMS。每一步都打印中间结果的统计信息比如sigmoid后的分数范围、解码后的框坐标范围。正常的中间结果应该是sigmoid后分数在0-1之间且大部分接近0因为大部分anchor没有目标解码后框坐标在0到640之间letterbox坐标系置信度过滤后剩下的框数量在几十个量级NMS后剩下的框数量在几个到几十个量级。如果某一步的统计信息明显异常比如分数全是0.5、框坐标出现负数或超过640那问题就在这一步。5.4 第四步RKNN转换配置的检查清单如果ONNX和后处理都验证通过问题出在RKNN转换那重点检查以下配置mean和stdRKNN转换时需要指定归一化的mean和std。YOLOv5的标准是mean[0,0,0]、std[255,255,255]如果预处理里没做归一化或者mean[0,0,0]、std[1,1,1]如果预处理里已经做了。这里必须和你的预处理严格对应否则输入数值范围就错了。量化算法RKNN支持多种量化算法如normal、mmse等不同算法对精度的影响不同。如果normal量化效果差可以试试其他算法。校准集前面说过校准集要覆盖实际场景的分布数量建议在100-300张。目标平台不同瑞芯微芯片如RK3588、RK3568的NPU能力不同转换时要选对target platform。注意mean和std的配置错误是一个非常隐蔽的坑因为它的后果和预处理不一致很像都是输入数值范围不对。排查时要把这两者一起检查。6. 那些文档里不会写的实操心得6.1 关于版本匹配的血泪教训YOLOv5的版本迭代很快不同版本之间后处理逻辑、anchor配置、导出行为都可能有细微差异。最稳妥的做法是训练、导出、后处理全部用同一个版本的代码。不要训练用v6.0、导出用v7.0、后处理抄网上的v5.0代码这种混搭是乱框和置信度异常的重灾区。如果实在要用不同版本那必须逐个确认anchor是否一致、输出是否包含sigmoid、坐标解码公式是否一致。这三项任何一项不一致结果都会崩。6.2 用一张标准测试图贯穿全流程我强烈建议准备一张固定的、结果已知的测试图从PT到板端全程用它验证。这张图最好满足目标清晰、数量适中3-5个、背景不太复杂。每次改动配置后都用这张图跑一遍对比结果是否一致。这样做的好处是排除了输入变量任何结果变化都能直接归因到你的改动上。如果每次换不同的图测试出了问题你都不知道是图的问题还是配置的问题。6.3 NMS的阈值设置也有讲究置信度异常有时候不是模型的问题而是NMS阈值设置不当。YOLOv5默认的置信度阈值是0.25NMS IoU阈值是0.45。但在量化之后分数分布会整体偏移原来的0.25可能就不合适了。我的经验是量化后的模型置信度阈值可以适当调低比如0.15-0.2因为量化会让分数整体偏低。但也不能调太低否则会引入大量误检。最好的方法是拿一批测试图画出分数分布直方图根据分布来定阈值。6.4 板端推理的输入格式要对齐到了板端还有一个容易忽略的点输入数据的格式和内存布局。RKNN的输入通常是NHWC或者NCHW具体取决于转换时的配置。如果你在板端喂进去的数据布局和模型期望的不一致结果也会乱。另外板端的图像数据来源比如摄像头通常是NV12或RGB888需要转换成模型期望的格式。这个转换过程如果出错比如颜色通道顺序、stride对齐也会导致检测异常。建议在板端先用一张已知的图片文件测试确认无误后再接摄像头。6.5 一个快速判断是量化问题还是逻辑问题的技巧如果时间紧想快速判断问题性质可以用这个技巧把RKNN的推理结果和ONNX的结果做相关性分析。如果两者的框位置高度相关只是分数偏低那大概率是量化问题如果框位置完全不相关比如ONNX的框在左上RKNN的框在右下那大概率是逻辑问题后处理、坐标解码、预处理。这个判断能帮你在调量化和查代码之间快速做出选择避免在错误的方向上浪费时间。7. 写在最后的一点个人体会做端侧部署这几年我最大的体会是模型转换这条链路上90%的问题都不是转换工具不行而是环节之间的对齐没做好。PT到ONNX的对齐、ONNX到后处理的对齐、预处理和训练的对齐、量化配置和实际数据的对齐——每一个对齐点都是一个潜在的坑。很多人遇到问题喜欢直接搜RKNN置信度异常怎么办然后照着网上的答案一通改结果越改越乱。更有效的方法是建立一套自己的排查流程从链路中间切开逐段验证用数据说话。上面那套四步排查法就是我在踩了无数次坑之后总结出来的虽然看起来笨但胜在可靠。最后再分享一个小习惯每次成功部署一个模型后把当时的完整配置版本号、anchor、mean/std、量化参数、后处理代码存档。下次遇到类似问题直接对比配置差异往往一眼就能看出问题所在。这个习惯帮我省下的时间比我学任何高级技巧都多。
返回列表