最近在做一个部署项目,把训练好的YOLOv5模型转成ONNX,再用onnxruntime推理。结果发现输出的检测框置信度整体偏低,有些原本能检出来的目标直接漏检了。试了opset 11和12,也试过dynamic_axes,情况没改善。
PyTorch转ONNX后推理结果和原模型差很多,是量化问题还是算子支持问题?
全部回复
共 35 条大概率是预处理没对齐,YOLOv5转ONNX时像素归一化和letterbox的参数得跟原模型一模一样。
先别急着怪量化,这情况大概率是ONNX里某些算子的实现和PyTorch不一致,尤其YOLO的检测头,建议逐层对比下输出。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截,大概率不是opset的问题。你试试把模型里头的anchor grid解码部分一起导出,而不是只导出主干加head,很多自定义逻辑在ONNX里会被图优化改掉。另外检查下输入归一化是不是被重复执行了,比如原模型里已经除以255,转的时候又加了一层。我之前是换了个onnxruntime版本就好了,你可以先排除下环境差异。
遇到过,先检查下预处理和后处理是不是跟原模型一致,特别是归一化和NMS阈值。
我之前也踩过类似的坑,后来发现多半不是opset的问题,而是YOLOv5输出层里有些自定义操作(比如grid生成、anchor解码)在转ONNX时被拆得不太对,导致坐标或置信度计算有偏差。你可以试着把后处理逻辑留在PyTorch里,只导出主干+head的原始输出,再用外部脚本还原,这样对比起来更容易定位。另外量化一般不会让置信度整体掉这么多,更像是数值精度或图优化引起的,建议用onnxruntime的log_severity_level看下有没有warning,或者直接用onnx-simplifier瘦身后再测一轮。
我之前也踩过类似的坑,置信度掉一截大概率不是opset的问题,先试试把模型的eval模式打开再转,BatchNorm层在训练模式下会带running stat偏差。另外YOLOv5里有个merge_factor之类的细节,ONNX导出时某些自定义的NMS后处理会被拆掉,导致输出分布变化,建议你直接对比下onnx和pth的中间层输出,看是哪个head开始漂移的。如果排除了这些,再查量化,但onnxruntime的FP32推理一般不会差这么多,漏检更像是anchor或输出解码逻辑没对齐。
我也踩过类似的坑,而且最后查出来跟量化、算子支持都没啥关系。你试试转ONNX的时候把model.eval()和torch.no_grad()都加上,有时候训练模式和推理模式的batchnorm统计量不一样,会导致输出差异。另外YOLOv5的检测头里有些自定义的nms或者锚框解码逻辑,如果转的时候没完全拆干净,onnxruntime跑出来的原始输出其实是对的,但后处理如果还沿用PyTorch里那套坐标变换,置信度可能会被错误缩放。我之前就是漏了这一步,导致框的置信度普遍掉了0.1到0.2。
还有个容易忽略的点是输入图像的归一化方式。PyTorch里如果用的是RGB顺序加上除以255,但onnxruntime那边加载图片时用了BGR或者忘了归一化,那输出差异会非常大,而且不是均匀偏移,是那种某些目标直接消失的效果。你可以先用同一张图,在PyTorch前向和onnxruntime里都打印出模型最后一层卷积的原始输出,对比一下数值差异有多大。如果原始输出基本一致,那问题就在后处理;如果原始输出就差很多,再考虑是不是resize时插值方式不同,或者opset版本对某些层(比如Focus)的翻译有bug。
另外你提到dynamic_axes,如果只是batch维动态,那影响不大,但如果是wh动态且某些卷积的padding是动态计算的,ONNX导出时可能固化了一个固定尺寸,导致特征图错位。建议你先固定输入尺寸试一次,排除这个变量。我最后是把导出的ONNX用onnx-simplifier过了一遍,再对比原始输出,发现有个Slice的索引顺序被搞反了,修正之后才完全一致。你可以先用onnxruntime的IO Binding或者直接对比输出张量的每个通道,看看是不是某个特定层开始数值发散,那样定位会快很多。
我之前也踩过这个坑,建议先别急着怀疑量化或算子问题,YOLOv5转ONNX最常见的是输出端的anchor处理逻辑没对齐。你对比一下原模型和onnxruntime的输出张量形状,如果raw predictions一致但后处理差异大,那大概率是解码部分写错了。另外检查下输入归一化方式,比如RGB/BGR顺序和除以255的操作是否在转换时被隐式改变了,这个很容易导致置信度偏移。如果确认这些都无误,再考虑用onnxruntime的CUDA执行提供程序试试,CPU和GPU的浮点精度差异有时候也会放大误差。
我之前也踩过类似的坑,置信度飘低不一定是量化或者算子的锅。你试试把模型里decode部分(比如anchor_grid、grid)留着别转,只转前馈部分,输出原始特征图再自己后处理,很多时候问题就出在导出时把nms或decode细节搞变形了。另外,检查下onnxruntime用的执行提供程序,CPU和CUDA的精度表现有时候差挺多的,尤其float16没开对的话。如果方便,可以对比下onnx输出的中间tensor和pytorch的差异,定位到具体哪一层开始漂移,比瞎调opset高效。
YOLOv5转ONNX掉点这事我踩过好几次坑,先说结论:大概率不是量化问题,因为onnxruntime默认跑的是FP32,你根本没重量化,置信度整体偏低更像是后处理或者anchor解码那一步对不上。最容易被忽略的是YOLOv5导出时如果没加--grid或者用了旧版export.py,ONNX里输出的还是未解码的原始预测,而你在PyTorch里对比的可能是已经过了NMS的结果,那数值肯定对不上。另外opset 11和12对某些算子确实有差异,比如Resize在11之后改了坐标变换逻辑,如果你模型里有上采样,输出会有细微偏移,累积下来就影响小目标检测。我建议你先把ONNX和PyTorch的raw output(NMS之前)逐层对比,用onnxruntime的profiling看哪一层开始偏差超过1e-3,定位到具体算子再想办法替换。还有个偏方是导出时把--dynamic去掉,固定batch和输入尺寸,有时候dynamic_axes会触发不同的kernel实现,精度也会飘。如果确认是Resize的问题,可以手动改成opset 10导出,或者用onnx-simplifier跑一遍再推理,我这边YOLOv5s这么搞完mAP基本能恢复到掉点前的水平。
YOLOv5转ONNX掉点这事我也踩过,大概率不是量化问题,因为onnxruntime默认跑的是fp32,根本没量化。更可能是后处理对不上,比如anchor解码或者NMS那块,原版是torch实现,导出后有些算子会走不同路径。建议先拿同一张图对比ONNX和原模型在sigmoid之前的raw输出,看差在哪儿,一般能定位到具体层。另外opset别乱升,YOLOv5官方导出脚本用哪个就跟着用哪个。
置信度整体偏低但框的位置还算靠谱的话,大概率不是算子不支持,而是前后处理对不上。YOLOv5导出ONNX时如果没把sigmoid和decode写进图里,onnxruntime出来的就是原始logits,得自己补上激活和anchor解码,不然分数肯定偏低。另外预处理也要对齐,letterbox的padding值、归一化、BGR还是RGB,差一点都会掉点。建议拿同一张图分别跑PyTorch和ONNX,把中间特征图dump出来对比,先定位是预处理、后处理还是模型本身的问题。
YOLOv5转ONNX掉点大概率不是量化的问题,你都没提到做量化,那基本就是导出环节的锅。先确认下预处理对不对,letterbox的padding值、归一化方式在ONNX里是不是和原版一致,这块最容易踩坑。另外看看输出层是不是把sigmoid或者decode那部分漏掉了,opset 11和12对某些算子处理确实有差异。建议用onnxruntime跑一张和PyTorch完全相同的输入图,逐层对比中间输出,定位到哪一层开始偏。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉,最后发现是预处理和后处理对不上。你检查一下onnx推理时的letterbox、归一化和NMS阈值是不是跟原版完全一致,差一点结果就偏。另外opset版本其实影响不大,重点看导出时有没有把Focus或者SiLU写成不支持的算子。可以先用onnxruntime跟torch对同一张图逐层比对输出,定位到哪一层开始偏。
你这情况大概率不是量化问题,因为onnxruntime默认跑的是fp32,除非你主动做了量化。更可能是后处理这块出了岔子,YOLOv5转ONNX时anchor解码、sigmoid这些如果对不上,置信度就会整体偏移。建议先拿同一张图分别跑原模型和ONNX,把raw output逐层对比一下,看是从哪一层开始数值对不上的。另外opset 11和12差别不大,可以试试opset 13或17,有些算子实现改了会好很多。