大模型之RAG:基于向量检索的理论与实战,对比关键字检索方案

科技   2024-11-20 12:01   上海  
点击蓝字,立即关注




前言



RAG系列的讲解,我们之前和大家分享了RAG的流程文档切基于关键字检索的方案。


在关键字检索的认识与实战一文中,我们讲到了基于关键字检索的局限性关键字检索可能会受到一些问题的影响,例如同义词、拼写错误等,这可能会导致一些相关的文档被漏掉或者一些不相关的文档被检索到。


今天再来和大家一起分享基于向量检索的方案与实战,再结合关键字检索方案做一下多维度的对比。


让我们对RAG的实现方案能够加深一些理解,在面对不同场景中,选择合适的方案。




向量检索的定义与原理



什么是向量


向量是一种有大小和方向的数学对象。它可以表示为从一个点到另一个点的有向线段。例如,二维空间中的向量可以表示为 (𝑥,𝑦)(𝑥,𝑦),表示从原点 (0,0)(0,0) 到点 (𝑥,𝑦)(𝑥,𝑦) 的有向线段。


以此类推,我可以用一组坐标 (𝑥0,𝑥1,…,𝑥𝑁−1)(𝑥0,𝑥1,…,𝑥𝑁−1) 表示一个 𝑁𝑁 维空间中的向量,𝑁𝑁 叫向量的维度。


文本向量(Text Embeddings)

  1. 将文本转成一组 𝑁𝑁 维浮点数,即文本向量又叫 Embeddings

  2. 向量之间可以计算距离,距离远近对应语义相似度大小


文本向量是怎么得到的

  1. 构建相关(正立)与不相关(负例)的句子对儿样本;

  2. 训练双塔式模型,让正例间的距离小,负例间的距离大;


向量间的相似度计算


我们用检索关键词和一组文本的样例来看下效果

余弦距离和欧氏距离的核心逻辑

def cos_sim(a, b):    '''余弦距离 -- 越大越相似'''    return dot(a, b)/(norm(a)*norm(b))

def l2(a, b): '''欧氏距离 -- 越小越相似''' x = np.asarray(a)-np.asarray(b) return norm(x) def get_embeddings(texts, model="text-embedding-ada-002", dimensions=None): '''封装 OpenAI 的 Embedding 模型接口''' if model == "text-embedding-ada-002": dimensions = None if dimensions: data = client.embeddings.create( input=texts, model=model, dimensions=dimensions).data else: data = client.embeddings.create(input=texts, model=model).data return [x.embedding for x in data] # query = "国际争端"
# 且能支持跨语言query = "global conflicts"
documents = [ "联合国就苏丹达尔富尔地区大规模暴力事件发出警告", "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判", "日本岐阜市陆上自卫队射击场内发生枪击事件 3人受伤", "国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营", "我国首次在空间站开展舱外辐射生物学暴露实验",]


执行并输出结果

query_vec = get_embeddings([query])[0]doc_vecs = get_embeddings(documents)
print("Query与自己的余弦距离: {:.2f}".format(cos_sim(query_vec, query_vec)))print("Query与Documents的余弦距离:")for vec in doc_vecs: print(cos_sim(query_vec, vec))
print()
print("Query与自己的欧氏距离: {:.2f}".format(l2(query_vec, query_vec)))print("Query与Documents的欧氏距离:")for vec in doc_vecs:    print(l2(query_vec, vec))


我们来看下执行的效果:

Query与自己的余弦距离: 1.00QueryDocuments的余弦距离:0.7622749944010915(越大越相似)0.75630381064935840.74266658025790380.70792736996080060.7254355321045072
Query与自己的欧氏距离: 0.00QueryDocuments的欧氏距离:0.6895288502682277(越小越相似)0.69813496379987690.71740287464922770.76429398336368290.7410323668625171


向量数据库


向量数据库(Vector Database),也叫矢量数据库,主要用来存储和处理向量数据。


再结合刚才我们对向量定义的描述,图像、文本和音视频这种非结构化数据都可以通过某种变换或者嵌入学习转化为向量数据存储到向量数据库中,从而实现对图像、文本和音视频的相似性搜索和检索。


这意味着您可以使用向量数据库根据语义或上下文含义查找最相似或相关的数据,而不是使用基于精确匹配或预定义标准查询数据库的传统方法。也就是我们提到的关键字检索的局限性。


向量数据库的特点


这里我们为了方便使用向量数据库完成向量检索的方案,简单介绍下向量数据库的特点:


向量数据库的主要特点是高效存储与检索。利用索引技术和向量检索算法能实现高维大数据下的快速响应。


向量数据库也是一种数据库,除了要管理向量数据外,还是支持对传统结构化数据的管理。实际使用时,有很多场景会同时对向量字段和结构化字段进行过滤检索,这对向量数据库来说也是一种挑战。


严格来说数据向量化本不属于向量数据库,但是数据向量化又是一项很重要的工作,为了流程的完整性暂且放进去。


区别与传统数据库主要有以下几个地方不相同数据向量化,向量检索和相似度计算。


chromadb的简单介绍


之所以介绍一下chromadb,下面我们的实战demo就是基于chromadb来实现。


Chroma的目标是帮助用户更加便捷地构建大模型应用,更加轻松的将知识(knowledge)、事实(facts)和技能(skills)等我们现实世界中的文档整合进大模型中。


Chroma提供的工具:

  1. 存储文档数据和它们的元数据:store embeddings and their metadata

  2. 嵌入:embed documents and queries

  3. 搜索:search embeddings


