(六)、基于 LangChain 实现大模型应用程序开发 | 基于知识库的个性化问答 (文档分割 Splitting)

本文主要是介绍(六)、基于 LangChain 实现大模型应用程序开发 | 基于知识库的个性化问答 (文档分割 Splitting),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在上一章中,我们刚刚讨论了如何将文档加载到标准格式中,现在我们要谈论如何将它们分割成较小的块。这听起来可能很简单,但其中有很多微妙之处会对后续工作产生重要影响。

文章目录

  • 1、为什么要做文档分割?
  • 2、文档分割方式
  • 3、基于字符分割:RecursiveCharacterTextSplitter 与 CharacterTextSplitter
    • 3.1、短句分割
    • 3.1、长句分割
  • 4、基于Token分割
  • 5、分割Markdown文档
    • 5.1、分割一个自定义 Markdown 文档
    • 5.2、分割数据库中的 Markdown 文档
  • Reference

1、为什么要做文档分割?

优点:

  • 1、模型大小和内存限制。
  • 2、计算效率。
  • 3、序列长度限制。
  • 4、更好的泛化:通过在多个文档块上进行训练,模型可以更好地学习和泛化到各种不同的文本样式和结构。
  • 5、数据增强:分割文档可以为训练数据提供更多的样本。例如,一个长文档可以被分割成多个部分,并分别作为单独的训练样本。

缺点:

  • 可能导致一些上下文信息的丢失,尤其是在分割点附近。因此,如何进行文档分割是一个需要权衡的问题。

因此,为了确保语义的准确性,我们应该尽量将文本分割为包含完整语义的段落或单元。

2、文档分割方式

Langchain 中文本分割器都根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割:

  • chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量
  • chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息
    在这里插入图片描述

Langchain提供了很多文本切割的工具,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小。其中langchain默认使用RecursiveCharacterTextSplitter:

  • 1、CharacterTextSplitter():按字符来分割文本。
  • 2、MarkdownHeaderTextSplitter():基于指定的标题来分割markdown 文件。
  • 3、TokenTextSplitter():按token来分割文本。
  • 4、SentenceTransformersTokenTextSplitter() : 按token来分割文本
  • 5、RecursiveCharacterTextSplitter():按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
  • 6、Language() - 用于 CPP、Python、Ruby、Markdown 等。
  • 7、NLTKTextSplitter():使用 NLTK(自然语言工具包)按句子分割文本。
  • 8、SpacyTextSplitter() - 使用 Spacy按句子的切割文本。

3、基于字符分割:RecursiveCharacterTextSplitter 与 CharacterTextSplitter

如何进行文本分割,往往与我们的任务类型息息相关。当我们拆分代码时,这种相关性变得尤为突出。因此,我们引入了一个语言文本分割器,其中包含各种为 Python、Ruby、C 等不同编程语言设计的分隔符。在对这些文档进行分割时,必须充分考虑各种编程语言之间的差异。

我们将从基于字符的分割开始探索,借助 LangChain 提供的 RecursiveCharacterTextSplitter 和 CharacterTextSplitter 工具来实现此目标。

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

RecursiveCharacterTextSplitter 需要关注的是如下4个参数:

  • separators - 分隔符字符串数组
  • chunk_size - 每个文档的字符数量限制
  • chunk_overlap - 两份文档重叠区域的长度
  • length_function - 长度计算函数

⭐从以下尝试可以看出,这就是递归字符文本分割器名字中“递归”的含义,总的来说,我们更建议在通用文本中使用递归字符文本分割器。

3.1、短句分割

# 导入文本分割器
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitterchunk_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)
# 可以看到,分割结果中,第二块是从“大模型规模非常大,模”开始的,刚好是我们设定的块重叠大小['在AI的研究中,由于大模型规模非常大,模','大模型规模非常大,模型参数很多,在大模型','型参数很多,在大模型上跑完来验证参数好不','上跑完来验证参数好不好训练时间成本很高,','好训练时间成本很高,所以一般会在小模型上','所以一般会在小模型上做消融实验来验证哪些','做消融实验来验证哪些改进是有效的再去大模','改进是有效的再去大模型上做实验。']# 字符文本分割器
c_splitter.split_text(text)
# 可以看到字符分割器没有分割这个文本,因为字符文本分割器默认以换行符为分隔符,因此需要设置“,”为分隔符。['在AI的研究中,由于大模型规模非常大,模型参数很多,在大模型上跑完来验证参数好不好训练时间成本很高,所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。']

