在阅读本文前,建议先看下langchain的基础,最主要的是先看下langchain 文档加载器使用教程有关的内容,会更容易把知识串联起来。

概述

        一旦加载了文档,您通常会想要转换它们以更好地适应您的应用程序。最简单的例子是,您可能希望将一个长文档分割成更小的块,以便适合模型的上下文窗口。LangChain有许多内置的文档转换器,可以轻松地拆分、组合、过滤和操作文档。

        当你想处理很长的文本时,有必要将文本分割成块。虽然这听起来很简单,但这里有很多潜在的复杂性。理想情况下,您希望将语义相关的文本片段放在一起。“语义相关”的含义可能取决于文本的类型。示例展示了几个方法来做到这一点。

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

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

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

  1. 文本的拆分方式
  2. 如何测量区块大小

①、文本拆分器的类型

        LangChain提供了许多不同类型的文本拆分器。这些都存在langchain-text-splitters包里。下表列出了所有的因素以及一些特征:

Name:文本拆分器的名称

Splits on:此文本拆分器如何拆分文本

Adds Metadata:此文本拆分器是否添加关于每个块来源的元数据。

Description:拆分器的描述,包括何时使用它的建议。

NameSplits OnAdds MetadataDescription
RecursiveA list of user defined characters递归拆分文本。递归分割文本的目的是试图将相关的文本片段保持在一起。这是开始拆分文本的推荐方式。
HTMLHTML specific characters基于特定于HTML的字符拆分文本。值得注意的是,这添加了关于该块来自何处的相关信息(基于HTML)
MarkdownMarkdown specific characters基于特定于Markdown的字符拆分文本。值得注意的是,这增加了关于该块来自哪里的相关信息(基于降价)
CodeCode (Python, JS) specific characters基于特定于编码语言的字符拆分文本。有15种不同的语言可供选择。
TokenTokens拆分令牌上的文本。有几种不同的方法来衡量token。
CharacterA user defined character基于用户定义的字符拆分文本。一种更简单的方法。
[Experimental] Semantic ChunkerSentences首先对句子进行拆分。然后,如果它们在语义上足够相似,就将它们组合在一起。取自Greg Kamradt

②、评估文本拆分器

        您可以使用Chunkviz实用程序Greg Kamradt创造.,Chunkviz是一个很好的工具,可视化你的文本拆分器是如何工作的。它将向您展示文本是如何拆分的,并帮助调整拆分参数。(这里不展开说,本文主要还是讲拆分器有关内容。)

一、按字符拆分 Split by character

        这是最简单的方法。这将基于字符进行拆分(默认情况下为 ““),并根据字符数测量块长度。

  1. 文本如何拆分:按单个字符拆分。
  2. 如何测量块大小:通过字符数。
pip install -qU langchain-text-splitters

在网上找一篇长文章,然后复制到spilitters_text.txt文本中,用于测试用例 

例如:人民财评:花香阵阵游人醉,“春日经济”热力足

# This is a long document we can split up.
with open("../../splitters_test.txt") as f:
    state_of_the_union = f.read()
- separators - 分隔符字符串数组
- chunk_size - 每个文档的字符数量限制
- chunk_overlap - 两份文档重叠区域的长度
- length_function - 长度计算函数
- is_separator_regex - 如果为真:应当被解释为正则表达式,因此不需要转义。如果为假:应当被当作普通字符串分隔符,并转义任何特殊字符。
from langchain_text_splitters import CharacterTextSplitter

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

texts = text_splitter.create_documents([state_of_the_union])
print(texts[0])

打印结果:只打印了第一段文字

这是一个将元数据与文档一起传递的示例,请注意它是与文档一起分割的。

meta_datas = [{"document": 1}, {"document": 2}]
documents = text_splitter.create_documents(
    [state_of_the_union, state_of_the_union], metadatas=meta_datas
)
print(len(documents))
print(documents[0])
print(documents[3])

 这里清晰看到分成了2个文档数据。 

上面返回的都是以文档格式获取到数据,另外还有一种是直接得到字符串数据。

text_splitter.split_text(state_of_the_union)[0]

开头不再是page_content,而是双引号,这就是字符串格式。

