欢迎来到啾啾的博客🐱。
记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。
有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。
1 引言
在上一篇中,我们了解到RAG的第一阶段,资料分块。了解到了分块的原因,大小的权衡与常用库。
本篇继续了解RAG的下一个步骤,向量化。
2 向量化:让机器看懂语言
RAG流程中,我们需要使用Embedding模型来充当“翻译”,将人类的语言(文本)精准地翻译成机器能够理解和计算的语言(向量)。
2.1 转换文本为坐标
文本经Embedding模型转换后成为一串数字坐标,比如:
[0.1, 0.9, 0.2, ...]
关键原则:在语义上相似的文本,它们转换后的向量,在数学空间中的距离也更近。
假设我们有三段文本需要转换:
- 文本A: "今天天气真好"文本B: "今天阳光明媚"文本C: "我喜欢吃披萨"
假设转换后的向量(Numpy数组)为:
import numpy as np# 文本A: "今天天气真好" 的向量vector_a = np.array([0.9, 0.8, 0.1, 0.2])# 文本B: "今天阳光明媚" 的向量vector_b = np.array([0.8, 0.9, 0.1, 0.3])# 文本C: "我喜欢吃披萨" 的向量vector_c = np.array([0.1, 0.2, 0.9, 0.8])
点积的值越大,通常表示两个向量在方向上越接近,也就是越“相似”。
我们继续使用Numpy计算”点积“:
a_b = vector_a @ vector_bprint(a_b) # 1.5100000000000002a_c = vector_a @ vector_cprint(a_c) # 0.5000000000000001b_c = np.dot(vector_b, vector_c)print(b_c) # 0.5900000000000001
很容易看出来文本A与文本B相似度更高。
当一个用户提问时,简单的RAG系统可以这样做:
- 把用户的问题(比如“今天天气怎么样?”)也转换成一个向量。然后用这个“问题向量”去和数据库里成千上万个“文本块向量”逐一计算点积(或者更高效的相似度算法)。最后,选出点积得分最高的那几个文本块,作为“开卷考试”的参考资料,喂给LLM。
实际场景中一般使用嵌入模型。
3 使用sentence-transformers在本地生成向量
使用适合中文处理的Embedding模型:bge-small-zh-v1.5。
# 1. 从库中导入 SentenceTransformer 类from sentence_transformers import SentenceTransformerimport numpy as np# --- 向量化部分 ---# 2. 加载本地模型。# 第一次运行时,它会自动从Hugging Face下载模型文件到您的本地缓存中。# 这可能需要一些时间,取决于您的网络。model_name = 'BAAI/bge-small-zh-v1.5'print(f"正在加载本地模型: {model_name}...")model = SentenceTransformer(model_name)print("模型加载完成。")# 3. 准备一些待转换的文本块text_chunks = [ "RAG的核心思想是开卷考试。", "RAG是一种结合了检索与生成的先进技术。", # 与上一句意思相近 "今天天气真好,万里无云。" # 与前两句意思完全不同]# 4. 使用 model.encode() 方法进行向量化。# 它会返回一个Numpy数组的列表。vectors = model.encode(text_chunks)# --- 观察与计算部分 ---# 5. 观察向量的形状 (shape)print("\n--- 向量信息 ---")# (句子数量, 每个向量的维度)print(f"生成向量的形状: {vectors.shape}") # 6. 计算相似度:我们来计算第一个句子和另外两个句子的相似度# 我们将使用余弦相似度,这是衡量向量方向一致性的标准方法。# 公式: (A·B) / (||A|| * ||B||)def cosine_similarity(v1, v2): dot_product = np.dot(v1, v2) norm_v1 = np.linalg.norm(v1) # linalg.norm 计算向量的模长 norm_v2 = np.linalg.norm(v2) return dot_product / (norm_v1 * norm_v2)similarity_1_vs_2 = cosine_similarity(vectors[0], vectors[1])similarity_1_vs_3 = cosine_similarity(vectors[0], vectors[2])print("\n--- 相似度计算结果 ---")print(f"句子1 vs 句子2 (语义相近) 的相似度: {similarity_1_vs_2:.4f}")print(f"句子1 vs 句子3 (语义无关) 的相似度: {similarity_1_vs_3:.4f}")
3.1 向量形状
print(f"生成向量的形状: {vectors.shape}") 生成向量的形状: (3, 512)# (样本数,维度)
向量的形状 (样本数, 维度) 是对我们向量化后数据集的一个宏观描述。维度 是由你选择的 Embedding模型本身决定 的,它代表了模型的复杂度和表达能力。
当我们打印出 vectors.shape 并看到 (3, 512) 时,这个元组 (3, 512) 告诉我们两件至关重要的事:
- 第一个数字 (3): 代表我们处理了多少个独立的文本项。在这里,它对应我们输入的列表 text_chunks 中有3个句子。如果我们将1000个文本块输入 model.encode(),这个数字就会是1000。它代表了我们数据集的 样本数量。第二个数字 (512): 这是更关键的那个,它代表了 每个向量的维度 (Dimension)。这意味着我们选择的 bge-small-zh-v1.5 模型,会将任何输入的文本,都映射到一个固定的、512维的超空间中的一个点。
512维空间在数学上有巨大的容量,可以编码非常丰富和细微的语义信息。一个文本的最终向量,就是它在所有这些维度上“得分”的组合。这使得模型能够区分出“苹果公司发布了新手机”和“我喜欢吃苹果”这样非常细微的语义差别。
3.2 “余弦相似度”为什么是比较相似度的首选?
# 6. 计算相似度:我们来计算第一个句子和另外两个句子的相似度# 我们将使用余弦相似度,这是衡量向量方向一致性的标准方法。# 公式: (A·B) / (||A|| * ||B||)def cosine_similarity(v1, v2): dot_product = np.dot(v1, v2) norm_v1 = np.linalg.norm(v1) # linalg.norm 计算向量的模长 norm_v2 = np.linalg.norm(v2) return dot_product / (norm_v1 * norm_v2)
在绝大多数RAG和语义搜索场景中,余弦相似度是“事实上的标准”,但它不是唯一的选择。余弦相似度公式:
为什么余弦相似度是首选?
只关心方向,不关心大小(模长)在语义空间中,我们认为两个句子的意思是否相近,主要取决于它们向量的 方向 是否一致,而与向量的长度(模长)关系不大。余弦相似度则会正确地判断出它们“方向一致”,相似度很高。它对文本长度和用词强度不那么敏感,这正是我们想要的。
归一化,结果直观余弦相似度的值被天然地归一化到 [-1, 1] 的区间内(对于非负的Embedding,通常是 [0, 1])。1 代表完全相同,0 代表完全无关,-1 代表方向完全相反。这个结果非常直观,易于比较。
3.2.1 归一化
在使用SentenceTransformer时,model.encode() 方法生成嵌入向量时,可以通过参数 normalize_embeddings 控制是否归一化。
vectors = model.encode(text_chunks, normalize_embeddings=True)
bge-small-zh-v1.5 模型在 model.encode() 中默认将 normalize_embeddings=True,即生成的嵌入向量已经是单位向量(模长为 1)。如果向量已归一化,余弦相似度公式简化为:
即,余弦等于点积。归一化后,使用点积计算快于余弦相似度计算。上述代码是为了演示,其实可以替换为:
# 引入封装好的工具类similarities = util.cos_sim(vectors, vectors)print(f"句子1 vs 句子2: {similarities[0][1]:.4f}")print(f"句子1 vs 句子3: {similarities[0][2]:.4f}")
util.cos_sim 支持批量计算所有向量对的相似度,避免逐个计算,提高效率。
3.3 向量比较方法
其他方法还有第二章演示向量比较的点积,另外还有欧式距离。
3.3.1 点积与余弦相似度
余弦相似度是点积的归一化形式:
若向量已归一化(),则点积等于余弦相似度
3.3.2 点积与欧氏距离
对于两个向量,欧氏距离与点积有以下关系(假设向量已归一化或未归一化):
若向量归一化(),则:因此,欧氏距离与余弦相似度呈反比:余弦相似度越大,欧氏距离越小。
3.3.3 余弦相似度与欧氏距离
对于归一化向量,余弦相似度为 1 时,欧氏距离为 0;余弦相似度为 0 时,欧氏距离为 。欧氏距离综合考虑长度和方向差异,而余弦相似度只关注方向。
from sentence_transformers import SentenceTransformer, utilimport torchimport numpy as np# 加载模型model = SentenceTransformer('BAAI/bge-small-zh-v1.5')# 输入句子sentences = ["今天天气很好。", "天气晴朗适合户外活动。"]# 生成嵌入(默认归一化)embeddings = model.encode(sentences, normalize_embeddings=True)# 计算点积dot_product = torch.dot(torch.tensor(embeddings[0]), torch.tensor(embeddings[1]))# 计算余弦相似度cosine_similarity = util.cos_sim(embeddings, embeddings)[0][1]# 计算欧氏距离euclidean_distance = np.linalg.norm(embeddings[0] - embeddings[1])print(f"点积: {dot_product:.4f}")print(f"余弦相似度: {cosine_similarity:.4f}")print(f"欧氏距离: {euclidean_distance:.4f}")
输出为:
点积: 0.6524余弦相似度: 0.6524欧氏距离: 0.8337