可以。这个问题非常适合从你最近正在学的“向量 → 点积 → 余弦相似度 → 梯度/空间方向”这条数学线继续往下走。
对于 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.5 和 bge-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)
这一步会把你目前正在学的向量 + 点积 + 梯度下降 + 神经网络真正串成一条完整的数学链。