RAG模型测试

简介RAG(Retrieval-Augmented Generation)是一种结合了检索和生成的技术,旨在通过外部知识库增强大型语言模型的能力。它通过检索相关信息来补充模型内部知识,提高输出的准确性和相关性。

特点

  • 可解释性:RAG模型的工作流程清晰,易于理解和解释。
  • 低难度:不需要对模型进行微调,可以作为一个外挂知识库使用。

工作原理RAG模型通过以下步骤工作:

  1. 向量数据库构建:构建一个包含大量知识的向量数据库。
  2. 信息检索:根据输入的提示词,从数据库中检索相关信息。
  3. 文本生成:结合检索到的信息和模型内部知识,生成文本。

难点与重点

  • 向量数据库的建设:需要精心设计和维护,以确保信息的质量和多样性。
  • 提取向量的模型选择:选择合适的模型来提取信息的向量表示,对检索效果至关重要。
  • 信息检索方法:高效的检索算法能够快速准确地找到相关信息。

测试数据集为了测试RAG模型,我们将使用汽车知识问答数据集。这个数据集相对简单,便于处理和分析。

数据下载数据集可以从以下链接下载:汽车知识问答数据集

注意事项

  • 请确保下载的链接有效,如果遇到问题,请检查链接或稍后重试。
  • 在使用数据集之前,进行必要的预处理,以适应RAG模型的需求。

结构化内容

  • 模型介绍:RAG模型的基本概念和工作原理。
  • 技术特点:RAG模型的技术优势和应用场景。
  • 实施难点:在实际应用中可能遇到的挑战和解决方案。
  • 测试方案:使用汽车知识问答数据集进行模型测试的方法和步骤。
  • 数据获取:提供数据集的下载链接和使用指南。 以上内容为RAG模型的测试准备和概述,旨在提供一个清晰的框架,以便进行有效的模型评估。
    在这里插入图片描述

rag步骤:

  1. 准备数据文档
  2. 构建向量库
  3. 以问题向量查询向量库
  4. 问题与向量库返回内容组成新的prompt
  5. 新prompt传入大模型返回结果

下面对以上步骤逐一分析

一 准备数据

简单地说,就是读取数据, 数据有各种格式, 这里我们用的问题时json, 知识文档是pdf

1
import jieba, json, pdfplumber # 对长文本进行切分 def split_text_fixed_size(text, chunk_size, overlap_size): new_text = [] for i in range(0, len(text), chunk_size): if i == 0: new_text.append(text[0:chunk_size]) else: new_text.append(text[i - overlap_size:i + chunk_size]) # new_text.append(text[i:i + chunk_size]) return new_text def read_data(query_data_path, knowledge_data_path): with open(query_data_path, 'r', encoding='utf-8') as f: questions = json.load(f) pdf = pdfplumber.open(knowledge_data_path) # 标记当前页与其文本知识 pdf_content = [] for page_idx in range(len(pdf.pages)): text = pdf.pages[page_idx].extract_text() new_text = split_text_fixed_size(text, chunk_size=100, overlap_size=5) for chunk_text in new_text: pdf_content.append({ 'page' : 'page_' + str(page_idx + 1), 'content': chunk_text }) return questions, pdf_content

构建向量库

