【可能是全网最丝滑的LangChain教程】十四、LangChain进阶之Text Splitters

我不能改变风向,但我可以调整我的帆,以始终到达目的地。

01 介绍

什么是Text Splitters?

Text Splitters,文本拆分器,顾名思义就是用来拆分(或者叫分割、或者叫切割)文本的。

为什么需要Text Splitters?

加载文档后,通常需要转换它们以更好地适应应用程序。文档加载可以看【LangChain进阶教程】六、LangChain进阶之Document loaders

我们希望将长文档拆分为更小的块,以便符合模型的上下文窗口大小(也就是我们在模型中经常说的token)。

所以当我们处理长文本时,有必要将该文本拆分为块。尽管这听起来很简单,但实现起来还是稍微有点复杂。理想情况下,我们希望将语义相关的文本片段放在一起

概括地说,文本拆分器的工作方式如下:

  1. 将文本拆分为语义上有意义的小块(通常是句子)。
  2. 开始将这些小块组合成一个更大的块,直到达到一定大小(由某些函数测量)。
  3. 达到该大小后,将该块设为自己的文本片段,然后开始创建一个具有一些重叠的新文本块(以保持块之间的上下文)。

这意味着我们可以沿着两个不同的“轴”自定义文本拆分器:

  • 如何拆分文本
  • 如何测量块大小。

LangChain有许多内置的文档转换器,可以很容易地拆分、组合、过滤和以其他方式操作文档。

02 LangChain内置Splitter介绍

Recursively split by character

基于字符的递归文本分割器,它是处理通用文本的推荐工具,可以通过设置字符列表来控制分割行为,默认列表为 [“\n\n”, “\n”, " ", “”],旨在保持段落、句子和单词的完整性

RecursiveCharacterTextSplitter 是一个用于分割文本的工具,它按照用户指定的字符列表进行分割,直到文本块达到预设的大小。该分割器支持设置 chunk_size(块大小)和 chunk_overlap(块重叠大小),以及定义分割行为的 length_functionis_separator_regex 参数。对于没有单词边界的语言(如中文、日文和泰文),可以通过添加额外的分隔符(如全角句号、中文全停、零宽空格等)来避免在分割时拆分单词

使用示例如下:

from langchain_text_splitters import RecursiveCharacterTextSplitter

url = "https://plato.stanford.edu/entries/goedel/"
headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
    ("h4", "Header 4"),
]

html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
html_header_splits = html_splitter.split_text_from_url(url)

# 这里继续做Document的分割,为了演示效果,我将分割后的块大小设置成100,前后关联度设置成80.
# 大家实战中不要这么设置~
chunk_size = 100
chunk_overlap = 80
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)

# Split
splits = text_splitter.split_documents(html_header_splits)
splits[80:85]

"""
[Document(page_content='and the Axiom of Choice with the Axioms of Zermelo-Fraenkel Set Theory 2.4.3 Consequences of', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='of Choice with the Axioms of Zermelo-Fraenkel Set Theory 2.4.3 Consequences of Consistency 2.4.4', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='Axioms of Zermelo-Fraenkel Set Theory 2.4.3 Consequences of Consistency 2.4.4 Gödel’s view of the', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='Set Theory 2.4.3 Consequences of Consistency 2.4.4 Gödel’s view of the Axiom of Constructibility', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='2.5.1 Intuitionistic Propositional Logic is not Finitely-Valued 2.5.2 Classical Arithmetic is', metadata={'Header 1': 'Kurt Gödel'})]
"""

我们也可以添加自定义的分隔符,如下:

text_splitter = RecursiveCharacterTextSplitter(
    separators=[
        "\n\n",
        "\n",
        " ",
        ".",
        ",",
        "\u200b",  # Zero-width space
        "\uff0c",  # Fullwidth comma
        "\u3001",  # Ideographic comma
        "\uff0e",  # Fullwidth full stop
        "\u3002",  # Ideographic full stop
        "",
    ],
    # Existing args
)

Split by HTML header

