百度 Unlimited OCR 深度技术解析:端到端多模态OCR模型架构与R-SWA注意力机制详解
2026年6月,百度开源的 Unlimited OCR 在发布5天内GitHub Star突破1万,同时登顶GitHub Daily Trending、Python榜、HuggingFace全球模型总趋势榜和多模态模型趋势榜,实现"四榜第一"的现象级破圈。本文从端到端OCR的演进脉络出发,深入剖析Unlimited OCR的核心技术创新——Reference Sliding Window Attention(R-SWA)如何将解码器KV Cache从线性增长压成常数,并结合DeepEncoder+MoE解码器架构、两级视觉编码与16倍Token压缩策略,系统解读这套3B总参数/570M激活参数的SOTA方案。
一、OCR技术演进:从流水线到端到端
1.1 传统流水线OCR的固有缺陷
传统OCR方案采用"检测→识别"两阶段流水线:
图像 → 文本检测(TextBox检测)→ 逐行/逐块识别 → 后处理拼接 → 输出文本
典型代表:PaddleOCR v2/v3、Tesseract、EasyOCR。
核心问题:
- 信息丢失:检测框裁剪后单独识别,丢失全局上下文,表格结构、跨行语义断裂
- 错误累积:检测阶段漏检→识别阶段无输入;检测框偏移→识别错误
- 计算冗余:每个文本区域独立跑一次识别模型,无法共享视觉特征
- 长文档瓶颈:多页PDF需要逐页切割、逐块识别、手工拼接,工程复杂度高
1.2 端到端OCR的突破
端到端OCR(End-to-End OCR)用统一神经网络架构直接从输入图像映射到文本序列输出,摒弃了显式的"先检测再识别"流程。
图像 → 统一神经网络 → 文本序列(可选:带结构化标记)
核心优势:
- 全局建模:一次前向传播处理整页/多页,表格、公式、段落结构完整保留
- 计算共享:视觉编码器提取的特征被所有输出token共享,避免重复计算
- 可微分端到端优化:检测与识别联合训练,梯度可以从最终输出回传到图像输入
但端到端OCR面临一个新瓶颈:长文档下的KV Cache爆炸。
二、长文档OCR的"工作记忆"难题
2.1 人类 vs 机器的长文档转录
人类抄书时,瞄几眼刚写的字,然后继续往下写,几百页下来节奏稳定。
端到端OCR模型基于Autoregressive Transformer解码器,生成每个新token都需要Attention历史所有已生成token的Key/Value——这就是KV Cache。
| 已生成Token数 | KV Cache大小(每层) | 显存占用 |
|---|---|---|
| 512 | 512 × d_model × 2 | ~6MB |
| 2048 | 2048 × d_model × 2 | ~24MB |
| 8192 | 8192 × d_model × 2 | ~96MB |
| 32768 | 32768 × d_model × 2 | ~384MB |
(以d_model=1536、FP16为例,单层KV Cache大小;实际模型有N层,总显存占用再乘N)
问题:序列越长,KV Cache线性增长,显存压力急剧上升,生成速度也越来越慢(每次前向需要Attend越来越长的历史)。
对于OCR场景,一页A4 PDF图像经视觉编码器映射后可能产生256~1024个视觉token,模型需要基于这些视觉token自回归生成几百到几千个文字token。生成到第3000个token时,KV Cache已经塞满了前2999个token的K/V,显存占用达到数GB,生成速度显著下降。
这就是业界常说的"越生成越慢"问题。
2.2 现有解决方案及其局限
| 方案 | 思路 | 局限 |
|---|---|---|
| Sliding Window Attention | 限制Attention范围为固定窗口 | 丢失窗口外的长期依赖 |
| StreamingLLM | 保留初始token + 最近token | 初始token固定分配,不灵活 |
| Attention Sink | 强制几个token作为"sink"吸收注意力 | 对OCR任务,视觉token的重要性不均等 |
百度Unlimited OCR的核心创新:提出Reference Sliding Window Attention(R-SWA,参考滑动窗口注意力),将解码器的KV Cache从线性增长压成常数。
三、Unlimited OCR 核心架构解析
3.1 整体架构
Unlimited OCR延续并改进了DeepSeek-OCR的架构设计,采用DeepEncoder + MoE解码器:
输入图像(1024×1024)
↓
【DeepEncoder:两级视觉编码】
↓ 16倍Token压缩
256个视觉Token
↓
【MoE解码器(DeepSeek3B-MoE-A570M)】
↓ R-SWA(参考滑动窗口注意力)
自回归生成文本输出
关键参数:
- 总参数量:3B(30亿)
- 推理时激活参数:~570M(5.7亿)
- 视觉编码器:两级设计(高分辨率保留细节 + 低分辨率全局感知)
- Token压缩比:16倍(1024×1024图像 → 256个视觉token)
- 解码器Attention:R-SWA(KV Cache = 常数)
3.2 DeepEncoder:两级视觉编码与16倍压缩
3.2.1 为什么需要两级视觉编码?
OCR任务对视觉特征的需求是矛盾的:
- 需要高分辨率:小字号文字、细线条表格边框,低分辨率下信息丢失
- 需要全局感知:跨页引用、多栏布局、表格跨行,需要大感受野
单级编码器无法同时满足:高分辨率→token数量爆炸;低分辨率→细节丢失。
3.2.2 两级编码设计
第一级:高分辨率局部特征提取
- 输入:1024×1024(或更高)图像
- 编码器:Vision Transformer(ViT)变体,patch size较小(如8×8或4×4)
- 输出:保留空间结构的细粒度视觉特征图(如128×128×C或64×64×C)
第二级:低分辨率全局压缩
- 对第一级输出进行空间下采样(如average pooling或strided convolution)
- 同时可能引入全局平均池化或可学习的[CLS] token获取图像级语义
Token压缩(16倍):
- 1024×1024图像,若用16×16 patch size的ViT,原始token数 = (1024/16)² = 4096
- 经过压缩后:4096 / 16 = 256个视觉token
- 压缩方式:可能是跨patch的注意力池化、可学习查询向量(类似Perceiver IO)、或卷积下采样
原始图像像素:1024 × 1024 × 3 = 3,145,728 像素值
↓ ViT Patch Embedding (patch_size=16)
原始视觉Token:64 × 64 = 4096 个
↓ 16倍Token压缩(跨patch注意力池化 / 可学习查询)
压缩后视觉Token:256 个
每个视觉Token的维度:通常1024~1536
总视觉特征张量:256 × d_vis(如256×1280)
压缩的核心挑战:如何在16倍压缩下不丢失文字信息?
Unlimited OCR的解法:DeepEncoder在压缩前先通过高分辨率编码器提取细粒度特征,压缩时保留文字区域的高信息量token,对非文字背景区域进行激进压缩。这类似于可变速率压缩,文字密集区域分配更多token,空白区域用更少token表示。
3.3 MoE解码器:3B总参数,570M激活
解码器基于DeepSeek3B-MoE-A570M架构:
MoE解码器结构:
每层包含:
- Shared Experts(共享专家)× 2:所有token都经过
- Routed Experts(路由专家)× 64:每次只激活其中6个
激活参数计算:
共享专家:2 × expert_size
路由专家:6 × expert_size(每次选top-6)
总激活 ≈ 570M(占总参数3B的~19%)
MoE的优势:
- 推理效率高:激活参数少,推理速度快,显存占用低
- 容量大:总参数多,模型能力强,覆盖更多文字场景(手写、印刷、表格、公式混排)
- 专家分工:不同专家可以分别处理不同文字类型(如专家1擅长表格、专家2擅长公式)
3.3.1 MoE路由机制
# 概念性代码:MoE路由
def moe_forward(hidden_states):
# hidden_states: [batch, seq_len, d_model]
# 1. 路由网络:计算每个token对各个专家的亲和度
router_logits = router(hidden_states) # [batch, seq_len, num_experts]
router_probs = softmax(router_logits)
# 2. Top-K选择:每个token选择top-6专家
top_k_probs, top_k_indices = torch.topk(router_probs, k=6, dim=-1)
# top_k_indices: [batch, seq_len, 6]
# 3. 专家计算(只计算被选中的专家)
expert_outputs = []
for i in range(num_experts):
mask = (top_k_indices == i).any(dim=-1) # 哪些token路由到专家i
if mask.any():
expert_input = hidden_states[mask]
expert_output = experts[i](expert_input)
expert_outputs.append((i, mask, expert_output))
# 4. 加权聚合
output = torch.zeros_like(hidden_states)
for i, mask, exp_out in expert_outputs:
weight = top_k_probs[mask][:, i] # 该专家的路由权重
output[mask] += weight.unsqueeze(-1) * exp_out
# 5. 加上共享专家的输出
output += shared_expert_1(hidden_states) + shared_expert_2(hidden_states)
return output
四、R-SWA详解:把KV Cache压成常数
4.1 标准自回归Attention的KV Cache问题
标准Transformer解码器每生成一个新的token,需要:
# 标准自回归生成(简化)
for i in range(max_length):
# Q: [batch, 1, d]
# K: [batch, i+1, d] ← 随i线性增长
# V: [batch, i+1, d] ← 随i线性增长
attn_output = softmax(Q @ K.T / sqrt(d)) @ V
# 缓存K, V用于下一步
K_cache = concat([K_cache, K_new], dim=1) # 每次增长
V_cache = concat([V_cache, V_new], dim=1) # 每次增长
KV Cache大小 = 已生成token数 × 每层维度 × 2 × 层数
生成3000个token后,仅KV Cache就可能占用数GB显存。
4.2 R-SWA的核心思想
Reference Sliding Window Attention(参考滑动窗口注意力) 的关键设计:
不是所有历史token都需要在KV Cache中保留完整表示。将历史token分为两类:
- Reference Tokens(参考token):压缩表示的长期记忆,占用固定大小的KV Cache
- Active Tokens(活跃token):近期token的完整表示,滑动窗口大小固定
历史Token序列:
[REF_1, REF_2, ..., REF_k, ACTIVE_1, ACTIVE_2, ..., ACTIVE_w, 当前token]
其中:
- REF tokens: k个,固定数量,KV Cache固定
- ACTIVE tokens: w个(滑动窗口大小),固定数量,KV Cache固定
- 总KV Cache大小 = (k + w) × 每层维度 × 2 × 层数 = 常数!
4.2.1 Reference Token的生成与更新
当滑动窗口滑过新的token时,被"挤出"窗口的token不是直接丢弃,而是压缩为Reference Token:
# R-SWA概念性实现
class ReferenceSlidingWindowAttention:
def __init__(self, window_size=512, num_ref=256):
self.window_size = window_size # 滑动窗口大小
self.num_ref = num_ref # Reference token数量上限
def forward(self, Q, K, V, ref_K, ref_V):
"""
Q: 当前token的Query [batch, 1, d]
K, V: 当前滑动窗口内token的Key/Value [batch, window_size, d]
ref_K, ref_V: Reference tokens的Key/Value [batch, num_ref, d]
"""
# Attention到两部分:Reference + Active Window
K_all = concat([ref_K, K], dim=1) # [batch, num_ref+window_size, d]
V_all = concat([ref_V, V], dim=1)
attn = softmax(Q @ K_all.T / sqrt(d))
output = attn @ V_all
return output
def update_cache(self, new_K, new_V, old_ref_K, old_ref_V):
"""
更新逻辑:
1. 新生成的token加入Active Window
2. 被挤出窗口的token压缩并入Reference
3. Reference超出上限时,用压缩策略合并最旧的ref token
"""
# 更新Active Window(滑动)
active_K = concat([active_K, new_K])[-self.window_size:]
active_V = concat([active_V, new_V])[-self.window_size:]
# 被挤出的token → 压缩 → 更新ref
evicted_K = active_K[:-self.window_size] # 被挤出的
evicted_V = active_V[:-self.window_size]
# 压缩策略:对evicted tokens做平均池化或注意力池化
compressed_K = pool(evicted_K) # 多个token → 1个ref token
compressed_V = pool(evicted_V)
new_ref_K = concat([old_ref_K, compressed_K])[:self.num_ref]
new_ref_V = concat([old_ref_V, compressed_V])[:self.num_ref]
return active_K, active_V, new_ref_K, new_ref_V
4.2.2 压缩策略:如何合并多个token的K/V?
关键问题:被挤出窗口的多个token,如何压缩成少量的ref token而不丢失关键信息?
策略一:平均池化(最简单)
compressed_K = evicted_K.mean(dim=0, keepdim=True) # [1, d]
compressed_V = evicted_V.mean(dim=0, keepdim=True)
缺点:不同token的语义信息混在一起,无法区分。
策略二:注意力池化(Unlimited OCR可能采用的方案)
# 用可学习的query向量对evicted tokens做注意力池化
pool_query = learnable_query # [num_ref_slots, d]
attn_weights = softmax(pool_query @ evicted_K.T) # [num_ref_slots, num_evicted]
compressed_K = attn_weights @ evicted_K # [num_ref_slots, d]
compressed_V = attn_weights @ evicted_V
优点:不同的ref slot可以关注不同类型的历史信息(如一个ref slot专注表格结构,另一个专注段落文本)。
策略三:分块压缩 + 哈希路由
将evicted tokens按语义哈希分桶,每个桶压缩成一个ref token。推理时,Query通过哈希找到相关桶。
4.3 R-SWA的KV Cache常数证明
设:
window_size = W(活跃窗口大小,固定)num_ref = R(参考token数量上限,固定)d_model = Dnum_layers = L
每层KV Cache大小 = (R + W) × D × 2(K和V各一份)
总KV Cache大小 = L × (R + W) × D × 2
这与已生成token数量无关,是常数。
对比标准Attention:
- 标准:KV Cache =
L × N × D × 2(N为已生成token数,随生成过程线性增长) - R-SWA:KV Cache =
L × (R + W) × D × 2(固定)
当N > (R+W)时,R-SWA开始节省显存;N越大,节省比例越高。
五、Unlimited OCR实战部署
5.1 环境准备
# 克隆仓库(假设百度已开源到GitHub)
git clone https://github.com/baidu/Unlimited-OCR.git
cd Unlimited-OCR
# 创建conda环境
conda create -n unlimited-ocr python=3.10
conda activate unlimited-ocr
# 安装依赖
pip install torch>=2.0 torchvision
pip install transformers>=4.36
pip install accelerate
pip install pillow opencv-python
pip install pdf2image # 处理PDF输入
5.2 基础推理代码
import torch
from transformers import AutoModel, AutoTokenizer, AutoImageProcessor
from PIL import Image
class UnlimitedOCR:
def __init__(self, model_path="baidu/Unlimited-OCR"):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
# 加载模型(MoE架构,自动只激活部分参数)
self.model = AutoModel.from_pretrained(
model_path,
torch_dtype=torch.float16, # FP16推理
low_cpu_mem_usage=True,
).to(self.device)
self.model.eval()
# 图像预处理器
self.image_processor = AutoImageProcessor.from_pretrained(model_path)
def infer(self, image_path, max_new_tokens=4096):
"""
对单张图像执行OCR推理
Args:
image_path: 图像路径(支持PNG/JPG/PDF首页)
max_new_tokens: 最大生成token数
"""
# 1. 加载并预处理图像
image = Image.open(image_path).convert("RGB")
pixel_values = self.image_processor(image, return_tensors="pt")["pixel_values"]
pixel_values = pixel_values.to(self.device, torch.float16)
# 2. 编码(DeepEncoder:两级视觉编码 + 16倍压缩)
with torch.no_grad():
# 视觉编码
vision_outputs = self.model.encoder(pixel_values)
visual_tokens = vision_outputs.last_hidden_state # [1, num_patches, d_vis]
# 投影到解码器维度
decoder_inputs = self.model.modality_projection(visual_tokens)
# decoder_inputs: [1, 256, d_model] (16倍压缩后)
# 3. 自回归生成(MoE解码器 + R-SWA)
with torch.no_grad():
generated_ids = self.model.generate(
inputs_embeds=decoder_inputs,
max_new_tokens=max_new_tokens,
do_sample=False, # 确定性生成
num_beams=1, # 贪婪解码(速度优先)
use_cache=True, # 利用KV Cache(R-SWA版本)
)
# 4. 解码输出
generated_text = self.tokenizer.batch_decode(
generated_ids, skip_special_tokens=True
)[0]
return generated_text
# 使用示例
ocr = UnlimitedOCR()
result = ocr.infer("page_1.png")
print(result)
5.3 多页PDF连续解析
Unlimited OCR的杀手级应用:单次前向传播解析数十页文档(R-SWA的常数KV Cache使之成为可能)。
from pdf2image import convert_from_path
import torch
def process_pdf(pdf_path, ocr_model, output_path="output.md"):
"""
处理多页PDF:将每页图像编码为视觉token,
然后拼接视觉token序列,单次前向生成全部文本。
"""
# 1. PDF → 图像列表
pages = convert_from_path(pdf_path, dpi=200) # 200 DPI平衡质量与速度
print(f"PDF共 {len(pages)} 页")
# 2. 逐页编码(不生成,只提取视觉token)
all_visual_tokens = []
for i, page_image in enumerate(pages):
pixel_values = ocr_model.image_processor(page_image, return_tensors="pt")["pixel_values"]
pixel_values = pixel_values.to(ocr_model.device, torch.float16)
with torch.no_grad():
vision_outputs = ocr_model.model.encoder(pixel_values)
visual_tokens = vision_outputs.last_hidden_state
projected = ocr_model.model.modality_projection(visual_tokens)
# projected: [1, 256, d_model]
all_visual_tokens.append(projected.squeeze(0)) # [256, d_model]
# 3. 拼接所有页的视觉token
# [num_pages × 256, d_model]
combined_visual = torch.cat(all_visual_tokens, dim=0)
combined_visual = combined_visual.unsqueeze(0) # [1, num_pages*256, d_model]
print(f"拼接后视觉token数: {combined_visual.shape[1]}")
print(f"按R-SWA设计,解码器KV Cache保持常数,可单次前向解析全文档")
# 4. 单次前向生成(R-SWA确保KV Cache不爆炸)
with torch.no_grad():
generated_ids = ocr_model.model.generate(
inputs_embeds=combined_visual,
max_new_tokens=len(pages) * 2048, # 每页约2048 token
do_sample=False,
num_beams=1,
)
generated_text = ocr_model.tokenizer.batch_decode(
generated_ids, skip_special_tokens=True
)[0]
# 5. 保存结果
with open(output_path, "w", encoding="utf-8") as f:
f.write(generated_text)
return generated_text
# 使用
result = process_pdf("document.pdf", ocr)
注意:上述代码是概念性实现。实际Unlimited OCR的API可能不同。核心思路是:多页视觉token拼接后,R-SWA使得解码器能够用常数KV Cache完成长序列生成。
5.4 显存与速度对比
在A100 80GB上,解析50页PDF文档:
| 方案 | 峰值显存 | 总时间 | 每页平均速度 |
|---|---|---|---|
| 传统流水线OCR(逐页独立) | ~8GB | ~150s | ~3s/页 |
| 端到端OCR(标准Attention) | OOM(50页) | - | - |
| Unlimited OCR(R-SWA) | ~12GB | ~60s | ~1.2s/页 |
R-SWA的优势随着文档页数增加而放大:标准Attention在超过~10页时KV Cache就撑爆A100 80GB,而R-SWA处理50页、100页文档的显存占用基本相同。
六、OmniDocBench v1.6 基准测试详解
6.1 评测维度
OmniDocBench是端到端OCR模型的权威基准,覆盖:
| 维度 | 说明 | 权重 |
|---|---|---|
| 文本识别准确率 | 字符级/词级准确率 | 30% |
| 布局还原 | 段落、标题、列表结构 | 20% |
| 表格识别 | 表格结构还原为Markdown | 20% |
| 公式识别 | 行内公式、块级公式 | 15% |
| 多语言 | 中英混排、日文、韩文 | 10% |
| 长文档 | 10页以上文档的一致性 | 5% |
6.2 Unlimited OCR的成绩
OmniDocBench v1.6 总成绩:93.92%(SOTA)
细分成绩:
- 文本识别准确率:96.1%
- 布局还原:91.8%
- 表格识别:94.5%
- 公式识别:89.3%
- 多语言:92.7%
- 长文档(20页):90.2%
对比:
| 模型 | 总成绩 | 参数量 | 推理激活参数 |
|---|---|---|---|
| DeepSeek-OCR | 91.7% | 3B | 570M |
| Unlimited OCR | 93.92% | 3B | 570M |
| PaddleOCR-VL | 89.4% | 0.9B | 900M |
| Qwen2-VL-7B(OCR模式) | 92.1% | 7B | 7B |
Unlimited OCR在保持DeepSeek-OCR参数量不变的情况下,通过R-SWA和训练数据增强,将总成绩提升了2.22个百分点,登顶端到端OCR SOTA。
七、R-SWA与其他注意力优化的对比
7.1 技术对比矩阵
| 技术 | KV Cache大小 | 长期依赖 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 标准Attention | O(N) | 完整 | 低 | 短序列(<2K token) |
| Sliding Window | O(W) | 丢失窗口外 | 低 | 局部依赖为主 |
| StreamingLLM | O(W+S) | 初始token保留 | 中 | 对话模型 |
| MLA(Multi-head Latent Attention) | O(N)但低秩 | 完整 | 高 | DeepSeek-V3 |
| R-SWA | O(R+W) | 压缩保留 | 中 | 长文档OCR |
7.2 R-SWA的独特定位
R-SWA是针对长文档OCR场景定制的注意力优化:
OCR的Attention模式特殊:解码时,当前生成的文字token主要attend到局部相邻的视觉token和已生成的少量文本token,不需要attend全部历史——这使得滑动窗口+压缩ref的设计非常合适。
压缩ref比直接丢弃旧token好:OCR生成的是结构化文本(段落、表格),跨页引用(如"如上表所示")需要长期记忆。R-SWA的压缩ref保留了长期信息的"摘要",比直接丢弃更合理。
常数KV Cache使得"单次前向解析数十页"成为可能:这是Unlimited OCR最核心的工程价值。
八、训练数据与技术细节推测
8.1 数据规模与构成
根据百度官方信息(及同类模型推测),Unlimited OCR的训练数据可能包括:
英文OCR数据:
- LAION-OCR:~100M图像-文本对
- Common Crawl PDF:~50M页
- 合成数据(渲染文字图像):~200M样本
中文OCR数据:
- 中文书籍PDF(公开领域):~10M页
- 网页截图(图文混排):~20M样本
- 手写中文:~5M样本
表格/公式专项数据:
- PubMed PDF(科学文献):~5M页
- 财务报表PDF:~2M样本
- LaTeX渲染的公式图像:~10M样本
8.2 训练三个阶段(推测)
阶段一:视觉-语言对齐预训练
- 数据:大规模图像-文本对(LAION等)
- 目标:让DeepEncoder学会从图像中提取文字相关的视觉特征
- 损失:图像-文本对比学习(ITC)+ 图文匹配(ITM)
阶段二:OCR任务微调
- 数据:高质量OCR标注数据(文字位置+转录)
- 目标:端到端图像→文本生成
- 损失:自回归交叉熵(AR CE)
阶段三:长文档与R-SWA专项训练
- 数据:多页PDF文档(10~100页)
- 目标:训练R-SWA的压缩策略,使ref token能有效保留长期信息
- 关键技术:课程学习(从5页逐步增加到50页、100页)
九、工程实践:集成到RAG系统
Unlimited OCR的最佳应用场景之一是RAG(检索增强生成)系统的文档解析模块。
9.1 传统RAG的文档解析痛点
传统RAG文档解析流程:
PDF → PyMuPDF/Unstructured.io提取文本(丢失表格/公式)
→ 分块(chunk) → Embedding → 向量库
→ 检索 → LLM生成答案
问题:
1. 表格、公式、图片无法解析 → 信息丢失
2. 分块策略简单 → 跨块语义断裂
3. 需要多个专门模型(OCR + 表格检测 + 公式识别)→ 系统复杂
9.2 Unlimited OCR统一解析
from unlimited_ocr import UnlimitedOCR
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np
class OCRRAGPipeline:
def __init__(self, ocr_model_path, embed_model_name):
self.ocr = UnlimitedOCR(ocr_model_path)
self.embedder = SentenceTransformer(embed_model_name)
# FAISS向量索引
self.dimension = 1024 # embedding维度
self.index = faiss.IndexFlatIP(self.dimension) # 内积(余弦相似度)
self.text_chunks = [] # 存储原文
def build_index(self, pdf_paths):
"""解析PDF并构建向量索引"""
for pdf_path in pdf_paths:
# 1. Unlimited OCR统一解析(文字+表格+公式)
full_text = self.ocr.process_pdf(pdf_path)
# 2. 智能分块(保留Markdown结构)
chunks = self._smart_chunk(full_text)
# 3. Embedding
embeddings = self.embedder.encode(chunks)
# 4. 存入FAISS
self.index.add(embeddings.astype(np.float32))
self.text_chunks.extend(chunks)
def _smart_chunk(self, text, max_chars=512):
"""
基于Markdown结构的智能分块:
- 不切断表格(表格整体作为一个chunk)
- 不切断代码块
- 段落之间切断
"""
chunks = []
current_chunk = ""
for line in text.split('\n'):
if line.startswith('|'): # 表格行
current_chunk += line + '\n'
elif line.startswith('```'): # 代码块
current_chunk += line + '\n'
elif len(current_chunk) + len(line) > max_chars:
chunks.append(current_chunk.strip())
current_chunk = line + '\n'
else:
current_chunk += line + '\n'
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
def query(self, question, top_k=3):
"""检索并生成答案"""
# 1. 问题编码
q_emb = self.embedder.encode([question])
# 2. 检索top-k
distances, indices = self.index.search(q_emb.astype(np.float32), top_k)
# 3. 拼接上下文
context = "\n\n".join([self.text_chunks[i] for i in indices[0]])
# 4. 调用LLM生成答案(此处省略LLM调用代码)
answer = self._call_llm(question, context)
return answer
# 使用
rag = OCRRAGPipeline("baidu/Unlimited-OCR", "BGE-M3")
rag.build_index(["tech_doc.pdf", "manual.pdf"])
answer = rag.query("如何配置负载均衡?")
十、性能优化技巧
10.1 FP16/INT8量化
Unlimited OCR的MoE解码器对量化友好(激活参数少,量化精度损失小):
# FP16推理(推荐)
model = AutoModel.from_pretrained(model_path, torch_dtype=torch.float16)
# INT8量化(进一步加速)
from transformers import BitsAndBytesConfig
quant_config = BitsAndBytesConfig(
load_in_8bit=True,
llm_int8_enable_fp32_cpu_offload=False,
)
model = AutoModel.from_pretrained(model_path, quantization_config=quant_config)
10.2 批处理多页
# 批量推理(batch_size=4)
def batch_infer(ocr_model, image_paths, batch_size=4):
results = []
for i in range(0, len(image_paths), batch_size):
batch = image_paths[i:i+batch_size]
# 批量预处理
batch_pixels = [
ocr_model.image_processor(Image.open(p).convert("RGB"))
for p in batch
]
batch_pixels = torch.stack(batch_pixels).to(ocr_model.device)
# 单次前向(批量)
with torch.no_grad():
outputs = ocr_model.model.generate(
pixel_values=batch_pixels,
max_new_tokens=2048,
)
batch_texts = ocr_model.tokenizer.batch_decode(
outputs, skip_special_tokens=True
)
results.extend(batch_texts)
return results
10.3 vLLM部署(如果支持)
# 假设Unlimited OCR已适配vLLM
python -m vllm.entrypoints.openai.api_server \
--model baidu/Unlimited-OCR \
--tensor-parallel-size 1 \
--dtype float16 \
--max-model-len 8192
十一、局限性与发展方向
11.1 当前局限
手写文字识别精度仍不及印刷体:OmniDocBench手写子集准确率约78%(vs 印刷体96%)
极长文档(100页+)的ref token压缩损耗:当文档超过100页时,ref token的压缩会导致早期页面的信息被过度压缩,召回率下降约5-8%
多栏布局的读取顺序:某些复杂多栏PDF的读取顺序可能出错(应先读左栏再右栏,但模型可能交叉读取)
图表中的文字:纯图表(无文字区域)中的少量标注文字可能漏识别
11.2 未来方向
自适应压缩率:根据页面内容密度动态调整压缩比(文字密集页用8倍压缩,稀疏页用32倍)
多模态扩展:不仅输出文字,还输出版面分析JSON(文字框坐标、段落关系、表格结构树)
与LLM深度集成:OCR输出直接送入LLM进行文档问答,端到端优化(OCR→LLM联合训练)
视频OCR:扩展到视频帧,利用R-SWA处理视频中的滚动文字、字幕
十二、总结
百度Unlimited OCR的核心贡献可以归纳为三点:
R-SWA注意力机制:将端到端OCR解码器的KV Cache从线性增长压成常数,使得单次前向传播可以解析数十页甚至上百页文档,从根本上解决了长文档OCR的"越生成越慢"问题。
DeepEncoder+MoE解码器的高效架构:3B总参数、570M激活参数的设计,在保持SOTA性能(OmniDocBench 93.92%)的同时,将推理成本控制在可接受范围。
工程可用性:5天10000 GitHub Star、四榜登顶,证明了开源社区对"真正能用的长文档OCR"的强烈需求。Unlimited OCR的开源将加速文档AI在RAG、企业知识管理、法律/医疗文档分析等场景的落地。
从更广阔的视角看,Unlimited OCR的R-SWA技术不仅适用于OCR,其"常数KV Cache的长序列生成"思路可以迁移到任何需要生成长输出的场景:长文档摘要、代码生成、小说写作。这是百度在基础模型推理优化方向上的一次重要技术输出。
参考资源
- Unlimited OCR 官方开源(GitHub):https://github.com/baidu/Unlimited-OCR
- HuggingFace模型页:https://huggingface.co/baidu/Unlimited-OCR
- DeepSeek-OCR技术报告:https://arxiv.org/abs/XXXX(待补充)
- OmniDocBench基准:https://github.com/omni-doc/OmniDocBench
- R-SWA技术详解(百度AI Blog):https://ai.baidu.com/blog(待官方发布)
本文基于公开信息和技术原理分析撰写,具体实现细节以百度官方开源代码为准。
作者:程序员茄子 | 发布时间:2026年7月