wow-rag学习|打造个人RAG,零基础也能快速搭建检索生成系统

科技   2024-12-03 08:38   浙江  

本文为Datawhale 开源项目wow-rag的学习笔记与分享,仅供参考,如有错误描述,请评论指正,感谢。

开源项目地址:
github.com/datawhalechina/wow-rag

欢迎大家点击关注我,学习更多AI相关知识。

获取免费大模型APIKEY

这里我们选择使用 智谱 的,操作很简单,访问下面链接,注册登录。

https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys

然后按图示步骤操作,如果你登录后不在APIKEY生成页面,也可以先点击右上角的钥匙按钮进入,然后点击添加新的API Key

给一个名字,方便后面区分(比如后面有其他业务或者学习使用其他的API Key ,后续可以区分调用来源)。

创建完成后,页面上点击复制即可。(看图示操作)

注意智谱的 embedding-2 模型不免费提供,需要先访问下方链接购买下,测试买这个7.5元的即可。(“图”方便就不找免费的代替了,欢迎评论区分享)

https://open.bigmodel.cn/tokenspropay?productIds=product-074

模型测试调用

安装需要的包:

pip install faiss-cpu scikit-learn scipy openai

首先创建一个 client,后面可以使用 client 调用各种接口模型。

'''
代码来源:github.com/datawhalechina/wow-rag
'''

# 智谱调用
from openai import OpenAI

api_key = "上一步骤复制的api_key"
base_url = "https://open.bigmodel.cn/api/paas/v4/"
chat_model = "glm-4-flash"
emb_model = "embedding-2"

client = OpenAI(
    api_key = api_key,
    base_url = base_url
)

对话模型使用的是 glm-4-flash,嵌入式模型模型使用的 embedding-2

调用对话模型 glm-4-flash

很固定的格式,调用了 client.chat.completions.create函数,主要参数有:

  • model,使用的模型
  • messages,消息列表
  • stream,是否使用流式输出
'''
代码来源:github.com/datawhalechina/wow-rag
'''

# 使用 OpenAI 的 Chat Completions API 创建一个聊天完成请求
prompt = "你好,你可以做什么,列5点"
response = client.chat.completions.create(
    model=chat_model,  # 填写需要调用的模型名称
    messages=[
        {"role""user""content": prompt},
    ],
    stream=True,
)
# 流式输出
if response:
    # 遍历响应中的每个块
    for chunk in response:
        # 获取当前块的内容
        content = chunk.choices[0].delta.content
        # 如果内容存在
        if content:
            # 打印内容,并刷新输出缓冲区
            print(content, end='', flush=True)

调用嵌入式模型 embedding-2

首先 clone github.com/datawhalechina/wow-rag 内容,作为源料,

git clone https://github.com/datawhalechina/wow-rag

然后读取 wow-rag/docs/第1课-手搓一个土得掉渣的RAG.md 文档作为测试 embedding 源料,读取后再使用 for 循环进行了简单的分块处理,设置每个文本块的大小为 150 个字符,结果存储在 chunks 变量。

'''
代码来源:github.com/datawhalechina/wow-rag
'''