二、按字符递归拆分 Recursively split by character

        对于通用文本,建议使用此文本拆分器。它由字符列表参数化。它试图按顺序分割它们,直到这些块足够小。默认列表是["\n\n", "\n", " ", ""]。这样做的效果是尽量将所有段落(然后是句子,然后是单词)保持在一起,因为这些段落一般看起来是语义最相关的文本片段。

  1. 文本如何拆分:按字符列表。
  2. 如何测量块大小:通过字符数。
pip install -qU langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter

# This is a long document we can split up.
with open("./demo_static/splitters_test.txt") as f:
    state_of_the_union = f.read()
text_splitter = RecursiveCharacterTextSplitter(
    # Set a really small chunk size, just to show.
    chunk_size=100,
    chunk_overlap=20,
    length_function=len,
    is_separator_regex=False,
)

texts = text_splitter.create_documents([state_of_the_union])
print(texts[0])
print(texts[1])

【敲重点啊!】

 从没有单词边界的语言中拆分文本

一些书写系统没有单词边界,例如 中文 、日文和泰文。使用默认分隔符列表拆分文本["\n\n", "\n", " ", ""]会导致单词在单词块之间拆分。要将单词放在一起,您可以覆盖分隔符列表以包括附加标点符号:

  • 添加ASCII句号“.”, Unicode句号““(用于中文文本),以及象形字句号 ““(用于日语和汉语)
  • 增加 zero-width space 用于泰语、缅甸语、缅甸语和日语。
  • 添加ASCII逗号“,“,Unicode全角逗号““,和Unicode表意逗号“
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
)

三、语义组块 Semantic Chunking

        基于语义相似性拆分文本。

        摘自Greg Kamradt的精彩笔记本:5 _ Levels _ Of _ Text _拆分

        一切都归功于他。

        在高层次上,这将拆分成句子,然后分组为3个句子的组,然后合并一个在嵌入空间中相似的句子。

安装依赖项

pip install --quiet langchain_experimental langchain_openai

加载示例数据

# This is a long document we can split up.
with open("./demo_static/splitters_test.txt") as f:
    state_of_the_union = f.read()

创建文本拆分器 

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings

text_splitter = SemanticChunker(OpenAIEmbeddings())

拆分文本

docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)

断点 

        这个分块器的工作原理是决定何时“拆分”句子。这是通过寻找任何两个句子之间嵌入的差异来实现的。当差异超过某个阈值时,它们就会被拆分。

        有几种方法可以确定这个阈值。

 - 百分位

        默认的分割方式是基于百分位数。在这种方法中,计算句子之间的所有差异,然后拆分任何大于X百分位的差异。

text_splitter = SemanticChunker(
    OpenAIEmbeddings(), breakpoint_threshold_type="percentile"
)
docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)

 - 标准偏差

在这种方法中,任何大于X个标准差的差异都会被拆分。

text_splitter = SemanticChunker(
    OpenAIEmbeddings(), breakpoint_threshold_type="standard_deviation"
)
docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)

 - 四分位间距

在这种方法中,四分位间距用于分割组块。

text_splitter = SemanticChunker(
    OpenAIEmbeddings(), breakpoint_threshold_type="interquartile"
)
docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)

 

四、按token拆分  split by tokens 

        语言模型有一个token限制。不应该超过token限额。因此,当您将文本分成块时,计算token的数量是一个好主意。有许多tokenizer。计算文本中的token时,应该使用语言模型中的tokenizer。

① 第一种:tiktoken

tiktoken 是一种快速 BPE tokenizer,是由OpenAI创建的。

 我们可以用它来估计使用的token数。用在OpenAI模型会更准确。

  1. 文本如何拆分:按传入的字符。
  2. 如何测量块大小:通过tiktoken标记器。
pip install --upgrade --quiet langchain-text-splitters tiktoken
from langchain_text_splitters import CharacterTextSplitter
# This is a long document we can split up.
with open("./demo_static/splitters_test.txt") as f:
    state_of_the_union = f.read()

