第三章是第二章的擴展,引入了“數據聚類”的概念,這是一種用以尋找緊密相關的事、人或觀點,並將其可視化的方法。
聚類(data intensive)時常被用於數據量很大的應用中。本章第一個例子將對博客用戶所討論的話題,以及特殊詞綴進行考察。第二個例子將對社區網站進行考察。
==================================================================================================
監督學習和無監督學習
之前在《機器學習實戰》裏面就看過一部分了,這裏再複習一下。
利用樣本輸入和期望輸出來學習如何預測的技術被稱爲監督學習法。包括神經網絡、決策樹、SVM,以及貝葉斯過濾。
聚類是無監督學習的一個例子。無監督學習算法不是利用帶有正確答案樣本數據進行“訓練”。它們的目的是要在一組數據中找尋某種結構,而這些數據本身不是我們要找的答案。
==================================================================================================
對博客用戶進行分類
本章我們將對兩個示例數據集進行處理。第一個數據集中,被用來聚類的是排名前120位的博客。
爲了對這些博客聚類,我們需要的是一組指定的詞彙在每個博客訂閱源中出現的次數。有關這一數據的一個小範圍的子集。
根據單詞出現的頻率對博客進行聚類,對於搜索,分類和挖掘都是非常有價值的。
爲了構造這樣的數據集,需要下載一系列博客訂閱源,然後處理。也可以從網上下載,不過書上給的網站似乎不見了,我們從網
上下載的代碼包裏面有 feedlist 文件。
==================================================================================================
對訂閱源中的單詞進行計數
這章跳過。因爲下載的代碼本身就包含了數據了。
==================================================================================================
分級聚類
上圖中元素的相似程度通過位置體現。越近越相似。
不得不說一下,我感覺我要少打字,而是以跑代碼爲主。
建立一個名爲 clusters.py 的文件。
# -*-coding:utf-8 -*-
def readfile(filename):
lines = [line for line in file(filename)]
# 第一行是標題
colnames = lines[0].strip().split('\t')[1:]
# Python strip() 方法用於移除字符串頭尾指定的字符(默認爲空格)
rownames = []
data = []
for line in lines[1:]:
p = line.strip().split('\t')
# 每行第一列是行名
rownames.append(p[0])
# 剩餘部分就是該行對應的數據
data.append([float(x) for x in p[1:]])
return rownames, colnames, data
這個主要是讀入數據。然後定義緊密度,要用到 皮爾遜相關度 。皮爾遜相關度的計算代碼接受兩個數字列表作爲參數,返回這兩個列表的相關度分值。
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))])
#計算 r (pearson score)
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 # 返回 1.0 減去皮爾遜相關度之和的結果
皮爾遜相關度的計算結果在兩者完全匹配的情況下爲1.0,在兩者毫無關係的情況下爲0.0。
新建一個bicluster 類,將所有的這些屬性存放其中,並以此來描述這顆層級樹。
# 新建一個類,代表“聚類”這一類型
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 算法加入:
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) # .vec 在 class 裏面定義了
# 遍歷每一個配對,尋找最小距離
for i in range(len(clust)):
for j in range(i+1, len(clust)):
# 用 distance 來緩存距離的計算值
if (clust[i].id, clust[j].id) not in distances: # .vec 在 class 裏面定義了
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用於list列表操作,刪除一個或者連續幾個元素
del clust[lowestpair[0]]
clust.append(newcluster)
return clust[0]
運行算法,我們要寫一個 run1.py
import clusters
blognames, words, data = clusters.readfile('blogdata.txt')
clust = clusters.hcluster(data)
現在添加一個 printclust 函數到 clusters.py 中:
def printclust(clust, labels = None, n = 0):
# 利用縮進來建立層級佈局
for i in range(n): print ' ',
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)
print clusters.printclust(clust,labels = blognames)
就可以輸出了。
結果太長我只放一部分。
=====================================================================
思考了一下,感覺理解最重要,於是先跑書本吧。講真打程序和糾錯太花時間了。程序放的是標準示例。
=====================================================================
負數標記代表那是一個分支,就是前面的短破折號。後面的具體文字就是葉節點。
接下來繪製樹狀圖,我們需要下載 PIL,可以從網上下載。但是由於當初安裝的時候是從Anaconda直接安裝,所以這個庫已經有了。
文件第一句話加入:
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
函數 drawdendrogram 爲每一個最終生成的聚類創建一個高度爲20像素、寬度固定的圖片。
該函數爲圖片建立相應的draw對象,然後在根節點調用 drawnode 函數。
def drawdendrogram(clust,labels,jpeg='clusters.jpg'):
# 高度和寬度
h=getheight(clust)*20
w=1200
depth=getdepth(clust) # 獲取深度
# 由於寬度固定,我們需要對距離值做相應的調整
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) # drawnode 函數
img.save(jpeg,'JPEG')
drawnode 接受一個聚類及其位置作爲輸入參數。函數取到子節點的高度,計算出這些節點所在位置,然後用線條將其連接起來。、線條越長越表明合併在一起的兩個聚類差別很大,線條越短則越表明兩個聚類相似度高。
當然我們要把 drawnode 加入到文件中。
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
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))
然後在運行腳本里面添加:
clusters.drawdendrogram(clust, blognames, jpeg = 'blogclust.jpg')
問題在於這裏不管有沒有print 似乎都沒有出現那個圖片。。不知道怎麼回事不過問題不大。
===============================================================================
列聚類
同時在行列聚類很有意義。
在博客數據中,列聚類代表的是單詞。
def rotatematrix(data):
newdata=[]
for i in range(len(data[0])):
newrow=[data[j][i] for j in range(len(data))]
newdata.append(newrow)
return newdata
從代碼中可以看出,首先把整個數據集轉置,使列變爲行。每一行對應一組數字(以前是博客),數字指明瞭某個單詞在每篇博客中的出現次數。
單詞數量比博客要多,所以花費時間更長。在運行文件裏面添加:
print '===========================================\n'
rdata = clusters.rotatematrix(data)
wordclust = clusters.hcluster(rdata)
clusters.drawdendrogram(wordclust, labels = words, jpeg = 'wordclust.jpg')
當然,還是沒有圖片。。。鬱悶,使用print wordclust命令更是內存出錯?看了看聽說是像素點不夠,沒法輸出。
http://blog.csdn.net/sunflower606/article/details/45870379 這個參考博客不錯。
====================================================================
K-均值聚類
除了分級聚類以外,還有一種可供選擇的聚類方法是K-均值聚類。和分級聚類不同,事先我們會告訴算法希望生成的聚類數量,然後算法會根據結構狀況來確定聚類大小。
其實原理沒多大意思,之前在《機器學習實戰》裏面已經初步瞭解了。
import random
def 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]))]
# 隨機創建 k 箇中心點
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
針對博客數據集實驗一下該數據,由於不需要前面的輸出,所以之前的東西都註釋掉:
# -*- coding:utf-8 -*-
import clusters
blognames, words, data = clusters.readfile('blogdata.txt')
clust = clusters.hcluster(data)
'''
print clusters.printclust(clust,labels = blognames)
print clusters.drawdendrogram(clust, blognames, jpeg = 'blogclust.jpg') # 不知道爲啥沒有圖片
print '===========================================\n'
rdata = clusters.rotatematrix(data)
wordclust = clusters.hcluster(rdata)
print wordclust
clusters.drawdendrogram(wordclust, labels = words, jpeg = 'wordclust.jpg')
'''
print '===========================================\n'
kclust = clusters.kcluster(data, k = 10) # 可以嘗試不同的 k 聚類
print [blognames[r] for r in kclust[0]]
print '===========================================\n'
print [blognames[r] for r in kclust[1]]
我們可以看見結果是:
可以嘗試不同的 k 進行聚類。
====================================================================
這裏新的一節要收集zebo的數據進行分析,但是我們已經有了,就在文件 zebo.txt 裏面,所以直接跳過下載數據的階段。
定義距離度量標準
皮爾遜相關度適合於博客數據集,包含的是單詞的實際統計值。但是這裏的數據集只有0和1兩種取值,代表無、有。
我們在此採用一種被稱爲 Tanimoto 係數的度量方法,代表的是交集與並集的比率。利用向量我們定義這樣的度量:
def tanimoto(v1,v2):
c1,c2,shr=0,0,0
for i in range(len(v1)):
if v1[i]!=0: c1+=1 # 在 v1 出現
if v2[i]!=0: c2+=1 # 在 v2 出現
if v1[i]!=0 and v2[i]!=0: shr+=1 # 兩個向量都出現
# 返回一個介於 1.0 到 0.0 之間的值
# 1.0 代表不存在同時喜歡兩件物品的人
# 0.0 代表所有人都同時喜歡兩個向量中的物品
return 1.0-(float(shr)/(c1+c2-shr))
注意 是tanimoto 不是 tanamoto,實例程序寫錯了,書上是對的。
==============================================================對結果聚類
利用上面的函數並相應傳入兩個向量,我們可以實現聚類,在run1添加:
wants, people, data = clusters.readfile('zebo.txt')
clust = clusters.hcluster(data, distance = clusters.tanimoto)
clusters.drawdendrogram(clust, wants)
然後,,,並沒有什麼圖片,當然也不能指望打印出來,因爲太多了。
書上說改變搜索條件也許有更有意思的發現。這裏略過
=================================================================
以二維形式展現數據
本屆要介紹一種 多維縮放 技術,利用這個技術我們可以爲數據集找到一種二維表達形式。
算法根據每對數據項之間的差距情況,嘗試繪製一幅圖。具體見書。P49
數據矩陣的距離就是實際距離。
簡單說就是將目標距離和當前距離進行比較,並求出一個誤差值,根據誤差的情況再按照比例將每個數據項的所在位置移近或移遠少許。
程序如下:
def scaledown(data,distance=pearson,rate=0.01):# 皮爾遜相關係數
n=len(data)
# 每一對數據項之間的真實距離
realdist=[[distance(data[i],data[j]) for j in range(n)]
for i in range(0,n)]
# 隨機初始化節點在二維空間中的起始位置
loc=[[random.random(),random.random()] for i in range(n)] # 全隨機只有i?
fakedist=[[0.0 for j in range(n)] for i in range(n)] # 初始全0
lasterror=None
for m in range(0,1000):
# 尋找投影后的距離
for i in range(n):
for j in range(n):
fakedist[i][j]=sqrt(sum([pow(loc[i][x]-loc[j][x],2) # 距離計算
for x in range(len(loc[i]))]))
# 移動節點
grad=[[0.0,0.0] for i in range(n)]
totalerror=0
for k in range(n):
for j in range(n):
if j==k: continue
# 誤差值等於目標距離與當前距離之間差值的百分比
errorterm=(fakedist[j][k]-realdist[j][k])/realdist[j][k]
# 每個節點都需要根據誤差的多少
# 按比例移離或移向其他節點
grad[k][0]+=((loc[k][0]-loc[j][0])/fakedist[j][k])*errorterm
grad[k][1]+=((loc[k][1]-loc[j][1])/fakedist[j][k])*errorterm
# 記錄總的誤差誤差值
totalerror+=abs(errorterm)
print totalerror
# 如果節點移動之後的情況變得更糟,則程序結束
if lasterror and lasterror<totalerror: break # and 前面不太懂
lasterror=totalerror
# 根據 rate 參數與 grad 值相乘的結果,移動每一個節點
for k in range(n):
loc[k][0]-=rate*grad[k][0] #這裏 loc 應該是一個k行2列的矩陣
loc[k][1]-=rate*grad[k][1]
return loc
最後利用 PIL 生成一幅圖,根據新的座標值,在圖上標出所有數據項的位置及其對應的標籤。
def draw2d(data,labels,jpeg='mds2d.jpg'):
img = Image.new('RGB',(2000,2000),(255,255,255))
draw = ImageDraw.Draw(img)
for i in range(len(data)):
x=(data[i][0]+0.5)*1000
y=(data[i][1]+0.5)*1000
draw.text((x,y),labels[i],(0,0,0))
img.save(jpeg,'JPEG')
運行倒是沒問題,就是不知道爲啥沒有圖片,跑哪去了?
尷尬。