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