大模型之RAG:如何做好文档切分,长上下文是否替代了RAG?

科技   2024-11-01 17:30   上海  
点击蓝字,立即关注




前言



回顾前序,我在写RAG系列:

大模型之RAG系列,走进RAG以及它未来的发展趋势

大模型之RAG:LLM性能的提升,RAG与Fine-Tune该如何选择?


我们对RAG有了基本的认识,也了解了下他的原理与发展。


Tips

  1. 果然不能让自己太忙,太忙挤占了学习成长空间。

  2. 大模型发展越来越快,按说新生技术是不能间隔这么久的,先机都被别人占了。希望也有一些和我一样最近没来得及学习的人。


目前主流大模型支持的上下文长度越来越长,Kimi支持200万字的上下文。在此背景下,也出现了一个新的声音:长上下文是否替代了RAG?


基础永远是基石

不管如何变化,我们扎马步的功夫还是不能抛弃。本文我们会介绍,从0搭建一个自己的RAG,需要哪些步骤,重点介绍第一步,如何做好文档切分。





RAG系统搭建的基本流程



  1. 准备对应的垂域资料

  2. 文档的读取解析,进行文档切分(本文重点介绍切分策略与方法)

  3. 将分割好的文本灌入检索引擎(向量数据库)

  4. 封装检索接口

  5. 构建流程:Query -> 检索 -> Prompt -> LLM -> 回复


形成如下的应用场景:





文档切分



文档切分方法


一、基于规则的切分方法

根据预定义的规则和标准进行文本切分,往往缺乏灵活性和对复杂语义的深入理解,如:

  • 基于字符分块 : 据固定字符数目以及特定的字符进行切分。

  • 固定大小分块:指定每个块的固定令牌数,通常会有一些重叠,以保持语义连贯性。

  • 基于token的分块:根据固定的token数进行切分,每个令牌代表一个词或语素,通常使用与目标语言模型相同的分词器。

  • 内容感知分块:使用 NLTK ,spaCy 等这工具来实现基于内容的切分,比如利用句子分割、识别段落、标题和标点符号。

  • 根据规则递归分块:递归分块首先尝试按照一定的标准(如段落或标题)分割文本,如果分割后的文本块仍然过大,就会在这些块上重复进行分割过程,直到所有块的大小都符合要求。这种方法适用于需要将长文本细分为较小片段的场景,同时尽量保持每个块的独立性和完整性。

  • 针对特定数据的分块:这些方法尊重内容的结构和格式元素,确保语义连贯性。例如,Markdown文本可以根据标题、列表和代码块等元素进行分块,而LaTeX文本可以根据章节、小节和公式等逻辑单元进行分块。


二、基于语义聚类的切分方法

基于嵌入的语义分块本质上是通过一个滑动窗口(combined_sentence )来计算相似度。那些相邻并且满足阈值的句子会被归为一个分块。如LlamaIndex中的SemanticSplitterNodeParser方法,其主要步骤如下:

  1. 文本嵌入:首先,文本被输入到一个嵌入模型中(如OpenAI的Embedding Model),该模型将文本中的每个句子或段落转换成高维空间中的向量。这些向量代表了文本的语义特征。

  2. 语义分析:利用这些嵌入向量,可以通过计算向量之间的相似度来评估句子或段落之间的语义关系。如通过余弦相似度等度量,来确定哪些文本部分在内容上是相似的。

  3. 分块决策:基于预设的相似度阈值或其他标准,这些语义相近的文本段落被分组为一个块(chunk)。这个过程通常涉及到确定“断点”,即在哪里开始新的分块,以确保每个分块在语义上尽可能的独立和完整。


三、基于机器学习模型的方法

利用自然语言模型,如BERT和其他Transformer模型,这些方法通过学习文本中的语言模式来预测最合适的分块点。


这些模型通常被训练来识别文本中的结构和语义断点,能够自动适应各种语言和文本类型。

  • Naive BERT

    使用BERT模型的下一句预测(NSP)功能来判断两个句子之间是否存在直接的连续关系。这种方法通过分析相邻句子的语义关系来确定分块点。


  • Cross Segment Attention[6]

    采用跨片段的注意力机制来分析文本,如通过BERT和双向LSTM结合的方式,来更细致地理解和划分文本。这种方法不仅考虑单个句子,还考虑其周围的上下文,以确定分割点。


  • SeqModel[7]

    在[6]的基础上提出了进一步的改进,即 SeqModel。SeqModel 利用 BERT 同时编码多个句子,在进行句子向量计算之前,建模了更长上下文内的依赖关系。然后,它预测每个句子之后是否会发生文本分割。此外,这个模型采用了自适应性滑动窗口方法来提高推理速度,同时不牺牲准确性。


四、基于代理的切分方法

通过将文本分解为独立的命题或信息单元,并根据这些单元的语义相关性进行聚类和组织。通常依赖于先进的自然语言处理技术来提取和评估文本中的关键信息,然后基于这些信息创建结构化的数据块。其步骤如下:

  1. 理解文本:首先,大型语言模型(LLM)需要理解整个文本。就像一个人在阅读文章时,理解每一个段落、句子和词汇的意义一样。

  2. 生成命题: 然后,模型将文本分解为命题。命题是文本中的独立观点或信息片段,每个命题都包含一个完整的思想或陈述。例如,如果原文是一篇关于历史事件的文章,一个命题可能是“XX年发生了YY事件”。

  3. 命题评估:接下来,模型(代理)会评估每个命题的相关性和上下文。它会考虑这个命题与文本中其他内容的关联程度。比如模型可能会问:“这个命题是否与我已经划分出的其他信息块有逻辑上的联系?”

  4. 创建或分配块: 基于评估结果,模型决定将命题放入现有的信息块中,还是为其创建一个新的块。如果一个命题与现有块紧密相关,它就会被添加到那个块中。如果它是一个全新的观点或信息片段,就会创建一个新的块。

  5. 迭代和优化: 模型会不断评估和调整,直到文本被有效地分割成一系列有意义的、相互关联的信息块。


