高级RAG(六): 句子-窗口检索

之前我们介绍了LlamaIndex的从小到大的检索 的检索方法,今天我们再来介绍llamaindex的另外一种高级检索方法: 句子-窗口检索(Sentence Window Retrieval),在开始介绍之前让我们先回顾一下基本的RAG检索的流程,如下图所示:

在执行基本RAG检索时我们会将文档按指定的块大小(chunk_size)进行切割,然后进行embedding的向量化处理后存入向量数据库中,在检索时我们会计算用户问题(question) 与文档块的相似度,并选取K个最相似的文档(context),并将其和用户问题一起发送给LLM, 并最终由LLM来生成最终的回复(response)。那么context的质量将直接影响到response的质量,然而context的质量往往取决于文档块的大小即chunk_size, 当chunk_size较小时它与question的匹配度越高,但此时context的信息量就会相对较少,这样也会导致最终的response质量变差,而当chunk_size较大时虽然context的信息量较大,但是context与question的匹配度就会降低,这也会导致最终的response质量变差,这就是基本RAG架构的弊端所在,不过之前我们已经介绍过的langchain的父文档检索器和LlamaIndex的从小到大的检索这两篇博客就是针对基本RAG架构的弊端的两种解决方法,接下来我们来介绍一种在LlamaIndex中更为强大的高级RAG方法:句子-窗口检索, 该方法的主要思想是首先将文档切割成更小的文档块, 当匹配到问题后,将该文档块周围的文档内容作为context输出,如下图所示:

一、环境配置

在介绍句子-窗口检索方法前我们首先需要对环境进行配置,我们需要安装如下python包:

pip install llama_hub 
pip install llama_index
pip install trulens-eval
pip install trafilatura
pip install torch sentence-transformers

 接下来我们需要做一些初始化的工作,比如导入openai或者gemini等大模型的api_key:

import os
from dotenv import load_dotenv, find_dotenv#导入.env配置文件
_ = load_dotenv(find_dotenv()) 

 下面我们需要导入在后续实验中所需要使用到的所有python包:

import os
from llama_index.readers.web import TrafilaturaWebReader
from llama_index import Document
from llama_index import VectorStoreIndex, StorageContext, load_index_from_storage
from llama_index import load_index_from_storage
from llama_index.readers.web import TrafilaturaWebReader
from llama_index.text_splitter import SentenceSplitter
from llama_index import VectorStoreIndex, ServiceContext
from llama_index.embeddings import resolve_embed_model
from llama_index.node_parser import SentenceWindowNodeParser
from llama_index.indices.postprocessor import MetadataReplacementPostProcessor
from llama_index.indices.postprocessor import SentenceTransformerRerank
from llama_index.llms import OpenAI
from llama_index.llms import Gemini

二、加载数据

这里我们仍然使用前几篇博客中使用的数据即从百度百科的网页中抓取一篇关于恐龙的文章:

url="https://baike.baidu.com/item/恐龙/139019"
docs = TrafilaturaWebReader().load_data([url])#将全角标点符号转换成半角标点符号+空格
for d in docs:d.text=d.text.replace('。','. ')d.text=d.text.replace('!','! ')d.text=d.text.replace('?','? ')#查看文档集
docs

这里我们采样LlamaIndex提供的网页爬虫工具TrafilaturaWebReader来爬取百度百科上的这篇文章,然后我们会将文章中全角标点符号如句号、感叹号、问号全部转换成半角标点符号+空格,至于为什么要将全角的标点符号替换成半角标点符号,我们后续会进行说明。

三、句子-窗口检索(Sentence Window Retrieval)

句子-窗口检索主要思想是将文档按句子来切割即每个句子成为一个文档,在检索时候将问题和所有的句子向量进行匹配,当匹配到较高相似度的句子后,将该句子周围(前,后)的若干条句子作为context,当前句子的前后句子数量由参数window_size来确定,如下图所示:

1.1 句子的识别

在之前介绍的基本RAG架构,还有langchain的父文档检索器,以及​LlamaIndex的从小到大的检索中我们都是按指定的块大小(chunk_size)来对文档进行切割的,然而“句子-窗口检索”方法中我们将不再按chunk_size来切割文档,而是按完整的句子来切割文档即每一个句子切割成一个文档,然而如何识别出文本中的句子呢?在LlamaIndx中采样的是通过句尾的标点符号如句号(.), 问号(?), 感叹号(!)等来识别句子,下面我们来创建一个句子解析器并尝试让它将按句子来切割文档:

