📘 第4章 权重和寻找最优解
📄 一句话概括

五种相似度:欧氏、曼哈顿、余弦、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 秒实验

  1. 第二段把查询 q 改成 (2, 2, 0, 0)(跟 d1 同方向),重跑。 余弦应该全是 1.0 附近(d1、d2 都是), 欧氏距离则翻倍 —— 这正好是「余弦只看方向」的极端例子。
  2. 第三段再加一套房源 C = (302, 95),然后只标准化总价这一维、不标准化面积。 答案会介于「万元」和「全标准化」之间 —— 说明量纲问题不是全有全无,而是「谁被缩放得多, 谁就少说话」。
  3. 第四段造一条 s5 = [2, 4, 6, 8, 10](正好 2 倍的 s1),算它跟 s1 的余弦和相关系数。 两个应该都是 1.0000 —— 倍数缩放同时被余弦和相关系数无视,只有平移才会让它们分家。

常见误区

  1. 以为欧氏距离最「标准」,别的都是变体。欧氏只是最常用,不是最对。 词频数据用欧氏会被文档长度带偏,标签数据用欧氏根本没意义(两个标签之间的距离是多少?)。
  2. 以为算之前不用标准化。Demo 里换个单位最近邻就换了人。 真实数据里各维量纲几乎从来不统一(金额、天数、次数、评分), 不标准化等于让数值大的那一维独占话语权。
  3. 以为余弦和相关系数差不多。差别就在减不减均值。 余弦会把「整体偏移」当成相似,相关系数不会。 看文本相似度用余弦,看两条曲线同不同步用相关系数。
  4. 以为相似度高就一定是「同类」。相似度只回答「按这个定义像不像」, 不回答「是不是一回事」。两个都爱买相机的人,可能一个是摄影师、 一个是送礼的 —— 度量看不出动机。
自测三问(点开对答案) 1. 为什么把文档复制一遍,余弦相似度不变,欧氏距离却变大? —— 因为**余弦算的是夹角**,复制一遍只改变向量的长度,方向没变, **夹角还是 0,余弦还是 1。**欧氏距离算的是两点之间的直线距离, **长度变了,端点就移动了,距离当然变大。** 这也是「长短文档该不该一视同仁」这个业务问题的数学版本。 2. 把总价的单位从万元改成元,为什么最近邻会换人? —— 因为**欧氏距离把各维的绝对差直接平方相加**,谁的数值大谁就主导。 总价差十万、面积差十几,**平方之后差了八个数量级,面积这一维等于没参与。** **标准化的作用就是把每一维换算成「差几个标准差」,把话语权拉平。** 3. 余弦 0.9103 和相关系数 1.0000,同一对数据为什么两个数差这么多? —— 因为这对数据差的是**平移**(每个数加 100),不是形状。 **余弦把「都在原点右上方」也算成了相似,所以只有 0.9103;** **相关系数先减掉均值,平移被消掉,形状完全一致,所以是 1.0000。**判断该用哪个, 就看你在意的是「绝对位置」还是「起伏形状」。

第 4 章到这儿结束。 下一章开始往「图」上走:把问题画成点和边,然后用搜索的办法在解空间里找路。遗传算法、 神经网络都会在里面出现 —— 它们看着不像搜索,底下都是。

相关笔记