最近在把一个训练好的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 条先检查下onnxruntime的execution_mode设成ORT_PARALLEL试试,可能跟opset12的算子实现有关。另外建议dump每层输出对比,focus层切片最容易出这种偏移。
我之前也踩过类似的坑,置信度掉这么多大概率不是opset的问题,建议先不用onnxruntime,改用onnxruntime的CPUExecutionProvider和PyTorch都跑同一张图,把中间层的输出导出来逐层对比,定位是哪个算子开始漂移的。另外YOLOv5的focus层在ONNX里会被拆成多个slice+concat,这种低精度操作在CPU上确实容易有微小误差,但0.1-0.2的差距感觉更可能是某些OP被替换成了低精度实现,试试在导出时加opset_version=11并显式指定operator_export_type,或者直接检查一下有没有触发自动混合精度。我后来是用onnx-simplifier处理一次,再把模型转成float16测,发现误差反而小了,你可以试试看。
先别急着怀疑量化,opset12下focus转出来容易有精度坑,试试把slice换成卷积重写。
之前跑过类似的迁移,YOLOv5的focus层在opset12下确实容易出问题,切片加拼接的图优化经常被拆得七零八落。建议你先用onnxruntime的graph优化开关单独关掉试试,或者把opset升到13以上,有些算子实现会更稳定。另外置信度整体低0.1-0.2更像输出后处理没对齐,比如原模型里有没有内置NMS或者对输出做了sigmoid,导出时这些层被剥掉了,你得在推理代码里补上。
我之前也踩过类似的坑,最后排查下来发现不是量化的问题,而是YOLOv5里的focus层在ONNX转换时会自动展开成多个slice和concat,这玩意儿在CPU上的onnxruntime里对数值精度确实有影响,尤其是当输入分辨率比较大的时候,误差会被放大。你opset设的12其实够用了,但建议先试一下把模型导出成FP32并在onnxruntime里用graph_optimization_level=ORT_ENABLE_ALL跑一遍,排除图优化把某些节点折叠成低精度算子的嫌疑,很多时候默认优化级别反而会引入细微差异。另外置信度整体低0.1-0.2这个幅度,我怀疑是后处理里对anchor的decode方式在导出时被改写了,比如grid坐标的生成逻辑如果用了linspace或arange,在ONNX里可能会被展开成不同的浮点计算顺序,导致最终的box置信度有偏移。你可以先写个脚本,把PyTorch和ONNX在同一个输入上的中间特征图逐层打印出来做对比,锁定第一个出现明显差异的节点,这样比盲目调参快得多。小目标漏检大概率是NMS的阈值在ONNX runtime里用了不同的实现,建议把NMS也导出成模型的一部分,或者干脆在外部用numpy重写一个和你PyTorch里一模一样的后处理逻辑,这样能排除很多不确定性。还有个偏门但有效的方法,把torch.onnx.export里的do_constant_folding=True关掉试试,有时候常量折叠会把一些动态shape相关的计算搞出精度问题,代价是模型大一点但数值更稳。如果还是没头绪,可以试试用opset 11对比一下,虽然老但某些算子的实现反而更保守,我之前有个分割模型就是这么解决的。
先检查下输入输出张量的layout和数值分布,YOLOv5的focus用切片重排很容易在ONNX里被优化成不同计算路径。
建议先单独导出模型对比逐层输出,重点查focus切片和上采样,八成是算子映射差异导致的。
大概率是focus切片转ONNX时精度丢了,试试把opset拉到13以上或手动拆成卷积+reshape。
先试下onnxruntime的CUDAExecutionProvider跑一遍,排除CPU算子实现差异再谈量化。
我之前也栽在过类似问题上,不过我是Faster R-CNN转ONNX。你试试把opset提到13以上,有些算子在低版本下会走fallback路径,数值精度就差一截。另外YOLOv5的focus层在转换时容易出问题,建议直接改成等效的卷积或者slice+concat,对比一下中间输出,误差很容易定位到具体哪一层。还有,onnxruntime的CPU执行精度和PyTorch默认的float32不完全一致,尤其涉及sigmoid和exp的时候,可以先开个graph优化级别的开关看看。
先查下预处理里letterbox的padding值是不是被ONNX的常量折叠吃掉了,之前踩过这坑。
我之前跑分类模型也遇到过类似情况,但没你这么明显。你这个置信度整体掉0.1-0.2,感觉不太像单纯的数值精度问题,更像是某个算子在ONNX里的实现路径和PyTorch不完全等价。特别是YOLOv5的focus层,本质是切片加concat,ONNX导出时有时会被拆成多个Gather或者StridedSlice,某些runtime版本对这类组合的优化很差,甚至会出现数值截断。我建议你先用onnxruntime的CPUExecutionProvider跑一遍,再试试TensorRT或者OpenVINO,如果不同后端结果差异很大,那基本就是图优化没吃透。另外你可以把导出时的opset调高到13或14,有些算子在高版本里有更精确的映射,特别是Resize和Upsample,YOLOv5的检测头里用了不少。还有一个土办法,就是导出的ONNX里把每一层输出都打印出来,和PyTorch逐层对比,定位到具体哪个节点开始偏差。我之前这么干过,最后发现是BatchNorm在folding时和卷积融合后,epsilon参数没带过去,差了1e-5级别,结果在深层网络里放大得很厉害。你那个keep_initializers_as_inputs试了没用也正常,那个主要影响权重是不是常量节点,不影响数值。
不过话说回来,如果只是CPU部署,你其实可以试试直接用torch的JIT trace后再转,有时候比torch.onnx.export更干净,我上次转一个分割模型就这么解决的。你检查过输出的坐标框本身有没有偏移吗?如果只是置信度低但框位置准,那可能问题出在最后的sigmoid或阈值处理上,ONNX导出后有些常量折叠会改变数值范围。建议你把原模型和ONNX的输出logits直接对比,别只看后处理后的框,这样能更快锁定是模型内部还是后处理的问题。
我之前也踩过类似的坑,后来发现问题出在yolo的anchor grid生成用了numpy操作,导出时被当成常量固化了,导致输入尺寸变化时对不齐。你可以试试把focus层换成普通conv加stride,或者干脆用onnx-simplifier过一遍,能解决不少奇奇怪怪的精度漂移。另外你比较过onnx和torch输出特征图的逐通道数值吗?有时候差异就集中在某个特定层,用onnxruntime的调试工具定位到具体算子再针对性处理,比瞎试参数高效多了。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是opset和量化,而是focus层的slice拼接在onnxruntime里会拆成多个Slice节点,默认情况下可能触发不同的内存布局优化,导致数值漂移。你试试把torch.onnx.export的opset提到13或者14,有时候更高版本对slice和concat的融合更友好。另外,检查一下你导出时的训练模式和推理模式,如果模型里有BN层,转的时候没冻结BN的running_mean和running_var,或者用了training=True,那差异会直接反映在置信度上。还有一个小技巧,你可以用onnxruntime的CPU EP和CUDA EP分别跑一下同一张图,如果CPU结果更差,那就是图优化里某些算子被替换成低精度实现,可以在SessionOptions里把graph_optimization_level设为ORT_ENABLE_BASIC试试。如果还不行,那就逐层对比输出,把PyTorch每层的tensor dump出来和ONNX对比,用onnxruntime的IO Binding加上自定义op来定位,但那个工程量挺大的,建议先检查一下预处理里有没有额外的letterbox操作被重复执行。我最后是把focus层替换成等效的conv+concat手动写进模型里才解决的,虽然丑但稳。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是量化,而是focus层在转换时被拆成多个slice和concat,ONNX Runtime的算子实现跟PyTorch的底层逻辑有细微差别。你可以先用onnxruntime的graph optimization level设为0跑一遍,排除图优化导致的计算顺序改动,然后再逐层对比输出,用脚本分别跑PyTorch和ONNX的中间特征图,看偏差是从哪一层开始累积的。另外opset 12的话,建议试试改成11,有些老版本算子对slice的支持反而更稳定,我遇到过opset高反而精度变差的情况。
遇到过一模一样的坑,YOLOv5转ONNX我折腾了小半个月,最后发现根本不是opset或者keep_initializers_as_inputs的问题,你这个置信度整体掉0.1-0.2太规律了,反而更像是在focus层或者上采样那里被改成了非等价的实现。你可以先别急着上量化和图优化,直接在onnxruntime里把CPU执行器的优化级别调成0,也就是全关掉,然后跑一遍看结果,如果这时候和PyTorch对上了,那就是图融合里某些算子被错误重排了,尤其是SiLU和conv的融合,在旧版ORT上出过类似精度问题。另外你用的opset12可能对某些slice的负索引处理有差异,建议升到15以上试试,YOLOv5官方现在都推荐opset17了。还有个小技巧,把导出时的dynamic_axes去掉,用固定shape先跑通,排除掉动态维度导致的隐式reshape插值误差。如果还是不行,就把每个算子的输出都dump出来,和PyTorch逐层对比,重点看focus的切片顺序和cat的维度顺序,这两个地方最容易出偏差,我最后就是手动重写了focus层才解决的。
这问题我当初也踩过坑,而且yolov5的focus层确实是重灾区。你opset12的话,slice+concat的组合在onnxruntime里经常会有精度损失,尤其当输入尺寸不是32的整数倍时,边界像素的补齐逻辑很容易出偏差。我建议你先别急着怀疑量化和图优化,直接写个脚本把onnx里每个节点的输出和pytorch的中间特征图逐层对比,这样能定位到具体是哪几个算子开始出现偏差。另外你确认下onnxruntime的execution_mode是不是设成了ORT_PARALLEL,有时候多线程下的浮点累加顺序不同也会导致置信度漂移。还有个冷门但有效的方法,把torch.onnx.export里的opset版本试一下11或者13,不同版本对某些算子的实现方式有差异,我上次就是换到13解决的。如果小目标漏检严重,也可以检查下你的anchor grid生成部分,转onnx后如果用了动态shape,grid的生成逻辑可能会被优化成简化版本,导致坐标映射有微小错位。最后实在不行,试下把focus层改成普通卷积+stride2的下采样,这样转换更干净,牺牲一点点推理速度换精度对齐,我这边实测是值得的。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的其实是focus层,pytorch里用切片加concat实现,ONNX的slice算子在不同opset下行为有差异,建议直接用onnx-simplifier过一遍看看图结构。另外你可以把中间层的输出导出来对比一下,锁定是哪个节点开始出现偏差的,大概率不是量化问题,因为onnxruntime默认fp32推理。如果确认是算子精度,可以试试把opset升到13以上,或者改用torch的onnx export时加上dynamic_axes的同时把模型里所有自定义操作都换成标准模块。
我之前也踩过类似的坑,YOLOv5转ONNX对focus层和slice的算子兼容性确实容易出问题,可以先试试把opset升到13或14,有些算子在旧版本下会走fallback实现。另外检查下onnxruntime的execution_mode和optimization_level,CPU上默认的图优化有时候会改变算子融合方式,导致数值漂移。建议用onnxruntime的onnx_test工具逐节点对比输出,定位到具体哪一层开始分叉,比瞎猜高效得多。还有一个冷门点,YOLOv5的export.py里有个simplify选项,开了之后通常能减少一些冗余节点,数值一致性会好一些。
遇到过类似的坑,不过我当时是分割模型,现象是边界模糊+置信度整体掉。你opset12对YOLOv5来说其实够用,但focus层在ONNX里经常被拆成多个slice+concat,这一步的数值误差确实会被放大,尤其是后面接BN的时候。建议你先别急着怀疑量化,因为ONNX默认是FP32,除非你手动开了dynamic quantization,否则精度损失不该这么大。可以先做个逐层对比,把PyTorch模型和ONNX模型同一张图的中间feature map导出来,用numpy算一下最大绝对误差,定位到具体是哪一层开始漂移的。我猜大概率是focus或者上采样附近的算子兼容性问题,特别是你用了opset12,有些老版本onnxruntime对slice的梯度处理跟PyTorch不完全一致。另外试试把onnxruntime的execution_mode设成ORT_PARALLEL,或者换成opset11,有时候高版本opset反而会触发某些图优化导致精度变化。还有个小技巧,导出时加上dynamic_axes的同时,把input_names和output_names里所有维度都标成动态,有些时候静态shape会让onnxruntime做错误的算子融合。如果还是不行,可以考虑把focus层改成普通卷积替代,YOLOv5官方后来也这么做了,能省很多事。