from llama_index.node_parser import SentenceWindowNodeParser#定义句子解析器
node_parser = SentenceWindowNodeParser.from_defaults(window_size=3,window_metadata_key="window",original_text_metadata_key="original_text",
)node_parser

 这里我们定义了一个句子解析器node_parser,它包含了一个关键的参数window_size,该参数表示context的大小即当前句子及其周围包含多少条其他句子,比如当window_size=3时,那么context将由当前句子之前的3条句子,当前句子,当前句子之后的2条句子一共6条句子组成的窗口(window)数据来表示。而window_metadata_key和original_text_metadata_key为我们自定义的在元数据中表示窗口数据和当前句子的关键字(key),我们来看下面的例子:

from llama_index import Documenttext = "hello. how are you? I am fine! aaa;ee. bb,cc"nodes = node_parser.get_nodes_from_documents([Document(text=text)])
print([x.text for x in nodes])

这里我们看到英文字符串 "hello. how are you? I am fine! aaa;ee. bb,cc" 被拆分成了5个句子,这是因为SentenceWindowNodeParser是根据句尾的标点符号如句号(.), 问候(?),感叹号(?)来识别和切割句子的,下面我们来看看节点中的"窗口"数据:

nodes[0].metadata

 这里我们需要说明一下的是当文档被切割以后,窗口数据和文档数据都会被存储在节点的元数据中并以自定义的window_metadata_key和original_text_metadata_key来表示。这里由于我们查看的是节点的第一个文档的元数据,那么第一个文档也就是原始文档的第一个句子,因此窗口数据中只包含了当前句子和后续两条句子共3个句子,下面我们查看最后一个文档的元数据:

nodes[4].metadata

这里我们查看了节点的最后一共文档,因为是最后一共文档因此它的窗口数据中只包含了当前句子的前三条句子和当前句子一共4个句子。

1.2 如何识别中文文档中的句子

经过我的测试,我发现LlamaIndex中的SentenceWindowNodeParser似乎只能识别半角的标点符号,而在中文文档中几乎都是使用全角的标点符号,而SentenceWindowNodeParser却无法识别全角的标点符号如全角的句号(。),全角的问号(?),全角的感叹号(!) 这将会导致SentenceWindowNodeParser无法切割中文的文档,为了解决这个问题,经过我的一番研究,我发现如果将中文文档中的全角句号、问号、感叹号全部替换成对应的半角标点符号并且在半角标点符号后面再多加一共空格,这样就可以让SentenceWindowNodeParser来切割中文文档中的句子了。下面我们来测试一下让SentenceWindowNodeParser切割带有全角的标点符号的中文文档会怎么样:

#带有全角标点符号的中文文本
text = "你好,很高兴认识你。 已经10点了,可我还不想起床!下雪啦!你的作业完成了吗?"nodes = node_parser.get_nodes_from_documents([Document(text=text)])
print([x.text for x in nodes])

 

这里我们看到这个中文的字符串没有被切割,它仍然作为一个整体被输出, 下面我们将文本中的句号,问号,感叹号全部替换成半角的标点符号并且再多加一共空格:

#带有半角标点符号的中文文本
text = "你好,很高兴认识你. 已经10点了,可我还不想起床! 下雪啦! 你的作业完成了吗?"nodes = node_parser.get_nodes_from_documents([Document(text=text)])
print([x.text for x in nodes])

这里我们看到文本中的全角的句号,问号,感叹号被替换成半角以后整个文本就被切割成了4个文档。 下面我们来看看节点中的"窗口"数据:

nodes[0].metadata

这里我们看到节点的第一个文档也就是文档中的第一个句子,在第一个文档的窗口(window)数据中包含了第一个句子以及后续的两个句子。而在“original_text”中存储着第一个文档即原始文档的第一个句子。接下来我们测试一下对之前我们获取的百度百科的文章进行切割:

sentence_nodes = node_parser.get_nodes_from_documents(docs)len(sentence_nodes)

这里我们看到原始文档被切割成了334个文档,下面我们再来查看一下其中某个文档的内容:

sentence_nodes[100].metadata

