原文:动手学RAG:汽车知识问答 - 知乎
在自然语言处理领域,大型语言模型(LLM)如GPT-3、BERT等已经取得了显著的进展,它们能够生成连贯、自然的文本,回答问题,并执行其他复杂的语言任务。然而,这些模型存在一些固有的局限性,如“模型幻觉问题”、“时效性问题”和“数据安全问题”。为了克服这些限制,检索增强生成(RAG)技术应运而生。
RAG技术结合了大型语言模型的强大生成能力和检索系统的精确性。它允许模型在生成文本时,从外部知识库中检索相关信息,从而提高生成内容的准确性、相关性和时效性。这种方法不仅增强了模型的回答能力,还减少了生成错误信息的风险。
本月的学习内容主要围绕检索增强生成(RAG)技术展开: - RAG技术背景与动机 - RAG技术基本原理和技术流程 - 知识库构建与管理、检索模块技术 - ChatGPT/ChatGLM的API使用
为了激励各位同学完成的学习任务,在完成学习后(本次活动,截止2月29),将按照积分顺序进行评选 Top3 的学习者。如果打卡积分相同,则按照prompt质量和文本长度进行排序。
昵称 | T1 | T2 | T3 | T4 | T5 | T6 | T7 |
---|---|---|---|---|---|---|---|
✅ | ✅ | ||||||
Colin_Cgz | ✅ | ✅ | |||||
boren | ✅ | ||||||
糖醋鱼 | ✅ | ||||||
全幼儿园最可爱 | ✅ | ||||||
Camellia | ✅ | ||||||
id | ✅ | ||||||
live_high | ✅ | ||||||
招亮 | ✅ | ||||||
郑 . | ✅ | ||||||
肖红忠 | ✅ |
Top1的学习者将获得以下奖励:
Top2-3的学习者将获得以下奖励: - 20元红包 - Coggle 竞赛专访机会
历史活动打卡链接,可以参考如下格式: - https://blog.csdn.net/weixin_42551154/article/details/125474519 - https://blog.csdn.net/weixin_42551154/article/details/125481695
本次活动参赛选手以大模型为中心制作一个问答系统,回答用户的汽车相关问题。参赛选手需要根据问题,在文档中定位相关信息的位置,并根据文档内容通过大模型生成相应的答案。涉及的问题主要围绕汽车使用、维修、保养等方面。
问题1:怎么打开危险警告灯?
答案1:危险警告灯开关在方向盘下方,按下开关即可打开危险警告灯。
问题2:车辆如何保养?
答案2:为了保持车辆处于最佳状态,建议您定期关注车辆状态,包括定期保养、洗车、内部清洁、外部清洁、轮胎的保养、低压蓄电池的保养等。
问题3:靠背太热怎么办?
答案3:您好,如果您的座椅靠背太热,可以尝试关闭座椅加热功能。在多媒体显示屏上依次点击空调开启按键→座椅→加热,在该界面下可以关闭座椅加热。
任务名称 | 所需技能 |
---|---|
任务1:初始RAG | 无 |
任务2:ChatGPT/GLM API使用 | Python |
任务3:读取汽车问答数据 | Python |
任务4:文本索引与答案检索 | TFIDF、BM25 |
任务5:文本嵌入与向量检索 | Embedding、transformer |
任务6:文本多路召回与重排序 | ReRank |
任务7:文本问答Promopt优化 | Python |
任务8:问答意图识别(进阶方向) | BERT/TFIDF |
任务9:问答关键词提取(进阶方向) | TextRank |
任务10:扩展词与扩展查询(进阶方向) | Word2Vec/BART |
任务11:本地微调ChatGLM(进阶方向) | ChatGLM |
大模型的局限性
大型语言模型在自然语言处理领域展示了显著的能力,但它们也存在一系列固有的缺点。首先,虽然这些模型在掌握大量信息方面非常有效,但它们的结构和参数数量使得对其进行修改、微调或重新训练变得异常困难,且相关成本相当可观。
其次,大型语言模型的应用往往依赖于构建适当的提示(prompt)来引导模型生成所需的文本。这种方法通过将信息嵌入到提示中,从而引导模型按照特定的方向生成文本。然而,这种基于提示的方法可能使模型过于依赖先前见过的模式,而无法真正理解问题的本质。
大模型现存问题 | 大型语言模型的局限性 |
---|---|
问题1.1 | 模型幻觉问题:生成内容可能不准确或不一致 |
问题1.2 | 时效性问题:生成的内容不具有当前时效性 |
问题1.3 | 数据安全问题:可能存在敏感信息泄露风险 |
在自然语言处理领域,幻觉(Hallucination)被定义为生成的内容与提供的源内容无关或不忠实,具体而言,是一种虚假的感知,但在表面上却似乎是真实的。在一般语境中,幻觉是一个心理学术语,指的是一种特定类型的感知。在自然语言处理或大型语言模型的语境下,这种感知即为一种虚假的、不符合实际的信息。
造成幻觉的原因主要可以归结为数据驱动原因、表示和解码的不完善以及参数知识偏见。首先,数据对不齐或不匹配可能导致幻觉,因为模型在训练中未能准确地理解源内容与参考内容之间的关系。
知识库问答(Knowledge Base Question Answering,KBQA)
知识库问答(Knowledge Base Question Answering,简称KBQA)是一种早期的对话系统方法,旨在利用结构化的知识库进行自然语言问题的回答。这种方法基于一个存储在图数据库中的知识库,通常以三元组的形式表示为<主题,关系,对象>,其中每个三元组都附带相关的属性信息。
知识库问答早期是对话系统中的有效方法,其基于知识图谱的结构为系统提供了丰富的语义信息,使得系统能够更深入地理解用户提出的问题,并以结构化的形式回答这些问题。随着技术的不断发展,KBQA方法也在不断演进,为对话系统的进一步提升奠定了基础。
在KBQA中,有两种主流方法用于处理自然语言问题: - 主题识别与实体链接:该方法从识别问题中的主题开始,将其链接到知识库中的实体(称为主题实体)。通过主题实体,系统能够在知识库中查找相关的信息并回答问题。 - 多跳查询:基于图数据库的优势,KBQA能够进行多跳查询,即通过多个关系跨越多个实体来获取更深层次的信息。这种灵活性使得系统能够更全面地理解和回答用户的复杂问题。
RAG介绍
检索增强生成(RAG)技术在弥补大型语言模型(LLM)的局限性方面取得了显著进展,尤其是在解决幻觉问题和提升实效性方面。在之前提到的LLM存在的问题中,特别是幻觉问题和时效性问题,RAG技术通过引入外部知识库的检索机制,成功提升了生成内容的准确性、相关性和时效性。
RAG优点 | 描述 |
---|---|
优点1.1 | 提高准确性和相关性 |
优点1.2 | 改善时效性,使模型适应当前事件和知识 |
优点1.3 | 降低生成错误风险,依赖检索系统提供的准确信息 |
RAG被构建为一个应用于大型语言模型的框架,其目标是通过结合大模型的生成能力和外部知识库的检索机制,提升自然语言处理任务的效果。 RAG并非旨在取代已有的知识库问答(KBQA)系统,而是作为一种补充,利用检索机制强调实时性和准确性,从而弥补大型语言模型固有的局限性。
RAG框架的最终输出被设计为一种协同工作模式,将检索到的知识融合到大型语言模型的生成过程中。在应对任务特定问题时,RAG会生成一段标准化的句子,引导大模型进行回答。下面是RAG输出到大型语言模型的典型模板:
你是一个{task}方面的专家,请结合给定的资料,并回答最终的问题。请如实回答,如果问题在资料中找不到答案,请回答不知道。
问题:{question}
资料:
- {information1}
- {information2}
- {information3}
其中,{task}
代表任务的领域或主题,{question}
是最终要回答的问题,而{information1}
、{information2}
等则是提供给模型的外部知识库中的具体信息。
RAG和SFT对比
在更新大型语言模型的知识方面,微调模型和使用RAG这两种方法有着各自的优缺点。微调模型优势在于能够通过有监督学习的方式,通过对任务相关数据的反复迭代调整,使得模型更好地适应特定领域的知识和要求。RAG能够从外部知识库中检索最新、准确的信息,从而提高了答案的质量和时效性。其优势在于可以利用最新的外部信息,从而更好地适应当前事件和知识。
微调模型 | RAG | |
---|---|---|
优点 | 针对特定任务调整预训练模型。优点是可针对特定任务优化; | 结合检索系统和生成模型。优点是能利用最新信息,提高答案质量,具有更好的可解释性和适应性: |
缺点 | 但缺点是更新成本高,对新信息适应性较差; | 是可能面临检索质量问题和曾加额外计算资源需求; |
特性 | RAG技术 | SFT模型微调 |
---|---|---|
知识更新 | 实时更新检索库,适合动态数据,无需频繁重训 | 存储静态信息,更新知识需要重新训练 |
外部知识 | 高效利用外部资源,适合各类数据库 | 可对齐外部知识,但对动态数据源不够灵活 |
数据处理 | 数据处理需求低 | 需构建高质量数据集,数据限制可能影响性能 |
模型定制化 | 专注于信息检索和整合,定制化程度低 | 可定制行为,风格及领域知识 |
可解释性 | 答案可追溯,解释性高 | 解释性相对低 |
计算资源 | 需要支持检索的计算资源,维护外部数据源 | 需要训练数据集和微调资源 |
延迟要求 | 数据检索可能增加延迟 | 微调后的模型反应更快 |
减少幻觉 | 基于实际数据,幻觉减少 | 通过特定域训练可减少幻觉,但仍然有限 |
道德和隐私 | 处理外部文本数据时需要考虑隐私和道德问题 | 训练数据的敏感内容可能引发隐私问题 |
RAG实现流程
如果使用RAG,主要包括信息检索和大型语言模型调用两个关键过程。信息检索通过连接外部知识库,获取与问题相关的信息;而大型语言模型调用则用于将这些信息整合到自然语言生成的过程中,以生成最终的回答。
RAG流程 | 描述 |
---|---|
步骤1:问题理解 | 准确把握用户的意图 |
步骤2:知识检索 | 从知识库中相关的知识检索 |
步骤3:答案生成 | 将检索结果与问题 |
RAG每个步骤都面临一些挑战,这些挑战使得RAG的实现变得复杂而困难。在问题理解阶段,系统需要准确把握用户的意图。用户提问往往是短文本,而知识库中的信息可能是长文本。 将用户提问与知识库中的知识建立有效的关联是一个难点,特别是考虑到用户提问可能模糊,用词不规范,难以直接找到相关的知识。
知识检索是RAG流程中的关键步骤,但也是面临挑战的步骤之一。用户提问可能以多种方式表达,而知识库的信息来源可能是多样的,包括PDF、PPT、Neo4j等格式。
此外用户的意图可能非常灵活,可能是提问,也可能需要进行闲聊 。在这个阶段,需要确保生成的答案与用户的意图一致,同时保持自然、连贯的文本。此外,大型模型的输出可能存在幻觉问题,即生成的内容可能与问题不相关,增加了生成准确回答的难度。
在论文综述「Retrieval-Augmented Generation for Large Language Models: A Survey」中,作者将RAG技术按照复杂度继续划分为Naive RAG,Advanced RAG、Modular RAG。
技术类型 | 描述 |
---|---|
Naive RAG | Naive RAG是RAG技术的最基本形式,也被称为经典RAG。包括索引、检索、生成三个基本步骤。索引阶段将文档库分割成短的Chunk,并构建向量索引。检索阶段根据问题和Chunks的相似度检索相关文档片段。生成阶段以检索到的上下文为条件,生成问题的回答。 |
Advanced RAG | Advanced RAG在Naive RAG的基础上进行优化和增强。包含额外处理步骤,分别在数据索引、检索前和检索后进行。包括更精细的数据清洗、设计文档结构和添加元数据,以提升文本一致性、准确性和检索效率。在检索前使用问题的重写、路由和扩充等方式对齐问题和文档块之间的语义差异。在检索后通过重排序避免“Lost in the Middle”现象,或通过上下文筛选与压缩缩短窗口长度。 |
Modular RAG | Modular RAG引入更多具体功能模块,例如查询搜索引擎、融合多个回答等。技术上融合了检索与微调、强化学习等。流程上对RAG模块进行设计和编排,出现多种不同RAG模式。提供更大灵活性,系统可以根据应用需求选择合适的功能模块组合。模块化RAG的引入使得系统更自由、灵活,适应不同场景和需求。 |
在RAG技术流程中,涉及多个关键模块,每个模块承担着特定的任务,协同工作以实现准确的知识检索和生成自然语言回答。
技术模块 | 描述 |
---|---|
意图理解 | 意图理解模块负责准确把握用户提出的问题,确定用户的意图和主题。处理用户提问的模糊性和不规范性,为后续流程提供清晰的任务目标。 |
文档解析 | 文档解析模块用于处理来自不同来源的文档,包括PDF、PPT、Neo4j等格式。该模块负责将文档内容转化为可处理的结构化形式,为知识检索提供合适的输入。 |
文档索引 | 文档索引模块将解析后的文档分割成短的Chunk,并构建向量索引。或通过全文索引进行文本检索,使得系统能够更快速地找到与用户问题相关的文档片段。 |
向量嵌入 | 向量嵌入模块负责将文档索引中的内容映射为向量表示,以便后续的相似度计算。这有助于模型更好地理解文档之间的关系,提高知识检索的准确性。 |
知识检索 | 知识检索模块根据用户提问和向量嵌入计算的相似度检索或文本检索打分。这一步骤需要解决问题和文档之间的语义关联,确保检索的准确性。 |
重排序 | 重排序模块在知识检索后对文档库进行重排序,以避免“Lost in the Middle”现象,确保最相关的文档片段在前面。 |
大模型回答 | 大模型回答模块利用大型语言模型生成最终的回答。该模块结合检索到的上下文,以生成连贯、准确的文本回答。 |
其他功能模块 | 可根据具体应用需求引入其他功能模块,如查询搜索引擎、融合多个回答等。模块化设计使得系统更加灵活,能够根据不同场景选择合适的功能模块组合。 |
两个大模型介绍
ChatGPT是OpenAI开发的聊天生成预训练转换器,基于GPT-3.5和GPT-4架构。该模型通过强化学习训练,具有出色的语言生成能力。ChatGPT支持文字方式的交互,用户可以使用自然语言对话的方式与ChatGPT进行通信。API的引入使得开发者能够将ChatGPT整合到自己的应用中,实现自动文本生成、自动问答等功能。
GLM是智谱AI推出的新一代基座大模型,相比上一代有着显著提升的性能,逼近GPT-4。GLM支持更长的上下文(128k),具备强大的多模态能力,并且推理速度更快,支持更高的并发。GLM的API接口为开发者提供了在自己应用中利用GLM进行语言生成的机会,为多种领域的任务提供了新的解决方案。
虽然这两个大模型都非常有效,但我们希望所有的学习者都需要学会对应的API调用。如果在本地使用ChatGLM3-6B等开源模型,也可以完成类似功能,但整体效果肯定不如这些费用的API。在任务2中,为了方便所有同学参与,我们将使用在线的ChatGPT/GLM API进行开发。这为没有本地GPU资源的同学提供了更便捷的方式。但ChatGPT/GLM API 都是需要注册账号并付费才能进行使用,如果你没有账号请联系小助手,我们将想要参与学习的同学提供API token。
ChatGPT | ChatGLM | |
---|---|---|
官网 | https://chat.openai.com/ | https://open.bigmodel.cn/ |
API文档 | https://platform.openai.com/docs/api-reference | https://open.bigmodel.cn/dev/api |
API计费说明 | https://openai.com/pricing | https://open.bigmodel.cn/pricing |
在继续后续的学习中,有以下注意事项: 1. ChatGPT/GLM API可以通过Python的库进行调用,也可以通过HTTP方式进行调用。为了代码方便,后续都使用HTTP方式调用。 2. ChatGPT/GLM API都有v3.5和v4两个对话版本的模型,但v4价格比v3.5高5-10倍,且更慢。所以除非必要,请默认使用v3.5模型。 3. ChatGPT API在国内无法链接,教程使用了第三方充值和转发方式。
对话 API
对话API是所有大模型的最常见的API,可以完成通用对话,也可以完成很多功能。但在进行调用时需要注意如下入参和参数返回结果。
参数 | 类型 | 必填 | 描述 |
---|---|---|---|
messages | Array | 必填 | 包含对话的消息列表。 |
model | String | 必填 | 要使用的模型的ID。 |
frequency_penalty | Number 或 null | 可选 | 根据文本中已有令牌的频率对新令牌进行惩罚。取值范围在-2.0到2.0之间。 |
logit_bias | Map | 可选 | 修改指定令牌在完成中出现的可能性。接受一个将令牌映射到偏置值(-100到100)的JSON对象。 |
logprobs | Boolean 或 null | 可选 | 是否返回输出令牌的对数概率。 |
top_logprobs | Integer 或 null | 可选 | 如果 logprobs 设置为 true,则返回每个令牌位置上最有可能的令牌数,每个都带有关联的对数概率。 |
max_tokens | Integer 或 null | 可选 | 可以在聊天完成中生成的最大 令牌数。 |
n | Integer 或 null | 可选 | 为每个输入消息生成的聊天完成选择的数量。 |
presence_penalty | Number 或 null | 可选 | 根据新令牌是否出现在到目前为止的文本中对其进行惩罚,增加模型谈论新主题的可能性。 |
seed | Integer 或 null | 可选 | 如果指定,系统将尽力进行确定性采样,以使具有相同 seed 和参数的重复请求应返回相同的结果。 |
stop | String/Array 或 null | 可选 | API 将停止生成进一步的令牌的序列,最多可设置为 4 个。 |
stream | Boolean 或 null | 可选 | 如果设置,将发送部分消息增量,就像在 ChatGPT 中一样。令牌将作为数据仅 server-sent events 发送,一旦可用,流将以 data: [DONE] 消息终止。参考 Example Python code。 |
temperature | Number 或 null | 可选 | 使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)会使输出更随机,而较低的值(如 0.2)会使其更集中和确定性。 |
top_p | Number 或 null | 可选 | 与温度采样的替代方法,称为核采样,其中模型考虑具有 top_p 概率质量的令牌的结果。因此,0.1 表示仅考虑构成前 10% 概率质量的令牌。 |
参数 | 类型 | 描述 |
---|---|---|
id | 字符串 | 用于唯一标识聊天完成的标识符。 |
choices | 数组 | 聊天完成选择的列表。如果n大于1,则可以有多个选择。 |
created | 整数 | 聊天完成创建的Unix时间戳(以秒为单位)。 |
model | 字符串 | 用于聊天完成的模型。 |
system_fingerprint | 字符串 | 此指纹表示模型运行时的后端配置。可与seed请求参数一起使用,了解可能影响确定性的后端更改。 |
usage | 对象 | 完成请求的使用统计信息。 |
finish_reason | 字符串 | 表示聊天完成的原因。可能的值包括"stop"(API返回了完整的聊天完成而没有受到任何限制),"length"(生成超过了max_tokens或对话超过了max context length),等等。 |
import requests
url = "https://openai.api2d.net/v1/chat/completions"
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer 填入Key'
}
data = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": """你好"""},]
}
response = requests.post(url, headers=headers, json=data)
print("Status Code", response.status_code)
print("JSON Response ", response.json())
import time
import jwt
import requests
# 实际KEY,过期时间
def generate_token(apikey: str, exp_seconds: int):
try:
id, secret = apikey.split(".")
except Exception as e:
raise Exception("invalid apikey", e)
payload = {
"api_key": id,
"exp": int(round(time.time() * 1000)) + exp_seconds * 1000,
"timestamp": int(round(time.time() * 1000)),
}
return jwt.encode(
payload,
secret,
algorithm="HS256",
headers={"alg": "HS256", "sign_type": "SIGN"},
)
url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
headers = {
'Content-Type': 'application/json',
'Authorization': generate_token("填入Key", 1000)
}
data = {
"model": "glm-3-turbo",
"messages": [{"role": "user", "content": """你好"""}]
}
response = requests.post(url, headers=headers, json=data)
print("Status Code", response.status_code)
print("JSON Response ", response.json())
Embedding API
import requests
url = "https://openai.api2d.net/v1/embeddings"
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer 填入Key'
}
data = {
"model": "text-embedding-ada-002",
"input": "魔兽世界坐骑去哪买"
}
response = requests.post(url, headers=headers, json=data)
print("Status Code", response.status_code)
print("JSON Response ", response.json())
import requests
url = "https://open.bigmodel.cn/api/paas/v4/embeddings"
headers = {
'Content-Type': 'application/json',
'Authorization': generate_token("填入Key", 1000)
}
data = {
"model": "embedding-2",
"input": "测试文本,今天很开心。"
}
response = requests.post(url, headers=headers, json=data)
print("Status Code", response.status_code)
print("JSON Response ", response.json())
Function call API
import requests
import json
url = "https://openai.api2d.net/v1/chat/completions"
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer 填入Key'
}
data = {
"model": "gpt-3.5-turbo-0613", # "gpt-4-0613",
"messages": [
{"role": "user", "content": "李华和小王是不是认识?"},
],
"functions": [
{
"name": "get_connection",
"description": "判断用户1和用户2 是否为朋友关系",
"parameters": {
"type": "object",
"properties": {
"user_id1": {
"type": "string",
"description": "用户ID 1"
},
"user_id2": {
"type": "string",
"description": "用户ID 2"
},
},
"required": ["user_id1", "user_id2"]
}
}
]
}
response = requests.post(url, headers=headers, json=data)
print("Status Code", response.status_code)
print("JSON Response ", response.json())
本次RAG学习使用了天池2023全球智能汽车AI挑战赛——赛道一:AI大模型检索问答的数据集,bing进行了重新标注。比赛要求参赛选手以大模型为中心制作一个问答系统,回答用户的汽车相关问题。参赛选手需要根据问题,在文档中定位相关信息的位置,并根据文档内容通过大模型生成相应的答案。本次比赛涉及的问题主要围绕汽车使用、维修、保养等方面。
在线评测地址:https://competition.coggle.club/
问题1:怎么打开危险警告灯?
答案1:危险警告灯开关在方向盘下方,按下开关即可打开危险警告灯。
问题2:车辆如何保养?
答案2:为了保持车辆处于最佳状态,建议您定期关注车辆状态,包括定期保养、洗车、内部清洁、外部清洁、轮胎的保养、低压蓄电池的保养等。
问题3:靠背太热怎么办?
答案3:您好,如果您的座椅靠背太热,可以尝试关闭座椅加热功能。在多媒体显示屏上依次点击空调开启按键→座椅→加热,在该界面下可以关闭座椅加热。
数据集下载地址: - 数据(百度云盘)链接: https://pan.baidu.com/s/19_oqY4bC_lJa_7Mc6lxU7w?pwd=v4bi 提取码: v4bi - 数据(谷歌云盘)链接:https://drive.google.com/drive/folders/1rD52-7W5ypzLk9ZXOrMBYx8F8xHaAzlW?usp=sharing
读取问答数据集
import json
import pdfplumber
questions = json.load(open("questions.json"))
print(questions[0])
pdf = pdfplumber.open("初赛训练数据集.pdf")
len(pdf.pages) # 页数
pdf.pages[0].extract_text() # 读取第一页内容
读取所有页内容
pdf_content = []
for page_idx in range(len(pdf.pages)):
pdf_content.append({
'page': 'page_' + str(page_idx + 1),
'content': pdf.pages[page_idx].extract_text()
})
文本检索流程
文本检索是一个多步骤的过程,其核心是构建倒排索引以实现高效的文本检索:
在实际应用中,倒排索引的构建和维护需要考虑性能问题,采用一些优化技术来提高检索效率,如压缩倒排索引、分布式索引等。这些步骤共同构成了一个有序而逻辑完整的文本检索流程。
文本检索与语义检索
下面是文本检索和语义检索的区别和联系的表格形式:
文本检索 | 语义检索 | |
---|---|---|
定义 | 通过关键词或短语匹配文本数据的过程 | 强调理解查询与文本之间的深层语义关系 |
方法 | 基于关键词匹配,使用TFIDF、BM25等权重计算 | 使用NLP技术,如词嵌入、预训练的语言模型 |
特点 | 强调字面意义,关注表面文本的匹配 | 关注词语之间的关联、语境和含义 |
应用场景 | 大规模文本数据的快速匹配 | 对语义理解要求较高的场景 |
优势 | 处理速度较快,适用于大规模文本数据 | 能够处理一词多义、近义词等语义上的复杂情况 |
联系 | 结合使用,先使用文本检索筛选出候选文档,然后在这些文档上应用语义检索 | 可以利用语义模型提取关键词的上下文信息,提升检索效果 |
在一些场景中,文本检索和语义检索可以结合使用,以充分利用它们各自的优势。例如,可以先使用文本检索筛选出候选文档,然后在这些文档上应用语义检索来进一步提高检索的准确性。当然具体使用哪种检索方法,需要具体分析,在RAG中可以结合两种方法一起进行使用。
TFIDF
TFIDF(Term Frequency-Inverse Document Frequency)是一种用于信息检索和文本挖掘的常用权重计算方法,旨在衡量一个词项对于一个文档集合中某个文档的重要性。该方法结合了两个方面的信息:词项在文档中的频率(TF)和在整个文档集合中的逆文档频率(IDF)。
词项在文档中出现的次数文档中所有词项的总数��(�,�)=词项t在文档d中出现的次数文档d中所有词项的总数
其中,$t$表示词项,$d$表示文档。TF表示了一个词项在文档中的相对频率,即在文档中出现的次数相对于文档总词项数的比例。
文档集合中的文档总数包含词项的文档数���(�)=log(文档集合中的文档总数包含词项t的文档数 + 1)
其中,$t$表示词项。IDF表示了一个词项在整个文档集合中的稀有程度,如果词项在许多文档中都出现,其IDF值较低,反之则较高。
�����(�,�,�)=��(�,�)���(�)
其中,$D$表示文档集合。TFIDF的最终值是将词项在文档中的频率和在整个文档集合中的逆文档频率相乘,这样可以得到一个更全面的评估,既考虑了在文档中的重要性,也考虑了在整个文档集合中的稀有性。
import jieba
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import normalize
# 对提问和PDF内容进行分词
question_words = [' '.join(jieba.lcut(x['question'])) for x in questions]
pdf_content_words = [' '.join(jieba.lcut(x['content'])) for x in pdf_content]
tfidf = TfidfVectorizer()
tfidf.fit(question_words + pdf_content_words)
# 提取TFIDF
question_feat = tfidf.transform(question_words)
pdf_content_feat = tfidf.transform(pdf_content_words)
# 进行归一化
question_feat = normalize(question_feat)
pdf_content_feat = normalize(pdf_content_feat)
# 检索进行排序
for query_idx, feat in enumerate(question_feat):
score = feat @ pdf_content_feat.T
score = score.toarray()[0]
max_score_page_idx = score.argsort()[-1] + 1
questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx)
# 生成提交结果
# https://competition.coggle.club/
with open('submit.json', 'w', encoding='utf8') as up:
json.dump(questions, up, ensure_ascii=False, indent=4)
BM25
BM25Okapi是BM25算法的一种变体,它在信息检索中用于评估文档与查询之间的相关性。以下是BM25Okapi的原理和打分方式的概述:
BM25Okapi的打分过程基于以下三个因素:词项在文档中的频率(TF)、文档的长度(doc_len)以及逆文档频率(IDF)。
文档长度对分数的影响通过 $b$ 控制。文档长度越长,该项的分数越小。BM25Okapi的打分公式综合考虑了以上三个因素,通过对每个词项的打分求和得到最终的文档与查询的相关性分数。
score=∑�∈query(IDF(�)⋅�_����⋅(�1+1)�_����+�1⋅(1−�+�⋅doc\_lenavgdl))
其中,$\text{avgdl}$是文档集合中的平均文档长度。BM25Okapi通过合理调整参数,兼顾了词项频率、文档长度和逆文档频率,使得在信息检索任务中能够更准确地评估文档与查询之间的相关性,提高检索效果。
# !pip install rank_bm25
from rank_bm25 import BM25Okapi
pdf_content_words = [jieba.lcut(x['content']) for x in pdf_content]
bm25 = BM25Okapi(pdf_content_words)
for query_idx in range(len(questions)):
doc_scores = bm25.get_scores(jieba.lcut(questions[query_idx]["question"]))
max_score_page_idx = doc_scores.argsort()[-1] + 1
questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx)
with open('submit.json', 'w', encoding='utf8') as up:
json.dump(questions, up, ensure_ascii=False, indent=4)
注意事项
语义检索流程
语义检索是通过词嵌入和句子嵌入等技术,将文本表示为语义丰富的向量。通过相似度计算和结果排序找到最相关的文档。用户查询经过自然语言处理处理,最终系统返回经过排序的相关文档,提供用户友好的信息展示。语义检索通过深度学习和自然语言处理技术,使得系统能够更准确地理解用户查询,提高检索的准确性和效果。
graph TD
A[加载模型] -->|Sentence Transformer| B((编码文本))
B -->|问题句子| C[问题Embeddings]
B -->|PDF内容句子| D[PDF内容Embeddings]
C -->|标准化| C
D -->|标准化| D
C -->|相似度计算| E[相似度矩阵]
D -->|相似度计算| E
E -->|排序| F[排序后的相似度]
F -->|选取最大值| G[最相似的页码]
G -->|写入结果| H[生成提交结果]
文本编码模型
文本编码模型对于语义检索的精度至关重要。目前,大多数语义检索系统采用预训练模型进行文本编码,其中最为常见的是基于BERT(Bidirectional Encoder Representations from Transformers)的模型,或者使用GPT(Generative Pre-trained Transformer)等。这些预训练模型通过在大规模语料上进行训练,能够捕捉词语和句子之间的复杂语义关系。选择合适的文本编码模型直接影响到得到的文本向量的有效性,进而影响检索的准确性和效果。
编码模型排行榜:https://huggingface.co/spaces/mteb/leaderboard
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('../hugging-face-model/moka-ai/m3e-small/')
question_sentences = [x['question'] for x in questions]
pdf_content_sentences = [x['content'] for x in pdf_content]
question_embeddings = model.encode(question_sentences, normalize_embeddings=True)
pdf_embeddings = model.encode(pdf_content_sentences, normalize_embeddings=True)
for query_idx, feat in enumerate(question_embeddings):
score = feat @ pdf_embeddings.T
max_score_page_idx = score.argsort()[-1] + 1
questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx)
with open('submit.json', 'w', encoding='utf8') as up:
json.dump(questions, up, ensure_ascii=False, indent=4)
model = SentenceTransformer('../hugging-face-model/BAAI/bge-small-zh-v1.5/')
# 剩余代码与M3E部分相同
model = SentenceTransformer("../hugging-face-model/maidalun1020/bce-embedding-base_v1", device='cuda')
model.max_seq_length = 512
# 剩余代码与M3E部分相同
文本切分方法
文本的长度是另一个关键因素,影响了文本编码的结果。短文本和长文本在编码成向量时可能表达不同的语义信息。即使两者包含相同的单词或有相似的语义,由于上下文的不同,得到的向量也会有所不同。因此,当在语义检索中使用短文本来检索长文本时,或者反之,可能导致一定的误差。针对文本长度的差异,有些系统采用截断或填充等方式处理,以保持一致的向量表示。
更多阅读资料: - https://python.langchain.com/docs/modules/data_connection/document_transformers/ - https://chunkviz.up.railway.app/
名称 | 分割依据 | 描述 |
---|---|---|
递归式分割器 | 一组用户定义的字符 | 递归地分割文本。递归分割文本的目的是尽量保持相关的文本段落相邻。这是开始文本分割的推荐方式。 |
HTML分割器 | HTML特定字符 | 基于HTML特定字符进行文本分割。特别地,它会添加有关每个文本块来源的相关信息(基于HTML结构)。 |
Markdown分割器 | Markdown特定字符 | 基于Markdown特定字符进行文本分割。特别地,它会添加有关每个文本块来源的相关信息(基于Markdown结构)。 |
代码分割器 | 代码(Python、JS)特定字符 | 基于特定于编码语言的字符进行文本分割。支持从15种不同的编程语言中选择。 |
Token分割器 | Tokens | 基于Token进行文本分割。存在一些不同的Token计量方法。 |
字符分割器 | 用户定义的字符 | 基于用户定义的字符进行文本分割。这是较为简单的分割方法之一。 |
语义分块器 | 句子 | 首先基于句子进行分割。然后,如果它们在语义上足够相似,就将相邻的句子组合在一起。 |
对于自然语言,可以推荐使用Token分割器,结合Chunk Size和Overlap Size可以得到不同的切分:
多路召回逻辑
多路召回逻辑是在文本检索中常用的一种策略,其目的是通过多个召回路径(或方法)综合获取候选文档,以提高检索的全面性和准确性。单一的召回方法可能由于模型特性或数据特点而存在局限性,多路召回逻辑引入了多个召回路径,每个路径采用不同的召回方法。
重排序逻辑(BM25 + BGE Rerank)
重排序逻辑是文本检索领域中一种重要的策略,主要用于优化原有文本检索方法返回的候选文档顺序,以提高最终的检索效果。在传统的文本检索方法中,往往采用打分的逻辑,如计算BERT嵌入向量之间的相似度。而重排序逻辑引入了更为复杂的文本交叉方法,通过特征交叉得到更进一步的打分,从而提高排序的准确性。
import jieba, json, pdfplumber
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import normalize
from rank_bm25 import BM25Okapi
questions = json.load(open("questions.json"))
pdf = pdfplumber.open("初赛训练数据集.pdf")
pdf_content = []
for page_idx in range(len(pdf.pages)):
pdf_content.append({
'page': 'page_' + str(page_idx + 1),
'content': pdf.pages[page_idx].extract_text()
})
# 加载重排序模型
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('../hugging-face-model/BAAI/bge-reranker-base/')
rerank_model = AutoModelForSequenceClassification.from_pretrained('../hugging-face-model/BAAI/bge-reranker-base/')
rerank_model.cuda()
pdf_content_words = [jieba.lcut(x['content']) for x in pdf_content]
bm25 = BM25Okapi(pdf_content_words)
for query_idx in range(len(questions)):
# 首先进行BM25检索
doc_scores = bm25.get_scores(jieba.lcut(questions[query_idx]["question"]))
max_score_page_idxs = doc_scores.argsort()[-3:]
# top3进行重排序
pairs = []
for idx in max_score_page_idxs:
pairs.append([questions[query_idx]["question"], pdf_content[idx]['content']])
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512)
with torch.no_grad():
inputs = {key: inputs[key].cuda() for key in inputs.keys()}
scores = rerank_model(**inputs, return_dict=True).logits.view(-1, ).float()
max_score_page_idx = max_score_page_idxs[scores.cpu().numpy().argmax()]
questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx + 1)
with open('submit.json', 'w', encoding='utf8') as up:
json.dump(questions, up, ensure_ascii=False, indent=4)
def ask_glm(content):
url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
headers = {
'Content-Type': 'application/json',
'Authorization': generate_token("填写key", 1000)
}
data = {
"model": "glm-3-turbo",
"messages": [{"role": "user", "content": content}]
}
response = requests.post(url, headers=headers, json=data)
return response.json()
pdf_content_words = [jieba.lcut(x['content']) for x in pdf_content]
bm25 = BM25Okapi(pdf_content_words)
for query_idx in range(len(questions)):
doc_scores = bm25.get_scores(jieba.lcut(questions[query_idx]["question"]))
max_score_page_idxs = doc_scores.argsort()[-3:]
pairs = []
for idx in max_score_page_idxs:
pairs.append([questions[query_idx]["question"], pdf_content[idx]['content']])
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512)
with torch.no_grad():
inputs = {key: inputs[key].cuda() for key in inputs.keys()}
scores = rerank_model(**inputs, return_dict=True).logits.view(-1, ).float()
max_score_page_idx = max_score_page_idxs[scores.cpu().numpy().argmax()]
questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx + 1)
prompt = '''你是一个汽车专家,帮我结合给定的资料,回答一个问题。如果问题无法从资料中获得,请输出结合给定的资料,无法回答问题。
资料:{0}
问题:{1}
'''.format(
pdf_content[max_score_page_idx]['content'],
questions[query_idx]["question"]
)
answer = ask_glm(prompt)['choices'][0]['message']['content']
questions[query_idx]['answer'] = answer
graph LR
A[用户提问] -->|步骤2| B{文本相似度或Prompt意图识别}
B --> |与汽车相关| C[RAG处理]
B --> |非汽车相关| D[通用问答处理]
C --> E[回答]
D --> F[回答]
通过这种方式,意图识别允许系统更加灵活地适应用户的多样化需求。它允许系统在不同的上下文中识别用户意图,从而提供更准确、定制的回答。这种方法的优势在于通过使用专门的模型来处理特定领域的问题,可以提高系统的准确性和用户体验。
文本相似度
Prompt意图识别
你是一个汽车维修和汽车销售的专家,请判断下面的提问是否与汽车使用相关。
{用户提问}
输出:相关 / 不相关
文本关键词抽取是自然语言处理领域的一项重要任务,其目标是从给定的文本中提取出最具代表性和有意义的单词或短语。这些关键词通常反映了文本的主题、内容或重要信息。常见的步骤包括分词、词性标注、停用词移除、计算词语权重以及关键词抽取算法等过程。
方法1:IDF
方法2:KeyBERT
https://github.com/MaartenGr/KeyBERT
distilbert-base-nli-mean-tokens
,将输入的文本嵌入到一个高维的向量空间中。BERT模型能够学习丰富的语义表示,因此生成的向量能够捕捉文本的语义信息。方法3:Prompt关键词提取
你是一个专业的文本理解专家,现在请你识别下面内容中的关键词,将关键词使用空格隔开:
{输入文本}
为了提高关键词提取过程的效率,可以采用一种优化策略。首先,将所有文档通过预训练的嵌入模型映射到向量空间中,生成它们的向量表示。接着,通过计算文档之间的相似性,使用余弦相似度等度量方法,将相似的文档聚合成一个文档聚类。在每个文档聚类中,选择一个代表性文档,利用关键词提取模型生成关键词。
查询改写(Query Rewriting,或称为查询扩展Query Expansion)。查询改写的应用方式是对原始Query拓展出与用户需求关联度高的改写词,多个改写词与用户搜索词一起做检索,从而用更好的表述,帮用户搜到更多符合要求的文本。
阅读链接:
通过词向量找到同义词
在进行查询改写时,可以利用词向量等技术找到同义词,以建立更丰富的词汇关联,从而提升搜索的全面性和准确性。
通过大模型生成扩展句
你是一个汽车维修和汽车销售的专家,将用户的提问改为含义相近当不相同的句子:
{用户提问}
ChatGLM3 是智谱AI和清华大学 KEG 实验室联合发布的对话预训练模型,ChatGLM3-6B 是 ChatGLM3 系列中的开源模型。ChatGLM3-6B 的基础模型 ChatGLM3-6B-Base 采用了更多样的训练数据、更充分的训练步数和更合理的训练策略。ChatGLM3-6B 采用了全新设计的 Prompt 格式 ,除正常的多轮对话外。同时原生支持工具调用(Function Call)、代码执行(Code Interpreter)和 Agent 任务等复杂场景。
ChatGLM6B 本地对话Demo
>>> from transformers import AutoTokenizer, AutoModel
>>> tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
>>> model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True, device='cuda')
>>> model = model.eval()
>>> response, history = model.chat(tokenizer, "你好", history=[])
>>> print(response)
你好 !我是人工智能助手
ChatGLM3 - 6
B, 很高兴见到你, 欢迎问我任何问题。
>>> response, history = model.chat(tokenizer, "晚上睡不着应该怎么办", history=history)
>>> print(response)
晚上睡不着可能会让你感到焦虑或不舒服, 但以下是一些可以帮助你入睡的方法:
1.制定规律的睡眠时间表: 保持规律的睡眠时间表可以帮助你建立健康的睡眠习惯, 使你更容易入睡。尽量在每天的相同时间上床, 并在同一时间起床。
2.创造一个舒适的睡眠环境: 确保睡眠环境舒适, 安静, 黑暗且温度适宜。可以使用舒适的床上用品, 并保持房间通风。
3.放松身心: 在睡前做些放松的活动, 例如泡个热水澡, 听些轻柔的音乐, 阅读一些有趣的书籍等, 有助于缓解紧张和焦虑, 使你更容易入睡。
4.避免饮用含有咖啡因的饮料: 咖啡因是一种刺激性物质, 会影响你的睡眠质量。尽量避免在睡前饮用含有咖啡因的饮料, 例如咖啡, 茶和可乐。
5.避免在床上做与睡眠无关的事情: 在床上做些与睡眠无关的事情, 例如看电影, 玩游戏或工作等, 可能会干扰你的睡眠。
6.尝试呼吸技巧: 深呼吸是一种放松技巧, 可以帮助你缓解紧张和焦虑, 使你更容易入睡。试着慢慢吸气, 保持几秒钟, 然后缓慢呼气。
如果这些方法无法帮助你入睡, 你可以考虑咨询医生或睡眠专家, 寻求进一步的建议。
ChatGLM3-6B 微调示例
https://github.com/THUDM/ChatGLM3/tree/main/finetune_chatmodel_demo
专业名词 | 描述 |
---|---|
大型语言模型 | 在自然语言处理领域展示出强大生成能力的模型,如GPT系列。但其修改、微调或重新训练困难,成本高。 |
Prompt | "Prompt"(提示)是指一种引导大型语言模型(LLM)生成特定文本的方法。或可以理解为输给大模型的输入文本。 |
幻觉(Hallucination) | 在自然语言处理领域被定义为生成的内容与提供的源内容无关或不忠实,一种虚假的感知。 |
知识库问答(KBQA) | 早期的对话系统方法,利用结构化的知识库进行自然语言问题的回答。知识库以三元组形式表示<主题,关系,对象>,存储在图数据库中。 |
RAG | RAG是检索增强生成(Retrieval-augmented Generation)的缩写,是一种结合了大型语言模型的生成能力和检索系统的精确性的技术,用于提高生成内容的准确性、相关性和时效性。 |
倒排索引 | 倒排索引(Inverted Index)是一种数据结构,用于加速文本检索过程。它将文档中的词汇映射到出现该词汇的文档列表,从而实现根据词汇快速检索相关文档的目的。 |
文本嵌入 | 文本嵌入是将文本信息映射到高维向量空间的过程,使得具有语义相似性的文本在向量空间中距离较近。 |
文本相似度 | 文本相似度是衡量两段文本之间语义接近程度的度量。通过计算文本在嵌入空间中的相似性,可以评估它们在语义上的相似程度。 |
排序与重排序 | 在信息检索中,排序指的是将检索到的文档按照其与查询的相关性进行排序。重排序则是在排序后的结果基础上再次调整文档的顺序,以进一步提高与用户查询的匹配度。 |