最近在把一个训练好的YOLOv5模型转成ONNX部署到CPU上,用的torch.onnx.export,opset设的12。转出来以后用onnxruntime测了一下,发现输出框的置信度普遍比PyTorch原模型低0.1-0.2,有些小目标直接漏检了。我对比了输入预处理,归一化方式和尺寸都没问题,也试过dynamic_axes。网上搜了一堆,有人说要加torch.onnx.export的keep_initializers_as_inputs=False,我试了没变化。也怀疑是不是算子精度问题,比如focus层和slice操作转换后数值有微小差异,但不知道该怎么定位。有没有大佬遇到过类似情况?是应该调整opset版本,还是需要在导出时做额外的校准或者融合操作?求指点一下排查思路。
PyTorch转ONNX后推理结果和原模型差很多,是量化问题还是图优化没配好?
全部回复
共 120 条我之前也踩过类似的坑,后来发现多半不是量化的问题,而是focus层转出来被拆成切片加concat,精度确实会有轻微抖动。你可以试试把opset升到13以上,或者用onnx-simplifier过一遍图,有时候能消掉一些冗余节点。另外建议你单独把onnx的输出和pytorch的中间层特征dump出来对比,看到底是从哪一层开始偏差的,这样定位更快。还有个小细节,检查下转的时候有没有把model设成eval模式,dropout和bn的行为不一样也会差这么多。
我之前也踩过类似的坑,后来发现问题不在ONNX本身,而是YOLOv5的检测头里有个隐式的anchor grid计算,转ONNX后某些常数折叠方式变了,导致解码坐标时小数点后几位有差异。你可以试着把模型的eval模式彻底关掉,还有检查一下有没有用到torch.jit.trace的check_trace,有时候是tracing时输入shape的固定导致动态分支被硬编码了。另外你试试opset 11或者13,12对某些slice算子处理确实有点迷,我换到11之后置信度偏差就小多了。如果还不行,建议用onnxruntime的graph_optimization_level逐个调一下,有时候是优化器把一些敏感算子合并了。
这个现象我碰到过类似情况,后来定位到是yolo里的anchor网格坐标在onnx里默认用了float32,而pytorch这边是float64,累计误差在置信度阈值边缘就会放大。你可以试着把模型里所有tensor显式cast成float32再导出,先排除精度问题,别急着怀疑量化。另外opset 12对某些slice和concat的组合确实会生成低效图,建议直接换成opset 13+,有时图优化规则不一样结果就正常了。如果还不行,建议把onnxruntime的execution_mode设成sequential,逐节点对比中间输出,这种排查比瞎调参数快得多。
我之前也踩过类似的坑,YOLOv5转ONNX特别容易在focus层和slice上出问题,因为pytorch的切片操作在onnx里会被拆成好几个Gather或者Split,数值精度确实会有一点点漂移。你opset12的话,建议先试试opset11,有些老算子对focus的支持反而更稳定,我这边当时换到11就明显好了一些。另外你说置信度整体低0.1-0.2,这个幅度不太像纯算子误差,更像是后处理里nms阈值或者坐标解码方式在导出时被固化成了不同的逻辑,比如pytorch里用的是直接sigmoid,而onnx模型里可能被融合成了别的形式,你可以把onnx的原始输出打印出来和pytorch的feature map逐层对一下,看是哪个stage开始偏差变大的。还有个小技巧,导出前把模型设成eval模式,并且把batch size固定成1再试试,有时候batch维度上的动态shape会触发onnxruntime的图优化重排,反而导致精度变化。如果你用的是yolov5官方仓库,可以看看他们的export.py里有个optimize参数,那个会做一遍onnx-simplifier,能去掉不少冗余的转换节点,对数值一致性有帮助。最后实在不行就对比一下onnxruntime和pytorch的输入张量是不是真的bitwise一致,有时候预处理里的padding方式不同也会造成小目标漏检。
这问题我碰到过,大概率不是量化的事,你opset12默认fp32导出的话精度损失不会这么大。重点查一下预处理里有没有用torch的某些操作,比如letterbox的填充值或者颜色通道顺序,ONNX这边容易在维度操作上出隐性问题。还有个思路,你可以把onnxruntime的CPUExecutionProvider改成TensorrtExecutionProvider或者OpenVINO试试,如果结果变好了那基本就是图优化没吃透。另外小目标漏检也可能是非极大值抑制的后处理差异,ONNX模型里不带NMS的话,你外部实现和torch原版逻辑对不齐也会这样。
先试试把opset调到13以上,focus层用卷积等效替换再导一次,多半是算子兼容的精度坑。
我之前也踩过类似的坑,YOLOv5转ONNX的置信度漂移大概率不是量化问题,因为你用的是fp32导出,跟量化关系不大。更可能出在模型里的自定义算子或者pytorch和onnxruntime对某些op的实现细节不一致上,比如focus层如果用view+slice实现,不同版本的onnxruntime优化策略会导致数值偏差累积。你可以先试着把onnx的opset调到13或更高,有些低版本对slice和gather的支持会引入额外转换,精度损失反而更大。另外建议你把onnx模型用onnx-simplifier过一遍,它会折叠掉很多冗余的reshape和transpose,有时候这些多余操作在推理时会被onnxruntime重排,造成浮点运算顺序变化。如果还不行,就逐层对比中间tensor,导出时在模型里临时加hook把关键层输出存下来,再用onnxruntime的run接口获取同名节点输出,差值最大那层就是问题源头,我当初就是这么定位到focus层前面的一个像素重排操作上。还有个容易被忽略的点,检查一下导出时是不是开了eval模式但模型里还有dropout或BN的training状态没关干净,这也会导致输出分布偏移。最后实在不行就考虑用torch2trt或者openvino的yolo专用路径,虽然不能完全等价,但工程上更省心。
试试把opset降到11,12对某些slice处理确实有精度坑,我之前也踩过。
这个差值0.1-0.2确实挺典型的,我之前转YOLOv5也踩过类似的坑。你重点先别怀疑量化,因为export默认是FP32,根本没走量化流程,问题大概率出在图优化或者算子实现差异上。先拿ONNX Runtime关掉所有图优化再跑一遍,用session_options.graph_optimization_level = ORT_DISABLE_ALL对比下,如果关了优化结果就接近了,那就是融合算子搞的鬼。再就是focus层,它本质是slice+concat,onnx里转换后数值精度可能因为不同的实现顺序产生累积误差,尤其小目标本身响应就弱,更容易被吃掉。建议你逐层dump中间输出,拿onnxruntime的profiling或者自己插节点对比PyTorch的feature map,定位到具体哪一层开始偏。还有个容易忽略的,YOLOv5的anchor decode和NMS如果是在模型里带出去的,导出时可能被追踪成常量折叠,检查下输出是不是已经过了sigmoid或者decode。实在不行opset降到11试试,12在某些slice和resize组合上确实有过已知的数值偏差。
我之前也踩过这个坑,置信度整体掉一截大概率不是量化,而是onnxruntime默认的图优化把某些算子融合后精度变了。你可以先试下把optimization level设成ORT_DISABLE_ALL,跑一遍对比看看差异还在不在。如果关掉优化就正常,那基本能锁定是图优化的问题。另外YOLOv5里slice和focus那几层在opset12下确实容易出幺蛾子,建议用onnxruntime的逐层输出对比工具卡一下,看从哪一层开始数值对不上。
这个我踩过类似的坑,YOLOv5转ONNX其实最容易出问题的是后处理那块,尤其是anchor解码和NMS,有时候不是模型本身数值差,而是导出时把decode逻辑也带进去了导致对不齐。你可以先只导出backbone+head,把后处理放到外面用numpy或onnxruntime自己写,看看置信度还差多少。另外opset 12对slice和focus的支持确实有点坑,试试换成opset 11或者13对比一下。定位的话建议逐层dump中间输出,用onnxruntime的debug工具跟PyTorch对,一般能揪出是哪一层开始飘的。
这个坑我踩过,大概率不是量化的问题,因为torch.onnx.export默认根本不做量化,opset 12也不涉及。置信度整体低0.1-0.2这种系统性偏移,更像是某几个算子在ONNX里实现细节不一致导致的。YOLOv5里最可疑的就是Focus层的slice+concat,还有后面Detect里的sigmoid和decode部分,PyTorch和onnxruntime在浮点累加顺序上会有细微差别,经过多层放大后就体现在置信度上了。建议你先把模型切成几段,从backbone输出开始逐层对比PyTorch和onnxruntime的中间tensor,用np.testing.assert_allclose看第一个出现明显偏差的位置在哪。另外别忘了检查一下export的时候模型是不是处于eval模式,BN层和dropout在train模式下转出来结果肯定对不上。dynamic_axes一般不影响数值,但如果你的输入尺寸和导出时不一致,reshape相关算子可能会出问题。定位到具体算子之后,要么换opset版本试试,要么手动改一下那部分的实现绕过去。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉,最后发现是推理时的padding方式跟原版不一致,letterbox的scale和pad值算错了,建议你逐层对比中间输出。可以用onnxruntime的debug工具或者自己写脚本把focus层前后的特征图dump出来比一下,数值差异大的地方基本就是问题所在。另外opset 12对某些slice支持确实不太一样,试试降到11或者升到13看看有没有改善。
先用onnxruntime和PyTorch逐层对数值,大概率是某个算子精度掉了,不是量化问题。
先拿onnxruntime和PyTorch逐层比对输出,多半是某个算子精度掉了,不是量化的事。
我之前也踩过这个坑,YOLOv5转ONNX掉点大概率不是量化,而是Focus层的切片拼接在opset12下数值对齐有问题。你可以先固定随机种子,把 PyTorch 和 onnxruntime 同一张图的中间特征图dump出来逐层比,一般能定位到某个Slice或Concat。另外记得把模型切到eval()再导出,BN和dropout没关也会导致输出漂移。
我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉点,最后定位到是focus层里的slice和concat在opset12下数值对不齐,CPU上尤其明显。你可以先用onnxruntime逐层dump中间输出,和PyTorch对应层对比,基本能锁定是哪一块出的问题。另外建议把opset升到13或16再试,有些算子精度处理改过。还有别忘了检查anchor解码那部分,ONNX里如果网格生成有偏差,小目标就特别容易漏。
这问题我踩过,大概率不是量化的事,你压根没开量化吧。先拿ONNX Runtime和PyTorch在同一个输入上跑,把中间层输出逐层比对,重点看Focus那个slice+concat和后面的sigmoid。另外opset 12对某些算子会做常量折叠,试试opset 11或者关掉优化再导出,看差异还在不在。
我之前也踩过这个坑,大概率不是量化问题,因为你不转ONNX量化这一步根本没参与。先重点查一下Focus层的切片和concat,那玩意在opset12下经常有数值偏差,建议逐层dump输出对比。另外试试opset11或者13,有时候12就是个坑。
YOLOv5转ONNX掉点这个事我踩过好几次,置信度整体偏低0.1到0.2,八成不是量化的锅,因为你压根没提到自己做了量化,那基本就是图优化和算子对不齐的问题。重点去查一下Focus层和后面的Slice,这俩在导出时经常被拆成Slice加Concat或者Transpose的组合,数值上会有细微偏差,经过sigmoid之后就会被放大成置信度差异。有个挺实用的定位方法,你在PyTorch里逐层hook输出,同时在onnxruntime里用中间节点输出做对比,把第一个误差超过1e-3的节点揪出来,基本就能锁定是哪一步转歪了。另外别忽略后处理,YOLOv5的decode和NMS如果一边在PyTorch一边在onnxruntime里做,anchor网格的生成顺序、stride对齐方式稍有不同,小目标漏检就很正常。opset 12对某些算子支持不算最稳,可以试试降到11或者升到16对比一下,有时候换个opset结果就回来了。还有别忘了检查模型是不是处于eval模式,BN和dropout没切eval也会让输出飘。