通过 HTML 标题将文本分割并添加相关的元数据。LangChain 中的 HTML 头部元数据转换器是一个专门为处理 HTML 文档而设计的工具。它在元素级别上对文本进行拆分,并为每个与给定文本块相关的标题添加元数据。这个转换器能够以两种方式返回处理后的内容:一种是将文本块逐个元素返回,另一种是将具有相同元数据的元素组合在一起。这样的处理方式有助于更好地组织和理解 HTML 文档的结构,便于后续的数据处理和分析

这个分块器可以单独使用,也可以与其他文本拆分器(如RecursiveCharacterTextSplitter)组合成分块管道。用户可以通过指定要分割的标签和相应的元数据键值,来定制分割行为。此外,HTMLHeaderTextSplitter 支持从本地文件或网址直接加载 HTML 内容进行分割。分割后的文档块可以包含元数据,这有助于后续的处理和分析。

例如,以下html内容布局:

<!DOCTYPE html>
<html>
<body>
    <div>
        <h1>Foo</h1>
        <p>Some intro text about Foo.</p>
        <div>
            <h2>Bar main section</h2>
            <p>Some intro text about Bar.</p>
            <h3>Bar subsection 1</h3>
            <p>Some text about the first subtopic of Bar.</p>
            <h3>Bar subsection 2</h3>
            <p>Some text about the second subtopic of Bar.</p>
        </div>
        <div>
            <h2>Baz</h2>
            <p>Some text about Baz</p>
        </div>
        <br>
        <p>Some concluding text about Foo</p>
    </div>
</body>
</html>

显示效果如下:

图片

通过HTMLHeaderTextSplitter我们可以将以上html内容进行拆分,HTMLHeaderTextSplitter初始化参数如下:

  1. 参数headers_to_split_on是一个由元组组成的列表,每个元组包含两个元素:要跟踪的头部的值和用于元数据的任意键。头部的值可以是"h1"、“h2”、“h3”、“h4”、“h5”、"h6"中的任意一个。例如:[(“h1”, “Header 1”), (“h2”, “Header 2”)]

  2. 参数return_each_element是一个布尔值,用于指定函数的返回值类型。如果设置为True,则函数将返回每个元素及其关联的头部;如果设置为False,则函数将返回聚合的块,这些块具有相同的头部

ok,下面我门看下分割效果:

from langchain_text_splitters import HTMLHeaderTextSplitter

html_string = """
<!DOCTYPE html>
<html>
<body>
    <div>
        <h1>Foo</h1>
        <p>Some intro text about Foo.</p>
        <div>
            <h2>Bar main section</h2>
            <p>Some intro text about Bar.</p>
            <h3>Bar subsection 1</h3>
            <p>Some text about the first subtopic of Bar.</p>
            <h3>Bar subsection 2</h3>
            <p>Some text about the second subtopic of Bar.</p>
        </div>
        <div>
            <h2>Baz</h2>
            <p>Some text about Baz</p>
        </div>
        <br>
        <p>Some concluding text about Foo</p>
    </div>
</body>
</html>
"""

headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
]
html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on,return_each_element=False)
html_header_splits = html_splitter.split_text(html_string)

# return_each_element 设置成False
"""
[Document(page_content='Foo'),
 Document(page_content='Some intro text about Foo.  \nBar main section Bar subsection 1 Bar subsection 2', metadata={'Header 1': 'Foo'}),
 Document(page_content='Some intro text about Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section'}),
 Document(page_content='Some text about the first subtopic of Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section', 'Header 3': 'Bar subsection 1'}),
 Document(page_content='Some text about the second subtopic of Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section', 'Header 3': 'Bar subsection 2'}),
 Document(page_content='Baz', metadata={'Header 1': 'Foo'}),
 Document(page_content='Some text about Baz', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'}),
 Document(page_content='Some concluding text about Foo', metadata={'Header 1': 'Foo'})]
"""

# return_each_element 设置成True
"""
[Document(page_content='Foo'),
 Document(page_content='Some intro text about Foo.', metadata={'Header 1': 'Foo'}),
 Document(page_content='Bar main section Bar subsection 1 Bar subsection 2', metadata={'Header 1': 'Foo'}),
 Document(page_content='Some intro text about Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section'}),
 Document(page_content='Some text about the first subtopic of Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section', 'Header 3': 'Bar subsection 1'}),
 Document(page_content='Some text about the second subtopic of Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section', 'Header 3': 'Bar subsection 2'}),
 Document(page_content='Baz', metadata={'Header 1': 'Foo'}),
 Document(page_content='Some text about Baz', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'}),
 Document(page_content='Some concluding text about Foo', metadata={'Header 1': 'Foo'})]
"""

