機器學習經典聚類演算法 —— k-均值演算法(附python實現程式碼及數據集)

  • 2019 年 10 月 3 日
  • 筆記

[toc]

工作原理

聚類是一種無監督的學習,它將相似的對象歸到同一個簇中。類似於全自動分類(自動的意思是連類別都是自動構建的)。K-均值演算法可以發現k個不同的簇,且每個簇的中心採用簇中所含值的均值計算而成。它的工作流程的偽程式碼表示如下:

創建k個點作為起始質心  當任意一個點的簇分配結果發生改變時      對數據集中的每個數據點          對每個質心              計算質心與數據點之間的距離          將數據點分配到距其最近的簇      對每一個簇,計算簇中所有點的均值並將均值作為質心    

python實現

首先是兩個距離函數,一般採用歐式距離

def distEclud(self, vecA, vecB):      return np.linalg.norm(vecA - vecB)  def distManh(self, vecA, vecB):      return np.linalg.norm(vecA - vecB,ord = 1)  

然後是randcent(),該函數為給點的數據集構建一個包含k個隨機質心的集合

def randCent(self, X, k):      n = X.shape[1]  # 特徵維數,也就是數據集有多少列      centroids = np.empty((k, n))  # k*n的矩陣,用於存儲每簇的質心      for j in range(n):  # 產生質心,一維一維地隨機初始化          minJ = min(X[:, j])          rangeJ = float(max(X[:, j]) - minJ)          centroids[:, j] = (minJ + rangeJ * np.random.rand(k, 1)).flatten()      return centroids  

對於kMeans和biKmeans的實現,參考了scikit-learn中kMeans的實現,將它們封裝成類。

  • n_clusters —— 聚類個數,也就是k
  • initCent —— 生成初始質心的方法,’random’表示隨機生成,也可以指定一個數組
  • max_iter —— 最大迭代次數
class kMeans(object):      def __init__(self, n_clusters=10, initCent='random', max_iter=300):          if hasattr(initCent, '__array__'):              n_clusters = initCent.shape[0]              self.centroids = np.asarray(initCent, dtype=np.float)          else:              self.centroids = None          self.n_clusters = n_clusters          self.max_iter = max_iter          self.initCent = initCent          self.clusterAssment = None          self.labels = None          self.sse = None      # 計算兩個向量的歐式距離      def distEclud(self, vecA, vecB):          return np.linalg.norm(vecA - vecB)        # 計算兩點的曼哈頓距離      def distManh(self, vecA, vecB):          return np.linalg.norm(vecA - vecB, ord=1)        # 為給點的數據集構建一個包含k個隨機質心的集合      def randCent(self, X, k):          n = X.shape[1]  # 特徵維數,也就是數據集有多少列          centroids = np.empty((k, n))  # k*n的矩陣,用於存儲每簇的質心          for j in range(n):  # 產生質心,一維一維地隨機初始化              minJ = min(X[:, j])              rangeJ = float(max(X[:, j]) - minJ)              centroids[:, j] = (minJ + rangeJ * np.random.rand(k, 1)).flatten()          return centroids        def fit(self, X):      # 聚類函數      # 聚類完後將得到質心self.centroids,簇分配結果self.clusterAssment          if not isinstance(X, np.ndarray):              try:                  X = np.asarray(X)              except:                  raise TypeError("numpy.ndarray required for X")          m = X.shape[0]  # 樣本數量          self.clusterAssment = np.empty((m, 2))  # m*2的矩陣,第一列表示樣本屬於哪一簇,第二列存儲該樣本與質心的平方誤差(Squared Error,SE)          if self.initCent == 'random':   # 可以指定質心或者隨機產生質心              self.centroids = self.randCent(X, self.n_clusters)          clusterChanged = True          for _ in range(self.max_iter):# 指定最大迭代次數              clusterChanged = False              for i in range(m):  # 將每個樣本分配到離它最近的質心所屬的簇                  minDist = np.inf                  minIndex = -1                  for j in range(self.n_clusters):    #遍歷所有數據點找到距離每個點最近的質心                      distJI = self.distEclud(self.centroids[j, :], X[i, :])                      if distJI < minDist:                          minDist = distJI                          minIndex = j                  if self.clusterAssment[i, 0] != minIndex:                      clusterChanged = True                      self.clusterAssment[i, :] = minIndex, minDist ** 2              if not clusterChanged:  # 若所有樣本點所屬的簇都不改變,則已收斂,提前結束迭代                  break              for i in range(self.n_clusters):  # 將每個簇中的點的均值作為質心                  ptsInClust = X[np.nonzero(self.clusterAssment[:, 0] == i)[0]]  # 取出屬於第i個族的所有點                  if(len(ptsInClust) != 0):                      self.centroids[i, :] = np.mean(ptsInClust, axis=0)            self.labels = self.clusterAssment[:, 0]          self.sse = sum(self.clusterAssment[:, 1])   # Sum of Squared Error,SSE  

kMeans的缺點在於——可能收斂到局部最小值。採用SSE(Sum of Squared Error,誤差平方和)來度量聚類的效果。SSE值越小表示數據點越接近於它們的質心,聚類效果也越好。
為了克服kMeans會收斂於局部最小值的問題,有人提出了一個稱為二分K-均值的演算法。該演算法偽程式碼如下:

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

python程式碼如下:

class biKMeans(object):      def __init__(self, n_clusters=5):          self.n_clusters = n_clusters          self.centroids = None          self.clusterAssment = None          self.labels = None          self.sse = None      # 計算兩點的歐式距離      def distEclud(self, vecA, vecB):          return np.linalg.norm(vecA - vecB)        # 計算兩點的曼哈頓距離      def distManh(self, vecA, vecB):          return np.linalg.norm(vecA - vecB,ord = 1)      def fit(self, X):          m = X.shape[0]          self.clusterAssment = np.zeros((m, 2))          if(len(X) != 0):              centroid0 = np.mean(X, axis=0).tolist()          centList = [centroid0]          for j in range(m):  # 計算每個樣本點與質心之間初始的SE              self.clusterAssment[j, 1] = self.distEclud(np.asarray(centroid0), X[j, :]) ** 2            while (len(centList) < self.n_clusters):              lowestSSE = np.inf              for i in range(len(centList)):  # 嘗試劃分每一族,選取使得誤差最小的那個族進行劃分                  ptsInCurrCluster = X[np.nonzero(self.clusterAssment[:, 0] == i)[0], :]                  clf = kMeans(n_clusters=2)                  clf.fit(ptsInCurrCluster)                  centroidMat, splitClustAss = clf.centroids, clf.clusterAssment  # 劃分該族後,所得到的質心、分配結果及誤差矩陣                  sseSplit = sum(splitClustAss[:, 1])                  sseNotSplit = sum(self.clusterAssment[np.nonzero(self.clusterAssment[:, 0] != i)[0], 1])                  if (sseSplit + sseNotSplit) < lowestSSE:                      bestCentToSplit = i                      bestNewCents = centroidMat                      bestClustAss = splitClustAss.copy()                      lowestSSE = sseSplit + sseNotSplit              # 該族被劃分成兩個子族後,其中一個子族的索引變為原族的索引,另一個子族的索引變為len(centList),然後存入centList              bestClustAss[np.nonzero(bestClustAss[:, 0] == 1)[0], 0] = len(centList)              bestClustAss[np.nonzero(bestClustAss[:, 0] == 0)[0], 0] = bestCentToSplit              centList[bestCentToSplit] = bestNewCents[0, :].tolist()              centList.append(bestNewCents[1, :].tolist())              self.clusterAssment[np.nonzero(self.clusterAssment[:, 0] == bestCentToSplit)[0], :] = bestClustAss          self.labels = self.clusterAssment[:, 0]          self.sse = sum(self.clusterAssment[:, 1])          self.centroids = np.asarray(centList)  

上述函數運行多次聚類會收斂到全局最小值,而原始的kMeans()函數偶爾會陷入局部最小值。

演算法實戰

對mnist數據集進行聚類

從網上找的數據集data.pkl。該數據集是mnist中選取的1000張圖,用t_sne降維到了二維。

讀取文件的程式碼如下:

dataSet, dataLabel = pickle.load(open('data.pkl', 'rb'), encoding='latin1')      print(type(dataSet))      print(dataSet.shape)      print(dataSet)      print(type(dataLabel))      print(dataLabel.shape)      print(dataLabel)  

列印出來結果如下:

<class 'numpy.ndarray'>  (1000, 2)  [[ -0.48183008 -22.66856528]   [ 11.5207274   10.62315075]   [  4.76092787   5.20842437]   ...   [ -8.43837464   2.63939773]   [ 20.28416829   1.93584107]   [-21.19202119  -4.47293397]]  <class 'numpy.ndarray'>  (1000,)  [0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0  9 5 5 6 5 0   9 8 9 8 4 1 7 7 3 5 1 0 0 2 2 7 8 2 0 1 2 6 3 3 7 3 3 4 6 6 6 ...   3 7 3 3 4 6 6 6 4 9 1 5 0 9 5 2 8 2 0 0 1 7 6 3 2 1 4 6 3 1 3 9 1 7 6 8 4 3]  

開始使用之前編寫的演算法聚類,並多次運行保存sse最小的一次所得到的圖。

def main():      dataSet, dataLabel = pickle.load(open('data.pkl', 'rb'), encoding='latin1')      k = 10      clf = biKMeans(k)      lowestsse = np.inf      for i in range(10):          print(i)          clf.fit(dataSet)          cents = clf.centroids          labels = clf.labels          sse = clf.sse          visualization(k, dataSet, dataLabel, cents, labels, sse, lowestsse)          if(sse < lowestsse):              lowestsse = sse  if __name__ == '__main__':      main()  

小結

聚類是一種無監督的學習方法。所謂無監督學習是指事先並不知道要尋找的內容,即沒有目標變數。聚類將數據點歸到多個簇中,其中相似數據點處於同一簇,而不相似數據點處於不同簇中。聚類中可以使用多種不同的方法來計算相似度(比如本文是使用距離度量)

K-均值演算法是最為廣泛使用聚類演算法,其中的k是指用戶指定要創建的簇的數目。K-均值聚類演算法以k個隨機質心開始。演算法會計算每個點到質心的距離。每個點會被分配到距其最近的簇質心,然後緊接著基於新分配到簇的點更新簇質心。以上過程重複數次,直到簇質心不再改變。這種方法易於實現,但容易受到初始簇質心的影響,並且收斂到局部最優解而不是全局最優解。

還有一種二分K-均值的演算法,可以得到更好的聚類效果。首先將所有點作為一個簇,然後使用K-均值演算法(k=2)對其劃分。下一次迭代時,選擇有最大誤差的簇進行劃分。該過程重複直到k個簇創建成功為止。

附錄

文中程式碼及數據集:https://github.com/Professorchen/Machine-Learning/tree/master/kMeans