解密 PDF 解析 03无 OCR 的小型模型方法

解密 PDF 解析 03无 OCR 的小型模型方法

Barry Lv6

概述、原则和见解

PDF 文件在转换为其他格式时可能会面临挑战,通常会将大量信息锁定在对 AI 应用程序无法访问的格式中。如果我们能够将 PDF 文件或其对应的图像转换为结构化或半结构化的机器可读格式,这将显著缓解这个问题。这也可以显著增强人工智能应用程序的知识库。

这一系列文章致力于揭示 PDF 解析的奥秘。 在本系列的 第一篇文章 中,我们介绍了 PDF 解析的主要任务,分类了现有方法,并对每种方法进行了简要介绍。在本系列的 第二篇文章 中,我们重点讨论了基于管道的方法。

本文是该系列的第三篇,介绍了另一种 PDF 解析方法:无 OCR 小模型方法。我们首先进行概述,然后介绍各种代表性的无 OCR 小模型 PDF 解析解决方案的原理。最后,我们分享我们获得的见解和思考。

请注意,本文提到的“无 OCR 小模型”相对于大型多模态模型而言是相对较小的,通常参数少于 30 亿。

概述

之前介绍的 基于管道的 PDF 解析方法 主要使用 OCR 引擎进行文本识别。然而,它导致了高计算成本、对语言和文档类型的不灵活性,以及可能影响后续任务的 OCR 错误。

因此,应开发无 OCR 方法,如图 1 所示。它们不显式使用 OCR 进行文本识别。相反,它们使用神经网络隐式完成任务。本质上,这些方法采用端到端的方法,直接输出 PDF 解析的结果。

从结构的角度来看,无 OCR 方法相比于基于管道的方法更为简单。无 OCR 方法中需要关注的主要方面是模型结构的设计和训练数据的构建。

接下来,我们将介绍一些具有代表性的基于小模型的无 OCR PDF 解析框架:

  • Donut : 无 OCR 文档理解变换器。
  • Nougat : 基于 Donut 架构,特别适用于 PDF 论文、公式和表格。
  • Pix2Struct : 截图解析作为视觉语言理解的预训练。

Donut

如图2所示,Donut 是一个端到端模型,旨在全面理解文档图像。其架构简单,包含一个基于transformer的视觉编码器和一个文本解码器模块。

Donut 不依赖于任何与OCR相关的模块。相反,它使用视觉编码器从文档图像中提取特征,并直接使用文本解码器生成令牌序列。然后,可以将输出序列转换为像JSON这样的结构化格式。

代码 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class DonutModel(PreTrainedModel):
r"""
Donut: an E2E OCR-free Document Understanding Transformer.
The encoder maps an input document image into a set of embeddings,
the decoder predicts a desired token sequence, that can be converted to a structured format,
given a prompt and the encoder output embeddings
"""
config_class = DonutConfig
base_model_prefix = "donut"

def __init__(self, config: DonutConfig):
super().__init__(config)
self.config = config
self.encoder = SwinEncoder(
input_size=self.config.input_size,
align_long_axis=self.config.align_long_axis,
window_size=self.config.window_size,
encoder_layer=self.config.encoder_layer,
name_or_path=self.config.name_or_path,
)
self.decoder = BARTDecoder(
max_position_embeddings=self.config.max_position_embeddings,
decoder_layer=self.config.decoder_layer,
name_or_path=self.config.name_or_path,
)

def forward(self, image_tensors: torch.Tensor, decoder_input_ids: torch.Tensor, decoder_labels: torch.Tensor):
"""
Calculate a loss given an input image and a desired token sequence,
the model will be trained in a teacher-forcing manner

Args:
image_tensors: (batch_size, num_channels, height, width)
decoder_input_ids: (batch_size, sequence_length, embedding_dim)
decode_labels: (batch_size, sequence_length)
"""
encoder_outputs = self.encoder(image_tensors)
decoder_outputs = self.decoder(
input_ids=decoder_input_ids,
encoder_hidden_states=encoder_outputs,
labels=decoder_labels,
)
return decoder_outputs
...
...

