文本类似度计算-JaccardSimilarity和哈希签名函数

在目前这个信息过载的星球上,文本的类似度计算应用前景仍是比较普遍的,他可让人们过滤掉不少类似的新闻,好比在搜索引擎上,类似度过高的页面,只须要展现一个就好了,还有就是,考试的时候,能够用这个来防做弊,一样的,论文的类似度检查也是一个检查论文是否抄袭的一个重要办法。 python

文本类似度计算的应用场景


  • 过滤类似度很高的新闻,或者网页去重
  • 考试防做弊系统
  • 论文抄袭检查

光第一项的应用就很是普遍。 git

文本类似度计算的基本方法


文本类似度计算的方法不少,主要来讲有两种,一是余弦定律,二是JaccardSimilarity方法,余弦定律不在本文的讨论范围以内,咱们主要说一下JaccardSimilarity方法。 github

JaccardSimilarity方法


JaccardSimilarity提及来很是简单,容易实现,实际上就是两个集合的交集除以两个集合的并集,所得的就是两个集合的类似度,直观的看就是下面这个图。 算法

数学表达式是: 编程

|S ∩ T|/|S ∪ T|

恩,基本的计算方法就是如此,而两个集合分别表示的是两个文本,集合中的元素实际上就是文本中出现的词语啦,咱们须要作的就是把两个文本中的词语统计出来,而后按照上面的公式算一下就好了,其实很简单。 数组

统计文本中的词语


关于统计文本中的词语,能够参考个人另一篇博文一种没有语料字典的分词方法,文章中详细说明了如何从一篇文本中提取有价值的词汇,感兴趣的童鞋能够看看。 app

固然,本篇博客主要是说计算类似度的,因此词语的统计使用的比较简单的算法k-shingle算法,k是一个变量,表示提取文本中的k个字符,这个k能够本身定义。 函数

简单的说,该算法就是从头挨个扫描文本,而后依次把k个字符保存起来,好比有个文本,内容是abcdefg,k设为2,那获得的词语就是ab,bc,cd,de,ef,fg。 优化

获得这些词汇之后,而后统计每一个词汇的数量,最后用上面的JaccardSimilarity算法来计算类似度。 搜索引擎

具体的简单代码以下:

file_name_list=["/Users/wuyinghao/Documents/test1.txt",
                "/Users/wuyinghao/Documents/test2.txt",
                "/Users/wuyinghao/Documents/test3.txt"]
hash_contents=[]

#获取每一个文本的词汇词频表
for file_name in file_name_list:
    hash_contents.append([getHashInfoFromFile(file_name,5),file_name])
    

for index1,v1 in enumerate(hash_contents):
    for index2,v2 in enumerate(hash_contents):
        if(v1[1] != v2[1] and index2>index1):
            intersection=calcIntersection(v1[0],v2[0]) #计算交集
            union_set=calcUnionSet(v1[0],v2[0],intersection) #计算并集
            print v1[1]+ "||||||" + v2[1] + " similarity is : " + str(calcSimilarity(intersection,union_set)) #计算类似度


完整的代码能够看个人GitHub

如何优化


上述代码其实能够完成文本比较了,可是若是是大量文本或者单个文本内容较大,比较的时候势必占用了大量的存储空间,由于一个词汇表的存储空间大于文本自己的存储空间,这样,咱们须要进行一下优化,如何优化呢,咱们按照如下两个步骤来优化。

将词汇表进行hash


首先,咱们将词汇表进行hash运算,把词汇表中的每一个词汇hash成一个整数,这样存储空间就会大大下降了,至于hash的算法,网上有不少,你们能够查查最小完美哈希,因为我这里只是为了验证整套算法的可行性,在python中,直接用了字典和数组,将每一个词汇变成了一个整数。

好比上面说的abcdefg的词汇ab,bc,cd,de,ef,fg,分别变成了[0,1,2,3,4,5]

使用特征矩阵来描述类似度


何为文本类似度的特征矩阵,咱们能够这么来定义

  • 一个特征矩阵的任何一行是全局全部元素中的 一个元素,任何一列是一个集合。
  • 若全局第i个 元素出如今第j个集合里面,元素(i, j) 为1,不然 为0。

好比咱们有world和could两个文本,设k为2经过k-shingle拆分之后,分别变成了[wo,or,rl,ld]和[co,ou,ul,ld]那么他们的特征矩阵就是

