写在前面
第二堂是动手构建一个Tiny-RAG,也就是RAG的简化版本,只包含RAG的核心功能Retrieval和Generation。
整体看完源码,并听完相应的视频讲解,内容感觉还算是很容易明白,本博文记录下自己感觉相对重要的以及一些思考(持续补充)。
课程链接:
1、RAG概述
检索增强生成技术(Retrieval-Augmented Generation,RAG),它的出现主要是解决大模型的“幻觉”问题,即当询问者询问一些大模型训练数据不包含的数据(如询问一些时效性比较强的或者是专业性比较强的问题),大模型可能会已读乱答,给出一些有误或者是无关的答案。
该技术原理是,通过在语言模型生成答案之前,先从广泛的文档数据库中检索相关信息,然后利用这些信息来引导生成过程,极大地提升了内容的准确性和相关性。
其提高了知识更新的速度,并增强了内容生成的可追溯性,使得大型语言模型在实际应用中变得更加实用和可信。
一开始觉得其有点像模型的微调,将模型的知识进行扩充,以便应对时刻变化的数据,就百度了下,依照百度百科,了解其与微调的一丢丢差别:
RAG | 微调 | |
特定任务 / 通用性 | 通用,可适应多种任务 | 特定任务进行优化,完成效果好,但通用性问题不灵活 |
知识引用 / 学习 | 引用大知识库生成答案,易核实 | 学习特定的任务知识生成答案 |
即时性 / 训练 | 可实现即时知识更新,无需训练 | 需要重新训练,时间成本高 |
可解释性 / 难以解释 | 来源于知识库,可解释性强 | 来源于内部学习可能难以解释 |
定制 / 通用性 | 可根据领域进行定制 | 需要为每个任务进行特定微调,需要更多任务特定数据 |
RAG的基本结构:
- 要有一个向量化模块,用来将文档片段向量化。
- 要有一个文档加载和切分的模块,用来加载文档并切分成文档片段。
- 要有一个数据库来存放文档片段和对应的向量表示。
- 要有一个检索模块,用来根据 Query (问题)检索相关的文档片段。
- 要有一个大模型模块,用来根据检索出来的文档回答用户的问题。
借用教程以及下面的论文中的图描述整个流程为:
Retrieval-Augmented Generation for Large Language Models: A Survey
2、向量化
这部分主要是加载向量化模型,将文档片段向量化,即将一段文本映射为一个向量。
教程里面给出的是几种向量化模型的使用方式,有API的调用以及本地开源模型的应用,需要注意的是此处使用的向量化模型应与后面要使用的大模型应保持一致,即同时使用OpenAI的或者是同时使用InternLM。
教程中的实例是使用余弦相似度来计算两个向量的相似程度。
3、文档加载和切分
这部分主要是对已有的文本按照不同格式进行数据获取,读取文档部分的代码不想过多记录,主要记录下教程中的文档切分。(因为之前做过一段时间NLP,也对文档进行过切分,在附录也会浅浅记录下当时切分的逻辑想法,进行对比学习)
def get_chunk(cls, text: str, max_token_len: int = 600, cover_content: int = 150):
chunk_text = []
curr_len = 0
curr_chunk = ''
lines = text.split('\n') # 假设以换行符分割文本为行
for line in lines:
line = line.replace(' ', '')
line_len = len(enc.encode(line))
if line_len > max_token_len:
print('warning line_len = ', line_len)
if curr_len + line_len <= max_token_len:
curr_chunk += line
curr_chunk += '\n'
curr_len += line_len
curr_len += 1
else:
chunk_text.append(curr_chunk)
curr_chunk = curr_chunk[-cover_content:]+line
curr_len = line_len + cover_content
if curr_chunk:
chunk_text.append(curr_chunk)
return chunk_text
上述代码中,enc = tiktoken.get_encoding("cl100k_base"),即使用的是OpenAI开发的开源的快速token切分器tiktoken(需要import),cl100k_base是其中的一种编码类型。
max_token_len是设置的单个文本片段最大的token数目,chunk_text保存的是一系列长度尽可能接近的、数目小于max_token_len的文本片段列表,cover_content是相邻两个文本片段的重叠的token数目,用来保证检索的时候能够检索到相关的文档片段。
4、数据库 && 向量检索
这里使用了向量数据库。
向量数据库,也叫矢量数据库,主要用来存储和处理向量数据。图像、文本和音视频这种非结构化数据都可以通过某种变换或者嵌入学习转化为向量数据存储到向量数据库中,借助两个向量的距离或者是相似性,来实现对图像、文本和音视频的相似性搜索和检索。(是不是又有点像hash)
其主要特点就是高效存储与检索。
这部分源码基本通俗易懂,主要就简单单拎出来里面的查询函数:
def query(self, query: str, EmbeddingModel: BaseEmbeddings, k: int = 1) -> List[str]:
# 获取查询的向量
query_vector = EmbeddingModel.get_embedding(query)
# for循环,余弦相似度计算查询语句的向量与数据库中的向量相似度
result = np.array([self.get_similarity(query_vector, vector)
for vector in self.vectors])
# 返回前k个相似度最高的文本
return np.array(self.document)[result.argsort()[-k:][::-1]].tolist()
5、大模型
这部分源码没有太多惊艳的值得深入思索的地方,就是调用一些API或者是加载本地的开源模型。
6、整合搭建
结合上面的几个模块,剩下的就是搭积木,完成整个流程:
from RAG.VectorBase import VectorStore
from RAG.utils import ReadFiles
from RAG.LLM import OpenAIChat, InternLMChat
from RAG.Embeddings import JinaEmbedding, ZhipuEmbedding
# 没有保存数据库
# 获得data目录下的所有文件内容并分割
docs = ReadFiles('./data').get_content(max_token_len=600, cover_content=150)
# 向量化
vector = VectorStore(docs)
# 创建EmbeddingModel
embedding = ZhipuEmbedding()
vector.get_vector(EmbeddingModel=embedding)
# 将向量和文档内容保存到storage目录下,下次再用就可以直接加载本地的数据库
vector.persist(path='storage')
"""
# 保存本地数据库
vector = VectorStore()
# 本地数据加载
vector.load_vector('./storage') # 加载本地的数据库
"""
question = 'git的原理是什么?'
# 查询
content = vector.query(question, model='zhipu', k=1)[0]
# 引入大模型
chat = InternLMChat(path='model_path')
# Tiny-RAG
print(chat.chat(question, [], content))