最近在把训练好的YOLOv5s模型转成ONNX,然后部署到手机端。转换过程没报错,但用onnxruntime推理时,输出的检测框置信度整体偏低,有些原本能检出来的目标直接漏掉了。对比了opset版本(用的12),也试过动态/静态batch,问题依旧。我怀疑是不是某些算子(比如Focus、SiLU)在转换时被替换成了近似实现?或者转换时精度损失是正常的?有没有人遇到过类似情况,一般怎么排查是哪个环节出了问题?另外,如果量化到INT8,是不是误差会更大?先谢谢各位了。
PyTorch模型转ONNX后推理结果和原模型对不上,是量化问题还是算子不支持?
全部回复
共 30 条我之前也碰到过类似情况,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对齐了再考虑量化吧。