有句話這麼說“物以類聚,人以群分”,說的很有道理,有些人就完全融入不了一些團隊,我覺得這裡面不只是氣場的問題,我也一直在苦苦思索這個問題,每個人有自己的生活習慣和思維習慣,相同生活習慣和思維習慣的人很容易聚在一起;反之則比較難了。
目標很明確,給你一堆電子書,怎麼把其中一些書歸為一類,其中另一些書歸為另一類······,我會先看書的厚度,太厚的書(超過800頁)我歸為和我沒什麼關係這一類(偶爾會有例外),下來我會按照我的愛好細分,專業類、經濟類、心理類、攝影類和其他類。如果書很多了,我一本一本的分類會很費時間和精力。
我有個朋友做資料採礦的,他給我說用貝葉斯分類,我研究了一下,貝葉斯的確是個好東東,但有個問題,需要提前建立一個模型,我有建立這個模型的時間還不如自己一本一本分類來著。有沒有不用先驗經驗就可以分類的辦法呢?我查了一下還真有:無監督學習,貝葉斯這種需要事前建立一個模型的演算法屬於監督學習。
聚類,根據什麼聚類呢?我的目的是根據書籍的內容來區分,書籍的內容怎麼描述呢?書籍無非是一些詞和片語(成語也算為片語吧?),根據詞和片語怎麼來區分書籍的分類呢?經驗是,不同分類書籍常用詞不同。什麼意思,比方說經濟類的書籍通常會出現“經濟”,“增長”,“蕭條”,“膨脹”,,,甚至“卡特爾”,“托拉斯”等等,攝影類的書籍通常會出現“光圈”,“快門”,“ISO”,“曝光”,“構圖”等等。能不能根據詞彙在書籍中出現的比例來判定這本書是那個分類的呢,好像是可以。於是有了下面的變數;
books = { 'book1': {'word1': 2, 'word2': 3, 'word3': 3, 'word4': 12, 'word5': 13, 'word6': 12}, 'book2': {'word1': 3, 'word2': 3, 'word3': 1, 'word4': 13, 'word5': 12, 'word6': 11}, 'book3': {'word1': 1, 'word2': 10, 'word3': 3, 'word4': 5, 'word5': 11, 'word6': 13}, 'book4': {'word1': 12, 'word2': 13, 'word3': 12, 'word4': 3, 'word5': 2, 'word6': 2}, 'book5': {'word1': 13, 'word2': 12, 'word3': 11, 'word4': 1, 'word5': 3, 'word6': 3}, 'book6': {'word1': 5, 'word2': 11, 'word3': 13, 'word4': 3, 'word5': 1, 'word6': 10}}
變數的含義很明確,數字表示該book中出現該word的次數。
為了實現方便,我簡化該資料結構:
books = [ [2, 3, 3, 12, 13, 12], [3, 3, 1, 13, 12, 11], [1, 10, 3, 5, 11, 13], [12, 13, 12, 3, 2, 2], [13, 12, 11, 1, 3, 3], [5, 11, 13, 3, 1, 10]]
下面來分類,一種叫分級分類的演算法,該演算法通過連續不斷的將最為相似的群組兩兩合并,
最終構造出一個群組的層級結構。相似性可以用上篇所說的皮爾遜相關度計算,注意的是最後一行,
皮爾遜公式計算出來的結果越相似值會越大,這裡我想讓越相似的值越小,
表示距離近,所以用1減了皮爾遜公式的傳回值;
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))]) 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
分級分類演算法先算一次所有book之間的相似性,把最相似的聚為一類,然後用他們的均值代表一個新值;
第二遍計算其他剩餘書籍和新值組成的新數組之間兩兩相似性,把最相似的聚為一類,然後用他們的他兩的均值代表一個新值;
。。。
直到最後就剩下一個元素,結束。
這裡面有個可以最佳化的地方,我們看第一遍計算了所有元素之間的相似性,在第二遍時還要再計算,
所以我們可以把中間結果值緩衝下來,來減少中間的計算。
我們先定義一個資料結構,代表聚類的一條資料:
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
下面來看看分級分類演算法的實現:
def hcluster(rows,distance=pearson): distances={} currentclustid=-1 clust=[bicluster(rows[i],id=i) for i in range(len(rows))] while len(clust)>1: lowestpair=(0,1) closest=distance(clust[0].vec,clust[1].vec) for i in range(len(clust)): for j in range(i+1,len(clust)): 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) currentclustid-=1 del clust[lowestpair[1]] del clust[lowestpair[0]] clust.append(newcluster) print len(clust) return clust[0]
中間的del只是刪除了clust數組中的元素,也就是只刪除了指標,實際元素並沒有被刪除。
這個演算法好像沒有解決我得問題,最後都聚為一類的,並沒有我想要的聚為幾類。
並且這個演算法的計算量很驚人,因為我們必須計算每兩個配對項之間的關係,
並且在合并項之後,這些關係還得重新再計算。
還有一種K-均值聚類的演算法好像可以滿足我的要求:
K-均值聚類演算法首先會隨即確定K個中心位置,然後將各個資料項目分配給最接近的中心點。
分配完成後,聚類中心會移動到分配給該聚類的所有節點的平均位置處。然後整個分配過程重新開始。
這一過程會一直重複下去,直到分配過程不再產生變化為止。
下面代碼執行個體:
import randomdef kcluster(rows,distance=pearson,k=4): ranges=[(min([row[i] for row in rows]),max([row[i] for row in rows])) for i in range(len(rows[0]))] clusters=[[random.random()*(ranges[i][1]-ranges[i][0])+ranges[i][0] for i in range(len(rows[0]))] for j in range(k)] lastmatches=None for t in range(100): print 'Iteration %d' % t bestmatches=[[] for i in range(k)] for j in range(len(rows)): row=rows[j] bestmatch=0 for i in range(k): d=distance(clusters[i],row) if d<distance(clusters[bestmatch],row): bestmatch=i bestmatches[bestmatch].append(j) if bestmatches==lastmatches: break lastmatches=bestmatches for i in range(k): avgs=[0.0]*len(rows[0]) if len(bestmatches[i])>0: for rowid in bestmatches[i]: for m in range(len(rows[rowid])): avgs[m]+=rows[rowid][m] for j in range(len(avgs)): avgs[j]/=len(bestmatches[i]) clusters[i]=avgs return bestmatches
對代碼進行點解釋:
ranges變數儲存第一次隨機產生中心點的值範圍,也就是word出現頻率的上下限
clusters變數就是隨機產生的K個中心點
迴圈100次:
定義bestmatches存放聚類到K個中心點的數組
對每一行:
計算每一行到K個中心點的距離,將改行放入最近中心點的數組bestmatches中
如果本次計算結果和上次計算結果相同,說明聚類完成,退出
重新計算聚類中心,每個聚類中心為聚到該類的行的平均值
來看看我機器上的結果,效果還算不錯吧
>>> clust = clusters.kcluster(books,clusters.pearson,2)Iteration 0Iteration 1>>> print clust[[3, 4, 5], [0, 1, 2]]