Embedding 是什么?1536 维什么意思?
结论:Embedding(嵌入)是一个把文本映射成定长稠密向量的函数,形如 f(text) → [0.021, -0.113, ...]。
"1536 维"说的是这个向量有 1536 个浮点数,它是模型输出层的宽度,不是 1536 个各自可解释的特征。
语义相近的文本,向量在空间里的方向接近,所以能用余弦相似度(Cosine Similarity)做检索。
1. 一段文字是怎么变成 1536 个数的
"报销要在 30 天内提交"
│ BPE 分词
▼
["报销", "要", "在", " 30", " 天", "内", "提交"] → 7 个 token id
│ Transformer 编码,每层做自注意力,token 之间交换上下文
▼
7 × 1536 的上下文矩阵(每个 token 一个 1536 维向量)
│ 池化(Pooling):取 [CLS] 位,或对所有 token 求平均
▼
1 × 1536
│ L2 归一化,使 ‖v‖₂ = 1
▼
[0.0134, -0.0871, 0.0522, ...] 共 1536 个 float32
归一化之后,余弦相似度就等于点积:cos(a,b) = a·b,向量库内部只做一次乘加,省掉开方与除法。
2. 为什么"语义接近"能变成"距离接近"
Embedding 模型通常用对比学习(Contrastive Learning)训练:一个批次里,把"同一句话的两种说法"或"问题-答案对"当正样本, 其余组合当负样本,损失函数要求正样本相似度升高、负样本相似度降低。训练久了,"怎么开发票"和"发票怎么开"自然被推到相邻位置, 而"发票"和"发票打印机型号"虽然共享字面,却在语义上被拉开。
这也解释了它的软肋:训练数据没覆盖的领域(内部缩写、产品代号)向量会挤在一起,此时必须靠关键词检索兜底。
3. "1536 维"到底意味着什么
| 模型 | 维度 | 最大输入 | 说明 |
|---|---|---|---|
| text-embedding-3-small | 1536 | 8191 token | 性价比高,默认首选 |
| text-embedding-3-large | 3072 | 8191 token | 更强,存储翻倍 |
| text-embedding-ada-002 | 1536 | 8191 token | 老模型,新项目不必再用 |
| bge-large-zh-v1.5 | 1024 | 512 token | 中文单语,可本地部署 |
| bge-m3 | 1024 | 8192 token | 多语言,稠密+稀疏+多向量 |
维度不是越大越好,三笔账要算清楚:
| 维度 | 存储(100 万条) | 检索耗时 | 精度 |
|---|---|---|---|
| 384 | ≈1.5 GB | 基准 | 一般 |
| 1024 | ≈3.9 GB | ×2.5 | 较好 |
| 1536 | ≈5.9 GB | ×4 | 好 |
| 3072 | ≈11.7 GB | ×8 | 略好于 1536 |
存储按 N × d × 4 字节 估算,索引结构还要再乘以 1.5 左右。text-embedding-3 系列支持 dimensions 参数把 1536 截断到 512,
精度只掉一点——这是 Matryoshka 表示学习的特性,前面的维度承载主要信息,适合"先便宜地粗排,再用全维度精排"。
4. 代码:把相似度算出来看
import numpy as np
from openai import OpenAI
client = OpenAI()
def embed(texts: list[str]) -> np.ndarray:
# 返回 shape = (len(texts), 1536)
resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
return np.array([d.embedding for d in resp.data], dtype=np.float32)
def cosine_matrix(mat: np.ndarray) -> np.ndarray:
# 服务端已返回归一化向量,再归一次防止某些兼容实现偷懒
unit = mat / np.linalg.norm(mat, axis=1, keepdims=True)
return unit @ unit.T
texts = [
"怎么申请开票",
"发票申请流程是什么",
"服务器磁盘占满了怎么处理",
"开票要提交哪些材料",
]
sim = cosine_matrix(embed(texts))
print(sim.round(3))
# 输出(示意,具体数值随模型版本浮动):
# [[1. 0.941 0.187 0.876]
# [0.941 1. 0.192 0.861]
# [0.187 0.192 1. 0.179]
# [0.876 0.861 0.179 1. ]]
三句"开票"互相在 0.86 以上,和"磁盘占满"只有 0.18 左右——这就是检索能工作的全部依据。
5. 常见追问
Q:每个维度有含义吗? 没有。单看第 37 维,你无法解释它代表"正式程度"还是"是否涉及金额"。语义是分布式编码在整组维度里的,不要试图给维度起名字。
Q:为什么用余弦而不是欧氏距离? 文本长度会改变向量模长,方向才承载语义。归一化之后余弦与欧氏距离单调对应,两者排序等价,选哪个只是实现偏好。
Q:换 embedding 模型要注意什么? 向量空间不可比,新旧模型混在一个索引里等于随机噪声,必须全量重建。同时把 model 和 dimension 记进集合元数据,防止误用。
Q:输入超长被截断怎么办? 8191 token 之外的内容会被静默丢弃,长文档必须先分块;这也是一篇文档要切成多个片段的直接原因。
小结
Embedding 是 RAG 的"翻译层",把语义压进几何空间。维度决定成本,模型决定效果,两者都不能在索引建好之后随意更换。