機器學習實戰之K-Means算法


一,引言

  先說個K-means算法很高大上的用處,來開始新的算法學習。我們都知道每一屆的美國總統大選,那叫一個競爭激烈。可以說,誰拿到了各個州盡可能多的選票,誰選舉獲勝的幾率就會非常大。有人會說,這跟K-means算法有什么關系?當然,如果哪一屆的總統競選,某一位候選人是絕對的眾望所歸,那自然能以壓倒性優勢競選成功,那么我們的k-means算法還真用不上。但是,我們應該知道2004年的總統大選中,候選人的得票數非常接近,接近到什么程度呢?如果1%的選民將手中的選票投向任何一位候選人,都直接決定了總統的歸屬。那么這個時候,這1%的選民手中的選票就非常關鍵,因為他們的選票將直接對選舉結果產生非常大的影響,所以,如果能夠妥善加以引導和吸引,那么這很少的一部分選民還是極有可能會轉換立場的。那么如何找出這類選民,以及如何在有限的預算下采取措施來吸引他們呢?

  答案就是聚類,這就要說到本次要講到的K-means算法了。通過收集用戶的信息,可以同時收集用戶滿意和不滿意的信息;然后將這些信息輸入到聚類算法中,就會得到很多的簇;接着,對聚類結果中的每一個簇(最好是最大簇),精心構造能吸引該簇選民的信息,加以引導;最后,再開展競選活動並觀察上述做法是否有效。而,一旦算法有效,那么就會對選舉結果產生非常大的影響,甚至,直接決定了最后的總統歸屬。

    可見,聚類算法是一個非常了不起的算法。下面,我們就正式開始今天的新算法,K-means聚類算法。

二,K-means聚類算法

1 K-means算法的相關描述

  聚類是一種無監督的學習,它將相似的對象歸到同一簇中。聚類的方法幾乎可以應用所有對象,簇內的對象越相似,聚類的效果就越好。K-means算法中的k表示的是聚類為k個簇,means代表取每一個聚類中數據值的均值作為該簇的中心,或者稱為質心,即用每一個的類的質心對該簇進行描述。

  聚類和分類最大的不同在於,分類的目標是事先已知的,而聚類則不一樣,聚類事先不知道目標變量是什么,類別沒有像分類那樣被預先定義出來,所以,聚類有時也叫無監督學習。

  聚類分析試圖將相似的對象歸入同一簇,將不相似的對象歸為不同簇,那么,顯然需要一種合適的相似度計算方法,我們已知的有很多相似度的計算方法,比如歐氏距離,余弦距離,漢明距離等。事實上,我們應該根據具體的應用來選取合適的相似度計算方法。

  當然,任何一種算法都有一定的缺陷,沒有一種算法時完美的,有的只是人類不斷追求完美,不斷創新的意志。K-means算法也有它的缺陷,但是我們可以通過一些后處理來得到更好的聚類結果,這些在后面都會一一降到。

  K-means算法雖然比較容易實現,但是其可能收斂到局部最優解,且在大規模數據集上收斂速度相對較慢。

2 K-means算法的工作流程

  首先,隨機確定k個初始點的質心;然后將數據集中的每一個點分配到一個簇中,即為每一個點找到距其最近的質心,並將其分配給該質心所對應的簇;該步完成后,每一個簇的質心更新為該簇所有點的平均值。偽代碼如下:

創建k個點作為起始質心,可以隨機選擇(位於數據邊界內)
  當任意一個點的簇分配結果發生改變時
    對數據集中每一個點
        對每個質心
          計算質心與數據點之間的距離
        將數據點分配到距其最近的簇
    對每一個簇,計算簇中所有點的均值並將均值作為質心

  再看實際的代碼:

#導入numpy庫
from numpy import *
#K-均值聚類輔助函數

#文本數據解析函數
def numpy import *
    dataMat=[]
    fr=open(fileName)
    for line in fr.readlines():
        curLine=line.strip().split('\t')
        #將每一行的數據映射成float型
        fltLine=map(float,curLine)
        dataMat.append(fltLine)
    return dataMat

#數據向量計算歐式距離    
def distEclud(vecA,vecB):
    return sqrt(sum(power(vecA-vecB,2)))