经过特征矩阵,咱们很容易看出来,两个文本的类似性就是他们公共的元素除以全部的元素,也就是1/7

在这个矩阵中,集合列上面不是0就是1,其实咱们能够把特征矩阵稍微修改一下,列上面存储的是该集合中词语出现的个数,我以为可靠性更高一些。

至此,咱们已经把一个简单的词汇表集合转换成上面的矩阵了,因为第一列的词汇表其实是一个顺序的数列,因此咱们须要存储的实际上只有后面的每一列的集合的数据了,并且也都是整数,这样存储空间就小多了。

继续优化特征矩阵,使用hash签名


对于保存上述特征矩阵,咱们若是还嫌太浪费空间了,那么能够继续优化,若是能将每一列数据作成一个哈希签名,咱们只须要比较签名的类似度就能大概的知道文本的类似度就行了,注意,我这里用了大概,也就是说这种方法会丢失掉一部分信息,对类似度的精确性是有影响的,若是在大量须要处理的数据面前,丢失一部分精准度而提供处理速度是能够接受的。

那么,怎么来制做这个hash签名呢?咱们这么来作

  • 先找到一组自定义的哈希函数H1,H2...Hn
  • 将每一行的第一个元素,就是词汇表hash后获得的数字,分别于自定的哈希函数进行运算,获得一组新的数
  • 创建一个集合(S1,S2...Sn)与哈希函数(H1,H2...Hn)的新矩阵T,并将每一个元素初始值定义为无穷大
  • 对于任何一列的集合,若是T(Hi,Sj)为0,则什么都不作
  • 对于任何一列的集合,若是T(Hi,Sj)不为0,则将T(Hi,Sj)和当前值比较,更新为较小的值。

仍是上面那个矩阵,使用hash签名之后,咱们获得一个新矩阵,咱们使用了两个哈希函数:H1= (x+1)%7 H2=(3x+1)%7 获得下面矩阵

而后,咱们创建一个集合组T与哈希函数组H的新矩阵

接下来,按照上面的步骤来更新这个矩阵。

  • 对于集合1,他对于H1来讲,他存在的元素中,H1后最小的数是1,对于H2来讲,最小的是0
  • 对于集合2,他对于H1来讲,他存在的元素中,H1后最小的数是0,对于H2来讲,最小的是2

因此,矩阵更新之后变成了

经过这个矩阵来计算类似度,只有当他们某一列彻底相同的时候,咱们才认为他们有交集,不然不认为他们有交集,因此根据上面这个矩阵,咱们认为集合1和集合2的类似度为0。这就是我刚刚说的大概的含义,他不能精确的表示两个文本的类似性,获得的只是一个近似值。

在编程的时候,上面那个矩阵其实并不须要彻底保存在内存中,能够边使用边生成,因此,对于以前用总体矩阵来讲,咱们最后只须要有上面这个签名矩阵的存储空间就能够进行计算了,这只和集合的数量还有哈希函数的数量有关。

这部分的简单算法描述以下:

res=[]
    for index1,v1 in enumerate(file_name_list):
        for index2,v2 in enumerate(file_name_list):
            g_hash.clear()
            g_val=0
            hash_contents=[]
            min_hashs=[]
            if(v1 != v2 and index2>index1):
                hash_contents.append(getHashInfoFromFile(v1)) #计算集合1的词汇表
                hash_contents.append(getHashInfoFromFile(v2)) #计算集合2的词汇表
                adjContentList(hash_contents) #调整hash表长度
                a=[x for x in range(len(g_hash))]
                minhash_pares=[2,3,5,7,11] #最小hash签名函数参数
                for para in minhash_pares:
                    min_hashs.append(calcMinHash(para,len(g_hash),a)) #最小hash签名函数生成        
                sig_list=calcSignatureMat(len(min_hashs)) #生成签名列表矩阵
                for index,content in enumerate(hash_contents):
                    calcSignatures(content,min_hashs,sig_list,index) #计算最终签名矩阵
                simalar=calcSimilarity(sig_list) #计算类似度
                res.append([v1,v2,simalar])

    return res


一样,具体代码能够参考个人GitHub,代码没优化,只是作了算法描述的实现,内存占用仍是多,呵呵

相关文章
相关标签/搜索