为了账号安全,请及时绑定邮箱和手机立即绑定

高精度检索增强生成在处理表格密集型文档中的应用(使用LangChain、Unstructured.io和KDB.AI)

为什么在表格上的丰富文档的RAG这么糟糕?

检索增强生成(RAG)革命一直在迅猛推进,但这条路上并非一路平坦,特别是在处理图像和表格等非文本内容时。其中一个让我头疼的问题是,每当要求RAG工作流从表格中提取特定数值时,准确率往往会下降。当文档中包含多个相关主题的表格时,如在收益报告中常见的情况,问题就更加严重了。因此,我决定着手改进我在RAG管道中对表格的检索功能……

主要挑战:
  1. 检索不一致:向量搜索算法常常难以准确找到正确的表格,特别是在包含多个外观相似的表格的文档中。
  2. 生成不准确:大型语言模型(LLM)经常误解或误读表格中的值,尤其是在结构复杂的表格中,具有嵌套列。我的假设是,这可能是由于格式不一致造成的。

[解决办法]

我主要考虑了以下几个关键概念:

  1. 精准提取:从文档中干净地提取所有表格。
  2. 上下文增强:利用大型语言模型(LLM)通过分析提取的表格及其周围文档内容,生成每个表格的稳健且具有上下文的描述。
  3. 格式统一化:使用大型语言模型将表格转换为统一的Markdown格式,提高嵌入效率并增强LLM的理解能力。
  4. 统一嵌入:通过结合上下文描述和Markdown格式化的表格生成“表格块”,优化其在向量数据库中的存储和检索性能。

经过上下文处理和格式标准化之后,一个表格元素将包含哪些内容?

实现

目标: 构建一个从Meta的2024年第二季度财报(包括文本和表格)中检索和回答问题的RAG管道,该管道旨在从文档的文本和多个表格中检索并回答问题。

实施架构

点击链接查看完整的 Google Colab 笔记本,或在GitHub上克隆并修改代码。本文介绍了如何使用上下文化的表格片段来创建一个 RAG 管道,完整的笔记本还包括了使用非上下文化的表格片段的对比。

第一步:精准地提取

首先,我们需要从文档中提取文本和表格,为此我们将用到Unstructured.io

我们来安装并引入这些依赖项吧。

    !apt-get -qq install poppler-utils tesseract-ocr  
    %pip install -q --user --upgrade pillow  
    %pip install -q --upgrade unstructured["all-docs"]  
    %pip install kdbai_client  
    %pip install langchain-openai  
    %pip install langchain  
    %pip install langchain-community  
    %pip install pymupdf  
    %pip install --upgrade nltk  

    import os  
    from getpass import getpass  
    import openai  
    from openai import OpenAI  
    from unstructured.partition.pdf import partition_pdf  
    from unstructured.partition.auto import partition  
    from langchain_openai import OpenAIEmbeddings  
    import kdbai_client as kdbai  
    from langchain_community.vectorstores import KDBAI  
    from langchain.chains import RetrievalQA  
    from langchain_openai import ChatOpenAI  
    import fitz  
    # 下载 punkt 分词器模型
    nltk.download('punkt')

设置你的 OpenAI API 密钥(key):

    # 设置 OpenAI API  
    if "OPENAI_API_KEY" in os.environ:  
        OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]  
    else:  
        # 提示用户输入 API 密钥  
        OPENAI_API_KEY = getpass("OPENAI API KEY: ")  
        # 将 API 密钥保存为当前会话的环境变量中  
        os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY

下载 Meta 2024 年第二季度财报 PDF(包含很多表格!):

运行wget命令来下载文件 `https://s21.q4cdn.com/399680738/files/doc_news/Meta-Reports-Second-Quarter-2024-Results-2024.pdf' -O './doc1.pdf'

我们将使用Unstructured提供的‘partition_pdf’功能,使用‘hi_res’高分辨率策略从PDF财报报告中提取文本和表格元素。

在分区时,我们可以设置一些相关的参数以便我们能够准确地从 PDF 中提取表格。

  • strategy = “hi_res” :用于识别文档的布局,尤其推荐在需要准确元素分类的场景中使用,比如表格元素。
  • chunking_strategy = “by_title”:‘by_title’分块策略通过在遇到‘标题’元素时开始新的分块来保留部分边界,即使当前分块仍有空间,确保来自不同部分的文本不会出现在同一个分块中。你还可以通过设置max_characters和new_after_n_chars来控制分块的大小。
将 PDF 文件分割成多个元素,策略为 hi_res,分块策略为 by_title,最大字符数为 2500,每 2300 个字符后新建一个元素。
elements = partition_pdf('./doc1.pdf',  
                                  strategy="hi_res",  
                                  chunking_strategy="by_title",  
                                  max_characters=2500,  
                                  new_after_n_chars=2300,  
                                  )

我们来看看都提取了哪些元素:

    导入 collections 中的 Counter  
    输出 Counter(对于 elements 中的每个元素,获取其类型)
    >>> Counter({unstructured.documents.elements.CompositeElement: 17,  
             unstructured.documents.elements.Table: 10})

提取出了17个CompositeElement元素,基本上是文本片段。另外有10个Table元素,即提取出的表格部分。

到目前为止,我们已经从文档中提取了文本块和表格。

第二步和第三步:表格内容丰富和格式标准化:

我们来看看一个 Table 元素,看看我们能不能理解为什么这个元素在 RAG 管道中可能会有问题。倒数第二个就是 Table 元素:

    print(elements[-2])  
    >>>2024年收入的外汇影响(基于2023年汇率) 不包括外汇影响的收入2024年 按GAAP计算的收入同比变化% 不包括外汇影响的收入同比变化% 按GAAP计算的广告收入 2024年广告收入的外汇影响(基于2023年汇率) 2024年广告收入(排除外汇影响) 2024年 $ 39,071美元 $ 39,442美元 22 % 23 % $ 38,329美元 $ 38,696美元 22 % 2023年 $ 31,999美元 $ 31,498美元 2024年 $ 75,527美元 $ 75,792美元 25 % 25 % $ 73,965美元 $ 74,226美元 24 % 2023年 按GAAP计算的广告收入同比变化% 不包括外汇影响的广告收入同比变化% 23 % 25 % 经营活动提供的净现金 购置固定资产的净额 财务租赁本金支付 $ 19,370美元 (8,173美元) (299美元) $ 10,898美元 $ 17,309美元 (6,134美元) (220美元) $ 10,955美元 $ 38,616美元 (14,573美元) (614美元) $ 23,429美元

我们看到表格被表示为一个混合了自然语言和数字的长字符串。如果我们只是将这个长字符串作为表格片段输入到RAG流程中,这将使得判断是否应该检索这个表格变得很困难。

我们需要为每个表格添加更多相关信息,然后将表格转换成Markdown格式。

首先,我们将从PDF文档中提取整个文本作为上下文。

    def 提取PDF文本(pdf_path):  
        内容 = ""  
        with fitz.open(pdf_path) as doc:  
            for 页面 in doc:  
                内容 += 页面.get_text()  
        return 内容  

    pdf_path = './doc1.pdf'  
    文档内容 = 提取PDF文本(pdf_path)

接下来,创建一个函数,该函数将接收来自上述代码的整个文档背景,以及特定表格的内容,并输出一个包含表格详细描述的新描述文本。具体来说,将表格转换成Markdown格式。

    # 初始化OpenAI客户端  
    client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))  

    # 获取表格描述的函数,根据表格内容和文档上下文生成描述。  
    def get_table_description(table_content, document_context):  
        prompt = f"""  
        根据以下表格及其在原始文档中的上下文,提供一个详细的表格描述。然后将表格以Markdown格式包含进去。  

        原始文档上下文:  
        {document_context}  

        表格内容:  
        {table_content}  

        请提供:  

1. 表格的详细描述。  

2. 以Markdown格式的表格。  
        """  

        response = client.chat.completions.create(  
            model="gpt-4",  
            messages=[  
                {"role": "system", "content": "请描述表格,并将其格式化为Markdown格式。"},  
                {"role": "user", "content": prompt}  
            ]  
        )  

        return response.choices[0]['message']['content']

现在,将刚才提到的函数应用到所有表格项上,并将每个表格项的原始文本替换为包含上下文说明和 Markdown 格式化的表格的新描述。

    # 处理目录中的每个表
    for element in elements:
      if element.to_dict()['type'] == 'Table':
        table_content = element.to_dict()['text']

        # 从GPT-4o获取描述和Markdown格式的表格
        result = get_table_description(table_content, document_content)
        # 将每个表元素的文本替换为新的描述
        element.text = result

    print("处理完毕。")

以下为增强的表格片段示例/元素。
(以Markdown格式编写)。

    这个Markdown表格简洁地呈现了财务数据,使其易于在数字格式下阅读和理解。
    ### 表格的详细说明

    该表展示了Meta Platforms, Inc.的分部信息,包括营业收入和营业利润。数据分为两个主要部分:

1. **营业收入**:此部分分为两个类别:“广告”和“其他营业收入”。这两个子类别生成的总营业收入然后汇总到两个分部:“应用程序家族”和“现实实验室”。该表提供了截至2024年和2023年6月30日的三个月和六个月的营业收入数据。

2. **营业利润**:此部分显示了“应用程序家族”和“现实实验室”分部的营业利润,同样涵盖了相同的时间段,即三个月和六个月的结束日期。

    该表允许比较Meta业务的两个分部随时间的变化,展示每个分部的营业收入和营业利润。

    ### Markdown格式的表格

    ```markdown
    ### 分部信息(单位:百万美元,未经审计)

    |                             | 2024年截至6月30日的三个月 | 2023年截至6月30日的三个月 | 2024年截至6月30日的六个月 | 2023年截至6月30日的六个月 |
    |---------------------------- | ---------------------------------- | ---------------------------------- | ------------------------------- | ------------------------------- |
    | **营业收入:**               |                                  |                                  |                                |                                |
    | 广告                        | $38,329                          | $31,498                          | $73,965                       | $59,599                       |
    | 其他营业收入                | $389                             | $225                             | $769                          | $430                          |
    | **应用程序家族**            | $38,718                          | $31,723                          | $74,734                       | $60,029                       |
    | 现实实验室                  | $353                             | $276                             | $793                          | $616                          |
    | **总营业收入**              | $39,071                          | $31,999                          | $75,527                       | $60,645                       |
    |                             |                                  |                                  |                                |                                |
    | **营业利润:**               |                                  |                                  |                                |                                |
    | 应用程序家族                | $19,335                          | $13,131                          | $36,999                       | $24,351                       |
    | 现实实验室                  | $(4,488)                         | $(3,739)                         | $(8,334)                      | $(7,732)                      |
    | **总营业利润**              | $14,847                          | $9,392                           | $28,665                       | $16,619                       |

如您所见,这比表格项原来的文本提供了更多的上下文信息,应该会显著提升我们RAG管道的性能。我们现在有了具有完整上下文的表格片段,可以将它们嵌入并存储在向量数据库中以备检索。

# 第 4 步:统一嵌入模型……准备 RAG 阶段

现在所有元素都具备了进行高质量检索和生成所需的所有背景信息,我们将这些数据或信息进行嵌入处理,并将它们存储在[KDB.AI](http://kdb.ai)向量数据库中。

首先,我们将为每个元素创建嵌入表示,这些嵌入表示只是每个元素语义的数值表示形式。

from unstructured.embed.openai import OpenAIEmbeddingConfig, OpenAIEmbeddingEncoder

embedding_encoder = OpenAIEmbeddingEncoder(
config=OpenAIEmbeddingConfig(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small",
)
)
elements = embedding_encoder.embed_documents(
elements=elements
)


接下来,接着创建一个Pandas DataFrame来存储我们的元素。该DataFrame将包含基于每个元素提取出的属性列。例如,Unstructured为每个元素生成了ID、文本(针对表格元素进行了处理)、元数据和嵌入(上面创建的)。我们将这些数据存储在DataFrame中,因为这种格式便于导入到KDB.AI的向量数据库中。

import pandas as pd
data = []

for c in elements:
row = {}
row['id'] = c.id
row['text'] = c.text
row['metadata'] = c.metadata.to_dict()
row['embedding'] = c.embeddings
data.append(row)

df = pd.DataFrame(data)


**设置KDB.AI云的步骤:**

在这里可以免费拿到KDB.AI的API密钥:<https://trykdb.kx.com/kdbai/signup/>
KDBAI_ENDPOINT = (  
    os.environ["KDBAI_ENDPOINT"]  
    if "KDBAI_ENDPOINT" in os.environ  
    else 输入("KDB.AI 端点: ")  
)  
KDBAI_API_KEY = (  
    os.environ["KDBAI_API_KEY"]  
    if "KDBAI_API_KEY" in os.environ  
    else 输入密码("KDB.AI 的 API 密钥: ")  
)  

session = kdbai.Session(api_key=KDBAI_API_KEY, endpoint=KDBAI_ENDPOINT)

你现在已经连接到了向量数据库实例,下一步是定义好准备在KDB.AI中创建的表格模式。
schema = [  
    {'name': 'id', 'type': 'str'},  
    {'name': 'text', 'type': 'bytes'},  
    {'name': 'metadata', 'type': '元数据'},  
    {'name': 'embedding', 'type': 'float32 数组'}  
]

我们为之前创建的 DataFrame 中的每一列在模式中创建相应的列。(id,文本,元数据 embedding)。embedding 列将用于执行检索的向量搜索。

下面,我们定义索引。这里定义了几个参数值。

* _name_ :索引名称。
* _column_ :此索引将应用于上述模式中的哪一列。在这种情况下,是“embedding”列。
* _type_ :索引的类型,在这里使用的是平面索引,但也可以使用qFlat(磁盘上的平面索引)、HNSW(分层最近邻搜索)、IVF(倒排文件索引)或IVFPQ(倒排文件聚类量化)。
* _params_ :使用的_dims_和向量搜索_metric_。_dims_是每个嵌入的维度数,由所使用的嵌入模型决定。在这种情况下,OpenAI的“text-embedding-3-small”输出1536维的嵌入。_metric_,即 L2 距离,是欧几里得距离,其他选项有余弦相似度和点积。

索引列表如下:

indexes = [  
       {'name': 'flat_index',   
        'column': 'embedding',   
        'type': 'flat',   
        'params': {'dims': 1536, 'metric': 'L2'}}  
]

根据上述结构创建表格。

    # 连接到 KDB.AI 的默认数据库
    database = session.database('default')

    # 定义表名
    KDBAI_TABLE_NAME = "Table_RAG"

    # 首先检查表是否已经存在
    if KDBAI_TABLE_NAME in database.tables:
        # 如果存在则删除表
        database.table(KDBAI_TABLE_NAME).drop()

    # 使用定义的表名、模式和索引来创建表
    table = database.create_table(table=KDBAI_TABLE_NAME, schema=schema, indexes=indexes)

将数据帧插入 KDB.AI 表

# 将数据插入KDB.AI表中
table.insert(df)

所有元素现在都存储在向量数据库中,这个数据库已经准备好进行检索。

使用LangChain和KDB.AI进行检索增强生成!

使用LangChain的基本配置:如下

    # 定义一个名为 embeddings 的变量,它是一个 OpenAI 的嵌入模型对象,用于将查询嵌入处理  
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")  

    # 使用 KDBAI 作为向量数据库  
    vecdb_kdbai = KDBAI(table, embeddings)

定义一个RAG链路,使用KDB.AI作为检索模块,使用gpt-4o作为生成模型的LLM:

    # 定义一个问答 LangChain 链
    # chain_type="stuff" 表示将所有内容直接传递给模型
    qabot = RetrievalQA.from_chain_type(  
        chain_type="stuff",  
        llm=ChatOpenAI(model="gpt-4-o"),  
        retriever=vecdb_kdbai.as_retriever(作为检索器(search_kwargs=dict(k=5, index='flat_index'))),  
        return_source_documents=True,  
    )

用于执行RAG的辅助函数

    # 一个用于执行RAG的辅助函数  
    def RAG(query):  
      print(query)  
      print("-----")  
      return qabot.invoke({'query': query})['result']
我们的发现

例子 1:

    # 查询RAG链吧!
    RAG('2024年GAAP广告收入在6月30日结束的三个月里是多少?经营活动产生的净现金流量呢?')

结果如下:
截至2024年6月30日的季度

  • 根据美国公认会计原则(GAAP),广告收入为383.29亿美元。
  • 经营活动产生的净现金流为193.70亿美元。

从原始表中获取的结果

例子 2:

    # 查询一下RAG链!
    RAG("2023年第三季度的成本和费用是多少?")

结果:
2023年第二季度,Meta Platforms, Inc. 的成本和费用为226.07亿美元。

原始表格中检索到的结果

例子 3:

     # 查询RAG链!  
    RAG("2023年底,Meta的商誉资产价值是多少?")

结果:
2023年底时,Meta的商誉资产价值为206.54亿美元

从原始表格获取的结果

例子 4:

     # 查询RAG链吧!
    RAG("截至2024年6月的半年内研发成本是多少?")

结果:

在截至2024年6月的这六个月里,研发成本总计20.515亿美元。

zh: 原始数据表中获取的结果

!!注意:这是一个示例,如果你使用了没有上下文的表格片段,就会导致错误。这意味着表格越复杂,添加额外的上下文和格式就越有用。

例子 5,

     # 查询RAG链吧!
    RAG("请给前景的情感评分打1到10分?说说你的理由吧")

那就是:

我会给Meta Platforms, Inc.的前景打**满分10分中的8分**。以下是这个评分的依据:

### 积极指标:
1. **每股盈利(EPS)显著增长**:
   - **基本每股盈利**从2023年第二季度的3.03美元增加到2024年第二季度的5.31美元,从2023年上半年的5.24美元增加到2024年上半年的10.17美元。
   - **稀释每股盈利**也大幅增长,从2023年第二季度的2.98美元增加到2024年第二季度的5.16美元,从2023年上半年的5.18美元增加到2024年上半年的9.86美元。

2. **营收增长**:
   - 营收从2023年第二季度的319.99亿美元增至2024年第二季度的390.71亿美元,增长了22%。

3. **运营收入增加**:
   - 运营收入从2023年第二季度的93.92亿美元增加到2024年第二季度的148.47亿美元,增长了58%。
   - 运营利润率从2023年第二季度的29%提高到2024年第二季度的38%。

4. **净利润增长**:
   - 2024年第二季度净利润为134.65亿美元,比2023年第二季度的77.88亿美元增长了73%。

5. **实际税率**:
   - 实际税率从2023年第二季度的16%下降到2024年第二季度的11%,有助于整体盈利能力。

### 消极或中性指标:
1. **成本与费用增加**:
   - 总成本与费用从2023年第二季度的226.07亿美元增加到2024年第二季度的242.24亿美元,增长了7%。

2. **留存收益额下降**:
   - 留存收益额从2023年底的820.70亿美元略微减少到2024年6月30日的811.88亿美元。

### 结论:
每股盈利、营收、运营收入和净利润的显著改善表明Meta Platforms, Inc.具有强劲的财务表现和良好的前景。成本与费用的增加以及留存收益额的略有下降是需要关注的地方,但这些并不抵消整体积极的势头。因此,8分的评分体现了公司在前景良好的同时,也需要对成本保持谨慎的监控。

我们发现LLM能够利用表格中的数字来为其生成的情感评分提供依据。

考虑因素

虽然这可能会提高你的表格密集型RAG管道的效果,但这是一种更昂贵的方法,因为它需要获取并生成这些上下文。此外,对于少数包含简单表格的数据集而言,这样做可能没有必要。我的实验显示,对于简单的表格,使用非上下文化处理的表格片段效果相当好,然而,当表格包含如“示例4”中所示的嵌套列时,非上下文化处理的表格片段表现差一些。

结束部分

检索增强生成(RAG)在处理包含大量表格的文档中面临的挑战,需要采取一种系统的方法来解决检索不一致和生成不准确的问题。通过实施包括精确提取、上下文丰富、格式标准化和统一嵌入在内的策略,我们可以显著提升RAG流程在处理复杂表格方面的表现。

我们的元收益报告的示例结果突显了生成答案的质量,当我们使用这些增强的表格片段时。随着RAG技术的不断进步,这些技术可能成为确保结果可靠和准确的好工具,特别是在包含大量表格数据的情况下。

试试其他几个不错的示例笔记本:
- 多模态RAG
- 元数据筛选
- 时间相似性搜索
- 混合型搜索

可以和我连个LinkedIn:点击这里

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消