欢迎来到啾啾的博客🐱。
记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。
有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。
1 引言
在上一篇中,我们了解到RAG的第一阶段,资料分块。 了解到了分块的原因,大小的权衡与常用库。
本篇继续了解RAG的下一个步骤,向量化。
2 向量化:让机器看懂语言
RAG流程中,我们需要使用Embedding模型来充当"翻译",将人类的语言(文本)精准地翻译成机器能够理解和计算的语言(向量)。
2.1 转换文本为坐标
文本经Embedding模型转换后成为一串数字坐标,比如:
txt
[0.1, 0.9, 0.2, ...]
关键原则:在语义上相似的文本,它们转换后的向量,在数学空间中的距离也更近。
假设我们有三段文本需要转换:
- 文本A: "今天天气真好"
- 文本B: "今天阳光明媚"
- 文本C: "我喜欢吃披萨"
假设转换后的向量(Numpy数组)为:
python
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计算"点积":
python
a_b = vector_a @ vector_b
print(a_b) # 1.5100000000000002
a_c = vector_a @ vector_c
print(a_c) # 0.5000000000000001
b_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。
python
# 1. 从库中导入 SentenceTransformer 类
from sentence_transformers import SentenceTransformer
import 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 向量形状
python
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 "余弦相似度"为什么是比较相似度的首选?
python
# 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和语义搜索场景中,余弦相似度是"事实上的标准",但它不是唯一的选择。 余弦相似度公式: <math xmlns="http://www.w3.org/1998/Math/MathML"> Cosine Similarity = cos θ = v 1 ⋅ v 2 ∥ v 1 ∥ ∥ v 2 ∥ \text{Cosine Similarity} = \cos\theta = \frac{\mathbf{v1} \cdot \mathbf{v2}}{\|\mathbf{v1}\| \|\mathbf{v2}\|} </math>Cosine Similarity=cosθ=∥v1∥∥v2∥v1⋅v2
为什么余弦相似度是首选?
-
只关心方向,不关心大小(模长) 在语义空间中,我们认为两个句子的意思是否相近,主要取决于它们向量的 方向 是否一致,而与向量的长度(模长)关系不大。 余弦相似度则会正确地判断出它们"方向一致",相似度很高。它对文本长度和用词强度不那么敏感,这正是我们想要的。
-
归一化,结果直观 余弦相似度的值被天然地归一化到 [-1, 1] 的区间内(对于非负的Embedding,通常是 [0, 1])。1 代表完全相同,0 代表完全无关,-1 代表方向完全相反。这个结果非常直观,易于比较。
3.2.1 归一化
在使用SentenceTransformer时,model.encode() 方法生成嵌入向量时,可以通过参数 normalize_embeddings 控制是否归一化。
python
vectors = model.encode(text_chunks, normalize_embeddings=True)
bge-small-zh-v1.5 模型在 model.encode() 中默认将 normalize_embeddings=True,即生成的嵌入向量已经是单位向量(模长为 1)。 如果向量已归一化,余弦相似度公式简化为: <math xmlns="http://www.w3.org/1998/Math/MathML"> cos θ = v 1 ⋅ v 2 \cos\theta = \mathbf{v1} \cdot \mathbf{v2} </math>cosθ=v1⋅v2
即,余弦等于点积。 归一化后,使用点积计算快于余弦相似度计算。 上述代码是为了演示,其实可以替换为:
python
# 引入封装好的工具类
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 点积与余弦相似度
余弦相似度是点积的归一化形式: <math xmlns="http://www.w3.org/1998/Math/MathML"> cos θ = a ⋅ b ∥ a ∥ ∥ b ∥ \cos\theta = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|} </math>cosθ=∥a∥∥b∥a⋅b
若向量已归一化( <math xmlns="http://www.w3.org/1998/Math/MathML"> ∥ a ∥ = ∥ b ∥ = 1 \|\mathbf{a}\| = \|\mathbf{b}\| = 1 </math>∥a∥=∥b∥=1),则点积等于余弦相似度
3.3.2 点积与欧氏距离
对于两个向量,欧氏距离与点积有以下关系(假设向量已归一化或未归一化): <math xmlns="http://www.w3.org/1998/Math/MathML"> ∥ a − b ∥ 2 = ∥ a ∥ 2 + ∥ b ∥ 2 − 2 a ⋅ b \|\mathbf{a} - \mathbf{b}\|^2 = \|\mathbf{a}\|^2 + \|\mathbf{b}\|^2 - 2 \mathbf{a} \cdot \mathbf{b} </math>∥a−b∥2=∥a∥2+∥b∥2−2a⋅b
若向量归一化( <math xmlns="http://www.w3.org/1998/Math/MathML"> ∥ a ∥ = ∥ b ∥ = 1 \|\mathbf{a}\| = \|\mathbf{b}\| = 1 </math>∥a∥=∥b∥=1),则: <math xmlns="http://www.w3.org/1998/Math/MathML"> ∥ a − b ∥ 2 = 2 − 2 cos θ \|\mathbf{a} - \mathbf{b}\|^2 = 2 - 2 \cos\theta </math>∥a−b∥2=2−2cosθ 因此,欧氏距离与余弦相似度呈反比:余弦相似度越大,欧氏距离越小。
3.3.3 余弦相似度与欧氏距离
对于归一化向量,余弦相似度为 1 时,欧氏距离为 0;余弦相似度为 0 时,欧氏距离为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 \sqrt{2} </math>2 。 欧氏距离综合考虑长度和方向差异,而余弦相似度只关注方向。
python
from sentence_transformers import SentenceTransformer, util
import torch
import 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}")
输出为:
python
点积: 0.6524
余弦相似度: 0.6524
欧氏距离: 0.8337