RAG系统进阶(五)文本分割优化技巧及代码

news2024/11/28 13:32:21

背景

        前边在介绍RAG系统时提到了文本分割(或分段)的作用和重要性。也提到了分段后所带来的一些问题,比如由于分段导致检索出来的TOP-n的结果可能未包含完整的答案。

  1. 粒度太大可能导致检索不精准,粒度太小可能导致信息不全面
  2. 问题的答案可能跨越两个片段

一、改进方案 

1.1按一定粒度,部分重叠式的切割文本,使上下文更完整

英文文档的重叠式文本实现。

from nltk.tokenize import sent_tokenize


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)]
    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

中文sent_tokenize实现:

import re
import jieba
import nltk
from nltk.corpus import stopwords

# nltk.download('stopwords')

def to_keywords(input_string):
    """将句子转成检索关键词序列"""
    # 按搜索引擎模式分词
    word_tokens = jieba.cut_for_search(input_string)
    # 加载停用词表
    stop_words = set(stopwords.words('chinese'))
    # 去除停用词
    filtered_sentence = [w for w in word_tokens if not w in stop_words]
    return ' '.join(filtered_sentence)

def sent_tokenize(input_string):
    """按标点断句"""
    # 按标点切分
    sentences = re.split(r'(?<=[。!?;?!])', input_string)
    # 去掉空字符串
    return [sentence for sentence in sentences if sentence.strip()]


if "__main__" == __name__:
    # 测试关键词提取
    print(to_keywords("小明硕士毕业于中国科学院计算所,后在日本京都大学深造"))
    # 测试断句
    print(sent_tokenize("这是,第一句。这是第二句吗?是的!啊"))

测试示例:

chunks = split_text(paragraphs, 300, 100)


# 创建一个向量数据库对象
vector_db = MyVectorDBConnector("demo_text_split", get_embeddings)
# 向向量数据库中添加文档
vector_db.add_documents(chunks)
# 创建一个RAG机器人
bot = RAG_Bot(
    vector_db,
    llm_api=get_completion
)


# user_query = "llama 2有商用许可协议吗"
user_query="llama 2 chat有多少参数"

search_results = vector_db.search(user_query, 2)
for doc in search_results['documents'][0]:
    print(doc+"\n")

response = bot.chat(user_query)
print("====回复====")
print(response)
1.2 检索后排序

解决问题:有时最合适的答案不一定排在检索的最前面,截断后会导致返回结果不完全准确。

方案:

  1. 检索时招回一部分文本
  2. 通过一个排序模型对 query 和 document 重新打分排序

安装模型 

pip install sentence_transformers

使用模型 

from sentence_transformers import CrossEncoder

# model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2', max_length=512) # 英文,模型较小
model = CrossEncoder('BAAI/bge-reranker-large', max_length=512) # 多语言,国产,模型较大

测试打分 

user_query = "how safe is llama 2"
# user_query = "llama 2安全性如何"
scores = model.predict([(user_query, doc)
                       for doc in search_results['documents'][0]])
# 按得分排序
sorted_list = sorted(
    zip(scores, search_results['documents'][0]), key=lambda x: x[0], reverse=True)
for score, doc in sorted_list:
    print(f"{score}\t{doc}\n")
1.3 混合检索(Hybrid Search)

在实际生产中,传统的关键字检索(稀疏表示)与向量检索(稠密表示)各有优劣。

举个具体例子,比如文档中包含很长的专有名词,关键字检索往往更精准而向量检索容易引入概念混淆。

# 背景说明:在医学中“小细胞肺癌”和“非小细胞肺癌”是两种不同的癌症

query = "非小细胞肺癌的患者"

documents = [
    "玛丽患有肺癌,癌细胞已转移",
    "刘某肺癌I期",
    "张某经诊断为非小细胞肺癌III期",
    "小细胞肺癌是肺癌的一种"
]

query_vec = get_embeddings([query])[0]
doc_vecs = get_embeddings(documents)

print("Cosine distance:")
for vec in doc_vecs:
    print(cos_sim(query_vec, vec))

所以,有时候我们需要结合不同的检索算法,来达到比单一检索算法更优的效果。这就是混合检索

混合检索的核心是,综合文档 𝑑 在不同检索算法下的排序名次(rank),为其生成最终排序。

一个最常用的算法叫 Reciprocal Rank Fusion(RRF)

𝑟𝑟𝑓(𝑑)=∑𝑎∈𝐴1𝑘+𝑟𝑎𝑛𝑘𝑎(𝑑)

其中 𝐴 表示所有使用的检索算法的集合,𝑟𝑎𝑛𝑘𝑎(𝑑) 表示使用算法 𝑎 检索时,文档 𝑑 的排序,𝑘 是个常数。