在使用(RAG)处理长文本数据时,合理的文本切割策略是提高模型性能和效率的关键。


文档切分实践-pdf文档的分割


今天我们基于特定文档类型的分割,使用pdfminer库(PDFMiner 是一个用于解析PDF文档的Python库。


它可以从PDF文件中提取文本和数据,包括文本内容、字体信息、页面布局、表格、图片以及文档元数据):

安装pdfminer库

pip install pdfminer.six

引用相关的包

from pdfminer.high_level import extract_pagesfrom pdfminer.layout import LTTextContainer

pdf解析方法

def extract_text_from_pdf(filename, page_numbers=None, min_line_length=1):    '''从 PDF 文件中(按指定页码)提取文字'''    paragraphs = []    buffer = ''    full_text = ''    # 提取全部文本    for i, page_layout in enumerate(extract_pages(filename)):        # 如果指定了页码范围,跳过范围外的页        if page_numbers is not None and i not in page_numbers:            continue        for element in page_layout:            if isinstance(element, LTTextContainer):                full_text += element.get_text() + '\n'    # 按空行分隔,将文本重新组织成段落    lines = full_text.split('\n')    for text in lines:        if len(text) >= min_line_length:            buffer += (' ' + text) if not text.endswith('-') else text.strip('-')        elif buffer:            paragraphs.append(buffer)            buffer = ''    if buffer:        paragraphs.append(buffer)    return paragraphs


调用与输出

paragraphs = extract_text_from_pdf("doc\demo.pdf", min_line_length=10)
for para in paragraphs[:50]:    print(para + "\n")


我解析了网上流传甚广的java八股文pdf


文本分割的粒度

文本切割策略主要依赖于两个参数:chunksize(块大小)和overlap(重叠)。正确配置这些参数可以显著影响模型的输出质量和处理速度。

#  按一定粒度,部分重叠式的切割文本,使上下文更完整def split_text(paragraphs, chunk_size=300, overlap_size=100):    '''按指定 chunk_size 和 overlap_size 交叠割文本'''    # 英文句子分割    # sentences = [s.strip() for p in paragraphs for s in sent_tokenize(p)]    # 中文句子分割    sentences = [s.strip() for p in paragraphs for s in re.split('(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)\s', p)]    chunks = []    i = 0    while i < len(sentences):        chunk = sentences[i]        overlap = ''        prev_len = 0        prev = i - 1        # 向前计算重叠部分        while prev >= 0 and len(sentences[prev])+len(overlap) <= overlap_size:            overlap = sentences[prev] + ' ' + overlap            prev -= 1        chunk = overlap+chunk        next = i + 1        # 向后计算当前chunk        while next < len(sentences) and len(sentences[next])+len(chunk) <= chunk_size:            chunk = chunk + ' ' + sentences[next]            next += 1        chunks.append(chunk)        i = next    return chunks
chunks = split_text(paragraphs, 300100)



我的需求



等我把市面主流的java面试资料都喂给了LLM,远程面试八股文这关,是不是可以放个麦克风在电脑边,面试官边讲,LLM边给我输出,反正chatgpt-o,文字、语音、图片等等多模态都可以,哈哈。

有了需求,下一篇我们就准备把解析的pdf资料灌入检索引擎。




长上下文是否替代了RAG?



长上下文窗口的好处

  1. 提高理解和连贯性:长上下文窗口允许模型在生成回答时,参考更多的背景信息,这使得模型能够更好地理解复杂的查询上下文,从而生成更加相关和连贯的回答

  2. 复杂任务处理能力:有些任务,如撰写文章、编程、数据分析等,需要对大量信息进行处理和引用。长上下文窗口使模型能够处理这类复杂任务提供更加深入和准确的输出

  3. 提升用户体验:用户在与模型交互时,往往希望模型能够记住并利用整个对话历史中的信息。长上下文窗口可以使模型在整个会话过程中保持信息的连续性,提供更加个性化和满意的用户体验。


RAG的不可替代性

虽然很多模型能处理更长的上下文窗口,但它们无法取代RAG,因为处理复杂RAG任务仍然需要更好的系统才能投入生产。


长上下文窗口允许RAG系统在检索精度较低的情况下仍然进行有意义的处理,从而促进了RAG生产的简化,提升了RAG应用的效能。



END



链接:https://juejin.cn/post/7373681115745517607

本文为51Testing经授权转载,转载文章所包含的文字来源于作者。如因内容或版权等问题,请联系51Testing进行删除




点点赞
点分享
点在看


51Testing软件测试网
博为峰20周年,青春正当燃,一起向未来! 博为峰51Testing软件测试网提供各种线上招聘、线上课程等网络服务,出版软件测试系列丛书及电子杂志,组织线上技术交流活动;同时还举办多种线下公益活动,如软件测试沙龙、软件测试专场招聘会等。
 最新文章