设置空格分隔符。可以看到出现了提示"Created a chunk of size 23, which is longer than the specified 20",意思是“创建了一个长度为23的块,这比指定的20要长。”。


# 是因为CharacterTextSplitter优先使用我们自定义的分隔符进行分割,所以在长度上会有较小的差距
c_splitter = CharacterTextSplitter(chunk_size=chunk_size,chunk_overlap=chunk_overlap,separator=','
)
c_splitter.split_text(text)Created a chunk of size 23, which is longer than the specified 20
['在AI的研究中,由于大模型规模非常大','由于大模型规模非常大,模型参数很多','在大模型上跑完来验证参数好不好训练时间成本很高','所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。']

3.1、长句分割

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

对于递归字符分割器,依次传入分隔符列表,分别是双换行符、单换行符、空格、空字符,
因此在分割文本时,首先会采用双换行符进行分割,同时依次使用其他分隔符进行分割(谁放列表前,谁优先级就大)。
意思就是先通过\n\n分割,然后在分割出来的每一段里继续用 [“\n”, " ", “”]分割,但每次分割要尽可能满足chunk_size和chunk_overlap

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

如果需要按照句子进行分隔,则还要用正则表达式添加一个句号分隔符


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

4、基于Token分割

LLM 的上下文窗口长度限制一般是按照 Token 来计数的。因此,以 LLM 的视角,按照 Token 对文本进行分隔,通常可以得到更好的结果。 通过一个实例理解基于字符分割和基于 Token 分割的区别

# 使用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)
# 可以看出token长度和字符长度不一样,token通常为4个字符
# 注:目前 LangChain 基于 Token 的分割器还不支持中文
['foo', ' bar', ' b', 'az', 'zy', 'foo']

5、分割Markdown文档

5.1、分割一个自定义 Markdown 文档

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

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