很多向量数据库都支持混合检索,比如 Weaviate、Pinecone 等。也可以根据上述原理自己实现。

手写混合检索示例:
  1. 基于关键字检索的排序
import time


class MyEsConnector:
    def __init__(self, es_client, index_name, keyword_fn):
        self.es_client = es_client
        self.index_name = index_name
        self.keyword_fn = keyword_fn

    def add_documents(self, documents):
        '''文档灌库'''
        if self.es_client.indices.exists(index=self.index_name):
            self.es_client.indices.delete(index=self.index_name)
        self.es_client.indices.create(index=self.index_name)
        actions = [
            {
                "_index": self.index_name,
                "_source": {
                    "keywords": self.keyword_fn(doc),
                    "text": doc,
                    "id": f"doc_{i}"
                }
            }
            for i, doc in enumerate(documents)
        ]
        helpers.bulk(self.es_client, actions)
        time.sleep(1)

    def search(self, query_string, top_n=3):
        '''检索'''
        search_query = {
            "match": {
                "keywords": self.keyword_fn(query_string)
            }
        }
        res = self.es_client.search(
            index=self.index_name, query=search_query, size=top_n)
        return {
            hit["_source"]["id"]: {
                "text": hit["_source"]["text"],
                "rank": i,
            }
            for i, hit in enumerate(res["hits"]["hits"])
        }

 

  1. 基于向量检索的排序
# 创建向量数据库连接器
vecdb_connector = MyVectorDBConnector("demo_vec_rrf", get_embeddings)

# 文档灌库
vecdb_connector.add_documents(documents)

# 向量检索
vector_search_results = {
    "doc_"+str(documents.index(doc)): {
        "text": doc,
        "rank": i
    }
    for i, doc in enumerate(
        vecdb_connector.search(query, 3)["documents"][0]
    )
}  # 把结果转成跟上面关键字检索结果一样的格式

print(json.dumps(vector_search_results, indent=4, ensure_ascii=False))
  1. 基于 RRF 的融合排序

 

def rrf(ranks, k=1):
    ret = {}
    # 遍历每次的排序结果
    for rank in ranks:
        # 遍历排序中每个元素
        for id, val in rank.items():
            if id not in ret:
                ret[id] = {"score": 0, "text": val["text"]}
            # 计算 RRF 得分
            ret[id]["score"] += 1.0/(k+val["rank"])
    # 按 RRF 得分排序,并返回
    return dict(sorted(ret.items(), key=lambda item: item[1]["score"], reverse=True))
import json

# 融合两次检索的排序结果
reranked = rrf([keyword_search_results, vector_search_results])

print(json.dumps(reranked, indent=4, ensure_ascii=False))
1.4 RAG-Fusion

RAG-Fusion 就是利用了 RRF 的原理来提升检索的准确性。

Image

原始项目(一段非常简短的演示代码):https://github.com/Raudaschl/rag-fusion

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

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

相关文章

教你一段代码激活计算机系统

方法简单粗暴&#xff0c;再也不用遭受未激活的烦恼了&#xff01; 新建文本 输入代码&#xff0c;把文件后缀.txt改.bat slmgr /skms kms.03k.org slmgr /ato

2024-2025最新软考系统架构设计师的复习资料教材,解决如何快速高效通过该考试,试题的重点和难点在哪里?案例分析题和论文题的要点和踩坑点分析

目录 引言考试概述 考试结构考试内容 复习策略 制定复习计划学习资源 知识点详解 系统架构基础设计原则与模式系统分析与设计软件开发过程项目管理系统集成性能与优化安全性设计新兴技术 试题解析 选择题案例分析题论文题 重点与难点分析模拟试题与答案参考资料总结 引言 系…

DeepSpeed Pipeline并行

DeepSpeed为了克服一般Pipeline并行的forward时weights,和backward时计算梯度的weights, 二者不相同的问题&#xff0c;退而求其次&#xff0c;牺牲性能&#xff0c;采用gradient-accumulate方式&#xff0c;backward时只累积梯度至local&#xff0c;并不更新weights&#xff1…

css display:grid布局,实现任意行、列合并后展示,自适应大小屏幕

现有6X7列的一个布局&#xff0c;如下图所示 想要用户能组成任意矩形盒子&#xff0c;并展示内容&#xff0c;具体效果如下&#xff08;仅为一个示例&#xff0c;其实可以任意组合矩形&#xff09;&#xff1a; html代码&#xff1a; <div class"grid-container"…

SQL 截取函数

目录 1、substring 2、left 3、right 4、substring_index 1、substring 用途&#xff1a;字段截取从指定开始的字符开始&#xff0c;截取要的数&#xff1b;指定开始的字符数字可以用负的&#xff0c;指定开始的字符从后往前(向左)数&#xff0c;截取要的数不能为负。 语…

