
构建rag系统的完整指南:使用nvidia 10-k文件实现数据驱动的生成与检索
与NVIDIA最新的10-K文件聊天
作者提供的图片。
在本教程中,我们将探索使用NVIDIA最新的10-K文件作为数据源创建检索增强生成(RAG)系统。
RAG结合了检索和生成,从给定的数据集中提供基于事实和上下文的答案。
这种方法确保响应基于提供的信息,从而最小化与语言模型相关的幻觉。
在本教程结束时,我们将构建一个管道,该管道:
- 从SEC EDGAR数据库下载并处理真实的金融数据。
- 将数据拆分和分块以实现高效的检索。
- 将分块嵌入高维空间,以便使用FAISS进行相似性搜索。
- 为给定查询检索相关信息。
- 使用大型语言模型(LLM)生成连贯且准确的答案。
为什么检索增强生成?
它非常适合以下任务:
- 总结公司报告。
- 为特定领域的问题提供上下文答案。
- 使用户能够“与他们的数据聊天。”
教程概述
我们将使用 NVIDIA 最新的 10-K 文件 作为我们的数据集。以下是我们将要涵盖的内容:
- 数据获取:如何直接从 SEC EDGAR 数据库获取最新的 10-K 文件。
- 数据预处理:将内容拆分为可管理的块,并具有重叠的上下文。
- 嵌入和索引:利用 sentence-transformers 进行嵌入,并使用 FAISS 进行高效的相似性搜索。
- 查询数据:检索与用户查询相关的块。
- 基于上下文的回答:使用预训练的大型语言模型生成基于检索信息的答案。
在我们开始之前
如果您在 Google Colab(或任何其他云平台)上运行此代码,请确保已安装所有必要的依赖项。
运行以下代码块以安装所需的软件包:
%%capture
!pip install transformers faiss-gpu==1.7.2 sentence-transformers==3.0.1
加载最新的 NVIDIA 年度财务报表
您无需理解或甚至阅读以下代码。
import requests
from bs4 import BeautifulSoup
def get_latest_10k_text(cik):
"""
获取给定公司 CIK 的最新 10-K 文件,并将内容作为单个文本返回。
参数:
- cik (str): 公司的 CIK 号码。
返回:
- str: 最新 10-K 文件的文本内容。
"""
base_url = "https://data.sec.gov/submissions/"
headers = {"User-Agent": "YourName [email protected]"}
response = requests.get(f"{base_url}CIK{cik}.json", headers=headers)
response.raise_for_status()
data = response.json()
filings = data["filings"]["recent"]
for i, form_type in enumerate(filings["form"]):
if form_type == "10-K":
accession_number = filings["accessionNumber"][i].replace("-", "")
file_url = f"https://www.sec.gov/Archives/edgar/data/{cik}/{accession_number}/index.json"
break
else:
raise ValueError("未找到给定 CIK 的 10-K 文件。")
filing_response = requests.get(file_url, headers=headers)
filing_response.raise_for_status()
filing_data = filing_response.json()
primary_doc = None
for doc in filing_data["directory"]["item"]:
if doc["name"].endswith(".htm") or doc["name"].endswith(".txt"):
primary_doc = doc["name"]
break
if not primary_doc:
raise ValueError("无法在文件中找到主要文档。")
filing_url = f"https://www.sec.gov/Archives/edgar/data/{cik}/{accession_number}/{primary_doc}"
filing_content = requests.get(filing_url, headers=headers).text
soup = BeautifulSoup(filing_content, "html.parser")
text_content = soup.get_text(separator=" ", strip=True)
return text_content
cik = "0001045810"
try:
latest_10k_text = get_latest_10k_text(cik)
print(latest_10k_text[:1000])
except Exception as e:
print(f"错误: {e}")
输出:
我们将仅保留项目 1 和项目 1A 用于本项目。
latest_10k_text = latest_10k_text[30332:185600]
步骤 1. 将文本分割成块
首先,让我们将文本分割并创建一个包含所有句子的列表。
sentences = [sentence.strip() for sentence in latest_10k_text.split('.') if sentence.strip()]
len(sentences)
输出:
如果按字符“.”分割,我们得到了 878 个句子。
我们可以直接使用这些句子来创建块,但我们将使用 重叠块。
在处理用于检索系统的文本时,确保没有关键信息丢失是至关重要的。
这对于那些思想和事实常常跨越多个句子或部分的文档尤其重要。
一种称为 重叠块 的技术可以帮助保持上下文的连贯性和相关性。
重叠块包括来自周围文本的部分句子,确保每个块的边界保留相邻部分的上下文。
这种方法最小化了在将文本分割成离散部分时可能发生的意义丧失。
例如,如果我们将文本分成三个句子的块,并重叠一个句子:
- 块 1: 句子 1, 2, 3
- 块 2: 句子 3, 4, 5
- 块 3: 句子 5, 6, 7
注意,每个块与前一个块共享一些句子,这提高了上下文的连贯性,并确保没有重要信息被遗漏。
重叠块的优势
- 改善上下文:通过重叠句子,每个块向前传递其前一个块的重要信息,创造出无缝的思路流动。
- 减少碎片化:位于块边缘的关键信息得以保留,降低了不完整或不准确检索的风险。
- 更高的相关性:重叠确保在查询处理过程中相关信息不太可能被排除,从而产生更准确的结果。
def chunk_sentences_with_context(sentences, sentences_per_chunk=3, overlap=1):
"""
Chunk sentences into groups with overlap for context.
Parameters:
- sentences (list): A list of sentences.
- sentences_per_chunk (int): The number of sentences per chunk.
- overlap (int): The number of sentences that should overlap between chunks.
Returns:
- list: A list of overlapping chunks.
"""
chunks = []
for i in range(0, len(sentences), sentences_per_chunk - overlap):
chunk = sentences[i:i + sentences_per_chunk]
chunks.append(' '.join(chunk))
if i + sentences_per_chunk >= len(sentences):
break
return chunks
sentences_per_chunk = 10
overlap = 2
texts = chunk_sentences_with_context(sentences, sentences_per_chunk, overlap)
让我们看看第三个块:
输出:
自创立以来,我们在研究和开发方面投资超过 453 亿美元,产生了对现代计算至关重要的发明。我们在 1999 年发明的 GPU 刺激了 PC 游戏市场的增长,并重新定义了计算机图形。2006 年推出的 CUDA 编程模型,使我们的 GPU 的并行处理能力能够广泛应用于计算密集型应用,铺平了现代 AI 的出现之路。2012 年,在 NVIDIA GPU 上训练的 AlexNet 神经网络赢得了 ImageNet 计算机图像识别比赛,标志着 AI 的“宇宙大爆炸”时刻。我们在 2017 年推出了首款 Tensor Core GPU,专为 AI 新时代从零开始构建,并在 2018 年推出了首款自动驾驶系统芯片(SoC)。我们在 2020 年收购了 Mellanox,扩大了我们的创新范围,包括网络,并引入了一种新的处理器类别——数据处理单元(DPU)。在过去的 5 年中,我们构建了在我们的 GPU 和 CUDA 之上运行的完整软件堆栈,将 AI 带入全球最大的行业,包括用于自动驾驶的 NVIDIA DRIVE 堆栈、用于医疗保健的 Clara 和用于工业数字化的 Omniverse;并推出了 NVIDIA AI Enterprise 软件——本质上是企业 AI 应用的操作系统。2023 年,我们推出了首款数据中心 CPU Grace,专为大规模 AI 和高性能计算而构建。凭借强大的工程文化,我们在计算的所有维度(包括硅、系统、网络、软件和算法)推动快速而和谐的产品和技术创新。
Step 2: Convert chunks to embeddings
在创建文本块之后,下一步是将这些块转换为称为 嵌入 的数值表示。
这些嵌入对于实现基于相似性的检索和检索任务至关重要。
什么是嵌入?
嵌入是在高维空间中对文本的密集向量表示。
它们捕捉文本的语义含义,使得能够基于上下文相似性而不仅仅是匹配关键字来比较和检索文本块。
例如:
- 句子“狗叫得很大声。”和“狗发出了很大的噪音。”尽管使用了不同的词,但可能具有相似的嵌入。
- 这种语义理解对于检索系统找到相关结果至关重要。
嵌入模型
为了生成嵌入,我们使用 sentence-transformers
库和 all-mpnet-base-v2
模型,该模型非常适合捕捉文本中的语义关系。
该模型为每个输入文本输出一个固定维度的嵌入(768 维)。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/all-mpnet-base-v2')
embeds = model.encode(texts, show_progress_bar=True)
第3步:将嵌入存储在索引数据库中
现在我们已经为文本块生成了嵌入,下一步是将这些嵌入存储在索引数据库中。
索引使得高效的相似性检索成为可能,让我们能够快速找到任何查询的最相关文本块。
为什么使用索引数据库?
当处理大量嵌入时,逐个搜索它们的相似性会变得计算上昂贵。
通过创建一个 索引,我们可以:
- 优化搜索速度:即使在数百万个嵌入中,也能在毫秒内执行最近邻搜索。
- 支持可扩展性:处理大型数据集而不降低性能。
- 支持复杂查询:为给定查询检索多个相关块。
使用 FAISS
我们使用 FAISS (Facebook AI Similarity Search),这是一个专为相似性检索任务设计的高效库。
它支持高维向量数据,并提供多种针对速度和可扩展性优化的索引方法。
import numpy as np
import pandas as pd
import faiss
dim = embeds.shape[1]
index = faiss.IndexFlatL2(dim)
index.add(np.float32(embeds))
第4步:检索查询的相关信息
在我们的FAISS索引准备好后,我们现在可以根据给定的查询搜索最相关的文本块。
这是**检索增强生成 (RAG)**管道中的检索步骤。
它使我们能够提取最具上下文相关的信息,以支持后续的生成任务。
它是如何工作的
编码查询:
- 查询被嵌入到与已索引文本块相同的向量空间中,使用相同的
SentenceTransformer
模型。 - 这确保了查询和文本块可以在语义上进行比较。
搜索 FAISS 索引:
- 查询 FAISS 索引以找到与查询嵌入最近的邻居(最相似的文本块)。
- 搜索返回最近嵌入的索引及其距离(相似性分数)。
检索和格式化结果:
- 使用索引获取相应的文本块。
- 结果,包括文本及其相似性距离,存储在结构化的 DataFrame 中,以便于分析和使用。
def retrieve(query, number_of_results=3):
"""
Search for the nearest neighbors of a query in the FAISS index.
Parameters:
- query (str): The query text to search for.
- number_of_results (int): Number of nearest neighbors to retrieve.
Returns:
- pd.DataFrame: A DataFrame containing the nearest texts and their distances.
"""
query_embed = model.encode([query])
distances, similar_item_ids = index.search(np.float32(query_embed), number_of_results)
texts_np = np.array(texts)
results = pd.DataFrame(data={
'texts': texts_np[similar_item_ids[0]],
'distance': distances[0]
})
return results
query = "which are the executive officers?"
results = retrieve(query)
results
输出:
作者提供的图片。
第5步. 使用大型语言模型回答
我们检索增强生成 (RAG) 管道的最后一步涉及利用大型语言模型 (LLM) 根据检索到的文本提供一个基于上下文的回答。
这一步通常被称为基于上下文的生成,确保大型语言模型生成的响应既在上下文上准确,又与查询相关。
工作原理
- 大型语言模型集成:
- 使用预训练的大型语言模型处理查询和检索到的上下文。
- Hugging Face 的
pipeline
使得集成文本生成模型变得简单。
- 提示设计:
- 大型语言模型通过精心设计的提示进行引导,其中包括:
- 角色: 定义模型的角色或专长。
- 指令: 指定大型语言模型如何处理检索到的信息。
- 查询: 将用户的问题与检索到的上下文结合起来。
- 基于上下文的回答:
- 大型语言模型基于检索到的上下文生成响应。
- 如果信息不足或不相关,大型语言模型会被指示承认这一点(例如,“我不确定”)。
from transformers import pipeline
pipe = pipeline(
task="text-generation",
model="Qwen/Qwen2.5-1.5B-Instruct",
return_full_text=False,
max_new_tokens=500,
do_sample=True,
temperature=0.2,
)
persona = "You are a helpful assistant specialized in company annual financial statements.\\n"
instruction = "Answer using the relevant information provided above. If you didn't find the information say 'I am not sure'.\\n"
llm_query = persona + results["texts"][0] + instruction + query
messages = [
{"role": "user", "content": llm_query}
]
output = pipe(messages)
print(output[0]["generated_text"])
输出:
给定文本中提到的高管有:
-
Jen-Hsun Huang - 他被描述为总裁和首席执行官(CEO),自公司成立以来就是董事会成员。
-
Colette M Kress - 她被列为执行副总裁和首席财务官。
-
Ajay K Puri - 他被认定为执行副总裁,负责全球现场运营。
-
Debora Shoquist - 她被命名为执行副总裁,负责运营。
-
Timothy S Teter - 他被称为执行副总裁和总法律顾问。
这些个人在 NVIDIA 中担任重要角色,并在其运营和领导中发挥关键作用。
它根据最新的 NVIDIA 年度财务报表第 12 页完美回答了。
图片来自 sec.gov
保持联系
不知道接下来该读什么?这里有两个推荐:
这个故事发布在 Generative AI 上。