通过上面代码我们可以看到:

  1. 当return_each_element设置成True的时候,Document的数量是9个,而False的时候是8个

  2. 除了我们自己定义的顶级标题标签,其他所有的标签内容都包含”上级“标签元数据信息

具体理解,还请大家将 Document 列表和 html 效果图对照,一对照就清晰了。

与其他的分割器一起使用,这里以 RecursiveCharacterTextSplitter 为例:

from langchain_text_splitters import RecursiveCharacterTextSplitter

url = "https://plato.stanford.edu/entries/goedel/"
headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
    ("h4", "Header 4"),
]

html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
html_header_splits = html_splitter.split_text_from_url(url)

# 这里继续做Document的分割,为了演示效果,我将分割后的块大小设置成100,前后关联度设置成80.
# 大家实战中不要这么设置~
chunk_size = 100
chunk_overlap = 80
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)

# Split
splits = text_splitter.split_documents(html_header_splits)
splits[80:85]

"""
[Document(page_content='and the Axiom of Choice with the Axioms of Zermelo-Fraenkel Set Theory 2.4.3 Consequences of', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='of Choice with the Axioms of Zermelo-Fraenkel Set Theory 2.4.3 Consequences of Consistency 2.4.4', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='Axioms of Zermelo-Fraenkel Set Theory 2.4.3 Consequences of Consistency 2.4.4 Gödel’s view of the', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='Set Theory 2.4.3 Consequences of Consistency 2.4.4 Gödel’s view of the Axiom of Constructibility', metadata={'Header 1': 'Kurt Gödel'}), 
 Document(page_content='2.5.1 Intuitionistic Propositional Logic is not Finitely-Valued 2.5.2 Classical Arithmetic is', metadata={'Header 1': 'Kurt Gödel'})]
"""

尽管 HTMLHeaderTextSplitter 提供了强大的功能,但它在处理某些结构复杂或非标准的 HTML 文档时可能会遇到限制,例如在某些文档中,标题和相关文本可能不在同一个子树中,导致分块器无法正确关联标题和文本。

Split by character

CharacterTextSplitter 是 LangChain 中的一个模块,用于按字符拆分长文本。该模块默认使用 “\n\n” 作为分隔符,并以字符数为标准来测量和控制分割后块的大小。

文本拆分的具体步骤包括读取一个长文档,然后使用 CharacterTextSplitter 类来定义分隔符、块大小、块重叠长度、长度函数以及是否使用正则表达式作为分隔符。

此外,还可以通过create_documents方法将文档和元数据一起进行拆分,并且可以通过split_text方法直接分割文本。

与 RecursiveCharacterTextSplitter 的区别是:

  1. CharacterTextSplitter 分隔符只能传递字符串,RecursiveCharacterTextSplitter 可以传分隔符数组

  2. CharacterTextSplitter 在指定分割符后,如果基于分隔符分割后的文本大于指定的 chunk_size,则会保持文本的完整性,此时 chunk_size、chunk_overlap 失效

示例代码展示了如何使用这个模块来拆分一个长文本:

with open("./index.txt",encoding='utf-8') as f:
    state_of_the_union = f.read()

from langchain_text_splitters import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
    separator="\n\n",
    chunk_size=40,
    chunk_overlap=10,
    length_function=len,
    is_separator_regex=False,
)

texts = text_splitter.create_documents([state_of_the_union])

