1. 程式人生 > 實用技巧 >機器學習實戰---K均值聚類演算法

機器學習實戰---K均值聚類演算法

一:一般K均值聚類演算法實現

(一)匯入資料

import numpy as np
import matplotlib.pyplot as plt

def loadDataSet(filename):
    dataSet = np.loadtxt(filename)
    return dataSet

(二)計算兩個向量之間的距離

def distEclud(vecA,vecB):   #計算兩個向量之間距離
    return np.sqrt(np.sum(np.power(vecA-vecB,2)))

(三)隨機初始化聚簇中心

def randCent(data_X,k):    #隨機初始化聚簇中心 可以隨機選取樣本點,或者選取距離隨機
    m,n 
= data_X.shape centroids = np.zeros((k,n)) #開始隨機初始化 for i in range(n): Xmin = np.min(data_X[:,i]) #獲取該特徵最小值 Xmax = np.max(data_X[:,i]) #獲取該特徵最大值 disc = Xmax - Xmin #獲取差值 centroids[:,i] = (Xmin + np.random.rand(k,1)*disc).flatten() #為i列特徵全部k個聚簇中心賦值 rand(k,1
)表示產生k行1列在0-1之間的隨機數 return centroids

(四)實現聚簇演算法

def kMeans(data_X,k,distCalc=distEclud,createCent=randCent):   #實現k均值演算法,當所有中心不再改變時退出
    m,n = data_X.shape
    centroids = createCent(data_X,k)    #建立隨機聚簇中心
    clusterAssment = np.zeros((m,2))    #建立各個樣本點的分類和位置資訊    第一個表示屬於哪一個聚簇,第二個表示距離該聚簇的位置
    changeFlag 
= True #設定識別符號,表示是否有聚簇中心改變 while changeFlag: changeFlag = False #開始計算各個點到聚簇中心距離,進行點集合分類 for i in range(m): #對於每一個樣本點,判斷是屬於哪一個聚簇 bestMinIdx = -1 bestMinDist = np.inf for j in range(k): #求取到各個聚簇中心的距離 dist = distCalc(centroids[j], data_X[i]) if dist < bestMinDist: bestMinIdx = j bestMinDist = dist if clusterAssment[i,0] != bestMinIdx: #該樣本點有改變聚簇中心 changeFlag = True clusterAssment[i,:] = bestMinIdx,bestMinDist #開始根據上面樣本點分類資訊,進行聚簇中心重新分配 for i in range(k): centroids[i] = np.mean(data_X[np.where(clusterAssment[:,0]==i)],0) return centroids,clusterAssment

(五)結果測試

data_X = loadDataSet("testSet.txt")
centroids,clusterAssment = kMeans(data_X,4)

plt.figure()
plt.scatter(data_X[:,0].flatten(),data_X[:,1].flatten(),c="b",marker="o")
plt.scatter(centroids[:,0].flatten(),centroids[:,1].flatten(),c='r',marker="+")
plt.show()

我們可以發現,在經過多次測試後,會出現聚簇收斂到區域性最小值。導致不能得到我們想要的聚簇結果!!!

二:多次測試,計算代價,選取最優聚簇中心

https://www.cnblogs.com/ssyfj/p/12966305.html

避免區域性最優:如果想讓找到最優可能的聚類,可以嘗試多次隨機初始化,以此來保證能夠得到一個足夠好的結果,選取代價最小的一個也就是代價函式J最小的。事實證明,在聚類數K較小的情況下(2~10個),使用多次隨機初始化會有較大的影響,而如果K很大的情況,多次隨機初始化可能並不會有太大效果

三:後處理提高聚類效能(可以不實現)

理解思路即可,實現沒必要,因為後面的二分K-均值演算法更加好。這裡的思路可以用到二分K-均值演算法中。

通過SSE指標(誤差平方和)來度量聚類效果,是根據各個樣本點到對應聚簇中心聚類來計算的。SSE越小表示資料點越接近質心,聚類效果越好。

