使用自定义非结构化数据构建 RAG
如果您不熟悉 RAG,请先在另一个笔记本中探索 RAG 的基础知识,然后返回此处了解有关使用自定义数据构建 RAG 的信息。
无论您是构建自己的基于 RAG 的个人助手、宠物项目还是企业 RAG 系统,您都会很快发现许多重要的知识存储在各种格式中,例如 PDF、电子邮件、Markdown 文件、PowerPoint 演示文稿、HTML 页面、Word 文档等等。
如何以一种可以用于 RAG 的方式预处理所有这些数据?在本快速教程中,您将学习如何构建一个将合并来自多种数据类型的数据的 RAG 系统。您将使用Unstructured 进行数据预处理,使用来自 Hugging Face Hub 的开源模型进行嵌入和文本生成,使用 ChromaDB 作为向量存储,并使用 LangChain 将所有内容整合在一起。
让我们开始吧!我们将首先安装所需的依赖项
!pip install -q torch transformers accelerate bitsandbytes sentence-transformers unstructured[all-docs] langchain chromadb langchain_community
接下来,让我们获取各种文档。假设,我想构建一个可以帮助我管理花园害虫的 RAG 系统。为此,我将使用涵盖 IPM(害虫综合治理)主题的各种文档
- PDF:
https://www.gov.nl.ca/ecc/files/env-protection-pesticides-business-manuals-applic-chapter7.pdf
- Powerpoint:
https://ipm.ifas.ufl.edu/pdfs/Citrus_IPM_090913.pptx
- EPUB:
https://www.gutenberg.org/ebooks/45957
- HTML:
https://blog.fifthroom.com/what-to-do-about-harmful-garden-and-plant-insects-and-pests.html
您可以随意使用您选择的主题的文档,从 Unstructured 支持的文档类型列表中选择:.eml
、.html
、.md
、.msg
、.rst
、.rtf
、.txt
、.xml
、.png
、.jpg
、.jpeg
、.tiff
、.bmp
、.heic
、.csv
、.doc
、.docx
、.epub
、.odt
、.pdf
、.ppt
、.pptx
、.tsv
、.xlsx
。
!mkdir -p "./documents"
!wget https://www.gov.nl.ca/ecc/files/env-protection-pesticides-business-manuals-applic-chapter7.pdf -O "./documents/env-protection-pesticides-business-manuals-applic-chapter7.pdf"
!wget https://ipm.ifas.ufl.edu/pdfs/Citrus_IPM_090913.pptx -O "./documents/Citrus_IPM_090913.pptx"
!wget https://www.gutenberg.org/ebooks/45957.epub3.images -O "./documents/45957.epub"
!wget https://blog.fifthroom.com/what-to-do-about-harmful-garden-and-plant-insects-and-pests.html -O "./documents/what-to-do-about-harmful-garden-and-plant-insects-and-pests.html"
非结构化数据预处理
您可以使用 Unstructured 库逐个预处理文档,并编写您自己的脚本来遍历目录,但使用本地源连接器来提取给定目录中的所有文档更容易。Unstructured 可以从本地目录、S3 存储桶、Blob 存储、SFTP 以及您的文档可能存储在许多其他位置提取文档。来自这些来源的提取非常相似,主要区别在于身份验证选项。这里您将使用本地源连接器,但可以随意在Unstructured 文档中探索其他选项。
或者,您还可以为处理后的文档选择一个目标 - 这可能是 MongoDB、Pinecone、Weaviate 等。在本笔记本中,我们将保留所有本地内容。
# Optional cell to reduce the amount of logs
import logging
logger = logging.getLogger("unstructured.ingest")
logger.root.removeHandler(logger.root.handlers[0])
>>> import os
>>> from unstructured.ingest.connector.local import SimpleLocalConfig
>>> from unstructured.ingest.interfaces import PartitionConfig, ProcessorConfig, ReadConfig
>>> from unstructured.ingest.runner import LocalRunner
>>> output_path = "./local-ingest-output"
>>> runner = LocalRunner(
... processor_config=ProcessorConfig(
... # logs verbosity
... verbose=True,
... # the local directory to store outputs
... output_dir=output_path,
... num_processes=2,
... ),
... read_config=ReadConfig(),
... partition_config=PartitionConfig(
... partition_by_api=True,
... api_key="YOUR_UNSTRUCTURED_API_KEY",
... ),
... connector_config=SimpleLocalConfig(
... input_path="./documents",
... # whether to get the documents recursively from given directory
... recursive=False,
... ),
... )
>>> runner.run()
INFO: NumExpr defaulting to 2 threads.
让我们仔细看看我们这里的配置。
ProcessorConfig
控制着处理管道的各个方面,包括输出位置、工作进程数量、错误处理行为、日志详细程度等等。这里唯一必填的参数是 output_dir
- 您希望存储输出的本地目录。
ReadConfig
可用于根据不同的场景自定义数据读取过程,例如重新下载数据、保留下载的文件或限制处理的文档数量。在大多数情况下,默认的 ReadConfig
即可使用。
在 PartitionConfig
中,您可以选择是在本地还是通过 API 对文档进行分块。此示例使用 API,因此需要 Unstructured API 密钥。您可以 在这里 获取您的密钥。免费的 Unstructured API 限制在 1000 页,并且针对基于图像的文档提供了比 Unstructured 本地安装更好的 OCR 模型。如果您删除这两个参数,则文档将在本地处理,但如果文档需要 OCR 和/或文档理解模型,您可能需要安装其他依赖项。具体来说,在这种情况下,您可能需要安装 poppler 和 tesseract,您可以使用 brew 获取它们。
!brew install poppler
!brew install tesseract
如果您使用的是 Windows,可以在 Unstructured 文档 中找到其他安装说明。
最后,在 SimpleLocalConfig
中,您需要指定原始文档所在的目录,以及是否希望递归遍历该目录。
文档处理完成后,您将在 local-ingest-output
目录中找到 4 个 json 文件,每个处理过的文档对应一个。Unstructured 以统一的方式对所有类型的文档进行分块,并返回包含文档元素的 json。
文档元素 具有类型,例如 NarrativeText
、Title
或 Table
,它们包含提取的文本以及 Unstructured 能够获取的元数据。某些元数据对所有元素都是通用的,例如元素所属文档的文件名。其他元数据取决于文件类型或元素类型。例如,Table
元素将在元数据中包含表格的 html 表示形式,电子邮件的元数据将包含有关发件人和收件人的信息。
让我们从这些 json 文件中导入元素对象。
from unstructured.staging.base import elements_from_json
elements = []
for filename in os.listdir(output_path):
filepath = os.path.join(output_path, filename)
elements.extend(elements_from_json(filepath))
现在您已经从文档中提取了元素,您可以将其分块以适应嵌入模型的上下文窗口。
分块
如果您熟悉将长文本文档拆分为较小块的分块方法,您会注意到 Unstructured 的分块方法略有不同,因为分块步骤已经将整个文档划分为其结构元素:标题、列表项、表格、文本等。通过这种方式对文档进行分块,您可以避免不相关文本片段最终出现在同一个元素中,然后出现在同一个块中的情况。
现在,当您使用 Unstructured 对文档元素进行分块时,单个元素已经很小,因此只有当它们超过所需的最大块大小时才会被拆分。否则,它们将保持原样。您还可以选择组合连续的文本元素,例如列表项,例如,这些元素组合在一起将适合块大小限制。
from unstructured.chunking.title import chunk_by_title
chunked_elements = chunk_by_title(
elements,
# maximum for chunk size
max_characters=512,
# You can choose to combine consecutive elements that are too small
# e.g. individual list items
combine_text_under_n_chars=200,
)
这些块已准备好用于 RAG。要将它们与 LangChain 一起使用,您可以轻松地将 Unstructured 元素转换为 LangChain 文档。
from langchain_core.documents import Document
documents = []
for chunked_element in chunked_elements:
metadata = chunked_element.metadata.to_dict()
metadata["source"] = metadata["filename"]
del metadata["languages"]
documents.append(Document(page_content=chunked_element.text, metadata=metadata))
设置检索器
此示例使用 ChromaDB 作为向量存储,并使用 BAAI/bge-base-en-v1.5
嵌入模型,您可以随意使用任何其他向量存储。
from langchain_community.vectorstores import Chroma
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import utils as chromautils
# ChromaDB doesn't support complex metadata, e.g. lists, so we drop it here.
# If you're using a different vector store, you may not need to do this
docs = chromautils.filter_complex_metadata(documents)
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-base-en-v1.5")
vectorstore = Chroma.from_documents(documents, embeddings)
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3})
如果您计划使用 Hugging Face Hub 中的门控模型,无论是嵌入模型还是文本生成模型,您都需要使用您的 Hugging Face 令牌进行身份验证,您可以在 Hugging Face 个人资料的设置中获取该令牌。
from huggingface_hub import notebook_login
notebook_login()
使用 LangChain 进行 RAG
让我们将所有内容整合在一起,并使用 LangChain 构建 RAG。在此示例中,我们将使用来自 Meta 的 Llama-3-8B-Instruct
。为了确保它能够在 Google Colab 的免费 T4 运行时中顺利运行,您需要对其进行量化。
from langchain.prompts import PromptTemplate
from langchain.llms import HuggingFacePipeline
from transformers import pipeline
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from langchain.chains import RetrievalQA
model_name = "meta-llama/Meta-Llama-3-8B-Instruct"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=bnb_config)
tokenizer = AutoTokenizer.from_pretrained(model_name)
terminators = [tokenizer.eos_token_id, tokenizer.convert_tokens_to_ids("<|eot_id|>")]
text_generation_pipeline = pipeline(
model=model,
tokenizer=tokenizer,
task="text-generation",
temperature=0.2,
do_sample=True,
repetition_penalty=1.1,
return_full_text=False,
max_new_tokens=200,
eos_token_id=terminators,
)
llm = HuggingFacePipeline(pipeline=text_generation_pipeline)
prompt_template = """
<|start_header_id|>user<|end_header_id|>
You are an assistant for answering questions using provided context.
You are given the extracted parts of a long document and a question. Provide a conversational answer.
If you don't know the answer, just say "I do not know." Don't make up an answer.
Question: {question}
Context: {context}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
prompt = PromptTemplate(
input_variables=["context", "question"],
template=prompt_template,
)
qa_chain = RetrievalQA.from_chain_type(llm, retriever=retriever, chain_type_kwargs={"prompt": prompt})
结果和后续步骤
现在您有了 RAG 链,让我们向它询问有关蚜虫的问题。它们是我的花园里的害虫吗?
question = "Are aphids a pest?"
qa_chain.invoke(question)["result"]
输出
Yes, aphids are considered pests because they feed on the nutrient-rich liquids within plants, causing damage and potentially spreading disease. In fact, they're known to multiply quickly, which is why it's essential to control them promptly. As mentioned in the text, aphids can also attract ants, which are attracted to the sweet, sticky substance they produce called honeydew. So, yes, aphids are indeed a pest that requires attention to prevent further harm to your plants!
看起来这是一个很有希望的开始!现在您了解了将复杂非结构化数据预处理以用于 RAG 的基础知识,您可以继续改进此示例。以下是一些想法
- 您可以连接到不同的源以从其中提取文档,例如 S3 存储桶。
- 您可以在
qa_chain
参数中添加return_source_documents=True
以使链返回作为上下文传递给提示的文档。这对于理解用于生成答案的来源很有用。 - 如果您希望在检索阶段利用元素元数据,请考虑使用 Hugging Face 代理并创建自定义检索器工具,如 此其他笔记本 中所述。
- 您可以做很多事情来改进搜索结果。例如,您可以使用混合搜索而不是单个相似性搜索检索器。混合搜索结合了多种搜索算法,以提高搜索结果的准确性和相关性。通常,它是基于关键词的搜索算法与向量搜索方法的组合。
祝您使用非结构化数据构建 RAG 应用程序愉快!
< > 在 GitHub 上更新