最近在把训练好的YOLOv5s模型转成ONNX,然后部署到手机端。转换过程没报错,但用onnxruntime推理时,输出的检测框置信度整体偏低,有些原本能检出来的目标直接漏掉了。对比了opset版本(用的12),也试过动态/静态batch,问题依旧。我怀疑是不是某些算子(比如Focus、SiLU)在转换时被替换成了近似实现?或者转换时精度损失是正常的?有没有人遇到过类似情况,一般怎么排查是哪个环节出了问题?另外,如果量化到INT8,是不是误差会更大?先谢谢各位了。
PyTorch模型转ONNX后推理结果和原模型对不上,是量化问题还是算子不支持?
全部回复
共 30 条大概率不是算子问题,先试试onnxruntime和pytorch用同一张图逐层对比输出,定位到具体哪一层开始漂移。INT8量化肯定更严重,先解决float32的误差再说。
先查下转onnx时有没有警告,Focus和SiLU一般不会丢精度,大概率是预处理或后处理没对齐。
试试用onnxruntime跑同一张图对比原模型输出,先别上int8,那个误差确实会放大。
我之前也踩过这个坑,YOLOv5转ONNX后置信度偏低大概率不是opset的问题,而是Focus和SiLU被拆成多个小算子后,某些版本对计算顺序或精度处理有细微差异,你试试在转换时加上--simplify并用onnx-simplifier过一遍,很多时候能解决。另外,可以先跑一下onnxruntime的CPU和GPU对比,排除是不是后端实现差异导致的数值漂移。至于INT8,肯定误差更大,尤其是小目标,建议先保证FP32对齐了再考虑量化,不然问题会叠加。你检查过输出层是不是被额外加了些后处理算子吗?有时候是NMS被并进去导致逻辑变了。
我之前也踩过这个坑,YOLOv5转ONNX后置信度偏低大概率不是量化的问题,因为你是FP32直接转的。建议先检查一下预处理是不是对齐了,尤其是归一化方式或者letterbox的填充值,稍有差异输出就会变。Focus层确实会被重构,但通常不影响精度,SiLU在opset12下也能正常映射。我上次是用netron对比了每个节点的输出,发现是某个Bilinear插值参数被改了。另外如果后面真要做INT8,误差肯定会更大,建议先跑个静态校准,用几百张真实图算好阈值再量化。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截大概率不是opset的问题,你可以先检查下导出时有没有把model.eval()和torch.no_grad()加上,另外Focus层在onnx里会被拆成切片加卷积,精度影响很小,SiLU一般是原样支持的。建议你先把输出层的raw prediction直接打印出来对比,看是后处理(比如nms阈值)变了还是模型输出本身就不同,如果原始输出差异大,再逐层对比中间tensor。INT8量化误差确实会更大,尤其是第一层和最后一层敏感,建议先用FP16试一下,或者用onnx-simplifier看看有没有多余op被替换。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的就是Focus层,它本身是切片加concat的操作,某些onnxruntime版本会把它优化成stride=2的卷积,数学上等价但浮点累加顺序变了,结果就差一点点。你那个置信度整体偏低,很可能就是这个原因,建议先用onnx-simplifier过一遍图,再对比每个节点的输出,定位到具体是哪个op开始产生偏差。另外SiLU一般不会丢精度,它就是个x*sigmoid(x),ONNX原生支持,除非你导出时用了老版本torch,把它拆成了sigmoid+乘法的组合,那中间值精度会有微小损失。至于量化到INT8,误差肯定更大,尤其是检测头最后的输出,对数值敏感,我建议你先把FP32的ONNX调准了,再考虑量化,不然排查起来更头疼。还有个思路,你可以用onnxruntime和torch的CPU推理跑同一个输入,逐层打印中间tensor的余弦相似度,很快能找到是哪个算子变了。如果实在查不出来,试试直接导出opset13或者14,有些旧版本对某些op有已知bug。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截,大概率不是量化的问题,因为你还没做INT8,float32直接转不应该有这么大的精度损失。你怀疑Focus和SiLU被替换,方向是对的,但更常见的原因是模型里的上采样或者grid生成部分,onnxruntime对某些算子的实现跟PyTorch原生行为有细微差异,尤其是涉及到坐标偏移或者sigmoid的数值精度时,结果就会飘。建议你先别急着上onnxruntime,用onnx的Python API配合onnxruntime的CPUExecutionProvider跑一遍,同时把原始PyTorch模型的输出导出成numpy,逐层对比中间tensor的差异,定位到具体是哪个op开始出现偏差。我遇到过类似情况,最后发现是torch的upsample的align_corners参数默认值和ONNX转换后的行为不一致,改一下导出参数就解决了。至于INT8量化,误差肯定比float32大,但如果你现在float32都还没对齐,先别碰量化,否则问题会叠加,根本没法排查。还有个土办法,把onnx模型用onnx-simplifier过一遍,有时候能消除一些冗余op,顺便避免某些算子被替换成低精度版本。你试试把opset换到11或者13看看,12有时候会有奇怪的兼容性问题。
我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉一截,多半不是opset的问题,而是Focus层和SiLU被拆成多个小算子后,某些实现在fp32下就有细微差异,尤其在低阈值目标上会被放大。你可以先试下把onnxruntime的execution_mode设成ORT_PARALLEL,或者开一下graph优化,有时候能改善;再不行就导出时加个simplify,把冗余算子清掉看看。INT8量化误差肯定更大,尤其是对置信度敏感的任务,建议先确认fp32对齐了再碰量化,不然排查起来更头疼。你用的onnxruntime哪个版本?不同版本对算子支持也有坑。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的就是Focus层,onnxruntime对它的支持不太好,建议你先用onnx-simplifier处理一下再对比输出。另外SiLU一般不会导致这么大偏差,可以先把输出层改成float32逐层对比,看看是哪一层开始分叉的。至于INT8量化,误差肯定更大,尤其是小目标,建议先搞定FP32的一致性再考虑量化。你试过用torch.onnx.export的keep_initializers_as_inputs参数吗?有时候这也会影响结果。
我之前也踩过这个坑,YOLOv5转ONNX后置信度偏低大概率不是量化问题,opset12下Focus和SiLU确实会被拆成子图,但精度影响很小。建议你先用onnxruntime的python接口逐层对比输出,定位到具体哪一层开始漂移,另外检查下预处理(比如归一化、letterbox)在转换前后是否完全一致,这个最容易忽略。还有,如果你只做FP32推理,暂时别碰INT8,那个误差会放大好几倍,等FP32对齐了再考虑量化。最后可以试试把opset升到13或14,有些旧版本算子替换有bug。
之前跑YOLOv5转ONNX也踩过这坑,置信度偏低多半不是opset问题,你试试把model.eval()开着转,然后检查下输入输出是否归一化一致,Focus层在转的时候确实容易出幺蛾子,可以手动改成卷积。另外ONNX默认的SiLU实现和PyTorch的精确度有细微差异,但一般不会导致漏检这么明显,建议先跑一下onnxruntime和PyTorch的逐层输出对比,定位到具体哪一层开始漂移。INT8量化误差肯定更大,尤其对检测头影响明显,建议先解决FP32精度,再考虑量化。
建议先单独验证下SiLU和Focus的输出,我上次是发现某些op被替换成近似实现导致精度掉。
试试用onnxruntime的CPU和GPU分别跑下,如果结果差异大那基本就是算子兼容性问题。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的其实不是SiLU,而是Focus层,PyTorch里是切片+concat,ONNX导出时可能被拆成多个slice,计算顺序变了精度就有微妙差异。建议先用onnx-simplifier过一遍,再看下onnxruntime的日志有没有警告哪些算子走了fallback。另外你试过把opset提到13或者14吗?有些版本对切片和resize的支持更完善。至于INT8量化,误差确实会放大,尤其是置信度阈值附近的目标,建议先跑FP32排查逻辑问题,最后再考虑量化。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的其实不是Focus,而是SiLU和上采样这块。你opset用的12没问题,但onnxruntime对某些算子的实现精度确实和PyTorch原生有点差异,尤其是SiLU,它可能会被拆成sigmoid加乘法,浮点误差在深层网络里会累积,置信度偏低就很正常了。我建议你先别急着怀疑量化,先检查一下转换后的onnx模型里是不是多了很多奇怪的节点,比如Reshape或者Transpose,这些有时候会把输出tensor的布局搞乱,导致后处理时坐标和置信度对不上。另外你可以试试用onnxruntime的CUDAExecutionProvider和CPU分别跑一遍,看看结果差异大不大,如果CPU反而更接近原模型,那大概率是算子的数值稳定性问题。我之前遇到类似情况,最后是手动把Focus层改成普通的Conv加切片,然后把SiLU换成ReLU重新训练了一版,精度损失可以接受,但推理稳定多了。至于INT8量化,误差肯定更大,尤其是你原本就有漏检,量化后可能更严重,建议先解决FP32的精度对齐,再考虑量化。你可以把onnx模型导出来用Netron看一眼,重点检查输出层的结构,看看是不是后处理里用的anchor和stride没对上。
我之前也踩过YOLOv5转ONNX的坑,置信度偏低大概率不是量化的问题,因为你还没做INT8,只是FP32转换对吧?我那次排查到最后发现是Focus层在转换时被拆成了多个Slice和Concat,虽然数学上等价,但某些onnxruntime版本对这几个算子的融合优化做得不好,导致中间结果有微小差异,叠加起来置信度就掉了。
建议你先别急着怀疑SiLU,这个算子一般会换成Sigmoid或者直接展开,精度损失很小。更靠谱的排查办法是逐层对比输出,用onnxruntime的IOBinding把中间tensor导出来,和PyTorch的hook输出对一下,看哪层开始偏差变大。我之前就是这么定位到是某个Resize算子的坐标变换模式(align_corners)不一致导致的。
另外,opset12其实有点老,YOLOv5官方推荐opset11或13,你试试13,它把一些动态shape和Resize的语义改得更明确了。动态batch和静态batch都试过但还是有差异的话,看看你导出时有没有加“keep_initializers_as_inputs=False”,这个有时候会影响图优化。
至于INT8,我只能说误差会显著放大,尤其对YOLOv5这种输出层比较敏感的网络,我试过PTQ量化后mAP掉了快8个点,所以建议先把FP32的一致性调好再说。你可以先用onnx-simplifier把图精简一下,再导出试试,我那次简化后就直接对齐了。
我之前也踩过这坑,多半是SiLU转成近似实现精度丢了,先试下用onnx-simplifier看看图结构。
量化到INT8误差肯定更大,建议先float16跑通再谈量化。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截,大概率是Focus和SiLU被拆成小算子后,onnxruntime的图优化给做了一些融合或者精度重排,跟PyTorch原生的计算顺序对不上。你可以试试把opset升到13或者14,有时候能解决一些隐式转换问题,另外用onnx-simplifier处理一下,看输出是否一致。至于INT8量化,误差确实会更大,尤其是第一层卷积和最后的检测头,建议先跑FP16看能不能接受,再考虑量化感知训练。你可以在转换后逐层对比中间tensor的数值,定位到第一个差异出现的节点,基本就能锁定是哪个算子的问题了。
建议先关掉onnxruntime的优化试下,另外YOLOv5导出时把opset调高到13+,Focus用卷积替代能解决不少问题。
先关掉整图量化试试,YOLOv5的Focus和SiLU在ONNX里容易出精度偏差,用onnx-simplifier优化下再对比输出。
大概率是SiLU被替换成近似实现导致的,先单独比对下中间层输出吧。INT8误差肯定更大,建议先搞定FP32一致性再谈量化。