大部分程序员由于理工科的背景,有一些高数、线性代数、概率论与数理统计的数学基础。所以当机器学习的热潮来临的时候,都跃跃欲试,对机器学习的算法以及背后的数学思想有比较强烈的探索欲望。

本文的作者就是其中的一位。然而实践的过程中,又发现数学知识的理解深度有些欠缺,在理解一些公式背后的意义时,有些力不从心的感觉。因此梳理了一些数学上的知识盲点,理顺自己的知识脉络,顺便分享给有需要的人。

本文主要讲解余弦相似度的相关知识点。相似度计算用途相当广泛,是搜索引擎、推荐引擎、分类聚类等业务场景的核心点。为了理解清楚余弦相似度的来龙去脉,我将会从最简单的初中数学入手,逐步推导出余弦公式。然后基于余弦公式串讲一些实践的例子。

一、业务背景

通常我们日常开发中,可能会遇到如下的业务场景。

精准营销,图像处理,搜索引擎 这三个看似风马牛不相及的业务场景,其实面临一个共同的问题就是相似度的计算。例如精准营销中的人群扩量涉及用户相似度的计算;图像分类问题涉及图像相似度的计算,搜索引擎涉及查询词和文档的相似度计算。相似度计算中,可能由于《数学之美》的影响,大家最熟悉的应该是余弦相似度。那么余弦相似度是怎么推导出来的呢?

二、数学基础

理解余弦相似度,要从理解金字塔开始。我们知道金字塔的底座是一个巨大的正方形。例如吉萨大金字塔的边长超过230m。构造这样一个巨大的正方形,如何保证构造出来的图形不走样呢?比如如何确保构造的结果不是菱形或者梯形。

1、勾股定理

要保证构造出来的四边形是正方形,需要保证两个点:其一是四边形的边长相等;其二是四边形的角是直角。四边形的边长相等很容易解决,在工程实践中,取一根定长的绳子作为边长就可以了。如何保障直角呢?古人是利用勾股定理解决的,更切确地说是勾股定理的逆定理。

构造一个三角形,保证三角形的三边长分别是3,4,5。那么边长为5的边对应的角为直角。中国有个成语“无规矩不成方圆”,其中的矩,就是直角的尺。

勾股证明是初中数学的知识,理解很容易。证明也很简单,据说爱因斯坦11岁就发现了一种证明方法。勾股定理的证明方法据统计有超过400种, 感兴趣的同学可以自行了解。另勾股定理也是费马大定理的灵感来源,费马大定理困扰了世间智者300多年,也诞生了很多的逸闻趣事,这里不赘述。

2、余弦定理

勾股定理存在着一个很大的限制,就是要求三角形必须是直角三角形。那么对于普通的三角形,三个边存在什么样的关系呢?这就引出了余弦定理。

余弦定理指出了任意三角形三边的关系,也是初中就可以理解的数学知识,证明也比较简单,这里就略过了。

其实对于三角形,理解了勾股定理和余弦定理。就已经掌握了三角形的很多特性和秘密了。比如根据等边三角形,可以推导出cos(60)=1/2。但是如果想理解几何更多的秘密,就需要进入解析几何的世界。这个数学知识也不算很高深,高中数学知识。

这里我们理解最简单就可以了,那就是三角形在直角坐标系中的表示。所谓“横看成岭侧成峰,远近高低各不同”,我们可以理解为三角形的另一种表现形式。

比如我们可以用a,b,c三个边描述一个三角形;在平面直角坐标系中,我们可以用两个向量表示一个三角形。

3、余弦相似度

当我们引入了直角坐标系后,三角形的表示就进入了更灵活、更强大和更抽象的境界了。几何图形可以用代数的方法来计算,代数可以用几何图形形象化表示,大大降低理解难度。比如我们用一个向量来表示三角形的一个边,就可以从二维空间直接扩展到高维空间。

这里,向量的定义跟点是一样的;向量的乘法也只是各个维度值相乘累加;向量的长度看似是新的东西,其实绕了一个圈,本质上还是勾股定理,只是勾股定理从二维空间扩展到了N维空间而已。而且向量长度又是两个相同向量乘法的特例。数学的严谨性在这里体现得淋漓尽致。