编码器

Donut 利用 Swin-Transformer 作为图像编码器,因为它在初步文档解析研究中表现出色。该图像编码器将输入文档图像转换为一组高维嵌入。这些嵌入将作为文本解码器的输入。

相应的代码 如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class SwinEncoder(nn.Module):
r"""
Donut encoder based on SwinTransformer
Set the initial weights and configuration with a pretrained SwinTransformer and then
modify the detailed configurations as a Donut Encoder

Args:
input_size: Input image size (width, height)
align_long_axis: Whether to rotate image if height is greater than width
window_size: Window size(=patch size) of SwinTransformer
encoder_layer: Number of layers of SwinTransformer encoder
name_or_path: Name of a pretrained model name either registered in huggingface.co. or saved in local.
otherwise, `swin_base_patch4_window12_384` will be set (using `timm`).
"""

def __init__(
self,
input_size: List[int],
align_long_axis: bool,
window_size: int,
encoder_layer: List[int],
name_or_path: Union[str, bytes, os.PathLike] = None,
):
super().__init__()
self.input_size = input_size
self.align_long_axis = align_long_axis
self.window_size = window_size
self.encoder_layer = encoder_layer

self.to_tensor = transforms.Compose(
[
transforms.ToTensor(),
transforms.Normalize(IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD),
]
)

self.model = SwinTransformer(
img_size=self.input_size,
depths=self.encoder_layer,
window_size=self.window_size,
patch_size=4,
embed_dim=128,
num_heads=[4, 8, 16, 32],
num_classes=0,
)
self.model.norm = None

# weight init with swin
if not name_or_path:
swin_state_dict = timm.create_model("swin_base_patch4_window12_384", pretrained=True).state_dict()
new_swin_state_dict = self.model.state_dict()
for x in new_swin_state_dict:
if x.endswith("relative_position_index") or x.endswith("attn_mask"):
pass
elif (
x.endswith("relative_position_bias_table")
and self.model.layers[0].blocks[0].attn.window_size[0] != 12
):
pos_bias = swin_state_dict[x].unsqueeze(0)[0]
old_len = int(math.sqrt(len(pos_bias)))
new_len = int(2 * window_size - 1)
pos_bias = pos_bias.reshape(1, old_len, old_len, -1).permute(0, 3, 1, 2)
pos_bias = F.interpolate(pos_bias, size=(new_len, new_len), mode="bicubic", align_corners=False)
new_swin_state_dict[x] = pos_bias.permute(0, 2, 3, 1).reshape(1, new_len ** 2, -1).squeeze(0)
else:
new_swin_state_dict[x] = swin_state_dict[x]
self.model.load_state_dict(new_swin_state_dict)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (batch_size, num_channels, height, width)
"""
x = self.model.patch_embed(x)
x = self.model.pos_drop(x)
x = self.model.layers(x)
return x
...
...

Donut 使用 BART 作为 解码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
class BARTDecoder(nn.Module):
"""
Donut Decoder based on Multilingual BART
Set the initial weights and configuration with a pretrained multilingual BART model,
and modify the detailed configurations as a Donut decoder

