langChain 构建 RAG02 - 文档分割

本文介绍如何切分不同类型的文档

Langchain 文档分割即将成功加载的文档进行分割,为什么要分割呢?理由如下:

  1. 模型大小和内存限制:GPT 模型,特别是大型版本如 GPT-3 或 GPT-4 ,具有数十亿甚至上百亿的参数。为了在一次前向传播中处理这么多的参数,需要大量的计算能力和内存。但是,大多数硬件设备(例如 GPU 或 TPU )有内存限制。文档分割使模型能够在这些限制内工作。
  2. 计算效率:处理更长的文本序列需要更多的计算资源。通过将长文档分割成更小的块,可以更高效地进行计算。
  3. 序列长度限制:GPT 模型有一个固定的最大序列长度,例如 2048 个 token 。这意味着模型一次只能处理这么多 token 。对于超过这个长度的文档,需要进行分割才能被模型处理。
  4. 更好的泛化:通过在多个文档块上进行训练,模型可以更好地学习和泛化到各种不同的文本样式和结构。
  5. 数据增强:分割文档可以为训练数据提供更多的样本。例如,一个长文档可以被分割成多个部分,并分别作为单独的训练样本。

需要注意的是,虽然文档分割有其优点,但也可能导致一些上下文信息的丢失,尤其是在分割点附近。因此,如何进行文档分割是一个需要权衡的问题。若仅按照单一字符进行文本分割,很容易使文本的语义信息丧失,这样在回答问题时可能会出现偏差。因此,为了确保语义的准确性,我们应该尽量将文本分割为包含完整语义的段落或单元。

Langchain 中文本分割器都根据 chunk_size (块大小) 和 chunk_overlap (块与块之间的重叠大小) 进行分割。Langchain 提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符 /token 组成、以及如何测量块大小

基于字符的分割

基于字符的分割 LangChain 提供的 RecursiveCharacterTextSplitter 和 CharacterTextSplitter 工具来实现此目标

CharacterTextSplitter 是字符文本分割,分隔符的参数是单个的字符串;RecursiveCharacterTextSplitter 是递归字符文本分割,将按不同的字符递归地分割(按照这个优先级 [“\n\n”, “\n”, " ", “”]),这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置。因此,RecursiveCharacterTextSplitter 比 CharacterTextSplitter 对文档切割得更加碎片化

短句分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 导入文本分割器
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitter

chunk_size = 20 #设置块大小
chunk_overlap = 10 #设置块重叠大小

# 初始化递归字符文本分割器
r_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap
)
# 初始化字符文本分割器
c_splitter = CharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap
)

text = "在AI的研究中,由于大模型规模非常大,模型参数很多,在大模型上跑完来验证参数好不好训练时间成本很高,所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。" #测试文本
r_splitter.split_text(text)
1
2
3
4
5
6
7
8
['在AI的研究中,由于大模型规模非常大,模',
'大模型规模非常大,模型参数很多,在大模型',
'型参数很多,在大模型上跑完来验证参数好不',
'上跑完来验证参数好不好训练时间成本很高,',
'好训练时间成本很高,所以一般会在小模型上',
'所以一般会在小模型上做消融实验来验证哪些',
'做消融实验来验证哪些改进是有效的再去大模',
'改进是有效的再去大模型上做实验。']
1
2
#字符文本分割器
c_splitter.split_text(text)
1
['在 AI 的研究中,由于大模型规模非常大,模型参数很多,在大模型上跑完来验证参数好不好训练时间成本很高,所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。']

CharacterTextSplitter 没有分割这个文本,因为字符文本分割器默认以换行符为分隔符,因此需要设置 “,” 为分隔符

长文本分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 中文版
some_text = """在编写文档时,作者将使用文档结构对内容进行分组。 \
这可以向读者传达哪些想法是相关的。 例如,密切相关的想法\
是在句子中。 类似的想法在段落中。 段落构成文档。 \n\n\
段落通常用一个或两个回车符分隔。 \
回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 \
句子末尾有一个句号,但也有一个空格。\
并且单词之间用空格分隔"""

c_splitter = CharacterTextSplitter(
chunk_size=80,
chunk_overlap=0,
separator=' '
)

'''
对于递归字符分割器,依次传入分隔符列表,分别是双换行符、单换行符、空格、空字符,
因此在分割文本时,首先会采用双分换行符进行分割,同时依次使用其他分隔符进行分割
'''
r_splitter = RecursiveCharacterTextSplitter(
chunk_size=80,
chunk_overlap=0,
separators=["\n\n", "\n", " ", ""]
)

c_splitter.split_text(some_text)
1
2
['在编写文档时,作者将使用文档结构对内容进行分组。 这可以向读者传达哪些想法是相关的。 例如,密切相关的想法 是在句子中。 类似的想法在段落中。 段落构成文档。',
'段落通常用一个或两个回车符分隔。 回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 句子末尾有一个句号,但也有一个空格。 并且单词之间用空格分隔']
1
r_splitter.split_text(some_text)
1
2
3
4
['在编写文档时,作者将使用文档结构对内容进行分组。     这可以向读者传达哪些想法是相关的。 例如,密切相关的想法    是在句子中。 类似的想法在段落中。',
'段落构成文档。',
'段落通常用一个或两个回车符分隔。 回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 句子末尾有一个句号,但也有一个空格。',
'并且单词之间用空格分隔']

基于 token 分割

很多 LLM 的上下文窗口长度限制是按照 Token 来计数的。因此,以 LLM 的视角,按照 Token 对文本进行分隔,通常可以得到更好的结果。

1
2
3
4
5
6
7
# 使用token分割器进行分割,
# 将块大小设为1,块重叠大小设为0,相当于将任意字符串分割成了单个Token组成的列
from langchain.text_splitter import TokenTextSplitter
text_splitter = TokenTextSplitter(chunk_size=1, chunk_overlap=0)
text = "foo bar bazzyfoo"
text_splitter.split_text(text)
# 注:目前 LangChain 基于 Token 的分割器还不支持中文
1
['foo', ' bar', ' b', 'az', 'zy', 'foo']

分割 markdown 文档

分割自定义 markdown 文档
分块的目的是把具有上下文的文本放在一起,我们可以通过使用指定分隔符来进行分隔,但有些类型的文档(例如 Markdown )本身就具有可用于分割的结构(如标题)

Markdown 标题文本分割器会根据标题或子标题来分割一个 Markdown 文档,并将标题作为元数据添加到每个块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 定义一个Markdown文档
from langchain.document_loaders import NotionDirectoryLoader#Notion加载器
from langchain.text_splitter import MarkdownHeaderTextSplitter#markdown分割器

markdown_document = """# Title\n\n \
## 第一章\n\n \
李白乘舟将欲行\n\n 忽然岸上踏歌声\n\n \
### Section \n\n \
桃花潭水深千尺 \n\n
## 第二章\n\n \
不及汪伦送我情"""

# 定义想要分割的标题列表和名称
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)#message_typemessage_type
md_header_splits = markdown_splitter.split_text(markdown_document)

print("第一个块")
print(md_header_splits[0])
print("第二个块")
print(md_header_splits[1])
1
2
3
4
第一个块
page_content='李白乘舟将欲行 \n忽然岸上踏歌声' metadata={'Header 1': 'Title', 'Header 2': '第一章'}
第二个块
page_content='桃花潭水深千尺' metadata={'Header 1': 'Title', 'Header 2': '第一章', 'Header 3': 'Section'}