一、Embedding 是什么:从单词到数学坐标

在深入原理之前,我们首先要理解 Embedding(嵌入) 的核心思想。简单来说,它就是一种将高维、离散、非结构化的数据(如一个单词、一个句子、一张图片,甚至一个用户ID)映射为低维、连续、稠密的实数向量的技术。想象一下,我们无法直接让计算机理解“快乐”这个词的含义,但如果我们能把“快乐”变成一组坐标 [0.2, -0.5, 0.8, ...],并且确保“快乐”和“开心”的坐标非常接近,而与“悲伤”的坐标相距甚远,那么计算机就通过数学运算“理解”了这些词的语义关系。

这个映射过程并非随意进行,其目标是通过模型的训练,让向量空间中的距离方向能够反映原始数据之间的语义相似性。例如,“国王”的向量减去“男人”的向量,再加上“女人”的向量,最终得到的向量应该非常接近“女王”的向量。这种神奇的“向量算术”能力,正是Embedding模型强大的体现。它为机器学习模型处理文本、推荐系统中的物品等提供了统一的、可计算的数字语言。

提示: Embedding向量的维度(长度)是一个关键超参数,常见范围从128到1024。维度越高,理论上能表达的语义信息越丰富,但也会增加计算和存储成本。

二、Embedding 模型是如何炼成的:学习语义的表示

Embedding向量不是手工设计的,而是通过模型在大量数据上学习出来的。其训练过程本质上是一个优化问题:调整模型参数,使得在特定任务下,语义相似的数据对,其对应的向量距离更近;语义不同的数据对,其向量距离更远。

Word2Vec(一种经典的词嵌入模型)为例,它主要通过两种方式训练:

  1. CBOW(连续词袋):根据上下文单词预测中心词。
  2. Skip-gram:根据中心词预测上下文单词。

在训练过程中,模型内部的查找表(即Embedding层)的权重被不断更新,最终使得出现在相似上下文中的单词,其向量表示在空间中聚集。

对于现代的句子或文档Embedding,我们通常使用深度神经网络(如Transformer)作为编码器。模型的训练目标可能是:

关键点: 预训练的通用Embedding模型(如all-MiniLM-L6-v2)可以在海量无标注文本上学习通用语义。在具体应用场景中,我们常使用领域数据对模型进行微调,让向量表示更贴合特定任务的语义相似性定义。

三、衡量相似:向量间的距离与夹角

得到Embedding向量后,下一步就是量化它们之间的相似程度。这完全是一个几何计算问题。最常用的两种度量方式是:

在实际应用中,余弦相似度更受青睐,因为它更关注语义内容本身(方向),而不是表示的“强度”(模长)。例如,两篇讨论同一主题的长文和短文,它们的Embedding向量模长可能不同,但方向应该高度一致。

import numpy as np

# 假设我们已经通过模型得到了两个句子的Embedding向量
vec_a = np.array([0.1, 0.3, 0.8])
vec_b = np.array([0.2, 0.35, 0.75])

# 计算余弦相似度
def cosine_similarity(v1, v2):
    dot_product = np.dot(v1, v2)
    norm_v1 = np.linalg.norm(v1)
    norm_v2 = np.linalg.norm(v2)
    return dot_product / (norm_v1 * norm_v2)

similarity = cosine_similarity(vec_a, vec_b)
print(f"余弦相似度: {similarity:.4f}") # 输出: 余弦相似度: 0.9915

四、语义检索的完整流程:从查询到结果

将Embedding与相似度计算结合,就构成了语义检索的核心流程。它与传统基于关键词匹配的检索(如BM25)有本质区别,即使查询和文档没有共同的词,只要语义相关,也能被检索到。整个流程如下:

  1. 离线索引
  1. 在线查询
重要: 整个流程成功的关键在于,编码查询和编码文档必须使用完全相同的模型。否则,两者的向量处于不同的语义空间,计算相似度毫无意义。

五、代码实战:用 Sentence-Transformers 实现语义搜索

下面我们用一个非常流行的库 sentence-transformers 来演示如何快速构建一个语义检索系统。

from sentence_transformers import SentenceTransformer, util

# 1. 加载预训练的Embedding模型
model = SentenceTransformer('all-MiniLM-L6-v2') # 一个轻量且效果不错的模型

# 2. 准备我们的“语料库”
corpus = [
    "今天天气真不错,适合出去散步。",
    "Python是一种广泛使用的编程语言。",
    "机器学习是人工智能的核心领域之一。",
    "这家餐厅的菜品非常美味,服务也很周到。",
    "自然语言处理让机器能理解人类语言。",
    "周末打算和朋友去看一场电影。"
]

# 3. 对语料库进行编码(离线索引过程)
corpus_embeddings = model.encode(corpus, convert_to_tensor=True)

# 4. 用户查询
query = "如何让电脑听懂人话?"

# 5. 对查询进行编码
query_embedding = model.encode(query, convert_to_tensor=True)

# 6. 计算查询向量与所有文档向量的相似度,并按相似度排序
cos_scores = util.cos_sim(query_embedding, corpus_embeddings)[0]
top_results = cos_scores.argsort(descending=True)

print(f"查询:{query}\n最相关的文档:")
for idx in top_results[0:3]: # 显示Top 3结果
    print(f"  相似度: {cos_scores[idx]:.4f} | 文档: {corpus[idx]}")

输出可能如下:

查询:如何让电脑听懂人话?
最相关的文档:
  相似度: 0.5621 | 文档: 自然语言处理让机器能理解人类语言。
  相似度: 0.4589 | 文档: 机器学习是人工智能的核心领域之一。
  相似度: 0.2653 | 文档: Python是一种广泛使用的编程语言。

这个简单的例子清晰地展示了,即使查询中没有出现“自然语言处理”、“理解”这些词,模型也能基于语义找到最相关的文档。这就是Embedding向量与语义检索的力量。在实际生产环境中,当语料库达到百万甚至亿级时,就需要引入向量数据库和近似最近邻算法来保证检索速度和效果了。