最近在搞一个部署项目,把训练好的YOLOv5模型转成ONNX,用onnxruntime跑了一下,发现输出的检测框置信度整体偏低,有些原本能检出的目标直接丢了。对比了torch和onnx的输入输出,数据预处理都是一样的,也关掉了amp。查了日志,提示有些算子(比如Focus和SiLU)被拆成了多个小算子,不确定是不是这个原因。有没有大佬遇到过类似情况?是算子兼容性问题,还是转换时需要设置opset版本或者dynamic_axes?另外,用onnx-simplifier简化后会不会有改善?希望有经验的朋友指点一下,实在不想从头用TensorRT重写。
PyTorch转ONNX后推理结果和原模型差很多,是量化问题还是算子不支持?
全部回复
共 72 条遇到过类似的,Focus和SiLU被拆开一般不影响精度,但如果你用了amp训练,转ONNX时权重里可能有half精度残留,建议先确认下导出的模型权重是不是fp32。另外opset版本建议至少设到12以上,dynamic_axes最好把batch和wh维度都标上,不然推理时shape变化可能导致数值异常。onnx-simplifier可以试试,但主要作用是去掉冗余节点,对精度损失帮助有限,更可能是预处理细节(比如归一化方式或letterbox的padding值)在前后端没完全对齐。如果实在查不出来,可以逐层对比torch和onnx的中间输出,定位到第一个出现偏差的节点。
大概率是算子拆解后精度丢失,先试下opset=12+dynamic_axes,simplifier对Focus帮助不大。
八成是opset版本太低导致Focus展开时精度丢了,试试opset=12以上顺便开下dynamic_axes。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截大概率不是量化问题,你关了amp就排除了这个。Focus和SiLU被拆小算子确实会影响数值精度,但更常见的是opset版本太低导致某些算子走的是旧版实现,建议试试opset=12或13,同时把dynamic_axes设上。onnx-simplifier可以跑一下,主要是把那些冗余的reshape和concat合并掉,对精度恢复有点帮助,但别指望完全一致。另外你对比下torch和onnx的输出特征图,看看是哪个层开始偏差变大的,这样定位更快。
我之前也踩过这个坑,YOLOv5转ONNX置信度掉得厉害,大概率不是量化问题,因为amp关了的话精度损失主要就在算子拆解上。Focus和SiLU被拆开确实会影响数值稳定性,尤其是SiLU在低精度下误差会累积,建议先试试opset设到12以上,然后把dynamic_axes配上,再看下输出差异具体是哪个层开始的。onnx-simplifier能合并一些冗余节点,有时候能救回来一点,但别指望完全一致,最稳的办法还是对比中间张量的最大误差,定位到具体算子再手动改图。如果实在不行,TensorRT确实更省心,但前期调试成本也高,能靠调ONNX解决就尽量别重写。
遇到过类似的,YOLOv5转ONNX后置信度飘了大概率不是量化的问题,你amp都关了基本能排除。Focus和SiLU被拆成小算子很常见,不影响数值,真正要查的是输出层有没有被错误地融合或剪枝,建议先比对一下中间层的tensor。opset版本建议直接上17,dynamic_axes这个对检测框本身没影响,但如果你用了NMS导出就得小心。onnx-simplifier可以跑一下,有时候能消除一些冗余op,但对精度误差帮助有限,我之前是发现转的时候模型自动加了个奇怪的尾差,手动改了下导出脚本才好。
opset和dynamic_axes得先确认好,Focus拆算子一般不影响精度,试下onnx-simplifier看能不能对齐。
我上次是改成opset12再简化就好了,你试试把SiLU换成ReLU6对比下输出。
遇到这种置信度整体偏低的情况,我第一反应还真不是算子拆分的问题,反而更像是输出层后处理那块没对齐。YOLOv5的原始输出是带anchor的,ONNX导出时有些版本会把decode部分也一起导出,但onnxruntime跑出来的是raw prediction,你如果直接拿它跟torch的最终detect结果比,那肯定差很多。建议你先确认下导出时有没有包含后处理,或者自己写个脚本把anchor decode逻辑加上再对比。算子拆分一般只会影响速度或极少数数值精度,不会让置信度系统性下降,除非某个子图被替换成了不支持的实现。opset版本倒是值得查一下,12以下对SiLU的支持可能走的是近似路径,但通常误差也在1e-5级别,不至于丢目标。你可以试试点开onnx的节点图,看看最后的输出层是不是完整的,而不是被截断的。onnx-simplifier大概率帮不上这种语义差异的忙,它只是清理冗余结构。我之前遇到过类似情况,最后发现是导出时没设dynamic_axes,导致输入shape固定后,模型内部某些reshape行为变了,输出空间也跟着错位。建议你把postprocess关掉,导出纯backbone+head的版本,然后用numpy手动对齐一下输出张量,这样能快速定位是哪个环节出了问题。
我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉一截,八成不是量化的问题,你amp都关了基本能排除。Focus和SiLU被拆成小算子挺正常的,ONNX导出时官方就喜欢这么干,关键是拆分后数值精度会不会有损失,尤其是SiLU,有些老版本opset对它的近似计算会引入误差。建议你先把opset设到12以上,最好用13或17,然后导出时把dynamic_axes配好,特别是batch和长宽维度,不然onnxruntime可能会用固定shape的优化路径,反而导致精度异常。onnx-simplifier我试过,对YOLOv5这种结构一般能合并不少冗余节点,但也不是万能,有时候会把一些自定义算子搞得更乱,建议先跑完simplifier再对比一下输出。还有个排查思路,你可以把onnx的中间层输出和PyTorch对应层对一下,看到底是哪个节点开始偏差变大的,这样定位比瞎猜快得多。如果实在不行,直接考虑用torch2trt或者mmdeploy转TensorRT,YOLOv5对TRT的适配已经很成熟了,省心很多。
我之前也踩过这坑,Focus和SiLU被拆成小算子后精度确实会掉一点,但一般不至于丢框。优先确认opset版本,YOLOv5建议用12以上,另外dynamic_axes如果没设对,batch维度会出问题。onnx-simplifier能帮忙合并一些冗余算子,值得试一下,但别指望它解决所有精度问题。最好先逐层对比torch和onnx的中间输出,定位到底是哪层开始偏的。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉得厉害,最后发现是SiLU和Focus那块的算子拆分导致的数值差异。你关掉amp是对的,但还得确认下torch和onnx的输入是不是真的完全一致,有时候归一化那步在导出时会被悄悄改掉。opset版本很关键,建议至少用11以上,Focus最好在导出前替换成等效的Conv,不然拆出来的Slice和Concat组合容易出精度问题。dynamic_axes如果没设对也会影响,特别是batch维度不固定的时候。onnx-simplifier可以试试,它能把一些冗余算子融合掉,但别指望它解决根本性的数值偏差。其实更靠谱的办法是用onnxruntime的perf工具对比每一层的输出,定位到具体是哪一层开始偏的,比瞎猜强多了。如果实在搞不定,转TensorRT反而是最省心的,YOLOv5的trt部署方案很成熟,没必要死磕ONNX。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉得厉害,后来发现是SiLU被拆成Sigmoid+Mul之后数值精度有细微差异,累积起来就明显了。你可以先试试把opset升到12以上,再跑一遍onnx-simplifier,Focus那块通常会被优化成Conv,效果会好一些。另外别忘了检查一下输出层的decode部分是不是也被转成了ONNX算子,有时候后处理在torch里是Python写的,导出时没跟上。实在不行就手动改一下导出脚本,把SiLU换回Hardswish试试,我之前这么搞过,基本能对齐。