4. 余弦相似度:公式推导、计算步骤与优缺点分析
各位同学,今天我们来聊聊文本相似度计算里最经典、也最常用的一个方法——余弦相似度。
说实话,我刚入行做NLP那会儿,第一个接触的相似度算法就是它。当时觉得这名字挺唬人,什么余弦、什么向量,听着像高等数学。但真正用起来才发现,它的核心思想其实特别朴素——就是看两个向量的方向是不是一致。
好,咱们一步步拆开来看。
4.1 余弦相似度的直觉理解
先问大家一个问题:怎么判断两段文本“像不像”?
一种最直接的想法是:把文本变成数字,然后比较这些数字的“模式”。
余弦相似度就是这么干的。它把每段文本表示成一个向量(比如用词频向量、TF-IDF向量),然后计算这两个向量之间的夹角余弦值。
- 如果夹角是0度,余弦值为1,说明两段文本完全一致(方向相同)。
- 如果夹角是90度,余弦值为0,说明两段文本毫不相关(方向垂直)。
- 如果夹角是180度,余弦值为-1,说明两段文本意思完全相反(方向相反)。
你想想看,这不就是看两个箭头是不是指向同一个方向吗?
核心思想:余弦相似度关注的是向量的“方向”,而不是“长度”。
这意味着:即使两段文本的长度差异很大(比如一篇长文和一段摘要),只要它们的内容分布相似,余弦相似度依然会给出较高的分数。
这一点在实际项目中特别有用。我记得有一次做新闻去重,两篇报道一篇3000字、一篇500字,但讲的是同一件事。如果用欧氏距离,结果差得离谱;换成余弦相似度,一下子就匹配上了。
4.2 公式推导:从几何到代数
好,咱们来点硬核的。公式推导其实不复杂,我尽量用大白话讲清楚。
4.2.1 二维空间中的余弦定理
先回忆一下初中数学。在二维平面上,有两个向量 A 和 B,它们之间的夹角为 θ。根据余弦定理:
cos(θ) = (A·B) / (||A|| × ||B||)
其中:
- A·B 是向量的点积(内积)
- ||A|| 是向量 A 的模长(L2范数)
举个例子:
A = [3, 4]
B = [1, 2]
点积:A·B = 3×1 + 4×2 = 3 + 8 = 11
模长:||A|| = √(3² + 4²) = √25 = 5
||B|| = √(1² + 2²) = √5 ≈ 2.236
cos(θ) = 11 / (5 × 2.236) ≈ 11 / 11.18 ≈ 0.984
这个0.984非常接近1,说明两个向量的方向几乎一致。
4.2.2 推广到n维空间
文本向量可不是二维的,动不动就几千维、几万维。但公式完全一样:
cos(θ) = (∑(A_i × B_i)) / (√(∑A_i²) × √(∑B_i²))
其中 i 从1到n,n是向量的维度。
说白了,就是:分子是各维度乘积之和,分母是两个向量模长的乘积。
个人习惯:我一般把余弦相似度的计算拆成三步:
- 算点积(分子)
- 算各自模长(分母的一部分)
- 做除法
这样不容易出错,也方便调试。
4.3 计算步骤:手把手教你算
咱们用一段实际代码来演示。假设有两段文本:
文本A: "我喜欢吃苹果"
文本B: "我不喜欢吃苹果"
第一步:构建词频向量
先分词,然后统计每个词出现的次数:
词汇表: ["我", "喜欢", "吃", "苹果", "不"]
向量A: [1, 1, 1, 1, 0] (没有"不")
向量B: [1, 0, 1, 1, 1] (没有"喜欢")
第二步:计算点积
A·B = 1×1 + 1×0 + 1×1 + 1×1 + 0×1 = 1 + 0 + 1 + 1 + 0 = 3
第三步:计算模长
||A|| = √(1² + 1² + 1² + 1² + 0²) = √4 = 2
||B|| = √(1² + 0² + 1² + 1² + 1²) = √4 = 2
第四步:计算余弦相似度
cos(θ) = 3 / (2 × 2) = 3 / 4 = 0.75
嗯,0.75。这两句话确实有相似之处(都提到“我”、“吃”、“苹果”),但意思完全相反(一个喜欢、一个不喜欢)。余弦相似度只捕捉到了词汇层面的重叠,没捕捉到语义上的否定关系。
注意:这就是余弦相似度的一个典型局限——它不考虑词序和语法结构,只关心“有没有这些词”。
我曾经在一个情感分析项目里吃过这个亏。两句话“这部电影不差”和“这部电影很差”,词频向量几乎一样,余弦相似度高达0.9以上,但情感极性完全相反。
4.4 优缺点分析
聊完了怎么算,咱们来客观评价一下这个方法的得失。
4.4.1 优点
| 优点 | 说明 |
|---|---|
| 不受文本长度影响 | 只关注方向,不关注模长。长文本和短文本可以公平比较。 |
| 计算简单高效 | 就是一次点积加两次开方,时间复杂度O(n),适合大规模计算。 |
| 结果直观易解释 | 值域在[-1, 1]之间,0.8就是挺像,0.2就是不太像,很好理解。 |
| 对稀疏向量友好 | 文本向量通常很稀疏(大部分维度是0),余弦相似度天然适合这种场景。 |
4.4.2 缺点
| 缺点 | 说明 |
|---|---|
| 忽略词序和语法 | "猫追老鼠"和"老鼠追猫"的向量完全一样,余弦相似度为1,但意思天差地别。 |
| 无法处理同义词 | "汽车"和"轿车"在词袋模型中是两个不同的维度,余弦相似度会认为它们毫无关系。 |
| 对高频词敏感 | 如果文本里充斥着"的"、"了"、"是"这类停用词,它们会主导相似度计算,掩盖真正有意义的词汇。 |
| 无法捕捉语义反转 | 如前面例子所示,"喜欢"和"不喜欢"在向量上差异很小,但语义差异很大。 |
避坑指南:我曾经在一个文本聚类项目里,直接用原始词频做余弦相似度,结果聚类效果一塌糊涂。后来加上了TF-IDF加权和停用词过滤,效果才明显提升。
所以我的建议是:别裸用余弦相似度,一定要配合好的向量表示方法(TF-IDF、词嵌入等)一起使用。
4.5 什么时候该用它?
根据我的经验,余弦相似度最适合以下场景:
- 短文本匹配:比如搜索query和文档标题的匹配
- 去重任务:新闻去重、商品去重等
- 推荐系统:计算用户兴趣向量和物品向量的相似度
- 聚类前置步骤:作为距离度量,配合K-Means等算法使用
但如果你的任务涉及语义理解(比如问答系统、情感分析),我建议你考虑更高级的方法,比如基于BERT的语义相似度模型。余弦相似度在这些场景下,只能作为一个baseline。
好了,关于余弦相似度,咱们就聊到这儿。下一节我会讲另一个经典方法——Jaccard相似度,它和余弦相似度有什么不同?各自适合什么场景?到时候咱们再细聊。