MiniCPM-OCR
MiniCPM-V 多模态系列
MiniCPM-OCR 1.0
用区域图片与对应转写文本适配特定语言、手写风格或文档领域。官方示例使用 Teklia/RIMES-2011-line 做法语手写文本行识别:输入文本行图片,提示词固定为 Text Recognition:,assistant 输出对应转写。
训练使用全参数 SFT,训练视觉编码器和语言模型,冻结多模态投影层,视觉 Token 压缩率为 4×。
下面两条配方都基于 tsjyma 的框架 fork,并锁定到固定提交。这个提交是训练框架的版本,不是 MiniCPM-OCR 的模型版本:主干代码缺少 4× 视觉 Token 压缩等改动,不 checkout 到对应提交无法复现。
LLaMA-Factory
环境准备
代码仓库 tsjyma/LlamaFactory,固定提交 38fa4bcbac2830c159f2baf9808d21a7fca4e790。
conda create -n "MiniCPM-OCR-French-LF" python=3.11 -y
conda activate "MiniCPM-OCR-French-LF"
pip install torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1
git clone https://github.com/tsjyma/LlamaFactory.git
cd LlamaFactory
git checkout 38fa4bcbac2830c159f2baf9808d21a7fca4e790
pip install -e .
pip install \
accelerate==1.13.0 deepspeed==0.18.8 \
peft==0.18.1 trl==0.24.0 datasets==4.0.0 \
flash-linear-attention==0.4.2 \
ninja einops safetensors tokenizers sentencepiece
pip install /path/to/transformers
export DISABLE_VERSION_CHECK=1
最后单独安装 Transformers 源码,版本需高于 5.20.2。
数据准备
使用 RIMES 官方训练、验证、测试划分:过滤含 ¤ 标注标记的训练样本,验证集与测试集只删除该分隔符并保留文本,同时保留大小写、重音符号、标点、内部空格和原始文本行图片。
| 数据集 | 样本数 |
|---|---|
| 训练集 | 10,138 |
| 验证集 | 1,138 |
| 测试集 | 778 |
将图片路径与转写文本整理为 JSONL:
{
"messages": [
{
"role": "user",
"content": "<image>\nText Recognition:"
},
{
"role": "assistant",
"content": "J'ai perdu mon emploi depuis 3 mois et je me"
}
],
"images": [
"/path/to/rimes_french/images/train/00000.jpg"
]
}
在数据目录的 dataset_info.json 中注册训练集和验证集:
{
"rimes_train": {
"file_name": "train.jsonl",
"formatting": "sharegpt",
"columns": {
"messages": "messages",
"images": "images"
},
"tags": {
"role_tag": "role",
"content_tag": "content",
"user_tag": "user",
"assistant_tag": "assistant"
}
},
"rimes_validation": {
"file_name": "validation.jsonl",
"formatting": "sharegpt",
"columns": {
"messages": "messages",
"images": "images"
},
"tags": {
"role_tag": "role",
"content_tag": "content",
"user_tag": "user",
"assistant_tag": "assistant"
}
}
}
启动训练
单机 8 × A100 80GB 训练 3 epoch:每轮 80 个更新步,共 240 步;每卡 batch size 1、梯度累积 16,全局 batch size 128。每轮用完整验证集计算 loss 并保存检查点,训练结束后取验证 loss 最低的检查点。
- 训练时追加
thinking\n\n</think>\n\n,用户提示词、图像和空思考前缀不参与 loss,答案与结束标记参与监督。 deepspeed指向的ds_zero2.json取自上游仓库的assets/minicpm-ocr/ds_zero2.json。
set -euo pipefail
export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0,1,2,3,4,5,6,7}"
export FORCE_TORCHRUN=1 NPROC_PER_NODE=8
export MASTER_ADDR=127.0.0.1 MASTER_PORT=29780
export DOWNSAMPLE_MODE=4x
export DISABLE_VERSION_CHECK=1
DOC_DIR=/path/to/ocr_doc
DATA_DIR=/path/to/rimes_french
OUTPUT_DIR=/path/to/outputs/minicpm_ocr_french_llamafactory
mkdir -p "$OUTPUT_DIR"
cat > "$OUTPUT_DIR/train.yaml" <<YAML
model_name_or_path: /path/to/MiniCPM-OCR-1.0
trust_remote_code: false
flash_attn: sdpa
use_reentrant_gc: false
stage: sft
do_train: true
finetuning_type: full
freeze_vision_tower: false
freeze_multi_modal_projector: true
dataset_dir: ${DATA_DIR}
dataset: rimes_train
template: minicpm_ocr
enable_thinking: false
train_on_prompt: false
cutoff_len: 4096
packing: false
preprocessing_num_workers: 8
dataloader_num_workers: 8
output_dir: ${OUTPUT_DIR}
logging_steps: 1
save_strategy: epoch
save_only_model: false
save_total_limit: 3
report_to: none
per_device_train_batch_size: 1
gradient_accumulation_steps: 16
learning_rate: 5.0e-06
num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_steps: 0.05
bf16: true
ddp_find_unused_parameters: true
seed: 42
data_seed: 42
eval_strategy: epoch
weight_decay: 0.1
do_eval: true
deepspeed: ${DOC_DIR}/assets/minicpm-ocr/ds_zero2.json
optim: adamw_torch_fused
adam_beta1: 0.9
adam_beta2: 0.95
adam_epsilon: 1.0e-08
max_grad_norm: 1.0
overwrite_cache: true
eval_dataset: rimes_validation
load_best_model_at_end: true
metric_for_best_model: eval_loss
greater_is_better: false
per_device_eval_batch_size: 1
YAML
llamafactory-cli train "$OUTPUT_DIR/train.yaml"
ms-swift
环境准备
代码仓库 tsjyma/ms-swift,固定提交 903565756360d81fdc16fa14b0340a6e17ace900。
conda create -n "MiniCPM-OCR-French-Swift" python=3.11 -y
conda activate "MiniCPM-OCR-French-Swift"
pip install torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1
git clone https://github.com/tsjyma/ms-swift.git
cd ms-swift
git checkout 903565756360d81fdc16fa14b0340a6e17ace900
pip install -e .
pip install \
accelerate==1.13.0 deepspeed==0.18.8 \
peft==0.18.1 trl==0.24.0 datasets==4.8.4 \
flash-linear-attention==0.4.2 \
ninja einops safetensors tokenizers sentencepiece
pip install /path/to/transformers
最后单独安装 Transformers 源码,版本需高于 5.20.2。
数据准备
RIMES 的清洗规则与样本量同 LLaMA-Factory。区别在 JSONL 里的提示词写法:ms-swift 模板会在图像占位符后补换行,因此写成 <image>Text Recognition:。
{
"messages": [
{
"role": "user",
"content": "<image>Text Recognition:"
},
{
"role": "assistant",
"content": "J'ai perdu mon emploi depuis 3 mois et je me"
}
],
"images": [
"/path/to/rimes_french/images/train/00000.jpg"
]
}
ms-swift 不需要 dataset_info.json,训练时直接把 train.jsonl 和 validation.jsonl 的路径传给命令行参数。
启动训练
训练超参与 LLaMA-Factory 一致:3 epoch、每轮 80 步、每卡 batch size 1、梯度累积 16、全局 128,Deepspeed 同样使用上游的 ds_zero2.json。
set -euo pipefail
export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0,1,2,3,4,5,6,7}"
export NPROC_PER_NODE=8
export MASTER_ADDR=127.0.0.1 MASTER_PORT=29781
export DOWNSAMPLE_MODE=4x
DOC_DIR=/path/to/ocr_doc
MODEL_PATH=/path/to/MiniCPM-OCR-1.0
TRAIN_DATA=/path/to/rimes_french/train_swift.jsonl
VAL_DATA=/path/to/rimes_french/validation_swift.jsonl
OUTPUT_DIR=/path/to/outputs/minicpm_ocr_french_swift
swift sft \
--model "$MODEL_PATH" \
--model_type minicpm_ocr \
--template minicpm_ocr \
--add_non_thinking_prefix true \
--dataset "$TRAIN_DATA" \
--split_dataset_ratio 0.0 \
--deepspeed "$DOC_DIR/assets/minicpm-ocr/ds_zero2.json" \
--tuner_type full \
--torch_dtype bfloat16 \
--freeze_vit false \
--freeze_aligner true \
--freeze_llm false \
--packing false \
--padding_free false \
--max_length 4096 \
--num_train_epochs 3.0 \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 16 \
--gradient_checkpointing true \
--vit_gradient_checkpointing true \
--gradient_checkpointing_kwargs '{"use_reentrant": false}' \
--learning_rate 5e-06 \
--lr_scheduler_type cosine \
--warmup_ratio 0.05 \
--optim adamw_torch_fused \
--adam_beta1 0.9 \
--adam_beta2 0.95 \
--adam_epsilon 1e-08 \
--weight_decay 0.1 \
--max_grad_norm 1.0 \
--logging_steps 1 \
--save_total_limit 3 \
--save_only_model false \
--load_from_cache_file false \
--dataset_num_proc 8 \
--dataloader_num_workers 8 \
--attn_impl sdpa \
--loss_scale ignore_empty_think \
--eval_strategy epoch \
--output_dir "$OUTPUT_DIR" \
--report_to none \
--seed 42 \
--data_seed 42 \
--add_version false \
--use_hf true \
--dataset_shuffle false \
--train_dataloader_shuffle true \
--val_dataset "$VAL_DATA" \
--save_strategy epoch \
--load_best_model_at_end true \
--metric_for_best_model eval_loss \
--greater_is_better false \
--per_device_eval_batch_size 1
评测结果
两条配方都选第 2 epoch 的 checkpoint-160,在 RIMES 官方测试集全部 778 行上评测。基座与微调后模型统一使用 4× 视觉 Token 压缩率、非 thinking 模式、greedy 解码、batch size 8,每行最多生成 512 tokens,提示词沿用训练时的 Text Recognition:。
CER 是字符错误率、WER 是按空格分词后的词错误率,计算均基于完整模型输出:统一 Unicode NFC、去除首尾空白,保留大小写、重音符号、标点和内部空格。
| 框架 | 模型 | RIMES CER ↓ | RIMES WER ↓ |
|---|---|---|---|
| LLaMA-Factory | MiniCPM-OCR 1.0 | 5.67% | 15.78% |
| LLaMA-Factory | 微调后 | 2.98% | 7.66% |
| ms-swift | MiniCPM-OCR 1.0 | 5.67% | 15.78% |
| ms-swift | 微调后 | 3.08% | 7.86% |
完整教程(训练曲线、验证 loss 明细、输出样例)见上游 LLaMA-Factory 教程 与 ms-swift 教程。
MiniCPM-OCR Jev
用已标注的 OCR 候选结果微调 Jev,让质量判别适配业务文档。官方配方接一个二分类序列分类头,按 8 × A100 80GB、Python 3.12、ms-swift + DeepSpeed 给出;训练和推理是两套环境,推理侧不需要 ms-swift 和 DeepSpeed。
环境准备
训练环境
conda create -n minicpm-ocr-jev python=3.12 -y
conda activate minicpm-ocr-jev
pip install torch==2.8.0 torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cu128
MAX_JOBS=32 NVCC_THREADS=4 pip install --no-build-isolation flash-attn==2.8.3
pip install \
torch==2.8.0 torchvision==0.23.0 \
transformers==5.10.4 tokenizers==0.22.2 ms-swift==4.4.0 \
deepspeed==0.18.9 accelerate==1.13.0 flash-attn==2.8.3 \
safetensors==0.8.0 pillow==11.3.0 numpy==2.5.1 \
qwen-vl-utils==0.0.14
推理 / 评测环境
pip install torch==2.8.0 torchvision==0.23.0 --index-url https://download.pytorch.org/whl/cu128
pip install \
torch==2.8.0 torchvision==0.23.0 transformers==5.10.4 \
tokenizers==0.22.2 safetensors==0.8.0 accelerate==1.13.0 \
pillow==11.3.0 numpy==2.5.1
transformers>=5.10.4 是硬性要求:更早的版本没有 Qwen3_5ForSequenceClassification 和 Qwen3VLProcessor。如果训练时 triton 编译失败,把 CUDA_HOME 指向 CUDA 安装目录。
数据格式
训练数据是(原图、OCR 文本、渲染图、标签)四元组,标签为二分类:
| 标签 | 含义 | 判定标准 |
|---|---|---|
1 | OCR 文本与图像内容一致 | 文字、公式、表格结构都对得上 |
0 | 不一致 | 出现任意一种:漏字错字、公式结构被破坏、表格行列错位等 |
标签针对整段 OCR 输出,不定位到具体字符,模型只回答「这份结果能不能用」。每行一个 JSON 对象:
{
"messages": [
{
"role": "user",
"content": "<image>\nOCR文本:\n<table><tr><td>Refs</td><td>Accuracy (%)</td></tr></table>\n<image>\n第一张图是原图,第二张图是上述 OCR 文本渲染得到的图片。\n请判断该 OCR 文本及其对应渲染图是否与原图内容一致。\n这是一个二分类任务:一致标为 1,不一致标为 0。"
}
],
"images": [
"/path/to/images/gt/case_0025318.png",
"/path/to/images/label/case_0025318.png"
],
"label": 1
}
images必须正好两个路径,顺序是[原图, 渲染图]。content里的两个<image>占位符分别对应这两张图,OCR 文本放在中间。- 末尾三句提示语一字不改,训练与推理必须使用同一套 prompt。
label是整数0或1。
准备标注数据
负样本两个来源缺一不可:
| 来源 | 做法 | 为什么需要它 |
|---|---|---|
| 真实负样本 | 取真实 OCR 模型的错误输出,标成不一致 | 错误分布与线上一致,正是 模型真正要学会分辨的那一类 |
| 合成负样本 | 对正确文本做规则扰动(改数字、破坏公式结构、挪动表格单元格),再重新渲染 | 补上真实负样本覆盖不到的错误类型;量大且便宜,标签天然正确 |
正样本使用标注为一致的真实 OCR 输出。训练前先剔掉 OCR 文本里嵌了 base64 图片的样本——这类文本长到会把图像的 token 预算挤没:
python scripts/minicpm-ocr-jev/data/filter_embedded_base64.py \
--input-dir /path/to/dataset_dir --output-dir /path/to/filtered --splits train
启动训练
环境和数据准备好后,执行以下脚本:
# run_jev_sft.sh
#!/bin/bash
set -euo pipefail
export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0,1,2,3,4,5,6,7}"
export NPROC_PER_NODE="${NPROC_PER_NODE:-8}"
# 训练侧图像 token 预算;评测和推理用原生预算,不要在那两侧设这个变量
export IMAGE_MAX_TOKEN_NUM=2048
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
MODEL_PATH="${MODEL_PATH:-/path/to/Qwen3.5-2B}"
TRAIN_DATA="${TRAIN_DATA:-/path/to/filtered/train.jsonl}"
OUTPUT_DIR="${OUTPUT_DIR:-/path/to/outputs/minicpm_ocr_jev}"
swift sft \
--model "${MODEL_PATH}" \
--dataset "${TRAIN_DATA}" \
--task_type seq_cls \
--num_labels 2 \
--problem_type single_label_classification \
--tuner_type full \
--torch_dtype bfloat16 \
--freeze_vit true \
--freeze_aligner true \
--freeze_llm false \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 1 \
--learning_rate 1e-5 \
--lr_scheduler_type cosine \
--warmup_ratio 0.05 \
--weight_decay 0.1 \
--max_grad_norm 1.0 \
--max_length 12000 \
--packing true \
--packing_length 12000 \
--packing_num_proc 8 \
--deepspeed zero3 \
--gradient_checkpointing true \
--attn_impl flash_attn \
--logging_steps 10 \
--eval_strategy no \
--save_strategy steps \
--save_steps 500 \
--save_total_limit 40 \
--save_only_model true \
--dataset_num_proc 48 \
--dataloader_num_workers 4 \
--ddp_timeout 10800 \
--seed 42 \
--output_dir "${OUTPUT_DIR}" \
--report_to tensorboard
关键参数:
--task_type seq_cls --num_labels 2 --problem_type single_label_classification把基座从生成改成打分,在最后一个 token 上接出二分类头。--packing true --max_length 12000: 两张图加一段 OCR 文本的序列很长,不开 packing 吞吐会明显下降。IMAGE_MAX_TOKEN_NUM=2048只作用于训练。评测和推理走 checkpoint 自带预处理器的原生预算,这个不一致是刻意的,不要统一。- 改卡数只需调
CUDA_VISIBLE_DEVICES和NPROC_PER_NODE;全局 batch = 卡数 × 单卡 batch × 梯度累积步数。
评测
评测在 JevOCRBench 上做。这是面向 OCR verifier 的块级评测集,每条样本是「块图 + 一条 OCR 候选文本 + 该候选的渲染图」,真值为人工标注的正确/错误,指标是 AUROC。
huggingface-cli download openbmb/JevOCRBench --repo-type dataset \
--local-dir /path/to/JevOCRBench
单卡打分并计算 AUROC:
# 1. 打分:每条出一个 prob_1
python scripts/minicpm-ocr-jev/eval/score.py \
--dataset /path/to/JevOCRBench/data/jevocrbench.jsonl \
--checkpoint /path/to/weights \
--out /path/to/predictions.jsonl
# 2. 算 AUROC:整体 + 分类别
python scripts/minicpm-ocr-jev/eval/evaluate.py \
--predictions /path/to/predictions.jsonl
已发布模型的榜单见上游 README,完整配方见 finetune_jev_zh.md。
实践建议
- 按页划分数据集,不要按样本划分——同一页上的各个区域彼此高度相关。
- 两类负样本都要留。只用合成负样本,模型会在真实 OCR 错误上失灵;只用真实负样本,又会有一批错误类型没被覆盖。
- 渲染器要冻住。换一个版本的渲染器,就等于换了一套输入分布,旧数据不能沿用,必须重测。
- 评测和推理时 prompt 要与训练时逐字节一致,并检查最后一个 token id 是否对得上。