结合勾股定理,余弦定理,直角坐标系,向量。我们就可以很自然地推导出余弦公式了,这里唯一的理解难点就是勾股定理和余弦定理都是用向量来表示。

得到了余弦公式后,我们该怎么理解余弦公式呢?

极端情况下,两个向量重合了,就代表两个向量完全相似。然而这里的完全相似,其实是指向量的方向。向量有方向和长度两个要素,这里只使用方向这一个要素,在实践中就埋下了隐患。但是毕竟一个数学模型建立起来了。我们可以用这个模型解决一些实际中的问题了。

所谓数学模型,有可能并不需要高深的数学知识,对外的表现也仅仅是一个数学公式。比如余弦定理这个数学模型,高中数学知识就足够理解了。而且关于模型,有这样一个很有意思的论述:“所有的数学模型都是错的,但是有些是有用的”。这里我们更多关注其有用的一面。

理解了向量夹角,那么该怎么理解向量呢?它仅仅是三角形的一条边吗?

人生有几何,万物皆向量。向量在数学上是简单的抽象,这个抽象我们可以用太多实际的场景来使它落地。比如用向量来指代用户标签,用向量来指代颜色,用向量来指代搜索引擎的逻辑...

三、业务实践

理解了余弦定理,理解了数学建模的方式。接下来我们就可以做一些有意思的事情了。比如前面提到的三个业务场景,我们可以看看如何用余弦相似度来解决。当然实际问题肯定远远要复杂得多,但是核心的思想都是类似的。

案例1:精准营销

假设一次运营计划,比如我们圈定了1w的用户,如何扩展到10万人呢?

利用余弦相似度,我们这里其实最核心的问题就是:如何将用户向量化?

将每个用户视为一个向量,用户的每个标签值视为向量的一个维度。当然这里实际工程中还有特征归一化,特征加权等细节。我们这里仅作为演示,不陷入到细节中。

对于人群,我们可以取人群中,所有用户维度值的平均值,作为人群向量。这样处理后,就可以使用余弦公式计算用户的相似度了。

我们通过计算大盘用户中每个用户跟圈定人群的相似度,取topN即可实现人群的扩量。

直接“show me the code”吧!

# -*- coding: utf-8 -*-

import numpy as np

import numpy.linalg as linalg

def cos_similarity(v1, v2):

num = float(np.dot(v1.T, v2)) # 若为行向量则 A.T * B

denom = linalg.norm(v1) * linalg.norm(v2)

if denom > 0:

cos = num / denom # 余弦值

sim = 0.5 + 0.5 * cos # 归一化

return sim

return 0

if __name__ == ‘__main__‘:

u_tag_list = [

["女", "26", "是", "白领"],

["女", "35", "是", "白领"],

["女", "30", "是", "白领"],

["女", "22", "是", "白领"],

["女", "20", "是", "白领"]

]

new_user = ["女", "20", "是", "白领"]

u_tag_vector = np.array([

[1, 26, 1, 1],

[1, 35, 1, 1],

[1, 30, 1, 1],

[1, 22, 1, 1],

[1, 20, 1, 1]

])

c1 = u_tag_vector[0]

c1 += u_tag_vector[1]

c1 += u_tag_vector[2]

c1 += u_tag_vector[3]

c1 += u_tag_vector[4]

c1 = c1/5

new_user_v1 = np.array([1, 36, 1, 1])

new_user_v2 = np.array([-1, 20, 0, 1])

print("vector-u1: ", list(map(lambda x: ‘%.2f‘ % x, new_user_v1.tolist()[0:10])))

print("vector-u2: ", list(map(lambda x: ‘%.2f‘ % x, new_user_v2.tolist()[0:10])))

print("vector-c1: ", list(map(lambda x: ‘%.2f‘ % x, c1.tolist()[0:10])))

print("sim: ", cos_similarity(c1, new_user_v1))

print("sim: ", cos_similarity(c1, new_user_v2))