#隨機初始化K個質心(質心滿足數據邊界之內)
def randCent(dataSet,k):
    #得到數據樣本的維度
    n=shape(dataSet)[1]
    #初始化為一個(k,n)的矩陣
    centroids=mat(zeros((k,n)))
    #遍歷數據集的每一維度
    for j in range(n):
        #得到該列數據的最小值
        minJ=min(dataSet[:,j])
        #得到該列數據的范圍(最大值-最小值)
        rangeJ=float(max(dataSet[:,j])-minJ)
        #k個質心向量的第j維數據值隨機為位於(最小值,最大值)內的某一值
        centroids[:,j]=minJ+rangeJ*random.rand(k,1)
    #返回初始化得到的k個質心向量
    return centroids
    
#k-均值聚類算法
#@dataSet:聚類數據集
#@k:用戶指定的k個類
#@distMeas:距離計算方法,默認歐氏距離distEclud()
#@createCent:獲得k個質心的方法,默認隨機獲取randCent()
def kMeans(dataSet,k,distMeas=distEclud,createCent=randCent):
    #獲取數據集樣本數
    m=shape(dataSet)[0]
    #初始化一個(m,2)的矩陣
    clusterAssment=mat(zeros((m,2)))
    #創建初始的k個質心向量
    centroids=createCent(dataSet,k)
    #聚類結果是否發生變化的布爾類型
    clusterChanged=True
    #只要聚類結果一直發生變化,就一直執行聚類算法,直至所有數據點聚類結果不變化
    while clusterChanged:
        #聚類結果變化布爾類型置為false
        clusterChanged=False
        #遍歷數據集每一個樣本向量
        for i in range(m):
            #初始化最小距離最正無窮;最小距離對應索引為-1
            minDist=inf;minIndex=-1
            #循環k個類的質心
            for j in range(k):
                #計算數據點到質心的歐氏距離
                distJI=distMeas(centroids[j,:],dataSet[i,:])
                #如果距離小於當前最小距離
                if distJI<minDist:
                    #當前距離定為當前最小距離;最小距離對應索引對應為j(第j個類)
                    minDist=distJI;minIndex=j
        #當前聚類結果中第i個樣本的聚類結果發生變化:布爾類型置為true,繼續聚類算法
        if clusterAssment[i,0] !=minIndex:clusterChanged=True
        #更新當前變化樣本的聚類結果和平方誤差
        clusterAssment[i,:]=minIndex,minDist**2
    #打印k-均值聚類的質心
    print centroids
    #遍歷每一個質心
    for cent in range(k):
        #將數據集中所有屬於當前質心類的樣本通過條件過濾篩選出來
        ptsInClust=dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]
        #計算這些數據的均值(axis=0:求列的均值),作為該類質心向量
        centroids[cent,:]=mean(ptsInClust,axis=0)
    #返回k個聚類,聚類結果及誤差
    return centroids,clusterAssment

  需要說明的是,在算法中,相似度的計算方法默認的是歐氏距離計算,當然也可以使用其他相似度計算函數,比如余弦距離;算法中,k個類的初始化方式為隨機初始化,並且初始化的質心必須在整個數據集的邊界之內,這可以通過找到數據集每一維的最大值和最小值;然后最小值+取值范圍*0到1的隨機數,來確保隨機點在數據邊界之內。

  在實際的K-means算法中,采用計算質心-分配-重新計算質心的方式反復迭代,算法停止的條件是,當然數據集所有的點分配的距其最近的簇不在發生變化時,就停止分配,更新所有簇的質心后,返回k個類的質心(一般是向量的形式)組成的質心列表,以及存儲各個數據點的分類結果和誤差距離的平方的二維矩陣。

  上面返回的結果中,之所以存儲每個數據點距離其質心誤差距離平方,是便於后續的算法預處理。因為K-means算法采取的是隨機初始化k個簇的質心的方式,因此聚類效果又可能陷入局部最優解的情況,局部最優解雖然效果不錯,但不如全局最優解的聚類效果更好。所以,后續會在算法結束后,采取相應的后處理,使算法跳出局部最優解,達到全局最優解,獲得最好的聚類效果。

  可以看一個聚類的例子:

