最近在把训练好的YOLOv5模型转成ONNX部署到服务端,用torch.onnx.export导出时opset设的是12,输入输出都检查了shape没问题。但用onnxruntime推理时,检测框的置信度普遍低了0.2左右,有些目标直接漏检了。我试过dynamic_axes,也试过把模型设为eval模式,结果都一样。网上查了说可能是某些算子(比如Focus、SiLU)在ONNX里实现不同,或者需要加NMS插件。但我对比过onnxruntime和PyTorch的输出张量,数值确实有差异,不是后处理的问题。有没有人遇到过类似情况?是精度损失不可避免,还是我漏了什么设置?求指点,搞了两天了有点崩溃。
PyTorch转ONNX后推理结果和原模型差好多,是量化问题还是算子不支持?
全部回复
共 10 条我之前调YOLOv5转ONNX也卡过这个问题,最后定位到是SiLU激活函数在opset 12下的实现差异,ONNX里Swish的梯度或者近似计算和PyTorch原版不完全一致,尤其是深层次特征图上误差会累积。你可以试试把opset升到13或者更高,新版对SiLU这类激活函数的支持更完整,我升到13之后输出张量的最大误差从0.2降到了1e-4级别。另外,Focus层在ONNX里会被拆成多个slice和concat操作,如果模型是动态shape,这些算子组合可能产生边界对齐问题,建议先用固定shape导出对比一次,排除这个变量。还有个坑是BN层折叠,PyTorch推理时BN是训练参数直接用的,但转换时如果没正确融合,ONNX里会多出一些额外计算,虽然理论上等价,但浮点运算顺序不同也能造成微小漂移。你对比输出张量时,最好挑几个不同尺度的特征图分别看,别只看最后检测头的输出,有时候误差在backbone就开始了。如果试完这些还是差很多,可以检查一下onnxruntime的优化级别,默认的ORT_ENABLE_ALL有时候会做图变换,改成ORT_DISABLE_ALL试试。最后实在不行,可以考虑导出fp16试试,有时候反而因为舍入方式接近而误差变小,不过这个有点玄学,但成本低值得一试。
遇到过类似的情况,opset12对YOLOv5的Focus层支持确实不太好,那个切片+concat的操作在ONNX里容易被拆成多个节点,导致数值传播有微小偏差。不过你说置信度普遍低0.2,这个幅度有点大,不像纯算子误差,我觉得可以先确认下导出时是不是把模型里的某些预处理或者归一化层也一起带进去了,比如像素缩放或者减均值那部分,有时候这些操作在PyTorch里是隐式的,但转成ONNX后会被单独计算,精度就变了。
另一个怀疑点是你有没有试过用onnxruntime的更高版本,比如1.16以上,它对YOLOv5的SiLU激活函数支持已经改进了不少,老版本会有浮点运算顺序不同的问题。另外,你可以导出时把opset调到17试试,有些算子在高版本下有更精确的实现,特别是SiLU,它本身不是标准ONNX算子,导出时会用多个基础算子组合模拟,不同opset的展开方式不一样,误差会积累。
如果你确认输出张量本身有差异,那大概率不是量化问题,因为你还没做INT8量化,纯FP32精度损失一般不会到0.2这么大。我建议你直接对比下ONNX模型里每个中间节点的输出,找出第一个数值分叉的地方,用torch.onnx.export的verbose=True参数或者用netron看下计算图,有时候是某个reshape或者transpose的排列顺序变了,导致后续计算方式不同。实在不行可以换用onnxsim简化下模型,再试试看,这个工具能消除一些冗余节点,有时候能顺带解决精度问题。
我之前部署yolo系列也踩过这个坑,opset12确实容易在SiLU和Focus上出问题,建议先把opset拉到15以上试试,新版onnxruntime对这两个算子的支持会好很多。另外你对比输出张量差异的时候,最好检查一下是不是某些层的计算图被优化掉了,比如BatchNormalization融合导致的数值漂移,这个在转模型时挺隐蔽的。如果改成opset17还不行,可以试试用onnx-simplifier处理一遍,有时候能解决不少精度损失。最后想确认下你导出时有没有把训练时的自定义模块(比如focus)改成等效卷积操作?
试试把opset调到11或13对比下,Focus算子拆成卷积和slice能解决大部分误差。
大概率是Focus和SiLU的onnx实现跟你torch版本对不上,试试把opset提到13或17,再不行就手动拆算子重写导出。
我上次也是这问题,最后用onnxsim精简一遍再转,数值差异就小多了,先排除环境因素再考虑精度。
先查下SiLU和Focus的onnx实现,我之前用opset11也遇到过类似偏差,换成13就好了。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的就是SiLU和Focus的融合方式,你试试把opset升到17,然后用onnx-simplifier把图简化一遍,很多情况下数值差异就是这些隐式转换导致的。另外检查下导出时有没有把model.eval()和torch.no_grad()都包进去,我遇到过因为BN层统计量没冻结导致的类似偏差。如果还不行,可以对比下中间层输出,定位是哪个节点开始漂移的,我上次就是发现是Slice算子在不同runtime下的边界处理不一样。
我之前也踩过这个坑,YOLOv5的Focus层在opset 12里导出后确实会有数值偏差,建议换成opset 11或者直接用官方export.py脚本跑一遍试试。另外你手动设eval模式还不够,得确认BN层有没有真的fold进去,有时候导出时偷懒没融合就会差一截。置信度掉0.2感觉不像是单纯的精度损失,更像是某几层权重没对齐,可以逐层dump输出对比一下。实在不行试试把SiLU换成LeakyReLU再导,虽然掉点但至少能定位问题出在哪。
YOLOv5的Focus层确实是个坑,它里面用了slice和concat的组合,转ONNX后某些版本会走不同的实现路径,数值偏差就出来了。你可以试试把Focus换成等效的Conv2d(6x6 stride2那种),Silu的话opset12应该支持了但最好确认下是不是被拆成了Sigmoid+Mul。另外别光看输出张量,中间层也dump出来对比一下,定位到具体是哪一层开始飘的。我之前也是类似情况,最后发现是 anchor grid 的生成方式在onnx里精度不一致导致的。
YOLOv5转ONNX有数值偏差这事儿太常见了,我当初也被坑过。你对比过PyTorch和onnxruntime的中间层输出没?光看最终张量差异还不够,得逐层定位是哪个算子开始飘的。Focus层在opset 12里其实是用slice拼出来的,跟原版实现有细微差别;SiLU如果没被正确识别成x*sigmoid(x)的组合,精度损失会累积得比较明显。还有个容易忽略的点:onnxruntime默认开graph optimization,有些fusion操作会改变计算顺序,你可以试试把优化等级设成ORT_DISABLE_ALL对比一下。置信度掉0.2这个幅度,我感觉不像是单纯精度截断造成的,更像是某个大算子走了fallback路径。另外你确认下导出时有没有加do_constant_folding=True?这个跟BN融合有关,关掉的话结果会差挺多。实在不行就上onnxsim做一遍简化再跑,有时候能修掉一些奇怪的算子展开问题。