最近在把一个训练好的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后推理结果和原模型差很多,是量化问题还是图优化没配好?
全部回复
共 100 条这问题我当初也踩过坑,而且和你一样先怀疑预处理,结果查了半天发现是模型里的检测头在导出时被隐式改写了。你试试把torch.onnx.export里的opset版本往上调一调,比如13或14,YOLOv5的focus层在旧opset下经常会被拆成多个slice+concat,数值误差会累积。另外你提到置信度普遍低0.1-0.2,这个量级不像量化误差,更像是输出层的sigmoid或者anchor解码部分被图优化给“优化”掉了。建议你用onnxruntime的graph optimization level设为ORT_DISABLE_ALL跑一遍,如果结果变好,那就是优化规则触发了某些算子融合的bug。还有个土办法,把导出后的onnx用onnx-simplifier过一遍,它能消除一些冗余节点,有时候数值差异就是这些多余节点造成的。最后实在不行,干脆在导出前把模型的eval模式彻底关掉,包括batch norm的track_running_stats也检查下,虽然你说了预处理没问题,但模型内部如果有dropout或者训练态残留,也会导致推理差异。
之前调过类似问题,你试试把opset升到13以上,YOLOv5的focus层在低版本opset下容易产生额外重构节点,数值偏差会累积。另外排查时先单独导出一个单张图,用onnxruntime的IO Binding和torch逐层对比中间tensor,能很快定位到是哪个op开始漂移。顺便确认下onnxruntime的优化级别,CPU上有时默认的图优化会做常量折叠,反而改变了算子计算顺序。
先检查下ONNX里是不是多了些没用的Identity节点,之前我遇到类似问题就是图优化没吃透,加个simplify试试。
大概率不是量化问题,你opset12默认float32导出,先逐层对比下中间tensor误差定位到具体算子再说。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是量化,而是focus层用切片+concat实现时,onnxruntime的算子融合策略和PyTorch不完全一致,导致数值精度漂移。建议你先把opset升到13或14试试,然后导出时加一句torch.onnx.export(model, ..., opset_version=13, do_constant_folding=True),同时用onnxruntime.transformers.optimizer跑一下图优化,再对比输出。另外可以写个脚本,把onnx中间层的输出和pytorch对应层逐层比对,定位到具体哪一层开始偏差,这样比瞎猜高效得多。如果还是差0.1,那就要检查一下是不是模型里用了自适应池化或者上采样,这些在CPU上不同实现会有微小浮点误差,但一般不会差这么多。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是opset版本,而是focus层那个slice+concat的组合,在onnxruntime里有时候会走不同的实现路径。你可以先把onnx模型的输出跟pytorch逐层对比一下,用onnxruntime的run_options把中间节点输出打出来,定位到具体哪一层开始偏差变大,这样比瞎猜高效很多。
另外置信度普遍低0.1-0.2这个量级,我怀疑不一定是量化问题(你都没开量化吧?),更像是某些算子在CPU上的数值实现跟GPU上不完全一致,尤其是sigmoid和exp这类非线性函数,onnxruntime在CPU上可能用了近似指令。你可以试试把opset升到15或者16,有些算子映射会更精确,或者干脆在导出时把模型切成几个子图分别验证。
还有个很隐蔽的点,YOLOv5的anchor grid生成在导出时如果用了动态shape,onnxruntime的shape推理可能会把某些维度当成常量折叠掉,导致坐标解码偏移。你可以固定输入尺寸再测一次,如果结果正常,那就是动态shape的锅,得手动把grid生成部分改成显式计算。
最后实在不行,可以考虑用onnx-simplifier过一遍模型,有时候它能去掉一些冗余的reshape和transpose,间接避开某些有精度问题的算子组合。别急着怀疑量化,先把数值偏差定位到具体层再说,八成是图优化踩了某个算子的坑。
先检查下ONNX模型输出是不是fp32还是被转成fp16了,CPU上精度掉这么多大概率不是图优化问题。
我之前也踩过这个坑,YOLOv5转ONNX对focus层和slice的兼容性确实容易出问题,尤其是opset12下部分算子会走fallback路径。建议你先把onnxruntime的日志级别调到VERBOSE,看下是不是有算子被替换成了低精度实现;另外可以试下opset13+,新版IR对slice和reshape的优化明显好一些。如果还不行,就逐层对比输出,写个小脚本把中间tensor导出来跟PyTorch对一下,基本能定位到是哪个节点开始漂移的。
这问题我太有同感了,之前调SSD的时候也栽过类似的坑,置信度掉得没你这么夸张但趋势一模一样。你先别急着怀疑量化,opset12默认走的还是fp32,除非你显式开了动态量化,否则精度损失大概率出在算子实现差异上。YOLOv5那个focus层确实是个重灾区,slice+concat的组合在ONNX里经常被重排成奇怪的shape变换,数值上会有微小的浮点误差,但0.1-0.2的置信度差距我觉得光靠浮点误差解释不了,更像是某个后处理步骤在转换时被隐式改掉了。建议你先把onnxruntime的推理结果导出来,跟PyTorch逐层比对中间tensor的max绝对误差,定位到具体是哪一层开始分叉的,别上来就调export参数。另外你试过用onnx-simplifier跑一遍吗?有时候图优化反而会引入不安全的融合,尤其对动态shape的模型,简化后精度可能就回来了。还有个小细节,YOLOv5的anchor grid生成在导出时如果用了固定的图像尺寸,但推理时又允许不同分辨率,那置信度偏移会特别明显,你确认下是不是这个原因。要是还查不出来,可以把转换后的onnx放到netron里看看输出层有没有多出奇怪的sigmoid或者rescale节点,经常是这里被重复计算了。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的是focus层,slice和concat在opset12下精度确实有损失,建议先单独导出focus部分对比下输出。另外你试过用onnx-simplifier处理一下吗?有时候冗余op会影响推理结果,我上次就是simplify后置信度就正常了。还有个小细节,yolov5官方仓库那个export.py里有针对onnx的优化参数,直接用它导出可能比手写torch.onnx.export更稳,你可以对比下两边的onnx文件看节点差异。
这个置信度差异大概率不是量化问题,先看看onnxruntime的execution_mode和优化级别,CPU上试试ORT_ENABLE_ALL,另外检查下yolo的anchor输出是不是被转成float16了。
先检查下模型里有没有用到上采样或近邻插值,opset12对这块支持容易出偏差。
建议先逐层对比输出,重点查focus切片和上采样,我上次就是bilinear对齐方式不一致导致掉点。
我之前也踩过这个坑,yolov5转onnx最容易出问题的就是focus层和上采样那块的算子映射,opset12对某些slice和concat的组合支持得并不好。建议你先用torch.onnx.export带的onnx checker和onnxruntime的graph优化日志对比一下,看看有没有warning提示某几个节点被替换成低精度实现。另外可以试试把opset调到13或14,有时候高版本对slice的语义解析更准确,置信度差异会小很多。还有个土办法,把输出层的sigmoid挪到onnx外面算,排除是融合算子精度问题。
重点查一下预处理到输入tensor的通道顺序,YOLOv5的focus层转ONNX容易在切片上出精度偏差,建议先导出时加onnx.checker验证下。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的地方其实是输出端的decode部分,你对比下原始模型的输出是不是带anchor的raw预测值,ONNX里如果没把decode一起导进去,后处理稍有差异置信度就会掉一截。另外建议你试试把opset提到13或14,有些slice和cat算子在低版本上会走不同实现,数值误差会被放大。还有个笨办法,把onnxruntime的算子执行改成CPU EP的逐算子调试,打印中间tensor和torch对一下,基本能定位到具体是哪层开始漂移的。
遇到过类似的坑,当时也是YOLOv5转ONNX,置信度掉得没你这么夸张但也够烦。你这个情况大概率不是量化问题,因为你根本没做量化对吧?纯FP32导出的话,数值差异主要就来自opset的算子拆解方式,尤其是focus层在ONNX里会被拆成slice+concat,某些版本下slice的step处理跟PyTorch原生的索引逻辑不完全一致,累积起来就会让特征图有微小偏移。
我建议你先别急着调图优化,第一步把onnxruntime的推理结果跟PyTorch逐层对比,用onnxruntime的IO Binding或者直接把中间张量导出来看差异在哪一层开始放大。另外你opset设12的话,试试把opset提到13或者14,有些slice和gather的语义在新版本里更贴近PyTorch的默认行为。
还有个很隐蔽的点,YOLOv5导出时如果没把detect头里的anchor网格生成逻辑一起导出,而是留在了后处理里,那ONNX模型输出的原始张量跟PyTorch的decode后结果对比本身就差着一层,你检查一下是不是把后处理也包进模型了。如果没包,那置信度差异很可能就是你后处理代码里对anchor的缩放方式跟PyTorch训练时不一致。
最后实在不行,可以试试用onnx-simplifier过一遍,有时候能消除一些冗余的shape操作带来的精度损失。别太迷信keep_initializers_as_inputs,那个一般只影响推理速度和内存占用,跟数值精度基本没关系。定位问题用二分法,先固定输入,把模型里每个block单独导出对比,总能找到罪魁祸首。
我之前也踩过类似的坑,yolov5的focus切片在onnx里容易被拆成一堆gather,数值误差确实会累积,但0.1-0.2的置信度差距有点大了。建议你先用onnxruntime的session.run单独对比某一层输出,把中间tensor导出来用numpy比对,定位是不是focus或者后面某些op的精度问题。另外你试过opset 11或者16吗?12有时候对某些算子处理挺怪的。还有,检查一下导出时模型是不是处于eval模式,batch norm层的running统计量和训练模式下的行为不一样,这个影响其实比量化大。
我之前也踩过类似的坑,后来发现是onnxruntime的算子实现精度和PyTorch不完全一致,尤其是focus层在转换时如果用了切片+concat,数值上会有细微漂移。你可以先用onnxruntime的CPUExecutionProvider跑一下,再用TensorRT或者openvino对比,如果差异变小说明是runtime的问题。另外建议把opset升到13或14试试,有些算子在新版本里映射更准。还有个小技巧,导出时加dynamic_axes的同时,把模型的eval模式确保关掉dropout,不然推理结果会随机波动。如果还不行,就逐层对比输出,写个脚本把每个中间节点的值dump出来,定位到具体哪一层开始偏差。
我之前用YOLOv5转ONNX也踩过这个坑,置信度掉一截大概率不是量化问题(你opset12默认fp32),更像是focus层切片+concat的转换引入了数值误差。建议先导出时加torch.onnx.export(model, ..., opset_version=12, dynamo=False),再在onnxruntime里逐层对比中间tensor,尤其是focus后第一个卷积的输入,看看是不是有微小偏移被放大了。另外小目标漏检也可能和NMS的阈值设置有关,ONNX里如果自己写了后处理,试试把conf_thres调低0.05对比下。我最后是靠改用opset11+显式重写focus为conv解决的,你可以先试试这个方向。