大部分程序员因为理工科的背景,有一些高数、线性代数、概率论与数理统计的数学根底。所以当机器学习的热潮降临的时候,都蠢蠢欲动,对机器学习的算法以及背地的数学思维有比拟强烈的摸索欲望。

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

本文次要解说余弦类似度的相干知识点。类似度计算用处相当宽泛,是搜索引擎、举荐引擎、分类聚类等业务场景的外围点。为了了解分明余弦类似度的前因后果,我将会从最简略的初中数学动手,逐渐推导出余弦公式。而后基于余弦公式串讲一些实际的例子。

一、业务背景

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

精准营销,图像处理,搜索引擎 这三个看似驴唇不对马嘴的业务场景,其实面临一个独特的问题就是类似度的计算。例如精准营销中的人群扩量波及用户类似度的计算;图像分类问题波及图像类似度的计算,搜索引擎波及查问词和文档的类似度计算。类似度计算中,可能因为《数学之美》的影响,大家最相熟的应该是余弦类似度。那么余弦类似度是怎么推导进去的呢?

二、数学根底

了解余弦类似度,要从了解金字塔开始。咱们晓得金字塔的底座是一个微小的正方形。例如吉萨大金字塔的边长超过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 npimport 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<u1,c1>: ", cos_similarity(c1, new_user_v1))    print("sim<u2,c1>: ", cos_similarity(c1, new_user_v2))

案例2:图像分类

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

 

这里咱们的外围问题是:图片如何向量化?

图片由像素形成,每个像素有RGB三个通道。因为像素粒度太细,将图片宰割成大小绝对的格子,每个格子定义3个维度,维度值取格子内像素均值。

参考博客: 图像根底7 图像分类——余弦类似度

上面也是给出样例代码:

# -*- coding: utf-8 -*-import numpy as npimport numpy.linalg as linalgimport 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 npimport numpy.linalg as linalgimport 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给出的答案是综合思考词频(词在以后文档中呈现的次数)以及逆文档频率(词呈现的文档个数)两个因素。

  1. 词在以后文档中呈现次数(TF)越多,  词越重要
  2. 词在其余文档呈现的次数(IDF)越少,词越独特

感兴趣的话,能够自行参考其余材料,这里不开展阐明。

回到咱们的外围问题: 咱们怎么基于实践公式推导出理论公式呢?

四步走就能够了,如下图:

第一步:计算向量乘法

向量乘法就是套用数学公式了。这里须要留神的是,这里有两个简化的思维:

  1. 查问语句中不存在的词tf(t,q)=0
  2. 查问语句根本没有反复的词tf(t,q)=1

所以咱们比较简单实现了第一步推导:

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

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

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

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

这里其实是不能沿用第二步的做法的。后面曾经提到,向量有两大因素:方向和长度。余弦公式只思考了方向因素。这样在理论利用中,余弦类似度就是向量长度无关的了。

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

这里的优化思路就是采纳文档词个数累积,从而升高长文档和短文档之间的差距。当然这里的业务诉求可能比拟多样,所以在源码实现的时候,凋谢了接口容许用户自定义。借以晋升灵便度。

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

所谓用户权重,就是指用户指定查问词的权重。例如典型地竞价排名就是人为晋升某些查问词的权重。所谓打分因子,即如果一个文档中相比其它的文档呈现了更多的查问关键词,那么其值越大。综合思考了多词查问的场景。通过4步,咱们再看推导进去的公式和理论公式,发现类似度十分高。

推导公式和官网公式根本就统一了。

五、总结

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

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

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

六、参考文献

  1. 书籍《数学之美》 作者:吴军
  2. 图像根底7 图像分类——余弦类似度

作者:Shuai Guangying