因为当前项目也遇到了回答与问题偏离比较大的情况,所以找到了这篇文章,相信有不少朋友也遇到过同样的问题。原文来自Pincecon官网。文章的内容其实也比较泛泛,但对于刚涉及这个领域的人来说,这篇文章足够帮助理解面临的问题域。 和很多技术文章的毛病一样,文章缺少一些背景性的说明和过程性的解释,一些语词的指代也没有说清,非常容易造成不知所云的情况。也许是因为这篇文章是一系列中的一篇的原因。在此一并说明。
大语言模型结合向量数据库的一般场景其实分为两个过程:
保存过程:各种格式的多个文本文件 --生成文本块--> 嵌入模型 --生成嵌入--> 保存至向量数据库 ----> 为每个嵌入生成索引
检索过程:查询(用户输入的问题) --生成文本块--> 嵌入模型 --生成嵌入--> 从向量数据库查询 --相关度匹配--> 文本文档 --结合提示--> 大语言模型 ----> 结果答案
嵌入:嵌入在汉语中是动词(embed),但在语言模型领域中这个词有了名词属性(embedding)。嵌入一段文本,实际含义为‘为文本生成一段向量’;一段文本嵌入,实际含义为‘一段能表示文本内容的向量’。因为词性的悄然变化,容易对理解造成障碍,译文中的嵌入时而作为动词时而作为名词,但原文中是不同的词。而向量则是具体的计算机表示:一段定长的浮点数数组。嵌入生成的向量,强大的地方在于,它能够表示语义。
嵌入模型: 提供嵌入功能的模型,与大语言模型不是同一种模型。但大语言模型本身具有嵌入功能,所以大语言模型也可以用作嵌入模型。
分块:分块的过程发生在调用嵌入模型之前的文本处理。无论是嵌入模型还是大语言模型,都无法一次性输入无限数量的文本内容,因此通过切分成较小块的文本,模型能够以其承载的能力处理数据。
分词:分词同样可作名词(token)也可作动词(tokenize)。分词是对单个字词的计算机表示,嵌入是对整句或整段话的计算机表示。嵌入并不是分词的序列,不是分词的简单的叠加和组成,分词与分词之间需要经过一系列复杂的矩阵运算才能得出嵌入。一般而言,单个汉字就是一个分词,常见双字也可能是一个分词;而英语中因为前缀,后缀,时态等组成结构的原因,单个单词一般而言会被分成2~3个分词。
在构建大语言模型应用的上下文中,分块指的是将大段文本切分成更小片段的过程。这是一项重要的技术,一旦使用大语言模型关联一些附加内容,分块可以帮助优化向量数据库返回内容的相关度。这篇文章将探讨分块是否可以以及如何帮助提高大语言模型相关应用的效率和准确度。
大家知道在Pinecone中为任何内容建立索引都需要首先进行嵌入(embeded)。分块的主要目的是为了确保嵌入的内容噪音尽可能少,但语义仍然保持关联。
例如,在语义搜索中,我们需要对文档语料进行索引,每个文档都包含特定主题和含义的信息。通过实施有效的分块策略,可以确保搜索结果能够准确地捕捉用户查询的本质。如果文本块太小或太大,可能会导致搜索结果不精确或显示错误的内容。根据经验,如果文本块在没有上下文的情况下对人类有意义,那么它对语言模型也有意义。因此,找到语料库中一篇文档最佳的文本分块大小对于确保搜索结果的准确性和相关性至关重要。
另一个例子是聊天机器人(之前在使用Python和Javascript时介绍过)。我们使用嵌入文本块(Embedding chunks)来构建基于知识库中可信信息的聊天机器人。在这种情况下,正确选择分块策略很重要,原因有两个:首先,分块内容会确定上下文是否与实际的语言模型指令提示(Prompt)相关。(译按:分块的内容从向量数据库中检索出来以后,需要和指令提示结合交给语言模型处理,如果一个不相关的分块夹杂其中会对结果生成产生负面影响) 其次,因为每个请求能够发送的分词(token)数量有限制,分块将决定能否把检索到的文本结合到上下文中,然后发送给外部模型端(例如OpenAI)。(译按:检索出来的分块内容需要和指令提示、参数数值等一起作为网络请求体发送到模型端,模型端需要先经过分词处理才能解析并理解请求体中的文本,而模型端始终有一次性处理上下文大小的限制) 某些情况下,例如使用GPT-4的32k上下文窗口中,较长文本块不是问题。不过,需要注意何时采用较大文本块,因为这样做可能会对Pinecone返回结果的相关性产生不利影响。
在这篇文章中,我们将探讨几种分块方法,并讨论在不同分块大小和方法时应考虑的利弊。最后,我们将提供一些建议,以确定适合自身应用最佳的分块大小和方法。
当嵌入内容时,可以根据内容长(如段落或整个文档)短(如句子)来预先采用不同的处理方式。
当嵌入单个句子时,生成的向量应当集中在句子的特定含义。 当与其他嵌入的句子进行比较的时候,含义自然是比较的重点。这样做同时也意味着嵌入单个句子可能会丢失整个段落或文档中更广泛的上下文信息。
当嵌入整个段落或文档时,嵌入过程需要考虑上下文整体以及文本中句子和短语之间的关系。 这样做可以生成更完整的向量表示,能够捕获文本中更广泛的含义和主题。另一方面,较大的输入文本可能会引入噪音,也可能削弱单个句子或短语的重要性,从而造成查询索引时不容易找到准确的匹配。
查询时的文本长度(译按:即用户输入的问题)也会影响嵌入向量之间的相互关系。较短的查询(例如单个句子或短语)将专注于细节,更适合与句子级嵌入进行匹配。跨越多个句子或段落的较长的查询更适合段落或文档级别的嵌入,因为需要寻找更广泛的上下文或主题。
向量数据库建立的索引可以是不同性质的,也可以包含大小不一的嵌入。索引的这种性质可能会在查询结果的相关性方面带来问题,但也可能会产生一些积极的效果。一方面,由于长短内容在语义表示上存在的差异,查询结果的相关性可能会产生波动。另一方面,不同性质的索引可能会捕获到更广泛的上下文和信息,因为不同的块大小代表了文本中不同的粒度。这样可以更灵活地适应不同类型的查询。
要确定最佳分块策略需要考虑几个变量,这些变量在不同情况下会发生不同的变化。一些需要牢记的关键点:
这些问题的答案能够捋清分块策略,平衡性能和准确性,反过来又能确保查询结果的相关性。
分块的方法有多种,每种方法适合不同的情况。通过验证每种方法的优点和缺点,明确应用这些方法的正确场景。
这是最常见、最直接的分块方法:我们只需决定一个文本块中的分词数量,以及选择性地决定文本块之间可否进行交叠(overlap)。一般来说,我们希望在文本块块之间保留一些交叠,这样可以防止上下文的语义不会丢失。大多数常见情况下,定长分块是最佳路径。与其他形式的分块相比,定长分块计算成本低且易于使用,因为不需要使用任何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使用递归分块的例子:
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])
from langchain.text_splitter import LatexTextSplitter latex_text = "..." latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0) docs = latex_splitter.create_documents([latex_text])
如果常见的分块方法(例如定长分块)不容易应用于当前工程,那么还有一些注意的点可以有助于找到最佳的分块大小。
数据预处理 在确定最佳文本块大小之前,首先需要预处理数据以确保质量。例如,如果数据是从网络爬虫得来,那么可能需要删除HTML标签和那些会增加噪音的标识符。
明确文本块大小的范围 数据经过预处理后,下一步是明确待测试的文本块大小的范围。如前所述,需要考虑内容的性质(例如,短消息文本还是冗长的文档)、准备使用的嵌入模型及承载能力(例如,分词数量限制)。目标是在保留上下文和维持准确性之间找到平衡。首先尝试各种文本块大小,包括捕获细粒度语义信息的较小文本块(例如,128或256个分词)和保留更多上下文信息的较大文本块(例如,512或1024个分词)。
评估块大小的性能 为了测试各种文本块大小,可以使用多个索引或具有多个命名空间的单个索引。使用有代表性的数据集,为待测试的不同大小的文本块生成嵌入,并为这些嵌入创建索引。然后,运行一系列查询,评估结果质量,同时比较不同大小文本块的性能。这很可能是一个迭代过程,可以针对不同的查询测试不同的块大小,直到能够确定最适合当前内容、满足查询预期的分块大小。
在大多数情况下,对内容进行分块非常简单——但脱离一般情况,要正确分块就会带来一定的挑战。不存在一种万能的分块解决方案,适用于一种用例的方法可能不适用于另一种。希望这篇文章能够帮助您更好地了解如何为大语言模型应用进行分块。