这里我们用黄色标记出了当前文档的句子,我们看到窗口数据(window)中一共包含了6个句子即当前句子之前的3个句子,以及当前句子和之后的两个句子合计6个句子。

1.3 创建向量数据库(index)

接下来我们开始创建句子-创建检索任务所需要组件如LLM、ServiceContext等,因为我们需要检索的是中文文档,因此我们选择的embedding模型是开源的bge-small-zh-v1.5模型,llm选择的是openai的gpt-3.5-turbo模型,当然你也可以选择gemini模型:

#创建OpenAI的llm
llm = OpenAI(model="gpt-3.5-turbo",api_key='your-opai-api-key',temperature=0.1)
#创建谷歌gemini的llm
# llm = Gemini()#创建ServiceContext组件
sentence_context = ServiceContext.from_defaults(llm=llm, embed_model="local:BAAI/bge-small-zh-v1.5",node_parser=node_parser,)#创建向量数据库
document = Document(text="\n\n".join([doc.text for doc in docs]))
sentence_index = VectorStoreIndex.from_documents([document], service_context=sentence_context
)

这里我们创建了向量数据库sentence_index ,我们可以将这个向量数据库持久化保存在本地,在需要的时候我们可以直接从本地读取向量数据库,从而可以省去重新获取数据和创建llm和ServiceContext等组件的步骤了:

#将向量数据库保存在本地
sentence_index.storage_context.persist(persist_dir="./sentence_index")#从本地读取向量数据库
if not os.path.exists("./sentence_index"):sentence_index = VectorStoreIndex.from_documents([document], service_context=sentence_context)sentence_index.storage_context.persist(persist_dir="./sentence_index")
else:sentence_index = load_index_from_storage(StorageContext.from_defaults(persist_dir="./sentence_index"),service_context=sentence_context)

1.4 创建postprocessor组件

要实现最终的检索我们还需要创建query engine组件,但是在query engine组件中需要设置一个postprocessor组件作为其参数,而postprocessor组件可以由若干个子组件组合在一起,下面我们首先来简单介绍一下postprocessor子组件:Replacement组件,该组件的作用是用来选择(由target_metadata_key参数确定)将哪些context发送给llm, 也就是说Replacement组件会从检索到的context中挑选指定的内容发送给llm,所以它具有选择context的功能.

另外postprocessor还有一个叫rerank的子组件,它的作用是对检索到的上下文进行从新排序,从而得到一个精度更高的检索结果,最后Replacement组件会将rerank组件的排序结果发送给llm, 不过这里需要说明一下的是rerank是可选组件,它不是必须的,rerank组件的作用仅仅是为了提高检索的精度。

#创建Replacement组件
postproc = MetadataReplacementPostProcessor(target_metadata_key="window"
)#创建rerank组件
# 参考: https://huggingface.co/BAAI/bge-reranker-base
rerank = SentenceTransformerRerank(top_n=2, model="BAAI/bge-reranker-base"
)

这里创建的Replacement组件中我们设置了target_metadata_key参数为"window", 它的作用是当执行检索操作时会将context中的元数据的“窗口”数据发送给llm。而rerank组件中的top_n=2的作用是对检索到的多个context进行重新排序并选取精度最高前2个context。这里所谓的精度是指相似度计算的精度,因为我们选择embedding模型和rerank模型的都是bge的模型 ,因此它们配合再一起计算出来的相似度精度要比用传统的用向量内积(如np.dot())方式计算出来的相似度要高一些,所以可以认为经过rerank模型的重新排序后会得到和question相关度更高的context。

1.5 创建query engine组件

接下来我们通过将上面创建的组件结合在一起来创建query engine组件:

#创建查询引擎
sentence_window_engine = sentence_index.as_query_engine(similarity_top_k=6, node_postprocessors=[postproc, rerank]
)

这里我们设置了similarity_top_k=6这表示说每次检索将返回相似度最高的6个文档, 而我们的rerank组件会对这6个文档进行重新排序后选取2个相似度最高的文档,最后Replacement组件会将这2个相似度最高的文档中的“窗口”数据发送给llm。下面我们来测试一下这个query engine:

window_response = sentence_window_engine.query("恐龙是冷血动物吗?"
)
print(window_response)

下面我们来看一下针对 "恐龙是冷血动物吗?"这个问题所检索出来的窗口数据及其句子:

