1. 程式人生 > >文件的相似度(2)--最小雜湊簽名

文件的相似度(2)--最小雜湊簽名

           接著上一篇的部落格繼續下去,這篇部落格主要講下最小雜湊簽名的東西。

          對於上篇部落格中提到的shingle,可以說是在壓縮資料量的基礎上又儘可能保留了源文件的特徵,以便於後面對不同的文件進行相似度比較。但是我們會發現,shingle集合非常大,即使將每個shingle都雜湊為4個位元組,一篇文件的shingle集合所需要的空間仍然大概是該文件所需空間的4倍(這是因為shingle分詞的特性,導致分詞後shingle的 個數略等於文件中字母的個數,而一個字母一般在計算機中佔一個位元組,shingle雜湊佔了4個位元組,故而是4倍左右)。那麼可想而知,如果有數百萬篇文件,很可能不能將這些文件的shingle集合都放入記憶體中,或者及時所有的集合都可以放入到記憶體中,那所需要對的數目也可能會多到無法估計沒對的相似度。

        這裡就需要將上述大集合替換成規模很多的“簽名”(signature)表示。所謂簽名,在日常生活中我們很多地方都需要簽名,進而代表我們個人,我們這裡的“簽名”其實代表的 就是一篇文件,它是由文件中經過特殊方法選擇出的具有代表性的資料組成。對於簽名而言,我們所需要的重要特徵是能夠僅僅通過比較兩篇文件的簽名集合就可以估計實際shingle集合之間的Jaccard相似度。當然,通過簽名無法得到原始shingle集合之間Jaccard相似度的精確值,但是估計結果與真實結果相差不大,並且簽名集合越大,估計的經度越高。例如,50000位元組文件的shingle可能會對映為200000位元組的雜湊結果,然後替換成1000位元組大小的簽名集合。基於最終簽名集合得到的原始文件Jaccard相似度的估計值與真實值的差異也就在幾個百分點之內。

集合的矩陣表示

在介紹如何構建雜湊簽名之前,首先來說一下如何將一系列集合表示成其特徵矩陣。矩陣的列對應集合,行對應全集(所有集合中可能的元素組成全集)中的元素。如果行r對應的元素屬於列c對應的集合,那麼矩陣第r行第r列的元素為1,否則為0。

例如:

下圖給出了全集{a,b,c,d,e}中元素組成的多個集合的矩陣表示。這裡S1={a,d},S2={c},S3={b,d,e},S4={a,c,d}。圖中最上面一行和最左邊一列並非矩陣的一部分,而是表示各行和各列的含義。

       

                                                    圖1

          需要記住的是,特徵矩陣並非資料真正的儲存方式,但是作為資料視覺化的一種方式則是非常有用的。在實際當中,資料不會儲存為矩陣的一個原因是該矩陣往往非常稀疏(0的個數遠多於1)。只儲存1所在的位置能夠大大節省儲存的開銷,同時又能完整地表示整個矩陣。另外一個原因是,資料往往基於其它目的而儲存成其它格式。

最小雜湊

想要構建的集合的簽名由大量計算(比如數百次 )的結果組成,而每次計算是特徵矩陣的最小雜湊過程。

       為了對特徵矩陣每列所表示的集合進行最小雜湊計算,首先選擇行的一個排列轉換(即是將行號重新排列)。任意一列的最小雜湊值是在排列轉換後的行排列次序下第一個列值為1的行的行號。

例如:

        對於圖1中的矩陣,假定採用beadc的行序重新排列,如下圖。改排列轉換定義了一個最小雜湊函式h,它將某個集合對映成一行。接下來我們基於函式h計算集合S1的最小雜湊值。按照beacd的順序來掃描集合S1所對應的第一列,由於b行對應的值為0,所以需要往下繼續掃描到e行,即排列轉換次序中的第二行,其對應的S1列的值仍然是0.於是再往下處理到行a,此時其對應的值為1,因此,就有了h(S1)=a。

       儘管物理上不可能對非常大的特徵矩陣進行排列轉換,最小雜湊 函式h卻隱式地將圖1矩陣的行重新排列,使之變成圖2中的舉證。在新矩陣中,h函式的值可以通過從上往下掃描至遇到1為止。因此,我們有h(S2)=c、h(S3)=b及h(S4)=a。

            

                                                          圖2

