文檔的相似度(1)--Jaccard相似度與文檔的shingling

     在當今的計算機高速發展的時代,對於文章的查重等涉及到數據比對的需求越來越高了。

     爲了識別字面上相似的文檔,日常生活中我們所做的就是比對兩個文檔中相似的語句的比重,如果大部分內容都是相同的話,那麼我們就會判定這兩篇文檔很大程度上是有抄襲嫌疑的。其實這個過程完全是可以類比到計算中來的,自己看了資料(大數據 互聯網大規模數據挖掘與分佈式處理,此博客中大部分的理論都是引用於此書)剛好寫了一個簡單的文檔相似度分析的程序,剛好分享下。

     在編程中,我們可以利用集合的思想對文檔的相似度進行分析,進而將文檔表示成一個集合。將文檔表示成集合的最有效的方法是構建文檔中的短字符串集合,如果文檔採用這樣的集合表示,那麼有相同句子甚至短語的文檔之間將會擁有很多公共的集合元素,即使這兩篇文檔中的語序並不相同也是如此的,這是爲什麼呢?這也是正是爲什麼會採用短字符串作爲集合元素的原因,因爲在一篇中,都是由基本的詞組成的,那麼試想一下兩個極端的情況:(1)一種是將短字符串放大到整篇文章,也就是說一篇問的文檔中的所有詞放在一起作爲一個元素,可以想象,這是毫無用處的;(2)另一種是將文檔中的每一個單獨的詞作爲一個元素,這樣乍一看感覺確實不錯,但我們仔細想一下啊,無論是那種語言,它的詞的個數是有限的,在文檔中經常是重複使用了相同的詞,也就是說作爲經常使用的詞,以英文爲例:a、the、is、an。。。這些詞語幾乎在文檔中都會出現,那麼我們如何根據這些詞去判斷文檔的相似度呢?

集合的Jaccard相似度

      這裏我們可以關注一個特定的“相似度”概念,即通過計算交集的相對大小來獲得集合之間的相似度。這種相似度稱爲Jaccard相似度。

      它的定義爲:集合S和T的Jaccard相似度爲|S∩T|/|S∪T|,也就是集合S和集合T的交集和並集大小之間的比值。下面將S和T的Jaccard相似度記爲SIM(S,T)。

文檔的Shingling

     下面說一下很常用的一種方法:k-shingle

      對於一篇文章而言,就是一個大的字符串(此處主要討論英文的查重)。文檔的k-shingle定義爲其中任意長度爲k的字符串,因此,每篇文檔可以表示成文檔中出現一次或者多次的k-shingle的集合。

      比如說一個文檔中有字符串"abcdabd",當選擇k=2時,則此文檔中的所有2-shingle組成的集合爲 {ab,bc,cd,da,bd}。我們也許會注意到,子串ab在文檔中出現了兩次,但是在集合中我們只算了1次。其實shingle的有一種變形是將文檔表示成包,在包內每個shingle的出現次數也被考慮在內,當然,這裏我們主要討論基於集合的方法。

      對於空白串(空格、tab即回車等)的處理存在多種方法,常常將任意長度的空白字符串替換爲單個空格。

     當然,說到文檔,我們首先想到的應該是如何讀取一個文檔,代碼如下:

"""
此函數用於獲得fileName文件中的內容,文件內容存放在字符串中返回
"""
def getFileContent(fileName):
    file=open(fileName,"r")
    fileContent=file.read()
    fileContent=fileContent.replace("\t"," ")
    fileContent=fileContent.replace("\n"," ")
    fileContent=fileContent.replace("\r"," ")
    file.close()
    return fileContent
       既然是比較文檔之間的相似度,那麼自然不可能只是讀取一篇文檔了,下面的這個函數可以通過傳入一個文件夾名稱的字符串參數,返回該文檔下所有文件的名稱的字符串 列表:

"""
此函數用於獲取dir文件夾中的文件的內容
"""
def getFilesName(dir):
    fileList=[]
    t=os.walk(dir)
    file=dir+'\\'
    for item in t:
        for name in item[2]:
            fileList.append(file+name)
    return fileList

         下面本來應該是對文檔的內容進行shingle的,但是先不急,因爲這裏將會把shingle和哈希進行一塊操作。

shingle的大小的選擇

         理論上,我們可以選擇任意的常數作爲k。但是,正如前面所說的,如果選擇的k太小,比如手k=1,那麼可以推測大部分長度爲k的字符串會出現在大部分的文檔中。如果這樣做,那麼我們就會有很多 Jaccard相似度很高的文檔,即使他們之間沒有任何相同的句子甚至短語。極端的就是在k爲1的情況下,大部分文檔中都有很常見的字符,而其它字符相對較少,因此 ,此時幾乎所有的web網頁之間都有較高的Jaccard相似度。

        k值的選擇依賴於文檔的典型長度以及典型的字符表大小。我們需要記住的是:k應該選擇得足夠大,以保證任意給定的shingle出現在任意文檔中的概率最低。

        因此,如果文檔集由郵件組成,那麼選擇k=5應該比較合適。爲理解這其中的原因,假定郵件中只有字符和普通的空白符(儘管實際當中,大部分可打印ASCII字符都有可能偶爾出現在郵件中)。於是,所有可能的5-shingle個數爲27ʌ5=14348907,由於典型的郵件長度會遠遠低於1400萬字符,所以我們希望k=5將會處理得很好,實際上的確如此。

對shingle進行哈希

        可以不講字符串直接用成shingle,而是同過某個哈希函數將長度爲k的字符串映射爲桶編號,然後將得出的的桶編號看成最終的shingle。於是,可以將文檔表示成這些桶編號整數構成的集合,這些桶編號代表一個或 多個文檔中出現的k-shingle。舉例來說,對於文檔可以構件9-shingle集合,然後將每個9-shingle映射到0到2ʌ32-1之間的一個桶編號。因此,每個shingle由4個字節而不是9個字節來表示,這是因爲計算機中整數一般佔4個字節,而1個字節是八位,那麼整數在計算機中就是佔了32位,它的範圍可以表示到2ʌ32-1。這樣做不僅數據上得到了壓縮,而且可以對哈希後得到的整數shingle進行單字機器運算。

        在這裏我就偷個懶了,其實也是由於比較糾結哈希函數的選擇,所以乾脆跳過了這一步,改而使用python提供的集合set代替哈希(因爲集合其實就是通過哈希創造出來的,當然這樣就會由於沒有針對性而造成效率上的損失,以後會做出改進)。

       下面列出一個簡單的函數處理:

"""
此函數用於對各個文件中的內容進行k-shingle,然後對詞條進行哈希(此處就用字典存儲了)
其中dir是文件夾的名稱字符串類型,k是int型
"""
def getShingleList(dir,k):
    fileList=getFilesName(dir)

    shingleList=list()
    for fileName in fileList:
        fileContent=getFileContent(fileName)
        shingle = set()
        for index in range(0,len(fileContent)-k+1):
            shingle.add(fileContent[index:index+k])
        shingleList.append(shingle)
    return shingleList
        此篇博客暫時就說這麼多,之後會繼續跟進。

        

         


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章