本文作为《探秘大模型应用开发》的补充篇,介绍文档处理的第一步——分块(chunking),陆续会有更多章节进行补充勘误,欢迎关注。
通常,在大段文本进行处理时,受限于大模型接受的Prompt长度的限制,以及召回内容的有效信息密度,我们需要对分块(chunking),所谓分块就是为了分块是将文本拆分为更小、更同质的单元,称为块(chunk),确保嵌入的内容尽可能少地包含噪音,并且具有语义相关性,从而帮助优化向量数据库返回内容的相关性,以便更好地被大语言模型 (LLM) 处理。例如,在语义搜索中,我们会索引一个文档语料库,每个文档都包含特定主题的有价值信息。通过采用有效的分块策略,我们可以确保搜索结果准确捕捉到用户查询的实质内容。如果我们的分块过小或过大,都可能导致搜索结果不精确或错过浮现相关内容的机会。根据经验,如果没有周围上下文的文本块对人来说是有意义的,那么对语言模型来说也是有意义的。
chunking的作用
从检索相关性角度看,直接embedding大段的文章内容,是更有利于召回更相关的内容,这是因为它不仅可以考虑词组句子字面的意思,更能够关注到其上下文的信息,也要考虑文本中句子和短语之间的关系从而产生更全面的向量表征,捕捉文本更广泛的含义和主题。但同时,较大的文本又可能会引入噪音或削弱单个句子或短语的重要性,使得在查询索引时更难找到精确的匹配项。查询的长度也会影响embedding之间的关系。一个较短的查询,如一个句子或短语,将集中于具体内容,可能更适合与句子级向量匹配。跨度超过一个句子或段落的较长查询可能更适合与段落或文档级别的向量匹配,因为它可能在寻找更广泛的上下文或主题。可以看出,它和人类理解是有相似之处,背景信息既不能太少也不能太多。
同时,索引也可能是非同质的,包含不同大小的块的embedding。这可能会给查询结果的相关性带来挑战,但也可能会产生一些积极的影响。一方面,由于长内容和短内容的语义表征之间存在差异,查询结果的相关性可能会出现波动。另一方面,非同质索引有可能捕捉到更广泛的上下文和信息,因为不同的块大小代表了文本中不同的粒度。这可以更灵活地适应不同类型的查询。
除此之外,chunking对于简化工程实现复杂度,提升处理效率,比如可以减少内存消耗和提高并行度,以及结果可解释性等方面都有很重要的作用。
因此,为语料库中的文档找到最佳块大小对于确保搜索结果的准确性和相关性至关重要。
chunking策略
常见的分块策略选择取决于多个因素,如:
1)文本类型及大小,比如是一篇文章博客还是一个推文,是markdown格式还是网页格式。
2)检索查询的长度和复杂程度,比如,是简短的问题,还是包含复杂的背景信息的描述。
3)具体使用的嵌入模型在何种大小的文本块上表现最佳。比如, sentence-transformer 上模型在单个句子上效果很好,但像 text-embedding-ada-002 这样的模型在包含 256 或 512 个标记的块上效果更好。
4)具体使用场景,比如,是用于语义搜索、问题解答、摘要还是其他目的,以及最终检索的chunk会提交给大模型,其大模型的上下文窗口与之匹配情况
5)资源的限制,比如在进行chunking时,算力及内存的一些约束,也会对chunking大小有限制。
分块策略有很多,每种策略都可能适用于不同的情况。 常见的文本分块方法有:
1)固定大小分块(FSC),这是最常见、最直接的分块方法:我们只需决定分块中token的数量,并选择它们之间是否应该有任何重叠。一般来说,我们会希望在分块之间保留一些重叠,以确保语义上下文不会在分块之间丢失。在大多数情况下,固定大小的分块是最佳选择。与其他形式的分块法相比,固定大小的分块法计算成本低,使用简单,因为它不需要使用任何 NLP 库。比如,下面使用langchain对文本进行分块,分块大小为256:
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])
在此基础上,结合它们的优劣势衍生了内容定义分块算法(CDC)、滑动窗口分块算法(SWC)和两阈值两除法分块算法(TTTD)。FSC 将文本分割成大小相同的块,而不考虑内容。这种方法简单快捷,但由于边界偏移问题,重复数据删除效率较低。CDC 根据内容的某些特征(如标点符号或哈希值)分割文本。这种方法可以实现更高的重复数据删除效率,但需要更多的计算和存储。SWC 在文本上滑动一个窗口,并在窗口内满足特定条件时标记一个块边界。这种方法可以适应不同的数据特征,但可能会生成不同大小的块。TTTD 使用两个不同的阈值和除数将文本分成两个层次:粗粒度块和细粒度块。这种方法可以在重复数据删除效率和计算成本之间取得平衡。
2)句子级分块,以句子的粒度进行拆分,最简单的方法是用句号(".")和新行分割句子。虽然这种方法既快又简单,但却没有考虑到所有可能的边角问题。为了解决这一情况可以使用一些NLP的技术感知到文本内容语义,从而更好地保留所产生的语块的上下文。langchain中内置了很多句子拆分的工具包。比如NLTK和spaCy。在langchain中的使用方法:
text = "..." # your text
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
3)递归分块,递归分块法使用一组分隔符,以分层和迭代的方式将输入文本分成较小的块。如果初次尝试分割文本时没有产生所需的大小或结构的分块,该方法就会使用不同的分隔符或标准对产生的分块进行递归调用,直到达到所需的分块大小或结构。这意味着,虽然分块的大小不会完全相同,但它们仍会倾向于达到相似的大小。在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])
4.)特定格式的分块:针对特定格式的文本,如Markdown和LaTeX,保留内容的原始结构,例如识别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])
除上面常见的分块方法之外,还有一些其它方法在探索,比如语义聚类方法分块方法,利用文本中蕴含的内在含义来指导分块。这些方法可能会利用机器学习算法来辨别上下文并推断文本的自然划分。
策略选择
刚才我们了解到分块有很多的方法,每种方法也有很多策略可以设置,那么什么样的策略最适合,就需要进行评估,从而找到最佳答案。以最常见的固定分块方法找出合适分块大小为例,整个过程大致分为三步:
1)预处理数据,在确定应用程序的最佳块大小之前,您需要先预处理数据以确保质量。例如,如果数据是从网络上获取的,就可能需要移除 HTML 标记或只会增加噪音的特定元素。
2)选择一定范围的块大小,数据预处理完成后,下一步就是选择一定范围的潜在块大小进行测试。如前所述,选择时应考虑内容的性质(如短信息或长文档)、将使用的嵌入模型及其功能(如标记限制)。目的是在保留上下文和保持准确性之间找到平衡。首先要探索各种块的大小,包括用于捕获更细粒度语义信息的较小块(如 128 或 256 token)和用于保留更多上下文的较大块(如 512 或 1024 token)。
3)评估每种分块大小的性能,为了测试各种分块大小,可以在向量数据库中使用多个索引或具有多个命名空间的单个索引。使用具有代表性的数据集,为要测试的数据块大小创建embeddings,并将其保存在索引(或多个索引)中。然后,您可以运行一系列可以评估质量的查询,并比较不同块大小的性能。这是一个迭代的过程,在这个过程中,你会针对不同的查询测试不同的块大小,直到你能根据内容和预期查询确定性能最好的块大小。
Chunking为后续步骤保证检索质量起奠基作用,但经过上面的介绍,可以看出,chunking本身是一个权衡的产物,再好的策略也无法做到百利无一害,为了克服因为chunking带来的负面影响,需要一些全局化、系统化策略协同起来才能进一步提高生成质量,比如,llamaindex等框架为chunk增加描述性metadata,以及精心设计索引结构,比如treeindex等,进而解决因为chunking导致的跨chunk的上下文丢失问题,也因此成为大模型应用研究的热点之一。