RAG技术终极指南:企业级文档问答系统构建秘籍,Meta Chunking切分详解,收藏这一篇就够!

1 概述

由中国人民大学和上海算法创新研究院在2024年10月份提出了一种新的文本切分方法,以提升切分效果,从而改善RAG的效果。

论文中提出了Meta Chunking的概念,它指的是句子和段落之间的颗粒度,我的理解是一种文本块。

文章基于Meta Chunking的概念,一种提出了两种方法:

  • Perplexity Chunking
  • Margin Sampling Chunking

下图左侧是Margin Sampling Chunking,右图是Perplexity Chunking。

只使用a方法无法维持句子完整性和逻辑完整性。使用a+b无法维持逻辑完整性。

感兴趣的朋友可以自行阅读论文,下面我将用尽可能通俗易懂的语言介绍这两种方法的核心原理:

  • Perplexity Chunking:这种方法会对每个句子计算一个困惑度(Perplexity),不熟悉这个概念的朋友,可以把它理解成,在一个句子中,平均而言每次拿上一个词来预测下一个词的难度,然后依次滚动预测,比如拿“中国”预测“的”,拿“中国的”预测“首都”……当前已经滚动到“中国的首都是”,需要预测“北京”,那它的难度就很小,把这样滚动预测的难度求平均,就是困惑度。这种方法的核心思想是,寻找那些能显著降低困惑度的句子(也就是论文或者代码中要找的minimal),那这些句子后面就应该是切分点(具体其实有两种情况,大家可以看论文后者看本文第3部分的代码注释)。这个其实也比较直观易懂,因为同一个语义的片段,随着片段的不断增加,预测难度是会不断降低的。就好比有一个段落讲解大语言模型的技术原理,在比较靠前的地方会把基本的术语之类的都介绍完,到后面只是具体的展开,LLM在相对熟悉的上下文环境中,相对容易预测,Perplexity就会随着句子的增加而呈降低的趋势,但到了下一段开始介绍大语言模型在行业的应用时,完全切换到另一个话题了,LLM也无法提前感知会切换成什么话题,因此预测难度大增,加入下一个话题的第一个句子后,Perplexity就会上升,由此就能找到最佳的切分点。这种方法,是论文主推的方法,号称是在性能和效果之间找到了很好的平衡。
  • Margin Sampling Chunking:这种方法比较直接粗暴,核心思想是,维护一个buffer,里面有一系列句子,每次拿这个buffer和下一个句子合并起来,问LLM合并的句子是不是要切分,只有在不切分的概率减去要切分的概率大于特定阈值时,才会进行切分,然后把buffer清除,采用滑动窗口的方式考虑后面的句子。换言之就是让LLM来看到底在什么地方切分,只不过不是一股脑把整篇文档送入LLM,而是用比较巧妙的方法采用滑动窗口的方式一点点送给LLM,这样一是不会让LLM上下文超长,二是还可以根据设定长度和切分情况来决定是不是需要合并,以组成符合切分长度要求的片段。值得注意的是,arxiv上的论文有2个版本,这种方法在最新的第2版中介绍非常少,摘要中没有出现,在核心方法章节也没有提到,甚至在实验设置部分都被分到Baseline组,实验结果也是绝大多数测试集都是Perplexity Chunking更优,而在第1版中,在摘要、核心方法章节中都会介绍Margin Sampling Chuning,实验部分也是和Perplexity Chunking互有胜负。论文给的示例代码中使用的LLM是Qwen2-1.5B-Instruct,我们也同样使用这个模型,这种方法在我们的系列文章实验中,是目前切分效果最好的,但由于我的显卡显存(只有11G)不足,只能用CPU(Intel i7 9700K),单次运行大约2.5小时。

2 效果

本文对PyPDFLoader解析成文本后直接拼接、MinerU转换为Markdown两种方式,对Meta Chunking中提到的PPL Chunking和Margin Sampling Chunking两种方法都进行了对比实现,结果如下表:

值得一提的是,从结果看,Margin Sampling Chuning这种方式的优势非常明显,无论是PDF解析后简单拼接,还是转成Markdown后,反倒是论文所提的主要方法Perplexity Chunking效果一般。其中转换为Markdown后的文档,使用这种Margin Sampling Chuning解析的文档,配合未经微调的bge-large-zh-v1.5,甚至超过了对使用检索优化(4)BM25和混合检索中经过微调的Embedding和BM25的混合检索。

3 核心算法

本文对应代码已开源,地址在:https://github.com/Steven-Luo/MasteringRAG/blob/main/split/04_meta_chunking.ipynb

核心算法都来自Meta Chunking,具体代码可以阅读https://github.com/IAAR-Shanghai/Meta-Chunking/tree/main/example

其中Margin Sampling Chunking方法的核心在app.py中,下面把一些不太好理解的部分加以注释:

def
 
meta_chunking
(original_text,base_model,language,ppl_threshold,chunk_length):

chunk_length=
int
(chunk_length)