Args:
decoder_layer:
Number of layers of BARTDecoder
max_position_embeddings:
The maximum sequence length to be trained
name_or_path:
Name of a pretrained model name either registered in huggingface.co. or saved in local,
otherwise, `hyunwoongko/asian-bart-ecjk` will be set (using `transformers`)
"""

def __init__(
self, decoder_layer: int, max_position_embeddings: int, name_or_path: Union[str, bytes, os.PathLike] = None
):
super().__init__()
self.decoder_layer = decoder_layer
self.max_position_embeddings = max_position_embeddings

self.tokenizer = XLMRobertaTokenizer.from_pretrained(
"hyunwoongko/asian-bart-ecjk" if not name_or_path else name_or_path
)

self.model = MBartForCausalLM(
config=MBartConfig(
is_decoder=True,
is_encoder_decoder=False,
add_cross_attention=True,
decoder_layers=self.decoder_layer,
max_position_embeddings=self.max_position_embeddings,
vocab_size=len(self.tokenizer),
scale_embedding=True,
add_final_layer_norm=True,
)
)
self.model.forward = self.forward # to get cross attentions and utilize `generate` function

self.model.config.is_encoder_decoder = True # to get cross-attention
self.add_special_tokens(["<sep/>"]) # <sep/> is used for representing a list in a JSON
self.model.model.decoder.embed_tokens.padding_idx = self.tokenizer.pad_token_id
self.model.prepare_inputs_for_generation = self.prepare_inputs_for_inference

# weight init with asian-bart
if not name_or_path:
bart_state_dict = MBartForCausalLM.from_pretrained("hyunwoongko/asian-bart-ecjk").state_dict()
new_bart_state_dict = self.model.state_dict()
for x in new_bart_state_dict:
if x.endswith("embed_positions.weight") and self.max_position_embeddings != 1024:
new_bart_state_dict[x] = torch.nn.Parameter(
self.resize_bart_abs_pos_emb(
bart_state_dict[x],
self.max_position_embeddings
+ 2, # https://github.com/huggingface/transformers/blob/v4.11.3/src/transformers/models/mbart/modeling_mbart.py#L118-L119
)
)
elif x.endswith("embed_tokens.weight") or x.endswith("lm_head.weight"):
new_bart_state_dict[x] = bart_state_dict[x][: len(self.tokenizer), :]
else:
new_bart_state_dict[x] = bart_state_dict[x]
self.model.load_state_dict(new_bart_state_dict)

...
...

def forward(
self,
input_ids,
attention_mask: Optional[torch.Tensor] = None,
encoder_hidden_states: Optional[torch.Tensor] = None,
past_key_values: Optional[torch.Tensor] = None,
labels: Optional[torch.Tensor] = None,
use_cache: bool = None,
output_attentions: Optional[torch.Tensor] = None,
output_hidden_states: Optional[torch.Tensor] = None,
return_dict: bool = None,
):
"""
A forward fucntion to get cross attentions and utilize `generate` function

Source:
https://github.com/huggingface/transformers/blob/v4.11.3/src/transformers/models/mbart/modeling_mbart.py#L1669-L1810

Args:
input_ids: (batch_size, sequence_length)
attention_mask: (batch_size, sequence_length)
encoder_hidden_states: (batch_size, sequence_length, hidden_size)

