寻找“最好”(6)——心的距离
“距离”这个词经常在用到,在初中几何上,它指两点间直线的长度,想要测量它很容易,然而果真如此吗?乘坐出租车从家到公司,下车后计价表显示30公里,这可不是两点间的直线。《三国》里,探马回报:“袁军距我军30里处的官渡处下寨,绵延百里”,到底是30里还是百里,怎样才算30里?2018年法国队赢得世界杯冠军,距离他们上次夺冠,已经过去了20年,这里的距离又是时间的跨度。一对单身男女相亲,在一顿无聊的晚餐后得出彼此“距离太远”的结论,人心的距离又该如何测量?
距离的多种度量
先来看一个简单的例子,平坦的地面上有一个边长为1的正方形凹陷,AB两点位于凹陷的边缘处,如下图所示,AB两点间的距离是多少?
暂且不考虑边长的单位,假设一个成年人正好可以一步跨越,那么这个人从A到B所经过的距离是1,如下图所示:
一个身材矮小的少年来了,他把凹陷当作楼梯,先下后上,于是有了这样的行进路线:
少年经过的实际距离是两个三角形的斜边,具体数值是:
又来了一个幼儿园的小朋友,他需要手脚并用,爬上爬下,行进距离是3:
现在有意思了,一个简单的行进,只因为有了一个小小的凹陷就导致了三种不同的行进距离。实际上也许有更多距离,比如一个文艺青年用大跳跨越了一个优美的弧线:
也许有人会说,反正结果都是从A到B,简单的计算为1不就好了?从空间转移来说这没错,但是在自然状态下,各种运动所消耗的能量不同,小朋友爬上爬下的消耗一定比成年人一步跨越要多。类似的例子很多,比如盘山路,更没办法算成两点间的直线了。
其实例子中的几种测量都有道理,根据实际需要和条件的变化,改变距离的计算方法才能得到相对靠谱的数值,接下来将要介绍的就是几种常见的距离计算方法。
一维空间的距离
一维空间可以看作一把带有刻度的直尺,在一维空间上计算距离是最简单的,只需要取两点间数值差的绝对值就可以了,如下图所示:
常见的时间轴就是典型的一维距离,此时距离代表时间的跨度:
欧几里德距离
欧几里得距离(Euclidean Distance)又称欧式距离或欧几里得度量,是以空间为基准的两点之间最短距离,简单地说,就是两点之间直线最短的概念。
二维空间内的算比较简单,主要是点到点和点到直线,如下图所示:
AB两点间的距离:
所有与原点的欧几里德距离为1的点可以构成一个半径为1的圆:
计算A点与直线的距离前,需要先把直线的表达式做个转化,变成ax + by + c = 0的形式:
A(xA, yA)到直线 2x – y – 3 = 0的距离:
三维空间内,主要是点到点和点到平面的距离,如下图所示,Q是平面ax + by + cz = d上的一点,P是平面外的另一点,PQ垂直于平面:
PQ间距离的计算公式与二维空间相同,只是多了一个分量:
计算P到平面的距离同样需要先把平面转化一下,变成ax + by + cz – d = 0,这样就和二维空间内的计算相同:
类似的公式也可以推广到多维空间。P和Q是n维空间内的两点,PQ间的距离:
在第1章已经讲过,n维空间的超平面可以写成:
点到超平面的距离公式:
将P点代入到公式中,点P到超平面的距离:
Python可以很方便地计算欧氏距离,代码如下:
import numpy as np x1 = [1, 1] x2 = [2, 2] np_x1 = np.array(x1) np_x2 = np.array(x2) # 直接使用公式计算 d1 = np.sqrt(np.sum((np_x1 - np_x2) ** 2)) # 使用内置的范数函数计算 d2 = np.linalg.norm(np_x1 - np_x2)
代码中两种方式的计算结果相同。
曼哈顿距离
想象一下在曼哈顿街头的一个路口坐出租车到另一个路口,司机会按照两个路口的直线距离行进吗?大多数时候不会,除非无视交通规则并且能穿越大楼。下图中的AB两点间的直线是欧几里德距离,折线是出租车行的实际行进距离。早在十九世纪,赫尔曼·闵可夫斯基就在曼哈顿街区研究过,将其命名为“曼哈顿距离(Manhattan Distance)”(如果在今天,可能被命名为“导航距离”)。
城市街区大多数是在地势平缓的二维平面,两点A(x1, y1)和B(x2, y2)间曼哈顿距离的计算公式是:
在AB间曼哈顿距离相同的情况下,可能有多种行进路线:
所有与原点的曼哈顿距离为1的点可以构成一个边长为根号2的正方形:
n维空间中的两点P和Q间的曼哈顿距离:
Python计算曼哈顿距离:
import numpy as np x1 = [1, 3] x2 = [4, 9] np_x1 = np.array(x1) np_x2 = np.array(x2) d = np.sum(np.abs(np_x1 – np_x2))
现在来看看《三国》中哨骑的探报,如果把军队变成团伙,将官渡战场的大战变成曼哈顿街头的火拼,就可以看出,哨骑回报的距离是袁军先头部队到曹军的曼哈顿距离。
切比雪夫距离
国际象棋中,国王走一步可以直行、横行、斜行移动到相邻八个方格中的任意一个,如下图所示:
如果把两个相邻方格的距离记为1,国王从方格A(x1, y1)走到方格B(x2, y2)最少需要的最短距离,就是切比雪夫距离(Chebyshev distance),下图展示了国王从棋盘某一方格处,到达其它方格的距离:
在二维空间内,A(x1, y1)和B(x2, y2)两点间的切比雪夫距离是两点横坐标差的绝对值与纵坐标差的绝对值中较大的那个:
扩展到多维空间,PQ间的切比雪夫距离:
棋盘上格子是距离的离散表示,在真正的二维坐标中,与原点距离为1的点会构成一个边长为2的正方形:
对比上一节同为正方形的曼哈顿距离,发现它们应该可以互相转换。实际上,在二维空间内曼哈顿距离与切比雪夫距离的坐标转换关系是:
Python计算切比雪夫距离:
import numpy as np x1 = [1, 3] x2 = [4, 9] np_x1 = np.array(x1) np_x2 = np.array(x2) d = np.max(np.abs(np_x1 – np_x2))
夹角余弦
夹角余弦(Cosine Similarity)测量的是两个样本间的相似性,样本间的距离越近,相似度越高。
夹角余弦来源于向量点积的几何意义,两个向量A和B的点积等于A和B的模乘以二者的夹角余玄:
可以看出,cosθ表示的是A和B方向的相似度,与它们的大小无关,虽然下图中A’ 的模长远小于A的模长,但AB的夹角余弦和等于A’B的相等:
扩展到多维空间,PQ间的夹角余弦:
Python能够方便地使用向量计算夹角余弦,下面给出了两种计算方式:
# 使除法变成精确除法 from __future__ import division import numpy as np x1 = [1, 2, 3, 5] x2 = [2, 3, 4, 6] np_x1 = np.array(x1) np_x2 = np.array(x2) result1 = np.dot(np_x1, np_x2) / np.sqrt(np.dot(np_x1, np_x1) * np.dot(np_x2, np_x2)) result2 = np.dot(np_x1, np_x2) / (np.linalg.norm(np_x1) * np.linalg.norm(np_x2)) theta = np.arccos(result1) print('result1 = %f, result2 = %f, theta = %f' % (result1, result2, theta)) # result1 = 0.993073, result2 = 0.993073, theta = 0.117774
夹角余弦取值范围是[-1,1],它提供了以下几个信息:
- 余弦越大,两个向量的夹角越小,相似度越高;余弦越小,两个向量的夹角越大,相似度越低。
- 两个向量的方向重合时余弦等于1;两个向量的方向完全相反余弦等于-1;两个向量垂直是,余弦值等于0。
- θ < 90°,A·B > 0;θ > 90°,A·B < 0;θ = 90°,A·B = 0。
其它度量方法
还有很多种方法用于度量距离,比如经常在推荐系统用来度量偏好习惯,但看起来不那么直观的的皮尔逊相关度;度量符号或布尔值个体间相似度的Jaccard系数和谷本系数;在信息论中,度量两个等长字符串之间对应位置的不同字符个数的汉明距离;还有马氏距离,闵可夫斯基距离,以及判断整个系统内部样本分布集中程度的信息熵等等。这些度量方法各有优缺点,不同的度量方法有些时候对于算法的结果差异很大,在实践中,可能要在不同的方法中反复切换,根据实际效果进行对比,最终选取效果最好的一个。
人心的距离
也许是为了摆脱单身状态,也许是为了应付长辈的特意安排,许多年轻的单身男女都参加过相亲,但是大多数相亲都是以“三观不合”或“距离太远”为由而没有下文。人与人之间的距离似乎是一个文学词汇,并非物理意义上的距离,真的能够测量吗?怎么才算三观基本一致?接下来,我们就用本章的知识去尝试度量人与人之间的距离。
相亲
小明是个28岁的城市白领,国庆期间在老妈的安排下分别见了可可、乐乐、小枫和小柔四个女孩。经过逛街吃饭后,小明对四个个女孩有了初步的了解,觉得她们都十分可爱,四个女孩似乎也想进一步发展。“你喜欢哪一个呢?”老妈问,“这个……”小明犯了难,不能脚踏四只船啊。“对了,就选和自己距离最近的!”小明想,于是他根据自己最关心的问题绘制了这样一个表格:
小明觉得文字对比不够直观,所以他的第一步是把文字转换为数字。
数据预处理
为了方便比较,先将非数值属性量化。小明有三个爱好,所以设自己的爱好值是3,同时设女孩们的初始爱好值是0,如果一个女孩有一个爱好与自己相同,则该女孩的爱好值加1,例如可可与小明都喜欢读书,所以可可的爱好值是1。同时小明也注意到,有些爱好虽然不同,但及其相近,比如音乐与唱歌,所以把二者看作同一爱好。于是,小明得到了下面的数据:
其中年龄的数值较大,直接使用会占据较大的权重,所以小明对它们做了进一步处理,他以自己的数据为标准,将女孩的数据与自己相减。此外,他对是否要小孩看的很重要,对年龄差距并不十分看中,所以分别增加和缩减了两个维度的权重。现在数据数值都在一个较小的范围内:
度量距离
现在终于可以测量了,看看那个女孩和小明最般配。不妨先将学历和爱好拿出来,在平面上看看距离,如下图所示:
看起来小枫和自己最近,乐乐相对较远。当所有维度都参与计算时,小明使用下面的代码帮助计算:
# 使除法变成精确除法 from __future__ import division import numpy as np # 欧几里德距离 def d_euclidean(x1, x2): return np.linalg.norm(x1 - x2) # 曼哈顿距离 def d_manhattan(x1, x2): return np.sum(np.abs(x1 - x2)) # 切比雪夫距离 def d_chebyshev(x1, x2): return np.max(np.abs(x1 - x2)) # 计算距离 def d_compute(x, d_name, d_fun): ''' :param x: 样本的特征集 :param d_name: 距离算法名称 :param d_fun: 距离算法 ''' print('%s:\t%f\t%f\t%f\t%f' % (d_name, d_fun(x[0], x[1]), d_fun(x[0], x[2]), d_fun(x[0], x[3]), d_fun(x[0], x[4])))
if __name__ == '__main__':
# 初始数据
data_set = [[0, 2, 3, 3], [-2, 1, 1, 3], [-1, 2, 0, 3], [-0.5, 3, 2, -3], [2, 4, 3, 3]]
# 将数据转换为numpy向量
x = np.array(data_set)
d_compute(x, 'euclidean', d_euclidean)
d_compute(x, 'manhattan', d_manhattan)
d_compute(x, 'chebyshev', d_chebyshev)
d_compute(x, '\tcos', d_cos)
由于夹角余弦仅与向量的方向有关,与向量的大小无关,在这里又希望更多地考虑向量的大小,所以只使用两位三种距离度量,最终得到这样的结果:
三种度量得到了三种不同的结果,而且每种结果中排在第一位的女孩都不同,综合考虑后,似乎小柔排在前面的次数较多,所以小明觉得明天应该重点发展一下小柔。
人心可测吗
问题似乎圆满解决了,然而这样真的合理吗?想想儿时的小伙伴们,今天,他们的学历、爱好、收入甚至三观都与自己极为不同,但似乎并没有影响我们的矫情,时隔多年大家仍然有相同的话题,仍然可以一起喝上一杯,距离的长度似乎并不影响我们成为朋友。相反,身边的同事无论学历、经历还是工作目标都与自己接近,然而一道浅浅的隔断就阻断了大多数交流,彼此封闭起内心。人心真的可测吗?还是留给大家去思考吧。
作者:我是8位的