"""
Created a chunk of size 43, which is longer than the specified 40
Created a chunk of size 54, which is longer than the specified 40
Created a chunk of size 55, which is longer than the specified 40
Created a chunk of size 54, which is longer than the specified 40
Created a chunk of size 46, which is longer than the specified 40
Created a chunk of size 80, which is longer than the specified 40
Created a chunk of size 46, which is longer than the specified 40
Created a chunk of size 42, which is longer than the specified 40
Created a chunk of size 47, which is longer than the specified 40

[Document(page_content='电影中的经典台词往往能够深入人心,成为人们记忆中的一部分。以下是一些电影中的经典台词:'),
 Document(page_content='"生活就像一盒巧克力,你永远不知道你会得到什么。" ——《阿甘正传》(Forrest Gump, 1994)'),
 Document(page_content='"永远不要小看自己,因为你永远不知道自己有多强大。" ——《狮子王》(The Lion King, 1994)'),
 Document(page_content='"我会回来的。" ——《终结者》(The Terminator, 1984)'),
 Document(page_content='"你不能改变过去,但你可以改变未来。" ——《回到未来》(Back to the Future, 1985)'),
 Document(page_content='"即使世界末日来临,我也要和你一起度过。" ——《泰坦尼克号》(Titanic, 1997)'),
 Document(page_content='"不要让别人告诉你你能做什么,不能做什么。如果你有一个梦想,就去捍卫它。" ——《当幸福来敲门》(The Pursuit of Happyness, 2006)'),
 Document(page_content='"我将永远爱你。" ——《保镖》(The Bodyguard, 1992)'),
 Document(page_content='"这就是生活,小家伙。" ——《美丽人生》(Life is Beautiful, 1997)'),
 Document(page_content='"我有一个梦想。" ——《我有一个梦想》(I Have a Dream, 1963)'),
 Document(page_content='"你只需要跟随你的黄砖路。" ——《绿野仙踪》(The Wizard of Oz, 1939)'),
 Document(page_content='这些台词不仅在电影中令人难忘,也在现实生活中激励着许多人。')]
"""

此外,还可以通过create_documents方法将文档和元数据一起进行拆分:

metadatas = [{"document": 1}, {"document": 2}]
documents = text_splitter.create_documents(
    [state_of_the_union, state_of_the_union], metadatas=metadatas
)

"""
[Document(page_content='电影中的经典台词往往能够深入人心,成为人们记忆中的一部分。以下是一些电影中的经典台词:', metadata={'document': 1}),
 Document(page_content='"生活就像一盒巧克力,你永远不知道你会得到什么。" ——《阿甘正传》(Forrest Gump, 1994)', metadata={'document': 1}),
 Document(page_content='"永远不要小看自己,因为你永远不知道自己有多强大。" ——《狮子王》(The Lion King, 1994)', metadata={'document': 1}),
 Document(page_content='"我会回来的。" ——《终结者》(The Terminator, 1984)', metadata={'document': 1}),
 Document(page_content='"你不能改变过去,但你可以改变未来。" ——《回到未来》(Back to the Future, 1985)', metadata={'document': 1}),
 Document(page_content='"即使世界末日来临,我也要和你一起度过。" ——《泰坦尼克号》(Titanic, 1997)', metadata={'document': 1}),
 Document(page_content='"不要让别人告诉你你能做什么,不能做什么。如果你有一个梦想,就去捍卫它。" ——《当幸福来敲门》(The Pursuit of Happyness, 2006)', metadata={'document': 1}),
 Document(page_content='"我将永远爱你。" ——《保镖》(The Bodyguard, 1992)', metadata={'document': 1}),
 Document(page_content='"这就是生活,小家伙。" ——《美丽人生》(Life is Beautiful, 1997)', metadata={'document': 1}),
 Document(page_content='"我有一个梦想。" ——《我有一个梦想》(I Have a Dream, 1963)', metadata={'document': 1}),
 Document(page_content='"你只需要跟随你的黄砖路。" ——《绿野仙踪》(The Wizard of Oz, 1939)', metadata={'document': 1}),
 Document(page_content='这些台词不仅在电影中令人难忘,也在现实生活中激励着许多人。', metadata={'document': 1}),
 Document(page_content='电影中的经典台词往往能够深入人心,成为人们记忆中的一部分。以下是一些电影中的经典台词:', metadata={'document': 2}),
 Document(page_content='"生活就像一盒巧克力,你永远不知道你会得到什么。" ——《阿甘正传》(Forrest Gump, 1994)', metadata={'document': 2}),
 Document(page_content='"永远不要小看自己,因为你永远不知道自己有多强大。" ——《狮子王》(The Lion King, 1994)', metadata={'document': 2}),
 Document(page_content='"我会回来的。" ——《终结者》(The Terminator, 1984)', metadata={'document': 2}),
 Document(page_content='"你不能改变过去,但你可以改变未来。" ——《回到未来》(Back to the Future, 1985)', metadata={'document': 2}),
 Document(page_content='"即使世界末日来临,我也要和你一起度过。" ——《泰坦尼克号》(Titanic, 1997)', metadata={'document': 2}),
 Document(page_content='"不要让别人告诉你你能做什么,不能做什么。如果你有一个梦想,就去捍卫它。" ——《当幸福来敲门》(The Pursuit of Happyness, 2006)', metadata={'document': 2}),
 Document(page_content='"我将永远爱你。" ——《保镖》(The Bodyguard, 1992)', metadata={'document': 2}),
 Document(page_content='"这就是生活,小家伙。" ——《美丽人生》(Life is Beautiful, 1997)', metadata={'document': 2}),
 Document(page_content='"我有一个梦想。" ——《我有一个梦想》(I Have a Dream, 1963)', metadata={'document': 2}),
 Document(page_content='"你只需要跟随你的黄砖路。" ——《绿野仙踪》(The Wizard of Oz, 1939)', metadata={'document': 2}),
 Document(page_content='这些台词不仅在电影中令人难忘,也在现实生活中激励着许多人。', metadata={'document': 2})]
"""

