五种相似度:欧氏、曼哈顿、余弦、Jaccard、相关系数。公式都不长, 可它们回答的是五个不同的问题。 选错了不是「差一点」,是「换了个答案」。这一节用四组实测把这句话坐实:同一批数据, 五种算法给出不同的最近邻。
这一节是第 7 章的前置。 k-means、kNN 这些算法的第一步都是「算相似度、取最近的几个」,底下用的就是这几个函数。 它也是第 10 章人脸比对、第 12 章检索的前置。 那些地方模型再复杂,最后一步仍然是「算个距离、比个大小」。
一、五种做法,各看一头
| 名字 | 怎么算 | 看的是什么 |
|---|---|---|
| 欧氏距离 | 逐维差平方求和再开根号 | 直线距离 |
| 曼哈顿距离 | 逐维差的绝对值求和 | 走直角路的距离 |
| 余弦相似度 | 点积除以两个长度 | 方向,不看长度 |
| Jaccard | 交集除以并集 | 两堆集合的重合度 |
| 相关系数 | 减均值后再算余弦 | 形状 |
| 这五个都叫「相似」,但它们回答的问题不一样。欧氏问「离多远」,余弦问「朝哪个方向」, | ||
| 相关系数问「起伏像不像」,Jaccard 问「重合多少」。 | ||
| 换句话说:相似度是你对「像」下的定义,算法只是把这个定义算出来。 | ||
| 定义下错了,后面再精细的模型也救不回来。 |
二、余弦不看长度
Demo 第二段:d1 = (2, 1, 0, 0),d2 = (4, 2, 0, 0) —— d2 就是 d1 抄了两遍,
d3 = (0, 0, 3, 3) 是完全另一篇。
| 欧氏距离 | 余弦 | |
|---|---|---|
| d1 | 1.0000 | 0.9487 |
| d2 | 3.1623 | 0.9487 |
| d3 | 4.4721 | 0.0000 |
| 余弦那一列 d1 和 d2 一模一样(0.9487)。因为余弦只算夹角, | ||
| d2 只是把 d1 拉长了一倍,方向没变。欧氏那一列差了一截:1.0000 对 3.1623。 | ||
| 哪一列对?看你要什么。做搜索排序时,长文档不该因为长就吃亏,所以要余弦; | ||
| 做聚类时,量的大小本身是信息(买 10 件和买 1 件不一样),就要欧氏。 | ||
| 余弦这个性质还带来一个副作用:它算不出「零向量」。长度是 0 的时候分母是 0,只能特殊处理。 | ||
| 真实系统里那些「什么都没买」的用户,余弦对他们是没定义的 —— | ||
| 这就是第 2 章讲冷启动时那个尴尬的另一面。 |
三、量纲一变,最近的就换人
Demo 第三段:两套房,两个特征 —— 总价(万元)和面积(平方米)。 待估的 Q = (300, 90),已知 A = (310, 100)、B = (301, 115)。
| 单位 | 到 A | 到 B | 谁更近 |
|---|---|---|---|
| 万元 | 14.1 | 25.0 | A |
| 元 | 100000 | 10000 | B |
| 换个单位,答案就翻了。原因很直白:总价的差是十万量级,面积的差是十几量级, | |||
| 平方一加,面积那一项直接被吃掉了。 | |||
| 这不是数学问题,是「单位选得好不好」的问题。把每一维各自标准化(减均值、除标准差)之后, | |||
| 量纲被抹掉,换单位不再改变结论 —— 这一点是硬的。 | |||
| 但标准化不是万能钥匙。Demo 里只有三个点,标准化后两个距离是 2.4274 和 2.4435, | |||
| 只差 0.016,几乎分不出来 —— 因为三个点算出来的标准差本身就很不稳。 | |||
| 标准化要求样本够多、够像一回事;样本少的时候,它只是把噪声换了个单位。 |
四、相关系数:先减均值,再算余弦
Demo 第四段用三条五天序列做对比:
| 比较 | 余弦 | 相关系数 |
|---|---|---|
| s1 与 s1 每个数加 100 | 0.9103 | 1.0000 |
| s1 与 s1 倒过来 | 0.6364 | −1.0000 |
| s1 与一串没关系的数 | 0.8602 | 0.3536 |
| 第一行最能说明问题:s2 只是 s1 每个数加了 100,形状完全一样。余弦给 0.9103 —— 看着还挺像, | ||
| 可那点像来自「都在原点右上方」这个事实。相关系数给 1.0000:平移多少都不影响。 | ||
| 第二行是反向:余弦 0.6364 是个正数,看不出它们在反着走;相关系数 −1.0000,一眼就知道完全相反。 | ||
| 差别就在减均值这一步。余弦把「离原点多远」也当成信息,相关系数只问「相对于各自的平均水平, | ||
| 起伏形状一样吗」。所以看趋势、看同步性要用相关系数;看强度、看占比要用余弦。 |
五、Jaccard:只数有没有
Demo 第五段三个购物车:u1 买了相机、镜头、三脚架;u2 买了镜头、三脚架、摄影包; u3 买的是咖啡豆和咖啡机。
| 比较 | 交集 | 并集 | Jaccard |
|---|---|---|---|
| u1 与 u2 | 2 个 | 4 个 | 0.5000 |
| u1 与 u3 | 0 个 | 5 个 | 0.0000 |
| u2 与 u3 | 0 个 | 5 个 | 0.0000 |
| Jaccard 只看「有没有」,不看「有几个」。买两个镜头和买一个镜头,对它来说是同一件事。 | |||
| 所以它适合「标签、兴趣、看过没看过」这类只有有无的数据,不适合「买了几个」这种有次数的数据 —— | |||
| 后者该用余弦。 | |||
| 这个区别在真实系统里经常被搞混。用户标签体系里,「喜欢篮球」和「喜欢篮球三次」是两种数据, | |||
| 用错了度量,推荐结果会系统性地偏向活跃用户。 |
六、配不配,比好不好重要
| 你的数据长什么样 | 该用 |
|---|---|
| 词频、点击次数(只关心比例) | 余弦 |
| 长宽高、温度压力(关心绝对差) | 欧氏,先标准化 |
| 标签、兴趣(只有有无) | Jaccard |
| 两条随时间变化的曲线 | 相关系数 |
| 网格上走路的代价 | 曼哈顿 |
| 曼哈顿那一条值得单独说一句。它算的是「只能横平竖直走」的距离, | |
| 所以它天然适合网格 —— 城市路网、棋盘、集成电路布线。在那些场景里, | |
| 欧氏距离算出来的「直线捷径」根本走不通。 | |
| 这一节真正要记住的不是五个公式。是这句话:相似度是你对「像」下的定义。第 7 章的聚类、 | |
| 第 10 章的人脸比对、第 12 章的检索,底下用的都是这五个函数里的某一个 —— 选哪个,才是真功夫。 |
七、动手 Demo:五种「像」,五个答案
五个函数都很短,cos 和 pear 甚至共用一套代码 —— 相关系数就是「减掉均值之后再算余弦」。
四段数据分别是:两篇文档和一个查询、三套房子、三条五天序列、三个购物车。
每一段都故意让两种度量给出不同答案,差别才看得出来。
def eu(a, b): # 欧氏
return float(np.sqrt(sum((x - y) ** 2 for x, y in zip(a, b))))
def man(a, b): # 曼哈顿
return float(sum(abs(x - y) for x, y in zip(a, b)))
def cos(a, b): # 余弦
na = sum(x * x for x in a) ** 0.5
nb = sum(x * x for x in b) ** 0.5
if na == 0 or nb == 0:
return 0.0
return sum(x * y for x, y in zip(a, b)) / (na * nb)
def jac(a, b): # Jaccard
A, B = set(a), set(b)
return len(A & B) / float(len(A | B))
def pear(a, b): # 相关系数 = 减均值后的余弦
a = np.array(a, dtype=float); a = a - a.mean()
b = np.array(b, dtype=float); b = b - b.mean()
return cos(a, b)
实跑输出:
--------------------------------------------------------------------
怎么算「像」
--------------------------------------------------------------------
==============================================================
[1] 五种做法,各看一头
名字 怎么算 看的是什么
------------------------------------------------------------------------
欧氏距离 逐维差平方求和再开根号 直线距离,越小越像
曼哈顿距离 逐维差的绝对值求和 走直角路的距离
余弦相似度 点积除以两个长度 方向,不看长度
Jaccard 交集大小除以并集大小 两堆集合的重合度
相关系数 先各自减去均值再算余弦 形状,不看位置和幅度
五种都叫「相似」,可它们回答的问题不一样。
选错哪一个,结果不是「差一点」,是「换了个答案」。下面逐个数。
[2] 余弦不看长度:把一篇文档复制一遍,相似度还是 1
四篇文档的词频向量(四个词的计数):
d1 = (2, 1, 0, 0) d2 = (4, 2, 0, 0) —— d2 是 d1 抄了两遍
d3 = (0, 0, 3, 3) 查询 q = (1, 1, 0, 0)
欧氏距离 余弦 曼哈顿
--------------------------------------------
d1 1.0000 0.9487 1.0000
d2 3.1623 0.9487 4.0000
d3 4.4721 0.0000 8.0000
余弦那一列:d1 和 d2 都是 0.9487 —— 一模一样。
因为它只算夹角,d2 只是把 d1 拉长了一倍,方向没变。
欧氏那一列:d1 是 1.0000,d2 是 3.1623,差了一截。
哪一列对?看你要什么。做搜索排序时,长文档不该因为长就吃亏,
所以要余弦;做聚类时,量的大小本身是信息,就要欧氏。
[3] 量纲一变,最近的那个就换人了
两套房,两个特征:总价(万元)、面积(平方米)。
待估的 Q = (300, 90),已知的 A = (310, 100)、B = (301, 115)。
单位 到 A 到 B 谁更近
--------------------------------------------------------
万元/平方米 14.1 25.0 A
元/平方米(总价乘一万) 100000 10000 B
换个单位,答案就翻了。原因很直白:总价的差是十万量级,
面积的差是十几量级,平方一加,面积那一项直接被吃掉了。
把每一维各自标准化(减均值、除标准差)之后:
标准化后到 A 标准化后到 B
------------------------------------------------
再看距离 2.4274 2.4435
标准化之后每一维都是「差几个标准差」,量纲被抹掉了,
所以换单位不会改变结论 —— 这一点是硬的。
但注意两个距离只差 0.0160,几乎分不出来。
原因很简单:只有三个点,标准差本身估得很糙,标准化也就糙。
标准化不是万能钥匙,它要求样本本身够多、够像一回事。
这也解释了为什么真实的特征工程里,先看数据的分布,再决定怎么缩放。
[4] 相关系数:先减去均值,再算余弦
五天的两组数据,看看它们「像不像」:
比较 余弦 相关系数
----------------------------------------------------
s1 和 s2(整体加了 100) 0.9103 1.0000
s1 和 s3(正好倒过来) 0.6364 -1.0000
s1 和 s4(没关系) 0.8602 0.3536
s1 和 s2 只差一个平移:s2 就是 s1 每个数加 100。
余弦是 0.9103,看着还挺像 —— 可那点像来自「都在原点右上方」。
相关系数是 1.0000:平移多少都不影响,形状完全一样。
s1 和 s3 的余弦是 0.6364,一个正数,看不出它们在反向走;
相关系数是 -1.0000,一眼就知道完全反向。
差别就在减均值这一步:余弦把「离原点多远」也当成了信息,
相关系数只问「相对于各自的平均水平,起伏形状一样吗」。
[5] Jaccard:只数「共同的」有多少
三个人的购物车:
u1:相机、镜头、三脚架
u2:镜头、三脚架、摄影包
u3:咖啡豆、咖啡机
比较 交集 并集 Jaccard
--------------------------------------------------------------
u1 与 u2 2 个 4 个 0.5000
u1 与 u3 0 个 5 个 0.0000
u2 与 u3 0 个 5 个 0.0000
Jaccard 只看「有没有」,不看「有几个」。
买两个镜头和买一个镜头,对 Jaccard 来说是同一件事。
所以它适合「标签、兴趣、看过没看过」这类只有有无的数据,
不适合「买了几个」这种有次数的数据 —— 后者该用余弦。
[6] 没有最好的相似度,只有配不配
你的数据长什么样 该用
------------------------------------------------------------
词频、点击次数(只关心比例) 余弦
长宽高、温度压力(关心绝对差) 欧氏,先标准化
标签、兴趣(只有有无) Jaccard
两条随时间变化的曲线 相关系数
网格上走路的代价 曼哈顿
这一节真正要记住的不是五个公式,是这句话:
相似度是你对「像」下的定义,算法只是把这个定义算出来。
定义下错了,后面再精细的模型也救不回来。
第 7 章的 k-means、kNN、第 10 章的人脸比对、第 12 章的检索,
底下用的都是这五个函数里的某一个 —— 选哪个,才是真功夫。
第一段那张表里,五个名字下面其实是四种「看的角度」:距离(欧氏、曼哈顿)、方向(余弦)、 重合(Jaccard)、形状(相关系数)。曼哈顿和欧氏是一家的两种走法,其余三个各看一头。 第二段 d1 和 d2 余弦相同这件事,很多人第一次看会觉得是 bug。不是 —— 词频向量里「长文档」和「短文档」讲同一件事,余弦认为它们像,这正是搜索排序想要的行为。 第三段那个翻转是这一节的核心。同一批数据,只改单位,最近邻从 A 变成 B。 注意这不是算法不稳,是「哪一维说话更大声」变了。标准化正是把这个话语权拉平。 第四段三行对比要连起来看。第一行的余弦 0.9103 和第二行的 0.6364 都是正数 —— 光看余弦,你分不出「平移」和「反向」哪个更像。相关系数把这两个都摆平了:1.0000 和 −1.0000。 第五段 Jaccard 那两个 0.0000 有点扎眼。它们不是「特别不像」, 是「完全没有交集」—— Jaccard 的下界就是 0,没有负值。这一点跟相关系数不同: 相关系数能给 −1,说明它连「反向相似」都能表达。 最后那张对照表是全节的落点。它没有一条是「技术先进」的,全是「跟数据配不配」。 这也是数据工作里最反直觉的一条:选对度量比换更强的方法收益大得多。
30 秒实验
- 第二段把查询 q 改成
(2, 2, 0, 0)(跟 d1 同方向),重跑。 余弦应该全是 1.0 附近(d1、d2 都是), 欧氏距离则翻倍 —— 这正好是「余弦只看方向」的极端例子。 - 第三段再加一套房源 C = (302, 95),然后只标准化总价这一维、不标准化面积。 答案会介于「万元」和「全标准化」之间 —— 说明量纲问题不是全有全无,而是「谁被缩放得多, 谁就少说话」。
- 第四段造一条
s5 = [2, 4, 6, 8, 10](正好 2 倍的 s1),算它跟 s1 的余弦和相关系数。 两个应该都是 1.0000 —— 倍数缩放同时被余弦和相关系数无视,只有平移才会让它们分家。
常见误区
- 以为欧氏距离最「标准」,别的都是变体。欧氏只是最常用,不是最对。 词频数据用欧氏会被文档长度带偏,标签数据用欧氏根本没意义(两个标签之间的距离是多少?)。
- 以为算之前不用标准化。Demo 里换个单位最近邻就换了人。 真实数据里各维量纲几乎从来不统一(金额、天数、次数、评分), 不标准化等于让数值大的那一维独占话语权。
- 以为余弦和相关系数差不多。差别就在减不减均值。 余弦会把「整体偏移」当成相似,相关系数不会。 看文本相似度用余弦,看两条曲线同不同步用相关系数。
- 以为相似度高就一定是「同类」。相似度只回答「按这个定义像不像」, 不回答「是不是一回事」。两个都爱买相机的人,可能一个是摄影师、 一个是送礼的 —— 度量看不出动机。
自测三问(点开对答案)
1. 为什么把文档复制一遍,余弦相似度不变,欧氏距离却变大? —— 因为**余弦算的是夹角**,复制一遍只改变向量的长度,方向没变, **夹角还是 0,余弦还是 1。**欧氏距离算的是两点之间的直线距离, **长度变了,端点就移动了,距离当然变大。** 这也是「长短文档该不该一视同仁」这个业务问题的数学版本。 2. 把总价的单位从万元改成元,为什么最近邻会换人? —— 因为**欧氏距离把各维的绝对差直接平方相加**,谁的数值大谁就主导。 总价差十万、面积差十几,**平方之后差了八个数量级,面积这一维等于没参与。** **标准化的作用就是把每一维换算成「差几个标准差」,把话语权拉平。** 3. 余弦 0.9103 和相关系数 1.0000,同一对数据为什么两个数差这么多? —— 因为这对数据差的是**平移**(每个数加 100),不是形状。 **余弦把「都在原点右上方」也算成了相似,所以只有 0.9103;** **相关系数先减掉均值,平移被消掉,形状完全一致,所以是 1.0000。**判断该用哪个, 就看你在意的是「绝对位置」还是「起伏形状」。第 4 章到这儿结束。 下一章开始往「图」上走:把问题画成点和边,然后用搜索的办法在解空间里找路。遗传算法、 神经网络都会在里面出现 —— 它们看着不像搜索,底下都是。
- ☐ (本章完,下一节 5.1 图论)