跳到主内容
快讯直播
AI智模界
教程

上手 OXR 1.3B:搭一条中文文档解析流水线

一套能落地的文档解析方案,通常不是“一个模型解决所有问题”,而是版面切分 → 文字与表格识别 → 字段抽取 → 人工复核四段接力。这篇教程以 1.3B 量级的轻量开源 OCR 模型 OXR 为例(模型权重、仓库地址、支持的输入格式以官方 README 当前内容为准),把这条链路在一台单卡机器上跑通,并给出可复现的效果实测方法。

适用场景

适合需要把扫描件、PDF、截图里的中文内容转成结构化数据,但预算和数据合规都不允许调用公有云 OCR 的团队:发票、合同、报关单、体检报告、质检单这类“版式固定或半固定”的批量单据。单张消费级显卡甚至纯 CPU 就能跑,数据全程留在内网。

不适合的场景也说清楚:手写体占比高、严重倾斜或污损的档案,1.3B 级别的模型识别率会明显下降,建议先做小样本验证再决定是否上量。

环境与前置条件

项目建议说明
操作系统Ubuntu 20.04 / 22.04 LTSWindows 建议走 WSL2,macOS 可用 MPS 但速度一般
Python3.10 或 3.11版本过低会导致部分依赖装不上
GPU8GB 显存起步,16GB 更宽裕1.3B 模型 fp16 权重约 2.6GB,加上图像编码器和激活值,6–8GB 可跑单页;显存越大越能堆 batch
CPU 兜底纯 CPU 可运行单页耗时会长很多,适合离线补数据,不适合实时
内存16GB 以上PDF 转图和多进程时会吃内存
磁盘20GB 以上模型权重 + 样本 + 输出归档

其余依赖:系统需要 libgl1、libglib2.0-0(OpenCV 用到)、poppler-utils(PDF 处理)、中文字体(生成预览图用)。

分步骤部署

步骤 1:确认显卡与驱动

```bash

nvidia-smi

```

输出里能看到显卡型号、显存总量、Driver Version 和右上角的 CUDA Version 就算通过。如果命令不存在,先装官方驱动;CUDA Version 低于 PyTorch 官方安装页要求时,按官方页面给出的对应版本命令安装,不要凭记忆抄旧命令。

步骤 2:装系统依赖

```bash

sudo apt-get update

sudo apt-get install -y python3-venv python3-pip git \

libgl1 libglib2.0-0 poppler-utils fonts-noto-cjk

```

这一步装的是“Python 包管不到的东西”,尤其是 libGL.so.1,缺了会在导入 OpenCV 时直接报错。

步骤 3:建独立虚拟环境

```bash

python3 -m venv ~/oxr-env

source ~/oxr-env/bin/activate

python -V # 确认 3.10 / 3.11

pip install -U pip

```

之后所有命令都在这个环境里执行。不要装进系统 Python,否则后面升级依赖会互相打架。

步骤 4:安装推理框架

先按 PyTorch 官方安装页当前给出的命令装 PyTorch(选择与你驱动匹配的 CUDA 版本,示例里的 cu121 只是占位,按官网选项替换):

```bash

pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

```

第二条命令输出 True 表示 GPU 可用;输出 False 说明装成了 CPU 版本,回去换安装命令重装。

接着装其余依赖:

```bash

pip install -U transformers accelerate pillow opencv-python-headless \

pymupdf pandas tqdm huggingface_hub

```

这一步不锁版本,先跑通;后面在“后续维护”里用 pip freeze 把版本钉死。

步骤 5:拉取模型权重

```bash

网络受限时先设镜像;镜像可用性以镜像站当前状态为准

export HF_ENDPOINT=https://hf-mirror.com

hf download <官方仓库ID> --local-dir ./models/oxr-1.3b

```

<官方仓库ID> 替换成官方 README 里给出的仓库名。成功标准:./models/oxr-1.3b 目录下出现权重文件(.safetensors 等)、config.json、preprocessor_config.json,以及模型自带的自定义代码文件。目录大小与官方页面标注的参数量基本吻合即可。

步骤 6:单图跑通最小推理

先写一个只做“图 → Markdown”的脚本,确认模型本身能出字,再谈流水线。

```python

infer_one.py

import torch

from PIL import Image, ImageOps

from transformers import AutoProcessor, AutoModelForVision2Seq

MODEL_DIR = "./models/oxr-1.3b"

PROMPT = ("请把这张图片里的全部文字按自然阅读顺序转成 Markdown;"

"表格用 HTML 表格输出并保留表头;公式用 LaTeX。不要输出解释性文字。")

processor = AutoProcessor.from_pretrained(MODEL_DIR, trust_remote_code=True)

model = AutoModelForVision2Seq.from_pretrained(

MODEL_DIR, torch_dtype=torch.bfloat16, device_map="auto",

trust_remote_code=True,

).eval()

def ocr(image_path: str) -> str:

img = ImageOps.exif_transpose(Image.open(image_path)).convert("RGB")

messages = [{"role": "user",

"content": [{"type": "image"}, {"type": "text", "text": PROMPT}]}]

text = processor.apply_chat_template(messages, add_generation_prompt=True)

inputs = processor(text=text, images=img, return_tensors="pt").to(model.device)

with torch.inference_mode():

out = model.generate(**inputs, max_new_tokens=2048, do_sample=False)

new_tokens = out[:, inputs["input_ids"].shape[1]:]

return processor.batch_decode(new_tokens, skip_special_tokens=True)[0]

if __name__ == "__main__":

print(ocr("samples/demo.png"))

```