with open("./wow-rag/docs/第1课-手搓一个土得掉渣的RAG.md"as f:
    embedding_text = f.read()
# 设置每个文本块的大小为 150 个字符
chunk_size = 150
# 使用列表推导式将长文本分割成多个块,每个块的大小为 chunk_size
chunks = [embedding_text[i:i + chunk_size] for i in range(0, len(embedding_text), chunk_size)]

接下来就是主要代码了,核心逻辑流程如下:

  • 嵌入生成
    • 使用 OpenAI API 将文本块转换为嵌入向量,并将结果存入 embeddings 列表。
  • 归一化处理
    • 调用 normalize 方法对嵌入向量进行归一化处理。
  • 向量存储
    • 使用 faiss.IndexFlatIP 创建一个用于内积相似性计算的索引。
    • 将归一化后的向量存入索引。
  • 索引查询
    • 使用 index.ntotal 获取索引中的向量总数。

输出

  • 打印索引中的向量总数,验证是否成功存储。

Tips 为什么要归一化?
若向量未归一化,则内积值不仅受方向影响,还受模长的影响。向量经过归一化后模为1,则内积等价于余弦相似性,确保仅计算方向的相似性。

'''
代码来源:github.com/datawhalechina/wow-rag
'''

# 用于对数据进行归一化处理,使每个嵌入向量的模为1
from sklearn.preprocessing import normalize
import numpy as np
# 一个高效的相似性搜索库,用于存储和检索向量
import faiss
import time

# 初始化一个空列表来存储每个文本块的嵌入向量
embeddings = []

print("开始向量化存储~")
t1 = time.time()
# 遍历每个文本块
for chunk in chunks:
    # 使用 OpenAI API 为当前文本块创建嵌入向量
    response = client.embeddings.create(
        model=emb_model,
        input=chunk,
    )
    
    # 将嵌入向量添加到列表中
    embeddings.append(response.data[0].embedding)

# 使用 sklearn 的 normalize 函数对嵌入向量进行归一化处理
normalized_embeddings = normalize(np.array(embeddings).astype('float32'))

# 获取嵌入向量的维度
d = len(embeddings[0])

# 创建一个 Faiss 索引,用于存储和检索嵌入向量
index = faiss.IndexFlatIP(d)

# 将归一化后的嵌入向量添加到索引中
index.add(normalized_embeddings)

print(f"完成向量化存储~耗时:{round(time.time()-t1, 2)}s")
t1 = time.time()

# 获取索引中的向量总数
n_vectors = index.ntotal

print(n_vectors)

综合使用

首先写一个函数match_text用于找到与输入文本最相似的前k个文本块。代码运行逻辑:

  • 问题转化为嵌入向量: 利用 OpenAI 接口代码调用嵌入模型(embedding-2),将输入文本块转化为嵌入向量
  • Faiss 索引: 归一化处理后,通过 index.search 函数高效检索最相似的K个嵌入向量
  • 返回结果: 遍历打印匹配的嵌入向量对应的内容,并返回
'''
代码来源:github.com/datawhalechina/wow-rag
'''

from sklearn.preprocessing import normalize
def match_text(input_text, index, chunks, k=2):
    """
    在给定的文本块集合中,找到与输入文本最相似的前k个文本块。

    参数:
        input_text (str): 要匹配的输入文本。
        index (faiss.Index): 用于搜索的Faiss索引。
        chunks (list of str): 文本块的列表。
        k (int, optional): 要返回的最相似文本块的数量。默认值为2。

    返回:
        str: 格式化的字符串,包含最相似的文本块及其相似度。
    """

    # 确保k不超过文本块的总数
    k = min(k, len(chunks))

    # 使用OpenAI API为输入文本创建嵌入向量
    response = client.embeddings.create(
        model=emb_model,
        input=input_text,
    )
    # 获取输入文本的嵌入向量
    input_embedding = response.data[0].embedding
    # 对输入嵌入向量进行归一化处理
    input_embedding = normalize(np.array([input_embedding]).astype('float32'))

    # 在索引中搜索与输入嵌入向量最相似的k个向量
    distances, indices = index.search(input_embedding, k)
    # 初始化一个字符串来存储匹配的文本
    matching_texts = ""
    # 遍历搜索结果
    for i, idx in enumerate(indices[0]):
        # 打印每个匹配文本块的相似度和文本内容
        print(f"similarity: {distances[0][i]:.4f}\nmatching text: \n{chunks[idx]}\n")
        # 将相似度和文本内容添加到匹配文本字符串中
        matching_texts += f"similarity: {distances[0][i]:.4f}\nmatching text: \n{chunks[idx]}\n"

    # 返回包含匹配文本块及其相似度的字符串
    return matching_texts

调用测试:

input_text = "教程使用了哪些模型"
matching_texts = match_text(input_text, index=index, chunks=chunks, k=3)
print(matching_texts)

有了检索函数match_text,接下来我们写一个简单的调用即可完成一个简单的RAG检索demo,运行逻辑:

  • 输入要检索的问题
  • 调用 match_text 从向量库中查找出相似度最高的3个记录
  • 将查找出的内容和问题一起丢给大模型,让 大模型 基于查找内容进行回复
'''
代码来源:github.com/datawhalechina/wow-rag
'''

def get_completion_stream(prompt):
    """
    使用 OpenAI 的 Chat Completions API 生成流式的文本回复。

    参数:
        prompt (str): 要生成回复的提示文本。

    返回:
        None: 该函数直接打印生成的回复内容。
    """

    # 使用 OpenAI 的 Chat Completions API 创建一个聊天完成请求
    response = client.chat.completions.create(
        model=chat_model,  # 填写需要调用的模型名称
        messages=[
            {"role""user""content": prompt},
        ],
        stream=True,
    )
    # 如果响应存在
    if response:
        # 遍历响应中的每个块
        for chunk in response:
            # 获取当前块的内容
            content = chunk.choices[0].delta.content
            # 如果内容存在
            if content:
                # 打印内容,并刷新输出缓冲区
                print(content, end='', flush=True)

input_text = "教程使用了哪些模型"
matching_texts = match_text(input_text, index=index, chunks=chunks, k=3)
prompt = f"""
根据找到的文档
{matching_texts}
生成
{input_text}
的答案,尽可能使用文档语句的原文回答。不要复述问题,直接开始回答,注意使用中文回复。
"""

get_completion_stream(prompt)

恭喜你,跟着跑到这里,你就完成了一个最简单的 RAG 项目,通过这个项目你可以很直观的了解到 RAG检索增强生成系统的运行流程、核心思路,这看似很简单,实际还有很多需要优化的地方,比如:怎么切分导入文本才是最优的 、数据如何长期存储、导入数据向量化操作如何并发等等。

欢迎大家去wow-rag项目学习后面更多内容,有什么问题或者想法也欢迎大家评论区交流反馈,也可以直接加我微信交流。

https://github.com/datawhalechina/wow-rag

今天就分享到这,我是老表,想要成为一名终身学习者。

阅读推荐

《AI群星闪耀时》是一部由清华大学刘知远团队倾力打造的作品,讲述了28位人工智能先驱的传奇人生和他们对AI发展的巨大贡献。本书以通俗易懂的语言,将科技故事描绘得热血沸腾,是了解人工智能发展史的绝佳读物。


翻开它,感受科技之光如何照亮人类历史。

拓展学习:内积是什么?怎么计算的

1. 内积索引的概念

Faiss 是一个高效的相似性搜索库,其中 内积索引 (IndexFlatIP) 是一种基于内积的向量相似性搜索方法。

  • 内积(Inner Product):内积是一种向量相似性的度量方法,对于两个向量 (a) 和 (b),内积定义为:

    内积的值越大,表示两个向量的方向越接近(假设向量已归一化)。

  • Faiss 内积索引的逻辑

    • 存储多个高维向量。
    • 在查询时,比较查询向量和索引中存储的向量的内积值,返回最相似的向量。

2. 内积索引的转换逻辑

内积索引的转换和使用逻辑分为以下几步:

2.1 数据准备和归一化

在内积索引中,向量的模会影响内积值的大小:

  • 若向量未归一化,则内积值不仅受方向影响,还受模长的影响。
  • 如果向量经过归一化(模为1),则内积等价于余弦相似性。

因此,案例代码中使用了 sklearn.preprocessing.normalize 将嵌入向量归一化,确保仅计算方向的相似性。

2.2 向量存储

  • 创建一个内积索引 faiss.IndexFlatIP(d),其中 d 是向量的维度。
  • 使用 index.add(normalized_embeddings) 将归一化后的向量存入索引。

2.3 向量检索

在检索阶段:

  1. 提供一个查询向量 (q)。
  2. Faiss 会计算查询向量与所有索引向量的内积:
    其中 (x_i) 是索引中的向量。
  3. 返回内积值最高的 (k) 个向量及其索引位置。

3. 使用场景

内积索引在许多机器学习和自然语言处理任务中非常常用,以下是几个典型场景:

3.1 语义搜索

  • 场景:基于用户输入的查询语句,找到最相关的文档。
  • 逻辑
    • 将文档和查询语句转化为嵌入向量。
    • 在索引中搜索与查询语句嵌入向量最接近的文档嵌入向量。
  • 应用
    • 搜索引擎(如知识库检索、FAQ 系统)。
    • 嵌入式问答。

3.2 推荐系统

  • 场景:基于用户的兴趣向量,找到与其兴趣最相似的产品或内容。
  • 逻辑
    • 将用户历史行为转化为嵌入向量。
    • 在索引中检索与用户兴趣向量最相似的内容嵌入向量。
  • 应用
    • 商品推荐(如电商平台)。
    • 内容推荐(如电影、音乐、新闻)。

3.3 聚类分析

  • 场景:在高维向量空间中找到相似的向量组。
  • 逻辑
    • 通过 Faiss 的内积索引,快速找到向量间的邻居。
  • 应用
    • 社交网络分析(如好友推荐)。
    • 数据分组(如特征相似样本的分簇)。

3.4 异常检测

  • 场景:识别数据集中异常或不相似的向量。
  • 逻辑
    • 计算每个向量的邻居。
    • 若向量的内积分数低于某一阈值,则标记为异常。
  • 应用
    • 网络安全中的异常检测。
    • 数据清洗中的异常识别。

4. 如何使用存储的数据

4.1 插入更多向量

  • 可以通过 index.add(new_vectors) 动态添加新的嵌入向量,扩展索引的存储。

4.2 查询相似向量

  • 使用 index.search(query, k) 检索最相似的 (k) 个向量:
    D, I = index.search(query_vector, k)
    • D 是内积分数。
    • I 是索引中相似向量的编号。

4.3 保存和加载索引

  • 保存索引:将索引存储到文件以供后续使用:
    faiss.write_index(index, "index_file_name")
  • 加载索引:从文件加载索引以快速恢复:
    index = faiss.read_index("index_file_name")

4.4 删除数据

原始的 IndexFlatIP 不支持删除操作。如果需要支持删除,可以使用 faiss.IndexIDMapfaiss.IndexIVFFlat 这样的高级结构。


5. 总结

  • 内积索引适合归一化后的嵌入向量,用于高效的相似性检索
  • 在语义搜索、推荐系统、异常检测等场景中非常有用
  • 存储的数据可以动态更新,并支持快速查询和持久化保存

参考

  • [1 ] Datawhale wow-rag 教程开源项目:https://github.com/datawhalechina/wow-rag
  • [2] 向量的内积(点乘):https://www.cnblogs.com/gxcdream/p/7597865.html
  • [3] Faiss index进阶操作:https://blog.csdn.net/u013066730/article/details/106298939
  • [4] 向量内积在机器学习中的重要性:https://juejin.cn/post/7320855522307489843
  • [5] 使用检索增强生成构建语义搜索:https://medium.com/orkes/rag-explained-using-retrieval-augmented-generation-to-build-semantic-search-97a3495df892

扫码加老表微信,进入学习交流群

简说Python
号主老表,自学,分享Python,SQL零基础入门、数据分析、数据挖掘、机器学习优质文章以及学习经验。
 最新文章