if
 base_model==
'PPL Chunking'
:

        final_chunks=extract_by_html2text_db_nolist(original_text,small_model,small_tokenizer,ppl_threshold,language=language)

else
:

      full_segments = split_text_by_punctuation(original_text,language)

# tmp就是在概述部分说到的那个buffer

      tmp=
''

# 这个是阈值,在论文中,这个阈值初始为0,后续会根据预测概率的margin(就是切分与不切分的差)的历史均值来求平均

      threshold=
0

      threshold_list=[]

      final_chunks=[]

for
 sentence 
in
 full_segments:

if
 tmp==
''
:

              tmp+=sentence

else
:

# 获取切分与不切分的概率差

              prob_subtract=get_prob_subtract(small_model,small_tokenizer,tmp,sentence,language)    

              threshold_list.append(prob_subtract)

if
 prob_subtract>threshold:

                  tmp+=
' '
+sentence

else
:

                  final_chunks.append(tmp)

                  tmp=sentence

if
 
len
(threshold_list)>=
5
:

              last_ten = threshold_list[-
5
:]  

              avg = 
sum
(last_ten) / 
len
(last_ten)

              threshold=avg

if
 tmp!=
''
:

          final_chunks.append(tmp)

      ...

def
 
get_prob_subtract
(model,tokenizer,sentence1,sentence2,language):

if
 language==
'zh'
:

        query=
'''这是一个文本分块任务.你是一位文本分析专家,请根据提供的句子的逻辑结构和语义内容,从下面两种方案中选择一种分块方式:

        1. 将“
{}
”分割成“
{}
”与“
{}
”两部分;

        2. 将“
{}
”不进行分割,保持原形式;

        请回答1或2。'''
.format(sentence1+sentence2,sentence1,sentence2,sentence1+sentence2)

        prompt=
"<|im_start|>system
\n
You are a helpful assistant.<|im_end|>
\n
<|im_start|>user
\n
{}
<|im_end|>
\n
<|im_start|>assistant
\n
"
.format(query)

        prompt_ids = tokenizer.encode(prompt, return_tensors=
'pt'
).to(model.device)

        input_ids=prompt_ids

# 因为Prompt中要求LLM只返回1或2,因此此处只获取这两种输出的tokenized后的结果

        output_ids = tokenizer.encode([
'1'
,
'2'
], return_tensors=
'pt'
).to(model.device)

with
 torch.no_grad():

            outputs = model(input_ids)

            next_token_logits = outputs.logits[:, -
1
, :]

            token_probs = F.softmax(next_token_logits, dim=-
1
)

        next_token_id_0 = output_ids[:, 
0
].unsqueeze(
0
)

# 分别获取到输出1、2的概率

        next_token_prob_0 = token_probs[:, next_token_id_0].item() 
# 1的概略

        next_token_id_1 = output_ids[:, 
1
].unsqueeze(
0
)

        next_token_prob_1 = token_probs[:, next_token_id_1].item() 
# 2的概率

        prob_subtract=next_token_prob_1-next_token_prob_0 
# 不分割的概率 - 分割的概率

else
:

        ...

return
 prob_subtract

Perplexity Chunking方法,在chunk_rag.py

def
 
extract_by_html2text_db_nolist
(sub_text,model,tokenizer,threshold,language=
'zh'
) -> List[
str
]:  

    ...

# segments是之前按照一些规则切分的句子组成的列表

for
 context 
in
 segments:

        tokenized_text = tokenizer(context, return_tensors=
"pt"
, add_special_tokens=
False
)

        input_id = tokenized_text[
"input_ids"
].to(model.device)

        input_ids = torch.cat([input_ids, input_id],dim=-
1
)

        len_sentences.append(input_id.shape[
1
])

        attention_mask_tmp = tokenized_text[
"attention_mask"
].to(model.device)

        attention_mask = torch.cat([attention_mask, attention_mask_tmp],dim=-
1
)

# 上面虽然对每个句子做了拼接,但最后还是相当于计算了整篇文档的loss

    loss, past_key_values = ch.get_ppl_batch( 

        input_ids,

        attention_mask,

        past_key_values=
None
,

        return_kv=
True

    )

    first_cluster_ppl=[]

    index=
0

# 分别获取从文档开篇,一直到当前句子做上下文,当前句子的平均loss,也就是每个句子的平均loss,只不过不是单独算的,而是要把之前的当做上下文

for
 i 
in
 
range
(
len
(len_sentences)):

if
 i ==
0
:

            first_cluster_ppl.append(loss[
0
:len_sentences[i]-
1
].mean().item())

            index+=len_sentences[i]-
1

else
:

            first_cluster_ppl.append(loss[index:index+len_sentences[i]].mean().item())

# print(loss[index:index+len_sentences[i]])

            index+=len_sentences[i]

# 找到PPL最小的切分点

    minima_indices=find_minima(first_cluster_ppl,threshold)

    first_chunk_indices=[]

    first_chunk_sentences=[]

