Embeddings:语义空间
"Embeddings 是离散的文字世界和连续的数字世界之间的桥梁。"
如果说 Tokenization 是 LLM "阅读"的方式,那么 Embeddings 就是它们"理解"的方式。Embedding 是一个向量——一组数字——表示一个 token 的含义。
什么是向量 Embedding?
本节介绍向量 Embedding 的基本概念,以及它们如何让机器以数值方式表示和处理语义。
想象一个二维坐标系:
- "狗" 可能在坐标
[0.8, 0.2]。 - "猫" 可能在
[0.7, 0.3](离"狗"很近)。 - "汽车" 可能在
[-0.9, -0.5](很远)。
现在将这个概念扩展到 4,096 维(Llama 3/4)或 12,288 维(GPT-4)。这个高维空间允许模型捕获意义的微妙差异——性别、复数、语气、意图和关系。
工程视角
向量捕获概念之间的语义关系。例如,"国王"的向量减去"男人"加上"女人",得到的结果非常接近"女王"的向量。这不是硬编码的——它是在大规模文本数据集上训练出来的。
2025 年:各模型的 Embedding 维度
| 模型 | Embedding 维度 | 类型 |
|---|---|---|
| GPT-4o | 12,288 | 上下文相关(解码器) |
| Llama 3/4 | 4,096 | 上下文相关(解码器) |
| Claude 3.5 | ~12,288 | 上下文相关(解码器) |
| OpenAI ada-002 | 1,536 | 静态(句子编码器) |
| all-MiniLM-L6-v2 | 384 | 静态(句子编码器) |
| BERT-base | 768 | 上下文相关(编码器) |
| RoBERTa | 1,024 | 上下文相关(编码器) |
静态 vs 上下文相关 Embeddings
理解从静态到上下文相关 Embeddings 的演进对于掌握现代语言模型如何处理歧义和上下文相关含义至关重要。
这是面试中的关键区分点。
1. 静态 Embeddings(Word2Vec、GloVe)
- 机制:每个词有一个固定的向量。
- 问题:"Bank" 在 "Bank of America" 和 "river bank" 中有完全相同的向量。模型必须在 Embedding 层之后才能根据上下文判断含义。
2. 上下文相关 Embeddings(BERT、GPT)
- 机制:初始输入 Embedding 是静态的,但随着通过 Transformer 层,向量会融合周围词的上下文信息。
- 结果:"river bank" 中 "Bank" 的输出向量与 "financial bank" 中的在数学上是不同的。
3. 句子 Embeddings(2025 年标准)
Sentence Transformers(SBERT、all-MiniLM 等)将此进一步推进:
- 目标:将整个句子/文档编码为固定向量
- 使用场景:语义搜索、RAG、聚类、相似度匹配
- 机制:对 token Embeddings 做均值池化(平均所有 token 向量)
示例:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
sentences = [
"The weather is lovely today.",
"It's so sunny outside!",
"He drove to the stadium.",
]
embeddings = model.encode(sentences) # Shape: [3, 384]
similarities = model.similarity(embeddings, embeddings)
# [[1.0000, 0.6660, 0.1046], # 天气相关的句子相似度高
# [0.6660, 1.0000, 0.1411],
# [0.1046, 0.1411, 1.0000]] # 体育场句子与其他句子相似度低
度量相似度:余弦 vs 点积
本节探讨度量向量相似度的两种主要方法,以及在不同机器学习应用中何时使用每种方法。
如何判断两个向量是否相似?
点积(Dot Product)
- 捕获:大小和方向。
- 使用场景:当向量的长度重要时(如在注意力分数中,我们想保留信号强度)。
- 注意力机制:自注意力使用缩放点积来防止梯度消失。
余弦相似度(Cosine Similarity)
- 捕获:仅方向(归一化后)。
- 使用场景:语义搜索、RAG。我们通常不关心一个文本是否比另一个长;我们关心它们是否关于同一主题。
- 范围:-1(相反)到 1(相同)。
import numpy as np
def cosine_similarity(v1, v2):
return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
# 在高维空间中,几乎所有随机向量都是正交的(相似度 ~0)。
# 有意义的相似度是非常强的信号。
2025 年:何时使用哪种
| 使用场景 | 相似度度量 | 原因 |
|---|---|---|
| 自注意力 | 点积(缩放) | 保留大小,高效 |
| 语义搜索 / RAG | 余弦相似度 | 与长度无关,专注语义 |
| 文档聚类 | 余弦相似度 | 归一化比较 |
| 推荐系统 | 点积 | 信号强度重要 |
位置 Embedding:模型如何知道"顺序"
Transformer 同时处理所有 token,需要显式的位置信息来理解词序。本节介绍位置编码技术的演进。
由于 Transformer 架构并行处理所有 token(与 RNN 不同),它没有内在的"第一"、"第二"、"第三"概念。我们必须注入这个信息。
1. 绝对位置 Embedding(原始 Transformer、BERT)
- 方法:给第一个 token 加一个固定向量
P_0,第二个加P_1。 - 局限:难以泛化到比训练时更长的序列。
2. 相对位置 Embedding(T5、ALiBi)
- 方法:不学"Token 5"在哪,而是学"Token A 和 Token B 之间相隔 3 步"。
- 优势:对更长上下文的泛化能力更好。
3. RoPE(旋转位置 Embedding)——当前黄金标准
被 Llama 2/3/4、PaLM、Mistral、GPT-NeoX 使用。
- 直觉:通过在空间中旋转向量来编码位置。
- 机制:
- Token 按与其位置成正比的角度旋转。
- 两个 token 之间的点积(相似度)仅取决于它们的相对距离。
- 为什么胜出:
- 衰减:注意力随 token 距离增加而自然衰减(长程依赖管理)。
- 外推:比绝对位置 Embedding 更好地处理超出训练长度的上下文。
面试提示:如果被问到 "为什么 Llama 使用 RoPE?",回答:"它通过向量旋转自然地捕获相对位置信息,实现了更好的长度外推,结合了绝对编码和相对编码的优势。"
2025 年:高级位置编码
PaTH Attention(MIT 2025):
- 将中间词视为数据依赖变换的路径
- 每次变换使用 Householder 反射
- 给模型"位置记忆"的感觉
- 与Forgetting Transformers (FoX) 结合,选择性地降低旧信息的权重
影响:
- 更好地跟踪代码中的状态变化
- 改进顺序推理
- 在长上下文任务上更强
2025 年:Matryoshka Embeddings(MRL)
Matryoshka Embeddings 代表了效率方面的突破,允许单一模型在不损失质量的情况下生成多种维度的 Embedding。本节解释这一前沿技术及其实际意义。
2024-2025 年 Embedding 领域最大的进步。
什么是 Matryoshka Embeddings?
受俄罗斯套娃启发,Matryoshka 表示学习(MRL)创建嵌套的、可截断的 Embedding:
- 单一模型生成 d 维向量
- 前 k 维(如 64、128、256...)形成有效的低维 Embedding
- 无需重训练即可使用不同维度
示例:一个 1024 维 Embedding 包含:
- 64 维前缀(粗粒度信息)
- 128 维前缀(细粒度)
- 256 维前缀(详细)
- ...
- 1024 维完整(最大保真度)
为什么 MRL 很重要
自适应部署:
# 同一模型,不同 Embedding 大小
embedding_1024 = model.encode(text) # 完整质量
embedding_512 = model.encode(text)[:512] # 98% 质量,2 倍速度
embedding_128 = model.encode(text)[:128] # 90% 质量,8 倍速度
成本节约:
- 存储:128 维 Embedding 只用 1024 维的 12.5% 存储
- 内存:设备端推理更低的 RAM 占用
- 延迟:更快的向量相似度计算
- 权衡:128 维通常只有 2-10% 的性能损失
训练 MRL 模型
多尺度 InfoNCE 损失:
# 在多个维度上的标准对比损失
dimensions = [64, 128, 256, 512, 1024]
total_loss = 0
for dim in dimensions:
emb_truncated = embeddings[:, :dim]
loss = contrastive_loss(emb_truncated, labels)
total_loss += loss
# 同时优化所有尺度
total_loss.backward()
核心创新:重要信息被优先编码在较前的维度中。
2025 年 MRL 现状
模型:
- mixedbread-ai/mxbai-embed-2d-large-v1:首个 2D-Matryoshka 模型
- OpenAI:正在为下一代 Embedding 探索 MRL
- Cohere:使用 MRL 进行高效检索
应用:
- 时间检索:带 MRL 的时间感知新闻聚类
- 多模态:跨模态检索,灵活维度
- 设备端:用低维 Embedding 实现移动端搜索
- RAG 系统:混合检索(快速低维过滤,慢速高维重排)
二值量化 + MRL:
- 结合 MRL(降维)和二值量化(每维 1 比特)
- 结果:每个 Embedding 仅 64 字节(float32 1024 维需要 4KB)
- 性能:约为 float32 质量的 85-90%,存储成本不到 2%
具体实现:PyTorch 示例
本节提供实际动手的 PyTorch 实现,展示 Embedding 在神经网络中是如何创建和使用的。
简单看一个 Embedding 层的代码。
import torch.nn as nn
# 词汇表大小:30,000 个 token
# Embedding 维度:512
embedding_layer = nn.Embedding(30000, 512)
# 输入:Token ID [101, 45, 23]
input_ids = torch.tensor([101, 45, 23])
# 输出:3 个大小为 512 的向量
vectors = embedding_layer(input_ids)
print(vectors.shape) # torch.Size([3, 512])
embedding_layer 就是一个大小为 的查找表(矩阵)。这些是可学习的参数,通过反向传播更新,与任何其他权重一样。
2025 年:现代 Embedding 流水线
from sentence_transformers import SentenceTransformer
# 加载支持 Matryoshka 的模型
model = SentenceTransformer("mixedbread-ai/mxbai-embed-2d-large-v1")
# 用灵活的维度编码
text = "Semantic search is powerful."
# 完整 1024 维 Embedding
emb_full = model.encode(text) # Shape: (1024,)
# 截断为 256 维(无需重训练)
emb_256 = model.encode(text)[:256] # Shape: (256,)
# 用于不同场景
# - 完整 1024:生产搜索(最大质量)
# - 512 维:缓存层
# - 256 维:快速预过滤
# - 128 维:设备端搜索
Spring AI Embedding 服务
Spring AI 为语义搜索、RAG(检索增强生成)和文档相似度提供 Embedding 服务。
基本 Embedding 服务
// Spring AI Embedding 服务
@Service
public class EmbeddingService {
private final EmbeddingModel embeddingModel;
public float[] embedText(String text) {
return embeddingModel.embed(text);
}
// 语义搜索
public List<Document> searchSimilar(String query, List<Document> corpus) {
float[] queryEmbedding = embeddingModel.embed(query);
return corpus.stream()
.map(doc -> new AbstractMap.SimpleEntry<>(
doc,
cosineSimilarity(queryEmbedding,
embeddingModel.embed(doc.getContent()))
))
.sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
.map(Map.Entry::getKey)
.limit(5)
.collect(Collectors.toList());
}
private double cosineSimilarity(float[] a, float[] b) {
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
}
Spring AI RAG 集成
// 使用 Spring AI 实现检索增强生成
@Service
public class RAGService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public String answerWithRetrieval(String question) {
// 检索相关文档
List<Document> relevant = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5)
);
// 基于上下文生成回答
String context = relevant.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
return chatClient.prompt()
.user(userSpec -> userSpec
.text("根据以下上下文回答:\n\n{context}\n\n问题:{question}")
.param("context", context)
.param("question", question))
.call()
.content();
}
}
向量存储配置
// 针对不同数据库的向量存储配置
@Configuration
public class VectorStoreConfiguration {
@Bean
public VectorStore vectorStore(
EmbeddingModel embeddingModel,
JdbcTemplate jdbcTemplate
) {
return new PgVectorStore(
jdbcTemplate,
embeddingModel,
PgVectorStoreConfig.builder()
.withTableName("document_embeddings")
.withDimension(1536) // OpenAI ada-002 维度
.build()
);
}
// 开发环境使用简单的内存向量存储
@Bean
@Profile("dev")
public VectorStore simpleVectorStore(EmbeddingModel embeddingModel) {
return new SimpleVectorStore(embeddingModel);
}
}
文档索引服务
// 将文档索引到向量存储的服务
@Service
public class DocumentIndexingService {
private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
// 索引单个文档
public void indexDocument(Document document) {
float[] embedding = embeddingModel.embed(document.getContent());
document.setEmbedding(embedding);
vectorStore.add(List.of(document));
}
// 带进度跟踪的批量索引
public void indexBatch(List<Document> documents) {
int total = documents.size();
for (int i = 0; i < total; i += 100) {
int end = Math.min(i + 100, total);
List<Document> batch = documents.subList(i, end);
// 批量生成 Embedding
batch.forEach(doc -> {
float[] embedding = embeddingModel.embed(doc.getContent());
doc.setEmbedding(embedding);
});
// 存储批次
vectorStore.add(batch);
log.info("已索引 {}/{} 文档", end, total);
}
}
// 更新已有文档
public void updateDocument(String documentId, String newContent) {
// 删除旧 Embedding
vectorStore.delete(documentId);
// 创建并存储新 Embedding
Document updated = new Document(documentId, newContent);
float[] embedding = embeddingModel.embed(newContent);
updated.setEmbedding(embedding);
vectorStore.add(List.of(updated));
}
}
面试 FAQ
关于 Embeddings 的常见面试问题,附有详细、技术准确的答案,帮助你在技术讨论中展示深入理解。
Q:为什么 Attention 公式中使用点积而非余弦相似度?
A: 计算效率。点积是简单的矩阵乘法。余弦相似度需要为每对向量计算范数(平方根),代价昂贵。在自注意力中,我们通过除以键维度的平方根来缩放点积,防止梯度消失。这在没有完全归一化开销的情况下模仿了归一化。
2025 年更新:一些现代架构(如 RoPE)通过旋转隐式归一化,使点积表现得更像余弦相似度,同时保持效率。
Q:如何处理 Embedding 中的词表外(OOV)词?
A: 现代 BPE 分词器几乎消除了所有文本的 OOV 问题。如果一个词"未知",它会被分解为子词块(最终分解为单个字节),这些都在词表中。现代生产环境中很少有真正的 <UNK> token。
2025 年更新:字节级 BPE(GPT-4o、Llama 3/4 的标准)保证零 OOV,因为任何 Unicode 文本都可以表示为字节。
Q:向量搜索中的"维度灾难"是什么?
A: 随着维度增加,"距离"的概念变得不那么有意义——所有点趋向于彼此等距。然而,在语言数据所在的流形上,Embedding 有效,因为数据分布在高维空间内的低维结构上。
2025 年缓解方案:Matryoshka Embeddings 通过允许在距离度量仍然有意义的低维子空间中工 作来解决这一问题,仅在需要时扩展到完整维度。
Q:ada-002、BERT 和 Llama Embeddings 有什么区别?
A: 三个关键区别:
-
目的:
- ada-002:为语义相似度/搜索设计(静态句子 Embedding)
- BERT:为掩码语言建模设计(上下文相关词 Embedding)
- Llama:为下一 token 预测设计(上下文相关 token Embedding)
-
用法:
- ada-002:单次前向传播,获得用于检索/RAG 的固定向量
- BERT:编码所有 token,获得上下文化向量(使用 CLS token 或均值池化)
- Llama:在生成过程中动态变化的 Embedding
-
训练:
- ada-002:在查询-文档对上的对比学习
- BERT:掩码语言建模 + 下一句预测
- Llama:因果语言建模(预测下一 token)
经验法则:RAG/搜索用 ada-002,分类/NLU 用 BERT,生成用 Llama。
Q:什么时候应该使用 Matryoshka Embeddings 而非标准 Embeddings?
A: 在以下情况使用 Matryoshka:
- 资源受限:需要在内存有限的移动/边缘设备上运行
- 多阶段检索:快速低维 预过滤,慢速高维重排
- 可变质量需求:不同用户/设备需要不同质量级别
- 存储考虑:想将向量数据库成本降低 50-90%
在以下情况使用标准 Embeddings:
- 一致资源:所有部署的计算能力相似
- 需要最大质量:能承担全维度计算
- 简单流水线:不想要可变维度的复杂性
2025 年结论:MRL 正在成为生产 RAG 系统的默认选择,因为它在不牺牲质量的情况下提供了灵活性。
Q:池化策略(mean、max、CLS)如何影响句子 Embedding?
A: 三种常见策略:
均值池化(最常见):
- 对所有 token Embedding 取平均
- 在一般语义相似度任务上表现良好
- 使用者:Sentence-Transformers、all-MiniLM
最大池化:
- 对每个维度取所有 token 的最大值
- 捕获显著特征
- 适合关键词检测
CLS Token:
- 使用特殊的分类 token
- BERT 风格:训练为表示句子含义
- 对长句子可能不够可靠
2025 年最佳实践:大多数语义搜索任务使用带归一化(L2 范数)的均值池化。分类任务使用 CLS token。
面试要点总结
一份简洁的清单,总结关键 Embedding 概念,用于技术面试前的快速复习。
- Embeddings 桥接离散 token 和连续向量,捕获语义含义。
- 静态 Embeddings(Word2Vec):每个词一个向量,无上下文感知。
- 上下文相关 Embeddings(BERT、GPT):向量根据周围上下文变化。
- 句子 Embeddings:通过均值池化获得句子的固定向量(SBERT、all-MiniLM)。
- RoPE 主导 2025 年的位置编码:更好的外推能力,相对位置感知。
- Matryoshka Embeddings(MRL) 是 2025 年的突破:嵌套的、可截断的 Embedding,无需重训练。
- 余弦相似度用于语义搜索(与长度无关),点积用于注意力(大小重要)。
- 维度趋势:句子编码器 384-1536,LLM token Embedding 4k-12k。
- MRL 优势:自适应部署,50-90% 成本节约,2-10% 质量损失。
- 2025 年工具:sentence-transformers 库、OpenAI ada-002、mixedbread MRL 模型。
试试 sentence-transformers 库来建立直觉:
from sentence_transformers import SentenceTransformer
import numpy as np
# 加载模型
model = SentenceTransformer("all-MiniLM-L6-v2")
# 比较句子
s1 = "The cat sits on the mat."
s2 = "A feline is resting on a rug."
s3 = "The stock market crashed today."
emb1 = model.encode(s1)
emb2 = model.encode(s2)
emb3 = model.encode(s3)
# 余弦相似度
def cos_sim(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"相似度 (s1, s2): {cos_sim(emb1, emb2):.3f}") # ~0.7(高)
print(f"相似度 (s1, s3): {cos_sim(emb1, emb3):.3f}") # ~0.1(低)
进阶练习,探索 mixedbread.ai 的 Matryoshka 模型:
model = SentenceTransformer("mixedbread-ai/mxbai-embed-2d-large-v1")
emb_full = model.encode("Your text here")
emb_128 = emb_full[:128] # 截断,无需重训练!