讀書筆記---《集體智慧編程》第3章:發現羣組

1.關於聚類的理解

聚類實際上就是分類,對一些樣本(樣品)進行歸類分組。本章第一個例子是對99篇博客進行聚類,也就是說每一篇博客便是一個樣本。要分類就要有分類的標準(指標)。比如把人按地區、身高、體重分類,那地區、身高、體重就是指標。抽象地說,對樣本X ,設有p 個指標,即X=(X1,X2,,Xp)T .在博客聚類的這個例子中,選取的分類指標是一些單詞(這裏暫時不管爲什麼要選這些單詞),即爲china,kids,music,yahoo,want等,總共706個,即有706個指標(變量)。統計出每篇博客中這些單詞出現的次數,即爲該篇博客的指標值(樣本值)。

2.對博客進行聚類

對99篇博客中指定單詞的統計結果存放到了blogdata.txt文件(隨書文件中附帶的有,也可以按照書中方法獲取)中,用記事本打開看的話會比較亂,用Notepad++打開之後,截圖如下:
blogdata.txt

剛好這類似於R語言中數據框的結構,本來我想用R或者SAS來完成本章的任務,畢竟有現成的函數,不過在直接導入blogdata.txt這個文件時,均出現了問題,這個以後再解決。

在《數據分析方法》中,我們學了快速聚類法與譜系聚類法。下面來看本書中的算法。

2.1 分級聚類法

分級聚類法(Hierarchical Clustering)實際上就是譜系聚類法,具體原理和步驟可以參見《數據分析方法》一書。譜系聚類的關鍵是依據樣品間的距離來定義類與類之間的距離。本章的分級聚類法實際上在計算類間距離時,採用的是重心距離,即用兩類的重心之間的距離作爲兩類間的距離。

2.1.1讀取數據

爲了方便數據的處理,我們定義一個讀取數據的函數,代碼如下(使用版本爲Python 3.3)

def readfile(filename):
    lines=[line for line in open(filename)]

    #第一行是列標題,也就是被統計的單詞是哪些
    colnames=lines[0].strip().split('\t')[1:]#之所以從1開始,是因爲第0列是用來放置博客名了
    rownames=[]
    data=[]
    for line in lines[1:]:
        p=line.strip().split('\t')
        #每行的第0列都是行名
        rownames.append(p[0])
        #剩餘部分就是該行對應的數據
        data.append([float(x) for x in p[1:]])#data是一個列表,這個列表裏每一個元素都是一個列表,每一列表的元素就是對應了colnames[]裏面的單詞
    return rownames,colnames,data

這裏代碼中的第一行

lines=[line for line in open(filename)]

中用了open函數,而原書代碼是用了file函數,但我運行時出了問題,原書代碼均是用Python 2.x編寫的,因此會略有不同。此外,該函數採用元組返回值實現了返回多個函數值的辦法。

此外,本書中經常使用reload函數,在Python 3.x中,應該這樣使用

from imp import reload
reload(MyModule)

或者寫爲

import imp
imp.reload(MyModule)

因爲在Python 3.x中,把reload內置函數移到了imp標準庫模塊中。它仍然像以前一樣重載文件,但是必須導入它才能使用。

2.1.2計算緊密度(相似度)

這裏要計算兩篇博客(樣本)的距離,可以通過計算兩個樣本X(i)X(j) 的Pearson相關係數rij ,再令距離度量爲

dij=1|rij|

這樣的話,兩篇博客越相似(相關係數越大),其距離越小。

假設有兩個變量XY ,則總體的相關係數爲

ρX,Y=Cov(X,Y)DXDY=E(XY)E(X)E(Y)E(X2)(EX)2E(Y2)(EY)2

設樣本觀測數據爲

(X1Y1X2Y2XnYn)

則樣本相關係數爲

r=SXYSXXSYY=i=1n(XiX¯)(YiY¯)i=1n(XiX¯)2i=1n(YiY¯)2

進行適當化簡可得

r=i=1nXiYii=1nXii=1nYini=1nX2i(i=1nXi)2ni=1nY2i(i=1nYi)2n

計算代碼如下:

from math import sqrt
def pearson(v1,v2):
    #先求和
    sum1=sum(v1)
    sum2=sum(v2)

    #求平方和
    sum1Sq=sum([pow(v,2) for v in v1])
    sum2Sq=sum([pow(v,2) for v in v2])

    #求乘積之和
    pSum=sum([v1[i]*v2[i] for i in range(len(v1))])

    #計算pearson相關係數
    num=pSum-(sum1*sum2/len(v1))
    den=sqrt((sum1Sq-pow(sum1,2)/len(v1))*(sum2Sq-pow(sum2,2)/len(v1)))
    if den==0:return 0

    return 1.0-num/den

2.1.3聚類過程

這裏採用面向對象的思維進行編程,將每一篇博客看成是一個對象,爲此定義一個類,代碼如下:

class bicluster:
    def __init__(self,vec,left=None,right=None,distance=0.0,id=None):
        self.left=left
        self.right=right
        self.vec=vec #就是詞頻列表
        self.id=id
        self.distance=distance

下面開始聚類的計算。在《數據分析方法》中,我們是先計算出了距離矩陣,然後利用遞推的方法逐步計算。這裏,我們也編寫一個hcluster函數直接進行計算,該函數爲一個列表數組(就是在讀取數據時返回的data)和一個距離函數(Python的函數式編程確實也很不錯),最後返回一個bicluster的對象,只有一個,但是這個對象是根節點,如果擴展其左右孩子,最後會得一個粗略的樹狀圖。代碼如下:

def hcluster(rows,distance=pearson):
    distances={} #每計算一對節點的距離值就會保存在這個裏面,這樣避免了重複計算
    currentclustid=-1

    #最開始的聚類就是數據集中的一行一行,每一行都是一個元素
    #clust是一個列表,列表裏面是一個又一個bicluster的對象
    clust=[bicluster(rows[i],id=i) for i in range(len(rows))]

    while len(clust)>1:
        lowestpair=(0,1)#先假設lowestpair是0和1號
        closest=distance(clust[0].vec,clust[1].vec)#同樣將0和1的pearson相關度計算出來放着
        #遍歷每一對節點,找到pearson相關係數最小的
        for i in range(len(clust)):
            for j in range(i+1,len(clust)):
                #用distances來緩存距離的計算值
                if(clust[i].id,clust[j].id) not in distances:
                    distances[(clust[i].id,clust[j].id)]=distance(clust[i].vec,clust[j].vec)
                d=distances[(clust[i].id,clust[j].id)]
                if d<closest:
                    closest=d
                    lowestpair=(i,j)
        #找到這一次循環的最小一對後,產生新的枝節點.先計算出這個新的枝節點的詞頻(重心)
        mergevec=[(clust[lowestpair[0]].vec[i]+clust[lowestpair[1]].vec[i])/2.0 for i in range(len(clust[0].vec))]

        #建立新的聚類
        newcluster=bicluster(mergevec,left=clust[lowestpair[0]],right=clust[lowestpair[1]],distance=closest,id=currentclustid)

        #不在初始集合中的聚類,其id設置爲負數
        currentclustid-=1
        del clust[lowestpair[1]]
        del clust[lowestpair[0]]
        clust.append(newcluster)

    #當只有一個元素之後,就返回,這個節點相當於根節點
    return clust[0]

2.1.4粗略的樹狀圖

這裏只是利用了縮進而已,先編寫函數:

def printclust(clust,labels=None,n=0):
    #利用縮進來建立層級佈局
    for i in range(n):print(' ',end=" ")
    if clust.id<0:
        #負數代表這是一個分支
        print('-')
    else:
        #正數代表這是一個葉節點
        if labels==None: print(clust.id)
        else:print(labels[clust.id])
    if clust.left!=None:printclust(clust.left,labels=labels,n=n+1)
    if clust.right!=None:printclust(clust.right,labels=labels,n=n+1)

然後在執行如下代碼即可畫出粗略的樹狀圖,這裏就不展示了。

blognames,words,data=readfile('blogdata.txt')
clust=hcluster(data)
printclust(clust,labels=blognames)

要注意的是與原書代碼不同的地方在於print函數,Python 3.x與Python 2.x關於print的主要區別如下:

2.X: print "The answer is", 2*2 
3.X: print("The answer is", 2*2) 
2.X: print x,             # 使用逗號結尾禁止換行 
3.X: print(x, end=" ")    # 使用空格代替換行 
2.X: print                # 輸出新行 
3.X: print()              # 輸出新行 
2.X: print >>sys.stderr, "fatal error" 
3.X: print("fatal error", file=sys.stderr) 
2.X: print (x, y)         # 輸出repr((x, y)) 
3.X: print((x, y))        # 不同於print(x, y)! 