一種好的方法是通過增加聚簇中心(是將具有最大SSE值的簇劃分為兩個簇)來減少SSE值,但是違背了K-均值思想(自行增加了聚簇數量),但是我們可以在後面進行處理,合併兩個最接近的聚簇中心,從而達到保持聚簇中心數量不變,但是降低SSE值的情況,獲取全域性最優聚簇中心。

(一)全部程式碼

import numpy as np
import matplotlib.pyplot as plt

def loadDataSet(filename):
    dataSet = np.loadtxt(filename)
    return dataSet

def distEclud(vecA,vecB):   #計算兩個向量之間距離
    return np.sqrt(np.sum(np.power(vecA-vecB,2)))

def randCent(data_X,k):    #隨機初始化聚簇中心 可以隨機選取樣本點,或者選取距離隨機
    m,n = data_X.shape
    centroids = np.zeros((k,n))
    #開始隨機初始化
    for i in range(n):
        Xmin = np.min(data_X[:,i])   #獲取該特徵最小值
        Xmax = np.max(data_X[:,i])   #獲取該特徵最大值

        disc = Xmax - Xmin  #獲取差值
        centroids[:,i] = (Xmin + np.random.rand(k,1)*disc).flatten() #為i列特徵全部k個聚簇中心賦值  rand(k,1)表示產生k行1列在0-1之間的隨機數
    return centroids

def getSSE(clusterAssment):    #傳入一個聚簇中心和對應的距離資料集
    return np.sum(clusterAssment[:,1])

def kMeans(data_X,k,distCalc=distEclud,createCent=randCent,divide=False):   #實現k均值演算法,當所有中心不再改變時退出
    m,n = data_X.shape
    centroids = createCent(data_X,k)    #建立隨機聚簇中心
    clusterAssment = np.zeros((m,2))    #建立各個樣本點的分類和位置資訊    第一個表示屬於哪一個聚簇,第二個表示距離該聚簇的位置
    changeFlag = True   #設定識別符號,表示是否有聚簇中心改變

    while changeFlag:
        changeFlag = False
        #開始計算各個點到聚簇中心距離,進行點集合分類
        for i in range(m):  #對於每一個樣本點,判斷是屬於哪一個聚簇
            bestMinIdx = -1
            bestMinDist = np.inf
            for j in range(k):  #求取到各個聚簇中心的距離
                dist = distCalc(centroids[j], data_X[i])
                if dist < bestMinDist:
                    bestMinIdx = j
                    bestMinDist = dist
            if clusterAssment[i,0] != bestMinIdx:   #該樣本點有改變聚簇中心
                changeFlag = True
            clusterAssment[i,:] = bestMinIdx,bestMinDist

        #開始根據上面樣本點分類資訊,進行聚簇中心重新分配
        for i in range(k):
            centroids[i] = np.mean(data_X[np.where(clusterAssment[:,0]==i)],0)

    midCentroids = centroids

    #進行後處理
    if divide == True:  # 開始進行一次後處理
        maxSSE = 0
        maxIdx = -1
        for i in range(k):  #先找到最大的那個簇,進行劃分
            curSSE = getSSE(clusterAssment[np.where(clusterAssment[:,0]==i)])
            if curSSE > maxSSE:
                maxSSE = curSSE
                maxIdx = i

        #將最大簇劃分為兩個簇
        temp,new_centroids,new_clusterAssment = kMeans(data_X[np.where(clusterAssment[:,0]==maxIdx)],2)
        centroids[maxIdx] = new_centroids[0]    #更新一個
        centroids = np.r_[centroids,np.array([new_centroids[1]])]   #更新第二個
        new_clusterAssment[0,:] = maxIdx
        new_clusterAssment[1,:] = centroids.shape[0] - 1


        #找的最近的兩個聚簇中心進行合併
        clusterAssment[np.where(clusterAssment[:,0]==maxIdx)] = new_clusterAssment  #距離更新
        distArr = np.zeros((k+1,k+1))
        for i in range(k+1):
            temp_disc = np.sum(np.power(centroids[i] - centroids,2),1)  #獲取L2正規化距離平方
            temp_disc[i] = np.inf   #將對角線的0值設定為無窮大,方便後面求取最小值
            distArr[i] = temp_disc

        #獲取最小距離位置
        idx = np.argmin(distArr)
        cluidx = int((idx) / (k+1)),(idx) % (k+1)    #獲取行列索引
        #計算兩個聚簇的新的聚簇中心
        new_centroids = np.mean(data_X[np.where((clusterAssment[:, 0] == cluidx[1]) | (clusterAssment[:, 0] == cluidx[0]))], 0)
        centroids = np.delete(centroids,list(cluidx),0)

        centroids = np.r_[centroids,np.array([new_centroids])]

    return midCentroids,centroids,clusterAssment

