最近在把一个训练好的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 条这问题我太熟了,之前跑CenterNet也踩过一模一样的坑。你提到focus层和slice,基本就是它俩在捣鬼,ONNX对某些切片操作的实现会引入额外的内存重排,数值上差个1e-5,但经过后面的卷积累积放大,置信度掉0.1真不夸张。建议你先别急着调量化,把onnxruntime的execution_mode设成ORIGINAL,再加个graph_optimization_level=ORT_ENABLE_BASIC试试,有时候默认的ALL优化会把一些算子融合得变了味。另外你检查下export时有没有把model.eval()和torch.no_grad()包严实,YOLOv5的detect头里有个grid生成逻辑,如果用到了shape的int计算,ONNX导出时很容易变成动态shape的隐式依赖,这个也会导致输出抖动。我之前是手动把focus层改成了普通卷积加步进切片,再用onnxsimplifier跑一遍,差值就降到1e-6以下了,你可以先跑个onnxruntime的IO Binding看看是不是输入内存对齐的问题。再不行就逐层对比中间tensor,写个脚本把每个节点的输出都dump出来和pytorch对齐,定位到具体哪一层开始发散,比瞎猜快得多。
我之前也踩过这个坑,YOLOv5转ONNX后置信度掉一截,试了一圈发现大概率不是量化问题,而是focus层转出来以后用了大量gather和slice,ONNXRuntime对这类算子的实现精度确实有细微差别。你可以试着把focus层手动改成普通卷积加reshape,或者直接升级到YOLOv8那种不用focus的结构。另外opset开到13或14试试,12版本对某些算子支持得不够好。还有个小技巧,导出时加一句torch.onnx.export的do_constant_folding=False,有时候能避开一些优化导致的数值抖动,你可以对比下输出差异具体出现在哪一层。
先用onnxruntime的CPUExecutionProvider跑一遍pytorch原模型对比下,排除算子实现差异再说。
这问题我踩过类似的坑,YOLOv5转ONNX最容易出偏差的地方其实是focus层切片后的拼接顺序,ONNX的slice索引跟PyTorch原实现有时候会有隐式差异,建议你先把focus和backbone前几层单独导出来做逐层输出对比。另外opset12对某些算子的实现比较老,试试升到15或16,有时候同样的算子在新opset下精度表现会好很多。还有你推理时onnxruntime的execution_mode要明确设成OR T_SEQUENTIAL,并行模式在某些CPU上会引入额外误差。
我之前也踩过类似的坑,YOLOv5转ONNX最容易翻车的就是focus层和slice的组合,因为PyTorch的im2col实现和ONNX的slice+concat转换后,如果opset选得太低,某些算子的行为会有细微偏差,甚至包括一些常量折叠的差异。你试试把opset升到13或者14,然后导出时加上dynamic_axes,同时把模型的eval模式、no_grad环境都确认好,有时候是BN层统计值被当成可训练参数导出了。另外,建议先别用onnxruntime直接跑,用onnx的shape_inference和graph_simplifier(比如onnx-simplifier)过一遍,很多情况下是冗余节点导致的数值漂移,而不是量化问题。还有一个排查技巧,你可以把ONNX的输出层加上一个恒等映射,对比中间特征图的逐通道均值,这样能定位到是哪一层开始产生偏差。如果只是置信度低,也可以检查一下NMS的阈值是不是被ONNX的opset默认值覆盖了,YOLOv5的导出脚本里有时候会把conf_thres写死。我上次就是花了半天发现是导出时把原始模型的half精度转成了float32,但某些自定义op没有对应实现,导致输出值被截断。
先检查下模型里有没有用Mish或者SiLU,ONNX对这类激活函数支持不太好,换LeakyReLU试试。
opset12对YOLOv5的focus层支持有坑,建议升到opset17再对比下数值差异。
置信度差这么多大概率是预处理细节没对齐,特别是letterbox的padding值,建议用onnxruntime直接喂numpy对比下中间层输出。
我之前也遇到过类似的情况,最后定位到是focus切片在ONNX里被拆成了一堆gather和concat,数值上确实有微小偏差,但0.1-0.2的置信度差距感觉不全是这个引起的。建议你先用onnxruntime的CPUExecutionProvider跑一下,再对比一下是不是输出节点的顺序或者坐标解码方式在导出时被改了,YOLOv5的anchor处理经常在导出时被简化。另外可以试试开graph_optimization_level,有时候默认的优化反而会引入误差,特别是对动态shape。如果还查不出,建议直接导出时把模型内部所有op都用onnx的opset13试下,新版算子对slice的支持更完善。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是量化,而是Focus层在转换时被拆成多个slice和concat,onnxruntime对这类操作的优化和PyTorch原图执行顺序不完全一样,数值误差会累积。你可以先用onnxruntime的graph_optimization_level设成ORT_DISABLE_ALL跑一遍,如果置信度回来了就说明是图优化搞的鬼。另外建议把opset升到13或14,老版本对某些算子支持不友好。如果还不行,就在导出前把模型切成几段,分别对比中间tensor的max abs diff,很快能定位到是哪个节点开始漂移。
先别急着怀疑量化,你opset12默认fp32导出的话,数值偏差大概率不是量化引起的。我之前遇到过类似问题,最后定位到是yolo的anchor网格生成在onnx里被拆成多个小算子,浮点累加顺序跟pytorch不一样,导致框坐标有微小漂移,置信度也跟着变。你可以把onnx的每个输出节点单独dump出来,跟pytorch逐层对比,重点看focus和sppf后面那几层,误差一般从那儿开始积累的。另外试试opset11,有时候新版本反而会触发一些奇怪的图优化。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是量化,而是focus层那个slice+concat的组合,在opset 12下有些算子会隐式转换成不同的实现,导致数值上有个很小的偏移,但经过后面几层卷积放大后置信度就掉了。你可以先试试把opset升到13或者14,很多旧版的slice行为在新版里修正了,我之前就这么解决过。另外,如果还不行,建议用onnxruntime的symbolic shape inference跑一遍,看看是不是某些动态维度被错误推断成了固定值,这会影响后续的算子融合。至于keep_initializers_as_inputs,那个一般只影响文件大小和加载速度,基本不碰数值,不用太纠结。还有一个思路是逐层对比输出,用onnxruntime的IO Binding把中间tensor导出来,和PyTorch的hook结果做diff,能精确定位是哪一层开始偏的,我当时就是这么找到问题点的。你提到小目标漏检,也可能和NMS的置信度阈值在导出时被写死有关,检查下模型里是不是带了后处理,如果带了,建议导出前把它去掉,用外部脚本做NMS。
我之前也踩过类似的坑,不过不是YOLOv5,是跑一个分割模型的时候发现输出概率整体偏移。你排查的方向我觉得没问题,但可以试试把onnxruntime的执行模式改成CPU EP的特定优化级别,有时候默认的图优化会折叠一些op,反而把数值精度带偏了。另外,你确认过ONNX模型和PyTorch模型在同一个中间层输出上的最大绝对误差吗?比如把focus层或者第一个卷积的输出拉出来对比,如果差异在1e-4以上,那基本就是算子映射的问题,而不是量化。我遇到过slice+concat的组合被onnx重写后浮点累加顺序变了,导致0.1级别的置信度漂移,这种只能手动改onnx图,或者换opset版本试试。如果不想改图,可以试试把torch.onnx.export里的operator_export_type设成ONNX_ATEN_FALLBACK,虽然慢但有时能保留原算子。还有个小技巧,用onnxruntime的IOBinding跑一遍原图,同时用pytorch跑同一张,把输出tensor直接逐元素对比,别只看框和置信度,定位到具体是哪一层开始分叉。你试过导出时把do_constant_folding关掉吗?有次我就是因为这个,折叠后数值变了。
我上次也栽在focus层上,试试把opset升到13或者17,数值误差能小不少。
我之前也踩过类似的坑,YOLOv5转ONNX重点不在opset,而是那个focus层,转出来后数值差一点点就会放大到框置信度上。建议你先用onnxruntime和torch逐层对比中间tensor,写个脚本把每层输出都dump出来,很快就能定位到是哪一步开始漂移。另外检查下是不是用了半精度或者onnxruntime的优化级别太高,有些图优化会重排算子导致数值变化,试试把session options里的graph optimization level设为OR T_DISABLE_ALL对比下。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的其实不是量化,是模型里那些自定义的focus层和slice组合在onnx里被拆得太碎,导致某些算子的数值精度在CPU上跟GPU对不上。你opset12的话,建议先把opset拉到17试试,新版onnxruntime对slice和gather这类算子的实现更稳,我上次就是改这个直接解决了置信度偏移。另外你确认一下onnxruntime用的是CPU还是CUDA的provider,CPU上有些算子会走不同的kernel实现,数值差异会被放大。还有个笨办法,把原模型的每层输出导出来跟onnx逐层对比,写个脚本用onnxruntime跑中间节点,找到第一个偏差超过1e-5的层,基本就是问题所在。最后提醒下,如果模型里有BatchNorm在训练和推理模式下行为不同,记得export前一定设成eval模式,这个最容易忽略但影响特别大。你那个小目标漏检,我猜大概率是final的sigmoid输出在转换时被融合进了前一层,导致数值范围被压缩了。
我上次转YOLOv5也碰到过类似情况,后来发现是pytorch的batch_norm在eval和train模式下统计量用的不一样,导出前忘了切model.eval(),导致ONNX里固化的是训练时的running stats,推理自然就偏了。你可以先检查下这个,另外focus层在ONNX里会被拆成多个slice+concat,数值误差确实存在但一般不会掉那么多,建议把中间层输出也dump出来对比一下,定位到具体哪一层开始漂移。如果确认是算子精度问题,试试opset 11或者手动把focus改成等价的conv,有时能绕过。
先跑一下onnx和torch的逐层输出对比,大概率是focus切片转出来精度丢了,别急着调量化。
先别急着怀疑量化,opset12下focus层转换容易出精度坑,试试把opset升到13或14。
遇到过一模一样的坑,YOLOv5转ONNX掉点基本不是量化问题,因为你这还是FP32推理,跟量化压根没关系。我上次排查到最后发现是focus层切片加拼接的操作,在opset12下会被拆成好几个gather和concat,每个算子都有微小浮点误差,叠加起来置信度就偏移了。你可以先用onnxruntime的CPU EP跑一下,再用TensorRT或者OpenVINO试试,如果不同后端结果差异大,那基本就是图优化或者算子实现的问题,不是模型本身的问题。另外检查下原模型里有没有用到SiLU激活,有些旧版onnxruntime对SiLU的数学近似跟PyTorch原版不完全一致,这也会导致输出偏差。建议你直接把ONNX里focus层用Conv替代掉,或者升级到opset13以上,顺便把torch.onnx.export里的do_constant_folding打开,这个对消除冗余节点很有帮助,但注意别把它跟量化混为一谈。最后可以逐层对比输出,在PyTorch里手动跑一遍fpn各层输出,跟onnxruntime的中间tensor做diff,这样能精确定位是哪个op开始飘的。
先试试把opset升到13以上,YOLOv5的focus层在低版本opset下确实容易出精度问题,我之前就是被这个坑的。