1. 为什么我要自己训练OCR模型
OCR这东西,用现成的接口和开源模型跑个通用场景确实够用,但一旦落到具体业务里,通用模型的表现往往让人抓狂。我最早接触PaddleOCR是因为手头有一批工业质检报告需要批量结构化,表格线密集、字体是那种老式针式打印机的点阵字,还夹杂手写批注。拿开源预训练模型直接推理,数字“0”和“8”混淆率超过15%,小数点经常被吞掉,整个流程跑下来人工复核的工作量比手动录入还大。
后来我花了两周时间从零训练了一个针对该场景的检测+识别模型,识别准确率从82%拉到了97%以上,单张推理耗时控制在300毫秒以内。这篇文章就是把这套流程完整拆开,从环境搭建、数据标注、格式转换、配置文件修改、训练调参到推理部署,每一步都配上可运行的代码和踩坑记录。适合已经了解Python基础、用过PaddleOCR做推理但没自己训练过模型的开发者,也适合需要针对特定场景定制OCR能力的算法工程师。
PaddleOCR是百度飞桨生态下的OCR工具库,覆盖文字检测、方向分类、文字识别三个核心模块,支持中英文、数字、多语言混合场景。它的优势在于模型轻量、部署链路完整、中文场景优化到位,而且训练配置相对友好,不需要从零写训练框架。但“相对友好”不等于“无脑跑通”,数据格式、配置文件、学习率策略这几块如果没搞明白,训练出来的模型大概率还不如预训练权重。
下面我按实际项目推进的顺序来讲,先讲整体设计思路,再拆数据、配置、训练、推理四个环节,最后把常见报错和排查方法整理成速查表。
2. 整体方案设计与技术选型思路
2.1 检测+识别两阶段架构的取舍
PaddleOCR默认走的是“检测+方向分类+识别”三段式流水线。检测模型负责把图中的文字区域框出来,方向分类判断文字是否旋转180度,识别模型再把每个文本框里的内容转成字符序列。这个架构的好处是模块解耦,你可以单独替换检测或识别模型,也可以根据场景决定要不要方向分类。
我一开始想过用端到端的方案,比如直接用一个模型输出文本位置和内容,省去中间环节。但实际评估下来,端到端方案在小样本场景下收敛困难,而且标注成本更高——你需要标注文本的精确位置和内容对应关系,而两阶段方案里检测和识别可以分开标注、分开训练。对于工业质检报告这种版面固定但文字密集的场景,两阶段方案的鲁棒性明显更好。
检测模型我选的是DB(Differentiable Binarization)算法,PaddleOCR里对应的是ch_PP-OCRv4_det系列。DB的核心思路是把二值化过程做成可微的,让分割网络在训练时就能直接优化文字区域的边界。相比传统的EAST或PSENet,DB在弯曲文本和密集小字上的表现更稳,而且后处理简单,阈值一调就能控制框的松紧程度。
识别模型选的是CRNN+CTC架构,PaddleOCR里对应ch_PP-OCRv4_rec。CRNN用CNN提取特征序列,RNN建模字符间的上下文关系,CTC解决不定长序列对齐问题。这套组合在中文识别上已经非常成熟,训练时不需要字符级对齐标注,只需要整行文本的转录结果,标注成本低。
2.2 预训练权重到底要不要用
这个问题我被问过很多次。我的建议是:一定要用,但要知道怎么用。PaddleOCR提供的预训练权重是在大规模通用数据上训出来的,特征提取层已经学到了边缘、纹理、笔画这些底层特征。你用自己的数据微调时,底层特征可以冻结或者用很小的学习率,主要更新的是高层语义部分。
我试过从随机初始化开始训,同样数据量下收敛轮次多了三倍,而且最终精度还差了两个点。后来改成加载预训练权重、检测模型学习率设0.001、识别模型设0.0005,收敛速度和最终指标都明显更好。这里的关键是学习率不能设太大,否则预训练学到的特征会被冲掉,也就是所谓的“灾难性遗忘”。
2.3 数据量到底要多少才够
网上有人说几百张就能训,有人说要几万张。我的经验是:检测模型对数据量要求相对低,500到1000张标注图就能出一个可用的模型;识别模型对数据量要求高,因为你要覆盖所有可能出现的字符组合,建议至少准备5000到10000条文本行样本。
但数据量不是唯一指标,多样性更重要。我第一版识别模型只用了3000条样本,但覆盖了不同字体、不同字号、不同模糊程度的文本行,效果比后来用10000条单一字体样本训出来的还好。所以标注的时候要有意识地覆盖:正常字、加粗字、倾斜字、模糊字、断裂字、粘连字,每种情况都要有。
3. 训练环境搭建与依赖安装
3.1 硬件与系统环境选择
训练OCR模型对显存有一定要求。检测模型训练时输入图像分辨率通常设为960×960,batch size设8的话,显存占用大概在6到8GB。识别模型输入高度32、宽度320,batch size设64的话,显存占用在4到6GB。所以一张8GB显存的卡可以跑检测,但识别模型想跑大batch最好有12GB以上。
我自己的训练机是Ubuntu 20.04 + RTX 3060 12GB,CUDA 11.7,cuDNN 8.4。这个配置跑检测和识别都够用,训练一个识别模型大概需要6到8小时(10万轮左右)。如果只有CPU,训练会非常慢,不建议。Windows环境也能跑,但数据加载和编译依赖容易出问题,有条件尽量用Linux。
3.2 PaddlePaddle GPU版本安装
安装PaddlePaddle GPU版本是第一步,也是最容易卡住的一步。官方命令会根据CUDA版本和系统自动生成,但实际安装时经常遇到版本不匹配的问题。我的做法是先确认CUDA版本:
nvcc --version然后去PaddlePaddle官网找到对应版本的安装命令。以CUDA 11.7为例:
python -m pip install paddlepaddle-gpu==2.5.1.post117 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html安装完成后验证:
import paddle paddle.utils.run_check()如果输出“PaddlePaddle is installed successfully!”并且能看到GPU信息,说明安装成功。如果报错找不到CUDA库,检查LD_LIBRARY_PATH是否包含CUDA的lib64目录。
注意:不要混用conda安装的cudatoolkit和系统CUDA,版本冲突会导致训练时随机崩溃。建议统一用系统CUDA,conda环境只装Python依赖。
3.3 PaddleOCR源码克隆与依赖安装
不要直接用pip install paddleocr,那个是推理包,训练需要克隆源码:
git clone https://github.com/PaddlePaddle/PaddleOCR.git cd PaddleOCR pip install -r requirements.txtrequirements.txt里的依赖比较多,安装过程中如果遇到opencv-python编译问题,可以换成opencv-python-headless。另外注意numpy版本,PaddlePaddle 2.5.x对numpy 1.24以上版本支持有问题,建议锁定numpy==1.23.5。
安装完成后进入PaddleOCR目录,运行一个简单的推理测试:
python tools/infer/predict_system.py --image_dir="./doc/imgs/11.jpg" --det_model_dir="./ch_PP-OCRv4_det_infer/" --rec_model_dir="./ch_PP-OCRv4_rec_infer/"如果能看到识别结果输出,说明环境基本没问题。
4. 数据集制作与标注规范
4.1 检测数据标注:用PPOCRLabel还是LabelImg
检测模型的标注格式是“图像路径 + 文本框坐标”,坐标格式是四个点的x,y值,按顺时针排列。标注工具我推荐PPOCRLabel,它是PaddleOCR官方出的半自动标注工具,内置了预训练模型,可以自动预标注文本框,你只需要修正和补充。
安装PPOCRLabel:
pip install PPOCRLabel PPOCRLabel --lang ch打开后导入图片目录,点击“自动标注”,模型会先把可能的文字区域框出来。你手动调整框的位置,然后在右侧输入框里填写该区域的文本内容。注意:检测标注只需要框位置,文本内容是为识别模型准备的,但PPOCRLabel会同时保存两者,方便后续生成识别数据。
如果场景特别复杂,比如文字和背景对比度极低,自动标注效果差,那就用LabelImg手动拉框。LabelImg的标注格式是Pascal VOC XML,需要写脚本转成PaddleOCR的检测格式。转换脚本我放在下一节。
4.2 识别数据标注:文本行裁剪与转录
识别模型的标注格式是“图像路径 + 制表符 + 文本内容”,每行一条。图像是裁剪好的文本行图片,高度统一缩放到32像素,宽度按比例缩放但不超过320像素。
从检测标注生成识别数据,可以用PPOCRLabel的导出功能,它会自动裁剪文本框并生成识别标注文件。但自动裁剪有个问题:框的松紧程度会影响识别效果。框太紧会切掉笔画边缘,框太松会引入背景噪声。我的做法是导出后在裁剪时向外扩5个像素,同时做一次灰度化和对比度增强。
如果已经有检测标注文件(det_train.txt),可以用下面的脚本批量裁剪:
import cv2 import os import numpy as np def crop_text_regions(image_path, label_path, output_dir): img = cv2.imread(image_path) with open(label_path, 'r', encoding='utf-8') as f: lines = f.readlines() for idx, line in enumerate(lines): parts = line.strip().split('\t') if len(parts) < 2: continue coords = list(map(int, parts[0].split(','))) text = parts[1] pts = np.array(coords).reshape(-1, 2) x_min, y_min = pts.min(axis=0) x_max, y_max = pts.max(axis=0) pad = 5 x_min = max(0, x_min - pad) y_min = max(0, y_min - pad) x_max = min(img.shape[1], x_max + pad) y_max = min(img.shape[0], y_max + pad) crop = img[y_min:y_max, x_min:x_max] crop = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) crop = cv2.resize(crop, (320, 32)) out_name = f"{os.path.basename(image_path).split('.')[0]}_{idx}.jpg" cv2.imwrite(os.path.join(output_dir, out_name), crop)这个脚本会把每个文本框裁剪出来,统一缩放到320×32,保存为单独图片。然后生成识别标注文件:
with open('rec_train.txt', 'w', encoding='utf-8') as f: for img_name in os.listdir(output_dir): text = img_name_to_text[img_name] f.write(f"{os.path.join(output_dir, img_name)}\t{text}\n")4.3 数据增强策略:什么时候该增强,什么时候不该
PaddleOCR内置了多种数据增强,在配置文件里可以开关。我的经验是:检测模型适合用随机缩放、随机裁剪、颜色抖动;识别模型适合用随机模糊、随机噪声、轻微透视变换。但增强不是越多越好,过度增强会让模型学到错误的特征。
比如识别模型如果加了大幅度的旋转增强,模型会花大量容量去学旋转不变性,但实际场景中文本行基本都是水平的,这些容量就浪费了。我一般只开随机模糊(模拟拍摄模糊)和随机亮度调整(模拟光照变化),其他都关掉。
注意:数据增强只在训练时生效,验证和推理时一定要关掉。PaddleOCR的配置文件里
Train和Eval的transforms是分开的,别改错了地方。
5. 配置文件修改与训练参数设置
5.1 检测模型配置文件详解
检测模型的配置文件在configs/det/目录下,我基于ch_PP-OCRv4_det_student.yml修改。关键参数如下:
Global: pretrained_model: ./pretrain_models/ch_PP-OCRv4_det_train/best_accuracy save_model_dir: ./output/det_custom/ epoch_num: 500 save_epoch_step: 50 eval_batch_step: [0, 200] Train: dataset: data_dir: ./train_data/det/ label_file_list: ["./train_data/det/train.txt"] transforms: - DecodeImage: {img_mode: BGR, channel_first: false} - DetLabelEncode: {} - RandomScale: {scale_range: [0.5, 2.0]} - RandomCrop: {size: [960, 960]} - MakeShrinkMap: {min_text_size: 8, shrink_ratio: 0.4} - NormalizeImage: {scale: 1./255., mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225]} - ToCHWImage: {} loader: batch_size_per_card: 8 num_workers: 4 Optimizer: lr: learning_rate: 0.001 decay: function: cosine warmup_epoch: 5pretrained_model指向预训练权重目录,里面要有best_accuracy.pdparams文件。epoch_num设500是因为我的数据量不大(800张),需要多跑几轮。如果数据量上万,200轮就够了。
RandomScale的scale_range设[0.5, 2.0]是让模型适应不同尺度的文字。MakeShrinkMap的shrink_ratio控制文本框收缩比例,0.4是经验值,太小会导致框重叠,太大会漏检小字。
学习率用cosine衰减,warmup 5轮。检测模型的学习率可以设大一点,0.001是安全的起点。如果训练loss震荡厉害,降到0.0005。
5.2 识别模型配置文件详解
识别模型配置文件在configs/rec/目录下,基于ch_PP-OCRv4_rec.yml修改:
Global: pretrained_model: ./pretrain_models/ch_PP-OCRv4_rec_train/best_accuracy save_model_dir: ./output/rec_custom/ epoch_num: 300 character_dict_path: ./ppocr/utils/ppocr_keys_v1.txt Train: dataset: data_dir: ./train_data/rec/ label_file_list: ["./train_data/rec/train.txt"] transforms: - DecodeImage: {img_mode: BGR, channel_first: false} - RecAug: {} - CTCLabelEncode: {} - RecResizeImg: {image_shape: [3, 32, 320]} - KeepKeys: {keep_keys: ['image', 'label', 'length']} loader: batch_size_per_card: 64 num_workers: 4 Optimizer: lr: learning_rate: 0.0005 decay: function: cosine warmup_epoch: 3character_dict_path是字符字典,默认的ppocr_keys_v1.txt包含6623个中文字符、数字和标点。如果你的场景有特殊符号(比如工业符号、数学符号),需要自己扩充字典。扩充方法是在文件末尾追加字符,每行一个,然后重新训练。
RecAug是识别模型的数据增强,包含模糊、噪声、颜色抖动。如果训练loss下降很慢,可以暂时关掉,等loss稳定后再开。
RecResizeImg的image_shape是[3, 32, 320],表示输入图像高度32、宽度320。如果你的文本行特别长(比如整行发票号),可以把宽度设大一点,比如[3, 32, 640],但显存占用会翻倍。
5.3 学习率与batch size的配合关系
学习率和batch size是联动的。经验公式是:lr = base_lr * sqrt(batch_size / base_batch_size)。PaddleOCR默认检测batch size是8,识别是64。如果你显存不够要降batch size,学习率也要按比例降。
我试过检测batch size从8降到4,学习率从0.001降到0.0007,最终精度只差了0.3个点。但如果batch size降了学习率没降,训练会非常不稳定,loss经常跳变。
注意:训练初期如果loss出现NaN,先检查学习率是不是太大,再检查数据里有没有空标签或非法字符。空标签会导致CTC loss计算异常。
6. 启动训练与过程监控
6.1 检测模型训练命令与日志解读
启动检测训练:
python tools/train.py -c configs/det/ch_PP-OCRv4_det_student.yml -o Global.pretrained_model=./pretrain_models/ch_PP-OCRv4_det_train/best_accuracy训练日志会输出loss、学习率、当前epoch和评估指标。检测模型关注loss和hmean(F1分数)。正常情况loss从1.0左右开始下降,500轮后降到0.1以下,hmean从0.6升到0.9以上。
如果loss下降很慢,检查数据标注质量。我遇到过标注框严重偏移的情况,loss卡在0.8下不去,重新标注后正常。如果hmean震荡,可能是学习率太大或者batch size太小。
6.2 识别模型训练命令与准确率曲线
启动识别训练:
python tools/train.py -c configs/rec/ch_PP-OCRv4_rec.yml -o Global.pretrained_model=./pretrain_models/ch_PP-OCRv4_rec_train/best_accuracy识别模型关注acc(字符级准确率)和norm_edit_dis(编辑距离)。正常情况acc从0.5左右开始上升,300轮后到0.95以上。如果acc卡在0.8上不去,大概率是字符字典不匹配——训练数据里出现了字典里没有的字符,这些字符会被忽略,导致模型学不到。
检查方法:遍历训练标注文件,找出不在字典里的字符:
with open('ppocr/utils/ppocr_keys_v1.txt', 'r', encoding='utf-8') as f: dict_chars = set(f.read().splitlines()) with open('train_data/rec/train.txt', 'r', encoding='utf-8') as f: for line in f: text = line.strip().split('\t')[1] for ch in text: if ch not in dict_chars: print(f"未在字典中: {ch}")6.3 训练过程可视化与中断恢复
PaddleOCR支持用VisualDL可视化训练过程:
visualdl --logdir ./output/det_custom/ --port 8080浏览器打开http://localhost:8080就能看到loss和指标曲线。如果训练中断,可以从保存的checkpoint恢复:
python tools/train.py -c configs/det/ch_PP-OCRv4_det_student.yml -o Global.checkpoints=./output/det_custom/latestlatest文件记录了最新的epoch和优化器状态,恢复后从断点继续,不会从头开始。
7. 模型评估与推理部署
7.1 评估指标解读:hmean、acc、编辑距离
检测模型的hmean是precision和recall的调和平均,反映框的准确程度。hmean到0.9以上基本可用,0.95以上算优秀。如果precision高但recall低,说明漏检多,需要调低检测阈值;如果recall高但precision低,说明误检多,需要调高阈值。
识别模型的acc是字符级准确率,norm_edit_dis是归一化编辑距离。acc到0.95以上、编辑距离到0.98以上,说明识别效果很好。如果acc高但编辑距离低,说明模型在长文本上容易出错,需要增加长文本样本。
7.2 导出推理模型与预测
训练完成后导出推理模型:
python tools/export_model.py -c configs/det/ch_PP-OCRv4_det_student.yml -o Global.pretrained_model=./output/det_custom/best_accuracy Global.save_inference_dir=./inference/det_custom/识别模型同理。导出后会生成inference.pdmodel和inference.pdiparams两个文件,部署时只需要这两个文件。
预测单张图片:
python tools/infer/predict_system.py --image_dir="./test.jpg" --det_model_dir="./inference/det_custom/" --rec_model_dir="./inference/rec_custom/" --use_gpu=True如果识别结果乱码,检查字符字典路径是否和训练时一致。字典不一致会导致解码错误,输出完全无关的字符。
7.3 推理速度优化技巧
推理速度主要受输入分辨率、batch size和模型精度影响。检测模型输入从960降到640,速度提升约40%,但小字检测率会下降。识别模型batch size从1增到8,吞吐量提升约3倍,但单张延迟会增加。
如果部署在CPU上,可以用OpenVINO加速:
python tools/infer/predict_system.py --image_dir="./test.jpg" --det_model_dir="./inference/det_custom/" --rec_model_dir="./inference/rec_custom/" --use_openvino=TrueOpenVINO在Intel CPU上通常有2到3倍加速。如果部署在GPU上,可以用TensorRT,但需要额外转换步骤,适合对延迟要求极高的场景。
8. 常见问题与排查速查表
8.1 训练报错与解决方案
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
CUDA out of memory | batch size太大或图像分辨率太高 | 降低batch size或输入尺寸 |
Loss is NaN | 学习率太大或数据有空标签 | 降低学习率,检查标注文件 |
KeyError: 'xxx' | 字符字典不匹配 | 检查字典文件是否包含所有字符 |
AssertionError: image shape | 图像通道数或尺寸不对 | 检查图像是否为三通道,尺寸是否统一 |
Permission denied | 文件权限问题 | 用chmod修改权限或换目录 |
8.2 识别效果差的排查思路
识别效果差通常有三个原因:检测框不准、图像质量差、模型欠拟合。排查顺序是:先看检测框是否准确覆盖文字区域,如果框偏了,先调检测模型;如果框准但识别错,看裁剪图像是否清晰,模糊图像需要做增强;如果图像清晰但识别错,看训练数据是否覆盖了该字体或字号,补充样本重新训练。
我遇到过一个案例:模型对数字“1”和字母“l”混淆严重。原因是训练数据里数字样本太少,模型没学到区分特征。后来专门补充了2000条纯数字样本,混淆率从12%降到2%以下。
8.3 部署时的坑与规避方法
部署时最常见的问题是推理结果和训练时不一致。原因通常是预处理不一致——训练时用了Normalize,推理时忘了加;或者训练时图像是BGR,推理时用了RGB。PaddleOCR的推理脚本默认和训练配置对齐,但如果自己写推理代码,一定要对照训练配置的transforms逐项检查。
另一个坑是模型文件路径。导出推理模型后,inference.pdmodel和inference.pdiparams必须在同一目录,且文件名不能改。如果改了名,推理时会报找不到参数文件。
注意:如果部署环境没有GPU,导出模型时不要指定
--use_gpu,否则模型会绑定GPU设备,CPU环境加载失败。
9. 我个人在实际操作中的体会
这套流程我前前后后跑了不下十次,最大的体会是:数据质量决定上限,参数调优决定下限。很多人花大量时间调学习率、换优化器,但效果提升有限,回头一看标注数据里有一堆框偏了、文本标错了。我的做法是每次训练前随机抽100条样本人工检查,确认标注质量后再启动训练。
另一个体会是不要迷信预训练模型。预训练权重能加速收敛,但如果你的场景和通用场景差异极大(比如古籍竖排文字、工业点阵字),预训练模型的特征反而会成为负担。这种情况下可以先在预训练权重上微调几轮,然后解冻所有层从头训,效果可能更好。
最后分享一个小技巧:训练识别模型时,可以把检测模型输出的框稍微放大一点再裁剪,这样识别模型能看到更多上下文,对模糊字符的区分能力更强。我实测下来,框放大5%到10%能让准确率提升1到2个点,代价是推理时裁剪区域变大,速度略降。这个取舍可以根据实际场景的精度要求来定。