Returns:
loss: (1, )
logits: (batch_size, sequence_length, hidden_dim)
hidden_states: (batch_size, sequence_length, hidden_size)
decoder_attentions: (batch_size, num_heads, sequence_length, sequence_length)
cross_attentions: (batch_size, num_heads, sequence_length, sequence_length)
"""
output_attentions = output_attentions if output_attentions is not None else self.model.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.model.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.model.config.use_return_dict
outputs = self.model.model.decoder(
input_ids=input_ids,
attention_mask=attention_mask,
encoder_hidden_states=encoder_hidden_states,
past_key_values=past_key_values,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)

logits = self.model.lm_head(outputs[0])

loss = None
if labels is not None:
loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
loss = loss_fct(logits.view(-1, self.model.config.vocab_size), labels.view(-1))

if not return_dict:
output = (logits,) + outputs[1:]
return (loss,) + output if loss is not None else output

return ModelOutput(
loss=loss,
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
decoder_attentions=outputs.attentions,
cross_attentions=outputs.cross_attentions,
)
...
...

Donut 使用公开可用的预训练多语言 BART 模型的权重来初始化解码器模型权重。

文本解码器的输出是生成的标记序列。

训练

预训练

预训练的目标是最小化下一个标记预测的交叉熵损失。这是通过对图像和先前上下文进行联合条件化来实现的。这个任务类似于伪OCR任务。模型本质上被训练为一个视觉语言模型,处理视觉语料库,如文档图像。

使用的训练数据是 IIT-CDIP ,这是一个包含1100万扫描英文文档图像的集合。同时,使用 Synthetic Document Generator (SynthDoG) 生成多语言数据,包括 英语 中文 日语 韩语 。它为每种语言生成了50万张图像。

生成的示例如图3所示。一个样本包含几个组件:背景、文档、文本和布局。

  • 背景图像来自ImageNet样本
  • 文档的纹理来源于收集的纸张照片。
  • 单词和短语来自维基百科。
  • 布局由一个简单的基于规则的算法生成,随机排列网格。

此外,利用各种图像渲染技术来模拟真实文档。

而且,图4显示了通过商业CLOVA OCR API获取的训练数据标签。

微调

微调的主要目的是适应下游任务。

例如,在文档分类任务中,解码器被训练生成一个令牌序列 [START class][memo][END class]。这个序列可以直接转换为 JSON 格式,如 {"class": "memo"}

牛轧糖

牛轧糖 是一个端到端、无OCR的小型模型,于2023年8月推出。它可以直接解析图像的内容。它接受从文学作品扫描的图像或从PDF转换的图像作为输入,并生成markdown作为输出。

模型架构

Nougat 是基于 Donut 架构开发的。它通过神经网络隐式识别文本,消除了对任何 OCR 相关输入或模块的需求,如图 5 所示。

训练数据集的构建

Nougat的模型并不是特别创新;其主要重点在于构建一个大型训练数据集,这是一项具有挑战性的任务。

Nougat通过创建由图像和markdown对组成的大规模训练数据,实施了一种具有成本效益的方法。这是Nougat最值得学习的方面。

数据源

由于缺乏包含PDF图像和markdown对的大规模数据集,Nougat从三个来源构建了数据集:arXiv PMC (PubMed Central)和IDL (行业文献库),如图6所示。

整体流程

ArXiv数据主要用于因为它包含TeX源代码。处理流程如图7所示。

如图7所示,主要目标是将现有资源,即PDF论文及其对应的TeX源代码,转换为对。每对由每个PDF页面的图像及其对应的Markdown组成。

获取图像作为输入

获取PDF页面图像 的过程相对简单;只需直接使用PyPDFium2的相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def rasterize_paper(
pdf: Union[Path, bytes],
outpath: Optional[Path] = None,
dpi: int = 96,
return_pil=False,
pages=None,
) -> Optional[List[io.BytesIO]]:
"""
Rasterize a PDF file to PNG images.

Args:
pdf (Path): The path to the PDF file.
outpath (Optional[Path], optional): The output directory. If None, the PIL images will be returned instead. Defaults to None.
dpi (int, optional): The output DPI. Defaults to 96.
return_pil (bool, optional): Whether to return the PIL images instead of writing them to disk. Defaults to False.
pages (Optional[List[int]], optional): The pages to rasterize. If None, all pages will be rasterized. Defaults to None.