3 后處理提高聚類性能

  有時候當我們觀察聚類的結果圖時,發現聚類的效果沒有那么好,如上圖所示,K-means算法在k值選取為3時的聚類結果,我們發現,算法能夠收斂但效果較差。顯然,這種情況的原因是,算法收斂到了局部最小值,而並不是全局最小值,局部最小值顯然沒有全局最小值的結果好。

  那么,既然知道了算法已經陷入了局部最小值,如何才能夠進一步提升K-means算法的效果呢?

  一種用於度量聚類效果的指標是SSE,即誤差平方和, 為所有簇中的全部數據點到簇中心的誤差距離的平方累加和。SSE的值如果越小,表示數據點越接近於它們的簇中心,即質心,聚類效果也越好。因為,對誤差取平方后,就會更加重視那些遠離中心的數據點。

  顯然,我們知道了一種改善聚類效果的做法就是降低SSE,那么如何在保持簇數目不變的情況下提高簇的質量呢?

  一種方法是:我們可以將具有最大SSE值得簇划分為兩個簇(因為,SSE最大的簇一般情況下,意味着簇內的數據點距離簇中心較遠),具體地,可以將最大簇包含的點過濾出來並在這些點上運行K-means算法,其中k設為2.

  同時,當把最大的簇(上圖中的下半部分)分為兩個簇之后,為了保證簇的數目是不變的,我們可以再合並兩個簇。具體地:

  一方面我們可以合並兩個最近的質心所對應的簇,即計算所有質心之間的距離,合並質心距離最近的兩個質心所對應的簇。

  另一方面,我們可以合並兩個使得SSE增幅最小的簇,顯然,合並兩個簇之后SSE的值會有所上升,那么為了最好的聚類效果,應該盡可能使總的SSE值小,所以就選擇合並兩個簇后SSE漲幅最小的簇。具體地,就是計算合並任意兩個簇之后的總得SSE,選取合並后最小的SSE對應的兩個簇進行合並。這樣,就可以滿足簇的數目不變。

  上面,是對已經聚類完成的結果進行改善的方法,在不改變k值的情況下,上述方法能夠起到一定的作用,會使得聚類效果得到一定的改善。那么,下面要講到的是一種克服算法收斂於局部最小值問題的K-means算法。即二分k-均值算法。

三,二分K-means算法

  二分K-means算法首先將所有點作為一個簇,然后將簇一分為二。之后選擇其中一個簇繼續進行划分,選擇哪一個簇取決於對其進行划分是否能夠最大程度的降低SSE的值。上述划分過程不斷重復,直至划分的簇的數目達到用戶指定的值為止。

  二分K-means算法的偽代碼如下:

將所有點看成一個簇
當簇數目小於k時
對於每一個簇
    計算總誤差
    在給定的簇上面進行k-均值聚類(k=2)
    計算將該簇一分為二之后的總誤差
選擇使得總誤差最小的簇進行划分

  當然,也可以選擇SSE最大的簇進行划分,知道簇數目達到用戶指定的數目為止。下面看具體的代碼:

#二分K-均值聚類算法
#@dataSet:待聚類數據集
#@k:用戶指定的聚類個數
#@distMeas:用戶指定的距離計算方法,默認為歐式距離計算
def biKmeans(dataSet,k,distMeas=distEclud):
    #獲得數據集的樣本數
    m=shape(dataSet)[0]
    #初始化一個元素均值0的(m,2)矩陣
    clusterAssment=mat(zeros((m,2)))
    #獲取數據集每一列數據的均值,組成一個長為列數的列表
    centroid0=mean(dataSet,axis=0).tolist()[0]
    #當前聚類列表為將數據集聚為一類
    centList=[centroid0]
    #遍歷每個數據集樣本
    for j in range(m):
        #計算當前聚為一類時各個數據點距離質心的平方距離
        clusterAssment[j,1]=distMeas(mat(centroid0),dataSet[j,:])**2
    #循環,直至二分k-均值達到k類為止
    while (len(centList)<k):
        #將當前最小平方誤差置為正無窮
        lowerSSE=inf
        #遍歷當前每個聚類
        for i in range(len(centList)):
            #通過數組過濾篩選出屬於第i類的數據集合
            ptsInCurrCluster=\
                dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]
            #對該類利用二分k-均值算法進行划分,返回划分后結果,及誤差
            centroidMat,splitClustAss=\
                kMeans(ptsInCurrCluster,2,distMeas)
            #計算該類划分后兩個類的誤差平方和
            sseSplit=sum(splitClustAss[:,1])
            #計算數據集中不屬於該類的數據的誤差平方和
            sseNotSplit=\
                sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])
            #打印這兩項誤差值
            print('sseSplit,and notSplit:',%(sseSplit,sseNotSplit))
            #划分第i類后總誤差小於當前最小總誤差
            if(sseSplit+sseNotSplit)<lowerSSE:
                #第i類作為本次划分類
                bestCentToSplit=i
                #第i類划分后得到的兩個質心向量
                bestNewCents=centroidMat
                #復制第i類中數據點的聚類結果即誤差值
                bestClustAss=splitClustAss.copy()
                #將划分第i類后的總誤差作為當前最小誤差
                lowerSSE=sseSplit+sseNotSplit
        #數組過濾篩選出本次2-均值聚類划分后類編號為1數據點,將這些數據點類編號變為
        #當前類個數+1,作為新的一個聚類
        bestClustAss[nonzero(bestClustAss[:,0].A==1)[0],0]=\    
                len(centList)
        #同理,將划分數據集中類編號為0的數據點的類編號仍置為被划分的類編號,使類編號
        #連續不出現空缺
        bestClustAss[nonzero(bestClustAss[:,0].A==0)[0],0]=\    
                bestCentToSplit
        #打印本次執行2-均值聚類算法的類
        print('the bestCentToSplit is:',%bestCentToSplit)
        #打印被划分的類的數據個數
        print('the len of bestClustAss is:',%(len(bestClustAss)))
        #更新質心列表中的變化后的質心向量
        centList[bestCentToSplit]=bestNewCents[0,:]
        #添加新的類的質心向量
        centList.append(bestNewCents[1,:])
        #更新clusterAssment列表中參與2-均值聚類數據點變化后的分類編號,及數據該類的誤差平方
        clusterAssment[nonzero(clusterAssment[:,0].A==\
                bestCentToSplit)[0],:]=bestClustAss
        #返回聚類結果
        return mat(centList),clusterAssment

  在上述算法中,直到簇的數目達到k值,算法才會停止。在算法中通過將所有的簇進行划分,然后分別計算划分后所有簇的誤差。選擇使得總誤差最小的那個簇進行划分。划分完成后,要更新簇的質心列表,數據點的分類結果及誤差平方。具體地,假設划分的簇為m(m<k)個簇中的第i個簇,那么這個簇分成的兩個簇后,其中一個取代該被划分的簇,成為第i個簇,並計算該簇的質心;此外,將划分得到的另外一個簇,作為一個新的簇,成為第m+1個簇,並計算該簇的質心。此外,算法中還存儲了各個數據點的划分結果和誤差平方,此時也應更新相應的存儲信息。這樣,重復該過程,直至簇個數達到k。

  通過上述算法,之前陷入局部最小值的的這些數據,經過二分K-means算法多次划分后,逐漸收斂到全局最小值,從而達到了令人滿意的聚類效果。

四,示例:對地圖上的點進行聚類

  現在有一個存有70個地址和城市名的文本,而沒有這些地點的距離信息。而我們想要對這些地點進行聚類,找到每個簇的質心地點,從而可以安排合理的行程,即質心之間選擇交通工具抵達,而位於每個質心附近的地點就可以采取步行的方法抵達。顯然,K-means算法可以為我們找到一種更加經濟而且高效的出行方式。

1 通過地址信息獲取相應的經緯度信息

  那么,既然沒有地點之間的距離信息,怎么計算地點之間的距離呢?又如何比較地點之間的遠近呢?

  我們手里只有各個地點的地址信息,那么如果有一個API,可以讓我們輸入地點信息,返回該地點的經度和緯度信息,那么我們就可以通過球面距離計算方法得到兩個地點之間的距離了。而Yahoo!PlaceFinder API可以幫助我們實現這一目標。獲取地點信息對應經緯度的代碼如下:

#Yahoo!PlaceFinder API
#導入urllib
import urllib
#導入json模塊
import json

#利用地名,城市獲取位置經緯度函數
def geoGrab(stAddress,city):
    #獲取經緯度網址
    apiStem='http://where.yahooapis.com/geocode?'
    #初始化一個字典,存儲相關參數
    params={}
    #返回類型為json
    params['flags']='J'
    #參數appid
    params['appid']='ppp68N8t'
    #參數地址位置信息
    params['location']=('%s %s', %(stAddress,city))
    #利用urlencode函數將字典轉為URL可以傳遞的字符串格式
    url_params=urllib.urlencode(params)
    #組成完整的URL地址api
    yahooApi=apiStem+url_params
    #打印該URL地址
    print('%s',yahooApi)
    #打開URL,返回json格式的數據
    c=urllib.urlopen(yahooApi)
    #返回json解析后的數據字典
    return json.load(c.read())