plt.figure()

data_X = loadDataSet("testSet.txt")

midCentroids,centroids,clusterAssment = kMeans(data_X,4,divide=True)
midCentroids[:, 1] += 0.1
plt.scatter(data_X[:,0].flatten(),data_X[:,1].flatten(),c="b",marker="o")
plt.scatter(midCentroids[:, 0].flatten(), midCentroids[:, 1].flatten(), c='g', marker="+")
plt.scatter(centroids[:,0].flatten(),centroids[:,1].flatten(),c='r',marker="+")
plt.show()
View Code

(二)計算SSE函式

def getSSE(clusterAssment):    #傳入一個聚簇中心和對應的距離資料集
    return np.sum(clusterAssment[:,1])

(三)修改原有的k-均值演算法

def kMeans(data_X,k,distCalc=distEclud,createCent=randCent,divide=False):   #實現k均值演算法,當所有中心不再改變時退出  最後一個引數,用來表示是否是後處理,True不需要後處理
    m,n = data_X.shape
    centroids = createCent(data_X,k)    #建立隨機聚簇中心
    clusterAssment = np.zeros((m,2))    #建立各個樣本點的分類和位置資訊    第一個表示屬於哪一個聚簇,第二個表示距離該聚簇的位置
    changeFlag = True   #設定識別符號,表示是否有聚簇中心改變

    while changeFlag:
        changeFlag = False
        #開始計算各個點到聚簇中心距離,進行點集合分類
        for i in range(m):  #對於每一個樣本點,判斷是屬於哪一個聚簇
            bestMinIdx = -1
            bestMinDist = np.inf
            for j in range(k):  #求取到各個聚簇中心的距離
                dist = distCalc(centroids[j], data_X[i])
                if dist < bestMinDist:
                    bestMinIdx = j
                    bestMinDist = dist
            if clusterAssment[i,0] != bestMinIdx:   #該樣本點有改變聚簇中心
                changeFlag = True
            clusterAssment[i,:] = bestMinIdx,bestMinDist

        #開始根據上面樣本點分類資訊,進行聚簇中心重新分配
        for i in range(k):
            centroids[i] = np.mean(data_X[np.where(clusterAssment[:,0]==i)],0)

    midCentroids = centroids

    #進行後處理
    if divide == True:  # 開始進行一次後處理
        maxSSE = 0
        maxIdx = -1
        for i in range(k):  #先找到最大的那個簇,進行劃分
            curSSE = getSSE(clusterAssment[np.where(clusterAssment[:,0]==i)])
            if curSSE > maxSSE:
                maxSSE = curSSE
                maxIdx = i

        #將最大簇劃分為兩個簇
        temp,new_centroids,new_clusterAssment = kMeans(data_X[np.where(clusterAssment[:,0]==maxIdx)],2)
        centroids[maxIdx] = new_centroids[0]    #更新一個
        centroids = np.r_[centroids,np.array([new_centroids[1]])]   #更新第二個
        new_clusterAssment[0,:] = maxIdx
        new_clusterAssment[1,:] = centroids.shape[0] - 1


        #找的最近的兩個聚簇中心進行合併
        clusterAssment[np.where(clusterAssment[:,0]==maxIdx)] = new_clusterAssment  #距離更新
        distArr = np.zeros((k+1,k+1))
        for i in range(k+1):
            temp_disc = np.sum(np.power(centroids[i] - centroids,2),1)  #獲取L2正規化距離平方
            temp_disc[i] = np.inf   #將對角線的0值設定為無窮大,方便後面求取最小值
            distArr[i] = temp_disc

        #獲取最小距離位置
        idx = np.argmin(distArr)
        cluidx = int((idx) / (k+1)),(idx) % (k+1)    #獲取行列索引
        #計算兩個聚簇的新的聚簇中心
        new_centroids = np.mean(data_X[np.where((clusterAssment[:, 0] == cluidx[1]) | (clusterAssment[:, 0] == cluidx[0]))], 0)
        centroids = np.delete(centroids,list(cluidx),0)

        centroids = np.r_[centroids,np.array([new_centroids])]

    return midCentroids,centroids,clusterAssment  #第一個返回的是正常k-均值聚簇結果,第二、三返回的是後處理以後的聚簇中心和樣本分類資訊

