聚類是一種無監督的學習,它將相似的對象歸到同一個簇中。它有點像全自動分類。聚類方法幾乎可以應用於所有對象,簇內的對象越相似,聚類的效果越好
簇識別給出聚類結果的含義。假定有一些數據,現在將相似數據歸到一起,簇識別會告訴我們這些簇到底都是些什么。聚類與分類的最大不同在於,分類的目標事先巳知,而聚類則不一樣。因為其產生的結果與分類相同,而只是類別沒有預先定義,聚類有時也被稱為無監督分類(unsupervised classification )。
聚類分析試圖將相似對象歸人同一簇,將不相似對象歸到不同簇。相似這一概念取決於所選擇的相似度計算方法
10.1K-均值聚類算法
K-均值聚類
優點:容易實現。
缺點:可能收斂到局部最小值,在大規模數據集上收斂較慢。
適用數據類型:數值型數據。
K-均值是發現給定數據集的k個簇的算法。簇個數k是用戶給定的,每一個簇通過其質心( centroid) , 即簇中所有點的中心來描述。
K-均值算法的工作流程是這樣的。首先,隨機確定k個初始點作為質心。然后將數據集中的每個點分配到一個簇中,具體來講,為每個點找距其最近的質心,並將其分配給該質心所對應的簇。這一步完成之后,每個簇的質心更新為該簇所有點的平均值。
上述過程的偽代碼表示如下:
創建k個點作為起始質心(經常是隨機選擇)
當任意一個點的簇分配結果發生改變時
對數據集中的每個數據點
對每個質心
計算質心與數據點之間的距離
將數據點分配到距其最近的簇
對每一個簇,計算簇中所有點的均值並將均值作為質心
K-均值聚類的一般流程
(1)收集數據:使用任意方法。
⑵准備數據:需要數值型數據來計算距離,也可以將標稱型數據映射為二值型數據再用於距離計算。
(3)分析數據:使用任意方法。
(4)訓練算法:不適用於無監督學習,即無監督學習沒有訓練過程。
(5)測試算法:應用聚類算法、觀察結果。可以使用量化的誤差指標如誤差平方和(后面會介紹)來評價算法的結果。
(6)使用算法:可以用於所希望的任何應用。通常情況下,簇質心可以代表整個簇的數據來做出決策。
K-均值聚類支持函數(即完成K均值聚類的一些輔助函數),代碼如下:
from numpy import * #general function to parse tab -delimited floats #assume last column is target value def loadDataSet(fileName): dataMat = [] fr = open(fileName) for line in fr.readlines(): curLine = line.strip().split('\t') #筆者使用的是python3,需要將map映射后的結果轉化為list #map all elements to float() fltLine = list(map(float,curLine)) dataMat.append(fltLine) return dataMat #樣本距離計算函數 def distEclud(vecA, vecB): return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB) #創建簇中心矩陣,初始化為k個在數據集的邊界內隨機分布的簇中心 def randCent(dataSet, k): n = shape(dataSet)[1] #create centroid mat centroids = mat(zeros((k,n))) #create random cluster centers, within bounds of each dimension for j in range(n): #求出數據集中第j列的最小值(即第j個特征) minJ = min(dataSet[:,j]) #用第j個特征最大值減去最小值得出特征值范圍 rangeJ = float(max(dataSet[:,j]) - minJ) #創建簇矩陣的第J列,random.rand(k,1)表示產生(10,1)維的矩陣,其中每行值都為0-1中的隨機值 #可以這樣理解,每個centroid矩陣每列的值都在數據集對應特征的范圍內,那么k個簇中心自然也都在數據集范圍內 centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1)) return centroids
測試截圖如下:
K -均值聚類算法,代碼如下:
#distMeas為距離計算函數 #createCent為初始化隨機簇心函數 def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): m = shape(dataSet)[0] #create mat to assign data points to a centroid, also holds SE of each point #創建一個(m,2)維矩陣,第一列存儲每個樣本對應的簇心,第二列存儲樣本到簇心的距離 clusterAssment = mat(zeros((m,2))) #用createCent()函數初始化簇心矩陣 centroids = createCent(dataSet, k) #保存迭代中clusterAssment是否更新的狀態,如果未更新,那么退出迭代,表示收斂 #如果更新,那么繼續迭代,直到收斂 clusterChanged = True while clusterChanged: clusterChanged = False #for each data point assign it to the closest centroid #對每個樣本找出離樣本最近的簇心 for i in range(m): #minDist保存最小距離 #minIndex保存最小距離對應的簇心 minDist = inf; minIndex = -1 #遍歷簇心,找出離i樣本最近的簇心 for j in range(k): distJI = distMeas(centroids[j,:],dataSet[i,:]) if distJI < minDist: minDist = distJI; minIndex = j #如果clusterAssment更新,表示對應樣本的簇心發生變化,那么繼續迭代 if clusterAssment[i,0] != minIndex: clusterChanged = True #更新clusterAssment,樣本到簇心的距離 clusterAssment[i,:] = minIndex,minDist**2 print(centroids) #遍歷簇心,更新簇心為對應簇中所有樣本的均值 for cent in range(k):#recalculate centroids #利用數組過濾找出簇心對應的簇(數組過濾真是好東西!) ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]#get all the point in this cluster #對簇求均值,賦給對應的centroids簇心 centroids[cent,:] = mean(ptsInClust, axis=0) #assign centroid to mean return centroids, clusterAssment
代碼測試截圖如下:
繪制測試截圖:
paint函數為筆者寫的繪圖函數:
def paint(xArr,yArr,xArr1,yArr1): fig = plt.figure() ax = fig.add_subplot(111) ax.scatter(xArr,yArr,c='blue') ax.scatter(xArr1,yArr1,c='red') plt.show()
效果如下(其中紅色的點為簇心):
可以看到,經過3次迭代之后K-均值算法收斂
10.2 使用后處理來提高聚類性能
考慮圖10-2中的聚類結果,這是在一個包含三個簇的數據集上運行K-均值算法之后的結果,但是點的簇分配結果值沒有那么准確。K-均值算法收斂但聚類效果較差的原因是,K-均值算法收斂到了局部最小值,而非全局最小值(局部最小值指結果還可以但並非最好結果,全局最小值是可能的最好結果)。
一種用於度量聚類效果的指標是SSE(Sum of Squared Error,誤差平方和),對應clusterAssment矩陣的第一列之和。SSE值越小表示數據點越接近於它們的質心,聚類效果也越好。因為對誤差取了平方,因此更加重視那些遠離中心的點。一種肯定可以降低SSE值的方法是增加簇的個數,但這違背了聚類的目標。聚類的目標是在保持簇數目不變的情況下提高簇的質量。
那么如何對結果進行改進?你可以對生成的簇進行后處理,一種方法是將具有最大SSE值的簇划分成兩個簇。具體實現時可以將最大簇包含的點過濾出來並在這些點上運行K-均值聚類算法,其中的K為2。
為了保持簇總數不變,可以將某兩個簇進行合並。從圖10-2中很明顯就可以看出,應該將圖下部兩個出錯的簇質心進行合並。可以很容易對二維數據上的聚類進行可視化,但是如果遇到40維的數據應該如何去做?
有兩種可以量化的辦法:合並最近的質心,或者合並兩個使得SSE增幅最小的質心。第一種思路通過計算所有質心之間的距離,然后合並距離最近的兩個點來實現。第二種方法需要合並兩個簇然后計算總SSE值。必須在所有可能的兩個簇上重復上述處理過程,直到找到合並最佳的兩個簇為止。接下來將討論利用上述簇划分技術得到更好的聚類結果的方法。
10.3 二分K-均值算法
為克服K-均值算法收斂於局部最小值的問題,有人提出了另一個稱為二分K均值(bisectingK-means)的算法, 該算法首先將所有點作為一個簇,然后將該簇一分為二。之后選擇其中一個簇繼續進行划分,選擇哪一個簇進行划分取決於對其划分是否可以最大程度降低SSE的值。上述基於SSE的划分過程不斷重復,直到得到用戶指定的簇數目為止。
二分K-均值算法的偽代碼形式如下:
將所有點看成一個簇
當簇數目小於k時
對於每一個簇
計算總誤差
在給定的簇上面進行K-均值聚類(k=2)
計算將該簇一分為二之后的總誤差
選擇使得誤差最小的那個簇進行划分操作
另一種做法是選擇SSE最大的簇進行划分,直到簇數目達到用戶指定的數目為止。這個做法聽起來並不難實現。下面就來看一下該算法的實際效果。
二分K均值聚類算法,代碼如下:
#distMeas為距離計算函數 def biKmeans(dataSet, k, distMeas=distEclud): m = shape(dataSet)[0] #(m,2)維矩陣,第一列保存樣本所屬簇,第二列保存樣本到簇中心的距離 clusterAssment = mat(zeros((m,2))) #取數據集特征均值作為初始簇中心 centroid0 = mean(dataSet, axis=0).tolist()[0] #centList保存簇中心數組,初始化為一個簇中心 #create a list with one centroid centList =[centroid0] #calc initial Error for j in range(m): clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2 #迭代,直到簇中心集合長度達到k while (len(centList) < k): #初始化最小誤差 lowestSSE = inf #迭代簇中心集合,找出找出分簇后總誤差最小的那個簇進行分解 for i in range(len(centList)): #get the data points currently in cluster i #獲取屬於i簇的數據集樣本 ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] #對該簇進行k均值聚類 centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) #獲取該簇分類后的誤差和 sseSplit = sum(splitClustAss[:,1])#compare the SSE to the currrent minimum #獲取不屬於該簇的樣本集合的誤差和,注意矩陣過濾中用的是!=i sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1]) #打印該簇分類后的誤差和和不屬於該簇的樣本集合的誤差和 print("sseSplit, and notSplit: ",sseSplit,sseNotSplit) #兩誤差和相加即為分簇后整個樣本集合的誤差和,找出簇中心集合中能讓分簇后誤差和最小的簇中心,保存最佳簇中心(bestCentToSplit),最佳分簇中心集合(bestNewCents),以及分簇數據集中樣本對應簇中心及距離集合(bestClustAss),最小誤差(lowestSSE) if (sseSplit + sseNotSplit) < lowestSSE: bestCentToSplit = i bestNewCents = centroidMat bestClustAss = splitClustAss.copy() lowestSSE = sseSplit + sseNotSplit #更新用K-means獲取的簇中心集合,將簇中心換為len(centList)和bestCentToSplit,以便之后調整clusterAssment(總樣本集對應簇中心與和簇中心距離的矩陣)時一一對應 bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #change 1 to 3,4, or whatever bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit print('the bestCentToSplit is: ',bestCentToSplit) print('the len of bestClustAss is: ', len(bestClustAss)) #更新簇中心集合,注意與bestClustAss矩陣是一一對應的 centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#replace a centroid with two best centroids centList.append(bestNewCents[1,:].tolist()[0]) #reassign new clusters, and SSE clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss return mat(centList), clusterAssment
二分K值最重要的是記住要將最佳分簇集合與clusterAssment一一對應
測試代碼如下:
datMat3 = mat(loadDataSet('testSet2.txt')) centList,myNewAssments = biKmeans(datMat3,3) print(centList) xArr = datMat3[:,0].flatten().A[0] yArr = datMat3[:,1].flatten().A[0] xArr1 = centList[:,0].flatten().A[0] yArr1 = centList[:,1].flatten().A[0] #paint為筆者自己寫的繪圖函數 paint(xArr,yArr,xArr1,yArr1) def paint(xArr,yArr,xArr1,yArr1): fig = plt.figure() ax = fig.add_subplot(111) ax.scatter(xArr,yArr,c='blue') ax.scatter(xArr1,yArr1,c='red') plt.show()
測試截圖如下:
上述函數可以運行多次,聚類會收斂到全局最小值,而原始的別的!!3 ()函數偶爾會陷人局部最小值。
10.4 示例:對地圖上的點進行聚類
假如有這樣一種情況:你的朋友Drew希望你帶他去城里慶祝他的生日。由於其他一些朋友也會過來,所以需要你提供一個大家都可行的計划。Drew給了你一些他希望去的地址。這個地址列表很長,有70個位置。我把這個列表保存在文件portland-Clubs.txt中,該文件和源代碼一起打包。這些地址其實都在俄勒岡州的波特蘭地區。
也就是說,一晚上要去70個地方!你要決定一個將這些地方進行聚類的最佳策略,這樣就可以安排交通工具抵達這些簇的質心,然后步行到每個簇內地址。Drew的清單中雖然給出了地址,但是並沒有給出這些地址之間的距離遠近信息。因此,你要得到每個地址的緯度和經度,然后對這些地址進行聚類以安排你的行程。
示例:對於地理數據應用二分K-均值算法
(1)收集數據:使用Yahoo!PlaceFinder API收集數據
(2)准備數據:只保留經緯度信息
(3)分析數據:使用Matplotlib來構建一個二維數據圖,其中包含簇與地圖
(4)訓練算法:訓練不適用無監督學習
(5)測試算法:使用10.4節中的biKmeans( )函教
(6)使用算法| 最后的輸出是包含簇及簇中心的地圖
10.4.1 Yahoo! PlaceFinder API
Yahoo! PlaceFinderAPI,代碼如下:
import urllib import json def geoGrab(stAddress, city): #create a dict and constants for the goecoder apiStem = 'http://where.yahooapis.com/geocode?' #請求參數字典 params = {} params['flags'] = 'J'#JSON return type params['appid'] = 'aaa0VN6k' params['location'] = '%s %s' % (stAddress, city) #url編碼請求參數,化為x1=xx&x2=xx形式 url_params = urllib.urlencode(params) #print url_params yahooApi = apiStem + url_params print(yahooApi) #請求api c=urllib.urlopen(yahooApi) #獲取json格式的數據 return json.loads(c.read()) from time import sleep def massPlaceFind(fileName): fw = open('places.txt', 'w') #對文件中的每個樣本調用geoGrab()獲取json數據,解析后寫入源文件 for line in open(fileName).readlines(): line = line.strip() lineArr = line.split('\t') retDict = geoGrab(lineArr[1], lineArr[2]) if retDict['ResultSet']['Error'] == 0: lat = float(retDict['ResultSet']['Results'][0]['latitude']) lng = float(retDict['ResultSet']['Results'][0]['longitude']) print("%s\t%f\t%f" % (lineArr[0], lat, lng)) fw.write('%s\t%f\t%f\n' % (line, lat, lng)) else: print("error fetching") sleep(1) fw.close()
測試代碼如下:
geoResults = geoGrab('1 VA Center', 'Augusta, ME') print(geoResults)
由於主要不是為了調用YahooAPI,因此筆者沒有實際調用API獲取數據,理解這個過程就可以了,首先獲取數據,然后調用二分K均值聚類對地址聚類分析。
10.4.2 對地理坐標進行聚類
這個例子中要聚類的俱樂部給出的信息為經度和維度,但這些信息對於距離計算還不夠。在北極附近每走幾米的經度變化可能達到數10度 ;而在赤道附近走相同的距離,帶來的經度變化可能只是零點幾。可以使用球面余弦定理來計算兩個經緯度之間的距離
球面距離計算及簇繪圖函數,代碼如下:
#利用球面余弦定理計算指定(經度,緯度)兩點的距離 def distSLC(vecA, vecB):#Spherical Law of Cosines a = sin(vecA[0,1]*pi/180) * sin(vecB[0,1]*pi/180) b = cos(vecA[0,1]*pi/180) * cos(vecB[0,1]*pi/180) * \ cos(pi * (vecB[0,0]-vecA[0,0]) /180) return arccos(a + b)*6371.0 #pi is imported with numpy import matplotlib import matplotlib.pyplot as plt def clusterClubs(numClust=5): datList = [] #讀取數據集,存儲在datList中 for line in open('places.txt').readlines(): lineArr = line.split('\t') datList.append([float(lineArr[4]), float(lineArr[3])]) datMat = mat(datList) #調用二分K聚類獲取簇中心集合以及clustAssing矩陣 myCentroids, clustAssing = biKmeans(datMat, numClust, distMeas=distSLC) fig = plt.figure() rect=[0.1,0.1,0.8,0.8] scatterMarkers=['s', 'o', '^', '8', 'p', \ 'd', 'v', 'h', '>', '<'] axprops = dict(xticks=[], yticks=[]) ax0=fig.add_axes(rect, label='ax0', **axprops) imgP = plt.imread('Portland.png') ax0.imshow(imgP) ax1=fig.add_axes(rect, label='ax1', frameon=False) #迭代簇集合,根據不同的marker畫出對應的簇 for i in range(numClust): ptsInCurrCluster = datMat[nonzero(clustAssing[:,0].A==i)[0],:] markerStyle = scatterMarkers[i % len(scatterMarkers)] ax1.scatter(ptsInCurrCluster[:,0].flatten().A[0], ptsInCurrCluster[:,1].flatten().A[0], marker=markerStyle, s=90) #畫出所有簇中心 ax1.scatter(myCentroids[:,0].flatten().A[0], myCentroids[:,1].flatten().A[0], marker='+', s=300) plt.show()
測試代碼如下:
kMeans.clusterClubs(5)
測試截圖如下:
10.5 本章小結
聚類是一種無監督的學習方法。所謂無監督學習是指事先並不知道要尋找的內容,即沒有目標變量。聚類將數據點歸到多個簇中,其中相似數據點處於同一簇,而不相似數據點處於不同簇中。聚類中可以使用多種不同的方法來計算相似度。
一種廣泛使用的聚類算法是K-均值算法,其中K是用戶指定的要創建的簇的數目。K-均值聚類算法以K個隨機質心開始。算法會計算每個點到質心的距離。每個點會被分配到距其最近的簇質心,然后緊接着基於新分配到簇的點更新簇質心。以上過程重復數次,直到簇質心不再改變。這個簡單的算法非常有效但是也容易受到初始簇質心的影響。為了獲得更好的聚類效果,可以使用另一種稱為二分K-均值的聚類算法。二分K-均值算法首先將所有點作為一個簇,然后使用K-均值算法(K = 2 ) 對其划分。下一次迭代時,選擇有最大誤差的簇進行划分。該過程重復直到K個簇創建成功為止。二分K-均值的聚類效果要好於K-均值算法。
K-均值算法以及變形的K-均值算法並非僅有的聚類算法, 另外稱為層次聚類的方法也被廣泛使用