案例2:图像分类

有两类图片,美食和萌宠。对于新的图片,如何自动分类呢?

这里我们的核心问题是:图片如何向量化?

图片由像素构成,每个像素有RGB三个通道。由于像素粒度太细,将图片分割成大小相对的格子,每个格子定义3个维度,维度值取格子内像素均值。

下面也是给出样例代码:

# -*- coding: utf-8 -*-

import numpy as np

import numpy.linalg as linalg

import cv2

def cos_similarity(v1, v2):

num = float(np.dot(v1.T, v2)) # 若为行向量则 A.T * B

denom = linalg.norm(v1) * linalg.norm(v2)

if denom > 0:

cos = num / denom # 余弦值

sim = 0.5 + 0.5 * cos # 归一化

return sim

return 0

def build_image_vector(im):

"""

:param im:

:return:

"""

im_vector = []

im2 = cv2.resize(im, (500, 300))

w = im2.shape[1]

h = im2.shape[0]

h_step = 30

w_step = 50

for i in range(0, w, w_step):

for j in range(0, h, h_step):

each = im2[j:j+h_step, i:i+w_step]

b, g, r = each[:, :, 0], each[:, :, 1], each[:, :, 2]

im_vector.append(np.mean(b))

im_vector.append(np.mean(g))

im_vector.append(np.mean(r))

return np.array(im_vector)

def show(imm):

imm2 = cv2.resize(imm, (510, 300))

print(imm2.shape)

imm3 = imm2[0:50, 0:30]

cv2.imshow("aa", imm3)

cv2.waitKey()

cv2.destroyAllWindows()

imm4 = imm2[51:100, 0:30]

cv2.imshow("bb", imm4)

cv2.waitKey()

cv2.destroyAllWindows()

imm2.fill(0)

def build_image_collection_vector(p_name):

path = "D:\python-workspace\cos-similarity\images\"

c1_vector = np.zeros(300)

for pic in p_name:

imm = cv2.imread(path + pic)

each_v = build_image_vector(imm)

a=list(map(lambda x:‘%.2f‘ % x, each_v.tolist()[0:10]))

print("p1: ", a)

c1_vector += each_v

return c1_vector/len(p_name)

if __name__ == ‘__main__‘:

v1 = build_image_collection_vector(["food1.jpg", "food2.jpg", "food3.jpg"])

v2 = build_image_collection_vector(["pet1.jpg", "pet2.jpg", "pet3.jpg"])

im = cv2.imread("D:\python-workspace\cos-similarity\images\pet4.jpg")

v3 = build_image_vector(im)

print("v1,v3:", cos_similarity(v1,v3))

print("v2,v3:", cos_similarity(v2,v3))

a = list(map(lambda x: ‘%.2f‘ % x, v3.tolist()[0:10]))

print("p1: ", a)

im2 = cv2.imread("D:\python-workspace\cos-similarity\images\food4.jpg")

v4 = build_image_vector(im2)

print("v1,v4:", cos_similarity(v1, v4))

print("v2,v4:", cos_similarity(v2, v4))

至于代码中用到的图片,用户可以自行收集即可。笔者也是直接从搜索引擎中截取的。程序计算的结果也是很直观的,V2(萌宠)跟图像D1的相似度为0.956626,比V1(美食)跟图像D1的相似度0.942010更高,所以结果也是很明确的。

案例3:文本检索

假设有三个文档,描述的内容如下。一个是疫情背景下,苹果公司的资讯,另外两个是水果相关的信息。输入搜索词“苹果是我最喜欢的水果”,  该怎么找到最相关的文档?

这里的核心问题也是文本和搜索词如何向量化?

这里其实可以把搜索词也视为文档,这样问题就简化成:文档如何向量化?

出于简化问题的角度,我们可以给出最简单的答案:文档由词组成,每个词作为一个维度;文档中词出现的频率作为维度值。

当然,实际操作时我们维度值的计算会更复杂一些,比如用TF-IDF。这里用词频(TF)并不影响演示效果,所以我们从简。

