文本類似度計算-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,代碼沒優化,只是作了算法描述的實現,內存佔用仍是多,呵呵

相關文章
相關標籤/搜索