bge-large-zh-v1.5的嵌入向量的相似度计算的数学原理

可以。这个问题非常适合从你最近正在学的“向量 → 点积 → 余弦相似度 → 梯度/空间方向”这条数学线继续往下走。

对于 BAAI/bge-large-zh-v1.5,最核心的一件事其实可以浓缩成:

\[\boxed{ \text{文本} \;\xrightarrow{\text{BGE}} \mathbf{x}\in\mathbb R^{1024} \;\xrightarrow{\text{L2归一化}} \hat{\mathbf{x}} \;\xrightarrow{\text{点积}} \hat{\mathbf{x}}^\top\hat{\mathbf{y}} } \]

而最后这个点积,在两个向量都归一化以后,数学上就是余弦相似度

FlagEmbedding 官方示例实际上就是:

embeddings_1 @ embeddings_2.T

官方文档明确说明 BGE embedding 可以通过 inner product 计算 similarity;对于 bge-large-zh-v1.5,官方示例也直接采用这种方式。(GitHub)

下面我们把这件事从数学底层一直拆到代码。


一、先建立整个数学模型

假设有两句话:

A:北京是中国的首都

B:中国的首都是北京

BGE 的任务不是直接告诉我们:

“这两个句子的意思很接近。”

而是把每句话转换成一个高维向量:

\[A \longrightarrow \mathbf{x} = (x_1,x_2,\ldots,x_{1024}) \]

\[B \longrightarrow \mathbf{y} = (y_1,y_2,\ldots,y_{1024}) \]

也就是说:

\[\boxed{ \text{一句话} \rightarrow 1024维空间中的一个点/方向 } \]

bge-large-zh-v1.5 最终输出的是 1024 维 embedding

因此:

\[\mathbf{x},\mathbf{y}\in\mathbb R^{1024} \]

接下来问题就变成:

如何判断 \(\mathbf{x}\)\(\mathbf{y}\) 在数学空间中有多接近?

这就进入向量相似度。


二、第一层:为什么不能直接比较两个向量的每个数字?

假设一个极度简化的 embedding:

\[\mathbf{x}=(0.8,0.6) \]

\[\mathbf{y}=(0.9,0.435) \]

如果单纯逐元素比较:

\[|0.8-0.9|,\quad |0.6-0.435| \]

实际上是在问:

两个坐标值分别差多少?

但 embedding 更重要的是:

两个向量在语义空间中的方向是否一致?

这是非常重要的思想。

例如:

\[\mathbf{x}=(1,0) \]

\[\mathbf{y}=(100,0) \]

它们的长度差别巨大:

\[\|\mathbf{x}\|=1 \]

\[\|\mathbf{y}\|=100 \]

但是方向完全一样。

从 embedding 的语义角度来说,我们往往更关心:

\[\boxed{\text{方向}} \]

而不是:

\[\boxed{\text{长度}} \]

因此 BGE 使用的核心思想就是 cosine similarity / cosine similarity 的等价形式


三、第二层:从余弦定理得到余弦相似度

两个向量:

\[\mathbf{x},\mathbf{y} \]

夹角为:

\[\theta \]

根据向量点积定义:

\[\mathbf{x}\cdot\mathbf{y} = \|\mathbf{x}\| \|\mathbf{y}\| \cos\theta \]

因此:

\[\boxed{ \cos\theta = \frac{\mathbf{x}\cdot\mathbf{y}} {\|\mathbf{x}\|\|\mathbf{y}\|} } \]

这就是余弦相似度:

\[\boxed{ \operatorname{cosine}(\mathbf{x},\mathbf{y}) = \frac{ \sum_{i=1}^{1024}x_i y_i }{ \sqrt{\sum_{i=1}^{1024}x_i^2} \sqrt{\sum_{i=1}^{1024}y_i^2} } } \]

这里已经出现了你前面一直在学习的三个核心数学对象:

向量 → 范数 → 点积

它们共同组成了 embedding similarity 的数学基础。


四、第三层:为什么 BGE 最后可以直接 @

这里是理解 FlagEmbedding 实现的关键。

假设我们先对向量进行 L2 normalization:

\[\hat{\mathbf{x}} = \frac{\mathbf{x}}{\|\mathbf{x}\|} \]

于是:

\[\|\hat{\mathbf{x}}\|=1 \]

同理:

\[\hat{\mathbf{y}} = \frac{\mathbf{y}}{\|\mathbf{y}\|} \]

所以:

\[\|\hat{\mathbf{y}}\|=1 \]

再计算点积:

\[\hat{\mathbf{x}}\cdot\hat{\mathbf{y}} \]

根据点积公式:

\[\hat{\mathbf{x}}\cdot\hat{\mathbf{y}} = \|\hat{\mathbf{x}}\| \|\hat{\mathbf{y}}\| \cos\theta \]

由于:

\[\|\hat{\mathbf{x}}\| = \|\hat{\mathbf{y}}\| = 1 \]

所以:

\[\boxed{ \hat{\mathbf{x}}\cdot\hat{\mathbf{y}} = \cos\theta } \]

于是:

\[\boxed{ \text{cosine similarity} = \text{normalized vectors' dot product} } \]

这就是为什么 FlagEmbedding 可以直接:

similarity = embeddings_1 @ embeddings_2.T

官方 embedding 示例就是这样实现的。(GitHub)


五、第四层:@ 到底在计算什么?

这是非常值得你彻底理解的地方。

假设有:

embeddings_1.shape = (2, 1024)
embeddings_2.shape = (3, 1024)

意味着:

embeddings_1

        1024维
           ↓
sentence A → [x₁ x₂ ... x₁₀₂₄]
sentence B → [x₁ x₂ ... x₁₀₂₄]

所以它是一个:

\[2\times1024 \]

矩阵。

第二个:

\[3\times1024 \]

矩阵。

但是:

embeddings_2.T

转置以后:

\[1024\times3 \]

于是:

\[(2\times1024) (1024\times3) = (2\times3) \]

因此:

\[\boxed{ E_1E_2^T } \]

得到:

\[\begin{bmatrix} s_{11}&s_{12}&s_{13}\\ s_{21}&s_{22}&s_{23} \end{bmatrix} \]

这里:

\[s_{ij} = \mathbf{x}_i^\top\mathbf{y}_j \]

也就是说:

第 i 个句子与第 j 个句子的相似度。

因此矩阵的每一个元素都有非常明确的含义:

\[\boxed{ S_{ij} = \operatorname{similarity} (\text{sentence}_i,\text{sentence}_j) } \]

这就是为什么 embedding 检索可以非常高效。


六、用一个 3 维例子把整个过程算一遍

为了不被 1024 维吓到,我们先使用 3 维。

假设 BGE 输出:

\[\mathbf{x}=(3,4,0) \]

\[\mathbf{y}=(6,8,0) \]

先计算长度:

\[\|\mathbf{x}\| = \sqrt{3^2+4^2} = 5 \]

\[\|\mathbf{y}\| = \sqrt{6^2+8^2} = 10 \]

归一化:

\[\hat{\mathbf{x}} = \left( \frac35,\frac45,0 \right) \]

即:

\[\hat{\mathbf{x}} = (0.6,0.8,0) \]

同理:

\[\hat{\mathbf{y}} = (0.6,0.8,0) \]

现在点积:

\[\hat{\mathbf{x}}\cdot\hat{\mathbf{y}} = 0.6\times0.6 + 0.8\times0.8 + 0\times0 \]

\[=0.36+0.64 \]

\[=1 \]

所以:

\[\boxed{similarity=1} \]

这意味着:

两个向量方向完全一致。


七、如果方向完全相反呢?

例如:

\[\mathbf{x}=(1,0) \]

\[\mathbf{y}=(-1,0) \]

夹角:

\[\theta=180^\circ \]

所以:

\[\cos180^\circ=-1 \]

因此:

\[\boxed{similarity=-1} \]

而如果:

\[\mathbf{x}=(1,0) \]

\[\mathbf{y}=(0,1) \]

那么:

\[\theta=90^\circ \]

所以:

\[\boxed{similarity=0} \]

因此标准 cosine similarity 的范围是:

\[\boxed{-1\leq similarity\leq1} \]

但实际 BGE embedding 的检索分数分布不会简单地覆盖整个 \([-1,1]\) 区间。BGE v1.5 的一个重要改动就是针对原版本的 similarity distribution 问题进行了调整。FlagEmbedding 的项目说明也明确指出,v1.5 是为了缓解 similarity distribution 问题并增强无 instruction 的检索能力。(GitHub)


八、第五层:BGE 的 Normalize 到底是什么?

我们可以直接看 bge-large-zh-v1.5 的模型结构。

Hugging Face 上这个模型的 modules.json 显示它包含:

Transformer
Pooling
Normalize

也就是:

\[\boxed{ Transformer \rightarrow Pooling \rightarrow Normalize } \]

(Hugging Face)

所以你可以把 BGE 的 embedding 流程理解成:

\[\text{文本} \]

\[\text{Tokenizer} \]

\[\text{Transformer} \]

\[\text{Token representations} \]

\[\text{Pooling} \]

\[\mathbf{x}\in\mathbb R^{1024} \]

\[\frac{\mathbf{x}}{\|\mathbf{x}\|} \]

\[\boxed{\hat{\mathbf{x}}} \]

\[\boxed{\hat{\mathbf{x}}^\top\hat{\mathbf{y}}} \]

这就是整个 embedding similarity 的数学主干。


九、Pooling 是怎么进入这个数学过程的?

这里非常关键。

Transformer 并不是直接吐出来一个:

\[1024 \]

维句向量。

它首先产生:

\[L\times1024 \]

的 token 表示。

例如一句话经过 tokenizer 后有 10 个 token,那么可以想象:

\[H= \begin{bmatrix} h_1\\ h_2\\ \vdots\\ h_{10} \end{bmatrix} \]

其中:

\[h_i\in\mathbb R^{1024} \]

因此:

\[H\in\mathbb R^{10\times1024} \]

Pooling 的任务就是:

把这一堆 token vectors 压缩成一个 sentence vector。

对于 BGE/Sentence Transformers 体系,模型配置中明确存在 Pooling 模块。(Hugging Face)

可以抽象理解为:

\[(h_1,h_2,\ldots,h_L) \rightarrow \mathbf{x} \]

然后:

\[\mathbf{x}\in\mathbb R^{1024} \]

最后再 Normalize。


十、所以“语义相似度”到底从哪里产生?

这里非常容易产生一个误解。

很多人会认为:

BGE 里面有一个“语义相似度计算器”。

实际上不是。

BGE 做的是:

\[\boxed{ \text{学习一个语义空间} } \]

也就是说,Transformer 通过训练学会:

什么样的文本应该在空间中靠近。

训练完成以后:

\[\text{文本} \rightarrow \text{空间中的向量} \]

然后相似度计算其实已经非常简单:

\[\boxed{ \text{向量之间的几何关系} } \]

所以真正复杂的是:

\[\boxed{ \text{如何训练出这个空间} } \]

而不是:

\[\boxed{ \text{如何计算两个向量的余弦} } \]

这一区分非常重要。


十一、从几何角度理解 BGE

你之前学习方向导数时,我们讨论过:

向量最重要的信息之一是“方向”。

现在这个思想可以直接迁移到 embedding。

假设:

\[\mathbf{x}\in\mathbb R^{1024} \]

你可以把它想象成 1024 维空间中的一个箭头。

一句:

“北京是中国的首都”

得到一个方向。

另一句:

“中国的首都是北京”

得到另一个方向。

如果训练得好:

\[\theta\approx0 \]

于是:

\[\cos\theta\approx1 \]

所以:

\[\boxed{ \text{语义相似} \Rightarrow \text{向量方向相近} } \]

这就是 embedding 模型最核心的几何思想。


十二、为什么是“方向”而不是“距离”?

假设:

\[\mathbf{x}=(1,2) \]

\[\mathbf{y}=(10,20) \]

欧氏距离:

\[d(\mathbf{x},\mathbf{y}) = \sqrt{9^2+18^2} \approx20.12 \]

看起来很远。

但:

\[\mathbf{y}=10\mathbf{x} \]

它们方向完全一致。

余弦:

\[\cos\theta=1 \]

所以:

\[\boxed{ \text{欧氏距离很大} } \]

并不意味着:

\[\boxed{ \text{语义差异很大} } \]

这也是 embedding 检索中 cosine similarity 非常重要的原因。


十三、为什么归一化以后“点积”和“余弦”完全等价?

这是整个问题最值得记住的数学恒等式。

原始点积:

\[\mathbf{x}^{T}\mathbf{y} \]

余弦:

\[\frac{\mathbf{x}^{T}\mathbf{y}} {\|\mathbf{x}\|\|\mathbf{y}\|} \]

定义:

\[\hat{\mathbf{x}} = \frac{\mathbf{x}}{\|\mathbf{x}\|} \]

\[\hat{\mathbf{y}} = \frac{\mathbf{y}}{\|\mathbf{y}\|} \]

那么:

\[\hat{\mathbf{x}}^T\hat{\mathbf{y}} = \left( \frac{\mathbf{x}}{\|\mathbf{x}\|} \right)^T \left( \frac{\mathbf{y}}{\|\mathbf{y}\|} \right) \]

把常数提出:

\[= \frac{\mathbf{x}^T\mathbf{y}} {\|\mathbf{x}\|\|\mathbf{y}\|} \]

所以:

\[\boxed{ \hat{\mathbf{x}}^T\hat{\mathbf{y}} = \cos(\mathbf{x},\mathbf{y}) } \]

因此 FlagEmbedding 的:

embeddings_1 @ embeddings_2.T

并不是“莫名其妙用矩阵乘法”。

它背后实际上是:

\[\boxed{ \text{L2 normalization} + \text{dot product} = \text{cosine similarity} } \]

官方文档也明确给出了 normalize_embeddings=True 后用矩阵乘法计算相似度的方式。(GitHub)


十四、FlagEmbedding 源码中的数学形式

FlagEmbedding 的 embedding 训练代码中可以看到非常直接的实现:

\[\texttt{p\_reps = normalize(p\_reps, dim=-1)} \]

随后:

\[\texttt{torch.matmul(q\_reps,p\_reps.transpose(...))} \]

也就是:

\[\boxed{ \hat Q\hat P^T } \]

源码中对应的核心计算就是 torch.matmul,而 representation 会先沿最后一个维度进行 normalize。(GitHub)

这和我们刚才的数学推导完全一致。


十五、为什么一定要 transpose

假设:

\[Q= \begin{bmatrix} q_1\\ q_2\\ q_3 \end{bmatrix} \]

\[P= \begin{bmatrix} p_1\\ p_2\\ p_3\\ p_4 \end{bmatrix} \]

其中:

\[q_i,p_j\in\mathbb R^{1024} \]

那么:

\[Q\in\mathbb R^{3\times1024} \]

\[P\in\mathbb R^{4\times1024} \]

我们希望得到:

\[\begin{bmatrix} q_1\cdot p_1&q_1\cdot p_2&q_1\cdot p_3&q_1\cdot p_4\\ q_2\cdot p_1&q_2\cdot p_2&q_2\cdot p_3&q_2\cdot p_4\\ q_3\cdot p_1&q_3\cdot p_2&q_3\cdot p_3&q_3\cdot p_4 \end{bmatrix} \]

也就是:

\[3\times4 \]

因此:

\[Q P^T \]

维度:

\[(3\times1024)(1024\times4) \]

结果:

\[\boxed{3\times4} \]

这一个矩阵同时完成了:

\[3\times4=12 \]

次 pairwise similarity。

所以向量数据库可以非常高效地进行批量相似度计算。


十六、这和向量数据库有什么关系?

现在你就可以理解 Chroma、FAISS、Milvus、Qdrant 等系统为什么喜欢 embedding。

假设你有:

\[1,000,000 \]

篇文档。

每篇:

\[\mathbf{p}_i\in\mathbb R^{1024} \]

用户输入 query:

\[\mathbf{q}\in\mathbb R^{1024} \]

那么理论上:

\[\mathbf{q}P^T \]

就可以一次得到:

\[1,000,000 \]

个 similarity。

即:

\[\begin{bmatrix} s_1&s_2&\cdots&s_{1000000} \end{bmatrix} \]

然后:

\[\operatorname{arg\,sort}(s) \]

找出最大的 Top-K。

于是:

\[\boxed{ Embedding \rightarrow Similarity \rightarrow Top-K \rightarrow RAG } \]

这就是现代语义检索的基本数学结构。


十七、BGE 的 Query Instruction 又是什么?

对于 BGE v1.5,官方推荐在 short query → long passage 的检索任务中,对 query 使用:

为这个句子生成表示以用于检索相关文章:

而 passage 不需要这个 instruction。(GitHub)

所以实际上:

\[q \rightarrow \text{instruction}+q \rightarrow BGE \rightarrow \mathbf q \]

而:

\[p \rightarrow BGE \rightarrow \mathbf p \]

最后:

\[\boxed{ similarity(q,p) = \hat{\mathbf q}^{T}\hat{\mathbf p} } \]

注意:

instruction 不是 similarity 计算公式的一部分。

它发生在:

\[\boxed{\text{文本}\rightarrow\text{embedding}} \]

这个阶段。

而 similarity 是:

\[\boxed{\text{embedding}\rightarrow\text{similarity}} \]

这两个阶段一定要分开理解。


十八、BGE embedding 与 BGE reranker 不要混淆

这对你目前学习 bge-large-zh-v1.5bge-reranker-v2-m3 尤其重要。

BGE Embedding

输入:

\[q \]

得到:

\[\mathbf q \]

输入:

\[p \]

得到:

\[\mathbf p \]

然后:

\[\boxed{ \mathbf q^T\mathbf p } \]

特点:

两个文本分别编码。

因此可以提前把百万篇文档全部编码:

\[p_1,p_2,\ldots,p_N \rightarrow \mathbf p_1,\mathbf p_2,\ldots,\mathbf p_N \]

存进向量数据库。


BGE Reranker

则不同。

它把:

\[(q,p) \]

一起送进模型:

\[(q,p) \rightarrow Transformer \rightarrow score \]

所以不能提前只计算 passage 的 embedding。

FlagEmbedding 的 reranker 示例也明确使用 compute_score([query, passage]) 这样的 pair 输入,并且其 score 可以通过 sigmoid 映射到 0~1。(GitHub)

因此经典 RAG:

\[\boxed{ BGE\ Embedding \rightarrow 召回100篇 \rightarrow BGE\ Reranker \rightarrow 重新排序 } \]

是两个完全不同的数学阶段。


十九、把整个过程压缩成一张“数学地图”

你现在可以把 bge-large-zh-v1.5 的相似度计算记成:

\[\boxed{ \text{文本} } \]

Tokenizer

\[tokens \]

Transformer

\[H\in\mathbb R^{L\times1024} \]

Pooling

\[\mathbf{x}\in\mathbb R^{1024} \]

L2 Normalize

\[\hat{\mathbf{x}} = \frac{\mathbf{x}}{\|\mathbf{x}\|} \]

两个文本

\[\hat{\mathbf{x}},\hat{\mathbf{y}} \]

Dot Product

\[\hat{\mathbf{x}}^T\hat{\mathbf{y}} \]

因为:

\[\|\hat{\mathbf{x}}\| = \|\hat{\mathbf{y}}\| = 1 \]

所以:

\[\boxed{ \hat{\mathbf{x}}^T\hat{\mathbf{y}} = \cos\theta } \]

最终:

\[\boxed{ \text{Semantic Similarity} \approx \text{Cosine Similarity} } \]


二十、你现在最值得掌握的 5 个数学公式

如果你的目标是真正理解 BGE,而不是只会调用 API,我建议把下面五个公式吃透:

① 向量长度

\[\boxed{ \|\mathbf{x}\| = \sqrt{\sum_i x_i^2} } \]

② 点积

\[\boxed{ \mathbf{x}^T\mathbf{y} = \sum_i x_i y_i } \]

③ 余弦相似度

\[\boxed{ \cos\theta = \frac{\mathbf{x}^T\mathbf{y}} {\|\mathbf{x}\|\|\mathbf{y}\|} } \]

④ L2 Normalization

\[\boxed{ \hat{\mathbf{x}} = \frac{\mathbf{x}}{\|\mathbf{x}\|} } \]

⑤ BGE 的最终 similarity

\[\boxed{ S = \hat Q\hat P^T } \]

其中:

\[Q\in\mathbb R^{N\times1024} \]

\[P\in\mathbb R^{M\times1024} \]

所以:

\[\boxed{ S\in\mathbb R^{N\times M} } \]

并且:

\[S_{ij} = \cos(q_i,p_j) \]


最后,把它和你正在学的“向量”联系起来

你前面学方向导数时,我们一直在建立一个非常重要的思想:

向量不仅仅是一组数字,它描述空间中的方向。

现在 embedding 正好把这个思想推到了 NLP:

\[\boxed{ \text{文字的“意义”} \rightarrow \text{高维空间中的方向} } \]

于是:

“人工智能是什么?”

“什么叫人工智能?”

如果 BGE 认为二者语义接近,那么它希望它们最终落在 embedding 空间中相近的方向:

\[\theta\rightarrow0 \]

于是:

\[\cos\theta\rightarrow1 \]

这就是为什么你前面学习的向量、点积、范数、单位向量、夹角、余弦、方向,实际上都可以自然地一路连接到今天的:

\[\boxed{ BGE\ Embedding\ Search } \]

而真正值得你下一步深入的是 “BGE 为什么能把语义变成方向?”——也就是从 Transformer 输出开始,一直追到 Pooling → Normalize → 对比学习损失 → temperature → in-batch negatives → 梯度如何推动“相似文本靠近、不相似文本远离”。FlagEmbedding 的训练代码正好把这一套数学结构暴露得很清楚:先 normalize,再计算矩阵点积,再除以 temperature,最后进入 cross-entropy。(GitHub)

这一步会把你目前正在学的向量 + 点积 + 梯度下降 + 神经网络真正串成一条完整的数学链。

posted @ 2026-09-23 22:23  立体风  阅读(4)  评论(0)    收藏  举报