将文本向量化后,剩下也是依样画葫芦,用余弦公式计算相似度, 流程如下:

最后,给出代码:

# -*- coding: utf-8 -*-

import numpy as np

import numpy.linalg as linalg

import jieba

def cos_similarity(v1, v2):

num = float(np.dot(v1.T, v2)) # 若为行向量则 A.T * B

denom = linalg.norm(v1) * linalg.norm(v2)

if denom > 0:

cos = num / denom # 余弦值

sim = 0.5 + 0.5 * cos # 归一化

return sim

return 0

def build_doc_tf_vector(doc_list):

num = 0

doc_seg_list = []

word_dic = {}

for d in doc_list:

seg_list = jieba.cut(d, cut_all=False)

seg_filterd = filter(lambda x: len(x)>1, seg_list)

w_list = []

for w in seg_filterd:

w_list.append(w)

if w not in word_dic:

word_dic[w] = num

num+=1

doc_seg_list.append(w_list)

print(word_dic)

doc_vec = []

for d in doc_seg_list:

vi = [0] * len(word_dic)

for w in d:

vi[word_dic[w]] += 1

doc_vec.append(np.array(vi))

print(vi[0:40])

return doc_vec, word_dic

def build_query_tf_vector(query, word_dic):

seg_list = jieba.cut(query, cut_all=False)

vi = [0] * len(word_dic)

for w in seg_list:

if w in word_dic:

vi[word_dic[w]] += 1

return vi

if __name__ == ‘__main__‘:

doc_list = [

"""

受全球疫情影响,3月苹果宣布关闭除大中华区之外数百家全球门店,其庞大的供应链体系也受到冲击,

尽管目前富士康等代工厂已经开足马力恢复生产,但相比之前产能依然受限。中国是iPhone生产的大本营,

为了转移风险,iPhone零部件能否实现印度制造?实现印度生产的最大难点就是,相对中国,印度制造业仍然欠发达

""",

"""

苹果是一种低热量的水果,每100克产生大约60千卡左右的热量。苹果中营养成分可溶性大,容易被人体吸收,故有“活水”之称。

它有利于溶解硫元素,使皮肤润滑柔嫩。

""",

"""

在生活当中,香蕉是一种很常见的水果,一年四季都能吃得着,因其肉质香甜软糯,且营养价值高,所以深受老百姓的喜爱。

那么香蕉有什么具体的功效,你了解吗?

"""

]

query = "苹果是我喜欢的水果"

doc_vector, word_dic = build_doc_tf_vector(doc_list)

query_vector = build_query_tf_vector(query, word_dic)

print(query_vector[0:35])

for i, doc in enumerate(doc_vector):

si = cos_similarity(doc, query_vector)

print("doc", i, ":", si)

我们检索排序的结果如下:

文档D2是相似度最高的,符合我们的预期。这里我们用最简单的方法,实现了一个搜索打分排序的样例,虽然它并没有实用价值,但是演示出了搜索引擎的工作原理。

四、超越余弦

前面通过简单的3个案例,演示了余弦定理的用法,但是没有完全释放出余弦定理的洪荒之力。接下来展示一下工业级的系统中是如何使用余弦定理的。这里选取了开源搜索引擎数据库ES的内核Lucene作为研究对象。研究的问题是:Lucene是如何使用余弦相似度进行文档相似度打分?

当然,对于Lucene的实现,它有另一个名字:向量空间模型。即许多向量化的文档集合形成了向量空间。我们首先直接看公式:

很明显,实际公式跟理论公式长相差异很大。那么我们怎么理解呢?换言之,我们怎么基于理论公式推导出实际公式呢?

首先需要注意的是,在Lucene中,文档向量的特征不再是我们案例3中展示的,用的词频,而是TF-IDF。关于TF-IDF相关的知识,比较简单,主要的思路在于:

如何量化一个词在文档中的关键程度?  TF-IDF给出的答案是综合考虑词频(词在当前文档中出现的次数)以及逆文档频率(词出现的文档个数)两个因素。

词在当前文档中出现次数(TF)越多,  词越重要

词在其他文档出现的次数(IDF)越少,词越独特

