直觉:把"语义"变成坐标
计算机不懂"国王"和"女王"语义相近,它只看到两个不同的字符串。嵌入(embedding)做的事,是把离散对象(词、句子、图片、用户)映射到一个连续的 维向量空间,使得语义相近的对象在空间里距离也近。一旦有了这个空间,“找相似"就退化成"找最近的点”,这正是现代检索、推荐、RAG 的地基。
本文从最经典的 word2vec 讲到工业级的近似最近邻(ANN),把这条链路打通。
机制:word2vec 是怎么学出向量的
word2vec 的核心假设是分布式语义:一个词的含义由它的上下文决定。以 skip-gram 为例,目标是用中心词预测窗口内的上下文词。给定中心词 和上下文词 ,用两套向量 (中心词)和 (上下文词),softmax 概率为:
问题在分母:词表 动辄几十万,每步都对全表求和,复杂度 ,不可接受。**负采样(negative sampling)**把它改写成二分类——对每个真实 (中心词, 上下文词) 正样本,采样 个噪声词作负样本,目标变成:
其中 是 sigmoid, 是经验上效果好的噪声分布(对高频词降权)。这样每步只算 个内积,复杂度从 降到 。训练完,(或 )就是词向量。著名的 king - man + woman ≈ queen 类比,反映的是这个空间里存在近似线性的语义方向。
现代句向量/文档向量(如基于 Transformer 的编码器)思路升级了——上下文相关、用对比学习训练——但"用向量近邻表达语义近邻"的内核没变。
相似度度量:别在度量上踩坑
拿到向量后用什么衡量"近"?两个主流:
- 余弦相似度:,只看方向不看长度。文本嵌入几乎都用它。
- 欧氏距离:,方向和长度都看。
关键工程技巧:如果把所有向量归一化到单位长度,则 。此时"欧氏距离最小"和"内积最大"和"余弦最大"三者完全等价。所以工业做法常是:先 L2 归一化,再用内积索引。这避免了在线计算除法,也让不同索引后端行为一致。
一个常见踩坑:训练时用余弦、检索时用欧氏(且没归一化),度量不一致会让召回质量莫名其妙下降。
为什么需要 ANN:精确检索扛不住
有了 个向量、一个查询 ,精确最近邻就是线性扫描全部,复杂度 。当 上亿、 是几百到上千维时,单次查询要做上亿次内积——延迟和成本都不可接受。
而且"维度灾难"让事情更糟:在高维空间里,最近点和最远点的距离趋于接近,精确最近邻的意义本身在变弱。于是工业界普遍接受用近似最近邻(ANN):牺牲一点召回率(recall@k),换取数量级的延迟下降。核心权衡就是 recall ↔ latency ↔ 内存 这个三角。
主流 ANN 索引有三类:
- IVF(倒排 + 量化):先用 k-means 把空间切成若干 cluster,查询时只搜最近的几个 cluster(参数
nprobe)。nprobe越大召回越高、越慢。常配合 PQ(乘积量化) 压缩向量:把 维切成 段,每段单独量化成 1 字节码本索引,把一个 float 向量从 字节压到 字节,显存/内存占用骤降,代价是距离计算变成近似查表。 - HNSW(分层可导航小世界图):构建多层近邻图,高层稀疏用于"跳远",底层稠密用于"精搜"。查询从顶层贪心下降,平均查询复杂度近似 ,召回-延迟曲线通常最好,但内存占用大(要存图的边),且增量构建成本高。
- 暴力 + GPU: 不太大(百万级)时,GPU 上的精确扫描反而简单可靠。
最小检索示例
下面用 numpy 演示归一化 + 内积检索的内核(生产请用 Faiss / ScaNN / hnswlib):
1 | import numpy as np |
注意 argpartition 而非 argsort:找 top-k 不需要全排序,前者是 ,后者 。这是检索里常被忽略的小优化。
工程权衡清单
- 维度 :维度高语义更丰富,但内存、计算、索引时间都线性涨。常用降维(PCA/OPQ)在召回和成本间折中。
- 归一化:基本无脑做,统一度量、稳定数值。
- 索引选型:内存吃紧 → IVF-PQ;要极致召回-延迟且内存富裕 → HNSW;库小 → 暴力。
- 召回评估:上线前一定测 recall@k(用精确检索做 ground truth),别只看延迟。
- 更新模式:HNSW 增量插入还行但删除麻烦;IVF 重训码本代价高。高频更新场景要提前规划重建策略。
小结
嵌入把离散语义压成连续向量,word2vec 用负采样把 的 softmax 降成 的二分类,奠定了"语义近邻=向量近邻"的范式。检索阶段,先做 L2 归一化统一余弦/内积/欧氏度量,再用 ANN(IVF-PQ、HNSW)在 recall、latency、内存三角里取舍——亿级规模下用一点点召回损失换数量级的速度,是这套系统能跑起来的根本原因。