最近在搞一个部署项目,把训练好的YOLOv5模型转成ONNX,用onnxruntime跑了一下,发现输出的检测框置信度整体偏低,有些原本能检出的目标直接丢了。对比了torch和onnx的输入输出,数据预处理都是一样的,也关掉了amp。查了日志,提示有些算子(比如Focus和SiLU)被拆成了多个小算子,不确定是不是这个原因。有没有大佬遇到过类似情况?是算子兼容性问题,还是转换时需要设置opset版本或者dynamic_axes?另外,用onnx-simplifier简化后会不会有改善?希望有经验的朋友指点一下,实在不想从头用TensorRT重写。
PyTorch转ONNX后推理结果和原模型差很多,是量化问题还是算子不支持?
全部回复
共 64 条我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉一截,多半不是量化问题,因为你说关了amp,那就是纯精度对齐的事。Focus和SiLU被拆确实会影响数值稳定性,尤其是SiLU,不同onnxruntime版本对它的实现有细微差别,建议先锁定onnxruntime版本,再对比一下每个节点的输出,看是哪个算子的误差开始累积的。opset版本很关键,试过12和17,后者对动态shape支持更好,但某些老算子反而会被拆得更碎,你可以先固定输入尺寸试试,排除dynamic_axes的干扰。onnx-simplifier能合并一些冗余的reshape和transpose,但别指望它解决数值偏差,它主要优化结构,不改变数学计算。如果简化后还是不行,可以试试直接导出时把Focus层改成卷积,YOLOv5的Focus本质就是切片加卷积,很多部署框架对它有原生支持,这样能省不少麻烦。另外有个小技巧,把模型输出后处理里的sigmoid或softmax挪到onnx外面算,有时候能绕开算子不兼容导致的精度损失。最后,如果实在不行,也别急着上TensorRT,先用onnxruntime的CPU和CUDA分别跑一下,对比下是不是GPU上的fp16或cudnn卷积算法引起的差异。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截大概率不是量化问题,而是Focus和SiLU被拆解后,某些onnxruntime版本对这类子图优化做得不好。建议先试试opset=12以上,然后dynamic_axes只对batch维度开,别对wh开,能省不少麻烦。onnx-simplifier可以跑一下,但别完全依赖它,我遇到过它把某些reshape合并出错的案例。另外你对比一下中间层输出,看是不是从某个节点开始偏差变大,这样定位更快。实在不行就换onnxruntime-gpu版本,有时候CPU和GPU的kernel实现也会有精度差异。
试试把opset调到11以上,SiLU和Focus拆算子一般不影响精度,大概率还是导出时模型里有批归一化层没融合。
遇到过类似的,YOLOv5转ONNX掉点大概率不是量化问题,你这推理用的还是FP32吧。Focus和SiLU被拆成小算子通常不影响数值,但要注意onnxruntime对某些算子的实现精度和PyTorch有细微差异。建议先查一下ONNX模型里输出层的坐标解码是不是也被拆了,YOLOv5的detect头在转换时经常被改得面目全非,我上次就是手动改模型结构才对齐的。opset版本建议设成12以上,dynamic_axes不影响精度只影响shape灵活性,可以先固定输入尺寸排除变量。onnx-simplifier可以跑一下,对某些融合算子有帮助,但别指望它解决精度问题,核心还是得对比每层输出找差异。
我也踩过类似的坑,当时是SSD转ONNX,置信度掉得离谱。后来发现主要是opset版本太低,导致某些算子的实现路径不一样,建议先试试opset=11以上,同时把dynamic_axes设好。Focus被拆成小算子通常不影响数值,但SiLU在某些旧版本上会有精度损失,可以手动替换成等价的数学表达式试试。onnx-simplifier对这类问题帮助不大,更多是清理冗余结构,别指望它解决精度偏差。如果确认预处理和权重都一致,建议直接用onnxruntime的CUDA执行提供方跑一下,排除CPU和GPU数值差异的可能。
我之前用YOLOv5转ONNX也踩过类似的坑,置信度掉得离谱,最后发现是opset版本太低导致的。你试试把opset设到12以上,Focus和SiLU拆成小算子其实不影响结果,但某些旧版本导出时会把权重布局搞乱,数值就对不上了。dynamic_axes如果不设,onnxruntime会固定输入尺寸,你如果用了不同分辨率的图,预处理虽然一样,但内部插值逻辑可能被优化掉了,也会影响输出。onnx-simplifier建议跑一下,但它主要解决冗余节点,对精度问题帮助不大,反而有时候会把某些融合操作改坏,跑完务必再对一遍输出。你关掉amp是对的,但还得确认导出时模型是eval模式,而且batch norm层有没有被冻结,我上次就是忘了切eval,结果推理时BN还在用batch统计量,直接崩了。另外,你对比一下torch和onnx在同一个输入上的原始输出张量(不要只看NMS后的结果),如果差异很大,很可能是某个算子实现有bug,比如旧版onnxruntime对SiLU的近似计算有误差。实在不行就换torchscript导出,或者用onnxruntime的CUDA provider试试,有时候CPU和GPU的算子实现也不一致。
我之前也踩过这个坑,YOLOv5转ONNX置信度掉一截大概率不是量化问题,而是opset版本太低导致Focus和SiLU被拆解后精度有细微损失。建议先把opset设到12以上,dynamic_axes只对batch维度开就行,另外onnx-simplifier确实能合并一些冗余算子,但改完一定要逐层对比中间tensor的误差。还有个偏门但有效的办法:转的时候把模型里的SiLU换成ReLU6,精度影响很小,但ONNX兼容性会好很多。你要是急用可以试试,不然排查算子兼容性真的费时间。
我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉一截太常见了。你提到的Focus被拆解其实问题不大,PyTorch导出时本来就会把一些复合操作展开,关键还是看onnxruntime对每个子算子的数值精度处理,尤其SiLU这种激活函数,不同实现可能有微小浮点差异,但一般不至于让目标直接消失。我建议你先别急着怀疑量化,因为你说关了amp,那大概率不是精度模式的问题,反而可以查一下opset版本,我遇到过用默认opset 9导出时某些算子走的是旧版实现,换成opset 12或13后结果就正常了。dynamic_axes肯定要设置的,不然batch维和长宽被固定死,推理时输入尺寸不匹配可能触发隐式resize,这也会影响检测框。onnx-simplifier可以跑一下,但别指望它解决数值差异,它主要做图优化和常量折叠,对精度提升有限,不过能减少一些冗余节点让排查更直观。你还可以试试点开onnxruntime的日志看有没有警告某个算子fallback到CPU,如果GPU上跑的是残缺实现,那结果偏差就说得通了。另外可以做个对照实验,把ONNX的中间层输出和PyTorch对应层打印出来对比,看误差是从哪个节点开始放大的,这样能快速定位是算子还是图结构的问题。最后实在不行,可以考虑用torch2trt或者直接导出engine,但我觉得你现在这个case大概率是导出参数没配好,多试几个opset组合应该能解决。
我之前跑YOLOv5转ONNX也踩过类似的坑,置信度掉一截大概率不是量化的问题,而是opset版本和算子映射的锅。建议你先试试opset=12以上,然后把dynamic_axes配上,Focus和SiLU拆开之后输出数值精度会受影响,不妨用onnx-simplifier优化一下,我这边简化后误差明显小了很多。另外检查下onnxruntime的execution_mode,有时候CPU和CUDA的算子实现差异也会导致这种偏差,可以换用CUDAExecutionProvider对比看看。如果还不行,建议直接导出时把模型里自定义的focus层替换成普通卷积,省得后面折腾。
大概率是Focus被拆后padding逻辑变了,试试把opset设高一点或者手动改模型结构绕开。
大概率是Focus和SiLU拆解后数值精度丢了,试下opset设12以上加dynamic_axes,simplifier能解决一部分但核心还得看onnxruntime的算子实现。
我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉一截,大概率不是量化的问题,而是Focus和SiLU被拆解后精度丢失了,建议先试试opset设到12以上,还有dynamic_axes要配好,不然输出形状不对也会影响后处理。onnx-simplifier我用了之后确实有改善,但主要是减少冗余节点,对精度影响不大,你可以先跑一下看看有没有变化。另外,你torch和onnx的NMS阈值或者conf_thres是不是设的不一致?这个也容易忽略。如果还不行,建议把onnx的中间输出和torch对一下,定位是哪个层开始漂移的,这样比盲调快很多。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是量化,而是Focus算子的切片加拼接在onnxruntime里会被拆成几个Gather和Concat,精度本身没问题,但如果你用了旧版opset,某些算子的默认行为会变,比如SiLU在opset 9和11下的实现细节就有差异。我建议先把opset设到12以上,然后dynamic_axes一定要配好,尤其是batch维和输出维,不然anchor的网格对齐会乱。另外你提到置信度整体偏低,我怀疑是输出层的前处理差异,onnxruntime对sigmoid的数值稳定性处理跟torch不完全一致,可以试试在转换前把模型里的SiLU换成显式的x*sigmoid(x)再导出。onnx-simplifier对这类情况帮助有限,它主要做常量折叠和冗余消除,不会改算子语义,但如果你的图里有Shape和Gather导致的动态shape问题,跑一遍simplifier反而能减少runtime的推断分支,间接提升精度稳定性。还有个笨办法,直接对比两个引擎的逐层输出,定位到底是哪一层开始偏差,通常能发现是Resize的模式或者坐标变换的细节对不齐。如果实在不行,我最后是改用torch的jit trace再转onnx,绕开了部分op组合的歧义,效果比直接export好一些。
大概率是opset版本低了导致算子拆解精度损失,试试opset=12以上,能解决大部分问题。
遇到过类似的坑,先说结论:大概率不是量化问题,你amp都关了,重点还是放在算子兼容性和opset上。Focus和SiLU被拆成小算子很常见,但拆完数值精度理论上不会差这么多,除非某个子图被onnxruntime优化时触发了奇怪的融合逻辑。建议你先检查一下opset版本,YOLOv5官方转onnx一般推荐opset 11或12,太新或太旧都可能导致某些算子走fallback路径,精度反而漂移。dynamic_axes这个也得设,不然batch维或尺寸维被固定后,检测头那边的grid生成逻辑可能跟原模型不一致,置信度低往往就是anchor或grid对不上。onnx-simplifier可以试,但别指望它解决精度问题,它主要是删冗余节点,对SiLU这种激活函数的拆分方式不会有什么本质改变。我上次遇到类似情况最后发现是转模型时把detect层的输出顺序搞混了,你最好把torch和onnx的输出tensor直接打印出来逐元素对比,看到底是哪里开始分的叉。如果确认算子层面没问题,那再考虑是不是onnxruntime的CPU/GPU后端实现差异,换个execution_mode或者provider试试。实在不行再考虑TensorRT,但别急着重写,先把问题定位清楚。
大概率是Focus被拆成slice后坐标错位了,试试opset12以上加dynamic_axes,simplifier对SiLU没啥用。
之前跑过类似的坑,Focus和SiLU被拆开其实还好,主要是opset版本太低的话某些算子会走fallback实现,数值精度就变了。你可以试试opset=12以上,然后dynamic_axes把batch和长宽都设成动态,有时候固定shape反而会触发奇怪的优化。onnx-simplifier建议跑一下,能消掉不少冗余的reshape和transpose,但核心问题可能不在这。另外检查下YOLOv5导出时是不是带了nms,那个在onnx里表现和torch不一致很常见,建议先导出不带nms的纯backbone对比下输出分布。如果还是差很多,直接看下onnx里每个节点的输出和torch对应层对比下,定位到具体哪一层开始漂移。
我也踩过类似的坑,YOLOv5转ONNX最容易出问题的就是Focus层和SiLU激活,尤其是Focus在转的时候会被拆成slice和concat,如果opset版本太低或者没开onnx-simplifier,计算图里会残留一些冗余节点,数值上就会有微小偏差,但置信度掉这么明显我觉得不太像纯算子拆解导致的。
你可以先试试固定输入尺寸导出,同时把opset设到12以上,dynamic_axes先别开,跑一遍看看结果是否正常。我之前遇到过类似情况,最后发现是torch和onnx的BN层折叠方式不同,导致推理时统计量有细微差别,虽然预处理一致,但模型内部对输入数据的分布敏感度不一样。
另外,你说的量化问题,如果导出时没做量化,那大概率不是这个原因,除非你onnxruntime用了int8或fp16的execution mode。建议先用fp32跑,对比一下中间层输出,写个小脚本逐层打印torch和onnx的feature map,定位到第一个差异明显的层,基本就能锁定问题。
onnx-simplifier确实值得试,它能消除很多不必要的reshape和transpose,有时候这些操作会引入数值误差,虽然看起来只是copy操作,但在某些优化级别下会改变浮点运算顺序。如果simplify后还是不行,可以检查一下onnxruntime的log,看看有没有kernel fallback的警告,那个能直接告诉你哪些算子没被原生支持。
最后实在不行,可以试试把YOLOv5的检测头拆出来单独转,或者用onnxruntime的CUDAExecutionProvider跑,有时候CPU和GPU的算子实现差异也会导致结果不一致。别急着上TensorRT,先把问题定位清楚再说。
之前做分类模型也踩过类似的坑,onnxruntime和torch的数值精度本来就有点差异,尤其SiLU被拆开后误差会累积。建议先试试把opset拉到13以上,同时检查下dynamic_axes是不是只设置了batch维,高度宽度固定了容易出问题。onnx-simplifier不一定能解决置信度偏差,但能减少一些冗余算子,可以跑一下对比看看。如果还不行,优先怀疑预处理里的归一化参数被转成常量后精度丢了,我之前就是卡在这。
我之前部署YOLOv5也踩过这个坑,置信度偏低大概率不是算子拆解的问题,而是opset版本太低导致某些层精度表达不对,建议直接设opset=12以上,顺便把dynamic_axes配上。另外SiLU在ONNX里确实会被拆成sigmoid+乘,但推理结果理论上应该和原模型一致,你可以先导出后对比一下中间层的输出,看看差在哪一层。onnx-simplifier可以跑一下,但解决不了精度问题,主要还是检查输入输出的归一化或者后处理阈值有没有被隐式改动。如果还不行,试试用torch.onnx.export的opset=17,我上次就是这么解决的。