流向量数据库功能对比


由于大模型的火热,现在市面上的向量数据库众多,主流的向量数据库对比如下所示:




一个基于文档向量检索的RAG实战例子



我们再回顾RAG的基本流程,对照如下例子,大家就可以更好理解了。


RAG系统搭建的基本流程

  1. 准备对应的垂域资料

  2. 文档的读取解析,进行文档切分

  3. 将分割好的文本灌入检索引擎(向量数据库)

  4. 封装检索接口

  5. 构建流程:Query -> 检索 -> Prompt -> LLM -> 回复

  6. 文档加载


def extract_text_from_pdf(filename,page_numbers=None,min_line_length=10):    """从 PDF 文件中(按指定页码)提取文字"""    paragraphs = []    buffer = ''    full_text = ''    # 提取全部文本    for i, page_layout in enumerate(extract_pages(filename)):        # 如果指定了页码范围,跳过范围外的页        if page_numbers is not None and i not in page_numbers:            continue        for element in page_layout:            if isinstance(element, LTTextContainer):                full_text += element.get_text() + '\n'    # 按空行分隔,将文本重新组织成段落    lines = full_text.split('\n')    for text in lines:        if len(text) >= min_line_length:            buffer += (' '+text) if not text.endswith('-') else text.strip('-')        elif buffer:            paragraphs.append(buffer)            buffer = ''    if buffer:        paragraphs.append(buffer)    return paragraphs


文档切割(交叠切割防止问题的答案跨两个片段,使上下文更完整)

def split_text(paragraphs,chunk_size=300,overlap_size=100):    """按指定 chunk_size 和 overlap_size 交叠割文本"""    sentences = [s.strip() for p in paragraphs for s in sent_tokenize(p)]    chunks = []    i= 0    while i < len(sentences):        chunk = sentences[i]        overlap = ''        prev_len = 0        prev = i - 1        # 向前计算重叠部分        while prev >= 0 and len(sentences[prev])+len(overlap) <= overlap_size:            overlap = sentences[prev] + ' ' + overlap            prev -= 1        chunk = overlap+chunk        next = i + 1        # 向后计算当前chunk        while next < len(sentences) and len(sentences[next])+len(chunk) <= chunk_size:            chunk = chunk + ' ' + sentences[next]            next += 1        chunks.append(chunk)        i = next    return chunks


向量化(这里使用openai的向量化模型)

def get_embedding(text, model="text-embedding-ada-002"):    """封装 OpenAI 的 Embedding 模型接口"""    return openai.Embedding.create(input=[text], model=model)['data'][0]['embedding']


灌入向量库(使用chromadb)

def __init__(self, name="demo"):        self.chroma_client = chromadb.Client(Settings(allow_reset=True))        self.chroma_client.reset()        self.name = name        self.collection = self.chroma_client.get_or_create_collection(name=name)
def add_documents(self, documents): self.collection.add( embeddings=[get_embedding(doc) for doc in documents], documents=documents, metadatas=[{"source": self.name} for _ in documents], ids=[f"id_{i}" for i in range(len(documents))]        )


检索向量数据库

def search(self, query, top_n):        """检索向量数据库"""        results = self.collection.query(            query_embeddings=[get_embedding(query)],            n_results=top_n        )        return results['documents'][0]


将检索数据带入提示词

def build_prompt(template=prompt_template, **kwargs):    """将 Prompt 模板赋值"""    prompt = template    for k, v in kwargs.items():        if isinstance(v, str):            val = v        elif isinstance(v, list) and all(isinstance(elem, str) for elem in v):            val = '\n'.join(v)        else:            val = str(v)        prompt = prompt.replace(f"__{k.upper()}__", val)    return prompt


调用大模型

def get_completion(prompt, context, model="gpt-3.5-turbo"):    """封装 openai 接口"""    messages = context + [{"role": "user", "content": prompt}]    response = openai.ChatCompletion.create(        model=model,        messages=messages,        temperature=0,  # 模型输出的随机性,0 表示随机性最小    )    return response.choices[0].message["content"]




向量检索与关键字检索的对比



总的来说,向量检索更适合处理复杂的语义匹配问题,而关键字检索则更适合简单的关键字匹配需求。




总结




本文的分享结束,也代表着我们对向量检索的RAG如何实现,向量化以及向量数据库,同时用一个实战的例子讲解了向量检索的RAG如何完成。


再结合之前的文章我们对于关键字检索的RAG的讲解,我们能够充分的了解RAG的两种实现方式,以及他们之间的对比。


RAG的核心在于检索增强,而检索增强的主要手段是知识库,我们引入外部知识库可以是ES类似的关键字检索,也可以是Chroma类似的向量检索


RAG已经是AIGC当中热门又尤为重要的一个方向,希望我们对于我们提高我们大模型的性能有所帮助。


END


链接:https://juejin.cn/post/7388025457822744586

本文为51Testing经授权转载,转载文章所包含的文字来源于作者。如因内容或版权等问题,请联系51Testing进行删除




点点赞
点分享
点在看

51Testing软件测试网
博为峰20周年,青春正当燃,一起向未来! 博为峰51Testing软件测试网提供各种线上招聘、线上课程等网络服务,出版软件测试系列丛书及电子杂志,组织线上技术交流活动;同时还举办多种线下公益活动,如软件测试沙龙、软件测试专场招聘会等。
 最新文章