说明两点:这里用的是多数 OCR-VLM 通用的 AutoProcessor + generate 写法;如果官方仓库给的是 AutoModel 加自定义 chat() 接口,按 README 替换加载与调用两行即可,其余流程不变。ImageOps.exif_transpose 不能省——手机拍的图带旋转标记,不处理会把横图当竖图识别。

成功标准:终端打印出与原图一致的中文段落,表格部分出现 <table> 标签。

步骤 7:PDF 转图 + 版面切分

整页直接送模型,在版式复杂时容易串行、漏行。做法是先用投影法把页面切成“文字块”,逐块识别,再按纵坐标拼回去。

```python

prep.py

import cv2, fitz, numpy as np

def pdf_to_images(pdf_path: str, out_dir: str, dpi: int = 200):

doc = fitz.open(pdf_path)

paths = []

for i, page in enumerate(doc):

pix = page.get_pixmap(dpi=dpi)

p = f"{out_dir}/page_{i:04d}.png"

pix.save(p)

paths.append(p)

return paths

def split_blocks(img_path: str, min_gap: int = 20, min_h: int = 30):

"""按水平投影的空白行切块,返回 [(y1, y2), ...]"""

img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)

bw = cv2.threshold(img, 0, 255,

cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

rows = (bw > 0).sum(axis=1)

blocks, start, gap = [], None, 0

for y, cnt in enumerate(rows):

if cnt > 0:

start = y if start is None else start

gap = 0

elif start is not None:

gap += 1

if gap >= min_gap:

if y - gap - start >= min_h:

blocks.append((start, y - gap))

start, gap = None, 0

if start is not None and len(rows) - start >= min_h:

blocks.append((start, len(rows)))

return blocks

```

切完后按 y1 排序、逐块裁剪送模型、按顺序拼接,跨块段落一般能自然衔接。遇到双栏排版、图文混排的合同,投影法会把左右栏粘在一起,这时换用版面检测模型(如 PP-Structure 一类方案,版本以官方文档为准)输出区块坐标,再走同样的“裁块 → 识别 → 拼接”。

步骤 8:表格还原与结构校验

让模型直接输出 HTML 表格,比让它输出 CSV 稳,因为合并单元格、表头这些信息 HTML 能表达。

```python

import io, pandas as pd

def parse_tables(html: str):

try:

return pd.read_html(io.StringIO(html))

except ValueError:

return []

def check_table(df: pd.DataFrame) -> bool:

"""列数一致、无整行为空,才算结构可信"""

return df.shape[1] > 1 and df.dropna(how="all").shape[0] > 0

```

校验不过的表格不硬猜,直接打上 need_review=True 丢进复核队列——这是整条流水线可信度的关键。

步骤 9:字段抽取(规则优先,模型兜底)

固定字段先用正则,命中率高、零成本;正则抽不到的再让模型按 JSON 输出。

```python

import re

FIELDS = {

"doc_no": r"(?:发票号码|单据编号|合同编号)[::\s]*([A-Za-z0-9\-]{6,})",

"date": r"(?:开票日期|签订日期|日期)[::\s]*(\d{4}[-年/]\d{1,2}[-月/]\d{1,2})",

"amount": r"(?:价税合计|合计金额|总金额)[::\s]*[¥¥]?\s*([\d,]+\.\d{2})",

}

def rule_extract(text: str) -> dict:

out = {}

for k, pat in FIELDS.items():

m = re.search(pat, text)

out[k] = m.group(1).replace(",", "") if m else None

return out

```

模型兜底的提示词示例:“从下面的文本中抽取 doc_no、date、amount 三个字段,输出严格 JSON,找不到的字段填 null,不要编造。”拿到结果后统一做类型校验(金额转 float、日期转标准格式),校验失败的进复核队列。

步骤 10:串成批处理脚本

```python

pipeline.py

import json, time, pathlib

from prep import pdf_to_images, split_blocks

from infer_one import ocr

from PIL import Image

def run(indir: str, outdir: str):

out = pathlib.Path(outdir); out.mkdir(parents=True, exist_ok=True)

review = open(out / "review.jsonl", "a", encoding="utf-8")

for pdf in sorted(pathlib.Path(indir).glob("*.pdf")):

t0 = time.time()

pages = pdf_to_images(str(pdf), str(out / pdf.stem))

texts = []

for p in pages:

img = Image.open(p)

for (y1, y2) in split_blocks(p):

crop = img.crop((0, y1, img.width, y2))

cp = f"{p}.{y1}.png"; crop.save(cp)

texts.append(ocr(cp))

rec = {"file": pdf.name, "text": "\n".join(texts),

"cost_s": round(time.time() - t0, 2)}

(out / f"{pdf.stem}.json").write_text(

json.dumps(rec, ensure_ascii=False, indent=2), encoding="utf-8")

if rec["cost_s"] > 60 or not rec["text"].strip():

review.write(json.dumps({"file": pdf.name, "reason": "slow_or_empty"},

ensure_ascii=False) + "\n")

review.close()

```

步骤 11:人工复核

复核不需要复杂系统,一个 JSONL + 表格软件就够起步:review.jsonl 里每行一条待核记录,字段包括文件名、页码、可疑原因、原始截图路径。核完把结论回写同一行("status": "ok" / "fixed"),这批数据就是后续调提示词、调切块参数的回归集。

验证部署是否成功

1. 环境层验证

```bash

python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"

```

预期输出 True 加显卡型号。

2. 单图验证

```bash

python infer_one.py

```

预期:打印出中文文本;若你的样例图里含表格,应能看到 <table>。输出为空或全是重复字符,说明提示词或解码参数有问题,先调 max_new_tokens,再查图片是否全白/全黑。

3. 小批量贯通验证

```bash

mkdir -p out && python -c "from pipeline import run; run('samples', 'out')"

ls out/*.json | wc -l

```

预期:JSON 数量与 samples/ 里 PDF 数量一致;out/review.jsonl 行数应远小于总数(复核率一般控制在 10%–20% 以内算健康)。

4. 效果实测(自己跑,别抄别人的数字)

取 20–30 页覆盖你真实业务的样本,人工校对一份“标准答案”,然后算字符错误率(CER):

```python

def edit_distance(a, b):

prev = list(range(len(b) + 1))

for i, ca in enumerate(a, 1):

cur = [i]

for j, cb in enumerate(b, 1):

cur.append(min(prev[j] + 1, cur[j-1] + 1, prev[j-1] + (ca != cb)))

prev = cur

return prev[-1]

def cer(pred: str, gold: str) -> float:

return edit_distance(pred, gold) / max(len(gold), 1)

```

把结果填进下面这张表——不同版式差异极大,别人的数字对你没有参考价值:

文档类型样本页数CER表格结构与原文一致字段一次抽对率单页耗时
增值税发票
采购合同
检验报告

判断标准:CER 低于 2% 可直接入库,2%–5% 抽检复核,超过 5% 的文档类型先做图像增强(去噪、纠偏、提高 DPI)再重测。

常见报错与解决

报错 1:torch.cuda.OutOfMemoryError: CUDA out of memory

原因:显存不够,通常是单页分辨率过高或 max_new_tokens 设太大。

解决:

```bash

export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True

```

同时把 PDF 转图 DPI 从 300 降到 200,整页切成小块送模型,max_new_tokens 按实际文本长度下调。

报错 2:OSError: We couldn't connect to 'https://huggingface.co' 或下载卡住

原因:网络访问受限。

解决:

```bash

export HF_ENDPOINT=https://hf-mirror.com

export HF_HUB_ENABLE_HF_TRANSFER=1

hf download <官方仓库ID> --local-dir ./models/oxr-1.3b

```

仍失败时在能联网的机器上下载后整目录拷贝,用 --local-dir 指向本地路径加载。

报错 3:ImportError: libGL.so.1: cannot open shared object file

原因:OpenCV 依赖的系统库缺失,常见于精简版镜像。

解决:

```bash

sudo apt-get install -y libgl1 libglib2.0-0

```

或改装 opencv-python-headless。

报错 4:加载模型时报 unexpected key(s) in state_dict 或 trust_remote_code 相关异常

原因:transformers 版本与模型自带的自定义代码不匹配。

解决:按官方仓库 requirements.txt 安装对应版本,不要盲目 pip install -U:

```bash

pip install "transformers==<仓库要求的版本>"

```

报错 5:识别结果中文乱码或出现大量重复片段

原因:图片带 EXIF 旋转、或者用了采样解码导致重复。

解决:读取时加 ImageOps.exif_transpose,生成参数用 do_sample=False,并检查 processor 与 model 是否来自同一目录。

后续维护

备份:三样东西必须备份——模型目录(记下官方仓库的 commit 或权重 sha256)、代码与依赖清单、历史输出 JSONL。输出文件按 年/月/日 分目录归档,保留原始截图,复核结论回写后不可覆盖原记录。

升级:上线前先 pip freeze > requirements.txt 锁死版本。模型或依赖升级时,用固定回归集(那 20–30 页人工校对样本)跑一遍,对比 CER、字段准确率、单页耗时三项,指标下降就回滚。升级永远走“新目录并行部署 → 回归集对比 → 切流量”,不要原地覆盖。

日志与监控:每页记录页码、耗时、输出 token 数、是否进复核队列、异常类型,写进结构化日志。日常盯三个指标:单页耗时 P95 是否漂移、复核率是否上升、GPU 显存峰值是否打满。磁盘水位单独告警——图片中间产物增长很快,建议处理后按策略清理裁剪块,只保留原图和 JSON。

最后提醒一句:投入生产前确认模型的许可证是否允许你的使用方式(尤其是商用和二次分发),以官方仓库的 LICENSE 为准。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。