最小雜湊及Jaccard相似度

        在集合的Jaccard相似度及集合的最小雜湊函式值之間存在著非同尋常的關聯:

        □ 兩個集合經隨機排列轉換之後得到的兩個最小雜湊值相等的概率等於這兩個集合的Jaccard相似度。

        為了理解上述結論的原因,必須要對兩個集合同一列對應的所有可能結果進行列舉。假設只考慮結合S1和S2所對應的列,那麼他們所在的行可以按照所有可能的結果分成三類:

(1)資料X類的行,兩列的值均為1;

(2)資料Y類的行,其中一列的值為0,另一列的值為1;

(3)屬於Z類的行,兩列的值均為0。

         由於特徵矩陣十分稀疏,因此大部分行都屬於Z類。但是X和Y類行數目的比例決定了SIM(S1,S2)及概率h(S1)=h(S2)的大小。假定X類行的數目為x,Y類的行的數目為ym,則SIM(S1,S2)=x/(x+y)。原因是S1∩S2的大小為x而S1∪S2的大小為x+y。

         接下來考慮h(S1)=h(S2)的概率。設想所有行進行隨機排列轉換,然後我們從上到下進行掃描處理,在碰到Y類行之前碰到X類行的概率為x/(x+y)。但是如果從上往下掃描遇到的除Z類行之外的第一行屬於X類,那麼肯定有h(S1)=h(S2)。另一方面,如果首先碰到的是Y類行,而不是Z類行,那麼值為1的那個集合的最小雜湊值為當前行。但值為0的那個集合必將會進一步掃描下去。因此 ,如果首先碰到Y類行,那麼此時h(S1)≠h(S2).於是,我們可以得到最終結論,即h(S1)=h(S2)的概率為x/(x+y),而這也是兩個集合Jaccard相似度的計算公式。

最小雜湊簽名

  此處將會繼續講解前面介紹的一系列集合的特徵矩陣表示M。為表示這些集合,我們隨機選擇n個排列轉換用於矩陣M的行處理。其中n一般為一百或幾百。對於集合S對應的列,分別呼叫這些排列轉換所決定的最小雜湊函式h1,h2,h3,.......,hn,則可以構建S的最小雜湊簽名向量[h1(S),h2(S),.......,hn(S)],該向量通常寫成列向量方式。因此,基於矩陣M可以構建一個簽名矩陣,其中M的每一列替換成該列所對應的最小雜湊簽名向量即可。

       需要注意的是,簽名矩陣與 M的列數相同但行數只有n。即使不顯示錶示M中的全部元素而採用適合於稀疏矩陣的某種壓縮形式(比如只儲存1所在的位置)來表示,通常情況下簽名矩陣所需要的空間仍比矩陣M本身的表示空間要小許多。

最小雜湊簽名的計算

        對於大規模特徵矩陣進行顯式排列轉換是不可行的。即使對上百萬甚至數十億的行選擇一個隨機排列轉換也是極其消耗時間,而對行進行必要的排序則需要花費更多的時間。因此,類似圖2給出的排列轉換的矩陣在概念上十分吸引人,但卻缺乏可操作性。

       幸運的是,我們可以通過一個隨機雜湊函式來模擬隨機排列轉化的效果,該函式將行號對映到與行數目大致相等的數量的桶中。通常而言,一個將整數0,1,2.....,k-1對映到桶號0,1,2,......,k-1的雜湊函式會將某些整數對對映到同一個桶中,而有些桶卻沒有被任何整數對映到。然而,只要k很大且雜湊結果衝突不太頻繁的話,差異就不是很重要。於是,我們就可以繼續假設雜湊函式“h”將原來的第r行放在排列轉換後次序中的第h(r)個位置上。

      因此,我們就可以不對行選擇n個隨機排列轉換,取而代之的是隨機選擇n個雜湊函式h1,h2,.......,hn作用於行。在上述處理基礎上,就可以根據每行在雜湊之後的位置來構建簽名矩陣。令SIG(i,c)為簽名矩陣中第i個雜湊函式在第c列上的元素。一開始,對於所有的i和c,將SIG(i,c)都初始化為∞。然後,對行進行如下操作:

     (1)計算 h1(r),h2(r),......,hn(r)。

      (2)對每列c進行如下操作:

(a)如果c在第r行為0,則什麼也不做;

(b)否則,如果c在第r行為1,那麼對於每個i=1,2,....,n,將SIG(i,c)置為原來的SIG(i,c)hi(r)之中的較小值。

例如:在此考慮圖1對應的特徵矩陣,我們在後面加上一些資料形成圖3。另外將每一行替換成其對應的行號0,1,.....,4。選擇的兩個雜湊函式分別為h1(x)=x+1 mod 5及h2(x)=3x+1 mod 5.兩個雜湊函式產生的結果顯示在圖3-4中的最後兩列。注意到這裡的兩個簡單雜湊函式對應真正的行排列轉換,當然這裡這有當行數目為質數(這裡為5, 這是為了避免不同的數之間具有相同的約數而導致餘數會相等進而會被分配到一個桶號中,這就會產生衝突了)時才會有真正的排列轉換。通常來說,雜湊結果都會存在衝突,即至少有兩行得到的雜湊值相等。

          

                                                                                       圖3

接下來 模擬計算簽名矩陣的演算法。一開始,簽名矩陣全部都由∞構成:

                           

           首先 ,考慮圖3中的第0行。此時,不論是h1(0)還是h2(0)的結果都是1。而只有集合S1和S4在第0行為1,因此簽名矩陣中只有這兩列的值需要修改。因為1<∞,因此實際上是對S1和S4的對應值進行修改,所以當前簽名矩陣的估計結果為:

                            

           接下來,我們下移到圖3中的第一行。對於該行,只有S3的值為1,此時其雜湊值為h1(1)=2,h2(1)=4。因此,SIG(1,3)置為2,SIG(2,3)置為4。因為第一行中其它列的值均為0,所以簽名矩陣的相應列的元素保持不變。於是,新的簽名矩陣為:

                             

          圖3第2行中只有S2和S4對應的列的值為1,且其雜湊值h1(2)=3,h2(2)=2,。S4對應的標籤名本應修改,但是簽名矩陣中對應列值為[1,1],因此其簽名最後不會修改。而S2對應的列中仍然是初始值∞,我們將其修改為[3,2],得到如下圖:

                               

           再接下來處理圖3中的第3行。此時只有S2對應的列的值不為1。而雜湊值h1(3)=4,h2(3)=0。h1的結果已經超過了矩陣中所有列上的已有值,因此不需要修改簽名矩陣的第一列的任一值。然而,h2的值為0小於矩陣元素,因此將SIG(2,1)、SIG(2,3)及SIG(2,4)減小為0。需要注意的是,由於圖3中S2列在當前行的取值已經為0,因此SIG(2,2)不可能再減小。於是,此時得到的簽名矩陣為:

                                

           最後考慮圖3中的第4行,此時h1(4)=0,h2(4)=3。由於第4行只在S3列取值為1,我們僅僅比較S3的當前值[2,0]與雜湊值[0,3]即可。由於0<2,因此將SIG(1,3)改為0,而同時由於 3>0,因此SIG(2,3)保持不變。最終得到的簽名矩陣為:

                                   

           基於上述簽名矩陣,可以估計原始集合之間的Jaccard相似度。注意到在簽名矩陣中S1和S4對應的列向量完全相同,因此我們可以猜測SIM(S1,S4)=1.0。如果回到圖3,會發現S1和S4的真是Jaccard相似度為2/3.需要記住的是,簽名矩陣中行之間的一致程度只是真實Jaccard相似度的一個估計值,因為本例規模太小,所以並不足以說明在大規模資料情況下估計值和真實值相近的規律。另外,在本例中,S1和S3在簽名矩陣中有一半元素一致(真實相似度為1/4),而S1和S2在簽名矩陣中沒有相同元素,所以相似度估計值為0(真實相似度也為0)。

(注:以上理論性的知識全部是來源於上篇部落格中所提到的書裡的,所以是可靠的)。

下面附上自己的python程式碼:

這個是依據上述理論一個版本