使用from_tiktoken_encoder()方法采用model_name作为一个参数(例如gpt-4)。所有附加参数,如chunk_sizechunk_overlap,以及separators用于实例化CharacterTextSplitter:

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)

        注意,如果我们使用CharacterTextSplitter.from_tiktoken_encoder,文本仅由CharacterTextSplittertiktokentokenizer用于合并拆分。这意味着拆分可能大于块大小,通过tiktoken tokenizer。我们可以使用RecursiveCharacterTextSplitter.from_tiktoken_encoder为了确保拆分不大于语言模型所允许的标记块大小,如果每个拆分具有更大的大小,则将对其进行递归拆分:

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    model_name="gpt-4",
    chunk_size=100,
    chunk_overlap=0,
)

        我们还可以直接加载tiktoken拆分器,这将确保每次拆分都小于块大小。

from langchain_text_splitters import TokenTextSplitter

text_splitter = TokenTextSplitter(chunk_size=10, chunk_overlap=0)

texts = text_splitter.split_text(state_of_the_union)
print(texts[0])

        一些书面语言(例如 中文 和日文)具有编码为2个或更多个标记的字符。使用TokenTextSplitter直接可以在两个块之间拆分字符的标记,从而导致Unicode字符格式错误。使用RecursiveCharacterTextSplitter.from_tiktoken_encoder或者CharacterTextSplitter.from_tiktoken_encoder确保块包含有效的Unicode字符串。

② 第二种:spaCy

spaCy 是一个用于高级自然语言处理的开源软件库,用编程语言Python和Cython编写。

  1. 文本拆分方式:由spaCy标记器。
  2. 如何测量块大小:通过字符数。
pip install --upgrade --quiet  spacy
from langchain_text_splitters import SpacyTextSplitter
# This is a long document we can split up.
with open("./demo_static/splitters_test.txt") as f:
    state_of_the_union = f.read()
text_splitter = SpacyTextSplitter(chunk_size=1000)

texts = text_splitter.split_text(state_of_the_union)
print(texts[0])

③第三种:SentenceTransformers

SentenceTransformersTokenTextSplitter是一个专门的文本拆分器,用于句子转换模型。默认行为是将文本分割成适合您想要使用的句子转换器模型的标记窗口的块。

from langchain_text_splitters import SentenceTransformersTokenTextSplitter
splitter = SentenceTransformersTokenTextSplitter(chunk_overlap=0)
text = "Lorem "
count_start_and_stop_tokens = 2
text_token_count = splitter.count_tokens(text=text) - count_start_and_stop_tokens
print(text_token_count)
token_multiplier = splitter.maximum_tokens_per_chunk // text_token_count + 1

# `text_to_split` does not fit in a single chunk
text_to_split = text * token_multiplier

print(f"tokens in text to split: {splitter.count_tokens(text=text_to_split)}")
text_chunks = splitter.split_text(text=text_to_split)

print(text_chunks[1])

④ 第四种:NLTK

自然语言工具包或者更常见的是NLTK是一套用于以Python编程语言编写的英语符号和统计自然语言处理(NLP)的库和程序。

除了在“”上拆分,我们还可以使用NLTK根据 NLTK Tokenizer  分开
.

  1. 文本拆分方式:由NLTK标记器。
  2. 如何测量块大小:通过字符数。
pip install nltk
from langchain_text_splitters import NLTKTextSplitter

# This is a long document we can split up.
with open("./demo_static/splitters_test.txt") as f:
    state_of_the_union = f.read()
text_splitter = NLTKTextSplitter(chunk_size=1000)

texts = text_splitter.split_text(state_of_the_union)
print(texts[0])

总结

总的来说,各种分割方式都有各自的优势,要按实际的业务场景来进行选择,例如简单的字符分割就能实现基础文本的问答需求。如果以中文文档为主的建议用Recursively split by character。目前各大平台主要是以token来计费,可想而知其重要性,也要重点关注实际的使用。

创作不易,给个三连(点赞、收藏、关注),同学们的满意是我(H-大叔)的动力。

 代码运行有问题或其他建议,请在留言区评论,看到就会回复,不用私聊。

专栏人工智能 | 大模型 | 实战与教程里面还有其他人工智能|大数据方面的文章,可继续食用,持续更新。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