(四)測試現象

plt.figure()

data_X = loadDataSet("testSet.txt")

midCentroids,centroids,clusterAssment = kMeans(data_X,4,divide=True)
midCentroids[:, 1] += 0.1  #將兩個K-均值處理結果錯開
plt.scatter(data_X[:,0].flatten(),data_X[:,1].flatten(),c="b",marker="o")  #原始資料
plt.scatter(midCentroids[:, 0].flatten(), midCentroids[:, 1].flatten(), c='g', marker="+")  #一般K-均值聚簇
plt.scatter(centroids[:,0].flatten(),centroids[:,1].flatten(),c='r',marker="+")  #後處理以後的聚簇現象
plt.show()

注意:紅色為後處理結果、綠色為一般K-均值處理

可以看到從左到右,後處理現象依次顯現,尤其是最右邊圖中,後處理對原始聚簇劃分進行了很大的改進!!!

雖然後處理不錯,但是後面的二分K-均值演算法是在聚簇時,直接考慮了SSE來進行劃分K個聚簇,而不是在聚簇後進行考慮再進行劃分合併。所以下面來看二分K-均值演算法

四:二分K-均值演算法

(一)全部程式碼

import numpy as np
from numpy import *
import matplotlib.pyplot as plt

def loadDataSet(filename):
    dataSet = np.loadtxt(filename)
    return dataSet

def distEclud(vecA,vecB):   #計算兩個向量之間距離
    return np.sqrt(np.sum(np.power(vecA-vecB,2)))

def randCent(data_X,k):    #隨機初始化聚簇中心 可以隨機選取樣本點,或者選取距離隨機
    m,n = data_X.shape
    centroids = np.zeros((k,n))
    #開始隨機初始化
    for i in range(n):
        Xmin = np.min(data_X[:,i])   #獲取該特徵最小值
        Xmax = np.max(data_X[:,i])   #獲取該特徵最大值
        disc = Xmax - Xmin  #獲取差值
        centroids[:,i] = (Xmin + np.random.rand(k,1)*disc).flatten() #為i列特徵全部k個聚簇中心賦值  rand(k,1)表示產生k行1列在0-1之間的隨機數
    return centroids

def kMeans(data_X,k,distCalc=distEclud,createCent=randCent):   #實現k均值演算法,當所有中心不再改變時退出
    m,n = data_X.shape
    centroids = createCent(data_X,k)    #建立隨機聚簇中心
    clusterAssment = np.zeros((m,2))    #建立各個樣本點的分類和位置資訊    第一個表示屬於哪一個聚簇,第二個表示距離該聚簇的位置
    changeFlag = True   #設定識別符號,表示是否有聚簇中心改變

    while changeFlag:
        changeFlag = False
        #開始計算各個點到聚簇中心距離,進行點集合分類
        for i in range(m):  #對於每一個樣本點,判斷是屬於哪一個聚簇
            bestMinIdx = -1
            bestMinDist = np.inf
            for j in range(k):  #求取到各個聚簇中心的距離
                dist = distCalc(centroids[j], data_X[i])
                if dist < bestMinDist:
                    bestMinIdx = j
                    bestMinDist = dist
            if clusterAssment[i,0] != bestMinIdx:   #該樣本點有改變聚簇中心
                changeFlag = True
            clusterAssment[i,:] = bestMinIdx,bestMinDist

        #開始根據上面樣本點分類資訊,進行聚簇中心重新分配
        for i in range(k):
            centroids[i] = np.mean(data_X[np.where(clusterAssment[:,0]==i)],0)

    return centroids,clusterAssment

