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

聚類是一種無監督的學習,它將類似的對象歸到同一個簇中。它有點像全自動分類。聚類方法幾乎能夠應用於全部對象,簇內的對象越類似,聚類的效果越好
簇識別給出聚類結果的含義。假定有一些數據,如今將類似數據歸到一塊兒,簇識別會告訴咱們這些簇到底都是些什麼。聚類與分類的最大不一樣在於,分類的目標事先巳知,而聚類則不同。由於其產生的結果與分類相同,而只是類別沒有預先定義,聚類有時也被稱爲無監督分類(unsupervised classification )。
聚類分析試圖將類似對象歸人同一簇,將不類似對象歸到不一樣簇類似這一律念取決於所選擇的類似度計算方法python

10.1K-均值聚類算法

K-均值聚類
優勢:容易實現。
缺點:可能收斂到局部最小值,在大規模數據集上收斂較慢。
適用數據類型:數值型數據。git

K-均值是發現給定數據集的k個簇的算法。簇個數k是用戶給定的每個簇經過其質心( centroid) , 即簇中全部點的中心來描述
K-均值算法的工做流程是這樣的。首先,隨機肯定k個初始點做爲質心。而後將數據集中的每一個點分配到一個簇中,具體來說,爲每一個點找距其最近的質心,並將其分配給該質心所對應的簇。這一步完成以後,每一個簇的質心更新爲該簇全部點的平均值算法

上述過程的僞代碼表示以下:
建立k個點做爲起始質心(常常是隨機選擇)
當任意一個點的簇分配結果發生改變時
  對數據集中的每一個數據點
    對每一個質心
      計算質心與數據點之間的距離
    將數據點分配到距其最近的簇
  對每個簇,計算簇中全部點的均值並將均值做爲質心json

K-均值聚類的通常流程
(1)收集數據:使用任意方法。
⑵準備數據:須要數值型數據來計算距離,也能夠將標稱型數據映射爲二值型數據再用於距離計算。
(3)分析數據:使用任意方法。
(4)訓練算法:不適用於無監督學習,即無監督學習沒有訓練過程
(5)測試算法:應用聚類算法、觀察結果。可使用量化的偏差指標如偏差平方和(後面會介紹)來評價算法的結果。
(6)使用算法:能夠用於所但願的任何應用。一般狀況下,簇質心能夠表明整個簇的數據來作出決策。api

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

測試截圖以下:
這裏寫圖片描述markdown

K -均值聚類算法,代碼以下:app

#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函數爲筆者寫的繪圖函數:dom

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-均值算法並不是僅有的聚類算法, 另外稱爲層次聚類的方法也被普遍使用

相關文章
相關標籤/搜索