为什么在表格上的丰富文档的RAG这么糟糕?
检索增强生成(RAG)革命一直在迅猛推进,但这条路上并非一路平坦,特别是在处理图像和表格等非文本内容时。其中一个让我头疼的问题是,每当要求RAG工作流从表格中提取特定数值时,准确率往往会下降。当文档中包含多个相关主题的表格时,如在收益报告中常见的情况,问题就更加严重了。因此,我决定着手改进我在RAG管道中对表格的检索功能……
主要挑战:- 检索不一致:向量搜索算法常常难以准确找到正确的表格,特别是在包含多个外观相似的表格的文档中。
- 生成不准确:大型语言模型(LLM)经常误解或误读表格中的值,尤其是在结构复杂的表格中,具有嵌套列。我的假设是,这可能是由于格式不一致造成的。
[解决办法]
我主要考虑了以下几个关键概念:
- 精准提取:从文档中干净地提取所有表格。
- 上下文增强:利用大型语言模型(LLM)通过分析提取的表格及其周围文档内容,生成每个表格的稳健且具有上下文的描述。
- 格式统一化:使用大型语言模型将表格转换为统一的Markdown格式,提高嵌入效率并增强LLM的理解能力。
- 统一嵌入:通过结合上下文描述和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:点击这里
共同学习,写下你的评论
评论加载中...
作者其他优质文章