在上一篇文章中,我们深入研究了检索增强生成(RAG)管道,全面了解了其各个组件。
在这篇文章中,我们的注意力将集中在 RAG 的数据准备方面。目标是有效地组织和构建数据,确保在我们的应用程序中找到答案的最佳性能。
第 1 步:数据输入
构建消费者友好型聊天机器人始于明智的数据选择。本文将探讨如何有效地收集、管理和清理数据,以实现语言模型 (LLM) 应用的成功。
第 2 步:数据清理
我们文件中的每个页面都会转化为 Document 对象,其中包含两个基本组件:page_content(页面内容)和 metadata(元数据)。
页面内容是直接从文档页面中提取的文本内容。
元数据是附加详细信息的重要组合,如文档的来源(源文件)、页码、文件类型和其他信息。元数据在编织魔力和生成有见地的答案时,会记录它所利用的特定来源。
为了实现这一目标,我们使用了数据加载器等强大的工具,这些工具由 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 步:分块
为什么选择 Chunk?
在应用程序领域,改变游戏规则的关键在于如何塑造数据--无论是标记符、PDF 还是其他文本文件。
进入 GPT-3.5 和它的同类。把上下文窗口想象成对文档的窥视,通常仅限于一页或几页。现在,一次共享整个文档?不太现实。不过不用担心!
神奇的诀窍在于将数据分块。将其分解成易于消化的部分,只将最相关的部分发送给模型。这样,你就不会让模型不堪重负,而且还能获得你所渴望的精确洞察力。
通过将结构化文件分解成易于管理的小块,我们让 LLM 能够以无与伦比的效率处理信息。这种方法不再受页数限制,确保关键细节不会丢失。
分块前的注意事项
文档的结构和长度:
嵌入模型: 分块大小决定了应使用何种嵌入模型。
预期查询: 使用案例是什么?
块大小
选择块大小
LLM 上下文窗口
评估每种分块大小的性能--要测试各种分块大小,可以使用多个索引,或者使用具有多个命名空间的单个索引。使用具有代表性的数据集,为要测试的块大小创建嵌入,并将其保存在索引(或多个索引)中。然后,你就可以运行一系列可以评估质量的查询,并比较不同块大小的性能。这很可能是一个反复的过程,在这个过程中,你会针对不同的查询测试不同的数据块大小,直到你能根据内容和预期查询确定性能最好的数据块大小。
高上下文长度的限制 :
在 LlamaIndex 发布的这个示例中,你可以从下表中看到,随着分块大小的增加,平均响应时间略有上升。有趣的是,平均忠实度似乎在数据块大小为 1024 时达到顶峰,而平均相关性则随着数据块大小的增大而持续提高,同样在 1024 时达到顶峰。这表明,1024 的数据块大小可以在响应时间和响应质量(以忠实度和相关性衡量)之间取得最佳平衡。
分块方法
有不同的分块方法,每种方法都可能适用于不同的情况。通过研究每种方法的优缺点,我们的目标是找出适合的应用场景。
固定大小的分块
我们决定每个分块中的标记数量,并在其中加入可选的重叠部分。为什么要重叠?确保语义上下文的丰富性在各语块之间保持不变。
为什么采用固定大小?这是大多数情况下的黄金路径。它不仅计算成本低廉,节省了处理能力,而且使用起来轻而易举。无需复杂的 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])
"上下文感知 "分块
这是一套利用我们正在分块的内容的性质并对其应用更复杂的分块的方法。下面是一些例子:
拆分句子
正如我们之前提到的,许多模型都针对嵌入句子级内容进行了优化。自然,我们会使用句子分块,有几种方法和工具可以做到这一点,包括:
text = "..." # your text
docs = text.split(".")
text = "..." # your text
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
text = "..." # your text
from langchain.text_splitter import SpacyTextSplitter
text_splitter = SpaCyTextSplitter()
docs = text_splitter.split_text(text)
递归分块
看看我们的秘密武器:LangChain 的 RecursiveCharacterTextSplitter。这款多功能工具可根据所选字符优雅地分割文本,并保留语义上下文。想想双换行符、单换行符和空格--它就像将信息雕刻成一口大小、有意义的部分。
它是如何工作的?很简单。只需传递文档并指定所需的块长度(比如 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])
专用分块
Markdown 和 LaTeX 是你可能会遇到的结构化和格式化内容的两个例子。在这种情况下,你可以使用专门的分块方法,在分块过程中保留内容的原始结构。
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])
from langchain.text_splitter import LatexTextSplitter
latex_text = "..."
latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0)
docs = latex_splitter.create_documents([latex_text])
多模式分块
从文档中提取表格和图像 LayoutPDFReader, Unstructured. 提取的表格和图像可标记元数据,如标题、描述和摘要。
多模式方法:
第 4 步:标记化
标记化是将短语、句子、段落或整个文本文档分割成更小的单元,如单个词或术语。在本文中,我们将了解有哪些主要的标记化方法以及它们目前的使用情况。
词级标记化
词级标记化包括将文本分割成单词单元。要正确完成这项工作,需要考虑一些注意事项。
空格和标点符号标记化
将文本分割成小块比看起来要难,有多种方法可以做到这一点。例如,我们来看看下面这个句子:
"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", "n't"]的方式来标记。其他一些临时规则可以进一步改进标记化。
然而,根据我们对文本标记化所采用的规则,同一文本会产生不同的标记化输出。因此,预先训练好的模型只有在输入时使用了与训练数据相同的标记化规则,才能正常运行。
词级标记化的问题
单词级标记化会给海量文本库带来问题,因为它会产生一个非常大的词汇量。例如,Transformer XL 语言模型使用空格和标点符号标记化,导致词汇量达到 267k。
由于词汇量如此之大,该模型的输入层和输出层都有一个巨大的嵌入矩阵,从而增加了内存和时间复杂度。
字符级标记化
如果词级标记化不可行,为什么不直接对字符进行标记化呢?
尽管字符标记化可以大大降低内存和时间复杂度,但却会使模型更难学习有意义的输入表征。例如,为字母 "t "学习一个有意义的、与上下文无关的表征,要比为单词 "today "学习一个与上下文无关的表征难得多。
因此,字符标记化往往会导致性能损失。为了两全其美,转换器模型通常会使用词级标记化和字符级标记化的混合方法,即子词标记化。
子词标记化
子词标记化算法所依据的原则是,不应该将常用词拆分成更小的子词,而应该将罕见词分解成有意义的子词。
例如,"annoyingly"可能被认为是一个罕见词,可以分解成"annoyingly"和 "ly"。作为独立子词的 "annoyingly"和 "ly "出现的频率会更高,同时 "annoyingly"和 "ly "的复合含义又保留了"annoyingly"的含义。
除了使模型的词汇量达到合理水平外,子词标记化还能让模型学习到有意义的、与上下文无关的表征。此外,子词标记化还可用于处理模型从未见过的单词,将其分解为已知的子词。
现在让我们来看看子词标记化的几种不同方法。
字节对编码 (BPE)
字节对编码(BPE)依赖于将训练数据分割成单词的预标记化器(如 GPT-2 和 Roberta 中使用的空格标记化)。
在预标记化之后,BPE 创建一个基础词汇表,该词汇表由语料库独特词集中出现的所有符号组成,并学习合并规则,以便用基础词汇表中的两个符号组成一个新符号。这一过程反复进行,直到词汇量达到所需的大小。
词块
用于 BERT、DistilBERT 和 Electra 的 WordPiece 与 BPE 非常相似。WordPiece 首先初始化词汇表,使其包含训练数据中的每个字符,然后逐步学习一定数量的合并规则。与 BPE 不同的是,WordPiece 不会选择最常见的符号对,而是选择将训练数据添加到词汇表后可能性最大的符号对。
从直观上看,WordPiece 与 BPE 略有不同,因为它要评估合并两个符号所带来的损失,以确保值得。
单字符
与 BPE 或 WordPiece 不同的是,Unigram 将其基本词库初始化为大量符号,然后逐步减少每个符号,以获得更小的词汇量。例如,基本词库可以对应所有预标记的单词和最常见的子串。Unigram 经常与 SentencePiece 结合使用。
句子片段
迄今为止描述的所有标记化算法都存在同样的问题:假定输入文本使用空格分隔单词。然而,并非所有语言都使用空格来分隔单词。
为了从总体上解决这个问题,SentencePiece 将输入文本视为原始输入流,从而将空格包含在要使用的字符集中。然后,它使用 BPE 或 Unigram 算法来构建适当的词汇。
总结
在文中,我们探讨了检索增强生成(RAG)应用的数据准备过程,强调了高效的结构化以获得最佳性能。它包括将原始数据转换为结构化文档、创建相关块以及标记化方法(如子字标记化)。文章强调了选择正确的块大小的重要性以及每种标记化方法的注意事项。该文章提供了针对特定应用需求定制数据准备的见解。