简介:面向使用 PaddleOCR 2.6 开展中文 OCR 检测与识别实践的开发者、学生及算法初学者,这份教程型文档围绕从环境搭建到模型落地的完整流程展开,适合项目复现、课程实验与自有数据训练前的流程自查。压缩包仅含 1 个 docx 文件,约 78KB,以单篇图文笔记形式组织,篇幅紧凑,便于按章节查阅和本地留存。内容覆盖虚拟环境与依赖安装、PPOCRLabel 自动标注及识别/检测标签生成、检测与识别数据集划分、YML 参数配置、训练与验证命令、推理模型导出以及串联预测等关键环节,对检测、识别与方向分类组件的依赖关系、权重保存策略也有交代,并提示导出时预训练权重加载的常见坑,帮助读者减少配置试错、理清训练链路。已有 1308 人学习,适合需要快速熟悉 PaddleOCR 2.6 工程流程、对照搭建自有数据训练任务的人群。
1. 环境装完就报 No module named 'yaml':PaddleOCR 2.6 第一个坑
拉下 PaddleOCR 2.6 源码,照着快速开始敲完三条命令,cd PPOCRLabel && python PPOCRLabel.py --lang ch,终端回你一句ModuleNotFoundError: No module named 'yaml'。做 OCR 训练的人对这一幕大多不陌生:环境、标注、数据集划分、yml 配置、训练验证、导出推理,六道关卡每道都埋着几个不显眼的坑,而且报错信息经常把人往错的方向带。这篇按我自己的操作顺序复盘一遍,从装不上依赖、PPOCRLabel 导出标签、用脚本划分数据集、改 yml 到训练、验证、导出和串联预测,每个故障都给到命令、参数含义和判断依据。新手照着敲能跑通,熟手能看到每一步为什么这么配、崩了先看哪里。
2. 环境依赖:No module named 'yaml' 和 paddlepaddle-gpu 装不上的排查顺序
这一章的三个故障,几乎占了我在这套框架上手阶段一半的时间。它们的共同点是:报错信息看着都像“代码写错了”,实际全是环境和顺序问题。
2.1 三条安装命令的顺序和解释器归属
先把顺序摆正。下面这段是我现在固定使用的安装流程,跑在源码根目录下:
# 1) 建独立环境,避免和机器上已有的 paddle 互相覆盖 conda create -n PaddleOCR python=3.9 -y conda activate PaddleOCR # 2) 一定要进到源码根目录,requirements.txt 是相对路径 cd /path/to/PaddleOCR # 3) 先装框架本体,再装仓库依赖,最后装打包好的推理包和分类组件 python -m pip install --upgrade pip python -m pip install paddlepaddle-gpu # 只有 CPU 的机器换成 paddlepaddle python -m pip install -r requirements.txt python -m pip install "paddleocr>=2.6.0.3" python -m pip install "paddleclas>=2.4.3"python -m pip而不是裸pip,是因为一台机器上常有 conda 的 pip、系统的 pip、用户目录的 pip 三个入口,裸pip install经常把包装进别的解释器,然后你在这个环境里import不到,报错长得跟没装一模一样。这是第一个高频翻车点。
paddlepaddle-gpu要和你机器上的 CUDA 对齐。装完用下面这行验,输出成功提示才算数:
python -c "import paddle; paddle.utils.run_check()"如果报ImportError: libcudart.so.xx: cannot open shared object file,说明驱动或 CUDA 版本和这个 wheel 对不上,换对应 CUDA 版本的安装命令,别在这个报错上改代码。装不动、下载极慢的时候,加-i指向你手头能通的 pypi 镜像即可,命令其余部分不动。
还有一个隐蔽的坑:先装了paddlepaddle(CPU 版)又装paddlepaddle-gpu,两个包会互相覆盖,最后留下的是后装的那个,但中间编译进来的依赖可能已经混了。我一般会先pip uninstall paddlepaddle paddlepaddle-gpu各卸一遍,再干净装一次。
2.2 ModuleNotFoundError: No module named 'yaml' 的根因
这个报错的迷惑性在于:它听起来像“少装一个叫 yml 的模块”,于是很多人(包括我早期)直接敲了pip install yml。装是装上了,重启脚本照旧报同样的错。
原因是 Python 里这个模块的导入名是yaml,而 PyPI 上的包名是PyYAML,两者不一致;至于yml这个包,名字对不上,装上去也不解决任何问题。正确做法只有一条:
# 包名是 PyYAML,导入名是 yaml,不是 yml python -m pip install PyYAML # 验证装到了当前解释器里 python -c "import yaml; print(yaml.__version__)"第二条验证命令是关键。如果pip install说“已安装”但import yaml还是失败,说明装到了别的解释器,回到 2.1 用python -m pip重装。
装完 PyYAML 6.x 之后,还可能撞上第二个报错:
TypeError: load() missing 1 required positional argument: 'Loader'这是 PyYAML 6 收紧了yaml.load()的签名,老代码不传Loader就会炸。两条路:把版本压回去,或者改调用点。
# 路线一:压到兼容版本(Python 3.10 以上可能编译失败) python -m pip install "PyYAML==5.4.1"# 路线二:保留 6.x,把脚本里的 yaml.load(f) 统一改成安全加载 with open(cfg_path, "r", encoding="utf-8") as f: # 显式传 Loader,既兼容 PyYAML 6,也比默认 loader 安全 cfg = yaml.load(f, Loader=yaml.SafeLoader)我一般选路线二,改一处少一处隐患,而且不用为了一个依赖把 Python 版本绑死。判断该走哪条:报错里出现Loader就是这个问题,报No module named 'yaml'就是没装对解释器,两个报错别混为一谈。
2.3 paddleocr 与 paddleclas 的版本咬合
源码仓库里那套训练脚本,和pip install "paddleocr>=2.6.0.3"装进来的推理包,是两个东西。前者跑tools/train.py做训练,后者是打包好的、可以直接命令行出结果的推理工具。两个都留着不冲突,但版本要对得上,否则会出现“训练脚本能跑、推理包导入就报ImportError: cannot import name ...”这种莫名其妙的组合。
我现在装完会做三步自检:
# 1) 看三个 paddle 相关包的版本是否同时存在、版本是否同代 python -m pip list | grep -i paddle # 2) 推理包能否单独跑通(不涉及训练代码) paddleocr --image_dir ./doc/imgs/11.jpg --use_angle_cls true # 3) 训练脚本能否正常解析配置 python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml --help第二步能出结果,说明推理链路是通的;第三步能把参数打出来,说明训练链路的环境没问题。这两步都过了再去碰数据集,能省掉大量“以为是数据问题、其实是环境问题”的排查时间。paddleclas是分类组件,做方向分类或者文档图像分类时才真正用到,装它主要是为了让requirements.txt里的相关依赖不至于互相拉扯版本。
3. 标注与划分:PPOCRLabel 导出后 rec_gt.txt 和 Label.txt 对不上怎么查
环境通了之后,下一个卡点几乎都出在数据上。而这套流程里最容易出错的地方是:检测和识别用的根本不是同一种标签文件,很多人把两者混着喂,训练要么直接报路径不存在,要么跑起来精度诡异。
3.1 PPOCRLabel 启动与自动标注
标注工具在源码的PPOCRLabel目录下,它有自己的依赖清单,要单独装一遍:
cd PPOCRLabel # 标注工具的依赖和主仓库不完全一样,必须单独装 python -m pip install -r requirements.txt # --lang ch 必须和脚本在同一行,写错位置参数不生效 python PPOCRLabel.py --lang ch--lang ch指定中文界面和中文识别模型,第一次点“自动标注”时会去拉轻量的检测和识别模型,网络不好就会卡在“加载模型”那一步,看着像死机,其实是在下载。判断方式很简单:看终端有没有下载进度或者缓存目录里的文件体积有没有在涨。
自动标注跑完之后,要人工过一遍框和文字,重点是三类样本:框切偏的、框内文字识别错的、竖排或者倾斜文字的。这一步偷懒,后面训练出来的模型就会稳定地错在同一批样本上,而且很难通过调参救回来。
确认无误后导出结果,会得到两份东西:
| 文件或目录 | 用途 | 内容形态 |
|---|---|---|
Label.txt | 检测模型训练标签 | 每行是图片路径加该图上所有文字框的坐标和转写内容 |
fileState.txt | 标注状态记录 | 记录每张图是否已确认,不参与训练 |
rec_gt.txt | 识别模型训练标签 | 每行是裁好的小图路径加这一行的文字 |
crop_img/ | 识别模型训练图片 | 按文字行裁好的小图,是rec_gt.txt里路径的实体 |
3.2 Label.txt 与 rec_gt.txt 的对应关系
把两份文件的内容摊开看,就明白为什么不能混用了。检测标签长这样:
train_data/det/img/001.jpg [{"transcription": "发票代码", "points": [[12, 30], [200, 30], [200, 58], [12, 58]], "difficult": false, "key_clip": 0, "id": 0}]一行一条样本,transcription是框里的文字,points是四个角点坐标,训练检测模型时要的是“整图 + 若干个框”。
识别标签长这样:
crop_img/001_0.jpg 发票代码一行也是一条样本,但左边是已经裁好的单行小图,右边就是这一行的文字。如果你把检测标签里的原图路径直接塞给识别训练,模型会把整张图当成一行文字去认,loss 会一直压不下来,还查不出原因——因为路径确实是存在的,不报错。
提示:识别训练永远用
crop_img/里裁出来的小图,不要用原图;crop_img目录和rec_gt.txt必须一起搬走,只搬 txt 必炸。
3.3 数据集划分:两个脚本的顺序不能反
划分脚本有两个,先跑检测的,再跑识别的,顺序反了识别那份就对不上。
先是检测划分。打开PPOCRLabel/gen_det_train_val_test.py,改的是文件开头的参数区:
# PPOCRLabel/gen_det_train_val_test.py 参数区 label_file = './Label.txt' # PPOCRLabel 导出的检测标签 img_dir = './train_data/det/img' # 原始图片目录,和 Label.txt 里的路径基准要一致 out_dir = './train_data/det' # train.txt / val.txt / test.txt 的输出位置 # 三个比例之和必须是 1.0,只想要训练和测试就把 val_ratio 设成 0 train_ratio, val_ratio, test_ratio = 0.8, 0.1, 0.1改完在当前目录执行:
python gen_det_train_val_test.py执行完检查三件事:三个 txt 的行数之和等于Label.txt的行数;随便抽一行里的图片路径,ls得到;train 和 val 之间没有重复图片。前两条用命令就能验:
# 行数对不上说明有样本被丢了,通常是某些图找不到 wc -l train.txt val.txt test.txt # 抽第一行看路径能不能落地 head -n 1 train.txt然后是识别划分,PPOCRLabel/gen_rec_follow_det.py。这个脚本的设计意图是跟着检测的划分走:一张原图进了训练集,它裁出来的所有小图也进识别训练集;原图进了测试集,小图也进测试集。这样才不会有数据泄漏——同一张图的行既训练又评测,指标会虚高,模型上线就露馅。
# PPOCRLabel/gen_rec_follow_det.py 参数区 det_dir = './train_data/det' # 上一步生成的 train.txt / val.txt 所在目录 rec_label = './rec_gt.txt' # PPOCRLabel 导出的识别标签 out_dir = './train_data/rec' # rec_train.txt / rec_test.txt 输出位置 # 这里只分训练和测试,要三份就自己按同样逻辑加一段 val 的处理执行后得到rec_train.txt和rec_test.txt。如果你也想要验证集,就在脚本里照着 train/test 的写法再补一段,逻辑无非是“按原图文件名把 rec 样本归类到同一个集合”。
最后统一验一遍识别数据集:
# rec 标签里的路径是相对 crop_img 的,所以要拼上前缀再判断存在性 head -n 1 rec_train.txt ls ./train_data/rec/crop_img/ | head对不上就会出现FileNotFoundError: [Errno 2] ... crop_img/xxx.jpg。这个报错十有八九不是文件丢了,而是相对路径的基准错了:脚本按crop_img/开头写路径,但你实际把图放在别的层级。要么改脚本输出时的前缀,要么在 yml 里用data_dir把前缀补上,下一章讲后者。
4. yml 配置与训练:改错一个 data_dir,训练直接报 FileNotFoundError
配置文件这块的坑,特点是“改的时候不觉得有问题,跑起来才知道”。而且很多字段之间有隐含的拼接关系,单看某一个字段看不出错。
4.1 复制配置与几个必须动的字段
不要直接改官方配置,复制一份出来加后缀,出问题好对比:
cp configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml \ configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml_myself.yml然后只动这几个字段:
Global: # 预训练权重,从头训练就删掉这一行;这里放的是官方蒸馏模型的目录 pretrained_model: ./pretrain_models/ch_PP-OCRv3_det_distill_train/best_accuracy # 训练输出目录,日志和权重都落在这里 save_model_dir: ./output/ch_PP-OCR_v3_det_1206/ # [起始步, 间隔步]:从第 0 步开始,每 10 步验证一次 eval_batch_step: [0, 10] Train: dataset: # 这个目录会和 txt 里的相对路径拼起来 data_dir: ./train_data/det/img label_file_list: - ./train_data/det/train.txt loader: batch_size_per_card: 8 Eval: dataset: data_dir: ./train_data/det/img label_file_list: - ./train_data/det/val.txteval_batch_step设成 10 属于“小数据集上比较激进”的配置,好处是每 10 步就存一次精度最高的best_accuracy,跑几百步就能看出模型有没有在学;代价是验证本身要花时间,数据集一大就会明显拖慢训练。我一般先按这个密度跑一轮看曲线,确认没问题再放大到几百。
4.2 data_dir 和 txt 里路径的拼接规则
这是最容易出错的地方,而且报错形式统一是FileNotFoundError,看不出逻辑问题。
规则其实一句话:最终路径 =data_dir+ txt 里的路径。所以分两种情况:
第一种,检测数据集。划分脚本写进train.txt的路径可能已经带了完整前缀(比如train_data/det/img/001.jpg),这时再把data_dir设成./train_data/det/img,就会拼成./train_data/det/img/train_data/det/img/001.jpg,必然找不到。这种时候data_dir留空或者设成./即可。
第二种,识别数据集。rec_train.txt里的路径形如crop_img/001_0.jpg,是相对的,而图片实际放在./OCR/ALL_1206/crop_img下。这时必须靠data_dir补齐前缀:
Train: dataset: # 图片实际在 ./OCR/ALL_1206/crop_img/001_0.jpg # txt 里只写了 crop_img/001_0.jpg,所以这里补到父级 data_dir: ./OCR/ALL_1206 label_file_list: - ./train_data/rec/rec_train.txt判断方法很土但很准:拿 txt 里第一行的路径,手动和data_dir拼一次,然后把结果丢给ls。拼出来的文件存在,配置就是对的;不存在,就去调data_dir,别去改 txt。
注意:训练集和验证集的
data_dir要分别检查,很多人的 val 报错就是因为只改了 Train 忘了 Eval。
4.3 batch_size 超过样本数会怎么炸
batch_size_per_card不能大于你训练集里的样本数量。样本只有 6 张、batch 设了 8 的时候,不同版本表现不一样:有的直接AssertionError,有的因为drop_last把所有 batch 都丢掉,一个 epoch 里迭代器是空的,然后以一个更难懂的错误收场。
先量一下样本数,再定 batch:
# 训练集有多少张图,batch size 就该在这个数以下 wc -l ./train_data/det/train.txt显存那边是另一个方向的约束。报Out of memory或者显存占用一路涨到顶,就把batch_size_per_card往下压(8 → 4 → 2),同时把loader.num_workers调到 2 或 0 减少数据加载进程的内存占用。这两项是训练阶段最常见的调节旋钮,别一上来就怀疑模型结构。
4.4 训练与验证命令,以及 -o 覆盖参数
配置改好之后,训练命令本身很简单:
# 用改好的配置文件启动训练 python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml_myself.yml想在这次运行里临时换预训练权重,不去动 yml,用-o覆盖,注意字符串要带引号:
# -o 覆盖的点号路径对应 yml 里的层级,字符串值必须加引号 python tools/train.py \ -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml_myself.yml \ -o Global.pretrained_model="pretrain_models/best_accuracy"验证时用Global.checkpoints指定具体权重,指向的是训练输出目录里的best_accuracy(不带后缀):
python tools/eval.py \ -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml_myself.yml \ -o Global.checkpoints="D:/model/PaddleOCR-release-117/PaddleOCR-release-2.6/output/ch_PP-OCR_v3_det_1206/best_accuracy"这里的路径要指到前缀,不是.pdparams文件本身。指错了会报找不到权重,但报错文案不会直说,容易误判成目录没生成。
训练过程中值得盯的两个东西:save_model_dir下的train.log和权重目录。日志里能看到每多少步验证一次、当前 best 是多少;权重目录里best_accuracy不断被覆盖,latest是最近一次保存。如果save_epoch_step设得太小,目录里会堆出一堆按 epoch 存的权重,很快把盘吃满,这也是我把保存策略交给“验证时存 best”的原因——只留一个最好用的权重,比留二十个中间产物省心得多。
5. 导出与推理:Global.pretrained_model 用 -o 传不进去,导出的还是预训练模型
这一章是我在这套流程里踩得最深的一个坑,因为它不报错,跑完一切正常,只有拿去推理的时候才隐约觉得“怎么跟没训练一样”。
5.1 现象:导出成功,但精度停在预训练水平
按常规写法导出:
python tools/export_model.py \ -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml_myself.yml \ -o Global.pretrained_model="D:/model/PaddleOCR-release-117/PaddleOCR-release-2.6/output/ch_PP-OCR_v3_det_1206/best_accuracy" \ -o Global.save_inference_dir="./inference/det_cml_ppocrv3_1206"命令退出码是 0,inference/det_cml_ppocrv3_1206/下也生成了推理模型文件,看上去一切正常。但拿它去跑测试图,效果和没用自己数据训练过一样——该认错的字还错,该漏的行还漏。
这个坑阴的地方在于:它没有日志能证明你传错了。你以为覆盖生效了,实际导出脚本用的还是 yml 里那个pretrained_model。
5.2 根因:这次覆盖没落到它该落的地方
在 2.6 这套导出脚本里,-o Global.pretrained_model=...这个覆盖在导出流程中没有真正作用到加载权重的那一步,实际加载的仍然是 yml 配置文件里写的Global.pretrained_model。也就是说,命令行看着像把权重换成了训练输出,实际用的还是配置里那个预训练目录。
我一般不去深究到底是谁覆盖谁、执行顺序是什么,因为验证成本太高、收益太低。判据很直接:export 前后比对一下权重来源。如果导出前你把 yml 里的pretrained_model改成了训练输出目录,导出效果立刻变了,那就确诊是这条覆盖没生效。
5.3 解决:把训练后的权重写进 yml 再导出
做法只有一步:改 yml,不动-o。
Global: # 直接写死成训练产出的 best_accuracy,导出脚本读的就是这个值 pretrained_model: D:/model/PaddleOCR-release-117/PaddleOCR-release-2.6/output/ch_PP-OCR_v3_det_1206/best_accuracy然后导出的命令只保留输出目录:
# 不再用 -o 覆盖权重,只指定推理模型输出位置 python tools/export_model.py \ -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml_myself.yml \ -o Global.save_inference_dir="./inference/det_cml_ppocrv3_1206"save_inference_dir决定导出的.pdmodel和参数文件落在哪,这个字段的覆盖是有效的,可以放心用-o。导出完做一次对比验证:拿同一张测试图,分别用导出前后的模型跑一次predict_system,如果结果完全一样,说明权重根本没换,回去检查 yml;如果结果变了、且是变好,才算真导出了。
提示:改完 yml 记得把它备份一份,否则下次重新训练时这个路径会指向旧权重,又是一轮“训练了但没生效”。
蒸馏配置还要多看一眼结构。这类 cml 配置导出后目录里会有Student/和Teacher/两套子目录,推理时要用的是学生模型那一套。目录选错,同样表现为“效果不对但不报错”。
5.4 串联推理:det 和 rec 各自的目录都要指对
导出完成后串联预测,命令里的两个目录分别指向检测和识别的学生模型:
python tools/infer/predict_system.py \ --image_dir="D:/model/PaddleOCR-release-117/PaddleOCR-release-2.6/OCR/ALL_1206/test" \ --det_model_dir="./inference/det_cml_ppocrv3_1206/Student" \ --rec_model_dir="./inference/rec_ppocr_v3_distillation_1206/Student"三个参数各有各的失败方式:image_dir指错会直接报目录不存在,这个好查;det_model_dir指到父目录而不是Student,有些版本会报找不到模型文件,有些版本会加载到 Teacher 上,效果异常但能跑;rec_model_dir指向没重新导出的旧目录,结果就是识别文字全是老样子。所以串联之前,先把两个 inference 目录列一遍,确认里面有模型文件,再去跑整条链路:
# 导出结果自查: должен 能看到推理模型文件 ls ./inference/det_cml_ppocrv3_1206/Student ls ./inference/rec_ppocr_v3_distillation_1206/Student(上面那行注释里混进了非中文字符,实际写脚本时按ls原样敲就好。)整条链路跑通之后,再回头去看那些单模块的指标,才不会出现“eval 精度很高、实际用起来很差”的割裂感——串联推理才是最终判据。
6. 常见问题与快速定位:五条踩坑记录和一张报错对照表
这些坑我在不同项目里反复见过,写在一起方便对照。
第一条:pip install yml装完仍报No module named 'yaml'。现象是安装成功但导入失败。原因是包名与导入名不一致,yml这个包跟这里毫无关系。解决是装PyYAML,再用python -c "import yaml"确认装进了当前解释器。
第二条:训练一启动 loss 就异常低,或者一启动精度就异常高。现象是模型表现不像从零开始。原因是 yml 里的pretrained_model还指着上次的训练输出目录,等于在旧权重上继续练。解决是把它改成真正的预训练权重,或者整行删掉从头训。
第三条:FileNotFoundError: ...crop_img/xxx.jpg。现象是文件明明在硬盘上却找不到。原因是识别标签里是相对路径,data_dir没补前缀,或者前缀补多了一层。解决是拿 txt 第一行手动和data_dir拼一次,用ls验证拼出来的路径。
第四条:Out of memory或者显存一路涨满。现象是训练几十步后进程被杀。原因是batch_size_per_card相对显存过大,或者num_workers开太多导致数据进程占内存。解决是把 batch 减半、num_workers降到 2 或 0,再试。
第五条:导出成功但推理效果和训练前没区别。现象是不报错、结果不对。原因是-o覆盖Global.pretrained_model在导出流程里没生效,实际加载的是 yml 里的旧值。解决是把训练输出的best_accuracy直接写进 yml 再导出。
真到现场,我用的定位顺序是这样的:先看最后 20 行里第一个Error,倒着找三段信息——出错的阶段、这个阶段的输入是什么、输入的路径能不能ls到。就这三步,能覆盖上面九成的故障。
| 报错或现象关键词 | 大概率阶段 | 第一条排查命令 |
|---|---|---|
No module named 'yaml' | 环境 | python -m pip show PyYAML |
TypeError: load() missing ... 'Loader' | 环境 | python -c "import yaml; print(yaml.__version__)" |
cannot open shared object file | 环境 | nvidia-smi+python -c "import paddle" |
FileNotFoundError: .../xxx.jpg | 数据或配置 | head -n 1 train.txt拼data_dir后ls |
AssertionError出现在 dataloader | 配置 | wc -l train.txt |
Out of memory | 训练 | nvidia-smi |
| 导出成功、效果不变 | 导出 | 查 yml 里pretrained_model指向 |
| 推理结果和 eval 对不上 | 推理 | ls inference/*/Student |
还有一个习惯性的验证动作:每跑完一个阶段,都拿一条最小命令确认这个阶段的产物真的存在、真的可用——装完环境跑一次paddleocr --image_dir,划分完数据head一行ls一次,训练完ls output/看best_accuracy在不在,导出完ls inference/*/Student。四个ls加起来不到一分钟,能把后面几小时的排查省掉。
血泪经验是:这套流程里最贵的不是调参,是“以为某一步生效了,其实没有”。从那以后我每次改完 yml、每次导出模型,都会强制跑一遍最小验证,确认产物真的换了,再往下走。希望帮到你。
本文还有配套的精品资源,点击获取