from time import sleep
#具體文本數據批量地址經緯度獲取函數
def massPlaceFind(fileName):
    #新建一個可寫的文本文件,存儲地址,城市,經緯度等信息
    fw=open('places.txt','wb+')
    #遍歷文本的每一行
    for line in open(fileName).readlines();
        #去除首尾空格
        line =line.strip()
        #按tab鍵分隔開
        lineArr=line.split('\t')
        #利用獲取經緯度函數獲取該地址經緯度
        retDict=geoGrab(lineArr[1],lineArr[2])
        #如果錯誤編碼為0,表示沒有錯誤,獲取到相應經緯度
        if retDict['ResultSet']['Error']==0:
            #從字典中獲取經度
            lat=float(retDict['ResultSet']['Results'][0]['latitute'])
            #維度
            lng=float(retDict['ResultSet']['Results'][0]['longitute'])
            #打印地名及對應的經緯度信息
            print('%s\t%f\t%f',%(lineArr[0],lat,lng))
            #將上面的信息存入新的文件中
            fw.write('%s\t%f\t%f\n',%(line,lat,lng))
        #如果錯誤編碼不為0,打印提示信息
        else:print('error fetching')
        #為防止頻繁調用API,造成請求被封,使函數調用延遲一秒
        sleep(1)
    #文本寫入關閉
    fw.close()

  在上述代碼中,首先創建一個字典,字典里面存儲的是通過URL獲取經緯度所必要的參數,即我們想要的返回的數據格式flogs=J;獲取數據的appid;以及要輸入的地址信息(stAddress,city)。然后,通過urlencode()函數幫助我們將字典類型的信息轉化為URL可以傳遞的字符串格式。最后,打開URL獲取返回的JSON類型數據,通過JSON工具來解析返回的數據。且在返回的結果中,當錯誤編碼為0時表示,得到了經緯度信息,而為其他值時,則表示返回經緯度信息失敗。

  此外,在代碼中,每次獲取完一個地點的經緯度信息后,延遲一秒鍾。這樣做的目的是為了避免頻繁的調用API,請求被封掉的情況。

  

2 對地理位置進行聚類

  我們已經得到了各個地點的經緯度信息,但是我們還要選擇計算距離的合適的方式。我們知道,在北極每走幾米的經度變化可能達到數十度,而沿着赤道附近走相同的距離,帶來的經度變化可能是零。這是,我們可以使用球面余弦定理來計算兩個經緯度之間的實際距離。具體代碼如下:

#球面距離計算及簇繪圖函數
def distSLC(vecA,vecB):
    #sin()和cos()以弧度未輸入,將float角度數值轉為弧度,即*pi/180
    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 arcos(a+b)*6371.0

import matplotlib
import matplotlib.pyplot as plt

#@numClust:聚類個數,默認為5
def clusterClubs(numClust=5):
    datList=[]
    #解析文本數據中的每一行中的數據特征值
    for line in open('places.txt').readlines():
        lineArr=line.split('\t')
        datList.append([float(lineArr[4]),float(lineArr[4])])
        datMat=mat(datList)
        #利用2-均值聚類算法進行聚類
        myCentroids,clusterAssing=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=[],ytick=[])
        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)
        for i in range(numClust):
            ptsInCurrCluster=datMat[nonzero(clusterAssing[:,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()

最后,將聚類的結果繪制出來:

五,小結

  1 聚類是一種無監督的學習方法。聚類區別於分類,即事先不知道要尋找的內容,沒有預先設定好的目標變量。

  2 聚類將數據點歸到多個簇中,其中相似的數據點歸為同一簇,而不相似的點歸為不同的簇。相似度的計算方法有很多,具體的應用選擇合適的相似度計算方法

  3 K-means聚類算法,是一種廣泛使用的聚類算法,其中k是需要指定的參數,即需要創建的簇的數目,K-means算法中的k個簇的質心可以通過隨機的方式獲得,但是這些點需要位於數據范圍內。在算法中,計算每個點到質心得距離,選擇距離最小的質心對應的簇作為該數據點的划分,然后再基於該分配過程后更新簇的質心。重復上述過程,直至各個簇的質心不再變化為止。

  4 K-means算法雖然有效,但是容易受到初始簇質心的情況而影響,有可能陷入局部最優解。為了解決這個問題,可以使用另外一種稱為二分K-means的聚類算法。二分K-means算法首先將所有數據點分為一個簇;然后使用K-means(k=2)對其進行划分;下一次迭代時,選擇使得SSE下降程度最大的簇進行划分;重復該過程,直至簇的個數達到指定的數目為止。實驗表明,二分K-means算法的聚類效果要好於普通的K-means聚類算法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM