最近在把一个训练好的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 条我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实是focus层,slice+concat在opset12下可能被拆成多个小算子,浮点累加顺序变了就会导致置信度漂移。你可以先试着把opset调到13或14,顺便把模型里focus层手动改成卷积等价替换,能规避不少算子映射问题。另外建议用onnxruntime的graph_optimization_level设成ORT_ENABLE_ALL对比一下,有时候图优化反而会引入精度损失,特别是CPU上做算子融合时。如果还不行,就逐层打印中间tensor的数值,定位到具体是哪个节点开始偏差变大的。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实是focus层在转的时候被拆成slice+concat,数值误差会累积。建议你先用onnxruntime的IO Binding或者直接对比中间tensor,把每个节点的输出都导出来跟PyTorch对一下,定位到具体哪一层开始漂移。另外可以试试opset设成11或者13,有时候12对某些算子的实现有差异。还有个小细节,确认下是不是转的时候模型默认带了训练时的dropout或者BN的running stats没冻结,这个也会导致置信度整体偏低。
先别急着怪量化,opset12下YOLOv5的focus切片容易有精度坑,建议先用onnxruntime的CPUExecutionProvider配合graph_optimization_level=ORT_DISABLE_ALL对比下。
之前做检测模型转换也踩过这个坑,置信度掉一截大概率不是量化问题,毕竟你都没开动态量化。建议先单独把focus层和slice的输入输出拉出来对一下,onnxruntime里用onnxruntime.transformers.optimizer或者直接比较中间tensor的余弦相似度,能快速定位是哪一步开始分叉的。另外试试opset 11,有些老算子映射在12上反而会有精度损失,我这边换成11后差异就明显缩小了。
遇到过类似的坑,当时也是YOLOv5转ONNX,置信度掉了大概0.15,排查了很久最后发现是focus层在ONNX里的实现方式问题。PyTorch里focus是切片加concat,但ONNX导出时可能被拆成多个slice和gather节点,浮点运算顺序变了,累积误差就出来了。你可以先用onnxruntime的CPUExecutionProvider跑一下,对比每一层输出的中间tensor,用onnxruntime的IOBinding或者自定义op来逐层dump,看是从哪一层开始偏差变大的。另外,建议试试opset 11,有些老版本算子对slice的边界处理更接近PyTorch,opset 12反而会引入一些优化导致数值变化。还有个思路是直接换用yolov5官方给的export.py,它里面有很多针对ONNX的优化,比如把focus层替换成卷积,这样能避免slice的精度问题。如果还是不行,可以考虑用onnx-simplifier过一遍图,有时候能消除一些冗余节点。最后,确认一下你推理时用的letterbox填充值是不是0.5?ONNX模型里如果缓存了固定的填充参数,而你在测试时用了不同的值,也会导致置信度偏移。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是图优化,而是focus层的slice+concat在onnxruntime里被拆成多个小op后,浮点累加顺序变了,误差会累积。你可以试试把opset升到13或15,有些版本对slice的优化更接近PyTorch原逻辑。另外,先别急着怀疑量化,你是直接导出FP32吧?可以写个脚本逐层对比onnx和pytorch的中间tensor,用onnxruntime的IOBinding或者pytorch的hook都能定位到具体哪一层开始漂移。我那次最后发现是batch norm的epsilon在转换时被写死成了1e-5,而原模型用的是1e-3,改过来就正常了。
先跑一下onnx和pytorch逐层输出对比,大概率是focus切片或上采样对齐的精度问题,跟量化关系不大。
先检查下export时training=False和模型.eval()有没有都设上,之前我漏了eval结果跟你一模一样。
我之前也踩过类似的坑,YOLOv5转ONNX后置信度掉一截,八成不是量化的问题,因为opset12默认还是FP32。你试试把torch.onnx.export里那个opset_version换到13或14,有时候旧版本对focus层的slice+concat组合支持得不够好,会额外插入一些cast或者reshape节点导致数值漂移。另外可以开onnxruntime的graph optimization级别调到all,再跑一遍对比下,如果结果有变化那就是图优化动了某些算子。实在不行就逐层对比输出,写个脚本把每个节点的中间结果导出来和PyTorch对一下,定位到具体是哪一层开始差的。
这问题我熟,之前转faster rcnn也碰到过类似情况,置信度整体掉一截。建议先别急着怀疑量化,你opset 12的配置下,focus层很容易被拆成多个slice和concat,数值误差会累积,试试把opset升到15以上,或者手动改模型把focus替换成等效卷积,误差能小很多。另外也可以导出时加一句torch.onnx.export(model, dummy_input, "model.onnx", dynamo=True),新版torch的dynamo导出对算子融合更友好。如果还不行,就写个脚本逐层对比onnx和pytorch的中间输出,定位到具体哪个节点开始漂移,基本就是那个算子的实现差异了。
先看看是不是前后处理不一致,YOLOv5的anchor和letterbox在ONNX里很容易踩坑,我上次就是这问题。
先对比下onnx和torch逐层输出,八成是focus切片转出来精度丢了,试试opset13。
我之前也踩过这个坑,YOLOv5转ONNX置信度掉0.1基本不是量化的问题,你opset12默认就是FP32,重点怀疑focus层用slice+concat实现后,在onnxruntime里某些算子对边界处理的数值精度和PyTorch原生图不一致。可以试下把模型导出时的optimize=False关掉,或者直接用onnx-simplifier处理一遍,看输出差异能不能缩小。另外建议你按输出特征图逐层对比中间tensor,比如先固定输入,把focus和第一个C3的输出都dump出来,很快就能定位到是哪一层开始漂的,我上次就是这么找到是Slice的step实现导致的误差累积。
我之前也踩过类似的坑,最后定位是ONNX里某些算子的实现跟PyTorch不完全一致,尤其是focus层拆成slice和concat之后,浮点误差会被放大。你可以试试先用onnxruntime的CPUExecutionProvider跑一遍,然后对比每一层输出的最大绝对误差,用脚本逐层打印出来,基本能锁定问题出在哪。另外opset12对YOLOv5来说确实有点老,建议升到15以上,有些图优化和算子融合会主动触发,置信度差异可能就消失了。还有个小技巧,导出时把model.eval()和torch.no_grad()都加上,有时候training模式会残留BN的统计量差异。
这问题八成是focus层切片转ONNX时精度丢了,试试把opset调到13或14,再不行就导出前关掉amp。
我之前也踩过类似的坑,YOLOv5转ONNX最容易翻车的地方其实不在导出参数,而是focus层和后续的slice在onnxruntime里被拆成了多个小算子,浮点累加顺序变了,误差就会累积。你试过把opset调到13或者更高吗?有些版本对slice和concat的融合处理更干净。另外,你对比过中间feature map的差异吗?我当初是写了个脚本,把PyTorch和ONNX每一层的输出都导出来做余弦相似度,最后定位到是anchor grid的生成方式在ONNX里被重排了,导致偏移计算有细微差别。置信度低0.1-0.2这个幅度,不太像单纯的量化误差,更像是某个关键节点数值偏移被放大。还有个偏门思路,试试用onnxsimplifier简化一下图,有时候冗余的identity节点会干扰runtime的优化。如果还不行,建议直接检查一下导出时training参数有没有设成False,这个会影响batch norm和dropout的行为,很多人会漏掉。
这个置信度整体掉0.1-0.2我第一反应不是量化,因为ONNX默认是FP32导出,你opset12也没开动态量化,不像精度损失。更可能是YOLOv5的focus层在ONNX里被拆成多个slice+concat,某些版本对这类操作的数值处理确实有微小偏差,累积到输出就放大了。建议你先用onnxruntime的CPUExecutionProvider跑一遍,再对比torch直接推理时每层输出的最大值和均值,锁定是哪个节点开始漂移,我之前排查过类似问题,最后是手动改了一个slice的indices才解决。另外确认下onnxruntime版本,老版本对某些op的kernel实现精度确实差一些。
我倒是觉得先别急着怀疑图优化,你试过把opset升到13或14吗?有些算子在不同opset下的数学等价性并不严格,特别是YOLOv5里的SiLU和focus层。我之前遇到过类似情况,最后发现是模型里用了inplace操作,导出时状态没处理好,导致BN层的running_mean和running_var对不上。你可以试试在export前把模型设为eval模式,并且把inplace都改成非inplace,再对比下输出特征图的余弦相似度,能更快定位是哪些层在“捣鬼”。
也遇到过类似情况,当时排查到最后是yolov5里focus层转onnx时slice+concat的顺序问题,数值误差会被后面卷积放大。你试试把opset调到13或者14,顺便用onnxruntime的symbolic_shape_infer跑一遍看有没有warning。另外置信度差0.1-0.2不像是量化,更像某个op实现差异,可以先导出fp32对比,排除量化嫌疑。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的地方其实不是opset,而是它那个focus层在转的时候会被拆成多个slice和concat,每个算子都可能有极小的浮点误差,叠起来置信度就会掉。你既然预处理和dynamic_axes都查过了,我建议先别急着怀疑量化,因为你现在还没开动态量化吧?纯FP32推理差这么多,大概率是图优化导致的算子融合问题。可以试试在onnxruntime里把优化级别调低,比如用ORT_ENABLE_ALL或者直接设session_options.graph_optimization_level = ORT_DISABLE_ALL,看下是不是某些融合规则把模型搞坏了。另外,你导出的时候有没有把model.eval()和torch.no_grad()都加上?有时候BN层统计量没固定也会导致输出漂移。还有个笨办法,把ONNX里每个节点的输出导出来跟PyTorch逐层比对,用onnxruntime的IOBinding或者写个小脚本,定位到第一个出现明显差异的节点,基本就能锁定是哪个算子的问题了。你试试看,如果还不行,可以试试用torch.onnx.export的opset_version=11,YOLOv5对opset12的某些算子支持反而没11稳。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是opset,而是focus层里的切片加concat操作,在onnxruntime里会被拆成多个小算子,数值精度确实会有细微差异,但0.1-0.2的置信度差距感觉有点大了,不太像纯浮点误差能解释的。你试着在导出时把opset提到13或者14看看,有些算子在新版本里实现方式不同,onnxruntime对高版本opset的支持反而更完整。另外你确认过onnx模型输出的是解码后的框还是原始预测吗?我遇到过导出时把后处理(比如nms和anchors解码)意外包含进去的情况,导致输出分布完全变样,但你说的置信度系统性偏低,更像是输入张量布局出了问题,比如HWC和CHW顺序反了,或者归一化时除以255的系数被重复应用了。建议你用onnxruntime的调试工具,把同一个输入喂给PyTorch和ONNX,逐层打印中间tensor对比,定位到具体哪个节点开始分叉,比瞎猜快很多。还有个思路是检查一下yolov5里用的nn.SiLU,某些旧版onnx导出会把它映射成精度较低的近似实现,手动替换成hard_swish可能更稳。最后实在不行,可以试试用onnxsim简化图结构,有时候冗余的identity节点也会干扰推理优化,导致数值漂移。