def binkMeans(data_X,k,distCalc=distEclud):   #實現二分-k均值演算法,開始都是屬於一個聚簇,當我們聚簇中心數為K時,退出
    m,n = data_X.shape
    clusterAssment = np.zeros((m,2))
    centroid0 = np.mean(data_X,0).tolist()   #全部資料集屬於一個聚簇時,設定中心為均值即可

    centList = [centroid0]  #用於統計所有的聚簇中心
    for i in range(m):
        clusterAssment[i,1] = distCalc(data_X[i],centroid0)#設定距離,前面初始為0,設定了聚簇中心類別

    while len(centList) < k:    #不滿足K個聚簇,則一直進行分類
        lowestSSE = np.inf  #用於計算每個聚簇的SSE值
        for i in range(len(centList)): #嘗試對每一個聚簇進行一次劃分,看哪一個簇劃分後所有簇的SSE最小
            #先對該簇進行劃分,然後獲取劃分後的SSE值,和沒有進行劃分的資料集的SSE值
            #先進行資料劃分
            splitClusData = data_X[np.where(clusterAssment[:, 0] == i)]
            #進行簇劃分
            splitCentroids,splitClustArr = kMeans(splitClusData,2,distCalc)
            #獲取全部SSE值
            splitSSE = np.sum(splitClustArr[:,1])
            noSplitSSE = np.sum(clusterAssment[np.where(clusterAssment[:, 0] != i),1])
            if (splitSSE + noSplitSSE) < lowestSSE:
                lowestSSE = splitSSE + noSplitSSE
                bestSplitClus = i

                #記錄劃分資訊
                bestSplitCent = splitCentroids
                bestSplitClu = splitClustArr.copy()
        #更新簇的分配結果 二分後資料集:對於索引0,則保持原有的i位置,對於索引1則加到列表後面
        bestSplitClu[np.where(bestSplitClu[:,0]==1)[0],0] = len(centList)
        bestSplitClu[np.where(bestSplitClu[:,0]==0)[0],0] = bestSplitClus

        #還要繼續更新聚簇中心
        centList[bestSplitClus] = bestSplitCent[0].tolist()
        centList.append(bestSplitCent[1].tolist())

        #還要對劃分的資料集進行標籤更新
        clusterAssment[np.where(clusterAssment[:,0]==bestSplitClus)[0],:] = bestSplitClu

    return np.array(centList),clusterAssment



data_X = loadDataSet("testSet2.txt")
centroids,clusterAssment = binkMeans(data_X,3)
plt.figure()
plt.scatter(data_X[:,0].flatten(),data_X[:,1].flatten(),c="b",marker="o")
print(centroids)

plt.scatter(centroids[:,0].reshape(1,3).tolist()[0],centroids[:,1].reshape(1,3).tolist()[0],c='r',marker="+")
plt.show()
View Code

(二)不變程式碼

import numpy as np
from numpy import *
import matplotlib.pyplot as plt

def loadDataSet(filename):
    dataSet = np.loadtxt(filename)
    return dataSet

def distEclud(vecA,vecB):   #計算兩個向量之間距離
    return np.sqrt(np.sum(np.power(vecA-vecB,2)))

def randCent(data_X,k):    #隨機初始化聚簇中心 可以隨機選取樣本點,或者選取距離隨機
    m,n = data_X.shape
    centroids = np.zeros((k,n))
    #開始隨機初始化
    for i in range(n):
        Xmin = np.min(data_X[:,i])   #獲取該特徵最小值
        Xmax = np.max(data_X[:,i])   #獲取該特徵最大值
        disc = Xmax - Xmin  #獲取差值
        centroids[:,i] = (Xmin + np.random.rand(k,1)*disc).flatten() #為i列特徵全部k個聚簇中心賦值  rand(k,1)表示產生k行1列在0-1之間的隨機數
    return centroids

