最近在把训练好的YOLOv5s模型转成ONNX,然后部署到手机端。转换过程没报错,但用onnxruntime推理时,输出的检测框置信度整体偏低,有些原本能检出来的目标直接漏掉了。对比了opset版本(用的12),也试过动态/静态batch,问题依旧。我怀疑是不是某些算子(比如Focus、SiLU)在转换时被替换成了近似实现?或者转换时精度损失是正常的?有没有人遇到过类似情况,一般怎么排查是哪个环节出了问题?另外,如果量化到INT8,是不是误差会更大?先谢谢各位了。
PyTorch模型转ONNX后推理结果和原模型对不上,是量化问题还是算子不支持?
全部回复
共 43 条我之前也碰到过类似情况,YOLOv5转ONNX后置信度掉了不少,后来发现是SiLU和Focus被拆成多个小算子后,某些版本的onnxruntime对它们的融合优化不到位,导致数值精度有微小差异,但累积起来就明显了。你可以试试先用onnx-simplifier过一遍,再对比每个节点的输出,定位到具体是哪个层开始偏差的。至于量化到INT8,误差肯定更大,尤其是对置信度阈值敏感的目标,建议先搞定FP32的精度对齐再说,不然量化后问题会被放大。另外,如果你用的是torch>=1.12,可以试试把Focus换成普通的Conv,有时候能少很多麻烦。
先跑一遍onnx模型的fp32输出,和原模型逐层对比,大概率是Focus展开或SiLU转译的边界处理问题。
别急着上INT8,先把fp32对齐了再说,量化只会让误差更明显。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截大概率不是量化的问题,而是Focus和SiLU被拆成多个小算子后,某些实现里对边界或者精度处理不一样。你可以先不转ONNX,直接拿PyTorch的jit trace导出再对比一下,或者用onnx-simplifier把图优化一遍,很多时候能解决。另外最好检查一下输入预处理是不是一致,比如归一化或者letterbox的填充值,这个最容易忽略。INT8量化误差肯定更大,但你现在这个情况建议先解决FP32的精度对齐,再考虑量化。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的地方就是Focus层,转的时候会被拆成slice加concat,不同框架实现顺序不一样,数值上就会有细微差异,再加上SiLU在某些opset下会被替换成近似公式,累积起来置信度就偏了。建议你先用onnxruntime把中间层的输出跟PyTorch逐层对比一下,能很快定位到是哪个节点开始分叉的。另外INT8量化误差肯定更大,尤其是对置信度这类敏感输出,建议先解决FP32下的对齐问题再考虑量化。
我之前转yolov5s也踩过这个坑,大概率不是量化的问题,因为你说的是fp32转onnx就有偏差。可以先检查下预处理和后处理是不是跟原模型完全一致,尤其是letterbox的填充值和归一化方式,很多情况是这边出的偏差。另外Focus层确实会被重构成slice和concat,但理论上精度影响可以忽略,建议你用onnxruntime直接对比中间层输出,定位到具体是哪一层开始分叉。如果确认是SiLU的近似计算,可以试试把opset升到17,新版本对激活函数的支持更完整。INT8的话误差肯定会放大,但你这情况应该先解决fp32的偏差再谈量化。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的就是Focus层,某些版本的onnxruntime会把它拆成几个slice和concat,但顺序或者padding方式跟原实现有细微差别,导致特征图对不上。你试试把Focus层直接改成普通的Conv加stride=2,或者用onnx-simplifier优化一下再看结果。另外SiLU(就是swish)在opset12下一般不会有大偏差,但如果你用的是自定义实现,建议换成标准的nn.SiLU再导出。排查的时候可以先逐层对比输出,写个脚本把每个节点的中间结果导出来和PyTorch对比,定位到具体层再想办法。INT8量化误差肯定更大,尤其是对置信度这种敏感输出,建议先用FP32跑通流程,最后再考虑量化。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的就是Focus层和SiLU激活,PyTorch里这些操作可能会被拆成多个基础算子,虽然onnxruntime理论上能跑,但某些老版本对这类组合的数值稳定性处理得不好,尤其在后处理时置信度会被压缩。你试过把opset升到13或者14吗?12的话对SiLU的支持其实不算特别完善,有时候会默默替换成近似公式,误差就悄悄累积了。另外,你说的置信度整体偏低,我建议先别急着怀疑量化,先对比一下onnx模型和原模型在同一个输入下的raw output,看是数值分布有差异还是直接变了符号,比如用cosine similarity或者直接打印几个锚点的输出值,这样能定位是网络前向的问题还是后处理解码的问题。我之前遇到过类似情况,最后发现是转模型时把grid和stride的常量折叠了,导致解码时坐标偏移算错,检测框位置偏了,置信度自然就降了。至于INT8量化,误差确实会更大,但你这个精度损失听起来不像纯量化造成的,更像是有算子被替换成低精度实现,建议先用FP16跑一遍,如果FP16没问题,再考虑是不是算子兼容性。还有个笨办法,就是转的时候把keep_initializers_as_inputs设为False,有时候能解决一些隐藏的图优化问题。你可以先试试这几步,大概率能锁定方向。
我之前也踩过这个坑,YOLOv5转ONNX后置信度偏差大概率不是opset的问题,Focus层和SiLU在onnxruntime里通常能正常跑,但如果你用了torch1.8以下版本,导出的模型可能会把SiLU拆成sigmoid+乘法,精度倒不会降这么多。我那次是发现onnxruntime默认用了float32,但手机端如果用fp16或者int8,误差会明显放大,尤其是小目标。建议你先在PC上对比一下onnxruntime和pytorch的逐层输出,找个脚本把中间层特征打印出来,看到底哪一层开始漂移,另外检查下预处理(比如归一化)在导出时有没有被固化进模型里,这个经常被忽略。量化int8的话,我试过用onnxruntime的dynamic quantize,置信度会再掉个3-5个点,但漏检率可能翻倍,还是先搞定浮点对齐再谈量化吧。
我之前转YOLOv5也踩过这个坑,置信度低大概率不是量化的问题,而是Focus和SiLU被拆成子图后精度确实有细微差异,尤其是低阈值场景下特别敏感。你可以先试试把opset拉到13或者14,有时候版本高了算子映射会更完整。另外检查一下预处理,ONNX这边输入归一化如果跟原模型不一致,输出差得会很隐蔽。我之前用onnxruntime跑FP32和原模型比,IOU差0.02以内算正常,超过这个数就优先怀疑图优化。INT8的话误差肯定会放大,建议先确认FP32没问题再考虑量化。
我之前也踩过这个坑,YOLOv5转ONNX后置信度偏低大概率不是量化的问题,因为你现在还是FP32。建议你先用onnxruntime跑一下官方给的yolov5s.onnx对比,如果也有偏差,那就是Focus和SiLU被拆成组合算子后数值精度有微小差异,但通常不至于漏检。更可能是你后处理里对输出层的解析方式变了,比如ONNX输出的坐标顺序或stride映射和原模型不一样,导致置信度阈值判断错位。量化到INT8误差确实会更大,尤其对检测头敏感,但你现在先把FP32对齐了再考虑量化吧。
先别急着怪量化,转onnx时把模型里带slice的focus层和silu换成等效节点试试,我之前这么弄就好了。
我上次遇到是batch维度写死了,你试试固定输入尺寸重新导出,顺便关掉dynamic_axes。
我之前也踩过这个坑,YOLOv5转ONNX后置信度不对大概率不是量化的锅,你先检查下预处理,尤其是归一化和letterbox的细节,ONNXruntime那边跑的时候很容易跟训练时不一致。至于Focus和SiLU,onnx官方算子库其实有对应支持,一般不会用近似替代,但你可以用onnxsim简化下模型再对比看看。另外,INT8量化误差肯定比FP32大,但你这情况先别急着量化,建议把每一层输出都dump出来跟PyTorch对一下,定位到第一个有差异的节点再说。
大概率是SiLU和Focus被拆成多个算子后,精度微小差异被放大了。先试下onnxsim优化,再对比每层输出定位。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的就是Focus和SiLU,PyTorch里这俩的底层实现和ONNX的标准算子不完全等价,尤其是Focus,建议直接用onnx-simplifier过一遍,再把opset拉到13以上试试,大概率能解决。另外查一下是不是有动态shape导致某些层被强制展开成静态图,这会改变计算路径。精度损失在FP32下不应该是量级差异,先别急着上INT8,不然误差会叠加得更难排查。我之前是逐层对比中间tensor才定位到问题,你可以写个脚本把onnx的每层输出和PyTorch的hook结果一对,基本就能锁定是哪个节点出事了。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的就是Focus和SiLU,尤其是Focus在opset12里会被拆成slice和concat的组合,如果输入尺寸不是64的倍数,边界像素处理会有细微差异,导致特征图错位。你试试把opset升到13或14,新版对Slice和Resize的坐标对齐方式改过,可能能解决一部分偏差。另外,检查一下转换时有没有把模型里的检测头(比如decode部分)一起导进去,如果ONNX里只含backbone和neck,后处理写错了,置信度低是必然的。我一般会先拿一张固定图片,把PyTorch和ONNX的每一层输出都打印出来对比,定位到具体哪个stage开始发散,而不是只看最终结果。关于INT8量化,误差肯定更大,尤其是第一层卷积和最后的检测头对量化敏感,建议先跑FP16试试,如果FP16没问题再考虑量化,而且量化时要用校准集,别用训练集。还有个常见坑是batch维度,你试过动态batch,但onnxruntime对动态shape的优化有时会触发不同的算子实现,建议固定batch=1再对比一次。如果还不行,直接看onnxruntime的日志,它有时候会警告某个算子用了fallback实现,那个就是嫌疑点。
我之前转YOLOv5也碰到过这情况,后来发现是输出层的decode逻辑没跟着一起转进去,ONNX只导出了模型前向,后处理还得自己在外面写一遍,置信度对不上很可能就是这里的问题。你可以先拿onnxruntime跑一下原始图片的tensor输出,跟PyTorch的对比看到底差在哪一层,再决定是不是算子的事。至于INT8,误差肯定会有,但一般不会导致漏检这么严重,建议先把FP32的精度对齐了再考虑量化。
说实话你这个问题我太有共鸣了,之前做检测模型部署时也被ONNX的“静默误差”坑过一整天。opset12对YOLOv5的Focus层确实不友好,它本质是切片重排,但某些导出路径会把它拆成多个StridedSlice+Concat,精度理论上没损失,可一旦跟后续BN层融合顺序乱了,数值就会漂移,建议你直接对比一下onnx和pth每一层输出的最大绝对误差,用onnxruntime的IO Binding把中间tensor打出来看。SiLU的话官方现在支持得还行,但如果你用的旧版torch,它可能被替换成Sigmoid+Mul的复合算子,浮点下误差极小,不太可能是置信度掉这么多的主因。我怀疑更大概率是预处理环节的差异——比如你原模型推理时用了letterbox的padding值或者归一化方式,而ONNX端如果直接吃原始图像,或者BGR/RGB通道顺序反了,检测框会整体变歪且置信度暴跌,这跟算子无关。另外你说的INT8量化,如果没做校准集或者用错量化方案(比如per-tensor替代per-channel),在手机端掉点0.2-0.3个mAP都很正常,尤其小目标会直接消失,建议先用FP16试试,排除精度问题再碰量化。最后给你个排查思路:固定一张图,分别跑torch和onnx,把输出tensor的原始值(未做NMS前)存成npy,逐位对比差异最大的位置,再反查对应特征图,基本能定位到是哪个op引起的断裂。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是SiLU,而是Focus层。你用opset12的话,Focus会被拆成slice和concat,理论上数学等价,但某些onnxruntime版本对这类张量操作的优化可能有细微差异,尤其是当输入尺寸不是64的整数倍时。我建议你先别急着怀疑量化,先用原始FP32模型在onnxruntime里跑一遍,把输出tensor和PyTorch的逐元素对比一下,看是整体偏差还是个别输出节点有问题。
另外你提到置信度整体偏低,这个特征很像后处理里的阈值或者坐标解码逻辑没跟上,ONNX里只导出了模型前向,但YOLO的nms和缩放还原如果在转换时没固化进去,推理结果就会显得“虚低”。你可以试着把导出的ONNX用onnx-simplifier过一遍,很多情况下是多余算子导致的精度抖动,简化后误差会小很多。
至于INT8量化,我个人的经验是误差确实会放大,但通常不会大到漏检的程度,除非你的校准集选得不好。所以你现在这个情况,大概率不是量化问题,而是转换时的精度基线就没对齐。建议你先跑一下onnxruntime和PyTorch的CPU推理,固定相同输入,直接打印输出logits,看看最大误差是多少,如果超过1e-3,那肯定是算子替换的问题,再去查具体节点。
先别急着怪量化,FP32转ONNX就可能因SiLU近似实现掉点,建议逐层对比输出找偏差源头。
Focus和SiLU确实是重灾区,尤其Silu在早期opset里会被拆成Sigmoid+Mul,数值上容易有偏差。建议先用onnxruntime和原模型跑同一张图,逐层对比feature map,能快速定位是哪一层开始飘的。另外YOLOv5的Detect层里anchor解码逻辑对精度挺敏感,置信度整体偏低往往出在那儿而不是卷积本身。INT8量化肯定误差更大,最好先解决FP32对齐问题再考虑量化。