RAG学习

文章介绍了RAG技术,一种通过外部知识库检索增强生成内容质量的方法,强调了其在准确性、相关性和时效性方面的优势,以及使用的关键技术如信息检索、大型语言模型调用、LangChain平台和API调用。文中还提到了多种文本处理和检索策略,如TF-IDF、BM25和SentenceTransformers的使用。
摘要由CSDN通过智能技术生成

首先感谢coggle社区提供的学习RAG的机会

任务 1 初始 RAG

大模型现有缺点

  • 幻觉:生成信息不真实
  • 时效性:生成信息滞后,而微调来增加知识成本太高
  • 安全性:敏感知识泄露
  • 过于依赖提示工程,无法理解问题本质

RAG 流程和基本步骤

RAG 技术通过引入外部知识库的检索机制,成功提升了生成内容的准确性、相关性和时效性,有效解决了上述的部分缺点。流程如下:

  • 首先,给定一个用户的输入,例如一个问题或一个话题,RAG 会从一个数据源中检索出与之相关的文本片段,例如网页、文档或数据库记录。这些文本片段称为上下文(context)。
  • 然后,RAG 会将用户的输入和检索到的上下文拼接成一个完整的输入,传递给一个大模型,例如 GPT。这个输入通常会包含一些提示(prompt),指导模型如何生成期望的输出,例如一个答案或一个摘要。
  • 最后,RAG 会从大模型的输出中提取或格式化所需的信息,返回给用户。

相较于微调通过监督学习反复迭代使得模型更适合于某个特定领域的知识和要求,RAG 可以快速补充最新的知识,满足时效性。

RAG 需要的技术

RAG 的使用主要包括信息检索和大型语言模型调用两个关键过程。信息检索通过连接外部知识库,获取与问题相关的信息;而大型语言模型调用则用于将这些信息整合到自然语言生成的过程中,以生成最终的回答。首先要理解提问者的意图,到知识库去检索信息,提供给大模型得到答案后再提取信息返回给用户。除此之外也有不少改进步骤。涉及的多个重要模块:

LangChain

LangChain 是一个专注于大模型应用开发的平台,其提供了一系列的组件和工具帮助构建 RAG 应用。包括加载数据、分割文本、嵌入文本(embedding)、向量存储器和检索器(查询 embedding)、聊天模型(实现文本生成)。

任务 2 chatGLM API 调用

API 文档:https://open.bigmodel.cn/dev/api

SDK 方面,调用提供同步、异步 (返回 ID,根据 ID 查询)、SSE 三种方式,文档提供了 system prompt、函数调用和 retrieval 的代码示例。主要用到的应该就是同步调用。

HTTP 调用好像麻烦一些。

模型聊天

组装 jwt 的 header 和 payload,然后将其放到请求的 Authorization 部分。在 data 部分部署要改的参数、想问的东西等。

import time
import jwt
import requests

# 实际KEY,过期时间
def generate_token(apikey: str, exp_seconds: int):
    #拆解api key的id和secret
    try:
        id, secret = apikey.split(".")
    except Exception as e:
        raise Exception("invalid apikey", e)
    
    #payload exp表示过期时间,timestamp表示当前时间 单位是毫秒
    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"},
    )
#以上都是文档示例代码 下面应该是标准的html调用
url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
headers = {
  'Content-Type': 'application/json',
  'Authorization': generate_token(apikey, 1000)
}

data = {
    "model": "glm-4",
    "messages": [{"role": "user", "content": """你好,请告诉我什么是RAG"""}]
}

response = requests.post(url, headers=headers, json=data)

print("Status Code", response.status_code)
print("JSON Response ", response.json())

嵌入

调用的 embedding-2 返回 1024 维向量。

function call

function call 指的是自己用语言描述一个函数,当模型接受到问题后,会分析是否要调用这个函数,若调用则从用户提供的文本中提取函数需要的函数值。就是一个文本结构化的工作。

任务 3 读取汽车问答数据

数据集来自去年的天池

解析 pdf 的工具用的 pdfplumber。

import json
import pdfplumber

questions = json.load(open("questions.json"))
print(questions[0])

pdf = pdfplumber.open("初赛训练数据集.pdf")
len(pdf.pages) # 页数

任务 4 文本索引与答案检索

文本检索和倒排索引

文本检索步骤:

  • 文本预处理 洗文本
  • 构建倒排索引 倒排索引就是知道关键词出现在哪里 关键词 -》文档。分词后对词构建倒排列表,记录该词所在位置和文档。
  • 文本检索 计算查询词的权重,然后对倒排索引匹配,利用检索算法排序。
    这种方法只是关注词的表面意思,与语义检索试图理解问题和文档含义有着较大区别。当然处理速度要快多了。可以将两者结合使用,先文本检索出重要文档再语义检索重要信息。
#先分词
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