def kMeans(data_X,k,distCalc=distEclud,createCent=randCent):   #實現k均值演算法,當所有中心不再改變時退出
    m,n = data_X.shape
    centroids = createCent(data_X,k)    #建立隨機聚簇中心
    clusterAssment = np.zeros((m,2))    #建立各個樣本點的分類和位置資訊    第一個表示屬於哪一個聚簇,第二個表示距離該聚簇的位置
    changeFlag = True   #設定識別符號,表示是否有聚簇中心改變

    while changeFlag:
        changeFlag = False
        #開始計算各個點到聚簇中心距離,進行點集合分類
        for i in range(m):  #對於每一個樣本點,判斷是屬於哪一個聚簇
            bestMinIdx = -1
            bestMinDist = np.inf
            for j in range(k):  #求取到各個聚簇中心的距離
                dist = distCalc(centroids[j], data_X[i])
                if dist < bestMinDist:
                    bestMinIdx = j
                    bestMinDist = dist
            if clusterAssment[i,0] != bestMinIdx:   #該樣本點有改變聚簇中心
                changeFlag = True
            clusterAssment[i,:] = bestMinIdx,bestMinDist

        #開始根據上面樣本點分類資訊,進行聚簇中心重新分配
        for i in range(k):
            centroids[i] = np.mean(data_X[np.where(clusterAssment[:,0]==i)],0)

    return centroids,clusterAssment

(三)二分K-均值實現

def binkMeans(data_X,k,distCalc=distEclud):   #實現二分-k均值演算法,開始都是屬於一個聚簇,當我們聚簇中心數為K時,退出
    m,n = data_X.shape
    clusterAssment = np.zeros((m,2))
    centroid0 = np.mean(data_X,0).tolist()   #全部資料集屬於一個聚簇時,設定中心為均值即可

    centList = [centroid0]  #用於統計所有的聚簇中心
    for i in range(m):
        clusterAssment[i,1] = distCalc(data_X[i],centroid0)#設定距離,前面初始為0,設定了聚簇中心類別

    while len(centList) < k:    #不滿足K個聚簇,則一直進行分類
        lowestSSE = np.inf  #用於計算每個聚簇的SSE值
        for i in range(len(centList)): #嘗試對每一個聚簇進行一次劃分,看哪一個簇劃分後所有簇的SSE最小
            #先對該簇進行劃分,然後獲取劃分後的SSE值,和沒有進行劃分的資料集的SSE值
            #先進行資料劃分
            splitClusData = data_X[np.where(clusterAssment[:, 0] == i)]
            #進行簇劃分
            splitCentroids,splitClustArr = kMeans(splitClusData,2,distCalc)
            #獲取全部SSE值
            splitSSE = np.sum(splitClustArr[:,1])
            noSplitSSE = np.sum(clusterAssment[np.where(clusterAssment[:, 0] != i),1])
            if (splitSSE + noSplitSSE) < lowestSSE:
                lowestSSE = splitSSE + noSplitSSE
                bestSplitClus = i

                #記錄劃分資訊
                bestSplitCent = splitCentroids
                bestSplitClu = splitClustArr.copy()
        #更新簇的分配結果 二分後資料集:對於索引0,則保持原有的i位置,對於索引1則加到列表後面
        bestSplitClu[np.where(bestSplitClu[:,0]==1)[0],0] = len(centList)
        bestSplitClu[np.where(bestSplitClu[:,0]==0)[0],0] = bestSplitClus

        #還要繼續更新聚簇中心
        centList[bestSplitClus] = bestSplitCent[0].tolist()
        centList.append(bestSplitCent[1].tolist())

        #還要對劃分的資料集進行標籤更新
        clusterAssment[np.where(clusterAssment[:,0]==bestSplitClus)[0],:] = bestSplitClu

    return np.array(centList),clusterAssment

重點:使用np.where查詢時,對資料集列值進行修改時,需要選取np.where()[0]---表示索引位置,之後在資料集中選取列數data[np.where()[0],:]=...

(四)結果測試

data_X = loadDataSet("testSet2.txt")
centroids,clusterAssment = binkMeans(data_X,3)
plt.figure()
plt.scatter(data_X[:,0].flatten(),data_X[:,1].flatten(),c="b",marker="o")
print(centroids)

plt.scatter(centroids[:,0].reshape(1,3).tolist()[0],centroids[:,1].reshape(1,3).tolist()[0],c='r',marker="+")
plt.show()