文字也有角度?搜索引擎与向量相似度

孙博 1 次阅读 技术分享
搜索引擎 向量相似度

想象一个场景:用户搜索"苏州园林",系统要从一堆景点介绍中找出最相关的。

有四条候选文本:

  • A:"拙政园,苏州的古典园林之首"

  • B:"留园,苏州四大名园之一,苏州著名景点"

  • C:"颐和园,北京皇家园林"

  • D:"西湖,杭州著名景点"

直觉告诉我们:A 最相关(直接提到"苏州"和"园林"),D 完全不相关。但 B 和 C 呢?哪个更相关?

机器怎么判断这种"相关程度"?怎么量化"最像"?

让我们从最简单的方法开始,一步步找到答案。

先试试最简单的方法:数共同词

查询"苏州园林"有两个关键词:苏州园林

文本 包含"苏州"? 包含"园林"? 共同词数
A 2
B 1
C 1
D 0

看起来不错——A 最相关,D 无关。但 B 和 C 都是 1 个共同词,怎么排序?

  • B(留园):有"苏州",但没有"园林"(用的是"名园")

  • C(颐和园):有"园林",但没有"苏州"(用的是"北京")

简单的计数无法区分这种差异。我们需要一种更精细的方法。

把文本变成向量

从"计数"到"向量":为什么要这样做?

数共同词的方法有个根本问题:它把文本看作一个"集合",只关心"有什么词",不关心"每个词有多重要"。

但直觉告诉我们,如果一条文本反复提到"苏州",说明它更专注于这个话题。我们应该把这种"强度"考虑进去。

怎么做到?我们需要一种新的表示方式——向量

向量不仅能记录"有什么词",还能记录"每个词出现了多少次"。更重要的是,一旦文本变成了向量,我们就可以将搜索词和文档都向量化,然后比较两个向量的"方向"是否相同、内容是否相似,以此判断是不是搜索目标文档。

具体怎么做?

先把四条文本分词:

  • A:拙政园 / 苏州 / 古典 / 园林 / 之首

  • B:留园 / 苏州 / 四大 / 名园 / 之一 / 苏州 / 著名 / 景点

  • C:颐和园 / 北京 / 皇家 / 园林

  • D:西湖 / 杭州 / 著名 / 景点

收集所有不重复的词,构成一个词典

拙政园苏州古典园林之首留园四大名园之一颐和园北京皇家西湖杭州著名景点

共 16 个词。每个词就是一个维度

现在,把每条文本表示成一个 16 维向量——某个词出现几次,对应位置就填几(查询关键词"苏州园林"用 Q 标记):

:向量的每个分量代表对应词的词频。比如 B 中"苏州"出现 2 次,所以第 2 维是 2。词频高,说明这个话题对文本更重要。

文本 拙政园 苏州 古典 园林 之首 留园 四大 名园 之一 颐和园 北京 皇家 西湖 杭州 著名 景点
Q 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0
A 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0
B 0 2 0 0 0 1 1 1 1 0 0 0 0 0 1 1
C 0 0 0 1 0 0 0 0 0 1 1 1 0 0 0 0
D 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1

以查询 Q 为例:分词后得到"苏州"和"园林"两个词。在 16 维词典中,"苏州"是第 2 维,"园林"是第 4 维,所以 Q 的向量是:

Q=(0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0)\vec{Q} = (0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

每条文本都变成了空间中的一个。Q、A、B、C、D 都是 16 维空间中的向量。

向量的两个属性:大小和方向

向量有两个属性:

  • 大小(长度):向量的"模",记作 A|\vec{A}|,代表向量的"强度"

  • 方向:向量在空间中的指向

在文本场景中:

  • 大小:向量的长度大致代表文档的"信息量"——长文档的向量更长,短文档的向量更短

  • 方向:向量的方向代表文档的"主题分布"——如果两篇文档的词频比例相似,它们的方向就相近

这就是向量化的好处:它不仅记录了"有什么词",还记录了"每个词有多重要"。

怎么衡量两个向量的相似度?

第一步:从二维空间的"方向"开始

16 维空间太抽象了,我们先从熟悉的二维空间说起。

在二维平面上,一个向量就是一个箭头——从原点出发,指向某个方向。比如 A=(1,2)\vec{A} = (1, 2) 就是从原点指向坐标 (1,2)(1, 2) 的箭头。

怎么直观理解向量的"方向"? 把向量看成直角三角形的斜边——从原点出发,过向量顶点向 X 轴做垂线,就形成了一个直角三角形。

向量 A=(1,2)\vec{A} = (1, 2):底 = 1,高 = 2,斜率 = 2

如果我们把词频比例看作向量的方向比例,如果两段文本提及了同一个词,那么在这个词的维度上,它们应当有着一致的分量。如果它们提及的所有关键词的词频一致,是不是就可以直接理解为向量的方向一致了?

既然在二维平面上,我们可以用直角三角形来表示向量,那么两个直角三角形如果相似(对应边成比例),它们的方向自然相同。反过来,方向相同的两个向量,对应的三角形也必然相似。

第二步:回忆一下相似三角形

两个三角形相似,意味着:形状相同,大小可以不同

看几个例子:

  • A=(1,2)\vec{A} = (1, 2)B=(2,4)\vec{B} = (2, 4):B 的每个分量都是 A 的 2 倍——方向完全相同,只是长度翻倍。

  • A=(1,2)\vec{A} = (1, 2)C=(3,6)\vec{C} = (3, 6):C 的每个分量都是 A 的 3 倍——方向相同,长度变成 3 倍。

  • A=(1,2)\vec{A} = (1, 2)D=(2,1)\vec{D} = (2, 1):长度相同,但方向完全不同——形状不一样。

"相似"的核心就是方向一致——就像相似三角形一样,形状相同,大小无所谓。

把向量看成直角三角形的斜边——从原点出发,过向量顶点向 X 轴做垂线:

  • 向量 A=(1,2)\vec{A} = (1, 2):底 = 1,高 = 2,斜率 = 2

  • 向量 C=(2,4)\vec{C} = (2, 4):底 = 2,高 = 4,斜率 = 2

两个向量的斜率相等——这意味着它们与 X 轴的夹角相等。对于两个直角三角形,一个锐角相等,再加上直角也相等,根据 AA 定理(两角对应相等),它们就是相似三角形。

向量与相似三角形
图:向量 A=(1,2)\vec{A}=(1,2)C=(2,4)\vec{C}=(2,4) 分别构成两个直角三角形。小三角形(蓝)套在大三角形(红)里面,斜率都是 2——这就是相似三角形。斜率相等 = 方向相同 = 向量相似。

第三步:怎么判断"方向一致"?

最直接的方式:算它们之间的夹角

  • 夹角 = 0°:方向完全一致

  • 夹角 = 90°:各走各的路,毫无关联

  • 夹角 = 180°:方向完全相反

这就是余弦相似度的核心思想——用夹角的余弦值来衡量方向的接近程度:

cos(θ)=ABA×B\cos(\theta) = \frac{\vec{A} \cdot \vec{B}}{|\vec{A}| \times |\vec{B}|}
  • 分子是点积

  • 分母是两个向量长度的乘积:用来归一化,消除长度影响

为什么用余弦,而不是其他函数?

函数 范围 0° 时 90° 时 适合做相似度?
cos [-1, 1] 1(最相似) 0(不相关) ✅ 完美
sin [-1, 1] 0 1 ❌ 反直觉
tan (-∞, +∞) 0 ❌ 无界
点积 [0, +∞) - - ❌ 受长度影响

余弦是唯一一个同时满足以下条件的函数:

  1. 有界:值域 [-1, 1],适合做相似度

  2. 直觉一致:0° 时最大(最相似),90° 时为 0(不相关)

  3. 消除长度影响:通过除以长度乘积,只看方向

第四步:那"距离"为什么不行?

直接用欧氏距离不行吗?两个点越近,不就越相似吗?

看这个例子:A=(1,2),B=(2,1),C=(0,3)\vec{A} = (1, 2), \quad \vec{B} = (2, 1), \quad \vec{C} = (0, 3)

算一下距离:d(A,B)=d(A,C)=21.41d(A, B) = d(A, C) = \sqrt{2} \approx 1.41

A 到 B 的距离 = A 到 C 的距离。 但 B 指向右下方,C 指向上方——方向完全不同!

两个向量可以距离相等,但方向完全不同。 如果我们想衡量的是"说的是不是同一件事",那距离就不是一个好的指标。

向量的方向与距离

图:A=(1,2),B=(2,1),C=(0,3),d(A,B)=d(A,C)=2\vec{A}=(1,2), \vec{B}=(2,1), \vec{C}=(0,3), d(\vec{A},\vec{B}) = d(\vec{A},\vec{C}) = \sqrt{2},但 B\vec{B}C\vec{C} 的方向完全不同。

第五步:推广到高维

好消息是:数学公式完全一样

16 维空间中,计算方式完全一样——只是维度更多,求和的项更多。

公式不需要改,直觉也不需要改。 高维空间只是"画不出来"而已,数学上没有任何区别。

三维空间下的向量相似度

图:三维空间中,向量 A=(1,2,3)\vec{A}=(1,2,3)B=(2,4,6)\vec{B}=(2,4,6) 分别构成两个直角三角形。小三角形(蓝)套在大三角形(红)里面,B=2A\vec{B}=2\vec{A},比例相等 → 方向相同。

小结

度量方式 受长度影响? 适合场景
欧氏距离 长度有意义时
余弦相似度 只关心方向时

在文本场景中,我们关心的是"说的是不是同一件事",而不是"篇幅是不是一样长"。所以余弦相似度更合适。

余弦值 含义
1 方向完全一致(说的是同一件事)
0 方向完全垂直(毫无关联)
-1 方向完全相反(文本中几乎不会出现)

对于文本场景,词频都是非负的,所以余弦值的实际范围是 [0, 1]。

动手算一下

让我们用上面的例子,实际计算查询与四条文本的余弦相似度。

查询 vs A(拙政园)

查询向量

Q=(0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0)\vec{Q} = (0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

A 向量

A=(1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0)\vec{A} = (1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

第一步:点积

QA=0×1+1×1+0×1+1×1+0×1+0+=2\vec{Q} \cdot \vec{A} = 0\times1 + 1\times1 + 0\times1 + 1\times1 + 0\times1 + 0 + \ldots = 2

第二步:各自的长度

Q=02+12+02+12+02+=21.414|\vec{Q}| = \sqrt{0^2 + 1^2 + 0^2 + 1^2 + 0^2 + \ldots} = \sqrt{2} \approx 1.414

A=12+12+12+12+12+02+=52.236|\vec{A}| = \sqrt{1^2 + 1^2 + 1^2 + 1^2 + 1^2 + 0^2 + \ldots} = \sqrt{5} \approx 2.236

第三步:余弦相似度

cos(θ)=22×5=21023.1620.632\cos(\theta) = \frac{2}{\sqrt{2} \times \sqrt{5}} = \frac{2}{\sqrt{10}} \approx \frac{2}{3.162} \approx 0.632

其他文本的计算结果

用同样的方法计算 B、C、D:

文本 点积 向量长度 余弦相似度
A(拙政园) 2 2.236 0.632
B(留园) 2 3.162 0.447
C(颐和园) 1 2 0.354
D(西湖) 0 2 0

结果汇总

文本 匹配词 余弦相似度 解读
A(拙政园) 苏州 + 园林 0.632 最相关——两个词都匹配
B(留园) 苏州(×2) 0.447 次相关——"苏州"出现两次
C(颐和园) 园林 0.354 较弱——只匹配"园林"
D(西湖) 0 完全不相关

0.632>0.447>0.354>00.632 > 0.447 > 0.354 > 0

这个梯度基本符合直觉:A 最相关,B 次之("苏州"出现两次),C 再次之,D 无关。

回头看:B 和 C 为什么能区分了?

之前朴素计数无法区分 B 和 C——它们都只包含 1 个查询关键词。但余弦相似度给出了不同的分数:

B(0.447)>C(0.354)B(0.447) > C(0.354)

为什么 B 比 C 高?因为 B 中"苏州"出现了两次。余弦相似度不仅看"匹配了什么",还看"匹配的词在文本中的权重"。B 反复提到"苏州",说明它更专注于这个话题。

反复提到的关键词,在向量中权重更高。 这就是余弦相似度相比朴素计数的优势——它不仅看"有没有这个词",还看"这个词有多重要"。

一个极端的例子:重复字符串

为了更直观地理解余弦相似度"只看方向,不看长度"的特性,我们来看一个极端的例子。

假设有两条文本:

  • 文本 X:"苏州拙政园"

  • 文本 Y:"苏州拙政园苏州拙政园苏州拙政园"

Y 是 X 的重复 3 次。假设词典只有两个词:苏州拙政园

  • X 的向量:(1,1)(1, 1)

  • Y 的向量:(3,3)(3, 3)

计算余弦相似度:

cos(X,Y)=1×3+1×32×18=636=66=1\cos(X, Y) = \frac{1\times3 + 1\times3}{\sqrt{2} \times \sqrt{18}} = \frac{6}{\sqrt{36}} = \frac{6}{6} = 1

余弦相似度 = 1,完全相似!

余弦不关心你说了多少,只关心你在说什么。 一篇 50 字的简介和一篇 5000 字的攻略,如果词频分布相同,余弦相似度就是 1。

从短文本到长文档

上面的例子用了 4 条短文本和 16 个词的词典。实际搜索场景中:

  • 词典可能有 10 万个词

  • 每篇文档可能有 几百到几千字

  • 向量是 10 万维的,绝大多数位置是 0(稀疏向量)

但核心思想完全一样:

  1. 分词 → 构建词典

  2. 统计词频 → 得到每篇文档的向量

  3. 计算查询向量和每篇文档向量的余弦相似度

  4. 取最高的 K 个返回

这就是向量空间模型(Vector Space Model,VSM)——由 Gerard Salton 在 1970 年代提出,是现代信息检索的基石。

向量空间模型的核心贡献:把"文本相似度"问题,转化成了"向量夹角"问题。

实际工程中的两个改进

上面的例子中,向量的每个维度是简单的词频计数。但在实际工程中,通常会做两个改进:

改进一:强度不是简单的词频计数

我们前面用的向量,每个维度的值是简单的词频计数——某个词出现几次,就填几。

但在实际工程中,向量的每个维度通常不是简单的词频,而是 TF-IDFBM25 的权重。这些算法会给"高频但无意义的词"降权,给"低频但有区分度的词"加权——这正是我们在上一篇中讨论的内容。

也就是说,向量的"方向"不仅取决于"有什么词",还取决于"每个词有多重要"。

改进二:用 Embedding 代替词袋向量

词袋向量有个根本性问题:它只能识别"用词相同"的文本,无法理解语义。

举个例子:查询"苏州园林",候选文本是"江南古典名园"。

  • 分词后:苏州 / 园林 vs 江南 / 古典 / 名园

  • 没有任何一个词相同

  • 在词袋模型中,余弦相似度 = 0

但直觉告诉我们,这两者说的是同一类东西。词袋模型无法捕捉这种语义关联。

现代搜索引擎引入 Embedding(词嵌入/句嵌入)来解决这个问题。Embedding 用神经网络把文本映射到一个语义空间——在这个空间中,意思相近的文本,向量方向相近,不需要字面匹配。

我们用实际的 Embedding 模型来验证一下。以 BAAI/bge-m3 为例,它会把每个文本映射为一个 1024 维的向量。

查询"苏州园林",对比两个候选:

候选 余弦相似度
江南古典名园 0.4955
北京故宫博物院 0.4392

具体计算过程(以"苏州园林" vs "江南古典名园"为例):

  • "苏州园林"的向量(前 5 维):(0.057,0.016,0.066,0.038,0.017,)(-0.057, 0.016, -0.066, -0.038, -0.017, \ldots)

  • "江南古典名园"的向量(前 5 维):(0.025,0.015,0.035,0.017,0.017,)(-0.025, -0.015, -0.035, -0.017, -0.017, \ldots)

  • 点积 = 0.49550.4955

  • 两个向量的模长均为 1.0001.000(模型默认做了 L2 归一化)

  • 余弦相似度 = 0.49551.000×1.000=0.4955\frac{0.4955}{1.000 \times 1.000} = 0.4955

在词袋模型中,"苏州园林"和"江南古典名园"的相似度是 0(没有共同词)。但在 Embedding 空间中,相似度是 0.4955——Embedding 捕捉到了它们在语义上的关联。

这就是 Embedding 的核心优势:它能理解"说的是不是同一件事",而不仅仅是"用的是不是同一个词"。

写在最后

回到开头的场景:用户搜索"苏州园林",系统要从一堆景点介绍中找出最相关的。

现在我们知道,搜索引擎是怎么做的:

  1. 把查询和文档都变成向量

  2. 用余弦相似度计算两个向量的方向是否一致

  3. 取最相似的那几个返回

从"数共同词"到"余弦相似度"——这就是搜索引擎的向量思维。

至于词袋向量的局限,以及 Embedding 如何解决这个问题,我们已经在"实际工程中的两个改进"中讨论过了。后续文章会详细介绍。