觉得langchain的文本分割器种类与参数难以界定?自己动手编写文本分割函数!
a.背景信息:将一个chatgpt所需的参考下文(文档问答+基础数据)进行文本分割
参考下文的文档信息(节选)如下:
参考下文的基础数据信息(节选)如下:
a. 使用langchain文本分割出的文本块问题:
首先,经过langchain的TokenTextSplitter分割器去分割,会造成的问题之一为,size的不均匀,有些块会过小。(即文本块的方差较大)且如果按照tokens计算的方式去划分的话造成问题:
- 一个问题的回答可能会被分成两部分,导致第二部分不匹配任何问题的答案,就会变为无关信息。
- 因为我自己最终采用的策略是将最相关的4个文本块进行拼接作为下为提供给chatgpt,是按照最大的tokens走的,如果文本块的tokens过小,会使得chatgpt参考的上下文过少从而导致问题回答出现偏差。
b. 如何解决?
为了解决以上问题,决定自己编写文本分割函数。
首先为一个计算tokens的函数:
import tiktoken
def tokens_count(text:str):
encoder=tiktoken.encoding_for_model("gpt-3.5-turbo")
tokens=len(encoder.encode(text))
return tokens
利用encoder=tiktoken.encoding_for_model得到大语言模型的编码器,使用它进行编码得到的是字符串序列转化为的tokens序列
token序列解释:
token序列为一个大列表其中列表的每个元素代表一个token,(一个token可能代表一个字符,也可能代表多个字符,也能多个token代表一个字符)
如果在切片解码过程中,多个token代表的一个字符被分开的话,会在末尾产生’�’字符。
from langchain.schema.document import Document
def my_split(path, block_length, stop_list, overlap_range):
with open(path, "r", encoding="utf-8") as f:
text = f.readlines()
text_str = str()
for i in range(len(text)):
text_str += text[i]
encoder=tiktoken.encoding_for_model("gpt-3.5-turbo")
encode_text_str=encoder.encode(text_str)
blocks = []
start_idx = 0
remaining_tokens=tokens_count(text_str)
remaining_str=copy(text_str)
#文本分块
while remaining_tokens!=0:
#print("dfd")
if block_length >remaining_tokens:
blocks.append(remaining_str)
break
temp_str=encoder.decode(encode_text_str[:block_length-overlap_range])[:-1]
remaining_str=remaining_str[len(temp_str):]
encode_text_str=encoder.encode(remaining_str)
toprange_str=encoder.decode(encode_text_str[:overlap_range])
stop_idx = None
for j in stop_list:
for i in range(len(toprange_str) - 1, -1, -1):
if toprange_str[i] == j:
stop_idx = i
break
if stop_idx:
temp_str+=toprange_str[:stop_idx+1]
remaining_str=remaining_str[stop_idx+1:]
encode_text_str=encoder.encode(remaining_str)
blocks.append(temp_str)
else:
temp_str+=toprange_str
remaining_str=remaining_str[len(toprange_str):]
encode_text_str=encoder.encode(remaining_str)
blocks.append(temp_str)
remaining_tokens=tokens_count(remaining_str)
#去掉标记并形成document对象
for i in range(len(blocks)):
blocks[i] = blocks[i].replace('#', '')
blocks[i] = Document(page_content=blocks[i], metadata={"source": path})
return blocks
A. 标记:
- 在文本设置’#‘作为最高切分优先级标记,即将按照’#'进行切分的文本块,作为一个完美切分。完美切分即一个文本块内所有的问题都有一个完整的回复。
- 优先级标记是需要根据自己的文本内容进行自定义的
B. 参数解释:
path: 为原始文本路径
block_length:一个文本块的最大tokens
stop_list: 优先级标记切分列表
overlap_range: block_length-overlap_range为一个文本块的最小tokens,即在**[block_length-overlap_range,block_length]**范围内按照标记优先级进行检索,找到最优切分点。
C. 运行流程:
1. 初始化
-
首先读取文件得到文件字符串:text_str
-
之后设置
remaining_str作为剩余的字符串
encode_text_str作为tokens序列(为剩余字符串的)
remaining_tokens作为剩余tokens
2. 循环:
-
条件: remaining_tokens!=0
-
一个循环内流程:
-
将encoder.decode(encode_text_str[:block_length-overlap_range])[:-1] 作为确定字符串(-1是为了取消’�’字符造成的影响)
-
toprange_str=encoder.decode(encode_text_str[:overlap_range]) 作为判断是否存在切分点的未确定字符串
-
for j in stop_list: for i in range(len(toprange_str) - 1, -1, -1): if toprange_str[i] == j: stop_idx = i break
为了让tokens最接近最大tokens,使用倒序遍历字符串,找到最优先标记并返回索引
-
最终判断:
if stop_idx: temp_str+=toprange_str[:stop_idx+1] remaining_str=remaining_str[stop_idx+1:] encode_text_str=encoder.encode(remaining_str) blocks.append(temp_str) else: temp_str+=toprange_str remaining_str=remaining_str[len(toprange_str):] encode_text_str=encoder.encode(remaining_str) blocks.append(temp_str) remaining_tokens=tokens_count(remaining_str)
就是将不确定字符串与确定字符串进行拼接,并更新remaining_tokens,remaining_str,encode_text_str
-
c.最终结果:
我们由上图可以看出文本块的tokens数值方差较小且由于我们的优先级标记机制,每个文本块都是最完美或是接近切分的。