流程概述: 将知识通过模型转换为向量形式,并存储于本地或内存中。虽然流程看似简单,但实际操作中包含许多影响效果的细节,例如选择何种模型来提取向量。本演示将展示流程,不深入探讨这些细节。 模型选择: 存在众多模型能够将句子映射为向量,常用的模型大多是基于BERT的变体。 参考链接: [Hugging Face Spaces

  • MTEB Leaderboard](https://huggingface.co/spaces/mteb/leaderboard) 模型应用: 我选择使用 stella_base_zh_v3_1792d 模型。为了获得更优的提取效果,建议对模型进行微调以适应当前数据。 向量库构建方法: 可以采用多种方法构建多个向量库,通过在检索时对多个结果进行重排,可以提高召回率。 方法示例: 这里将展示两种不同的方法来提取向量。 注意: 请根据上述内容,进行以下操作。
1
# 文本检索类向量库 pdf_content_words = [jieba.lcut(x['content']) for x in pdf_content] bm25 = BM25Okapi(pdf_content_words) # 语义检索类向量库 model = SentenceTransformer( # 'E:\PyCharm\PreTrainModel\stella_base_zh_v3_1792d' '/mnt/e/PyCharm/PreTrainModel/stella_base_zh_v3_1792d', # '/mnt/e/PyCharm/PreTrainModel/moka_aim3e_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)

向量检索策略优化

在处理向量检索任务时,我们通常会遇到召回率不足的问题。为了提升召回率,可以采取多路召回与结果重排的策略。以下是优化步骤的详细说明:

  1. 构建多个向量库:首先,我们需要构建多个向量库。每个库中的向量可以采用不同的编码方式或特征提取方法,以确保多样性。

  2. 计算相似度:对于给定的查询向量,与每个向量库中的所有向量计算相似度。这一步骤的目的是找出与查询向量最相似的向量。

  3. 多路召回:每个向量库会返回其top-k个最相似的结果。这一步是多路召回的核心,通过多个向量库的召回,可以增加检索结果的覆盖面。

  4. 结果重排:将所有向量库返回的top-k结果合并,然后进行统一的重排。重排的目的是优化最终的检索结果,提高准确性和相关性。

  5. 提取最终结果:在重排后的结果中,再次提取top-k个最相关的向量作为最终的检索结果。 通过上述步骤,可以显著提高向量检索的召回率和准确率,从而提升整体的检索效果。
    在这里插入图片描述

这里使用文件检索+语义检索,各返回topk结果,之后使用bge-reranker-base模型重排结果。

1
# 使用重排模型,获得当前数据对最高得分对应的索引 def get_rank_index(max_score_page_idxs_, questions_, pdf_content_): 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=scores.cpu().numpy().argmax() index = max_score_page_idxs[max_score] return max_score, index for query_idx in range(len(questions)): # 首先进行BM25检索 doc_scores = bm25.get_scores(jieba.lcut(questions[query_idx]["question"])) bm25_score_page_idxs = doc_scores.argsort()[-10:] # 再进行语义检索 score = question_embeddings[query_idx] @ pdf_embeddings.T ste_score_page_idxs = score.argsort()[-10: ] # questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx) # questions[query_idx]['reference'] = pdf_content[max_score_page_idxs]['page'] bm25_score,bm25_index=get_rank_index(bm25_score_page_idxs,questions, pdf_content) ste_score,ste_index=get_rank_index(ste_score_page_idxs,questions, pdf_content) if ste_score>=bm25_score: questions[query_idx]['reference'] = 'page_' + str(ste_index+ 1) else: questions[query_idx]['reference'] = 'page_' + str(bm25_index+ 1)

四,五 构建新prompt进行大模型RAG推理

整合以上各模块,以qwen作为推理大模型

1
# -*- coding: utf-8 -*- # @Time : 2024/6/13 23:41 # @Author : yblir # @File : qwen2_rag_test.py # explain : # ======================================================= # from openai import OpenAI 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 # import requests # 加载重排序模型 import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer, AutoModelForCausalLM from sentence_transformers import SentenceTransformer # client = OpenAI(api_key="sk-13c3a38819f84babb5cd298e001a10cb", base_url="https://api.deepseek.com") device = "cuda" rerank_tokenizer = AutoTokenizer.from_pretrained(r'E:\PyCharm\PreTrainModel\bge-reranker-base') rerank_model = AutoModelForSequenceClassification.from_pretrained(r'E:\PyCharm\PreTrainModel\bge-reranker-base') rerank_model.cuda() model_path = r'E:\PyCharm\PreTrainModel\qwen2-1_5b' # model_path = r'E:\PyCharm\PreTrainModel\qwen_7b_chat' # model_path = r'E:\PyCharm\PreTrainModel\qwen2_7b_instruct_awq_int4' tokenizer = AutoTokenizer.from_pretrained( model_path, # trust_remote_code=True ) model = AutoModelForCausalLM.from_pretrained( pretrained_model_name_or_path=model_path, torch_dtype="auto", device_map="auto", # trust_remote_code=True # attn_implementation="flash_attention_2" ) # 对长文本进行切分 def split_text_fixed_size(text, chunk_size, overlap_size): new_text = [] for i in range(0, len(text), chunk_size): if i == 0: new_text.append(text[0:chunk_size]) else: new_text.append(text[i - overlap_size:i + chunk_size]) # new_text.append(text[i:i + chunk_size]) return new_text def get_rank_index(max_score_page_idxs_, questions_, pdf_content_): pairs = [] for idx in max_score_page_idxs_: pairs.append([questions_[query_idx]["question"], pdf_content_[idx]['content']]) inputs = rerank_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 = scores.cpu().numpy().argmax() index = max_score_page_idxs_[max_score] return max_score, index def read_data(query_data_path, knowledge_data_path): with open(query_data_path, 'r', encoding='utf-8') as f: questions = json.load(f) pdf = pdfplumber.open(knowledge_data_path) # 标记当前页与其文本知识 pdf_content = [] for page_idx in range(len(pdf.pages)): text = pdf.pages[page_idx].extract_text() new_text = split_text_fixed_size(text, chunk_size=100, overlap_size=5) for chunk_text in new_text: pdf_content.append({ 'page' : 'page_' + str(page_idx + 1), 'content': chunk_text }) return questions, pdf_content def qwen_preprocess(tokenizer_, ziliao, question): """ 最终处理后,msg格式如下,system要改成自己的: [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Tell me who you are."}, {"role": "assistant", "content": "I am a large language model named Qwen..."} ] """ # tokenizer.apply_chat_template() 与model.generate搭配使用 messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": f"帮我结合给定的资料,回答问题。如果问题答案无法从资料中获得," f"输出结合给定的资料,无法回答问题. 如果找到答案, 就输出找到的答案, 资料:{ziliao}, 问题:{question}"}, ] # dd_generation_prompt 参数用于在输入中添加生成提示,该提示指向 <|im_start|>assistant\n text = tokenizer_.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) model_inputs_ = tokenizer_([text], return_tensors="pt").to(device) input_ids = tokenizer_.encode(text, return_tensors='pt') attention_mask_ = torch.ones(input_ids.shape, dtype=torch.long, device=device) # print(model_inputs) # sys.exit() return model_inputs_, attention_mask_ if __name__ == '__main__': questions, pdf_content = read_data(query_data_path=r"E:\localDatasets\汽车问答系统\questions.json", knowledge_data_path=r'E:\localDatasets\汽车问答系统\初赛训练数据集.pdf') # 文本检索类向量库 pdf_content_words = [jieba.lcut(x['content']) for x in pdf_content] bm25 = BM25Okapi(pdf_content_words) # 语义检索类向量库 sent_model = SentenceTransformer( r'E:\PyCharm\PreTrainModel\stella_base_zh_v3_1792d' # '/mnt/e/PyCharm/PreTrainModel/stella_base_zh_v3_1792d', # '/mnt/e/PyCharm/PreTrainModel/moka_aim3e_small', ) question_sentences = [x['question'] for x in questions] pdf_content_sentences = [x['content'] for x in pdf_content] question_embeddings = sent_model.encode(question_sentences, normalize_embeddings=True) pdf_embeddings = sent_model.encode(pdf_content_sentences, normalize_embeddings=True) for query_idx in range(len(questions)): # 首先进行BM25检索 doc_scores = bm25.get_scores(jieba.lcut(questions[query_idx]["question"])) bm25_score_page_idxs = doc_scores.argsort()[-10:] # 再进行语义检索 score = question_embeddings[query_idx] @ pdf_embeddings.T ste_score_page_idxs = score.argsort()[-10:] # questions[query_idx]['reference'] = 'page_' + str(max_score_page_idx) # questions[query_idx]['reference'] = pdf_content[max_score_page_idxs]['page'] bm25_score, bm25_index = get_rank_index(bm25_score_page_idxs, questions, pdf_content) ste_score, ste_index = get_rank_index(ste_score_page_idxs, questions, pdf_content) max_score_page_idx = 0 if ste_score >= bm25_score: questions[query_idx]['reference'] = 'page_' + str(ste_index + 1) max_score_page_idx = ste_index else: questions[query_idx]['reference'] = 'page_' + str(bm25_index + 1) max_score_page_idx = bm25_index model_inputs, attention_mask = qwen_preprocess( tokenizer, pdf_content[max_score_page_idx]['content'], questions[query_idx]["question"] ) generated_ids = model.generate( model_inputs.input_ids, max_new_tokens=128, # 最大输出长度. attention_mask=attention_mask, pad_token_id=tokenizer.eos_token_id ) generated_ids = [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] # print(response) # answer = ask_glm(pdf_content[max_score_page_idx]['content'], questions[query_idx]["question"]) print(f'question: {questions[query_idx]["question"]}, answer: {response}') # data_path = '/media/xk/D6B8A862B8A8433B/GitHub/llama-factory/data/train_clean_eval.json' # with open(data_path, 'r', encoding='utf-8') as f: # data = json.load(f)

共测试了qwen2-1.5b,qwen-7b-chat, qwen2-7b-instruct-awq-int4三个模型, qwen2-1.5b预测不能停止的问题还是存在,qwen2-7b-instruct-awq-int4 的RAG效果明细比qwen-7b-chat好,侧面印证了qwen2能力有明细提升。

qwen2-1.5b

在这里插入图片描述

qwen-7b-chat

在这里插入图片描述

qwen2-7b-awq-int4

在这里插入图片描述