Returns:
Optional[List[io.BytesIO]]: The PIL images if `return_pil` is True, otherwise None.
"""
pils = []
if outpath is None:
return_pil = True
try:
if isinstance(pdf, (str, Path)):
pdf = pypdfium2.PdfDocument(pdf)
if pages is None:
pages = range(len(pdf))
renderer = pdf.render(
pypdfium2.PdfBitmap.to_pil,
page_indices=pages,
scale=dpi / 72,
)
for i, image in zip(pages, renderer):
if return_pil:
page_bytes = io.BytesIO()
image.save(page_bytes, "bmp")
pils.append(page_bytes)
else:
image.save((outpath / ("%02d.png" % (i + 1))), "png")
except Exception as e:
logging.error(e)
if return_pil:
return pils

获取Markdown作为标签

如图7所示,为了获取markdown,我们必须首先将TeX源代码转换为HTML文件。然后,我们可以解析和格式化这些文件为markdown。

这涉及两个挑战。

第一个挑战是弄清楚如何对Markdown进行分页,因为训练数据由每个PDF页面的图像和相应的markdown作为标签组成。

由于每篇论文的LaTeX源文件尚未重新编译,我们无法像LaTeX编译器那样自动确定PDF文件的分页。

为实现这一目标,有必要利用当前可用的资源。策略是启发式地将原始PDF页面的文本与Markdown文本进行匹配。

具体来说,首先使用PDFMiner 提取PDF中的文本行 ,然后预处理文本以删除页码和潜在的标题或页脚。接着,训练一个tfidf_transformer模型 ,使用PDF行作为输入,页码作为标签。然后,应用训练好的模型 将Markdown划分为段落,并预测每个段落的页码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def split_markdown(
doc: str,
pdf_file: str,
figure_info: Optional[List[Dict]] = None,
doc_fig: Dict[str, str] = {},
minlen: int = 3,
min_num_words: int = 22,
doc_paragraph_chars: int = 1000,
min_score: float = 0.75,
staircase: bool = True,
) -> Tuple[List[str], Dict]:
...
...
if staircase:
# train bag of words
page_target = np.zeros(len(paragraphs))
page_target[num_paragraphs[1:-1] - 1] = 1
page_target = np.cumsum(page_target).astype(int)
model = BagOfWords(paragraphs, target=page_target)
labels = model(doc_paragraphs)

# fit stair case function
x = np.arange(len(labels))
stairs = Staircase(len(labels), labels.max() + 1)
stairs.fit(x, labels)
boundaries = (stairs.get_boundaries().astype(int)).tolist()
boundaries.insert(0, 0)
else:
boundaries = [0] * (len(pdf.pages))
...
...

最后进行一些收尾调整。

第二个挑战涉及PDF中的图表未能与Markdown文件中的位置对齐。

为了解决这个问题,Nougat最初使用pdffigures2 来提取图表。识别出的标题与TeX源代码中的标题进行比较,基于Levenshtein距离进行匹配 。此方法使我们能够确定每个图形或表格的TeX源代码和页码。这是因为图7的JSON结构 包含图表标题和相应的页码。

一旦Markdown被划分为单独的页面,之前提取的图表将重新插入到各自页面的末尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def split_markdown(
doc: str,
pdf_file: str,
figure_info: Optional[List[Dict]] = None,
doc_fig: Dict[str, str] = {},
minlen: int = 3,
min_num_words: int = 22,
doc_paragraph_chars: int = 1000,
min_score: float = 0.75,
staircase: bool = True,
) -> Tuple[List[str], Dict]:
...
...

# Reintroduce figures, tables and footnotes
figure_tex = list(doc_fig.keys()), list(doc_fig.values())
if len(doc_fig) > 0:
iterator = figure_info.values() if type(figure_info) == dict else [figure_info]
for figure_list in iterator:
if not figure_list:
continue
for i, f in enumerate(figure_list):
if "caption" in f:
fig_string = f["caption"]
elif "text" in f:
fig_string = f["text"]
else:
continue
ratios = []
for tex in figure_tex[1]:
if f["figType"] == "Table":
tex = tex.partition(r"\end{table}")[2]
ratios.append(Levenshtein.ratio(tex, fig_string))
k = np.argmax(ratios)
if ratios[k] < 0.8:
continue
if f["page"] < len(out) and out[f["page"]] != "":
out[f["page"]] += "\n\n" + remove_pretty_linebreaks(
figure_tex[1][k].strip()
)

for i in range(len(out)):
foot_match = re.findall(r"\[FOOTNOTE(.*?)\]\[ENDFOOTNOTE\]", out[i])
for match in foot_match:
out[i] = out[i].replace(
"[FOOTNOTE%s][ENDFOOTNOTE]" % match,
doc_fig.get("FOOTNOTE%s" % match, ""),
)

out[i] = re.sub(r"\[(FIGURE|TABLE)(.*?)\](.*?)\[END\1\]", "", out[i])
return out, meta

Pix2Struct

Pix2Struct 是一个经过预训练的图像到文本模型,专门用于纯视觉语言理解。此外,它可以针对许多下游任务进行微调。

模型架构

Pix2Struct 是一个基于 ViT 的图像编码器-文本解码器。

由于 Pix2Struct 的架构在论文中没有说明,并且在网上也找不到其他地方的相关信息,因此我在这里提供一个参考图,基于 ViT 架构,如图 8 所示。

使用 标准 ViT 方法,该方法在提取固定大小块之前将输入图像缩放到预定义分辨率,可能会产生两个负面影响:

  • 它可能会扭曲真实的宽高比,对于文档、移动用户界面和图形可能会有显著不同。
  • 将模型转移到具有更高分辨率的下游任务变得具有挑战性,因为模型在预训练期间仅观察到特定的分辨率。

因此,Pix2Struct 引入了一项小的增强功能,允许对输入图像进行保持宽高比的缩放,可以向上或向下缩放,如图 9 所示。

预训练任务

Pix2Struct 提出了一个截图解析目标,需要从网页的遮罩截图中预测基于 HTML 的解析。

  • 遮罩输入鼓励对其共现进行联合推理。
  • 使用简化的 HTML 作为输出是有利的,因为它提供了关于文本、图像和布局的清晰信号。

如图 10 所示,Pix2Struct 提出的截图解析有效地结合了几种知名的预训练策略的信号:

  • 恢复未遮罩部分。这个任务类似于 OCR,这是理解语言的基本技能。Donut 中也提出了使用合成渲染或 OCR 输出进行 OCR 预训练。在图 10 中,预测 <C++> 是这个学习信号的一个例子。
  • 恢复被遮罩部分。这个任务类似于 BERT 中的遮罩语言建模。然而,一个关键的区别是视觉上下文通常提供额外的强大线索。例如,预测图 10 中的 <Python> 是这种类型信号的一个例子。
  • 从图像中恢复 alt-text。这是一种常用的预训练图像标题策略的方法。在这种方法中,模型被允许使用网页作为额外的上下文。例如,预测 img alt=C++,如图 10 所示,体现了这个学习信号。

Pix2Struct 已经预训练了两个模型变体:

  • 一个包含 282M 参数的基础模型。
  • 一个包含 1.3B 参数的大型模型。

预训练数据集

预训练的目标是使 Pix2Struct 具备表示输入图像基本结构的能力。为此,Pix2Struct 根据 C4 语料库 中的 URL 生成自监督的输入图像和目标文本对。

Pix2Struct 收集了 8000 万个截图,每个截图都配有其 HTML 源文件。这大约占总文档数量的三分之一。每个截图的宽度为 1024 像素,高度根据内容的高度进行调整。获得的 HTML 源文件将被转换为简化的 HTML。

图 11 展示了预训练数据的截图,附有真实值和预测解析。

微调

微调Pix2Struct的主要步骤涉及对下游数据进行预处理。这确保了图像输入和文本输出准确地代表了任务。

图12展示了一些下游任务的示例。

关于预处理:

见解与思考

代表性无OCR解决方案的介绍到此结束,现在让我们谈谈见解与思考。

关于预训练任务

为了全面理解图像或PDF中的布局、文本和语义信息,Donut、Nougat和Pix2Struct设计了类似的训练任务:

  • Donut: 图像 → JSON-like格式
  • Nougat: 图像 → Markdown
  • Pix2Struct: 被遮罩的图像 → 简化的HTML

如果我们旨在开发自己的无OCR PDF解析工具,我们的初步步骤应该是设计训练任务。考虑所需的输出格式以及获取相应训练数据的挑战是至关重要的。

关于预训练数据

训练数据对无OCR方法至关重要。

获取Donut和Nougat的训练数据具有挑战性,因为(图像,JSON)和(图像,Markdown)对并不容易获得。

相反,Pix2Struct直接从公共数据集中的网页进行适配,使数据获取更加方便。然而,由于Pix2Struct的训练数据来自网页,这可能会引入有害内容。多模态模型对此特别敏感。Pix2Struct尚未实施措施来解决这些有害内容。

如果我们旨在开发一个无OCR的PDF解析工具,一种策略是使用公共数据逐步构建(输入,输出)对进行训练。

此外,确定输入图像的适当分辨率以及在一张图像中包含的PDF页面数量也是重要的考虑因素。

关于性能

Donut 和 Pix2Struct 都是支持多种下游任务的一般预训练模型。因此,它们的评估方法基于这些任务的基准。

根据 Pix2Struct 的实验,它在多个任务上的性能显著超过 Donut,并且在大多数任务上也超越了最先进的技术(SOTA),如图 13 所示:

然而,图 13 中显示的这些任务与我们之前定义的 PDF 解析任务 不同。在这方面,Nougat 更加专业。

Nougat 主要专注于 Markdown 的端到端生成。因此,它的评估方案依赖于编辑距离、BLEU、METEOR 和 F-measure,如图 14 所示。

此外,Nougat 可以更准确地将复杂元素,如 公式 表格 ,解析为 LaTeX 源代码,如图 15 和图 16 所示。

此外,Nougat 可以 方便地获取表格标题并将其与相应的表格关联

基于管道的方法 vs. 无OCR方法

图17比较了两种方法的整体架构和性能。左上角展示了基于管道的方法,而下左角则表示了Donut模型。

如图17右侧所示,Donut相比于基于管道的方法使用更少的存储,并提供更高的准确性。然而,它的运行速度较慢。其他无OCR解决方案与Donut类似。

OCR-Free 小模型方法的局限性

  • 尽管 基于管道的方法 涉及多个模型,但每个模型都很轻量。总参数量甚至可能显著少于无OCR模型。这一因素导致无OCR模型的解析速度较慢,这可能对大规模部署构成挑战。例如,尽管是小模型,Nougat的参数量为250MB或350MB。然而,其生成速度较慢,如Nougat的论文所述:

  • 为这种方法构建训练数据集的成本很高。这是由于需要构建大规模的图像-文本对。此外,它需要更多的GPU和更长的训练时间,增加了机器成本。
  • 此外,端到端方法无法针对特定的坏案例进行优化,导致更高的优化成本。在 基于管道的解决方案 中,如果表格处理模块表现不佳,仅需优化该模块。然而,对于端到端解决方案,在不改变模型架构的情况下,必须创建新的微调数据。这可能会导致其他场景中出现新的坏案例,例如公式识别。

结论

本文概述了基于小模型的无OCR PDF解析方法。它通过三个代表性模型作为例子深入探讨了这种方法,提供了详细的介绍并分享了所获得的见解。

通常,使用无OCR小模型的PDF解析方法的好处在于其一步到位的过程,避免了中间步骤可能造成的任何损害。然而,其有效性在很大程度上依赖于多模态模型的结构和训练数据的质量。此外,它的训练和推理速度较慢,使其在实用性上不如基于管道的方法。此外,该方法的可解释性也不如基于管道的方法强。

尽管需要改进,无OCR方法在表格和公式识别等领域表现良好。这些优势为我们构建自己的PDF解析工具提供了宝贵的见解。

如果您对PDF解析或文档智能感兴趣,请随时查看我的其他文章。

最后,如果本文存在任何错误或遗漏,或者您有任何想法想要分享,请在评论区指出。

  • 标题: 解密 PDF 解析 03无 OCR 的小型模型方法
  • 作者: Barry
  • 创建于 : 2024-06-02 02:25:06
  • 更新于 : 2024-08-31 06:59:45
  • 链接: https://wx.role.fun/2024/06/02/76930430e76648e589b1b6d0097d709d/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。