2.1.5精細的樹狀圖

實際上就是我們學過的譜系圖。爲了用Python畫出這個圖,需要用到Python的圖像處理模塊PIL(Python Image Library),其並不支持Python3,但網上有人把它重新編譯生成Python3下可安裝的exe了,比如我下載的就是PIL-1.1.7.win32-py3.3.exe,直接搜索PIL py3.3就出來了,然後安裝即可。

畫圖的過程還是很複雜的,具體可見這篇博客
畫樹狀圖

總體代碼如下:

from PIL import Image,ImageDraw

def getheight(clust):
    #這是一個葉節點嗎?若是,則高度爲1
    if clust.left==None and clust.right ==None:return 1
    #否則,高度爲每個分支的高度之和
    return getheight(clust.left)+getheight(clust.right)


def getdepth(clust):
    #一個葉節點的距離是0.0,這是因爲葉節點之後就沒有了,將其放在最左邊也沒事
    if clust.left==None and clust.right ==None:return 0

    #而一個枝節點的距離等於左右兩側分支中距離較大的那一個
    #加上自身距離:所謂自身距離,與就是某節點與兩一節點合併時候的相似度
    return max(getdepth(clust.left),getdepth(clust.right))+clust.distance

def drawdendrogram(clust,labels,jpeg='clusters.jpg'):
    #高度和寬度
    h=getheight(clust)*20
    w=1200
    depth=getdepth(clust)

    #我們固定了寬度,所以需要對每一個節點的橫向擺放做一個縮放,而不像高度一樣,每一個葉節點都分配20
    scaling=float(w-150)/depth

    #新建一張白色的背景圖片
    img=Image.new('RGB',(w,h),(255,255,255))
    draw=ImageDraw.Draw(img)

    draw.line((0,h/2,10,h/2),fill=(255,0,0)) #僅僅是畫了一個起點

    #畫第一個節點
    drawnode(draw,clust,10,(h/2),scaling,labels)
    img.save(jpeg,'JPEG')

def drawnode(draw,clust,x,y,scaling,labels):
    if clust.id<0:
        h1=getheight(clust.left)*20 #兩個分支的高度
        h2=getheight(clust.right)*20

        top=y-(h1+h2)/2 #如果是第一次畫點的話,top居然是最高點,也就是等於0,是上面邊界.針對某一個節點,其高度就是左節點的高度加右節點的高度
        bottom=y+(h1+h2)/2 #這個確實也是下邊界。

        #線的長度
        ll=clust.distance*scaling

        #聚類到其子節點的垂直線
        draw.line((x,top+h1/2,x,bottom-h2/2),fill=(255,0,0))

        #連接左側節點的水平線
        draw.line((x,top+h1/2,x+ll,top+h1/2),fill=(255,0,0))

        #連接右側節點的水平線
        draw.line((x,bottom-h2/2,x+ll,bottom-h2/2),fill=(255,0,0))

        #調用函數繪製左右節點
        drawnode(draw,clust.left,x+ll,top+h1/2,scaling,labels)
        drawnode(draw,clust.right,x+ll,bottom-h2/2,scaling,labels)
    else:
        #如果這是一個葉節點,則繪製節點的標籤.其實現在突然覺得這種思路非常好,繪製的是標籤,本題中繪製的博客名字
        draw.text((x+5,y-7),labels[clust.id],(0,0,0))

blognames,words,data=readfile('blogdata.txt')
clust=hcluster(data)
drawdendrogram(clust,blognames,jpeg='分級聚類圖.jpg')

畫出來的圖如下:
譜系圖

2.2 列聚類

這裏也就是將整個blogdata.txt中的數據集進行了轉置,使列(也就是單詞)變成了行,其中的每一行都對應一組數字,這組數字指明瞭某個單詞在每篇博客中出現的次數。轉置後在調用上面的函數即可。

至於這種方式聚類出來的結果有何意義,需要結合具體情況分析。

2.3 K 均值聚類

K 均值聚類(K-Means Clustering)實際上就是我們學過的快速聚類法,原理和步驟可參見《數據分析方法》,只不過本章中的K 均值聚類中最初的k 個聚點是隨機生成的。

未完待續

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