BoxSizer布局

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 在前面的实例中&#xff0c;使用了文本和按钮等控件&#xff0c;并将这些控件通过pos参数布置在pannel画板上。虽然这种设置坐标的方式很容易理解&am…

GitLab教程(四):分支(branch)和合并(merge)

文章目录 1.分支&#xff08;branch&#xff09;&#xff08;1&#xff09;分支的概念&#xff08;2&#xff09;branch命令 2.合并&#xff08;merge&#xff09;&#xff08;1&#xff09;三个命令pullfetchmergegit fetchgit mergegit pull &#xff08;2&#xff09;合并冲…

C++开源软件:跨平台本地密码管理器KeePassXC/KeePassDX

KeePassXC、KeePass和KeePassDX在功能、平台和特点上有所区别&#xff0c;以下是对这三款密码管理器的清晰区分&#xff1a; KeePassXC&#xff1a; 平台&#xff1a;跨平台&#xff0c;支持Windows、macOS和Linux等主流操作系统。 安全性&#xff1a;使用AES加密算法&#x…

路虽远,行则将至 - 附暑期实习、秋招历程经验分享

前言 大家好 许久没有时间静下心来打开编辑器写文章了 忙碌暂过&#xff0c;难得一闲时 求学三年&#xff0c;终到离别时 回忆过往&#xff0c;枯燥且多彩 有一点经验&#xff0c;以文字形式分享&#xff0c;希望帮助到大家 可能是这段时间事多且杂&#xff0c;加上很长一…

NSS题目练习8

[SWPUCTF 2022 新生赛]numgame 打开发现不能直接更改数值&#xff0c;会变成负数&#xff0c;快捷键不能用&#xff0c;输入view-source查看源代码&#xff0c;发现js文件 点开后发现最下面有个酷似flag的东西 提交后是错的&#xff0c;看着像是base64&#xff0c;解码后得到另…

The Sandbox 游戏创作比赛|巴黎 CITY JAM

The Sandbox City Jam 邀请大家参与 The Sandbox 全新 Game Jam&#xff1a;City Jam&#xff01;活动将以社区为中心&#xff0c;每次一个城市&#xff0c;旨在将国际文化带入The Sandbox。你可以通过参与比赛赢得奖品&#xff0c;发展技能&#xff0c;并与其他创作者为伴&…

autosleep框架设计与实现

在低功耗系统中&#xff0c;autosleep是一个较小的模块&#xff0c;是低功耗主流程的入口。在Linux内核中&#xff0c;autosleep是休眠流程的触发点和入口点&#xff0c;PM Core的休眠流程入口pm_suspend()就是被autosleep的睡眠工作队列调用而进入休眠的。 该功能的支持受宏…

反向代购系统建站技术详解

一、引言 随着全球化趋势的深入和电商行业的快速发展&#xff0c;反向代购&#xff08;Reverse Daigou&#xff09;作为一种新兴的商业模式逐渐受到关注。反向代购&#xff0c;即海外消费者通过国内电商平台购买国内商品并委托平台或第三方进行国际配送&#xff0c;满足了海外…

[Java基本语法] 从0到1带你精通Java基本语法

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;线程与…

类和对象(二)(C++)

初始化列表 class Date{public:Date(int year, int month, int day){_year year;_month month;_day day;}private:int _year;int _month;int _day;}; 虽然上述构造函数调用之后&#xff0c;对象中已经有了一个初始值&#xff0c;但是不能将其称为对对象中成员变量的初始化…

第十五篇——条件熵和信息增益:你提供的信息到底值多少钱?

目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么&#xff1f; 四、总结五、升华 一、背景介绍 通过这篇文章&#xff0c;我知道了条件熵和信息增益&#xff1b;如果你试…

创建应用程序

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 使用wxPython之前&#xff0c;先来了解两个基础对象&#xff1a;应用程序对象和顶级窗口。 应用程序对象管理主事件循环&#xff0c;主事件循环是wx…

JavaScript的运算符(算术、比较、赋值、逻辑、条件)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

直播预告|就在明天,解锁半导体测试全攻略

随着5G、物联网、人工智能和新能源汽车等新兴产业的快速发展&#xff0c;半导体测试设备与服务的市场需求将更加多样化。IC测试作为产品良率和成本管理的重要环节&#xff0c;行业测试工程师面临着降低测试成本和提高产品良率的强劲压力。如何提升测试的质量与效率&#xff0c;…

Spring Boot + Mybatis Plus实现登录注册

Spring Boot 实现登录注册 1. 注册 业务逻辑 客户端输入注册时需要的用户参数&#xff0c;比如&#xff1a;账户名、密码、确认密码、其他服务端接收到客户端的请求参数进行校验&#xff0c;然后判断是否有误&#xff0c;有误的地方就将错误信息抛出将密码进行加密之后存储到…