"""
此函式用於獲得所有文件的最小雜湊簽名,signatureNum表示簽名行數
"""
def getMinHashSignature(shingleList,signatureNum):
    #tatalSet用於存放所有集合的並集
    totalSet=shingleList[0]
    for i in range(1,len(shingleList)):
        totalSet=totalSet|shingleList[i]
    temp=int(math.sqrt(signatureNum))
    #randomArray用於模擬隨機雜湊函式
    randomArray=[]
    #signatureList用於存放總的雜湊簽名
    signatureList=[]
    maxNum=sys.maxint
    for i in range(signatureNum):
        randomArray.append(random.randint(1,temp))
        randomArray.append(random.randint(1,temp))
    #buketNum用於記錄所有元素的個數,作為隨機雜湊函式的桶號
    buketNum=len(totalSet)
    for i in range(signatureNum):
        """
        A用於代表隨機雜湊函式的係數,B代表常數,signature用於存放雜湊函式產生的簽名
        """
        A=randomArray[i*2]
        B=randomArray[i*2+1]
        signature=[]
        for shingleSet in shingleList:
            minHash=maxNum
            index=-1
            for item in totalSet:
                index+=1
                if item in shingleSet:
                    num=(A*index+B)%buketNum
                    minHash=min(minHash,num)
            signature.append(minHash)
        signatureList.append(signature)
    return signatureList

此處是上述函式的一個跟進版本,做了些微修補:
"""
此處是新版的函式,將雜湊簽名的矩陣換的行列換了一下,便於接下來使用
"""
def getMinHashSignature(shingleList,signatureNum):
    #tatalSet用於存放所有集合的並集
    totalSet=shingleList[0]
    for i in range(1,len(shingleList)):
        totalSet=totalSet|shingleList[i]

    temp=int(math.sqrt(signatureNum))
    #randomArray用於模擬隨機雜湊函式
    randomArray=[]
    #signatureList用於存放總的雜湊簽名
    signatureList=[]
    maxNum=sys.maxint
    for i in range(signatureNum):
        randomArray.append(random.randint(1,temp*2))
        randomArray.append(random.randint(1,temp*2))
    #buketNum用於記錄所有元素的個數,作為隨機雜湊函式的桶號
    buketNum=len(totalSet)
    """
    此處將不同文件的自己的雜湊簽名存成一個list,然後再進行彙總到一個總的list
    """
    for shingleSet in shingleList:
        """
        signature用於存放雜湊函式產生的簽名
        """
        signature=[]
        for i in range(signatureNum):
            minHash=maxNum
            for index,item in enumerate(totalSet):
                if item in shingleSet:
                    num=(randomArray[i*2]*index+randomArray[i*2+1])%buketNum
                    minHash=min(minHash,num)
            signature.append(minHash)
        signatureList.append(signature)
    return signatureList

下面的函式是對相似度進行計算:
"""
此函式通過比較兩個文件的最小雜湊簽名進行計算相似度,傳入的參入是兩個文件的最小雜湊簽名的集合,
存放在list中,最後結果返回相似度
"""
def calSimilarity(signatureSet1,signatureSet2):
    count=0
    for index in range(len(signatureSet1)):
        if(signatureSet1[index]==signatureSet2[index]):
            count+=1
    return count/(len(signatureSet1)*1.0)

"""
此函式用於將計算所有文件的相似度,並將結果存放在一個list中,結果用元組存放
"""
def calAllSimilarity(signatureList,filesName):
    signatureNum=len(signatureList)
    fileNum=len(filesName)
    result=[]
    for index1,signatureSet1 in enumerate(signatureList):
        for index2,signatureSet2 in enumerate(signatureList):
            if(index1<index2):
                result.append((calSimilarity(signatureSet1,signatureSet2),filesName[index1],filesName[index2]))
    return result


dir="D://E07"              # 存放文件的資料夾路徑
filesName=getFilesName(dir)
shingleList=getShingleList(dir,4)   #此處數字控制取出的對比的欄位的長度
signatureList=getMinHashSignature(shingleList,100)  #此處數字表示從程式碼中去的個數
result=calAllSimilarity(signatureList,filesName)
result.sort()
result.reverse()
for each in result:
    print each
          至此,一個簡單版本的查重小程式就出來了,後面將會繼續跟進,做進一步的更新。