AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


使用RAG技术构建企业级文档问答系统:检索优化(4)BM25和混合检索
发布日期:2024-08-28 04:25:25 浏览次数: 1790 来源:超乎想象的科技圈


概述

大语言模型兴起之前的很长时间里,在信息检索领域,用的比较多的其实是TF-IDF、BM25这类检索方法,这些方法也经历住了时间的考验。在大模型时代,将BM25这类稀疏检索与向量检索相结合,通常能取长补短,大幅提升检索效果。

之所以将BM25称为稀疏检索算法,是因为文档和Query是使用稀疏向量表示的,向量维度通常等于所有文档中唯一词(词表)的数量,其中大部分值都是0,所以称为稀疏向量,与之相对的是稠密向量,也就是深度学习火了之后,Word Embedding以及向量模型所产出的向量,它的维度通常只有几百维,多的也只不过几千,其中几乎所有维度都有非0值,跟TF-IDF、BM25这种动辄上万维的向量相比算是维度很低了,这也是为什么在最开始Word Embedding概念出现时,被称为低维向量。

值得注意的是,在中文中,使用Langchain默认的BM25检索器参数,效果非常差,本人踩过的坑是,在一次项目中没有单独检查稀疏检索的效果,直接进行混合检索,通过调整两者配比最终效果比纯向量检索略好就结束了,以为语义检索效果比稀疏检索会有压倒性地优势,二者混合只略微涨点是合理的,但事后分析才发现,在中文中,使用默认参数设置,BM25检索效果不可能好。本文在核心代码的部分也会解释为什么会这样。

原理

这部分公式比较多,不喜欢的朋友可以跳过直接看代码,也不影响使用。

注意:下面说到的文档,对应到我们的场景,就是知识片段,语料库对应的是所有文档片段

BM25

BM25公式

BM25是一个给定query来计算文档相关性的重要的函数,BM指的是best matching。

BM25的评分函数可以用以下公式表示:


其中:

  • 是文档  对查询的评分

  • 是问题中的词数

  •  是问题中的第个词

  • 是词 的逆文档频率(Inverse Document Frequency),计算公式为:

其中:

  • 是文档总数

  •  是包含词的文档数量

  • 是词在文档中的词频(Term Frequency),不要被“频”这个词误解,这是个次数,不是占比

  • 是文档的长度(词数)

  • 是语料库中文档的平均长度(平均词数)

  •  和是参数,通常在1.2 到 2.0 之间,在 0.5 到 0.75 之间

前辈TF-IDF

这里顺带介绍一下TF-IDF的计算公式,因为BM25是对TF-IDF的改进:

其中 是问题Q中的第个词,跟BM25中的不同,它代表词在文档中的占比,例如在文档中共20个词,出现了30次,则为3/20。

的计算公式为:




表示文档总数,表示包含的文档的数量。

简单解释一下这个公式在干嘛,其实衡量的是一个词在某个文档中的重要性,聪明的读者应该能一眼看出来,如果只考虑,那分数高的,肯定是“的”、“了”这类没什么用的词,因此引入用来平衡这种高频词的影响,这样两者一综合,反而会使得“的”、“了”这类词的TF-IDF得分不高,而真正在一个文档中独有(说明这个词不是类似“的”这种每篇文档都在用的通用词)却高频(说明这篇文档重点讲这个词相关的话题)的那些词凸显出来。

两者对比

相比TF-IDF,BM25的改进主要在以下3点:

  • 词频(TF)调整

BM25考虑了词频的饱和效应,即随着词频的增加,增加的相关性会逐渐减少。这是通过使用一个非线性函数来调整词频的影响。主要体现在下面这部分:


当词频比较小时,主导这个公式值的,是除之外的部分,那可以认为,这部分公式,会随着词频的增长而增长,而当词频增大到一定程度时,主导这个公式值的,就变成词频了,综合分子分母可以发现,越到后面,分子上涨的部分,被分母抵消掉了。

为了便于大家理解,我做这样一个实验,假设= 2.0,= 0.75,为20,文档中一共5个词,出现了1词,之后每次向中增加一个与相同的词,则整个增长过程如下所示,其中TF就是,TF_adj是上面这个公式,大家可以观察diff,它表示随着TF的增长,TF_adj增量:

TFTF_adjdiff
11.600000
22.0338980.433898 
32.2360250.202127 
42.3529410.116916 
52.4291500.076209 
62.4827590.053609 
72.5225230.039764 
82.5531910.030669 
92.5775660.024374 
102.5974030.019837 

可以看到,随着TF的增大,diff越来越小,越到后面,越“涨不动”了,也就说明了词频是有饱和效应的

  • 文档长度标准化

TF-IDF未考虑文档长度的影响,而BM25通过引入文档长度标准化因子,长文档通常会包含更多的词,因此需要对词频进行标准化,以避免长文档得到不成比例的高分。这个标准化是通过参数控制的。主要体现在下面这部分:


因为在BM25中,词频表示的词在文档中出现的次数,那对于越长的文档,相对来说它里面的词的就会越大,因此引入文档长度标准化:

当文档比较长时,较大,从而使分母较大,降低词频的影响。

当文档较短时,较小,从而使分母较小,扩大词频的影响。

  • 引入额外的调节参数

BM25中引入的参数和提供了额外的调节手段,使得模型可以更好地适应不同的应用场景,而TF-IDF则没有这样的灵活性。

混合检索

混合检索的流程如下图,会分两路分别使用稠密向量和稀疏检索(BM25)方式检索知识片段,然后将检索结果使用使用RAG技术构建企业级文档问答系统:检索优化(3)RAG Fusion中介绍的RRF算法进行排序,截断Top N的知识片段送入大模型,然后由大模型结合用户问题和知识片组生成答案。

效果对比

从下表可以看出,使用BM25检索和混合检索(Embedding微调+BM25)的方法,检索的命中率超过了之前的所有方法,混合检索的全流程问答准确率也达到了目前最好成绩。

核心代码

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

BM25检索

使用Langchain实现非常简单,传入切分好的知识库列表splitted_docs即可构建好检索器

   BM25Retriever

vanilla_bm25 = BM25Retriever.from_documents(splitted_docs)

chunks = vanilla_bm25.get_relevant_documents(question)

但这种方式,效果会非常差,对于本系列构建的测试集,Top1~8的命中率如下:

retrievertop_khit_rate
vanilla_bm2510.000000 
vanilla_bm2520.000000 
vanilla_bm2530.032258 
vanilla_bm2540.064516 
vanilla_bm2550.064516 
vanilla_bm2560.064516 
vanilla_bm2570.064516 
vanilla_bm2580.064516 

为什么会出现这种情况?查看Langchain源代码,https://github.com/langchain-ai/langchain/blob/master/libs/community/langchain_community/retrievers/bm25.py会发现,在创建BM25检索器时,会使用preprocess_func处理文档列表,而preprocess_func参数如果不指定,默认使用的是default_preprocessing_func,也就是按空格切分,这对中文来说,肯定是没法正常工作的。

下面对这段代码进行修改,增加中文分词器:

   BM25Retriever
 

chz_cut_bm25_retriever = BM25Retriever.from_documents(splitted_docs, preprocess_func= text: (jieba.cut(text)))


k = 
chz_cut_bm25_retriever.k = k


chz_cut_bm25_retriever.get_relevant_documents(question)

从下面的Top1~8的召回命中率来看,结果正常多了

retrievertop_khit_rate
jieba_cut_bm2510.666667 
jieba_cut_bm2520.784946 
jieba_cut_bm2530.838710 
jieba_cut_bm2540.870968 
jieba_cut_bm2550.870968 
jieba_cut_bm2560.870968 
jieba_cut_bm2570.870968 
jieba_cut_bm2580.870968 

混合检索

可以使用如下方式创建混合检索器:

   BM25Retriever, EnsembleRetriever
 


chz_cut_bm25_retriever = BM25Retriever.from_documents(splitted_docs, preprocess_func= text: (jieba.cut(text)))


 (k, weights=[, ]):
    chz_cut_bm25_retriever.k = k
 EnsembleRetriever(
        retrievers=[vector_db.as_retriever(search_kwargs={: k}), chz_cut_bm25_retriever], weights=weights
    )


ensemble_retriever = get_ensemble_retriever()
ensemble_retriever.get_relevant_documents(question)

在这里有一个超参数:稀疏检索和向量检索的权重,针对这个系列的数据,本文对不同权重配比进行了实验,不同权重的检索性能对比如下图:


53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询