# 定义一个Markdown文档markdown_document = """# Title\n\n \
## 第一章\n\n \
李白乘舟将欲行\n\n 忽然岸上踏歌声\n\n \
### Section \n\n \
桃花潭水深千尺 \n\n 
## 第二章\n\n \
不及汪伦送我情"""print(markdown_document)
# Title## 第一章李白乘舟将欲行忽然岸上踏歌声### Section 桃花潭水深千尺 ## 第二章不及汪伦送我情
from langchain.text_splitter import MarkdownHeaderTextSplitter#markdown分割器# 定义想要分割的标题列表和名称
headers_to_split_on = [("#", "Header 1"),("##", "Header 2"),("###", "Header 3"),
]markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on
)#message_typemd_header_splits = markdown_splitter.split_text(markdown_document)
print(len(md_header_splits),md_header_splits)print(md_header_splits[0])
print(md_header_splits[1])
print(md_header_splits[2])
3 
[Document(page_content='李白乘舟将欲行  \n忽然岸上踏歌声', metadata={'Header 1': 'Title', 'Header 2': '第一章'}), Document(page_content='桃花潭水深千尺', metadata={'Header 1': 'Title', 'Header 2': '第一章', 'Header 3': 'Section'}), Document(page_content='不及汪伦送我情', metadata={'Header 1': 'Title', 'Header 2': '第二章'})]
page_content='李白乘舟将欲行  \n忽然岸上踏歌声' metadata={'Header 1': 'Title', 'Header 2': '第一章'}
page_content='桃花潭水深千尺' metadata={'Header 1': 'Title', 'Header 2': '第一章', 'Header 3': 'Section'}
page_content='不及汪伦送我情' metadata={'Header 1': 'Title', 'Header 2': '第二章'}

5.2、分割数据库中的 Markdown 文档

在上一章中,我们尝试了 Notion 数据库的加载,Notion 文档就是一个 Markdown 文档。我们在此处加载 Notion 数据库中的文档并进行分割。

from langchain.document_loaders import NotionDirectoryLoader#Notion加载器
loader = NotionDirectoryLoader("./data/Notion_DB")
docs = loader.load()
txt = ' '.join([d.page_content for d in docs])# 如果Notion_DB下有多个md文件,那就拼一起headers_to_split_on = [("#", "Header 1"),("##", "Header 2"),
]#加载文档分割器
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on
)md_header_splits = markdown_splitter.split_text(txt)#分割文本内容print(len(md_header_splits), md_header_splits[0])#分割结果
8 
page_content="This is a living document with everything we've learned working with people while running a startup. And, of course, we continue to learn. ...."metadata={'Header 1': "Blendle's Employee Handbook (1)"}

Reference

  • [1] 吴恩达老师的教程
  • [2] DataWhale组织

这篇关于(六)、基于 LangChain 实现大模型应用程序开发 | 基于知识库的个性化问答 (文档分割 Splitting)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/395218

相关文章

基于Python和Tkinter实现高考倒计时功能

《基于Python和Tkinter实现高考倒计时功能》随着高考的临近,每个考生都在紧锣密鼓地复习,这时候,一款实用的倒计时软件能有效帮助你规划剩余时间,提醒你不要浪费每一分每一秒,今天,我们来聊聊一款... 目录一、软件概述:二、功能亮点:1. 高考倒计时2. 添加目标倒计时3. 励志语句4. 透明度调节与

Python实现PDF与多种图片格式之间互转(PNG, JPG, BMP, EMF, SVG)

《Python实现PDF与多种图片格式之间互转(PNG,JPG,BMP,EMF,SVG)》PDF和图片是我们日常生活和工作中常用的文件格式,有时候,我们可能需要将PDF和图片进行格式互转来满足... 目录一、介绍二、安装python库三、Python实现多种图片格式转PDF1、单张图片转换为PDF2、多张图

Java实现数据库图片上传与存储功能

《Java实现数据库图片上传与存储功能》在现代的Web开发中,上传图片并将其存储在数据库中是常见的需求之一,本文将介绍如何通过Java实现图片上传,存储到数据库的完整过程,希望对大家有所帮助... 目录1. 项目结构2. 数据库表设计3. 实现图片上传功能3.1 文件上传控制器3.2 图片上传服务4. 实现

Java实现MD5加密的四种方式

《Java实现MD5加密的四种方式》MD5是一种广泛使用的哈希算法,其输出结果是一个128位的二进制数,通常以32位十六进制数的形式表示,MD5的底层实现涉及多个复杂的步骤和算法,本文给大家介绍了Ja... 目录MD5介绍Java 中实现 MD5 加密方式方法一:使用 MessageDigest方法二:使用

mysql删除无用用户的方法实现

《mysql删除无用用户的方法实现》本文主要介绍了mysql删除无用用户的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 1、删除不用的账户(1) 查看当前已存在账户mysql> select user,host,pa

Nginx配置location+rewrite实现隐性域名配置

《Nginx配置location+rewrite实现隐性域名配置》本文主要介绍了Nginx配置location+rewrite实现隐性域名配置,包括基于根目录、条件和反向代理+rewrite配置的隐性... 目录1、配置基于根目录的隐性域名(就是nginx反向代理)2、配置基于条件的隐性域名2.1、基于条件

Linux配置IP地址的三种实现方式

《Linux配置IP地址的三种实现方式》:本文主要介绍Linux配置IP地址的三种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录环境RedHat9第一种安装 直接配置网卡文件第二种方式 nmcli(Networkmanager command-line

Java实现将Markdown转换为纯文本

《Java实现将Markdown转换为纯文本》这篇文章主要为大家详细介绍了两种在Java中实现Markdown转纯文本的主流方法,文中的示例代码讲解详细,大家可以根据需求选择适合的方案... 目录方法一:使用正则表达式(轻量级方案)方法二:使用 Flexmark-Java 库(专业方案)1. 添加依赖(Ma

使用EasyExcel实现简单的Excel表格解析操作

《使用EasyExcel实现简单的Excel表格解析操作》:本文主要介绍如何使用EasyExcel完成简单的表格解析操作,同时实现了大量数据情况下数据的分次批量入库,并记录每条数据入库的状态,感兴... 目录前言固定模板及表数据格式的解析实现Excel模板内容对应的实体类实现AnalysisEventLis

Mybatis从3.4.0版本到3.5.7版本的迭代方法实现

《Mybatis从3.4.0版本到3.5.7版本的迭代方法实现》本文主要介绍了Mybatis从3.4.0版本到3.5.7版本的迭代方法实现,包括主要的功能增强、不兼容的更改和修复的错误,具有一定的参考... 目录一、3.4.01、主要的功能增强2、selectCursor example3、不兼容的更改二、