window = window_response.source_nodes[0].node.metadata["window"]
sentence = window_response.source_nodes[0].node.metadata["original_text"]print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

这里我们用黄色标记出检索到的句子,从检索结果中我们看到了窗口数据和检索到的句子。因为经过rerank模型重新排序后最后只剩下两个相似度最高的context,下面我们再看一下第二个context中的内容:

window = window_response.source_nodes[1].node.metadata["window"]
sentence = window_response.source_nodes[1].node.metadata["original_text"]print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

 这里我们观察到在第二个检索到的窗口数据中包含了“简言之,它们的生理机能在现代社会并不常见.” 这句话,但是这句话没有出现在第一个文档中,最后这句话也出现在了llm的返回结果中,这说明LLM对这两个context的窗口数据进行了总结和归纳,它从这两个窗口数据中分别提取了和question最相关的内容,然后再将它们组织在一起形成最终的response。下面我们再测试一个问题:"恐龙灭绝原因是什么?"

window_response = sentence_window_engine.query("恐龙灭绝原因是什么?"
)
print(window_response)

 

下面我们来看一下针对 "恐龙灭绝原因是什么?"这个问题所检索出来的第一个文档的窗口数据及其句子:

window = window_response.source_nodes[0].node.metadata["window"]
sentence = window_response.source_nodes[0].node.metadata["original_text"]print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

下面我们查看一下检索出来的第二个文档的窗口数据及其句子:

window = window_response.source_nodes[1].node.metadata["window"]
sentence = window_response.source_nodes[1].node.metadata["original_text"]print("------------------")
print(f"Window: {window}")
print("------------------")
print(f"Original Sentence: {sentence}")

这里很明显我们可以看到LLM给出的response是总结了两个context的窗口数据内容后得到的。 

四、评估

未完待续....

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/618314.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Fedora 36 正式发布稳定的Linux桌面版本

Fedora 36今天发布,这是最近一段时间以来又一个强大、前沿而又稳定可靠的Linux发行版本,除了这些特点外,Fedora 36还在原先的基础上增加了新的功能和细节打磨。 Fedora 36使用GNOME 42作为其默认的Fedora工作站桌面环境。 OpenSSL 3.0&#x…

用React给XXL-JOB开发一个新皮肤(三):实现登录页和Layout骨架

目录 一. 简述二. 接口服务调整 2.1. 登录接口2.2. 登出接口2.3. 修改密码接口2.4. 修改配置文件 三. 前端HTTP 请求四. 登录页面 4.1. 搭建登录页面4.2. 对接登录接口 五. Layout 骨架 5.1. 搭建骨架5.2. Header5.3. 修改密码5.4. 退出登录 六. 其他 一. 简述 上一篇文章我…

视频监控平台的管理员账号在所有客户端都无法登录的问题解决

目 录 一、问题描述 二、问题排查 1、看问题提示 2、看日志信息 3、问题定位 三、问题解决 1. 添加权限角色 2、添加操作用户 3、验证 一、问题描述 AS-V1000视频监控平台安装部署完成后,发现管理员admin不能到web客户端,觉…

D25XB100-ASEMI家用电器整流桥D25XB100

