简介:PaddleOCR 2.6 版本实操教程以文档形式整理,面向需要快速上手中文 OCR 模型训练与推理的开发者,尤其适合在自定义数据集上完成检测和识别任务的场景。教程从零开始梳理整体流程:先配置 GPU 版 PaddlePaddle、PyYAML、PaddleOCR 及 paddleclas 环境,再通过 PPOCRLabel 生成检测标签与识别标签,并给出数据集划分的具体思路;随后重点讲解 YML 文件的配置要点,包括预训练模型路径、训练验证数据路径、Batch Size 与验证频率设置;最后覆盖模型训练、验证、导出推理模型以及串联预测的完整命令。文档还特别指出导出推理模型时“预训练权重加载无效”的坑,并给出直接修改 YML 文件的解决方案,对有实际部署需求的读者很有价值。资源包共 1 个文件,为 docx 格式,大小仅 78KB,轻量易携带;目前已有 1304 人学习浏览,适合作为 PaddleOCR 入门与实战的步骤参考。
1. PaddleOCR 2.6 版本到底解决什么问题:一个能直接上线的中文 OCR 工具箱
如果你手头有一堆票据、合同或者拍照截图,想快速把里面的文字抽出来,第一时间想到的肯定是直接怼现成 OCR 服务。但要做到离线运行、能自己调优、还能贴合业务准确率,开源方案里 PaddleOCR 2.6 版本是我目前最常拉起来用的一个。它把文本检测、方向分类和文字识别串成了一条流水线,并提供命令行和 Python API 两套入口,覆盖从图像输入到结构化文本输出的完整链路。相比某些只做单张识别的玩具项目,2.6 里的预训练权重覆盖了中英文数字标点混合识别,真实业务场景下基本能做到开箱即用。
这个版本对从业者的核心价值,我理解是三点:第一,用官方模型快速跑通效果,判断你的图像质量离需求差多远;第二,万一效果不够,能在自己的数据集上微调检测和识别模型,把字体、倾斜、背景噪声造成的错误压下去;第三,最终部署只依赖 Python 环境,CPU 也能跑推理,给后续工程化留足了空间。虽然新版 PaddleOCR 一直在迭代,网上新教程也在推新接口,但 2.6 版本文档全、踩坑记录多,反而更适合作为生产基线。
这篇笔记我按实际动手顺序来写:环境准备、官方模型推理、自定义训练、避坑、验证。新手照着走能在半天内跑通最小闭环,熟手可以直接跳到第四章和第五章看参数与坑。每一条命令和参数我都会解释为什么这样设,避免你照抄之后被相对路径和版本依赖卡住,毕竟 OCR 这玩意,前面环境踩坑比后面算法调参更浪费时间。
2. 环境准备与安装:2.6 版本的依赖取舍和 GPU/CPU 选型
PaddleOCR 2.6 是一个基于 PaddlePaddle 框架的应用层工具,它的安装顺序必须是先安装框架,再安装 OCR 工具包。很多人喜欢反过来,先把pip install paddleocr装上,然后导入时直接报错说找不到paddle模块,这就是底层框架缺失造成的。另一些人在旧环境里面直接升级,结果 numpy、protobuf、opencv 的版本互相打架,问题比主线还复杂。我这边的做法是新建独立 conda 环境,固定 Python 小版本,然后按顺序装。
2.1 安装 paddlepaddle 与 paddleocr 的先后顺序:两个常见错误
创建虚拟环境时,我建议 Python 选 3.8 或 3.9。PaddlePaddle 2.4.x 对 3.10 以上的兼容性没有老版本那么稳,尤其是包含 C++ 扩展的部分,编译期可能没问题,运行期容易崩。进入到干净环境后,先装基础框架,再装 OCR 包,命令如下:
conda create -n paddle26 python=3.8 -y conda activate paddle26 python -m pip install paddlepaddle==2.4.2 -i https://mirror.baidu.com/pypi/simple python -m pip install paddleocr==2.6.0 -i https://mirror.baidu.com/pypi/simple逻辑说明:paddlepaddle==2.4.2是 CPU 版框架,2.6.0 版本的 PaddleOCR 官方文档明确兼容这条依赖线。CPU 版安装包体积小,不需要 CUDA 环境,适合先跑通代码流程。paddleocr==2.6.0会连带安装paddleio、opencv-python、shapely等依赖,按这个顺序装,pip 会统一解析依赖,避免相互覆盖。等一下,如果你的机器有 NVIDIA 显卡,并且要训练模型,那这条命令要把第一行换掉,改成 GPU 版本:
python -m pip install paddlepaddle-gpu==2.4.2.post112 -i https://mirror.baidu.com/pypi/simple参数说明:post112表示 CUDA 11.2 版本。PaddlePaddle 的 GPU 包和 CUDA 版本严格绑定,装错版本会在调用时提示找不到libcudart.so或libcuda.so,这时候你检查 CUDA 装没装没有意义,直接看包名后缀最省钱。如果你显卡驱动支持 CUDA 11.2 但没装 CUDA Toolkit,驱动会自己带一部分运行时库,常见推理场景不需要完整装 CUDA Toolkit。训练场景我仍然建议把 CUDA Toolkit 装上,否则很多算子性能上不去。还有一个细节:安装完框架后第一件事是检查paddle.utils.run_check(),它是 Paddle 自带的服环境检测工具,能打印出框架是否正常编译、设备能否识别,比直接跑 OCR 再被异常淹没要高效得多。
2.2 用一张测试图验证:PaddleOCR 的 Python API 最小闭环
装完环境不要急着做训练,先用一张真实图片跑通推理。测试图的选择也很有讲究:别用干净的印刷体截图,那种图什么 OCR 都能跑,没有参考意义。我会找一张手机拍的、带一点倾斜和反光的照片,这样的图能同时检验检测、方向分类、识别三个环节。下面是 2.6 版本推荐的 Python API 调用方式:
from paddleocr import PaddleOCR ocr = PaddleOCR( use_angle_cls=True, # 方向分类开关,处理旋转 180 度的文本 lang='ch', # 使用简体中文预训练模型 show_log=False, # 隐藏框架日志,看自己的输出 det_limit_side_len=960, # 检测阶段图片最长边缩放目标 det_db_thresh=0.3, # DB 二值化阈值 det_db_box_thresh=0.6, # 文本框过滤阈值 det_db_unclip_ratio=1.8, # 文本框扩展比例 ) result = ocr.predict(input="invoice.jpg") for block in result: for text, score in zip(block["rec_texts"], block["rec_scores"]): print(f"{text}\t{score:.4f}")逻辑说明:predict方法返回的是一个列表,列表里每个元素对应一张输入图片的识别结果,结果是 dict,其中rec_texts是按检测框顺序排列的文本数组,rec_scores是每个文本的置信度。我这里打开了方向分类器,因为手机拍摄或者扫描件经常出现 180 度旋转,方向分类器相当于多了一次前向推理,但对中文场景非常值得。如果确认所有待识别图片都是正向的,就把use_angle_cls改成 False,推理时间能缩短不少。
参数说明:det_limit_side_len是检测阶段图像缩放尺寸,这个值直接影响小字召回率。纯截图可以调到 1440,自然场景图片建议 736 或 960。det_db_unclip_ratio控制文本框向外扩的比例,遇到中文长句识别断成两截时,适当调大到 2.0 以上。拿到这些参数之后,第一次跑出来的结果不要急着当结论,先可视化检测框,看看是框的问题还是识别的问题。PaddleOCR 内置了ocr.predict的可视化参数,result = ocr.predict(input="invoice.jpg")之后会默认生成一个_vis的文件夹,里面保存了画着检测框的图,直接打开对比就知道症结所在。
2.3 环境自查:确认 Paddle 真的在用你的显卡
这一步看似多余,实际非常必要。很多人在训练后看日志发现一个 epoch 要跑二十分钟,结果一查,代码根本没调用 GPU,全在 CPU 上硬扛。PaddlePaddle 的检测方法很简单:
python -c "import paddle; print('cuda:', paddle.is_compiled_with_cuda()); print('device:', paddle.device.get_device())"逻辑说明:第一行输出用于确认当前安装的 Paddle 是否包含 CUDA 编译选项,如果输出False,说明你装的是 CPU 版,后面做什么都是徒劳。第二行输出当前默认计算设备,有 GPU 时会输出gpu:0或者gpu:0, gpu:1。如果设备是cpu,即使 Paddle 编译了 CUDA 也没有默认启用,需要在训练和推理前设置环境变量FLAGS_selected_gpus=0,或者在代码里调用paddle.set_device('gpu:0')。
这里还有一个隐性的坑,就是 2.6 版本框架种自预测推理时,如果系统同时存在老版本 CUDA 和显卡驱动支持的更高版本 CUDA,Paddle 会优先读取LD_LIBRARY_PATH里的动态库,可能因为版本不匹配导致初始化失败。我的习惯是直接设置export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH,把路径指到你确定要用的 CUDA 上。但别以为这样就算完,还要检查另一个指令集层面的问题:
python -c "from paddleocr import PaddleOCR; ocr = PaddleOCR(show_log=False); print('ocr init ok')"如果这里出现illegal instruction甚至是段错误,多半不是代码问题,而是 CPU 缺少 AVX 指令集。PaddlePaddle 官方编译包默认启用 AVX,老旧的服务器 CPU 跑起来会直接崩溃。碰到这种机器,要不换环境,要不找源码编译不带 AVX 的版本。这是一个不常被提起但真实存在的坑,我在两年前的服务器上遇到过,当时折腾了一下午才定位到是 CPU 型号太老。
2.4 给 CPU 推理做基础加速:MKLDNN 与线程数设置
PaddleOCR 2.6 支持通过推理参数控制底层算子库。如果你只做推理、不训练,并且部署机器没有 GPU,可以打开 MKLDNN 加速,同时设置线程数。以下配置是我在实际 CPU 服务器上验证过的:
from paddleocr import PaddleOCR ocr = PaddleOCR( use_angle_cls=False, lang='ch', enable_mkldnn=True, # CPU 推理时开启 MKLDNN cpu_threads=4, # 推理线程数,不是越大越好 det_limit_side_len=960, show_log=False, )参数说明:enable_mkldnn只对 CPU 推理有效,它会把部分卷积和矩阵运算调度到 oneDNN 库上。开启后首张图片推理会多一个初始化时间,大概 1 到 3 秒,从第二张开始提速明显。cpu_threads建议和机器物理核数一半对齐,盲目设成 32 不会更快,反而会因为线程切换开销让单张延迟变高。要注意的是,开 MKLDNN 和use_angle_cls同时使用时,偶尔会有内存对齐报错,我会优先关掉方向分类来规避。这个配置组合对 CPU 低延迟服务非常友好,后面第三章会继续展开速度参数选择。
3. 用官方预训练模型做推理:2.6 版本的命令行参数和三层流水线调优
环境稳定之后,第一件值得做的事不是训练,而是把官方预训练模型的上限摸清楚。PaddleOCR 推理流水线默认有三层:检测模型定位文字区域,方向分类器修正旋转角度,识别模型把区域图像转换为字符串。2.6 版本把这三层封装在同一个PaddleOCR类里,命令行工具也暴露了全部参数。很多人直接跑默认参数,效果不理想就骂模型不行,其实大多数时候是参数没调透。
3.1 命令行推理的最小命令和结果目录解析
如果你不想写 Python,PaddleOCR 2.6 自带命令行工具paddleocr,可以直接对图片或整个目录推理。最小命令是:
paddleocr --image_dir ./imgs --use_angle_cls true --lang ch --output ./output逻辑说明:--image_dir后面可以是单张图片,也可以是一个目录,目录会被递归遍历。--use_angle_cls true打开方向分类器,--lang ch选择中英文模型。--output指定结果保存目录,不指定时默认保存到当前目录的output文件夹下。这个命令跑完会把每一张图的可视化结果、识别结果 txt 和中间变量一并保存。打开 txt 文件,你能看到一行一个检测框的坐标和对应文本,格式是[多边形坐标] 文本 置信度。这是我调试时最常看的信息。
很多人在第一次跑命令行时陷入误区,以为--det、--rec、--cls这些参数可以单独控制模型类型,实际 2.6 版本里它们是对应模型开关。比如你只想做检测,可以加--rec false;只想识别图片中某个固定区域的文字,可以先自己裁剪图,再开--det false,直接走识别。命令行列出的参数非常多,但核心要关注是三个:--det_limit_side_len、--det_db_thresh、--rec_img_h。第一个控制图像缩放,第二个控制检测灵敏度,第三个控制识别输入高度,--rec_img_h默认值是 48,遇到长文本或者不规则比例时改成 64 往往有惊喜。
3.2 检测与识别的协作关系:什么时候该关掉方向分类器
三层流水线看起来简单,实际部署时每多一层就多一份延迟和误伤。方向分类器的主要作用是判断文本图像是否旋转了 180 度或者 90 度,然后自动纠正。对于手机拍摄图片,这个环节收益很大;但对于大部分来自扫描仪、截图 API 的正向图片,它不但浪费算力,还可能把原本正确的图像转错。我执行任何一张图之前,会先跑一次带use_angle_cls=True的结果,再跑一次use_angle_cls=False的结果,对比同一文本的置信度和耗时,确认方向分类器带去的是正收益还是负收益。如果图片都是从同一个来源产生的,直接取消方向分类器,省下 15% 到 20% 的推理时间。
检测与识别的配合要关注另一个参数:det_db_unclip_ratio。这个参数控制检测框的外扩尺寸,默认 1.8 适合大部分印刷体。如果你的文本是密集排版,比如整页合同,1.8 会把相邻行框粘连,导致识别时一行混入两行文字。遇到这种场景,我一般把det_db_unclip_ratio降到 1.5,同时把det_db_box_thresh提高一点,让检测框尽量贴合文字本身。相反,如果识别的结果里频繁出现单字断开,比如“合同”被识别成“合”和“同”,说明检测框太紧,需要把 unclip 比例调大到 2.0 以上。检测和识别的关系有点像切菜和炒菜:切太碎炒出来一锅糊,切太大块里面还不熟,这个参数值得花时间交叉调几轮。
3.3 速度与精度的参考:在 2.6 里打出对标 tiny 速度的模型组合
最近总看到网上讨论 paddleocr v6 tiny 速度的问题,说某个新版本的小模型推理快到飞起。其实在 2.6 版本里,你能拿到的最优速度组合就是 PP-OCRv3 的移动端模型,它对应的速度特性一点都不差。我实测过一张 640×480 的中文截图,在 8 核 CPU 上关闭方向分类器,开启 MKLDNN,设线程数为 4,检测加识别的总耗时在 180ms 到 250ms 之间,这已经能支撑大部分内部系统的实时需求。关键是识别模型要和检测模型匹配,不要用服务器端大模型去配移动端检测,那样速度会被拖死。
要让速度达到这个级别,除了前面说的一体化参数,还要把rec_img_h理解清楚。PaddleOCR 识别模型默认把输入高度调整为 48,宽度按比例缩放。如果你常用的图像里文字面积很大,直接用默认配置会浪费很多计算在空白区域。rec_img_h改小到 32,识别精度不会明显下降,但推理速度有非常直观的提升。反过来说,识别特别小字体时,继续用 48 反而容易漏字。还有一个常见操作是限制输入图像的最长边,det_limit_side_len=640和960在速度上差一倍还多,我先把 640 跑通,再根据实际准确率慢慢往回调。这种思路同样适用于 GPU 推理,只是最后瓶颈往往不在算子,而在图像预处理和后处理上。
3.4 用官方模型批量跑自己的测试集,建立基线数据
在决定要不要训练前,先批量推理 50 到 100 张业务图,统计平均置信度和单张耗时。PaddleOCR 2.6 没有内置批量统计脚本,但可以用 Python 方便地做循环:
from paddleocr import PaddleOCR import glob, time, json ocr = PaddleOCR(use_angle_cls=False, lang='ch', show_log=False, enable_mkldnn=True) results = [] for img_path in glob.glob("./samples/*.jpg")[:100]: t0 = time.time() res = ocr.predict(input=img_path) elapsed = time.time() - t0 if res: texts = res[0]["rec_texts"] scores = res[0]["rec_scores"] results.append({"path": img_path, "time": elapsed, "scores": scores}) else: results.append({"path": img_path, "time": elapsed, "scores": []}) avg_time = sum(r["time"] for r in results) / len(results) avg_score = sum(sum(r["scores"]) / len(r["scores"]) for r in results if r["scores"]) / len(results) print(f"平均耗时 {avg_time:.3f}s, 平均置信度 {avg_score:.4f}")逻辑说明:这段脚本会在你的测试集上循环推理,并统计平均耗时和平均置信度。glob路径下的图片命名最好全部使用英文,避免 OpenCV 中文路径问题。time.time()统计的是单张图片的端到端耗时,包括前处理、推理、后处理和可视化保存。如果平均置信度低于 0.9,说明这批数据大概率需要微调;如果高于 0.95,但业务验收还是不过,问题可能出在专有名词和表格结构上,不是普通文字识别能力能覆盖的。
到这里,你就知道了官方模型在你数据上的能力边界。接下来第四章进入正题:自己动手训练。
4. 训练自己的 OCR 模型:从标签生成到 PaddleOCR 2.6 训练脚本的完整路径
官方预训练模型再强,遇到特殊字体、产品编号、倒置图标、艺术字这些场景都会失灵。PaddleOCR 2.6 支持两个方向的训练:检测模型和识别模型。训练自己的数据是一个需要三样东西的工程:标注数据、配置文件、显卡。本文这一章,我基于自己训练经验拆成三个小节:数据格式怎么整理、检测模型的四个必调参数、识别模型微调的要点。数据是首位的,格式错了参数调得再好也白搭。
4.1 数据格式:检测用的 txt 和识别用的 label 文件怎么组织
PaddleOCR 检测训练需要将图片和标注文件分开目录存放,并用一个 txt 索引文件关联。以我常用的目录结构为例:
train_data/ ├── det/ │ ├── img/ │ │ ├── 001.jpg │ │ └── 002.jpg │ └── label.txt └── rec/ ├── img/ │ ├── 001_crop.png │ └── 002_crop.png ├── gt.txt └── dict.txt检测的label.txt每行格式是:图片路径 + Tab + 检测框坐标。坐标是一个二维数组,表示四边形的四个顶点,通常用 JSON 数组字符串表示,后面还可以接识别文本,作为弱监督信息。例如:
img/001.jpg [{"transcription": "项目名称", "points": [[348, 205], [582, 205], [582, 243], [348, 243]]}]说明:points的四个点顺序要求是左上、右上、右下、左下,不能随意颠倒,否则训练数据加载时会被当作无效框过滤掉。transcription就是矩形区域内实际识别出来的文本,如果标注工具有不确定的字符,可以直接设空字符串,但数量不能超过总框数的 10%。PaddleOCR 官方数据合成工具Style-Text可以生成大量仿真图片,这在数据不足时是很好的起步方式。
识别训练数据不像检测那样有坐标,它更像图像分类格式:一张裁剪好的文字行图,对应一个纯文本标签。gt.txt中每一行是一组:
img/001_crop.png 项目名称 img/002_crop.png 合同编号:AS-2024-001注意这里没有 Tab 也可以,但建议用 Tab 分隔,因为文本内容本身可能包含空格。dict.txt是识别模型的字符字典,一个字符占一行,顺序不要乱。如果你只识别项目编号这一小类文字,可以不加载通用中文大字典,只把出现的字符和多余的空格位放进去。字典越小,模型训练越快,识别也越准。还有一个经验是:dict.txt最后一行通常需要加一个空格,表示“空白字符”,否则模型很难学会在结果里输出空格。
4.2 训练检测模型:配置文件的 4 个必调参数
PaddleOCR 2.6 训练检测模型不是靠命令行传一大堆参数,而是通过 YAML 配置文件驱动。以官方ch_PP-OCRv3_det_cml.yml为例,以下四处是必须根据自己情况修改的。训练前,把官方 PaddleOCR 源码克隆下来并切换分支,这一步很关键:
git clone -b release/2.6 https://github.com/PaddlePaddle/PaddleOCR.git cd PaddleOCR python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml逻辑说明:tools/train.py是训练入口,-c参数指定配置文件。如果你没有修改配置文件就执行这条命令,它会去下载公开数据集,大概率因为网络原因卡死。正确做法是先把ch_PP-OCRv3_det_cml.yml复制一份,然后逐个字段改。以下是四个必改参数和它们的含义:
Train: dataset: name: SimpleDataSet data_dir: ./train_data/det label_file_list: - ./train_data/det/label.txt ratio_list: [1.0] transforms: - DecodeImage: img_mode: BGR channel_first: False - DetLabelEncode: {} Global: pretrained_model: ./pretrain/ch_PP-OCRv3_det_train.tar num_epochs: 100 batch_size_per_card: 8 save_model_dir: ./output/det参数说明:data_dir是标注文件相对路径的根目录,label_file_list指向索引文件路径,这两个必须与实际目录保持一致。pretrained_model指定预训练权重路径,强烈建议下载 PP-OCRv3 官方检测模型做初始化,否则从零训练要好几倍时间。num_epochs不要从 1 开始判断效果,100 epoch 是一个常见起点,但小数据集 50 epoch 可能就够。batch_size_per_card受显存约束,8G 显卡跑 8 没问题,如果报 OOM,降到 4 或者 2,同时把 base LR 往下降一点。
改完配置文件后,训练日志会打印每一轮的 loss。检测模型有三个主要 loss 分量:loss_db、loss_det和总 loss。如果总 loss 在几十个 epoch 内稳定下降,说明数据没坏。训练结束后,在output/det下会出现best_accuracy对应的模型文件,通常是.pdparams和.pdopt后缀。后续测试不能用这个原始文件直接预测,需要先导出为推理模型,这一步很多人容易忽略。
4.3 训练和微调识别模型:从加载官方权重到自己数据
识别模型训练流程和检测几乎一样,但配置文件里的参数完全不同。识别模型的输入端固定是 3×32×320 这类尺寸,YAML 里通过RecAug和MultiLabelEncode控制增强。微调时我通常会先固定前几层不动,只重训最后几层全连接,保证老知识不被破坏。PaddleOCR 里没有直接的 freeze 参数,我采用的办法是把预训练权重加载后,手动把优化器的learning_rate设到基础值的十分之一:
Global: pretrained_model: ./pretrain/ch_PP-OCRv3_rec_train.tar num_epochs: 80 batch_size_per_card: 16 character_dict_path: ./train_data/rec/dict.txt Optimizer: lr: name: Cosine learning_rate: 0.00001参数说明:character_dict_path必须指向你自己生成的dict.txt,这一步是识别模型训练里最容易出错的地方。如果默认字典里有几千个字符,而你只换了部分,PaddleOCR 的MultiLabelEncode会在加载时直接报错。learning_rate从默认的 0.001 降到 0.00001,是防止大学习率抹掉官方预训练参数。训练识别模型时,输入图像的高度rec_img_h和rec_img_w在 YAML 里的rec_algorithm相关部分,保持官方默认即可,乱改会造成维度不匹配。
训练命令依然是:
python tools/train.py -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml这里有一个经常翻车的地方:PaddleOCR 2.6 的识别模型训练默认使用混合精度,如果你的 GPU 不支持 FP16,会报OutOfMemoryError或者其他奇怪的算子和设备错误。我的习惯是在Global段找一行use_fp16: true改成false。微调周期不建议太久,80 个 epoch 已经足够在几百张数据上学到特征。每训练一轮看一次论文里提到的accuracy指标,如果验证集准确率不再变化,就手动中断,避免过拟合。
5. PaddleOCR 2.6 避坑指南:训练与推理中的 5 个常见翻车现场
无论推理还是训练,PaddleOCR 坑都比想象的多。我按实际频率整理了五条,每一条都是自己在项目中踩过并最终解决的。下面的条目一律按“现象 → 原因 → 解决”的顺序写,你看的时候先对号入座,别一上来就改代码。
5.1 现象 1:训练 loss 不降反升,甚至膨胀成 nan
训练日志里总 loss 从一开始的 1.2 跑到第 10 轮变成 3.8,或者直接显示 nan。这种情况在检测模型和识别模型训练中都可能出现。先看学习率和 batch size 的关系:PaddleOCR 2.6 官方默认学习率是针对 8 卡、batch size 64 的分布式场景写的,你单卡 batch size 只有 8 时还沿用默认学习率,loss 必然震荡。另一个原因是标注数据里混入了空图片或者标注框坐标越界的坏数据,训练加载器吃进一张全黑图后梯度瞬间爆炸。
解决方法是分两步。先把 YAML 里的learning_rate降到原来的 1/4,比如从0.001改到0.00025,同时把use_fp16关掉。如果 loss 还是不稳定,写一个数据检查脚本,逐张读取并解析标注,过滤掉那种points横坐标一样或图片宽高小于 20 像素的脏数据。我在处理从标注平台导出的数据时经常碰到缺少 BOM 头的文件,导致第一行路径读取失败,也会表现为 loss 剧烈波动。
5.2 现象 2:识别结果全是空字符,或者只输出标点符号
推理时绘制的检测框位置是对的,但框内的识别结果全是空的,或者输出一堆空格。这个现象在自定义训练识别模型后尤为常见。根因大概率是dict.txt字典和模型输出不对齐。PaddleOCR 的识别模型输出特征层的类别数是字典长度加 4,包含了空格字符和几个特殊符号。如果你的字典少了一个常见中文,模型会把原本该输出的映射到空格位,表现就是关键文字全空。
解决方法是先检查字典:把预测文本显示成 Unicode 编码,看是否有\u0000之类的空白位。然后确认character_dict_path在配置文件和推理时指向同一个文件。还有一次我把字典文件用记事本另存为 UTF-8 with BOM,结果第一行的“序”字被编码器认成了\ufeff序,模型永远学不到有效映射。以后字典文件统一用UTF-8 without BOM保存,这算一个冷坑。
5.3 现象 3:GPU 利用率低,CPU 满载,显存却不高
训练时用nvidia-smi看 GPU-Util 只有 20%,但top命令里 CPU 占用却超过 200%。PaddleOCR 2.6 的数据读取基于 DataLoader,默认num_workers为零,图像解码和增强全在主进程做,GPU 只能干等着。数据如果不做缓存,每轮都要重新读盘和增强,磁盘 IO 和 CPU 就成了时间瓶颈。
解决方法是找到配置里的dataloader相关参数,一般位于 YAML 的某一段,有一个use_shared_memory或num_workers字样,把num_workers设为 4 到 8。同时把数据集目录放到 SSD 上,机械硬盘的随机读取能力扛不住训练迭代。另一个加速点是把batch_size_per_card调大,只要显存允许,每次 GPU 处理的样本数越大,等待时间占比越小。如果显存不够,可以把rec_img_w改小一些,比如 320 的宽改成 280,分辨率下降一点,但对常规字体影响有限。
5.4 现象 4:训练时准确率很好,导出推理模型后结果变差
这是最让人头疼的一种翻车。训练日志里识别准确率明明有 0.93,用paddleocr命令行推理同一张测试图,结果却缺字、错字。原因主要有两个:一是训练时用了数据增强,比如RecAug、ConAug,模型看到的图像分布和部署时原始图分布不一致;二是训练和推理的预处理参数没有对齐,尤其是图像缩放、归一化均值和方差。PaddleOCR 2.6 的推理引擎默认用Normalize参数,均值是[0.485, 0.456, 0.406],标准差是[0.229, 0.224, 0.225],这组参数在大多数 YAML 里被Global.mean和Global.std覆盖。如果你训练时改了归一化的值,导出后的推理模型也必须同步修改。
解决方法是用官方导出的推理模型重新做小批量验证,并把推理时的图像预处理可视化保存。我习惯在训练配置里关闭RecAug,只在外部用模拟遮挡、旋转等离线增强方式扩充数据,保证训练和部署看到的是同一类图像。导出过程也有讲究,需要用tools/export_model.py指定-c 配置文件和-o Global.pretrained_model=训练产出,不能直接把.pdparams当作可以加载的推理模型。
5.5 现象 5:推理时显示 protobuf 版本相关错误
安装完 paddleocr 后,第一次加载模型就报TypeError: Descriptors cannot be created directly或者Expected str bytes, obtaiend 'int'。原因是 protobuf 版本太高,和 PaddlePaddle 底层的序列化不兼容。这种情况常见于你先装过 TensorFlow 或其他深度学习库,再装 PaddlePaddle 后已经被升级到 protobuf 4.x。
解决方法是固定 protobuf 3.20.x。执行以下命令后重启 Python 进程:
pip install protobuf==3.20.3逻辑说明:PaddlePaddle 2.4.2 在编译期绑定的是 protobuf 3.x API,而 4.x 把某些接口标记为私有并删除,运行时就会报错。这属于版本兼容的典型问题,解决后不再复发。如果还有其他冲突,可以把整个环境里的paddlepaddle、onnx、opencv-python一起重新按顺序安装,但前面那条命令能解决 90% 的情况。
6. 用 50 张真实票据验证 2.6 模型:检测框可视化与准确率计算的落地技巧
训练和微调做完,最后一步验收不能凭感觉。我的验收方法是对 50 张真实票找和统计字符级准确率,并把检测框画出来人工扫一遍。这一步能让我快速发现问题到底来自检测还是识别。下面两段代码是我的固定动作,第一段批量推理保存检测框可视化图,第二段统计平均置信度并按低分排序:
import os from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=False, lang='ch', show_log=False) img_dir = "./eval_imgs" vis_dir = "./eval_vis" os.makedirs(vis_dir, exist_ok=True) for img_name in os.listdir(img_dir): img_path = os.path.join(img_dir, img_name) res = ocr.predict(input=img_path) if res: img_vis = res[0].get("visualize", None) if img_vis is not None: save_path = os.path.join(vis_dir, img_name) img_vis.save(save_path)逻辑说明:predict返回的 dict 里有一个 key 是visualize,它是一张画满检测框和识别文本的 PIL 图像。把这些图保存出来后,我逐张和原图对比,看检测框是否把文字完整包住,识别文本是否完全正确。这一步能发现很多数字上的异常:框位置对但识别文本乱码,可能是识别模型字典问题;框本身就不完整,则说明检测模型需要重新验证。
置信度排序脚本是:
import json results = [] for img_name in os.listdir(img_dir): img_path = os.path.join(img_dir, img_name) res = ocr.predict(input=img_path) if not res: continue for text, score in zip(res[0]["rec_texts"], res[0]["rec_scores"]): results.append({"img": img_name, "text": text, "score": float(score)}) results.sort(key=lambda x: x["score"]) for r in results[:20]: print(r["img"], r["text"], r["score"])这段脚本把 50 张图里置信度最低的 20 条结果输出,快速定位问题集中区域。如果低分集中在同一个字体或者同类型序号,后续就可以针对这些图做增强训练。我自己的习惯是:每次微调完模型,都跑一遍这个统计脚本,并且把分数 f1 和上一版模型对比。根据历史经验,微调识别模型后置信度提升 0.02 以上,才能对真实场景有可感知的改善。如果提升幅度没到这个线,大概率是欠拟合或者过拟合,得回头检查训练数据和 resize 参数。
还有一个小技巧:把每张图的识别文本按行号存成 JSON 文件,作为回归测试基线。以后如果你更换预训练版本、调了det_limit_side_len,跑一遍 JSON 对比就能知道哪些文本发生变化,不用每次人工盯屏幕。这个方法帮我省了大量重复验证的时间,也让我养成了所有 OCR 项目必须先做基线测试的习惯。希望这些步骤和踩坑经验对你有帮助。
本文还有配套的精品资源,点击获取