最近在把一个训练好的图像分类模型(ResNet-50)从PyTorch导出到ONNX,再用ONNX Runtime做推理。结果发现精度掉得很明显,Top-1从原来的92.3%掉到了88%左右。我试了用torch.onnx.export,设置opset_version=11,也试了用onnx-simplifier简化模型,但效果不大。有没有老哥遇到过类似的问题?是ONNX对某些算子(比如BatchNorm、AdaptiveAvgPool)的转换有精度损失,还是我导出时没开正确的优化选项?另外,如果后续要部署到移动端,这个精度掉得能接受吗?还是说应该直接上TFLite?求指点。
PyTorch转ONNX后推理精度下降,是量化问题还是算子不支持?
全部回复
共 9 条这问题我前阵子刚踩过坑,说下我的排查思路吧。
Top-1掉4个点确实不正常,ResNet-50这种成熟结构,算子转换一般不会差这么多。先别急着怀疑量化,你说没开优化选项,那ONNX默认的优化其实已经比较保守了,不是精度下降的主因。我建议你先把问题定位到具体的层。
最有可能的凶手是BatchNorm和AdaptiveAvgPool。BatchNorm在训练和推理时的行为不同,ONNX导出时如果没正确冻结BN层(比如某些自定义的BN实现),或者torch.onnx.export里没设training=False,结果可能跑偏。另外AdaptiveAvgPool在ONNX里可能会被拆成多个基础op,不同runtime实现有精度差异。你可以试着手动把AdaptiveAvgPool换成固定kernel size的AvgPool,看精度能不能回来。
另一个常见坑是输入张量的预处理不一致。PyTorch里你用的归一化参数(mean/std)在ONNX Runtime推理时是否正确对齐?有时模型内部带了归一化,外部又手动做一遍,等于做了两次,精度直接崩。建议你在导出前就把预处理写进模型里,或者确保两边输入像素值范围完全一致。
如果以上都排除了,还有个冷门原因:opset版本。opset 11对某些op的支持不如12或13稳定,比如Resize的坐标对齐方式在opset 11和13之间行为有变化。可以试试opset 13或17,但要注意移动端runtime版本是否兼容。
至于移动端部署,TFLite对量化/剪枝的支持确实更成熟,尤其如果你后续要跑在ARM或NPU上,TFLite的算子库和硬件加速更友好。但ONNX Runtime现在也有移动端版本(ORT Mobile),精度和性能也在追。关键是看你目标平台的生态——如果团队已经有TFLite的部署流水线,那就直接转;如果还在选型,建议先用ONNX把精度问题彻底定位清楚,再决定迁移路径,别带着问题跳到另一个框架。
这个精度掉得确实有点狠,92%到88%已经不是“误差范围”能解释的了。我最近也踩过类似的坑,简单说几个可能性,可以对照排查一下。
首先,BatchNorm在ONNX Runtime下默认是fused到Conv里的,按理说不会引入精度偏差,但如果你导出的模型里BN层是training状态,那推理时running_mean/running_var用的是当前batch的统计量,结果直接崩。检查一下export前有没有model.eval(),很多人会忘这一步。
其次,AdaptiveAvgPool是个经典雷区。PyTorch里的实现是动态计算kernel size和stride,但ONNX标准里没有这个算子,导出时会被拆成Gemm或者若干静态Pooling组合,如果输入尺寸在导出时是固定的还好,但如果是动态batch或者非正方形输入,这种拆分就会产生细微的数值偏差。你可以在导出后用onnxruntime直接跑一下中间层的输出,跟PyTorch逐层对比,看看偏差是从哪一层开始放大的。
另外,opset_version=11确实有点老了,现在至少用opset_version=15以上,很多精度相关的优化和新算子支持都是后面版本加进来的。还有,你在ONNX Runtime里跑的时候,执行后端是CPU还是CUDA?如果是CPU,有些算子的实现精度和GPU上不一样,比如LayerNorm、Softmax的数值稳定性,ResNet-50里没有这些,但BatchNorm在CPU上用不同库实现也可能有微小差异。
至于移动端部署,4个点的精度掉在部署场景里大概率是不能接受的,尤其是如果这是生产模型的话。TFLite也不是万能药,它一样有量化对齐问题,但如果你最终目标就是移动端,不如直接从PyTorch导出成TorchScript再用TFLite转换,少过一道ONNX手续,有时候反而更稳定。当然,前提是你验证了ONNX这条路的精度问题确实能修,否则不建议直接跳到TFLite换方案。
建议先把上面几点排查一遍,尤其是逐层对比,这是定位偏差最直接的办法。
遇到过,ResNet-50这种经典模型精度掉这么多大概率不是算子不支持,而是BatchNorm层在导出时fuse到Conv里导致了数值差异。可以试试在export前把model.eval(),同时设置torch.onnx.export的training=torch.onnx.TrainingMode.EVAL,再检查下onnxruntime的execution provider是不是用了CPU,GPU下精度可能不同。移动端的话这个精度损失其实算正常范围,TFLite量化后也可能掉1-2个点,建议先确认导出流程再决定。
老哥试试把BN层和AdaptiveAvgPool换成固定尺寸的卷积或池化,ONNX对动态尺寸支持不太好。
大概率是BatchNorm和AdaptiveAvgPool在转换时精度丢了,试试opset_version=12或者手动替换成固定尺寸的AvgPool。
遇到过类似的情况,当时也是ResNet系列,不过是50还是101记不清了。先说结论:你这4个点的精度掉法,大概率不是量化问题,因为ONNX导出默认是FP32,没开量化的话精度损失主要来自算子层面的实现差异。
我踩过的坑主要有两个方向,你可以排查一下。第一个是BatchNorm的融合问题。PyTorch里BN在训练和推理时有不同的行为,export的时候如果没设对模式(model.eval()),或者torch.onnx.export里没加do_constant_folding=True,BN的缩放偏移参数可能会被展开成更复杂的计算图,ONNX Runtime解析时浮点运算顺序不同产生微小误差,叠加起来就掉点。第二个是AdaptiveAvgPool,这个算子ONNX标准里没有直接对应的实现,导出时会被拆成若干基础算子(比如ReduceMean配合Reshape),不同opset版本拆法不一样,opset=11确实可能拆得不够好,可以试试opset=12或13,新版对这类动态形状操作支持更成熟。
另外,onnx-simplifier有时候会过度简化,把一些必要的shape推断信息剪掉,反而导致Runtime里用自动填充的方式补全,精度就漂了。建议你导出后先用onnxruntime的session.run()跑一遍原图对比一下中间层的输出,看哪一层开始偏差变大,定位具体算子。
至于移动端部署,4个点精度损失我个人觉得偏大了,ResNet-50这种成熟结构应该能控制在1个点以内。如果最终目标是移动端,建议直接拿TFLite做量化感知训练(QAT),或者用NCNN、MNN这类端侧推理引擎重新导出,它们对ONNX的兼容性反而更可控。
这问题我踩过类似的坑,ResNet-50转ONNX精度掉4个点大概率不是算子不支持,而是BatchNorm和AdaptiveAvgPool在导出时融合或实现细节有差异。你试试把opset_version升到15或17,有些优化在低版本里没生效。另外检查下模型是否处于eval模式,dropout和bn在train模式下导出会出问题。移动端的话88%其实还行,但既然都到这步了,不如直接试TFLite量化,int8能压到85%以上就值了。
我之前也踩过这个坑,ResNet-50在ONNX Runtime下精度掉到88%确实不正常。建议你检查一下BN层是否被fuse了,ONNX默认会合并BN和Conv,但有时会引入细微误差;AdaptiveAvgPool在opset 11之后是支持的,但不同后端实现有差异。另外可以试下用onnxruntime的CUDA Execution Provider跑一下,排除CPU推理的浮点对齐问题。移动端的话,如果精度差4%以上我建议直接上TFLite,量化校准做得好能控制在1%以内。
遇到过类似情况,我那次是AdaptiveAvgPool搞的鬼,PyTorch实现和ONNX标准转换后计算细节有差异,建议你导出时把pooling层手动改成固定尺寸的AvgPool试试。另外ONNX Runtime默认用CPU跑可能跟你的GPU训练环境有数值精度差异,检查下推理时是否开了float16,或者试下opset_version=12。移动端部署的话4%的掉点其实不小了,得先排查清楚原因再考虑TFLite,不然换了框架问题可能还在。