image

引言

在信息化时代,数据和知识的快速提取变得尤为重要。特别是对于学术和技术文档,如何从格式丰富且结构复杂的PDF文件中准确地提取信息,是提高研究效率的关键。本文将探讨几种先进的技术方法,用于优化学术论文的PDF解析及其表格内容的处理。

对于文档解析,langchain 和 llama_index 提供的 document loader 能够支持多种文件类型,覆盖绝大多数文档格式的需求。但对于pdf解析而言还存在着一些挑战。

人类与机器的阅读差异

尽管PDF格式适合人类阅读,提供了优秀的视觉体验和格式保真,但它并不适合机器阅读。PDF文件通常包含复杂的布局和多样的内容元素,如多列布局、公式和表格,这些都给自动解析带来了挑战。

常见的PDF解析问题

使用传统的PDF解析库可能遇到多种问题:

  • 多列布局导致的文本流读取错误。

  • 公式和表格的解析效果差,难以正确提取信息。

  • 解析过程中结构化信息(如标题和列表)的丢失。

  • 影印版PDF的文本无法被标准OCR工具识别。

高级解析技术

根据unstractued提供的方案,文档解析可以大致分为两种方法:结构化解析和基于图像的解析。结构化解析侧重于从文档中提取文字和其它字符数据。而图像基解析则包括以下两种技术:

  1. OCR技术:首先使用目标检测模型对文档布局进行分析,识别出标题、表格等关键元素的位置,然后在这些位置上使用OCR技术提取文字。

  2. 基于Transformer的端到端解析:使用深度学习模型直接将图像转换为相应的文字内容。在这方面,Dount 和 Nougat 模型表现出色,尤其是 Nougat 能够将图像中的文章整理成Markdown或Latex格式,非常适合需要保留结构信息(如标题层级、公式和表格)的场景。

只可惜Nougat只针对英文的学术论文做了训练,但遇到其他语言如中文论文的效果就不是那么的好了。

必须强调一点的是,Markdown格式因其简洁性和易于解析的特点,被广泛认为是LLM(大型语言模型)友好的文档格式。Markdown通过明确的标记语法,帮助模型更好地理解文档结构和内容,从而提高信息提取的准确性和效率。特别在存在大量公式,表格的学术论文场景下,Markdown可能是更合适的格式选择。

快速上手:使用Nougat将pdf解析成适合LLM读取的markdown

依赖按照

!pip install -q pymupdf python-Levenshtein nltk
!pip install -q git+https://github.com/huggingface/transformers.git

Load model and processor

from transformers import AutoProcessor, VisionEncoderDecoderModel
import torch

processor = AutoProcessor.from_pretrained("facebook/nougat-base")
model = VisionEncoderDecoderModel.from_pretrained("facebook/nougat-base")
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)

将pdf转成图像

from typing import Optional, List
import io
import fitz
from pathlib import Path

def rasterize_paper(
    pdf: Path,
    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.
    """

    pillow_images = []
    if outpath is None:
        return_pil = True
    try:
        if isinstance(pdf, (str, Path)):
            pdf = fitz.open(pdf)
        if pages is None:
            pages = range(len(pdf))
        for i in pages:
            page_bytes: bytes = pdf[i].get_pixmap(dpi=dpi).pil_tobytes(format="PNG")
            if return_pil:
                pillow_images.append(io.BytesIO(page_bytes))
            else:
                with (outpath / ("%02d.png" % (i + 1))).open("wb") as f:
                    f.write(page_bytes)
    except Exception:
        pass
    if return_pil:
        return pillow_images
from transformers import StoppingCriteria, StoppingCriteriaList
from collections import defaultdict

class RunningVarTorch:
    def __init__(self, L=15, norm=False):
        self.values = None
        self.L = L
        self.norm = norm

    def push(self, x: torch.Tensor):
        assert x.dim() == 1
        if self.values is None:
            self.values = x[:, None]
        elif self.values.shape[1] < self.L:
            self.values = torch.cat((self.values, x[:, None]), 1)
        else:
            self.values = torch.cat((self.values[:, 1:], x[:, None]), 1)

    def variance(self):
        if self.values is None:
            return
        if self.norm:
            return torch.var(self.values, 1) / self.values.shape[1]
        else:
            return torch.var(self.values, 1)


class StoppingCriteriaScores(StoppingCriteria):
    def __init__(self, threshold: float = 0.015, window_size: int = 200):
        super().__init__()
        self.threshold = threshold
        self.vars = RunningVarTorch(norm=True)
        self.varvars = RunningVarTorch(L=window_size)
        self.stop_inds = defaultdict(int)
        self.stopped = defaultdict(bool)
        self.size = 0
        self.window_size = window_size

    @torch.no_grad()
    def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor):
        last_scores = scores[-1]
        self.vars.push(last_scores.max(1)[0].float().cpu())
        self.varvars.push(self.vars.variance())
        self.size += 1
        if self.size < self.window_size:
            return False

        varvar = self.varvars.variance()
        for b in range(len(last_scores)):
            if varvar[b] < self.threshold:
                if self.stop_inds[b] > 0 and not self.stopped[b]:
                    self.stopped[b] = self.stop_inds[b] >= self.size
                else:
                    self.stop_inds[b] = int(
                        min(max(self.size, 1) * 1.15 + 150 + self.window_size, 4095)
                    )
            else:
                self.stop_inds[b] = 0
                self.stopped[b] = False
        return all(self.stopped.values()) and len(self.stopped) > 0

将pdf转成markdown

from tqdm import tqdm

def trans_pdf_to_markdown(filepath):
  output = ""

  images = rasterize_paper(pdf=filepath, return_pil=True)
  for i in tqdm(range(len(images))):
    image = Image.open(images[i])
    pixel_values = processor(images=image, return_tensors="pt").pixel_values

    # autoregressively generate tokens, with custom stopping criteria (as defined by the Nougat authors)
    outputs = model.generate(pixel_values.to(device),
                              min_length=1,
                              max_length=3584,
                              bad_words_ids=[[processor.tokenizer.unk_token_id]],
                              return_dict_in_generate=True,
                              output_scores=True,
                              stopping_criteria=StoppingCriteriaList([StoppingCriteriaScores()]),
    )
    generated = processor.batch_decode(outputs[0], skip_special_tokens=True)[0]
    generated = processor.post_process_generation(generated, fix_markdown=True)

    output+=generated
    output+="\n\n"

  return output

filepath = 'xxx.pdf'
makrdown_ = trans_pdf_to_markdown(filepath)

具体参考Nougat的官方示例。

表格类数据的RAG问题

表格类目前的解决方案主要有两种:

对于内容较小的表格,一般采用LLM对表格进行summary, 以table summary构建查询的index,在召回后, 进行表格内容的复原或者根据问题后处理提供至LLM进行生成。

对于内容较多的表格, 一般采取结构化处理, 也就是存储至数据库, 在查询阶段根据问题匹配对应的表schema, 有模型执行text-to-sql任务,生成sql并执行获得结果,最终根据结果回答内容。

对于第一种方案,在Langchain 的cookbook中提到了Semi-structured RAG的方法值得借鉴;

Untitled.png

此外在HRoT这篇工作中, 也提到了类似的方法;

截屏2024-04-17 16.22.17.png

在HRoT这篇工作中, 增加了将table基于问题进行重构的算法,

Type-Aware Table Reconstruction algorithm

截屏2024-04-17 16.31.19.png

该算法的实现步骤如下:

  1. 问题分类:首先,算法对问题进行分类,判断问题是算术问题还是跨列选择问题。这有助于确定需要从表格中检索的信息类型。

  2. 获取表格和证据:对于算术问题,算法获取与问题相关的表格集合。然后,从这些表格中提取出作为证据的文本和表格片段。

  3. 表格分区:对于每个表格,算法将其分区为多个子表格。这是通过获取表格的跨度列表(Lt)来完成的,该列表包含了表格中每个头部(行和列)的起始和结束位置。

  4. 确定保留的行和列:算法根据问题类型和证据来确定哪些行和列是回答问题所必需的。这是通过分析每个证据的子标题(即表格中的行和列标题)来实现的。

  5. 重建表格:在确定了需要保留的行和列之后,算法根据这些信息重建表格。重建的表格只包含对问题回答有帮助的信息,从而减少了无关信息的干扰。

在TableQuery 这篇工作中, 利用一系列模块组件,实现了基于LLM对数据库的高效查询。

截屏2024-04-17 19.57.08.png

TableQuery的架构设计包括以下几个主要模块:

  1. Datastore:Datastore是一个表格集合,可以通过自然语言查询。这些表格可以是数据库或电子表格目录。除了表格数据,Datastore还包含模式文件,这些文件包含每个表的元数据,如表关键词、列名、列类型、列关键词(用户为列名提供的关键词)等。模式文件可以手动为这些表创建,也可以通过应用各种启发式方法自动生成。

  2. Table Selector:表选择器根据输入查询从Datastore中选择合适的表。这是通过从输入查询中提取关键词,并找到与问题关键词(以及模式中的表关键词、列名、列关键词等)重叠系数最大的表来完成的。

  3. Known Fields Extractor:已知字段提取器提取查询中已经给出值的列。对于这些列中的每一个,也提取了相应的值。这是通过一个预训练的深度学习模型来完成的,该模型用于在自由文本上执行问答。

  4. Unknown Fields Extractor:未知字段提取器提取需要从选定表中检索值的列。这是通过排除已知字段提取器已经从查询中提取的列,并找到其列关键词与问题关键词重叠系数最大的列来完成的。

  5. Aggregate Function Classifier:一些查询可能需要对选定表中的未知字段的值执行进一步操作。这是通过在生成的SQL查询中包含SQL聚合函数(如COUNT、SUM、MIN、MAX、AVG等)来实现的。聚合函数分类器决定给定输入查询要使用的聚合函数。为了执行这项任务,作者训练了一个两层的神经网络,该网络以使用通用句子编码器编码的查询作为输入,并输出要使用的适当聚合函数(如果有的话)。

  6. SQL Generator:结合已知字段和值、未知字段和聚合函数来构建SQL查询,当该查询在选定的表上运行时,返回所需的结果。

关于text-to-sql的更详尽内容, 可以参考这篇综述文章:Natural Language Interfaces for Tabular Data Querying and Visualization: A Survey。 限于篇幅就不展开了。

参考文献

langchain: Document loaders

Nougat: Neural Optical Understanding for Academic Documents

langchain: Semi-structured RAG

HRoT: Hybrid prompt strategy and Retrieval of Thought for Table-Text Hybrid Question Answering

TableQuery: Querying tabular data with natural language

Natural Language Interfaces for Tabular Data Querying and Visualization: A Survey