感兴趣的话,可以自行参考其他资料,这里不展开说明。

回到我们的核心问题: 我们怎么基于理论公式推导出实际公式呢?

四步走就可以了,如下图:

第一步:计算向量乘法

向量乘法就是套用数学公式了。这里需要注意的是,这里有两个简化的思想:

查询语句中不存在的词tf(t,q)=0

查询语句基本没有重复的词tf(t,q)=1

所以我们比较简单完成了第一步推导:

第二步: 计算查询语句向量长度|V(q)|

计算向量长度,其实就是勾股定理的使用了。只不过这里是多维空间的勾股定理。

这里取名queryNorm, 表示这个操作是对向量的归一化。这个其实是当向量乘以queryNorm后,就变成了单位向量。单位向量的长度为1,所以称为归一化,也就是取名norm。理解了这一层,看lucene源码的时候,就比较容易理解了。这正如琅琊榜的台词一样:问题出自朝堂,答案却在江湖。这里是问题出自Lucene源码,答案却在数学。

第三步:计算文档向量长度|V(d)|

这里其实是不能沿用第二步的做法的。前面已经提到,向量有两大要素:方向和长度。余弦公式只考虑了方向因素。这样在实际应用中,余弦相似度就是向量长度无关的了。

这在搜索引擎中,如果查询语句命中了长文档和短文档,按照余弦公式TF-IDF特征,偏向于对短小的文档打较高的分数。对长文档不公平,所以需要优化一下。

这里的优化思路就是采用文档词个数累积,从而降低长文档和短文档之间的差距。当然这里的业务诉求可能比较多样,所以在源码实现的时候,开放了接口允许用户自定义。借以提升灵活度。

第四步:混合用户权重和打分因子

所谓用户权重,就是指用户指定查询词的权重。例如典型地竞价排名就是人为提升某些查询词的权重。所谓打分因子,即如果一个文档中相比其它的文档出现了更多的查询关键词,那么其值越大。综合考虑了多词查询的场景。经过4步,我们再看推导出来的公式和实际公式,发现相似度非常高。

推导公式和官方公式基本就一致了。

五、总结

本文简单介绍了余弦相似度的数学背景。从埃及金字塔的建设问题出发,引出了勾股定理,进而引出了余弦定理。并基于向量推导出来了余弦公式。

接下来通过三个业务场景的例子,介绍余弦公式的应用,即数学模型如何落地到业务场景中。这三个简单的例子代码不过百行,能够帮助读者更好地理解余弦相似度。

最后介绍了一个工业级的样例。基于Lucene构建的ES是当前最火热的搜索引擎解决方案。学习余弦公式在Lucene中落地,有助于理解业界的真实玩法。进一步提升对余弦公式的理解。

六、参考文献

作者:Shuai Guangying

