Task3:源大模型RAG实战
为了克服知识局限性、数据安全性、大模型幻觉等问题,检索增强生成(Retrieval Augmented Generation, RAG)被提出,通过引入外部知识,使大模型能够生成准确且符合上下文的答案,同时能够减少模型幻觉的出现。
RAG三个基本步骤
-
索引:将文档库分割成较短的 Chunk,即文本块或文档片段,然后构建成向量索引。
-
检索:计算问题和 Chunks 的相似度,检索出若干个相关的 Chunk。
-
生成:将检索到的Chunks作为背景信息,生成问题的回答。
一个完整的RAG链路
线上接收到用户query
后,RAG会先进行检索,然后将检索到的 Chunks
和 query
一并输入到大模型,进而回答用户的问题。为了完成检索,需要离线将文档(ppt、word、pdf等)经过解析、切割甚至OCR转写,然后进行向量化存入数据库中。
离线计算
首先,知识库中包含了多种类型的文件,如pdf、word、ppt等,这些 文档
(Documents)需要提前被解析,然后切割成若干个较短的 Chunk
,并且进行清洗和去重。(知识库中知识的数量和质量决定了RAG的效果)
然后将知识库中的所有 Chunk
都转成向量,这一步也称为 向量化
(Vectorization)或者 索引
(Indexing)。向量化
需要事先构建一个 向量模型
(Embedding Model),它的作用就是将一段 Chunk
转成 向量
(Embedding)。一个好的向量模型,会使得具有相同语义的文本的向量表示在语义空间中的距离会比较近,而语义不同的文本在语义空间中的距离会比较远。
由于知识库中的所有 Chunk
都需要进行 向量化
,这会使得计算量非常大,因此这一过程通常是离线完成的。随着新知识的不断存储,向量的数量也会不断增加。这就需要将这些向量存储到 数据库
(DataBase)中进行管理。至此,离线计算就完成了。
在线计算
在实际使用RAG系统时,当给定一条用户 查询
(Query),需要先从知识库中找到所需的知识,这一步称为 检索
(Retrieval)。在 检索
过程中,用户查询首先会经过向量模型得到相应的向量,然后与 数据库
中所有 Chunk
的向量计算相似度,最简单的例如 余弦相似度
,然后得到最相近的一系列 Chunk
。
由于向量相似度的计算过程需要一定的时间,尤其是 数据库
非常大的时候。这时,可以在检索之前进行 召回
(Recall),从 数据库
中快速获得大量大概率相关的 Chunk
,然后只有这些 Chunk
会参与计算向量相似度。这样,计算的复杂度就从整个知识库降到了非常低。召回
步骤不要求非常高的准确性,通常采用简单的基于字符串的匹配算法。由于这些算法不需要任何模型,速度会非常快,常用的算法有 TF-IDF
,BM25
等。另外,也有很多工作致力于实现更快的 向量检索
,例如 faiss,annoy。
另一方面,人们发现,随着知识库的增大,除了检索的速度变慢外,检索的效果也会出现退化,如下图中绿线所示。由于 向量模型
能力有限,而随着知识库的增大,已经超出了其容量,因此准确性就会下降。在这种情况下,相似度最高的结果可能并不是最优的。
为了解决这一问题,提出增加一个二阶段检索——重排
(Rerank),即利用 重排模型
(Reranker),使得越相似的结果排名更靠前。这样就能实现准确率稳定增长,即数据越多,效果越好(如上图中紫线所示)。通常,为了与 重排
进行区分,一阶段检索有时也被称为 精排
。而在一些更复杂的系统中,在 召回
和 精排
之间还会添加一个 粗排
(Coarse Ranking) 步骤,承上启下。粗排的主要任务是对召回阶段获取的大量候选结果进行初步筛选和排序,快速剔除那些明显不符合用户需求的低质量结果,同时保留那些可能高度相关的候选项,为后续的精排阶段减少计算量,提高处理效率。
综上所述,在整个 检索
过程中,计算量的顺序是 召回
> 精排
> 重排
,而检索效果的顺序则是 召回
< 精排
< 重排
。当这一复杂的 检索
过程完成后,就会得到排好序的一系列 检索文档
(Retrieval Documents)。然后从中挑选最相似的 k
个结果,将它们和用户查询拼接成prompt的形式,输入到大模型。最后,大型模型就能够依据所提供的知识来生成回复,从而更有效地解答用户的问题。至此,一个完整的RAG链路就构建完毕了。
开源RAG框架
-
TinyRAG:DataWhale成员宋志学精心打造的纯手工搭建RAG框架。
-
LlamaIndex:一个用于构建大语言模型应用程序的数据框架,包括数据摄取、数据索引和查询引擎等功能。
-
LangChain:一个专为开发大语言模型应用程序而设计的框架,提供了构建所需的模块和工具。
-
QAnything:网易有道开发的本地知识库问答系统,支持任意格式文件或数据库。
-
RAGFlow:InfiniFlow开发的基于深度文档理解的RAG引擎。
源2.0-2B RAG实战
以 Yuan2-2B-Mars
模型为基础,进行RAG实战
魔塔社区创造PAI实例
魔搭社区 可以用task01的那个实例
打开终端 下载文件
git lfs install
git clone https://www.modelscope.cn/datasets/Datawhale/AICamp_yuan_baseline.git
cp AICamp_yuan_baseline/Task\ 3:源大模型RAG实战/* .
双击打开Task 3:源大模型RAG实战.ipynb,然后运行所有单元格
RUN >Run All Cells
# 安装 streamlit
pip install streamlit==1.24.0
构建向量模型
向量模型通常采用BERT架构。目前,开源的基于BERT架构的向量模型有如下:
-
BGE Embedding:智源通用embedding(BAAI general embedding, BGE)
-
BCEmbedding:网易有道训练的Bilingual and Crosslingual Embedding
-
jina-embeddings:Jina AI训练的text embedding
-
M3E:MokaAI训练的 Massive Mixed Embedding
除了BERT架构之外,还有基于LLM的向量模型有如下:
-
LLM-Embedder:智源LLM-Embedder
其次,还有API:
本次选用基于BERT架构的向量模型 bge-small-zh-v1.5
,它是一个4层的BERT模型,最大输入长度512,输出的向量维度也为512。bge-small-zh-v1.5
支持通过多个平台进行下载,因为机器就在魔搭,所以直接通过魔搭进行下载。模型在魔搭平台的地址为 AI-ModelScope/bge-small-zh-v1.5。(源大模型RAG实战.ipynb,单元格里都有)
# 向量模型下载
from modelscope import snapshot_download
model_dir = snapshot_download("AI-ModelScope/bge-small-zh-v1.5", cache_dir='.')
"""
使用的是 modelscope 中的 snapshot_download 函数,第一个参数为模型名称 AI-ModelScope/bge-small-zh-v1.5,第二个参数 cache_dir 为模型保存路径,这里.表示当前路径。模型大小约为91.4M。下载完成后,会在当前目录增加一个名为 AI-ModelScope 的文件夹,其中 bge-small-zh-v1___5 里面保存着下载好的向量模型。
- 还需要下载源大模型 IEITYuan/Yuan2-2B-Mars-hf
下载方法和 Task 1:零基础玩转源大模型
"""
# 源大模型下载
from modelscope import snapshot_download
model_dir = snapshot_download('IEITYuan/Yuan2-2B-Mars-hf', cache_dir='.')
实战
索引
# 构造索引,封装一个向量模型类 EmbeddingModel:
# 定义向量模型类
class EmbeddingModel:
"""
class for EmbeddingModel
"""
def __init__(self, path: str) -> None:
self.tokenizer = AutoTokenizer.from_pretrained(path)
self.model = AutoModel.from_pretrained(path).cuda()
print(f'Loading EmbeddingModel from {path}.')
def get_embeddings(self, texts: List) -> List[float]:
"""
calculate embedding for text list
"""
encoded_input = self.tokenizer(texts, padding=True, truncation=True, return_tensors='pt')
encoded_input = {k: v.cuda() for k, v in encoded_input.items()}
with torch.no_grad():
model_output = self.model(**encoded_input)
sentence_embeddings = model_output[0][:, 0]
sentence_embeddings = torch.nn.functional.normalize(sentence_embeddings, p=2, dim=1)
return sentence_embeddings.tolist()
"""
通过传入模型路径,新建一个 EmbeddingModel 对象 embed_model。
初始化时自动加载向量模型的tokenizer和模型参数。
"""
print("> Create embedding model...")
embed_model_path = './AI-ModelScope/bge-small-zh-v1___5'
embed_model = EmbeddingModel(embed_model_path)
"""
EmbeddingModel 类还有一个 get_embeddings() 函数,它可以获得输入文本的向量表示。
注意,这里为了充分发挥GPU矩阵计算的优势,输入和输出都是一个 List,即多条文本和他们的向量表示。
"""
> Create embedding model... Loading EmbeddingModel from ./AI-ModelScope/bge-small-zh-v1___5.
检索
# 定义向量库索引类
class VectorStoreIndex:
"""
class for VectorStoreIndex
"""
def __init__(self, doecment_path: str, embed_model: EmbeddingModel) -> None:
self.documents = []
for line in open(doecment_path, 'r', encoding='utf-8'):
line = line.strip()
self.documents.append(line)
self.embed_model = embed_model
self.vectors = self.embed_model.get_embeddings(self.documents)
print(f'Loading {len(self.documents)} documents for {doecment_path}.')
def get_similarity(self, vector1: List[float], vector2: List[float]) -> float:
"""
calculate cosine similarity between two vectors
"""
dot_product = np.dot(vector1, vector2)
magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2)
if not magnitude:
return 0
return dot_product / magnitude
def query(self, question: str, k: int = 1) -> List[str]:
question_vector = self.embed_model.get_embeddings([question])[0]
result = np.array([self.get_similarity(question_vector, vector) for vector in self.vectors])
return np.array(self.documents)[result.argsort()[-k:][::-1]].tolist()
"""
类似地,通过传入知识库文件路径,新建一个 VectorStoreIndex 对象 index。
初始化时会自动读取知识库的内容,然后传入向量模型,获得向量表示。
"""
print("> Create index...")
doecment_path = './knowledge.txt'
index = VectorStoreIndex(doecment_path, embed_model)
> Create index... Loading 3 documents for ./knowledge.txt.
get_embeddings() 函数支持一次性传入多条文本,但由于GPU的显存有限,输入的文本不宜太多。 所以,如果知识库很大,需要将知识库切分成多个batch,然后分批次送入向量模型。 这里,因为知识库比较小,所以就直接传到了 get_embeddings() 函数。
VectorStoreIndex 类还有一个 get_similarity() 函数,它用于计算两个向量之间的相似度,这里采用了余弦相似度。 VectorStoreIndex 类的入口,即查询函数 query()。传入用户的提问后,首先会送入向量模型获得其向量表示,然后与知识库中的所有向量计算相似度,最后将 k 个最相似的文档按顺序返回,k默认为1。
question = '介绍一下广州大学'
print('> Question:', question)
context = index.query(question)
print('> Context:', context)
生成
# 定义大语言模型类
class LLM:
"""
class for Yuan2.0 LLM
"""
def __init__(self, model_path: str) -> None:
print("Creat tokenizer...")
self.tokenizer = AutoTokenizer.from_pretrained(model_path, add_eos_token=False, add_bos_token=False, eos_token='<eod>')
self.tokenizer.add_tokens(['<sep>', '<pad>', '<mask>', '<predict>', '<FIM_SUFFIX>', '<FIM_PREFIX>', '<FIM_MIDDLE>','<commit_before>','<commit_msg>','<commit_after>','<jupyter_start>','<jupyter_text>','<jupyter_code>','<jupyter_output>','<empty_output>'], special_tokens=True)
print("Creat model...")
self.model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype=torch.bfloat16, trust_remote_code=True).cuda()
print(f'Loading Yuan2.0 model from {model_path}.')
def generate(self, question: str, context: List):
if context:
prompt = f'背景:{context}\n问题:{question}\n请基于背景,回答问题。'
else:
prompt = question
prompt += "<sep>"
inputs = self.tokenizer(prompt, return_tensors="pt")["input_ids"].cuda()
outputs = self.model.generate(inputs, do_sample=False, max_length=1024)
output = self.tokenizer.decode(outputs[0])
print(output.split("<sep>")[-1])
"""
传入 Yuan2-2B-Mars 的模型路径,新建一个 LLM 对象 llm。
初始化时自动加载源大模型的tokenizer和模型参数。
"""
print("> Create Yuan2.0 LLM...")
model_path = './IEITYuan/Yuan2-2B-Mars-hf'
llm = LLM(model_path)
"""
LLM 类的入口是生成函数 generate(),它有两个参数:
- question: 用户提问,是一个str
- context: 检索到的上下文信息,是一个List,默认是[],代表没有使用RAG
"""
You are using the default legacy behaviour of the <class 'transformers.models.llama.tokenization_llama.LlamaTokenizer'>. This is expected, and simply means that the `legacy` (previous) behavior will be used so nothing changes for you. If you want to use the new behaviour, set `legacy=False`. This should only be set if you understand what it means, and thoroughly read the reason why this was added as explained in https://github.com/huggingface/transformers/pull/24565 - if you loaded a llama tokenizer from a GGUF file you can ignore this message > Create Yuan2.0 LLM... Creat tokenizer... You are using the default legacy behaviour of the <class 'transformers.models.llama.tokenization_llama_fast.LlamaTokenizerFast'>. This is expected, and simply means that the `legacy` (previous) behavior will be used so nothing changes for you. If you want to use the new behaviour, set `legacy=False`. This should only be set if you understand what it means, and thoroughly read the reason why this was added as explained in https://github.com/huggingface/transformers/pull/24565 - if you loaded a llama tokenizer from a GGUF file you can ignore this message. Creat model... Loading Yuan2.0 model from ./IEITYuan/Yuan2-2B-Mars-hf.
体验使用RAG技术之后 Yuan2-2B-Mars
模型的回答效果:
print('> Without RAG:')
llm.generate(question, [])
print('> With RAG:')
llm.generate(question, context)
使用RAG之后,模型能够结果知识库中的知识,准确回答用户的提问。
体验新模型
浪潮信息模型上新:Yuan2-2B-July-hf
将Notebook中 # 源大模型下载
和 # 创建llm对象
下面的被注释的行都打开,重启单元格
结果
课后习题
知识库构建
1.确定想要开发应用所在的领域:大模型在哪些领域还存在问题?能否通过知识库进行解决?
高度专业化的领域(如医学、法律、精密工程)中,构建针对该领域的专业知识库,能够为大模型提供精确、权威的数据支持,弥补其在该领域的认知短板;
大模型可能无法实时更新以反映最新动态,知识库通过定期更新和维护,可以确保数据的新鲜度和准确性,为应用提供及时有效的信息支持;
大模型可能面临数据泄露的风险,而知识库可以通过数据脱敏、加密存储等手段,在保护用户隐私的同时,提供必要的知识服务。
2.收集领域数据:该领域的数据有哪些来源?
百度百科等在线百科(提供了大量经过整理的、结构化的知识条目,获取基础知识,其内容的权威性和准确性需进一步验证);
专业书籍与期刊(书籍包含该领域的深入见解和经典案例,而期刊论文代表该领域的最新研究成果);
学术论文数据库:如CNKI、PubMed、Google Scholar等(汇聚了全球范围内的学术论文);
行业报告与白皮书(由行业机构或企业发布的报告和白皮书,通常包含了对该领域现状、趋势及未来展望的深入分析,对于把握行业脉搏、构建行业知识库具有重要意义);
开源数据集(数据集经过精心整理,往往具有较高的质量和可用性)。
3.构建知识库:收集好的数据需要哪些预处理步骤?
数据清洗:去除数据中的噪声、冗余和错误信息,确保数据的准确性和一致性。这包括纠正拼写错误、去除无关字符、删除重复记录等。
数据去重:对于可能存在的重复数据,进行去重处理,以避免在知识库中产生冗余信息。
数据标准化:将数据转换为统一的格式和结构,以便于后续的处理和查询。这包括统一单位、标准化术语、建立统一的数据模型等。
数据分类与标签:根据数据的属性和特征,对数据进行分类和标签化处理,以便于后续的检索和利用。
数据质量评估:对数据的质量进行评估,确保数据满足知识库构建的要求。这包括检查数据的完整性、准确性、时效性和相关性等方面。
RAG实战
-
知识库索引:知识库怎么切分效果最好?考虑到效果和效率等因素,哪个向量模型更适配?
知识库切分:
知识库的切分效果直接影响到检索的效率和准确性。理想的切分方式应确保每个片段既包含独立且完整的语义信息,又能保持适当的粒度以平衡检索速度和相关性。
-
语义切分:利用NLP的篇章分析(discourse parsing)工具或BERT等预训练语言模型,根据段落或句子之间的语义关系进行切分。例如,可以使用BERT的NSP(Next Sentence Prediction)能力来判断两个段落是否具有语义连贯性,从而决定是否将它们合并或分开。
-
固定大小切分:对于某些场景,也可以采用固定大小的切分方式,如每10个段落为一块,但这种方式可能无法很好地保持语义的完整性。
向量模型选择:
在选择向量模型时,需要考虑模型的泛化能力、嵌入向量的维度、以及是否支持目标语言的特性。
-
适配性:Hugging Face的Transformer系列模型(如BERT、RoBERTa等)是常用的选择,因为它们经过广泛的预训练,对多种语言和任务都有良好的表现。
-
效率与效果:在效果和效率之间寻找平衡,可以选择适当维度的嵌入向量。通常,维度越高,模型捕捉的信息越丰富,但计算成本也越高。
2.检索:如果知识库比较大或者为了实现更快的检索,需要哪些工具?
-
向量数据库:如Milvus、FAISS、Pinecone等,这些数据库专为处理大规模嵌入向量而设计,能够提供高效的向量检索能力。
-
索引技术:利用倒排索引、全文索引或层次化索引等技术,可以进一步加速检索过程。
-
分布式系统:对于极端大规模的知识库,可能需要采用分布式系统来分散存储和检索压力。
3.生成:检索出内容怎么能被大模型利用好?prompt怎么调优?
内容利用:
-
融合检索结果:将检索到的相关内容以合理的方式融入prompt中,让大模型能够基于这些外部知识生成更准确的回答。
-
上下文整合:确保prompt中不仅包含检索结果,还包含足够的上下文信息,以帮助大模型理解问题的背景和意图。
prompt调优:
-
模板化prompt:设计一系列针对不同场景的模板化prompt,通过调整模板中的占位符来适应不同的输入和问题。
-
迭代优化:通过多次尝试和反馈,不断迭代优化prompt的设计。可以关注生成结果的准确性、相关性以及流畅性等方面。
-
利用元数据:在prompt中融入检索结果的元数据(如来源、可信度评分等),以提供更丰富的上下文信息。
其他
参考下面资料,探索RAG框架的使用方法: