最近在把一个训练好的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 条这问题我踩过坑,建议先别急着怀疑量化,opset12的focus层转换确实容易出幺蛾子,尤其是slice+concat的组合。你可以试试把模型里focus改成普通conv,或者用onnx-simplifier处理一下,很多时候是冗余节点导致精度漂移。另外确认下onnxruntime是不是用了float32,CPU上某些算子会偷偷降精度。我之前遇到过类似情况,最后是手动改onnx图把几个reshape删掉才解决。
这问题我踩过坑,大概率不是量化,opset12默认fp32精度损失没这么大。你先试试把onnxruntime的execution_mode设成ORT_PARALLEL,顺便把session options里的optimization_level调到ORT_ENABLE_ALL,有时候图优化反而会把某些算子融合出问题。另外focus层转出来经常有slice+concat的组合,你可以用onnx-simplifier过一遍,再对比下中间tensor的max abs error,定位到具体哪个节点开始飘的。我之前就是卡在slice的indices类型上,转成int64就好了。
遇到过类似的,最后排查出来是上采样和concat的精度问题,建议先单独导出各层对比下输出差异。
把focus层换成普通卷积试试,这层在转换时容易出数值偏差,我之前就是这么解决的。
我之前也踩过类似的坑,YOLOv5的focus层在onnx里会被拆成slice+concat,fp32下确实会有几个e-6级别的误差,但置信度差0.1-0.2不太像纯精度问题。你试试把onnxruntime的execution_mode设成ORT_SEQUENTIAL,顺便检查下有没有被自动融合成某些不稳定的算子。另外,强烈建议你导出时把模型输出直接存下来和torch的逐层对比,用onnxruntime的IO Binding或者写个脚本跑中间节点,大概率能定位到是哪个op开始漂移的。我之前是发现batch维度被动态化后,某些卷积的padding计算方式变了,改成固定shape就正常了。
先单独把focus层和slice输出导出来对比下数值,大概率是这里精度丢了,跟量化关系不大。
先关掉onnxruntime的优化试试,之前我遇到过类似的,是图优化把算子融合搞出精度问题了。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实是focus层里的切片和拼接操作,opset12对某些算子的实现会走不同的kernel。建议你先用onnxruntime的run_options打开enable_profiling,或者直接用onnxruntime.transformers.optimizer对比一下优化前后的输出,看看是不是某个节点数值偏差被放大了。另外可以试试把opset提到13或14,有些精度问题跟版本默认的算子实现有关,不一定非要纠结量化。
还有个思路是导出时把do_constant_folding设为False,有时候图优化会把一些系数提前折叠,导致浮点误差累积。如果还不行,可以逐层打印onnx的中间输出跟pytorch对一下,定位到具体哪一层开始漂移,比瞎猜快得多。
我之前跑YOLOv5转ONNX也踩过这个坑,置信度掉0.1-0.2基本不是量化问题,因为你用的是fp32导出,大概率是图优化把某些op融合后引入了数值截断。你试试把onnxruntime的session options里graph_optimization_level设成ORT_ENABLE_BASIC,有时候默认的ALL模式会把BN和conv融合得过于激进,对focus层这种自定义结构特别不友好。另外我怀疑你那个slice操作,YOLOv5的focus如果用切片实现,转出来是四个stride为2的slice再concat,ORX在CPU上对这类pattern的kernel选择很迷,可以手动把focus重写成conv2d加reshape,数值就差在几个e-6上。还有个小细节,你对比的时候是不是忘了把模型切到eval模式?dropout和BN的running stats在训练模式下会漂移,这个最容易忽略。如果还不行,用onnxruntime的IO Binding把输入输出改成numpy数组直传,排除掉DML或者CUDA EP的精度差异,我之前就是这么定位到是CPU kernel的问题。最后建议你dump一下onnx模型里每个节点的输出,跟pytorch的中间tensor对一下,用numpy的allclose设rtol=1e-3,很快就能锁死是哪个节点开始偏差的。
先别急着怀疑量化,opset12下yolo的focus切片容易出精度坑,试试转成opset11或者手动重写focus层。
我倒是觉得大概率不是量化的问题,你opset12默认就是fp32导出,先排除这个。之前我碰到过类似情况,最后定位到是yolo的anchor grid生成那块,pytorch和onnx的算子实现细节有偏差,尤其focus层用切片重写后很容易出这种隐性bug。建议你先用onnxruntime的IO Binding逐层对比中间tensor,或者干脆把focus层换成普通的conv加reshape,很多转模型的问题都是这种小算子搞的鬼。另外你试试opset11,有时候高版本反而会触发一些奇怪的图优化。
不过置信度整体低0.1-0.2这个幅度有点大,不像纯数值误差,你检查过输出层最后的sigmoid或者decode部分吗?有时候转出来会把某些fused操作拆开导致数值范围不对。我上次调一个模型就是这里出了问题,改完直接一致了。实在不行你用onnx-simplifier过一遍,再对比下onnxruntime和pytorch的nms前输出,这样能快速二分定位是网络前向还是后处理的问题。
大概率是focus层转出来有精度损耗,试试把slice和concat合并成单个卷积,能救回来不少。
先跑个onnxruntime和pytorch的逐层输出对比,定位到具体哪一层开始漂移,别瞎调参数。
我之前也踩过这个坑,YOLOv5转ONNX最容易出问题的其实不是opset,而是focus层里的slice和concat,onnxruntime对这类操作的图优化有时候会重排内存导致数值漂移。你可以试试把focus层手动改成conv+reshape,或者用onnx-simplifier过一遍再看差异,我上次就是这么解决的。另外置信度差0.1-0.2不像是量化,更像是某个op的精度丢了,建议你在onnxruntime里开下CPU EP的算子级日志,看看有没有fallback到默认kernel的情况。
先检查下ONNX里有没有opset不支持的算子,用onnxsim精简下再对比逐层输出试试。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的其实不是量化,而是focus层那波slice+concat操作在ONNX里会被拆成多个小算子,某些opset下精度损耗会累积。你opset12的话可以试试升到13或者17,特别是如果你用到了一些比较新的算子,数值表示方式会有差异。另外,你对比的置信度低0.1-0.2,这个幅度不太像纯浮点误差,更像是某个中间层的输出被截断或者用了不匹配的opset导致计算图被重排了。我建议你先用onnxruntime的graph优化开关做A/B测试,把优化全关掉再跑一次,如果精度恢复了那就是图优化把某些节点融合错了,如果还是差那就是算子映射本身有问题。还有个笨办法,你把原模型的pth输出和onnx的输出在同一个中间层拉出来对比,用hook逐层打印tensor,定位到第一个出现差异的层,基本就能锁定是哪个算子的问题。另外注意一下yolov5的export.py里有个half参数,如果你转的时候没关半精度,CPU上跑onnxruntime默认是float32,这也会导致输出分布变化。最后建议你直接用onnxruntime.transformers.optimizer看看能不能识别出focus层,有些版本会自动把它重写成conv,效果会好很多。
这种置信度普遍掉0.1-0.2大概率不是量化问题,opset12下YOLOv5的focus切片确实容易出精度差异,建议先用onnxruntime的symbolic shape inference跑一遍再对比每层输出。
我之前也踩过这个坑,最后发现是yolo的anchor和grid生成逻辑里用了不少python控制流,转ONNX时被拆成了很多小算子,累积误差就出来了。建议你先用onnxruntime的graph优化全开试试,不行就写个脚本逐层对比中间tensor,重点看focus和slice那几层,数值差个1e-3都可能放大到置信度上。另外opset可以试下13或更高,有些算子实现更稳定。
先跑一下onnxruntime和torch的逐层输出对比,大概率是focus切片在转换时精度丢了。
我之前也踩过类似的坑,YOLOv5转ONNX最容易出问题的地方其实是focus层和后续的slice,因为PyTorch里实现方式和ONNX的算子映射不完全一致,数值误差会累积。建议你先用onnxruntime的IO Binding或者把中间层输出导出来逐层对比,定位到具体哪一层开始漂移。另外,你opset12的话可以试试把torch.onnx.export里的opset_version提到13或14,有些算子在新版本里精度处理更好。还有,如果用的是onnxruntime CPU,检查一下是否开了graph_optimization_level,默认是ORT_ENABLE_ALL,有时候优化反而会引入差异,可以手动调成ORT_DISABLE_ALL对比试试。我那次最后是改成了dynamic_axes配合固定输入尺寸,并且把focus层改成普通卷积才解决的,你参考下。
我之前也踩过类似的坑,最后定位到是yolo的anchor grid生成那部分在onnx里被拆成了多个小算子,浮点累加顺序变了导致坐标偏移,置信度也跟着掉。你可以先试试把opset拉到13或更高,有些版本对slice和concat的优化更稳。另外,别光看置信度,把bbox坐标也对比一下,如果坐标有细微偏差那基本就是算子融合的问题,可以试着用onnx-simplifier过一遍图,有时候能解决。
我之前跑过类似的项目,也是yolo系列转onnx,conf掉点的情况跟你几乎一模一样。后来折腾了半天发现,问题大概率不在opset或者keep_initializers上,而是focus层里的slice和concat操作在onnx里会被拆成多个子图,导致中间结果的数值精度跟pytorch里不完全一致,尤其当你开了fp16或者cpu端用mkldnn加速时,这种微小差异会被放大。建议你先别纠结全局,直接用onnxruntime的IO Binding接口把中间层的输出导出来,跟pytorch的对应层做逐元素对比,定位到第一个出现偏差的算子,我那次就是发现slice的步长在转换后多了一个隐式reshape,改一下导出时的input_names和output_names顺序就解决了。另外,如果你的模型里有batch norm,记得转onnx前先把bn层fold进conv里,pytorch导出时默认会做,但如果你用了自定义的检测头,可能会漏掉。还有个笨办法,试试opset 11,有时候12的某些新算子对旧版推理引擎兼容性反而不好。至于量化,你要是没做ptq或qat,就不该怀疑量化,纯float32下的偏差基本都是图结构问题。