跳到主要内容
返回

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。

Bash
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:

JSON
{
"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 中注册训练集和验证集:

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。
Bash
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。

Bash
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:。

JSON
{
"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。

Bash
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-FactoryMiniCPM-OCR 1.05.67%15.78%
LLaMA-Factory微调后2.98%7.66%
ms-swiftMiniCPM-OCR 1.05.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。

环境准备​

训练环境

Bash
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

推理 / 评测环境

Bash
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 文本、渲染图、标签)四元组,标签为二分类:

标签含义判定标准
1OCR 文本与图像内容一致文字、公式、表格结构都对得上
0不一致出现任意一种:漏字错字、公式结构被破坏、表格行列错位等

标签针对整段 OCR 输出,不定位到具体字符,模型只回答「这份结果能不能用」。每行一个 JSON 对象:

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 预算挤没:

Bash
python scripts/minicpm-ocr-jev/data/filter_embedded_base64.py \
--input-dir /path/to/dataset_dir --output-dir /path/to/filtered --splits train

启动训练​

环境和数据准备好后,执行以下脚本:

Bash
# 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。

Bash
huggingface-cli download openbmb/JevOCRBench --repo-type dataset \
--local-dir /path/to/JevOCRBench

单卡打分并计算 AUROC:

Bash
# 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。

实践建议​

  1. 按页划分数据集,不要按样本划分——同一页上的各个区域彼此高度相关。
  2. 两类负样本都要留。只用合成负样本,模型会在真实 OCR 错误上失灵;只用真实负样本,又会有一批错误类型没被覆盖。
  3. 渲染器要冻住。换一个版本的渲染器,就等于换了一套输入分布,旧数据不能沿用,必须重测。
  4. 评测和推理时 prompt 要与训练时逐字节一致,并检查最后一个 token id 是否对得上。