并且可以通过split_text方法直接分割文本:

text_splitter.split_text(state_of_the_union)

"""python
['电影中的经典台词往往能够深入人心,成为人们记忆中的一部分。以下是一些电影中的经典台词:',
 '"生活就像一盒巧克力,你永远不知道你会得到什么。" ——《阿甘正传》(Forrest Gump, 1994)',
 '"永远不要小看自己,因为你永远不知道自己有多强大。" ——《狮子王》(The Lion King, 1994)',
 '"我会回来的。" ——《终结者》(The Terminator, 1984)',
 '"你不能改变过去,但你可以改变未来。" ——《回到未来》(Back to the Future, 1985)',
 '"即使世界末日来临,我也要和你一起度过。" ——《泰坦尼克号》(Titanic, 1997)',
 '"不要让别人告诉你你能做什么,不能做什么。如果你有一个梦想,就去捍卫它。" ——《当幸福来敲门》(The Pursuit of Happyness, 2006)',
 '"我将永远爱你。" ——《保镖》(The Bodyguard, 1992)',
 '"这就是生活,小家伙。" ——《美丽人生》(Life is Beautiful, 1997)',
 '"我有一个梦想。" ——《我有一个梦想》(I Have a Dream, 1963)',
 '"你只需要跟随你的黄砖路。" ——《绿野仙踪》(The Wizard of Oz, 1939)',
 '这些台词不仅在电影中令人难忘,也在现实生活中激励着许多人。']
"""

Split code

LangChain 的 CodeTextSplitter 模块是一个用于拆分代码文本的工具,它支持多种编程语言,包括但不限于 Python、JavaScript、TypeScript、Markdown、LaTeX、HTML、Solidity、C#、Haskell 和 PHP。

用户可以通过导入 Language 枚举并指定特定的编程语言来使用该工具。该模块提供了 RecursiveCharacterTextSplitter 类,用于递归地将文本拆分成更小的部分,以便进行进一步的处理。用户可以根据需要设置 chunk_size 和 chunk_overlap 参数来控制拆分的粒度。

使用方式如下,这里以 Python 代码为例:

from langchain_text_splitters import (
    Language,
    RecursiveCharacterTextSplitter,
)

# Full list of supported languages
[e.value for e in Language]

"""
['cpp',
 'go',
 'java',
 'kotlin',
 'js',
 'ts',
 'php',
 'proto',
 'python',
 'rst',
 'ruby',
 'rust',
 'scala',
 'swift',
 'markdown',
 'latex',
 'html',
 'sol',
 'csharp',
 'cobol',
 'c',
 'lua',
 'perl']
"""

