機器學習實戰筆記-利用K均值聚類算法對未標注數據分組


聚類是一種無監督的學習,它將相似的對象歸到同一個簇中。它有點像全自動分類。聚類方法幾乎可以應用於所有對象,簇內的對象越相似,聚類的效果越好
簇識別給出聚類結果的含義。假定有一些數據,現在將相似數據歸到一起,簇識別會告訴我們這些簇到底都是些什么。聚類與分類的最大不同在於,分類的目標事先巳知,而聚類則不一樣。因為其產生的結果與分類相同,而只是類別沒有預先定義,聚類有時也被稱為無監督分類(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-均值算法並非僅有的聚類算法, 另外稱為層次聚類的方法也被廣泛使用

版權聲明:本文為博主原創文章,未經博主允許不得轉載。


免責聲明!

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



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