最近在做一个部署项目,把训练好的YOLOv5模型转成ONNX,再用onnxruntime推理。结果发现输出的检测框置信度整体偏低,有些原本能检出来的目标直接漏检了。试了opset 11和12,也试过dynamic_axes,情况没改善。
PyTorch转ONNX后推理结果和原模型差很多,是量化问题还是算子支持问题?
全部回复
共 20 条我之前也踩过这个坑,排查下来大概率不是opset的问题,而是YOLOv5导出时自带的一些后处理逻辑(比如NMS和缩放)没被完整转换。你可以先关掉onnxruntime的优化试试,或者直接对比一下中间层的feature map输出,看到底是哪个阶段开始偏差的。
另外量化的话,如果你没显式做动态/静态量化,默认应该是FP32推理,所以精度损失应该不是主因。我更怀疑是模型里某些算子(比如Focus层或者自定义的切片)在ONNX里被转换成了低效或不兼容的实现。
还有个偏门但常见的坑:导出时如果没固定输入尺寸,dynamic_axes会让某些reshape操作在动态shape下行为异常,导致置信度分布偏移。你试试用固定shape导出,加上--grid和--end2end这些YOLOv5自带的导出参数,说不定就正常了。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截,大概率不是量化的问题,你都没开动态量化对吧?建议先检查一下预处理和后处理是不是跟原模型完全一致,尤其是BGR/RGB顺序和归一化系数,很多情况下是这里悄悄变了。另外onnxruntime默认的算子实现跟PyTorch的某些细节有差异,比如NMS的iou阈值实现,你试试把输出层的后处理拆出来自己写,别用模型里的。如果还不行,可以导出时把model.eval()和torch.no_grad()加上,偶尔会影响bn层的统计参数。
试过把模型的eval模式关掉再导出吗?YOLO的BN层在train和eval下差异挺大的,我之前就这么踩过坑。
先查下预处理和后处理是不是一致,尤其归一化和letterbox的参数,ONNX这边经常在这上面出偏差。
我之前也踩过这个坑,置信度掉一截大概率不是opset的问题。你先检查下预处理是不是一致,比如归一化用的0-1还是0-255,还有BGR/RGB通道顺序,YOLOv5转ONNX时这些最容易隐性出错。如果预处理确认没问题,再看看模型里有没有用到一些比较新的算子,像Mish或者Focus层在ONNX里可能被拆成多个小操作,精度会有微小损失,但一般不会导致漏检这么夸张。实在不行可以试着用onnx-simplifier过一遍图,有时候冗余计算也会影响数值稳定性。
先检查下预处理里归一化参数有没有对齐,我之前就是栽在这上面,差一点点结果天差地别。
我之前也踩过类似的坑,先说结论:大概率不是量化问题,因为你这还只是转ONNX,没做int8量化,精度掉这么多基本就是算子和图优化导致的。YOLOv5的detect头里有些自定义操作,比如grid生成、anchor解码这些,PyTorch里实现和ONNX的算子映射不完全一致,尤其opset版本低的时候,某些reshape和concat的顺序会被重排,数值上就有细微差异,累积到后面置信度就偏了。
我建议你先别急着调opset,把导出的ONNX用onnxruntime的session options里的优化级别调低,比如把graph optimization level设为ORT_DISABLE_ALL,看结果是不是能接近原模型。如果能,那说明是ORT的图优化把某些节点融合错了,而不是算子不支持。另外,你可以用onnxruntime的Python API跑一下onnx.checker和onnx.shape_inference,确认模型结构没被破坏。
还有个小技巧,导出时把model.eval()和torch.no_grad()都加上,然后检查一下输入输出是否加了批处理维度,YOLOv5默认是(1,3,H,W),但有些版本会带num_anchors的中间维度,这会导致输出tensor的shape和原模型对不上,置信度自然就崩了。如果实在找不到,试下用onnx-simplifier处理一遍,它能把冗余节点和常量折叠掉,有时候能解决精度漂移。
最后问一下,你对比过ONNX和PyTorch输出特征图的逐通道数值吗?如果差异集中在某些通道,可能就是某个特定算子(比如Sigmoid或者Exp)在精度上有问题,可以针对性地替换成等价实现。
大概率是预处理和后处理没对齐,尤其是归一化和letterbox的细节,先排查这个再怀疑算子。
我之前也踩过这个坑,yolov5转onnx最容易出问题的是前后处理那部分,尤其是anchor和grid生成逻辑,建议先排查一下这部分有没有被正确映射。另外opset版本影响不大,但onnxruntime的精度模式可以试试用FP32跑一遍,排除半精度导致的置信度衰减。还有个小细节,如果用了torch的nn.Upsample,在onnx里可能被替换成Resize,坐标变换模式不同也会影响小目标检测,可以改成nearest或显式指定coordinate_transformation_mode。最后实在不行就用onnx-simplifier过一遍图,有时候冗余节点会导致数值漂移。
我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉一截,排查半天发现是模型里的某些自定义op(比如Focus层)在ONNX里被拆解的方式不一样,导致数值精度有细微偏差。你可以先试试把onnxruntime的execution_mode设成ORT_PARALLEL,或者对比一下onnxruntime和PyTorch的CPU/GPU输出差异,看是不是纯推理后端的问题。另外,如果用了半精度导出,大概率是量化误差,建议先用FP32排查,再考虑算子兼容性。我之前还遇到过opset版本影响NMS后处理的情况,你可以检查下导出的图里有没有额外的后处理节点。
我也碰到过类似情况,不过最后发现是YOLOv5的anchor grid生成逻辑在ONNX里被隐式转换了,导致输出坐标偏移,置信度反而成了次要因素。你可以先单独dump一下ONNX的输出tensor,和PyTorch的原始输出逐元素对比,看是整体偏差还是局部异常。如果确认是算子支持问题,建议把模型里一些自定义层改成标准卷积或上采样实现,再重新导出试试。另外,ONNX Runtime对某些op的优化可能默认关闭,可以查下文档开启对应的优化选项。
这问题我熟,八成不是量化,因为opset11/12默认都是FP32,更像是模型里的某些动态shape操作在转换时被
大概率是模型里有些算子在ONNX下被替换或折叠了,特别是YOLO的detect头,试试关掉某些优化再对比中间层输出。
遇到过类似情况,最后是用onnx-simplifier+固定shape解决的,你可以先排除一下输入尺寸变化的影响。
我之前也踩过这个坑,YOLOv5转ONNX最容易翻车的其实是NMS后处理那部分,模型输出本身反而是好的。你可以先单独对比一下转出来的ONNX和原模型在同一个输入上的原始输出张量,看是不是数值上就差了,如果原始输出一致那问题大概率出在后面的解码逻辑上。另外试试把opset固定到11然后加optimize=False,有时候onnx的图优化会动到一些算子的实现细节,虽然看起来是等价的但结果就是会漂。还有个小建议,检查下输入输出的dtype是不是float32,我之前遇到过转出来变成float64的诡异情况,推理速度慢结果还不对。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉得离谱,最后发现是输出层的后处理被拆开了,比如decode和nms得自己补上,光靠ONNX Runtime跑原始输出会差不少。你可以先对比一下ONNX里每个节点的输出和PyTorch中间结果,看到底是哪一层开始飘。另外,试试把模型转成FP16或者用TensorRT跑,有时候精度损失比opset版本影响大得多。对了,检查下torch和onnx的版本,旧版onnx导出有时会静默丢掉一些op。
这问题我之前也踩过坑,先说结论:大概率不是opset或者dynamic_axes的事,这俩只影响图结构和动态shape,跟输出数值偏差关系不大。你提到的置信度整体偏低,我第一反应是预处理和后处理没对齐,特别是YOLOv5的letterbox操作,padding的填充值、图像归一化的scale系数,onnxruntime里如果用了不同版本的cv2或者numpy精度,很容易造成细微偏差,但一般不至于漏检这么严重。
另一个更隐蔽的点是模型里的某些op在ONNX转换时被“优化”了,比如focus层或者SiLU激活在某些opset下会拆成多个基本算子,浮点运算顺序变了,累积误差就出来了。我建议你先用onnxruntime跑一下官方给的yolov5s.onnx,如果官方模型没问题,那问题就锁定在你的转换流程或自定义代码上。
还有,你说试过opset 11和12,但ONNX的算子支持表里有些高级op(比如GridSample)在低版本下可能走fallback实现,精度损失会放大。建议直接上opset 13+,并且用torch.onnx.export的checker=True验证一遍图合法性。
最后想问下,你推理时是不是开了fp16?onnxruntime的GPU版本如果默认开启混合精度,而你的模型没有做QDQ量化,那误差会非常大。可以先强制fp32跑一遍对比,如果正常了再考虑用onnxruntime的量化工具做校准。漏检问题多半是置信度阈值卡得太死,建议把conf阈值调低到0.1看下召回率变化,能帮你快速定位是数值偏移还是真的特征丢失。
我之前也踩过这个坑,置信度掉一截不一定就是量化问题,先排查下预处理和后处理是不是在转ONNX时被改掉了。YOLOv5的anchor和nms逻辑是纯Python写的,ONNX导出时可能没包含进去,导致跟你本地推理的流程不一致。另外试试用onnxruntime的CUDA执行提供程序跑一下,CPU和GPU在算子实现上偶尔会有精度差异。如果还不行,可以检查下输入图像的归一化方式,很多项目就是在这里悄悄变了。
遇到过类似的,建议先别急着怀疑量化,opset版本影响没那么大。你检查下转出来的ONNX图里有没有奇怪的Resize或者Transpose节点,YOLOv5导出时经常因为输入尺寸固定导致某些层被错误折叠。还有,onnxruntime默认的优化级别可能会改动图结构,试试把graph_optimization_level设成ORT_DISABLE_ALL对比下。我上次就是被一个多余的BatchNormalization融合搞崩了精度。
我怀疑你是用了onnx-simplifier之类的工具吧?有时候简化过程会误伤一些算子,比如把Gather换成Slice,精度就差一点。如果没简化过,那大概率是模型里用了SiLU或者Mish这类激活函数,某些ONNX runtime版本对它们的实现精度不够。你可以把每个层的输出导出来对比一下,定位到具体哪一层开始偏差变大,这样比瞎猜
我之前也踩过类似的坑,后来发现多半不是opset的问题,而是模型里的预处理和后处理没跟着一起转进去。YOLOv5的anchor和nms这些逻辑在ONNX里经常被忽略,导致输出分布对不上。你可以先对比一下ONNX的输出tensor和PyTorch的原始输出,看是数值整体偏移还是某些通道异常。另外检查下转的时候有没有把模型切成training模式,batch norm的行为在eval和train下差别很大,这个很容易忽略。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的其实不是量化,而是模型里的上采样和anchor grid生成部分。你试试把onnxruntime的execution mode改成ORT_ENABLE_ALL,有时候默认的CPU优化会改变算子行为。另外先确认下转出的ONNX里有没有包含一些自定义的op,比如focus层,如果没被解析成标准卷积就会导致输出偏差。之前我碰到过类似情况,最后发现是torch版本和onnx的算子映射没对齐,特别是SiLU激活函数在旧版本opset下会被错误替换。还有个笨办法,你可以把ONNX的输出和PyTorch的中间层特征图逐个对比,看到底从哪一层开始分叉,我那次就是这么定位到是上采样模式的问题。另外你提到置信度整体偏低,会不会是输入图像的归一化方式变了?ONNX模型里如果嵌了归一化参数,而你在推理时又手动做了一遍,就会双重缩放。建议你直接用onnxruntime的IO binding接口把numpy数组喂进去,别走默认的预处理管线。最后实在不行就试下onnxsim,有些冗余算子化简后精度反而能恢复。
我之前也遇到过类似情况,排查下来发现多半不是opset的问题,而是YOLOv5导出时把一些后处理逻辑(比如nms和anchors解码)写进了模型里,导致onnxruntime跑的时候精度有偏差。你可以试试用torch.onnx.export时把model.eval()和opset_version=12加上,同时检查一下输入输出的尺度是否对齐,尤其是batch维度的动态设置。另外,如果用的是官方仓库,记得看下导出脚本里有没有autoanchor相关的参数,那个对置信度影响挺大的。你对比过onnxruntime和PyTorch在相同输入下的原始输出特征图吗?如果特征图差异不大,那问题可能出在后处理上。
大概率不是量化问题,先检查下预处理和anchor设置,YOLOv5转ONNX踩过这坑。
先检查下预处理和后处理是不是一致,ONNX这边经常是图片归一化或者letterbox尺寸没对齐导致的偏差。
之前也踩过类似的坑,yolov5转onnx后置信度掉一截大概率不是opset的问题。建议先检查一下预处理和后处理是不是对齐了,尤其是letterbox的padding和缩放参数,onnxruntime里跑的输入尺寸和原pytorch不一致很常见。另外如果模型里有hardswish或者focus层,某些老版本onnx导出会出偏差,升级到最新torch和onnx试试。还可以用onnxruntime的CUDAExecutionProvider对比一下CPU结果,排除一下环境差异。我上次就是卡在BN层折叠上,转之前记得把model.eval()挂上,不然推理统计量会乱。