python勾股定理中三个数的关系是、找出三十以内的_从勾股定理到余弦相似度-程序员的数学基础...相关推荐

  1. 余弦相似度 高维数据_从勾股定理到余弦相似度-程序员的数学基础

    大部分程序员由于理工科的背景,有一些高数.线性代数.概率论与数理统计的数学基础.所以当机器学习的热潮来临的时候,都跃跃欲试,对机器学习的算法以及背后的数学思想有比较强烈的探索欲望. 本文的作者就是其中 ...

  2. 从勾股定理到余弦相似度-程序员的数学基础

    大部分程序员由于理工科的背景,有一些高数.线性代数.概率论与数理统计的数学基础.所以当机器学习的热潮来临的时候,都跃跃欲试,对机器学习的算法以及背后的数学思想有比较强烈的探索欲望. 本文的作者就是其中 ...

  3. python求三个数的最大值编程_python 判断三个数字中的最大值实例代码

    python 判断三个数字中的最大值,具体代码如下所示: #判断三个数中最大值 n1= int(input('please enter the firest number:')) n2 = int(i ...

  4. 有史以来的第一个脚本 找出三个数的最大数字

    其实老师后来上课的时候我才知道只是要找出三个数中最大的一个,而我当时却本着从小到大排列的思想去写脚本了,也由于是第一次写脚本所以也费了好长的时间. 刚开始看到这个题目的时候觉得很简单,这一贯是我眼高手 ...

  5. c语言比较三个数大小输出最小值,比较ABC三个数的大小,从小到大排列输出。,c语言比较三个数大小按从大到小顺序输出...

    匿名网友: 思路分析:先比较2个数,取其中大者与第三个数比较得出较大者为最大数,记为max.步骤如下:第一步:输入a,b,c.第二步:比较a,b.若a>b,则执行第三步:否则,执行第四步.第三步 ...

  6. 输入三个数,判断能否以这三个数构成一个三角形,若能则输出所构成三角形的类别(等边、等腰、直角或斜三角形)

    输入三个数,判断能否以这三个数构成一个三角形,若能则输出所构成三角形的类别(等边.等腰.直角或斜三角形) #include<stdio.h> int main() {int a,b,c; ...

  7. 7-107 找出三位水仙花数 (15 分)

    7-107 找出三位水仙花数 (15 分) 本题要求编写程序,输出给定正整数M和N区间内的所有三位水仙花数.三位水仙花数,即其个位.十位.百位数字的立方和等于该数本身. 输入格式: 输入在一行中给出两 ...

  8. Java基础算法题(18):两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单。有人向队员打听比赛的名单。a说他不和x比,c说他不和x,z比,请编程序找出三

    查看所有50道基础算法题请看: Java的50道基础算法题 两个乒乓球队进行比赛,各出三人.甲队为a,b,c三人,乙队为x,y,z三人.已抽签决定比赛名单.有人向队员打听比赛的名单.a说他不和x比,c ...

  9. 两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单。有人向队员打听比赛的名单。a说他不和x比,c说他不和x,z比,请编程序找出三队赛手的名单

    题目 两个乒乓球队进行比赛,各出三人.甲队为a,b,c三人,乙队为x,y,z三人.已抽签决定比赛名单.有人向队员打听比赛的名单.a说他不和x比,c说他不和x,z比,请编程序找出三队赛手的名单 解析: ...

最新文章

  1. 根据两点间的经纬度计算距离
  2. 老梁观世界“20120713期 癌症女博士的生命箴言”
  3. 从nginx-rtmp中提取一帧h264帧
  4. java mp4 视频时间戳_MP4文件中音视频时间戳的计算
  5. One order search dynamic SQL build logic
  6. API设计原则 - Qt官网的设计实践总结
  7. excel下划线转驼峰公式
  8. html5中音频、视频标签、自定义播放器常用属性及方法、全屏操作、新增属性兼容问题
  9. GoogLeNet——CNN经典网络模型详解(pytorch实现)
  10. mariadb mysql 重建_(MariaDB/MySQL)之DML(2):数据更新、删除
  11. iOS混排计算中英文个数,中午算一个字符,英文和符号算半个字符
  12. 传统Tier1“大象转身”:不够快?你永远没有机会
  13. ThoughtWorks 结对编程
  14. 深度分析何恺明新作MAE:通向CV大模型
  15. Java分布式锁那点事
  16. InnoDB——锁、事务和复制
  17. 遭遇蚊子的獠牙,想起一种叫黄鸡婆的小虫子
  18. expected 3 got 2 错误提示
  19. 纵断面 java_道路土方计算软件
  20. grub制作USB启动盘

热门文章

  1. python 通达信板块_通达信如何自定义选股,使用python在通达信里面选股
  2. JavaScript数据结构和算法笔记一(前八章)
  3. 生存之道,每个人都值得尊敬
  4. 理工男,我感觉 win10 挺好用的,一点不卡,为什么有那么多人买Macbook 呢?
  5. php起始符大全,以下哪种标签不是 PHP 起始 / 结束符
  6. 朱刘算法 有向图的最小生成树
  7. (附源码)ssm医务志愿者管理系统 毕业设计 100910
  8. MySQL 索引、事务与存储引擎
  9. java黑白棋原码_JAVA课程设计黑白棋源代码
  10. 8086/8088 CPU寄存器组