本文寫於2018.12.11,原文地址,技術博客地址
說明:造輪子是為了深入理解演算法原理和參數,實際工作中不用造輪子
今天我們學習並實踐KMeans聚類演算法,分成以下幾個部分,跟上節奏燥起來!
聚類是一種無監督學習的方法,所謂「無監督」,就是指參與訓練的樣本沒有標籤。
KMeans聚類演算法過程如下: 1. 對於一組數據集,隨機選取k個點作為質心,將數據集中的點歸為離其最近的質心一簇,此時數據集被劃分為k個簇; 2. 對這k個簇,重新計算各簇的質心(均值); 3. 根據新的質心,按照step1繼續聚類,然後再根據聚類重新計算各簇質心,直到質心不再改變,分類完成。
說白了,就是不斷地聚類、劃分的過程。
通過KMeans原理,可以看到幾個顯而易見的缺點: 1. 簇數量k由用戶指定,無法預先知道最佳k值 >>解法:分為幾簇,最終由輪廓係數S(i)決定,取輪廓係數最大的分類數(05節劇透) 2. 最終質心可能與初始點選擇有關 >> 因此KMeans的結果可能收斂到局部最小值,而不是全局最小值 >> 解法: - BiKMeans(03節) - KMeans++(KMeans++ 演算法在選擇初始質心時並不是隨機選擇,而是選擇盡量相互分離的質心,即,下一個質心點總是離上一個質心點較遠的點)
代碼實現
def loadDataSet(fileName): dataList=[] dataMat=[] fr=open(fileName) for line in fr.readlines(): curLine=line.strip().split( ) fltLine=list(map(float,curLine)) dataList.append(fltLine) dataMat=mat(dataList) return dataMat
def distEclud(vecA,vecB): return sqrt(sum(power(vecA-vecB,2))) #歐式距離
#為輸入數據集構造k個隨機中心,中心位置在各特徵最大最小值之間 def randCent(dataSet,k): n=shape(dataSet)[1] center=mat(zeros((k,n))) for j in range(n): #對每個特徵 minJ=min(dataSet[:,j]) rangeJ=float(max(dataSet[:,j])-minJ) center[:,j]=mat(minJ+rangeJ*random.rand(k,1)) #質心第j維坐標在數據集第j維數據之間 return center
def KMeans(dataSet,k,distMeas=distEclud,createCent=randCent): m=shape(dataSet)[0] clusterAssment=mat(zeros((m,2))) #用於記錄各樣本當前歸屬於哪個簇以及到該簇質心的歐式距離平方 center=createCent(dataSet,k) clusterChanged=True
while clusterChanged: clusterChanged=False #對每個樣本,計算樣本到各質心的距離,尋找距離最近的質心,將該樣本歸為該質心所在簇 for i in range(m): minDist=inf;minIndex=-1 for j in range(k): #對每個質心,計算到i樣本的距離 distJI=distMeas(center[j,:],dataSet[i,:]) if distJI<minDist: minDist=distJI;minIndex=j #i樣本暫屬於j簇,到j簇質心距離為minDist if clusterAssment[i,0]!=minIndex: clusterChanged=True #若任一樣本在本次迭代中改變了簇類,則要進行下一次迭代(即,直到任何樣本都不再改變簇類,聚類停止) clusterAssment[i,:]=minIndex,minDist**2 #記錄樣本i的簇類情況 #print (center) #更新質心 for cent in range(k): ptsInClust=dataSet[nonzero(clusterAssment[:,0].A==cent)[0]] #篩選出屬於當前簇類的點 center[cent,:]=mean(ptsInClust,axis=0) #對該簇類各樣本的各列求均值,作為新質心 return center,clusterAssment
用一組數據來測試一下:
dataMat1=loadDataSet(rD:DMpythondataMLiA_SourceCodemachinelearninginactionCh10 estSet.txt) dataMat2=loadDataSet(rD:DMpythondataMLiA_SourceCodemachinelearninginactionCh10 estSet2.txt) center_testSet1,clusterAssment_testSet1=KMeans(dataMat1,4) center_testSet2,clusterAssment_testSet2=KMeans(dataMat2,3)
plt.figure(figsize=(6,6)) plt.scatter(dataMat1[:,0].T.tolist()[0],dataMat1[:,1].T.tolist()[0],c=pink,s=30) plt.scatter(center_testSet1.T[0].tolist()[0],center_testSet1.T[1].tolist()[0],c=blue,s=50)
結果如下,藍色點為聚類質心
剛才我們實現了KMeans演算法,不過也提到,KMeans有個缺點,就是其聚類的最終質心可能與初始點選擇有關,因此KMeans的結果可能收斂到局部最小值,而不是全局最小值。
怎麼解決呢?提示:想想決策樹是如何分支的?
為了得到全局最優解,BiKMeans演算法出現了,它的過程如下(是不是跟決策樹分支有點神似?) 1. 將整個數據集看作一個簇,計算初始質心,即所有數據點各特徵的均值 2. 遍歷各質心,對各質心,將質心所在簇用原始KMeans演算法二分,計算二分後整個數據集的SSE(即平方誤差和,即簇各點到簇質心距離平方和),找到二分後整體數據集SSE最小的質心,認為此質心是本次劃分的最佳質心,對其進行二分 3. 不斷重複step2,直到質心總數=設置的k
說白了,BiKMeans演算法過程類似於決策樹的分支,通過啟發的方法,每次迭代只分裂當前最佳質心,直到簇數量達到k,這樣的方法可以保證最終劃分得到的質心是全局最優解,而原始KMeans可能會陷入局部最優解。
def biKMeans(dataSet,k,distMeas=distEclud): m=shape(dataSet)[0] clusterAssment=mat(zeros((m,2))) #記錄各樣本歸屬和距離平方 center0=mean(dataSet,axis=0).tolist()[0] #初始質心 centerList=[center0] #用於記錄聚類質心坐標 for j in range(m): clusterAssment[j,1]=distMeas(mat(center0),dataSet[j,:])**2 while len(centerList)<k: lowestSSE=inf #SSE=Sum of Square Error #對每個簇,嘗試二分,計算二分後整體數據的SSE,若小於lowestSSE,則將簇二分,如此往複,直到分到k個簇為止 for i in range(len(centerList)): ptsInCluster=dataSet[nonzero(clusterAssment[:,0].A==i)[0],:] #默認質心索引就是數據對應簇類 centerMat,splitClustAss=KMeans(ptsInCluster,2,distMeas) sseSplit=sum(splitClustAss[:,1]) #被二分後的簇的平方誤差和 sseNotSplit=sum(clusterAssment[nonzero(clusterAssment[:,0]!=i)[0],1]) #整體數據中,未被二分的簇的平方誤差和 if (sseSplit+sseNotSplit)<lowestSSE: bestCentToSplit=i bestNewCents=centerMat bestClustAss=splitClustAss.copy() #.copy()防止splitClustAss被覆蓋時影響到bestClustAss lowestSSE=sseSplit+sseNotSplit #確定好本次迭代被二分的簇後,將被二分的數據對應的簇類更新 bestClustAss[nonzero(bestClustAss[:,0].A!=0)[0],0]=len(centerList) #更新先後順序很重要! bestClustAss[nonzero(bestClustAss[:,0].A==0)[0],0]=bestCentToSplit print(The bestCentToSplit is:,bestCentToSplit) print(The number of samples to split is,len(bestClustAss)) centerList[bestCentToSplit]=bestNewCents[0,:].tolist()[0] #將被二分的原簇質心替換為二分後的質心之一 centerList.append(bestNewCents[1,:].tolist()[0]) #將二分後的另一質心添加在質心列表末尾 #將二分後的簇的數據歸屬更新到總記錄中 clusterAssment[nonzero(clusterAssment[:,0].A==bestCentToSplit)[0],:]=bestClustAss return mat(centerList),clusterAssment
測試
center_testSet22,clusterAssment_testSet22=biKMeans(dataMat2,3)
plt.figure(figsize=(6,6)) plt.scatter(dataMat2[:,0].T.tolist()[0],dataMat2[:,1].T.tolist()[0],c=pink,s=30) plt.scatter(center_testSet22.T[0].tolist()[0],center_testSet22.T[1].tolist()[0],c=blue,s=50)
我們在前節實現了KMeans演算法和BiKMeans演算法,現在讓我們來用用這兩個輪子吧。
設想,你邀請了70個朋友參加你的party,他們住在城市的各個地方,你需要在party開始前2小時開車去接他們(假設你的車子可以容納70人,哈哈哈)。 為了時間效率最大化,請問你該如何規劃每個人的上車點呢?總不能你一個一個去家門口接吧。
設想,你邀請了70個朋友參加你的party,他們住在城市的各個地方,你需要在party開始前2小時開車去接他們(假設你的車子可以容納70人,哈哈哈)。
我們用聚類的方法來規劃!
現在我們獲取了每個朋友住址的經緯度,那麼請開始你的表演。
很簡單,只需要調用我們早好的輪子BiKMeans
#球面距離計算函數:球面餘弦距離(向量夾角*地球半徑) #求球面上兩向量vecA,vecB的距離 def distSLC(vecA,vecB): a=sin(vecA[0,1]*pi/180)*sin(vecB[0,1]*pi/180) #由於抓取的經緯度為角度,需要通過 *pi/180來轉換為弧度 b=cos(vecA[0,1]*pi/180)*cos(vecB[0,1]*pi/180)*cos(pi*(vecA[0,0]-vecB[0,0])/180) return 6371.0*arccos(a+b) #6371為地球半徑,單位為英尺
def clusterClubs(k=5): dataList=[] for line in open(rD:DMpythondataMLiA_SourceCodemachinelearninginactionCh10places.txt).readlines(): lineArr=line.strip().split( ) dataList.append([float(lineArr[4]),float(lineArr[3])]) #讀取地址的經緯度 dataMat=mat(dataList) center,clusterAss=biKMeans(dataMat,k,distMeas=distSLC) #將地址按經緯度聚類 #作圖 fig=plt.figure(figsize=(10,8)) rect=[0.1,0.1,0.8,0.8] #用於設置坐標軸刻度,[]中前兩個值表示左邊起始位置,後兩個值對應坐標長度 scatterMarkers=[^,o,h,8,p,d,v,s,>,<] #用於設置散點圖點的形狀 axprops=dict(xticks=[],yticks=[]) ax0=fig.add_axes(rect,label=ax0,**axprops)
#讀圖,並將圖片顯示在設定好的坐標軸中 imgP=plt.imread(rD:DMpythondataMLiA_SourceCodemachinelearninginactionCh10Portland.png) ax0.imshow(imgP) #將地址按聚類結果作散點圖 ax1=fig.add_axes(rect,label=ax1,frameon=False) for i in range(k): ptsInCluster=dataMat[nonzero(clusterAss[:,0].A==i)[0],:] markerStyle=scatterMarkers[i] ax1.scatter(ptsInCluster[:,0].flatten().A[0],ptsInCluster[:,1].flatten().A[0], marker=markerStyle,s=90) #標出質心 ax1.scatter(center[:,0].flatten().A[0],center[:,1].flatten().A[0],marker=+,s=300) plt.show()
好了,我們寫的這個「上車點規劃器」就可以使用了,只需要輸入聚類簇數量即可 ,下面看看幾個測試結果:
但是到底聚為幾類比較合理呢?這個合理是指即不讓你的朋友跑太遠去坐車,也不用你跑太多地方去接人。
不論是KMeans演算法還是BiKMeans演算法,都仍有一個缺點沒有解決:簇數量k由用戶指定,用戶指定的k不一定是最佳的簇數量。這也是上一節我們沒有解決的問題。
怎麼辦呢?
使用輪廓係數,最佳k值由輪廓係數S(i)決定,取輪廓係數最大的分類數。
什麼是輪廓係數,輪廓係數S(i)用于衡量聚類效果,取值在-1~1之間,越接近1表示聚類效果越好,公式如下,
其中a(i)=i點到同簇各點距離均值;b(i)=min(i點到某個非同簇各點均值),即i點到其他簇質心距離的最小值,整體數據集的輪廓係數是各點輪廓係數的平均值
基於此,我們可以寫一個計算聚類結果輪廓係數的函數,然後看看輪廓係數與聚類數量k的關係,從而找到最佳的聚類數量k。
輪廓係數計算函數
def outlineOfCluster(filename,maxClusterNum,distMeas=distEclud): dataList=[] for line in open(filename).readlines(): lineArr=line.strip().split( ) dataList.append([float(lineArr[4]),float(lineArr[3])]) #讀取地址的經緯度 dataMat=mat(dataList) #遍歷2~nn個質心,求不同數量的簇的輪廓係數,設置輪廓係數最大的簇數量為k值 sk={} for k in range(2,maxClusterNum+1): center,clusterAss=biKMeans(dataMat,k,distMeas) outline=[] #代表聚類為k個簇時各點的輪廓係數列表 for i in range(k): #遍歷各簇i ptsInCluster=clusterAss[nonzero(clusterAss[:,0].A==i)[0],:] for j in range(len(ptsInCluster)): #遍歷i簇的各點j ptsInClusterNotJ=vstack([ptsInCluster[:j,:],ptsInCluster[j+1:,:]]) ajn=[] for n in range(len(ptsInClusterNotJ)): #遍歷i簇非j的點 ajn.append(distSLC(ptsInCluster[n],ptsInCluster[j])) aj=nanmean(ajn) bjm=[] centerWithoutI=vstack([center[:i,:],center[i+1:,:]]) for m in range(len(centerWithoutI)): #遍歷非i簇質心 bjm.append(distSLC(centerWithoutI[m],ptsInCluster[j])) bj=min(bjm) sj=(bj-aj)/max(bj,aj) #i簇中j點的輪廓係數 outline.append(sj) #將i簇中各點的輪廓係數保存在outline中,outline用於存儲聚類為k個簇時各點的輪廓係數,在遍歷k時需要重置 sk[k]=nanmean(outline) #聚類為k個簇時的輪廓係數是各點輪廓係數的均值 return sk
現在我們可以接近上車點規劃問題的遺留問題了,規劃幾個上車點最合理?
sk=outlineOfCluster(rD:DMpythondataMLiA_SourceCodemachinelearninginactionCh10places.txt,35,distMeas=distSLC)
plt.figure(figsize=(10,5)) plt.plot(list(sk.keys()),list(sk.values()),linewidth=3) plt.title(聚類簇數-效果,fontsize=18,fontweight=bold) plt.xlabel(最終聚類簇數量,fontsize=14) plt.ylabel(輪廓係數,fontsize=14)
可以看到,聚類簇數在達到10個簇之後,輪廓係數的增量就變得非常小了,因此從性價比方面考慮,選擇聚類為10個簇的性價比是最高的,即規劃10個上車點就好。
本文實踐了KMeans聚類演算法,分成以下幾個部分,
下期我們將實踐Apriori演算法,這是一種挖掘關聯規則得到頻繁項集的演算法,比如可以預測你買了A商品後會買B商品,敬請期待~
《機器學習實戰》 Peter Harrington Chapter10
如果覺得本文對你有幫助,點贊讓更多的同學看到吧