最近在把一个训练好的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后推理结果和原模型差很多,是量化问题还是图优化没配好?
全部回复
共 108 条我之前也踩过类似的坑,后来发现多半不是量化的问题,而是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处理确实有精度坑,我之前也踩过。