TF 和 IDF 一个关注词在当前出现频率,一个关注词出在当前文档的独特性(出现在了多少个文档),两者相乘就能得到这个词对该文档的独一无二性。用问题和每页的 TFIDF 相乘就能得到这个问题的词和哪页最相似,锁定目标页数。

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   #第query idx个文本的TFIDF与所有pdf的TFIDF进行点乘 
    score = score.toarray()[0]   #转换成数组 降维度
    max_score_page_idx = score.argsort()[-1] + 1
    questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx)
BM25

BM25 发现词频和相关性之间是非线性的,当词出现的阈值超过一定限度后其影响力不再增加。而这个阈值和文档本身相关,因此有两个超参来调整词频和文档长度的影响。

当询问很长时,还需要知道询问中每个单词对询问的重要度,询问中的词频影响是可调的。

BM25Okapi 是一种变体(改的简单了一点,感觉是省略掉了询问和文档之间的词频差异)下面 avgdl 代表文档平均长度,qfreq 代表询问中的词频,k1 和 b 都是可调超参,分别控制词频和文档长度影响。

上述都不在工业界用,工业界用更高级的文本搜索。并且没有做到停用词筛选(一般这几种算词频的时候就把停用词筛下去了其实)和任务特化、筛选关键词、pdf 精细处理(是按页划分的而不是按章节划分的)等。

from rank_bm25 import BM25Okapi
#修改了一下不要再跑一次jieba了
bm25 = BM25Okapi(pdf_content_words)

for query_idx,query in enumerate(question_words):
    doc_scores = bm25.get_scores(query)
    max_score_page_idx = doc_scores.argsort()[-1] + 1
    questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx)

任务 5 文本嵌入和向量检索

就是计算问题的 embedding 和文档的 embedding 的相似度。现在是用预训练模型来 embedding。

sentence_transformer 的文档:https://github.com/UKPLab/sentence-transformers

https://www.sbert.net/

看文档应该是可以指定模型路径就能跑 假如模型支持的话其页面也会写其实 然后默认 cuda 我们找个中文的试试 用什么模型是现往本地下的 应该也可以下到本地直接指定路径调用

当然短文本和长文本生成 embedding 来比较还是有一定差距的,实际上做出了还不如 BM25,但是比提供的 BGE,M3E 好一点。有些系统会进行截断,将其分成较小的句子再拼接成一个大的块,并且还有 overlap 来保持上下文的语义链接。

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("DMetaSoul/Dmeta-embedding", device="cuda")

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)

文本多路召回与重排序

多路召回

看着像把几个模型混一块,每个模型都提供一些候选结果,然后加权排序得到最终的结果。在这里可以考虑两种实现方式,比如说结合BM25和语义搜索的按照排名加权,或者说从段落、句子、页三个角度来加权。这里就不再实现了。

重排序

重排序就是在获取原先top k候选文档的基础上进一步精排序,引入了更复杂的文本交叉方法。经典的重排方法一般使用交叉编码器,结合文档和查询的语义信息进行打分和排序。

用的是bge ranker bm25前五名重排序 最后分数是0.39

#加载重排序模型
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-reranker-base')
rerank_model = AutoModelForSequenceClassification.from_pretrained('BAAI/bge-reranker-base')

from rank_bm25 import BM25Okapi
bm25 = BM25Okapi(pdf_content_words)

for query_idx,query in enumerate(question_words):
    doc_scores = bm25.get_scores(query)
    max_score_page_idxs = doc_scores.argsort()[-5:]
    #获取bm25排序前五名的 然后把前五名重新装进pairs 问题-答案
    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优化

就把最优匹配的文本发给他,问他问题,看他的回复。

prompt尝试:

import tqdm
for query_idx in tqdm(range(len(questions))):
    prompt = '''你是汽车方面的专家,给你一个问题,你需要判断这个问题是否与汽车有关,若相关则返回数字[1],不相关则返回数字[0]
    你只需要回答1或0,不需要返回别的内容。
    '''.format(questions[query_idx]["question"])
    answer = ask_glm(prompt)['choices'][0]['message']['content']
    print(answer, query_idx)

for query_idx in tqdm(range(len(questions))):
    doc_scores = bm25.get_scores(jieba.lcut(questions[query_idx]["question"]))
    max_score_page_idxs = doc_scores.argsort()[-5:]

    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}

该资料在第{2}页
    '''.format(
        pdf_content[max_score_page_idx]['content'].replace("\n", ""),
        questions[query_idx]["question"],
        max_score_page_idx + 1
    )
    answer = ask_glm(prompt)['choices'][0]['message']['content']
    questions[query_idx]['answer'] = answer

参考:

https://coggle.club/blog/30days-of-ml-202401

https://juejin.cn/post/7310569308916334604

https://zhuanlan.zhihu.com/p/676996307

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值