编辑:ll D25XB100-ASEMI家用电器整流桥D25XB100 型号:D25XB100 品牌:ASEMI 封装:GBJ-5(带康铜丝) 平均正向整流电流(Id):25A 最大反向击穿电压(VRM&…

AcWing 103. 电影(map、pair连用or离散化)

题目 方法一(mappair) 其实上面这么长巴拉巴拉就是在说 首先,每个科学家会的语言都不同。但是呢每部电影的字幕和语言是不一样的(字幕和语言一定不相同) 要求找到一部电影使得在场能听懂的科学家最多(如果存…

Linux Kernel Stack Overflow/Linux 内核栈溢出

不同于Linux应用程序的栈能够动态增长,Linux内核栈是固定的,并且比较小,比如Linux 2.6.x内核,在X86 32位架构上一般是4K或8K(在进行内核编译时,Kernel hacking下进行配置,默认8K)&am…

上门回收小程序开发,让回收更加简单

资源回收一直是当下深受大众关注的话题,如何做到资源不浪费,成为了大众要考虑的问题。在人们环保意识的加深下,回收行业也是获得了大众的关注,逐渐形成了一个新的商业模式。 随着互联网技术的发展,回收行业也更加方便…

C/S架构,集成三维影像后处理功能,自主版权的一套医院PACS系统源码

一、PACS简介 PACS(PictureArchivingandCommunicationsSystem)即图像存储与传输系统,是应用于医院的数字医疗设备如CT、MR(磁共振)、US(超声成像)、X光机、DSA(数字减影&#xff09…

KIBANA可视化管理界面说明

更说明转自https://blog.csdn.net/IT_ZRS/article/details/125496588 1 主要结构功能 使用浏览器访问 ip:5601 默认端口,进入首页 Discover:日志管理视图 主要进行搜索和查询Visualize:统计视图 构建可视化的图表Dashboard&#xf…

【目标检测】YOLOv5算法实现(八):模型验证

本系列文章记录本人硕士阶段YOLO系列目标检测算法自学及其代码实现的过程。其中算法具体实现借鉴于ultralytics YOLO源码Github,删减了源码中部分内容,满足个人科研需求。   本系列文章主要以YOLOv5为例完成算法的实现,后续修改、增加相关模…

JavaWeb,CSS的学习

CSS,层叠样式表(Cascading Style Sheets),能够对网页中元素位置的排版进行像素级精确控制,支持几乎所有的字体字号样式,拥有网页对象和模型样式编辑的能力,简单来说,美化页面。 CSS…

c++临时对象的探讨及相关性能提升

产生临时对象的情况 我们定义一个类进行测试 class tempVal { public:int v1, v2;tempVal(int v1 0, int v2 0);tempVal(const tempVal& t) :v1(t.v1), v2(t.v2) {cout << "调用拷贝构造函数" << endl;}virtual ~tempVal() {cout << "…

【python】——turtle动态画

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

AR HUD全面「上新」

AR HUD赛道正在迎来新的时代。 上周&#xff0c;蔚来ET9正式发布亮相&#xff0c;新车定位为D级行政旗舰轿车&#xff0c;其中&#xff0c;在智能座舱交互层面&#xff0c;继理想L系列、长安深蓝S7之后&#xff0c;也首次取消仪表盘&#xff0c;取而代之的是业内首个全焦段AR H…

分块矩阵的定义、计算

目录 一、定义 二、分块矩阵的加减乘法 三、考点 一、定义 分块&#xff0c;顾名思义&#xff0c;将整个矩阵分成几部分&#xff0c;如下图所示 二、分块矩阵的加减乘法 三、考点 分块矩阵的考点不多&#xff0c;一般来说&#xff0c;有一种&#xff1a; 求分块矩阵的转置…

PHP如何拆分中文名字(包括少数民族名字)

/*** param string|null $name* return array|null*/ function splitName($name) {if (empty($name) || empty(trim($name))) {return null;}//该正则是用来提取$name参数里面的中文字符的。preg_match_all(/[\x{4e00}-\x{9fff}]/u, $name, $matchers);$matchersCount isset($…

2024年,谷歌云首席技术官眼中的生成AI三大支柱,来看看有啥新花样

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

App在线封装的革命性创新

随着移动互联网的蓬勃发展&#xff0c;App已经成为我们日常生活中不可或缺的一部分。从购物、交通、社交到娱乐&#xff0c;几乎每个人的智能手机都装载着数十个应用程序&#xff0c;以满足各式各样的需求。然而&#xff0c;对于许多非技术背景的企业家和小型企业而言&#xff…

【机器学习】模型参数优化工具:Optuna使用分步指南(附XGB/LGBM调优代码)

常用的调参方式和工具包 常用的调参方式包括网格搜索(Grid Search)、**随机搜索(Random Search)和贝叶斯优化(Bayesian Optimization)**等。 工具包方面&#xff0c;Scikit-learn提供了GridSearchCV和RandomizedSearchCV等用于网格搜索和随机搜索的工具。另外&#xff0c;有一…

VS报错:error:LNK2005 _main 已经在 *.obj 中定义

应该是重定义了&#xff0c;但是又解决不了&#xff0c;看似又没有重定义啊&#xff0c;就在一个文件定义了啊&#xff1f;怎么会出现这种情况呢&#xff1f;关键是&#xff0c;编译报错&#xff0c;程序运行不了了。 这里提一下我的前期操作&#xff0c;是因为将一个头文件和…