RAG轻松通-P2:向量化

欢迎来到啾啾的博客🐱。

记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。

有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。

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系统可以这样做:

  1. 把用户的问题(比如"今天天气怎么样?")也转换成一个向量。
  2. 然后用这个"问题向量"去和数据库里成千上万个"文本块向量"逐一计算点积(或者更高效的相似度算法)。
  3. 最后,选出点积得分最高的那几个文本块,作为"开卷考试"的参考资料,喂给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) 告诉我们两件至关重要的事:

  1. 第一个数字 (3): 代表我们处理了多少个独立的文本项。在这里,它对应我们输入的列表 text_chunks 中有3个句子。如果我们将1000个文本块输入 model.encode(),这个数字就会是1000。它代表了我们数据集的 样本数量
  2. 第二个数字 (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
相关推荐
机器之心2 小时前
谢赛宁团队新基准让LLM集体自闭,DeepSeek R1、Gemini 2.5 Pro都是零分
人工智能·llm
LLM大模型2 小时前
LangGraph篇-LangGraph快速入门
人工智能·程序员·llm
LLM大模型2 小时前
LangGraph篇-核心组件
人工智能·程序员·llm
量子位2 小时前
大模型全员 0 分!谢赛宁领衔华人团队,最新编程竞赛基准出炉,题目每日更新禁止刷题
llm
数据智能老司机3 小时前
AI产品开发的艺术——AI用户体验:为不确定性而设计
人工智能·llm·产品
AI大模型技术社4 小时前
💻 工业级代码实战:TransformerEncoderLayer六层堆叠完整实现(附调试技巧)
人工智能·llm
MarkGosling4 小时前
【资源合集】强化学习训练LLM Agents的实战资源库:AgentsMeetRL
llm·agent·强化学习
啾啾大学习4 小时前
RAG轻松通-P4:检索
llm
智泊AI5 小时前
怎么通俗易懂地理解AI大模型微调?一篇大白话文章解释模型微调!
llm