最近在把一个训练好的图像分类模型(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后推理精度下降,是量化问题还是算子不支持?
全部回复
共 183 条你这掉法大概率不是量化问题,opset 11的BN和AdaptiveAvgPool在ONNX Runtime里都有对应的官方实现,精度损失一般不会这么夸张。建议先检查一下预处理环节,比如mean/std的归一化方式在导出时有没有被固化进模型,或者推理时输入数据的通道顺序是不是和训练时一致。另外可以试试把opset升到15以上,某些老版本算子映射确实有坑。移动端的话,如果这个精度差距对你业务影响大,还是别将就,直接上TFLite的量化感知训练流程会更稳。
这种精度掉法大概率不是算子不支持,ResNet-50那几个层ONNX支持都挺成熟了。你试试导出前把模型设成eval模式,然后核对下预处理和后处理是不是跟PyTorch里完全一致,有时候是输入归一化参数没对齐。另外opsert_version可以拉到13以上,顺便检查下onnxruntime的优化级别,默认开全量优化有时候会改图结构。移动端的话4个点确实有点肉疼,但量化后可能更离谱,建议先拿校准集做下动态量化对比再决定。
遇到过类似的,但你这个4个点的跌幅确实大了点,不太像单纯的量化问题。建议先检查下模型里有没有自定义的forward逻辑或者预处理没对齐,ONNX导出时这些很容易被“静默”掉,尤其是ResNet这种带残差的,batch norm折叠后数值分布可能变。另外AdaptiveAvgPool在opset 11下会转成动态shape的Gather+ReduceMean,某些runtime实现会有微小误差,可以试试固定输入尺寸导出看精度是否回升。如果后续要上移动端,建议直接对比下TFLite和ONNX的量化后精度,而不是只看浮点结果,毕竟真机部署通常要int8,那才是真正的考验。
我之前也栽在过这坑里,ResNet-50导出ONNX后精度掉4个点大概率不是算子问题,而是导出时模型默认进入了训练模式,BatchNorm的running_mean和running_var没被冻结。你试试在export前强制调model.eval(),再把opset提到13以上,有时能解决不少隐式转换的精度差。另外AdaptiveAvgPool在ONNX里会展开成动态shape的Gather,对输入尺寸敏感,最好用固定分辨率导出并加一个Resize层预处理,能稳住精度。移动端部署的话,如果硬件不支持INT8,这精度损失确实明显,建议先做PTQ或QAT量化校准,比直接换TFLite靠谱,TFLite对自定义算子支持更麻烦。
92.3掉到88这个幅度有点太大了,一般纯转换不会差这么多,八成是你导出时没切eval模式或者dtype对不上。建议先检查model.eval()和torch.no_grad(),再对比ONNX和PyTorch同一张输入的输出,看是从哪一层开始飘的。BatchNorm和AdaptiveAvgPool导出基本没啥精度问题,opset 11也够用,别急着甩锅给算子。移动端部署的话88其实也还行,但得先搞清楚这4个点到底丢哪儿了,不然换TFLite一样会踩坑。
BatchNorm在ONNX里一般不会掉这么多,先别急着怀疑算子。你导出时模型是不是还在train模式?忘了eval()的话BN统计量不对,精度直接崩。另外确认下预处理和后处理两边是不是完全一致,归一化参数差一点都能掉好几个点。如果这些都没问题,再对比下ONNX和PyTorch同一批输入的输出差异,定位到具体层。移动端88%其实也能用,但TFLite对ResNet-50支持更成熟,值得试试。
92.3掉到88确实有点狠,正常ONNX转换的精度损失通常在0.1到0.5个点以内,你这个幅度基本可以排除单纯的浮点误差了。我建议先别急着怀疑算子,第一步应该做逐层对比:把PyTorch和ONNX Runtime每一层的输出都dump出来,看是从哪一层开始偏差变大的。很多时候问题出在预处理上,比如PyTorch的Normalize和ONNX里你自己写的mean/std顺序不一致,或者Resize的插值方式不同,这些坑特别隐蔽。另一个常见原因是eval模式,导出前忘了model.eval()的话BatchNorm会走training分支,统计量直接用batch的,精度肯定崩。opset 11对ResNet这种结构支持已经很成熟了,AdaptiveAvgPool会被拆成AveragePool加Reshape,一般不会有大问题。至于移动端,88%的Top-1其实还能用,但前提是你要先搞清楚这4个点到底丢在哪,不然换TFLite也是一样的结果,因为根源可能在导出流程而不是框架本身。
先查下是不是没设eval模式,BN的running stats没同步过去,我踩过这坑。
先查一下ONNX和PyTorch的输出差多少,八成是某层算子对不上,别急着换TFLite。
先对比下ONNX和PyTorch同一输入的逐层输出,八成是BN或池化那里对不齐,别急着换TFLite。
BatchNorm和AdaptiveAvgPool这俩其实在ONNX里支持度挺好的,一般不会是它们直接导致这么大的掉点。我怀疑你大概率是踩了eval模式没切或者导出时忘了torch.no_grad()的坑,尤其是模型里如果有Dropout或者BN,train模式下导出来的计算图跟推理完全是两码事,Top-1掉四个点挺符合这个症状的。另外你可以对比一下ONNX Runtime和PyTorch在同样输入下的中间层输出,用onnxruntime的profiling或者逐层dump,看看到底是哪一层开始偏的,一般这么查很快就能定位。opset 11对ResNet-50是够用的,simplifier主要做常量折叠和图优化,对精度影响微乎其微,指望它救回来不太现实。还有一点容易被忽略的是预处理,PyTorch里如果用了PIL加ToTensor,跟ONNX部署时用cv2或者numpy读图,归一化和通道顺序差一点,Top-1就能差好几个点,建议先把输入张量对齐了再谈模型问题。至于移动端,四个点的掉幅说实话有点大,正常转换控制在1个点以内才算健康,先别急着换TFLite,把上面几个点排一遍再说。
92.3掉到88其实挺典型的,先别急着怀疑量化,你压根没提量化,所以大概率不是量化引起的。ResNet-50里最容易出问题的是AdaptiveAvgPool,PyTorch导出时经常会转成一堆AveragePool组合,某些opset下算出来的结果和原始实现有细微差别,累积起来就能吃掉几个点。你可以先把ONNX Runtime的输出和PyTorch逐层对比一下,用onnxruntime的profiling或者写个脚本把中间层feature map抽出来比,定位到具体哪一层开始偏的。还有个坑是导出时的eval模式和no_grad,如果模型里还有dropout或者BN在train状态,那精度肯定崩,这个一定要确认。另外输入预处理也得对齐,比如PyTorch的Normalize和ONNX里如果又加了一遍,均值和方差对不上也会掉点。opset 11对ResNet来说其实够用了,但可以试试opset 13或17,有些算子实现更稳。移动端的话88%的Top-1其实不算不能用,关键看你业务容忍度,如果本来92%是及格线那肯定不行。TFLite不一定更好,转换链路更长,坑只会更多,先把ONNX这条链路对齐再考虑换框架。
92.3掉到88确实有点夸张了,一般导出顶多差个零点几。建议先排查下预处理和后处理是不是对齐了,比如归一化参数、resize方式、还有输出有没有过softmax,这些坑比算子本身更常见。BatchNorm和AdaptiveAvgPool在opset11下基本不会有明显精度损失,你可以逐层对比下ONNX和PyTorch的输出,定位到具体哪一层开始偏的。另外移动端部署不一定非得TFLite,ONNX Runtime Mobile或者NCNN都挺香的,先别急着换框架。
92.3掉到88这个幅度不太像是单纯的浮点误差,ResNet-50里BN和AdaptiveAvgPool在opset 11下转换其实挺成熟的,不太可能背这个锅。你先别急着怀疑算子,建议拿同一批输入分别跑PyTorch和ONNX Runtime,逐层对比中间输出,大概率能定位到是哪一层开始飘的。我踩过的坑里,最常见的是预处理没对齐,比如PyTorch用PIL加ToTensor,而ONNX那边用了cv2或者归一化参数写错,这种能直接吃掉好几个点。另外要确认模型导出时是不是eval模式,BN在train模式下统计量会跟着输入变,导出来自然对不上。还有动态shape那块,如果你用了dynamic_axes但实际推理尺寸和训练不一致,插值相关的算子也会引入偏差。量化的话你压根没提,如果没做PTQ或QAT,那就不是量化问题,别往那边想。至于移动端,88和92的差距在分类任务上其实挺敏感的,建议先把精度追回来再考虑TFLite,不然换框架只是把问题搬家。
精度掉这么多不太像是算子不支持,更像导出时某些层进入了eval之外的路径,或者输入尺寸、归一化参数没对齐。我建议先比对ONNX和PyTorch同一批输入的中间层输出,重点看第一个BatchNorm和最后的pooling。另外opset 11对AdaptiveAvgPool支持没问题,但如果你开了dynamic axes或没设model.eval(),结果会飘。移动端部署前还是先把ONNX推理对齐到92%再说,不然换TFLite也白搭。
ONNX Runtime默认跑的是FP32,理论上不该掉这么多,你先把onnx模型的输出和PyTorch逐层对一下,看误差是从哪层开始炸的。AdaptiveAvgPool在opset 11里转成AveragePool时如果输入尺寸不匹配,容易出问题,可以试试固定输入分辨率再导出。另外检查下是不是用了动态batch,动态轴有时候会触发一些奇怪的kernel。移动端的话4个点其实挺疼的,建议先定位原因再考虑换框架。
ResNet-50从PyTorch导到ONNX掉4个点有点夸张了,正常情况不该这样。你说的BatchNorm和AdaptiveAvgPool其实ONNX都支持得挺好,opset 11也够用,问题大概率不在这。我之前踩过一个坑,导出的时候如果模型还在train模式或者没做eval(),BatchNorm的running stats会出问题,精度直接崩,你可以先确认下这个。另外可以逐层对比一下PyTorch和ONNX Runtime的输出,用onnxruntime的profiling或者写个脚本把中间层feature map拉出来比对,看看到底是哪一层开始偏的,这样比瞎猜算子靠谱多了。还有个容易被忽略的点,输入预处理得完全一致,比如归一化的mean/std、resize的方式、甚至RGB还是BGR,这些对不上精度也会掉得莫名其妙。至于量化,你如果没主动做INT8量化,那跟量化没关系,别往那上面想。移动端部署的话TFLite确实生态更成熟,但你这精度掉得明显是导出环节有问题,先把ONNX这条链路调通再说换框架的事,不然换到TFLite一样会掉。
ResNet-50掉4个点不太正常,一般纯转ONNX精度损失在千分位。先别急着怀疑算子,建议逐层对比PyTorch和ORT的输出,大概率是某个op的数值行为不一致,比如 AdaptiveAvgPool 在动态shape下容易出问题。另外检查下预处理,mean/std和resize方式两边是否完全对齐,这个坑踩过好几次。移动端的话4个点确实有点多,但先定位到具体层再决定要不要换TFLite也不迟。
有没有更详细的教程推荐?
我之前也遇到过类似问题,后来换了方案。