# You can also see the separators used for a given language
RecursiveCharacterTextSplitter.get_separators_for_language(Language.PYTHON)

"""
['\nclass ', '\ndef ', '\n\tdef ', '\n\n', '\n', ' ', '']
"""

# 以Python代码为示例,其他代码语言类似
PYTHON_CODE = """
def hello_world():
    print("Hello, World!")

# Call the function
hello_world()
"""
python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
python_docs = python_splitter.create_documents([PYTHON_CODE])

"""
[Document(page_content='def hello_world():\n    print("Hello, World!")'),
 Document(page_content='# Call the function\nhello_world()')]
"""

MarkdownHeaderTextSplitter

MarkdownHeaderTextSplitter 是 LangChain 中的一个模块,旨在通过遵循 Markdown 文件的标题结构来对文本进行分块。

这种方法可以确保具有相同上下文的文本被分组在一起,从而在嵌入和矢量存储阶段获得更全面的向量表示。

使用该工具,用户可以指定要拆分的标题层级,例如使用 #、## 和 ### 等。默认情况下,MarkdownHeaderTextSplitter 会从分割后的文本块中移除这些标题,但用户可以通过设置 strip_headers = False 来保留它们。

对如下效果的 markdown 内容进行分割:

图片

使用方式如下:

from langchain_text_splitters import MarkdownHeaderTextSplitter

markdown_document = "# Foo\n\n    ## Bar\n\nHi this is Jim\n\nHi this is Joe\n\n ### Boo \n\n Hi this is Lance \n\n ## Baz\n\n Hi this is Molly"

headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on, strip_headers=True)
md_header_splits = markdown_splitter.split_text(markdown_document)

"""
[Document(page_content='Hi this is Jim  \nHi this is Joe', metadata={'Header 1': 'Foo', 'Header 2': 'Bar'}),
 Document(page_content='Hi this is Lance', metadata={'Header 1': 'Foo', 'Header 2': 'Bar', 'Header 3': 'Boo'}),
 Document(page_content='Hi this is Molly', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'})]
"""

此外,该工具还支持在每个分割的 Markdown 组中应用其他文本拆分器,例如 RecursiveCharacterTextSplitter,以进行更细粒度的分割。这使得 MarkdownHeaderTextSplitter 成为一个灵活且强大的文本分割工具,适用于需要处理结构化文档的场景。

这里可以和 HTMLHeaderTextSplitter 做对比理解,代码如下:

markdown_document = "# Intro \n\n    ## History \n\n Markdown[9] is a lightweight markup language for creating formatted text using a plain-text editor. John Gruber created Markdown in 2004 as a markup language that is appealing to human readers in its source code form.[9] \n\n Markdown is widely used in blogging, instant messaging, online forums, collaborative software, documentation pages, and readme files. \n\n ## Rise and divergence \n\n As Markdown popularity grew rapidly, many Markdown implementations appeared, driven mostly by the need for \n\n additional features such as tables, footnotes, definition lists,[note 1] and Markdown inside HTML blocks. \n\n #### Standardization \n\n From 2012, a group of people, including Jeff Atwood and John MacFarlane, launched what Atwood characterised as a standardisation effort. \n\n ## Implementations \n\n Implementations of Markdown are available for over a dozen programming languages."

headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
]

# MD splits
markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on, strip_headers=False
)
md_header_splits = markdown_splitter.split_text(markdown_document)

# Char-level splits
from langchain_text_splitters import RecursiveCharacterTextSplitter

chunk_size = 250
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)

# Split
splits = text_splitter.split_documents(md_header_splits)

"""
[Document(page_content='# Intro  \n## History  \nMarkdown[9] is a lightweight markup language for creating formatted text using a plain-text editor. John Gruber created Markdown in 2004 as a markup language that is appealing to human readers in its source code form.[9]', metadata={'Header 1': 'Intro', 'Header 2': 'History'}),
 Document(page_content='Markdown is widely used in blogging, instant messaging, online forums, collaborative software, documentation pages, and readme files.', metadata={'Header 1': 'Intro', 'Header 2': 'History'}),
 Document(page_content='## Rise and divergence  \nAs Markdown popularity grew rapidly, many Markdown implementations appeared, driven mostly by the need for  \nadditional features such as tables, footnotes, definition lists,[note 1] and Markdown inside HTML blocks.', metadata={'Header 1': 'Intro', 'Header 2': 'Rise and divergence'}),
 Document(page_content='#### Standardization  \nFrom 2012, a group of people, including Jeff Atwood and John MacFarlane, launched what Atwood characterised as a standardisation effort.', metadata={'Header 1': 'Intro', 'Header 2': 'Rise and divergence'}),
 Document(page_content='## Implementations  \nImplementations of Markdown are available for over a dozen programming languages.', metadata={'Header 1': 'Intro', 'Header 2': 'Implementations'})]
"""

Recursively split JSON

递归 JSON 拆分器(RecursiveJsonSplitter)是一个用于处理大型 JSON 数据的工具,它通过深度优先的方式遍历 JSON 数据,并将其拆分成更小的 JSON 块。

这个拆分器的目标是尽可能地保持嵌套 JSON 对象的完整性,但在必要时会对它们进行拆分,以确保每个块的大小都在用户设定的最小块大小(min_chunk_size)和最大块大小(max_chunk_size)之间。

如果 JSON 值是一个非常大的字符串,该拆分器不会对字符串进行拆分。

如果用户需要对块大小进行硬性限制,可以在拆分后的块上使用递归文本拆分器(RecursiveCharacterTextSplitter)。此外,该工具还支持一个可选的预处理步骤,将列表转换为字典形式(即将列表中的每个元素转换为键值对,键为索引),然后再进行拆分。

用户可以通过调用 split_json 方法来递归拆分 JSON 数据:

import requests
from langchain_text_splitters import RecursiveJsonSplitter

# 加载json数据
json_data = requests.get("https://api.smith.langchain.com/openapi.json").json()

splitter = RecursiveJsonSplitter(max_chunk_size=300)

# Recursively split json data - If you need to access/manipulate the smaller json chunks
json_chunks = splitter.split_json(json_data=json_data)
json_chunks[0]

"""
{'openapi': '3.1.0',
 'info': {'title': 'LangSmith', 'version': '0.1.0'},
 'paths': {'/api/v1/sessions/{session_id}': {'get': {'tags': ['tracer-sessions'],
    'summary': 'Read Tracer Session',
    'description': 'Get a specific session.'}}}}
"""

或者使用 create_documents 方法来生成文档对象:

# The splitter can also output documents
docs = splitter.create_documents(texts=[json_data])
docs[0]

"""
Document(page_content='{"openapi": "3.1.0", "info": {"title": "LangSmith", "version": "0.1.0"}, "paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": ["tracer-sessions"], "summary": "Read Tracer Session", "description": "Get a specific session."}}}}')
"""

或者使用 split_text 方法来获取一系列字符串:

# or a list of strings
texts = splitter.split_text(json_data=json_data)
texts[0]

"""
'{"openapi": "3.1.0", "info": {"title": "LangSmith", "version": "0.1.0"}, "paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": ["tracer-sessions"], "summary": "Read Tracer Session", "description": "Get a specific session."}}}}'
"""

在拆分列表时,可以通过设置 convert_lists=True 参数来启用预处理步骤,将列表转换为字典形式,从而确保拆分后的块都不超过最大块大小:

# The json splitter by default does not split lists
# the following will preprocess the json and convert list to dict with index:item as key:val pairs
texts = splitter.split_text(json_data=json_data, convert_lists=True)
texts[0]

"""
'{"openapi": "3.1.0", "info": {"title": "LangSmith", "version": "0.1.0"}, "paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": ["tracer-sessions"], "summary": "Read Tracer Session", "description": "Get a specific session."}}}}'
"""

Semantic Chunking

语义分块的工作原理,即首先将文本分割成句子,然后将这些句子分组,最后合并相似性高的句子组。

这个过程依赖于嵌入空间中的相似性判断

使用方式如下:

import torch
from langchain_experimental.text_splitter import SemanticChunker
from langchain_community.embeddings import HuggingFaceEmbeddings

with open("./custom_loader.txt",encoding='utf-8') as f:
    state_of_the_union = f.read()

# 词嵌入模型
EMBEDDING_DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
embeddings = HuggingFaceEmbeddings(model_name='D:\models\m3e-base', model_kwargs={'device': EMBEDDING_DEVICE})

text_splitter = SemanticChunker(embeddings)

docs = text_splitter.create_documents([state_of_the_union])
docs

"""
No sentence-transformers model found with name D:\models\m3e-base. Creating a new one with MEAN pooling.

[Document(page_content='这是第一行\n\n第二行是空白行,这是第三行')]
"""

Split by tokens

在处理自然语言处理(NLP)任务时,如何使用各种分词器(tokenizers)来将文本拆分为适合语言模型处理的令牌(token)大小。

语言模型有最大令牌数限制,因此在处理大型文本时需要将文本拆分。

  1. tiktoken: 由 OpenAI 创建的快速 BPE 分词器,适用于 OpenAI 模型,可以用来估计使用的代币数量。
  2. spaCy: 一个用于高级自然语言处理的开源软件库,可以用作 NLTK 的替代分词器。
  3. SentenceTransformers: 提供了一个专用的文本拆分器,用于将文本拆分为适合句子转换器模型的标记窗口大小。
  4. NLTK: 一个用于英语符号和统计自然语言处理的库和程序集合,可以根据 NLTK 分词器进行文本拆分。
  5. KoNLPy: 专门用于韩语的 Python NLP 包,其中包含了 Kkma 分析器,用于对韩语文本进行详细的形态分析和句子分割。
  6. Hugging Face tokenizer: 如 GPT2TokenizerFast,用于计算文本长度以及进行文本拆分。

tiktoken 使用如下:

with open("./index.txt", encoding='utf-8') as f:
    state_of_the_union = f.read()

from langchain_text_splitters import CharacterTextSplitter

text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding="cl100k_base", chunk_size=100, chunk_overlap=0
)
# 或者
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    model_name="gpt-4", chunk_size=100, chunk_overlap=0
)
texts = text_splitter.split_text(state_of_the_union)

spaCy 使用如下:

from langchain_text_splitters import SpacyTextSplitter

text_splitter = SpacyTextSplitter(chunk_size=1000)
texts = text_splitter.split_text(state_of_the_union)

SentenceTransformers 使用如下:

from langchain_text_splitters import SentenceTransformersTokenTextSplitter

splitter = SentenceTransformersTokenTextSplitter(chunk_overlap=0)

texts = splitter.split_text(state_of_the_union)

NLTK使用如下:

from langchain_text_splitters import NLTKTextSplitter

text_splitter = NLTKTextSplitter(chunk_size=1000)
texts = text_splitter.split_text(state_of_the_union)

KoNLPy 使用如下:

from langchain_text_splitters import KonlpyTextSplitter

text_splitter = KonlpyTextSplitter()
texts = text_splitter.split_text(state_of_the_union)

Hugging Face tokenizer 使用如下:

from transformers import GPT2TokenizerFast

tokenizer = GPT2TokenizerFast.from_pretrained("gpt2")

text_splitter = CharacterTextSplitter.from_huggingface_tokenizer(
    tokenizer, chunk_size=100, chunk_overlap=0
)
texts = text_splitter.split_text(state_of_the_union)

03 总结

这里我们要把握一个核心:无论 LangChain 玩的多么花里胡哨,它最终都是服务于 LLM。正是因为 LLM 的上下文窗口大小有限制,所以才有了各种不同的 Text Splitter

到这里,LangChain 中的 Text Splitters 就全部讲解完毕。

如果能帮我点个免费的关注,那就是对我个人的最大的肯定。

在这里插入图片描述

以上内容依据官方文档编写,官方地址:https://python.langchain.com/docs/modules/data_connection/document_transformers

图片

【LangChain进阶教程】六、LangChain进阶之Document loaders

【LangChain进阶教程】五、LangChain进阶之Retrieval

【LangChain进阶教程】四、LangChain进阶之Output Parsers

【LangChain进阶教程】三、LangChain进阶之Prompts

  • 31
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值