# split_points注意,这个里面存储的是句子的索引

    split_points = [
0
] + minima_indices + [
len
(first_cluster_ppl)-
1
]    

for
 i 
in
 
range
(
len
(split_points)-
1
):

        tmp_index=[]

        tmp_sentence=[]

if
 i==
0
:

            tmp_index.append(
0
)

            tmp_sentence.append(segments[
0
])

for
 sp_index 
in
 
range
(split_points[i]+
1
,split_points[i+
1
]+
1
):

            tmp_index.append(sp_index)

# 把相邻切分点中的所有句子,都放到tmp_sentence中

            tmp_sentence.append(segments[sp_index])

        first_chunk_indices.append(tmp_index)

# 把相邻两个索引之间的句子放到句子列表中,first_chunk_sentences是一个二维数组

        first_chunk_sentences.append(tmp_sentence)

    final_chunks=[]

for
 sent_list 
in
 first_chunk_sentences:

        final_chunks.append(
''
.join(sent_list))

return
 final_chunks

def
 
find_minima
(values,threshold):  

    minima_indices = []  

for
 i 
in
 
range
(
1
, 
len
(values) - 
1
):  

# 当前句子的PPL值,比前、后的都小

if
 values[i] < values[i - 
1
] 
and
 values[i] < values[i + 
1
]:

# 比前、后多出来的部分都超过阈值

if
 (values[i - 
1
]-values[i]>=threshold) 
or
 (values[i + 
1
]-values[i]>=threshold):

                minima_indices.append(i)  

# 当前句子的PPL值,比之前的小,并跟之前的一样大

elif
 values[i] < values[i - 
1
] 
and
 values[i] == values[i + 
1
]:

# 之前部分的PPL减当前句子的PPL超过阈值

if
 values[i - 
1
]-values[i]>=threshold:

                minima_indices.append(i) 

return
 minima_indices
往期文章

Agent系列文章

Langchain中使用Ollama提供的Qwen大模型进行Function Call实现天气查询、网络搜索

Langchain中使用千问官方API进行Function Call实现天气查询、网络搜索

使用Ollama提供的Llama3 8B搭建自己的斯坦福多智能体AI小镇

使用Ollama提供的Qwen2 7B搭建自己的中文版斯坦福多智能体AI小镇

RAG系列文章

数据准备
使用RAG技术构建企业级文档问答系统之QA抽取

Baseline
使用RAG技术构建企业级文档问答系统之基础流程

评估

使用TruLens进行评估

使用GPT4进行评估
解析优化

解析(1)使用MinerU将PDF转换为Markdown

切分优化
切分(1)Markdown文档切分

切分(2)使用Embedding进行语义切分

切分(3)使用Jina API进行语义切分

检索优化

检索优化(1)Embedding微调

检索优化(2)Multi Query

检索优化(3)RAG Fusion

检索优化(4)BM25和混合检索

检索优化(5) 常用Rerank对比

检索优化(6) Rerank模型微调

检索优化(7)HyDE

如何学习大模型 AI ?

由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。

但是具体到个人,只能说是:

“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

在这里插入图片描述

第一阶段(10天):初阶应用

该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。

  • 大模型 AI 能干什么?
  • 大模型是怎样获得「智能」的?
  • 用好 AI 的核心心法
  • 大模型应用业务架构
  • 大模型应用技术架构
  • 代码示例:向 GPT-3.5 灌入新知识
  • 提示工程的意义和核心思想
  • Prompt 典型构成
  • 指令调优方法论
  • 思维链和思维树
  • Prompt 攻击和防范

第二阶段(30天):高阶应用

该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。

  • 为什么要做 RAG
  • 搭建一个简单的 ChatPDF
  • 检索的基础概念
  • 什么是向量表示(Embeddings)
  • 向量数据库与向量检索
  • 基于向量检索的 RAG
  • 搭建 RAG 系统的扩展知识
  • 混合检索与 RAG-Fusion 简介
  • 向量模型本地部署

第三阶段(30天):模型训练

恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。

到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?

  • 为什么要做 RAG
  • 什么是模型
  • 什么是模型训练
  • 求解器 & 损失函数简介
  • 小实验2:手写一个简单的神经网络并训练它
  • 什么是训练/预训练/微调/轻量化微调
  • Transformer结构简介
  • 轻量化微调
  • 实验数据集的构建

第四阶段(20天):商业闭环

对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。

  • 硬件选型
  • 带你了解全球大模型
  • 使用国产大模型服务
  • 搭建 OpenAI 代理
  • 热身:基于阿里云 PAI 部署 Stable Diffusion
  • 在本地计算机运行大模型
  • 大模型的私有化部署
  • 基于 vLLM 部署大模型
  • 案例:如何优雅地在阿里云私有部署开源大模型
  • 部署一套开源 LLM 项目
  • 内容安全
  • 互联网信息服务算法备案

学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。

如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

这份完整版的大模型 AI 学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值