资讯动态

PaddleOCR模型转ONNX部署实战:动态shape与精度对齐全记录

发布时间:2026/10/21 6:03:08 来源:尧图企业网站定制
最近在做一个OCR识别模块的迁移部署目标平台不方便直接跑完整的PaddlePaddle推理环境我得把文本检测和文本识别模型统一转成ONNX再交给跨平台推理引擎去执行。整个过程比我预想的复杂一些——模型本身的转换不算太难真正花时间的是版本搭配、动态尺寸处理、转换后的精度对齐这些边角问题。前前后后折腾了近两天总算把Paddle OCR的检测模型和识别模型都成功转成了ONNX格式并在ONNX Runtime里跑通了完整的端到端OCR流程。这篇东西把整个过程中的关键步骤、参数含义、踩过的坑都整理了出来给还在做模型迁移的朋友当个参考。1. 为什么要把Paddle文本检测识别模型转成ONNX1.1 部署场景下的实际需求OCR模型在资源受限或环境固定的平台上部署时ONNX Runtime和TensorRT是两类常见选择这些推理引擎更习惯读取ONNX格式。PaddlePaddle原生推理格式是.pdmodel加.pdiparams在本地或者有完整框架的服务器上跑没问题但一旦到了嵌入式设备、容器、Web端或者异构计算单元上再装一个完整的Paddle深度学习框架就很不现实。ONNX作为一个开放、中立的模型交换格式几乎能被所有主流推理引擎识别把它作为中间格式转换一次就能服务多个后端这是做迁移的核心动机。还有一个容易被忽略的点模型转换之后推理过程和训练框架彻底解耦了。训练和场景迭代继续用Paddle部署环节统一走ONNX研发和工程之间的分工清晰了发布流程也轻了。我参与的项目里模型一般只在训练集群上跑Paddle到了生产环境的服务节点所有深度学习模型统一走ONNX Runtime方便统一治理、监控和灰度。这个前提下把Paddle OCR的两个模型转成ONNX不是可选优化而是上线路径上必须完成的一步。1.2 PP-OCR模型结构回顾Paddle的OCR系列模型里文本检测模型基于DBNet结构整个网络由Backbone、FPN和DB Head组成。输入是经过resize和归一化后的图像张量输出是文字区域对应的概率图。注意这里的输出已经经过sigmoid激活但真正的后处理比如阈值二值化、连通域分析、文本框构建都在模型外部由Python或C代码完成。好在这个Head里的算子大多是卷积、上采样、加法和sigmoid都属于ONNX原生支持的标准算子所以检测模型整图导出基本没有障碍。文本识别模型一般用CRNN结构输入是检测模型裁剪出来的文本行图片经过固定高度resize后送入网络输出是每个时间步在字符表上的logits序列之后再用CTC解码得到最终文本。这一链路里同样不包含复杂的自定义算子模型内部就是卷积、LSTM/GRU、全连接和softmax/LogSoftmax的组合。因此识别模型也是完全可以导出的。对paddle2onnx来说只要算子图中没有特殊自定义op转换成功率就很高PP-OCR系列的检测和识别模型恰好都属于这一类。1.3 转换工具的选型逻辑行业内有一种做法是用ONNX手动搭建同结构网络再把训练好的权重逐层复制。这种做法在小模型上可行但OCR检测和识别网络动辄几十上百层且存在多分支和重复使用参数的情况手动搭建不仅慢还极易在复制权重时出错排查起来难度很大。相比之下paddle2onnx是Paddle生态的官方转换组件能够解析训练框架的算子图再映射到ONNX标准算子上自动完成绝大多数情况下的算子翻译。它的误差基本集中在少数算子翻译策略上可控性高得多这也是一线工程里选择它的主要原因。2. 环境准备与版本搭配2.1 版本对应关系是第一个坑paddle2onnx和PaddlePaddle版本之间的对应关系非常严格不是随便装最新版就能顺畅使用。转换工具需要读取模型中的算子信息如果Paddle版本太新paddle2onnx没有及时适配会在解析阶段报出一些奇怪的错误如果paddle2onnx版本太旧又可能无法识别新版本模型里的新算子。我这次使用的版本组合是经过多轮测试后确定下来的组件版本说明Python3.8.18建议用3.8或3.10兼容性最稳PaddlePaddle2.5.2训练/推理框架本体paddle2onnx1.2.4官方转换组件onnx1.14.1ONNX格式处理库onnxruntime1.16.3转换后推理验证这个组合在PP-OCR的mobile系列模型上实测非常稳定。如果你的Paddle版本不同最好自查一下版本兼容矩阵再安装能省掉大量莫名其妙的报错时间。我在第一次尝试时就是用了最新版的Paddle结果paddle2onnx在解析某个算子的属性时直接崩溃后来回退版本才解决问题。版本问题一定要放在第一步解决否则后面排查起来非常痛苦。2.2 安装与基础验证安装本身不复杂几个pip命令基本就能搞定pip install paddlepaddle2.5.2 pip install paddle2onnx1.2.4 pip install onnx onnxruntime装完先跑一下版本确认paddle2onnx --version能正常输出版本号就说明工具可用。另外建议顺带装一个onnxruntime后面转换完做推理验证必须用。还有一个小习惯每次转换前确认当前Python环境里的onnx和paddle2onnx与Paddle版本匹配避免环境里历史版本干扰。曾经遇到过一个问题环境里同时存在两个版本的onnxPython import时加载了旧版导致onnx模型修改时莫名报错清理环境后一切正常。2.3 准备推理模型文件PaddleOCR的推理模型下载后通常是这样的目录结构inference/ ├── det_model/ │ ├── inference.pdmodel │ └── inference.pdiparams └── rec_model/ ├── inference.pdmodel └── inference.pdiparams这里的inference.pdmodel是模型结构文件inference.pdiparams是权重参数文件。有些模型会多一个inference.pdiparams.info那个只是参数说明信息转换时用不到。自己训练的模型也可以用paddle.jit.save导出成同样格式关键是最终得到这两个文件即可转换工具对训练和导出方式不做额外限制。这步完成后就可以开始实际的转换操作了。3. 文本检测模型转换实操3.1 命令行的基本用法paddle2onnx支持命令行和Python API两种方式。命令行适合快速转换Python API适合嵌入到自动化流程里。命令行转换文本检测模型我使用的完整命令是paddle2onnx \ --model_dir ./inference/det_model \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./det_model.onnx \ --opset_version 11 \ --enable_onnx_checker True--model_dir指向存放推理模型的目录--model_filename和--params_filename分别指定结构文件和参数文件名。--save_file是输出ONNX文件的路径。--opset_version指定ONNX算子集的版本一般11或12就够用。不建议追求最新opset部分目标部署引擎对高版本算子集的支持不完整反而容易在推理阶段报错。--enable_onnx_checker True会在生成之后自动跑一遍ONNX模型合法性检查相当于转换完成后自我体检建议一直开着。第一次转换时我漏掉了这个参数后来在别的机器上加载模型时报出结构问题就是因为缺少了这一步校验。3.2 动态shape的处理文本检测的输入图像宽高在推理时是不固定的实际传入的图像会经过resize到一定尺寸但不同图像的长宽比差异可能很大。如果模型输入被固定成某个shape部署时就会很被动。paddle2onnx在多数情况下会保留模型里的动态维度但有时导出的模型输入仍是固定尺寸需要额外处理。检测模型的输入通常声明为[1, 3, 736, 736]这样的固定shape。虽然Paddle推理引擎可以容忍动态输入但ONNX模型如果固定了换一张不同宽高的图就会直接报错。解决方法是转换后手动修改输入维度我用的是onnx库的Python APIimport onnx from onnx import helper model onnx.load(./det_model.onnx) input model.graph.input[0] input.type.tensor_type.shape.dim[0].dim_param batch input.type.tensor_type.shape.dim[2].dim_param H input.type.tensor_type.shape.dim[3].dim_param W onnx.save(model, ./det_model_dynamic.onnx)这样输入张量的维度就变成了[batch, 3, H, W]的动态形式。要注意动态维度越多推理引擎在内存分配和图优化上的开销就越大。我一般只把H和W设置为动态batch维度保持固定因为线上推理大多是单张图片请求batch固定为1不仅在性能上更优还可以让ONNX Runtime保留更多图优化机会。如果确认后续有批量推理需求再把batch也设成动态。3.3 转换后的快速验证转换完成后先用ONNX Runtime加载一下确认模型能正常推理。验证脚本我习惯放在转换流程的最后一步import onnxruntime as ort import numpy as np sess ort.InferenceSession(./det_model_dynamic.onnx) input_name sess.get_inputs()[0].name output_name sess.get_outputs()[0].name print(input:, sess.get_inputs()[0].shape, sess.get_inputs()[0].type) print(output:, sess.get_outputs()[0].shape, sess.get_outputs()[0].type) x np.random.rand(1, 3, 640, 640).astype(np.float32) result sess.run([output_name], {input_name: x})[0] print(输出shape:, result.shape)这段代码能正常执行输出shape符合预期就说明模型结构、输入输出定义都没问题。这一步建议在转换后立即执行因为只要转换工具和模型本身有偏差错误信息就能第一时间暴露出来。我在识别模型转换时遇到过一个问题模型能成功保存但推理时输出维度与预期不一致就是因为没有及时验证直到接入全流程才暴露排查时又花了额外时间。3.4 与Paddle推理结果的核对跑通只是第一步还需要和Paddle推理结果做对比。可以先用Paddle跑一次前向保存输出再用ONNX Runtime用完全相同的输入跑一次比较两组输出的差异。文本检测模型的输出是概率图浮点误差一般很小。我会从最大误差、平均误差两项指标来评估要求绝对误差在1e-4以内超过这个量级就需要排查输入预处理或模型权重加载是否出了问题。pred_paddle ... # paddle推理得到的概率图 pred_onnx ... # onnxruntime推理得到的概率图 diff np.abs(pred_paddle - pred_onnx) print(最大误差:, diff.max(), 平均误差:, diff.mean())实测经验是如果只是浮点舍入差异最大误差在1e-5到1e-4之间如果出现明显差异大概率是推理模式不同造成的比如Paddle推理时开启了TensorRT加速或某些融合优化选项导致数值规律和ONNX Runtime不一致。这时可以先关闭Paddle的加速选项再对比优先保证逻辑一致。4. 文本识别模型转换实操4.1 识别模型的独特点文本识别模型和检测模型在转换流程上基本一致但有两个特性需要特别留意。第一识别模型的输入高度一般固定为32或者48宽度是动态的第二识别模型的输出是未经解码的logits序列shape通常为[1, T, C]其中T是时间步数C是字符类别数。验证时不能只看shape还要看logits的数值分布是否和Paddle输出一致因为logits的微小偏差会直接影响到CTC解码后的文本结果。识别模型转换命令和检测模型几乎一样paddle2onnx \ --model_dir ./inference/rec_model \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./rec_model.onnx \ --opset_version 11 \ --enable_onnx_checker True转换后如果发现输入shape固定同样可以用onnx库把宽度维度改成动态。我一般只改宽度高度保持固定因为识别模型在整条OCR链路中对输入高度的约束本来就强改了反而容易出问题。宽度动态则能适配不同长度的文本行图片这个在实际业务里非常关键。4.2 识别模型的输出验证识别模型的输出是未经CTC解码的logits验证时同样需要和Paddle输出对比。关键是最后一维对应字符类别数不直接对应文本内容要看到最终识别文本还得接上CTC解码和后处理脚本。单独验证模型时比较logits的一致性就足够说明模型转换没问题。# 假设rec_model.onnx已经加载 logits_onnx sess.run(None, {input_name: img_tensor})[0] # 与Paddle推理得到的logits_paddle对比 print(logits最大误差:, np.abs(logits_onnx - logits_paddle).max())如果最大误差在1e-4以内基本可以认为识别模型转换成功。如果误差偏大不要急着怀疑转换工具先确认输入图像预处理是否一致。我在实际验证时遇到过输入图像归一化参数漏写的问题导致logits差异达到了0.1量级排查后修正归一化均值方差误差立刻降到1e-5以内。4.3 在OCR全流程里验证识别效果更贴近实际的验证方式是把转换后的检测模型和识别模型直接嵌入到完整OCR流程里。流程大概是原始图像进入检测模型得到概率图经过二值化和连通域分析等后处理得到文本框裁剪出文本行图片再进入识别模型得到logits最后用CTC解码得到文本。每个环节的输入输出格式都要和Paddle原版保持一致尤其是检测输出经过后处理后任何微小偏移都可能影响框的位置进而影响后续识别效果。我用包含印刷体、手写体和倾斜文本的测试图做了端到端对比。转换前后的检测框位置基本一致识别出的文本内容也高度吻合个别手写体样本因为预处理差异出现单字符波动属于正常范围。调整后处理里的阈值参数后识别效果恢复到原有水平。这轮验证做完我的心才真正放下因为模型级对比再一致都不如端到端效果来得可靠。5. 转换后的精度验证细化5.1 预处理一致性是不可妥协的底线很多人做完模型转换后发现精度对不上第一反应是转换工具出问题了但实际排查下来大部分情况是输入预处理不一致。PaddleOCR在推理时会对图片做标准化归一化用的均值、方差非常关键。比如ImageNet风格的归一化均值通常是[0.485, 0.456, 0.406]方差是[0.229, 0.224, 0.225]一旦在ONNX Runtime下漏掉这一步模型输出的概率图就会出现明显偏移。所以在做精度对比时一定要复用Paddle推理时的预处理代码至少保证resize方式、归一化参数、通道顺序完全一致。5.2 误差的量化标准端到端工程里的误差判断不能只看是否识别出相同文字。一个比较可靠的方法是把误差拆到两个阶段分别评估检测阶段看概率图浮点误差识别阶段看logits浮点误差最后再看整个OCR结果的字符级一致率。概率图和logits用绝对误差、平均误差来衡量文本结果可以用编辑距离或字符精确率来评估。实际操作中我把ONNX结果和Paddle结果做逐字段比对OCR结果整体一致率在99%以上就认为转换精度符合上线要求。5.3 性能和稳定性观察转换后模型在ONNX Runtime上的运行时间、显存占用和稳定性和Paddle推理有差异是非常正常的。建议在上线前做一个简单的压力测试连续跑几千张图观察是否有偶发的异常输出和内存增长。ONNX Runtime在CPU上对PP-OCR系列的推理速度通常够用GPU环境下可以额外启用CUDAExecutionProvider甚至TensorRTExecutionProvider开启后性能往往还有提升。但启用加速时要注意算子支持和精度变化半精度模式和动态尺寸同时开启时容易出现数值抖动需要反复测试后才能确定参数。5.4 典型对比流程简表对比项Paddle推理ONNX Runtime推理检测输入shape[1,3,H,W]H/W动态需手动设定动态H/W检测输出概率图[1,1,H,W]概率图[1,1,H,W]识别输入shape[1,3,32,W]高度固定宽度动态识别输出logits[1,T,C][1,T,C]预处理resize标准化必须保持一致后处理DB后处理CTC解码直接复用原逻辑这张表我通常会挂在项目文档里每次做模型升级或者环境变更后就按照表里的项目逐项检查一遍避免遗漏。6. 常见问题与避坑实录6.1 版本不匹配导致转换报错我最初的转换尝试里在一个全新环境中装了最新版Paddle和paddle2onnx结果转换一开始就报解析类错误提示某个算子属性无法处理。后来查明是paddle2onnx尚未适配最新版Paddle引入的新算子属性。解决办法很简单把两个工具的版本对齐到release note中验证过的组合即可。版本对应关系是paddle2onnx使用中最容易忽视的细节建议在项目文档里单独写明验证过的转换环境版本避免团队成员更新依赖后同一个坑反复踩。6.2 动态shape和算子优化之间的冲突在某些版本组合下如果输入的所有维度都设为动态ONNX Runtime会放弃部分图优化推理速度反而下降。我的处理方式是尽量保持batch固定只让H和W动态这样既满足多尺寸输入需求又能让运行时保留大多数优化机会。如果实际业务中图片尺寸范围很窄还可以把输入做成固定尺寸并配合自适应resize卷积和循环网络的优化效果会更明显。这个经验和模型本身无关但直接影响上线后的推理延迟值得提前评估。6.3 遇到不支持的算子怎么办转换过程中最头疼的就是遇到paddle2onnx尚未映射的算子。处理思路是能拆则拆能降则降。先在模型层面看能不能把这个算子移动到后处理里如果不行就尝试把相关分支在Paddle里单独导出为ONNX再拼接成一个完整的图实在不行也可以考虑替换模型结构。比如把识别模型里的双层双向LSTM替换成能映射的GRU实现这通常会损失一点精度但多数场景下影响不大。好在PP-OCR系列的检测和识别模型使用的算子都很常规实际遇到这种极端情况不多。6.4 检测输出与原版有细微差异转换后的检测模型概率图整体差异不大但某些具体位置的数值与Paddle输出存在万分之一量级的偏差最终二值化结果偶尔会在文字边缘处多几个像素或少几个像素。这类问题通常是浮点运算顺序不同导致的属于正常现象调整二值化阈值做一点补偿就能解决。如果概率图差异超过0.01那就得按照前面讲的方法检查输入预处理了大概率是归一化参数或者resize方式不一致。6.5 ONNX Runtime的线程和性能配置ONNX Runtime默认并行线程数根据机器自动选择在部分容器环境里可能不理想。可以通过session_options.intra_op_num_threads和inter_op_num_threads手动设置。我遇到过在一个4核容器里默认线程数过高推理延迟反而变大把线程数调到2之后性能恢复正常。这是一个与模型转换本身无关却会影响最终上线效果的问题顺带提出来建议在部署阶段一并测试。6.6 官方模型与自训练模型的差异官方下载的推理模型结构比较固定转换起来很顺利自己训练的模型只要导出格式正确过程也一样。唯一要留意的是如果训练时使用了自定义的动态图Layer导出后可能残留一些自定义节点转换前需要对模型做一层梳理确保结构清晰、命名规范。paddle2onnx虽然能处理多数情况但复杂自定义算子多了之后依然可能出现无法映射的问题。遇到这种情况优先检查模型结构里是否存在自定义算子再决定是修剪计算图还是人工补充转换逻辑。这次转换让我最大的体会是模型转换这件事工具只能解决大部分问题剩下的小部分都藏在版本、shape、预处理和对齐细节里。先把环境版本固定好再把动态shape处理干净最后认真做一次精度回归转换成功率其实非常高。最后再给一个实用建议转完ONNX之后把转换命令、验证脚本和对比结果都留档保存。后期模型升级时有了一套可复现的流程再转换就只是几分钟的事情。希望这篇记录能帮到你。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