点击上方“AI公园”,关注公众号,选择加“星标“或“置顶”
作者:Vipra Singh
编译:ronghuaiyang
导读
在系列博客中,我们通过检索增强生成(RAG)应用的视角来学习大规模语言模型(LLM)。
在上一篇文章中,我们深入探讨了检索增强生成(Retrieval Augmented Generation, RAG)的流程,全面理解了它的各个组成部分。
任何机器学习应用的初始阶段都涉及数据准备。这包括建立数据获取流程和预处理数据,使其与推理流程兼容。
在这篇文章中,我们将关注RAG的数据准备方面。目标是高效地组织和构建数据,确保在我们的应用在找到答案时具备最佳性能。
下面让我们做详细的了解。
1. 第1步:数据获取
构建面向消费者的聊天机器人首先需要明智的数据选择。本篇博客将探讨如何有效地收集、管理和清洗数据,从而打造一个成功的语言模型(LLM)应用。
明智选择: 确定从门户到API的数据来源,并为您的LLM应用程序设置推送机制以实现持续更新。
治理至关重要: 提前实施数据治理政策。审核并编辑文档来源,对敏感数据进行脱敏处理,并为上下文训练建立基础。
质量把控: 评估数据的多样性、规模及噪声水平。较低质量的数据集会稀释响应质量,因此早期分类机制至关重要。
前瞻布局: 即便在快速发展的LLM开发过程中,也要遵守数据治理原则。这能降低风险,并确保数据提取的可扩展性和健壮性。
实时净化: 从Slack等平台抓取数据时,实时过滤掉噪声、错别字及敏感内容,确保LLM应用的干净与高效。
2. 第2步:数据清理
我们的文件中的每一页都被转化为一个名为“Document”的对象,该对象包含两个核心组成部分:page_content 和 metadata。
page_content 部分展示的是直接从文档页面中提取的文本内容。
而 metadata 则是一个至关重要的附加详情集合,涵盖了诸如文档来源(即其原始文件)、页码、文件类型以及其他信息片段。这些元数据详细跟踪了在模型施展其功能、生成富有洞察力的答案时所依赖的具体来源信息。
为了实现这一过程,我们采用了诸如Data Loaders这类强大工具,这些工具由LangChain和Llamaindex等开源库提供。这些库支持多种格式的数据加载,包括从PDF和CSV到HTML、Markdown,甚至是数据库等多种类型。通过这些工具,我们能够灵活且高效地处理不同来源和格式的信息,确保文档内容及其元数据被准确无误地转化和利用,进而增强语言模型的理解能力和响应质量。
!pip install pypdf
!pip install langchain
#for PDF file we need to import PyPDFLoader from langchain framework
from langchain_community.document_loaders import PyPDFLoader
# for CSV file we need to import csv_loader
# for Doc we need to import UnstructuredWordDocumentLoader
# for Text document we need to import TextLoader
filePath = "/content/A_miniature_version_of_the_course_can_be_found_here__1701743458.pdf"
loader = PyPDFLoader(filePath)
#Load document
pages = loader.load_and_split()
print(pages[0].page_content)
3. 第3步:分块
3.1. 为什么要分块?
在应用程序领域,数据处理方式是改变游戏规则的关键——不论是Markdown、PDF还是其他文本文件。想象一下:你手头有一个庞大的PDF文件,急于就其内容提出问题。问题在于?传统的做法是将整个文档和你的问题一股脑儿扔给模型处理,但这种方法效果不佳。为什么呢?这就涉及到模型上下文窗口的局限性了。
随着GPT-3.5及其同类模型的出现,情况发生了变化。可以把上下文窗口想象成是对文档的一瞥,通常只局限于一页或几页内容。如果一次性上传整个文档?这并不现实。但是不用担心!
神奇的解决办法在于数据分块。将文档拆解成易于处理的小部分,只向模型发送最相关的部分。这样一来,既不会让模型感到负担过重,又能确保你获得渴望的精确信息。
通过将结构化的文档分解成易于管理的片段,我们使LLM能够以前所未有的效率处理信息。不再受制于单页的限制,这种策略确保了关键细节在处理过程中不会丢失,提高了回答的准确性和深度。
3.2. 分块之前的考虑
文档的结构与长度:
长文档:书籍、学术文章等。
短文档:社交媒体帖子、客户评论等。
嵌入模型: 分块大小决定所使用的嵌入模型类型。
预期查询: 应用场景是什么?
3.3. 分块大小
小分块大小: 例如:单个句子 → 生成时上下文信息较少。
大分块大小: 例如:整页、多段落、完整文档。在这种情况下,分块包含更多信息,可能会因上下文更加丰富而提高生成的有效性。
3.3.1. 选择分块大小
LLM上下文窗口
限制了可以输入到LLM中的数据量。
检索到的顶部K个分块: 假设LLM的上下文窗口大小为10,000个tokens,针对给定的用户查询,我们预留约1,000个tokens,再为指令提示和聊天历史预留2,000个tokens,这样就只剩下7,000个tokens用于其他信息。如果我们打算传入K=10,即顶部的10个分块到LLM中,意味着我们要将剩余的7,000个tokens除以总共10个分块,每个分块的最大大小约为700个tokens。
分块大小范围: 下一步是选择一系列潜在的分块大小进行测试。如前所述,选择应考虑内容性质(如短消息或长篇文档)、所使用的嵌入模型及其能力(如token限制)。目标是在保持上下文和保证准确性之间找到平衡点。开始时,探索多种分块大小,包括用于捕捉更细粒度语义信息的小分块(如128或256个tokens)和用于保留更多上下文的大分块(如512或1024个tokens)。
评估各分块大小的性能 —— 为了测试不同的分块大小,你可以使用多个索引或单个索引中的多个命名空间。使用代表性数据集,为想要测试的分块大小创建嵌入,并保存在索引(或索引中)。然后,运行一系列查询,评估质量,并比较不同分块大小的性能。这很可能是一个迭代过程,你需对不同查询测试不同的分块大小,直到确定出最适合你的内容和预期查询的最佳分块大小。
高上下文长度的局限性:
高上下文长度会导致时间与内存需求呈二次增长,这是由于Transform模型的自注意力机制所致。
在LlamaIndex发布的这个示例中,可以看到下表随着分块大小的增加,平均响应时间有轻微上升。有趣的是,平均忠实度似乎在分块大小为1024时达到峰值,而平均相关性则随着分块大小的增大表现出持续改善,同样在1024达到顶峰。这表明,1024个tokens的分块大小可能在响应时间和以忠实度及相关性衡量的响应质量之间达到了最优平衡。
3.4. 分块方法
在进行分块时,有不同的方法,每种方法可能适用于不同的情况。通过考察每种方法的优缺点,我们的目标是确定应用它们的正确场景。
3.4.1. 固定大小分块
我们决定每个分块中的tokens数量,并可选择性地加入重叠部分以确保语义上下文的丰富性在分块间得以保留。为何要重叠?这是为了确保分块之间的语义上下文完整性不受影响。
为何选择固定大小分块?对于大多数场景而言,这是一种理想的方案。它不仅计算成本低,节省处理能力,而且使用起来也非常简单。无需复杂的NLP库;只需利用固定大小分块的优雅特性,就能轻松分解数据。
以下是使用LangChain执行固定大小分块的一个示例:
text = "..." # your text
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator = "\n\n",
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])
3.4.2. “上下文感知” 分块
这是一系列方法,旨在利用我们正在分块的内容特性,并对其应用更复杂的分块技术。以下是一些示例:
3.4.2.1. 句子分割
如前所述,许多模型针对句子级内容的嵌入进行了优化。自然而然地,我们会使用句子分块,为此有几种方法和工具可供选择,包括:
朴素分割法: 最朴素的方法是通过句号(“.”)和换行符来分割句子。尽管这种方法快速且简单,但它并未考虑到所有可能的边缘情况。这里有一个非常简单的示例:
text = "..." # your text
docs = text.split(".")
NLTK:自然语言工具包(NLTK)是一个流行的Python库,用于处理人类语言数据。它提供了一个句子分词器,可以将文本分割成句子,有助于创建更有意义的分块。例如,要在LangChain中使用NLTK,你可以这样做:
text = "..." # your text
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
spaCy:spaCy是另一个用于NLP任务的强大Python库。它提供了先进的句子分割功能,可以有效地将文本划分为单独的句子,从而使生成的分块更好地保留上下文。例如,要在LangChain中使用spaCy,你可以这样做:
text = "..." # your text
from langchain.text_splitter import SpacyTextSplitter
text_splitter = SpaCyTextSplitter()
docs = text_splitter.split_text(text)
3.4.2.2. 递归分块
介绍我们的秘密武器:来自LangChain的RecursiveCharacterTextSplitter。这个多功能工具根据选定的字符优雅地分割文本,同时保留语义上下文。想象一下双换行、单换行和空格——它就像将信息雕琢成易于处理的、有意义的部分。
它是如何工作的?很简单。只需传递Document
并指定所需的分块长度(假设为1000个单词)。你甚至可以微调分块之间的重叠量。
以下是使用LangChain进行递归分块的示例:
text = "..." # your text
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])
3.4.2.3. 专业分块
Markdown和LaTeX是两种可能遇到的结构化和格式化内容示例。在这种情况下,你可以使用专门的分块方法,在分块过程中保留内容的原始结构。
Markdown:Markdown是一种轻量级的标记语言,常用于文本格式化。通过识别Markdown语法(如标题、列表和代码块),你可以根据内容的结构和层次智能地进行划分,从而得到语义上更加连贯的分块。例如:
from langchain.text_splitter import MarkdownTextSplitter
markdown_text = "..."
markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)
docs = markdown_splitter.create_documents([markdown_text])markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)
docs = markdown_splitter.create_documents([markdown_text])
LaTex: LaTeX是一种文档准备系统和标记语言,常用于学术论文和技术文档中。通过解析LaTeX命令和环境,你可以创建尊重内容逻辑组织的分块(如节、小节和方程式),从而得到更准确、上下文关联性更强的结果。例如:
from langchain.text_splitter import LatexTextSplitter
latex_text = "..."
latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0)
docs = latex_splitter.create_documents([latex_text])
3.5. 多模态分块
从文档中提取表格和图像: 使用LayoutPDFReader或Unstructured等工具,表格和图像可以被提取出来,并能附上元数据标签,如标题、描述和摘要。
多模态方法:
文本嵌入: 对图像和表格进行概括性描述
多模态嵌入: 采用能够直接处理原始图像的嵌入模型
在处理包含多样化内容(如文本、表格、图像)的文档时,采用多模态方法尤为重要,这不仅能提取和理解文本信息,还能有效整合图像和表格内容。例如,通过文本嵌入,可以为图像和表格生成描述性文本,使得这些非文本元素也能被语言模型理解并融入到后续的分析或查询响应中。而多模态嵌入技术则更进一步,它允许模型直接对图像这类非文本数据进行编码,结合文本信息形成统一的表示,有利于提升整体内容的理解精度和上下文关联性。
4. 第4步:分词
分词(Tokenization)是指将短语、句子、段落或整个文本文档切分成更小的单位,如单个单词或术语。在本文中,我们将了解主要的分词方法以及它们当前的应用场景。我建议您也参考一下Hugging Face制作的这个分词器概述,以便获得更深入的指南。
4.1. 单词级分词
单词级分词涉及将文本切分为单词单位。为了正确执行这一操作,需要考虑一些预防措施。
空格与标点符号分词
将文本切分成更小的部分比看起来要复杂,并且有多种方法可以实现。例如,让我们看下面这个句子:
"Don't you like science? We sure do."
一种简单的分词方法是通过空格来分割这段文本,这样做会得到:
["Don't", "you", "like", "science?", "We", "sure", "do."]
如果我们观察分词结果 "science?"
和 "do."
,会注意到标点符号与单词 "science"
和 "do"
连在一起,这并不理想。我们应该考虑标点符号,这样一来,模型就不必为一个单词及其可能跟随的所有标点符号学习不同的表示形式,否则会使模型需要学习的表示数量激增。
考虑到标点符号,对我们的文本进行分词会得到:
["Don", "'", "t", "you", "like", "science", "?", "We", "sure", "do", "."]
基于规则的分词
之前的分词方法比纯粹基于空格的分词有所改进。然而,我们还可以进一步优化如何处理单词 "Don't"
。"Don't"
代表 "do not"
,因此更好的分词方式应该是 ["Do", "n't"]
。其他一些特定的规则可以进一步提升分词的效果。
但是,根据我们应用于文本分词的规则不同,即使是相同的文本也会产生不同的分词输出。因此,预训练模型只有在输入按照与其训练数据相同的分词规则进行分词时,才能正常工作。
词级分词的问题
词级分词对于大规模文本语料库可能会导致问题,因为它会产生非常大的词汇量。例如,Transformer XL 语言模型使用空格和标点符号进行分词,导致词汇量达到 267,000。
由于如此庞大的词汇量,模型的输入和输出层拥有一个巨大的嵌入矩阵,这不仅增加了内存需求,也提高了时间复杂度。作为参考,Transformer模型的词汇量很少超过 50,000。
4.2. 字符级分词
如果词级分词存在不足,为何不直接采用字符级分词呢?
尽管字符级分词能够显著减少内存占用和降低时间复杂度,但它使模型学习到有含义的输入表示变得更为艰难。例如,相比学习单词 "today"
的上下文无关表示,学习单个字母 "t"
的上下文无关表示要困难得多。
因此,字符级分词常导致性能损失。为了兼顾两方面的优点,Transformer模型通常采取一种词级和字符级分词相结合的方法,即子词分词(Subword Tokenization)。这种方法通过创建既考虑了整个词汇单元,又包含了字符序列信息的子词单位,来平衡模型学习的有效性和效率,从而在保留词汇语义完整性的同时,有效控制词汇表的大小。
4.3. 子词分词
子词分词算法基于这样的原则:常用词不应拆分成更小的子词,而罕见词则应分解为有意义的子词。
例如,单词 "annoyingly"
可能被视为一个罕见词,可以分解为 "annoying"
和 "ly"
。独立来看,"annoying
" 和 "ly
" 作为子词会更频繁地出现,同时,"annoyingly
" 的含义通过组合"annoying
" 和 "ly
" 的意义得以保留。
除了能使模型的词汇表保持在一个合理的规模外,子词分词还允许模型学习到有意义的、独立于上下文的表示。此外,子词分词还能通过将模型从未见过的单词拆解为已知的子词来处理这些新词。
接下来,让我们了解几种不同的子词分词方法。
字节对编码(BPE)
字节对编码(BPE)依赖于一个预分词器,该预分词器将训练数据按单词分割(如使用空格分词,在GPT-2和RoBERTa中)。
预分词后,BPE构建一个基本词汇表,包含语料库中所有唯一单词出现过的所有符号,并学习合并规则以从基础词汇表中的两个符号形成一个新的符号。这一过程迭代进行,直到词汇表达到期望的大小。
WordPiece
WordPiece,用于BERT、DistilBERT和Electra,与BPE非常相似。WordPiece首先初始化词汇表以包含训练数据中出现的每个字符,然后逐步学习一定数量的合并规则。与BPE不同的是,WordPiece并不选择最频繁出现的符号对,而是选择一旦加入词汇表后最能提高训练数据概率的那个。
直观上,WordPiece与BPE稍有不同,它评估合并两个符号所损失的信息,确保这一操作是有价值的。
Unigram
与BPE或WordPiece不同,Unigram将基础词汇表初始化为大量符号,并逐步缩减每个符号以获得较小的词汇表。基础词汇可能包括所有预分词的单词和最常见的子串。Unigram常与SentencePiece一起使用。
SentencePiece
迄今为止描述的所有分词算法都有一个共同的问题:假设输入文本使用空格来分隔单词。然而,并非所有语言都用空格分隔单词。
为普遍解决这一问题,SentencePiece将输入视为原始输入流,因此将空格也包括在使用的字符集中。然后,它使用BPE或Unigram算法构建合适的词汇表。
使用SentencePiece的模型示例包括ALBERT、XLNet、Marian和T5。
OpenAI分词可视化:https://platform.openai.com/tokenizer
总结
在本篇博文中,我们深入探讨了检索增强生成(RAG)应用中的数据预处理流程,着重强调了为了达到最佳性能而进行的有效数据结构化方法。内容涵盖了将原始数据转化为结构化文档、创建相关数据块以及诸如子词分词等分词方法。文章突出了选择合适的数据块大小的重要性,并对每种分词方法的考虑因素进行了说明。通过这些讨论,为针对特定应用场景定制数据预处理工作提供了深刻见解。
—END—
英文原文:https://medium.com/@vipra_singh/building-llm-applications-data-preparation-part-2-b7306d224245
请长按或扫描二维码关注本公众号
喜欢的话,请给我个在看吧!