實戰部分主要針對某一具體演算法對其原理進行較為詳細的介紹,然後進行簡單地實現(可能對演算法效能考慮欠缺),這一部分主要介紹一些常見的一些聚類演算法。
K-means聚類演算法
0.聚類演算法演算法簡介
聚類演算法算是機器學習中最為常見的一類演算法,在無監督學習中,可以說聚類演算法有著舉足輕重的地位。
提到無監督學習,不同於前面介紹的有監督學習,無監督學習的資料沒有對應的資料標籤,我們只能從輸入X中去進行一些知識發現或者預處理。
過去在有監督學習中,我們(讓機器)通過X去預測Y,而到了無監督學習中,我們(讓機器)只能從X去發現什麼,或者X中哪些輸入對我們是有用的,因此:
無監督學習中包括了兩大方面:聚類和降維。
在無監督學習中,我們通過X可以發現什麼。聚類就是主要回答這一類問題。而對於一個具有很多維的資料,那些維度對於我們想要知道的事情的影響比較大,這就是降維要做的事情。
聚類演算法,顧名思義,就是一種能將屬於同類別的資料聚集在一起的演算法,稱之為“物以類聚”。聚類的目的就是將相似的物件歸為同一簇中,不相似的物件歸到不同簇中。
聚類演算法比較常用的有K-means、層次聚類演算法、DBSCAN等,這些後面會一一介紹,本節主要是K-means演算法。
具體聚類演算法有很多應用場景,如客戶群體分析、社交網路分析等,還有很多間接的應用,如資料預處理、半監督學習等。
1.K-means聚類原理
K-means聚類就是給定一組資料,以及一個k值,然後把這些資料分為k個類別的演算法。其中k是事先需要給定的引數。每一個簇(類別)通過這個簇的中心(質心)進行描述。大概就是下面這樣子:
K-means演算法是聚類演算法中較為簡單的一種,原理簡單,易於實現。其原理大致是:首先給定k箇中心(質心),然後將資料分別劃分到k個簇中去,也就是說把每個資料分到距離其最近的那個中心所在的簇中。
然後重新計算每個簇的中心,即屬於這個簇的樣本的均值即為新的簇的中心。
具體演算法流程如下:
1.初始化k個點作為聚類中心(通常隨機選擇)
2.計算每個樣本距離k個聚類中心的距離,然後將每個樣本分到距離其最近的中心所屬的簇中;
3.重新計算k個簇的中心,中心為簇中所包含資料點的均值,;
4.重複2~3,知道k個聚類中心不再移動。
圖片來源於網路,這裡稍微整理了一下。
上面就是K-means演算法的過程,下面我們先對上面的過程做一個簡單的實現:
from numpy import * # 定義距離計算 def cal_dist(vect_a, vect_b): return sqrt(sum(power(vect_a, vect_b, 2))) # 隨機選取聚類中心 def rand_center(data, k): n = shape(data)[1] centroids = mat(zeros((k, n))) for j in range(n): min_j = min(data[:, j]) range_j = float(max(data[:, j]) - min(data[:, j])) # np.random.rand(k, 1) 生成size為(k,1)的0~1的隨機array centroids[:, j] = min_j + range_j * random.rand(k, 1) return centroids def Kmeans(data, k, dis_meas=cal_dist, create_center=rand_center): m = shape(data)[0] # 用於儲存每個樣本所屬類別的矩陣,第0維為所屬類別,第一維為樣本距離該類別的距離 clusterAssment = mat(zeros((m, 2))) # 初始化聚類中心 centroids = create_center(data, k) cluster_changed = True while cluster_changed: cluster_changed = False for i in range(m): min_dist = inf min_index = -1 for j in range(k): dist_ij = dis_meas(data[i, :], centroids[j, :]) if dist_ij < min_dist: min_dist = dist_ij min_index = j # 如果樣本的類別發生了變化,則繼續迭代 if clusterAssment[i, 0] != min_index: cluster_changed = True # 第i個樣本距離最近的中心j存入 clusterAssment[i, :] = min_index, min_dist ** 2 print(centroids) # 重新計算聚類中心 for cent in range(k): # 找出資料集中屬於第k類的樣本的所有資料,nonzero返回索引值 points_in_cluster = data[nonzero(clusterAssment[:, 0].A == cent)[0]] centroids[cent, :] = mean(points_in_cluster, axis=0) return centroids, clusterAssment
然後定義一個讀取資料的函式,使用testdata進行測試:
def loadData(filename): data_mat = [] fr = open(filename) for line in fr.readlines(): cur_line = line.strip().split('\t') flt_line = [float(example) for example in cur_line] data_mat.append(flt_line) return mat(data_mat)
data = loadData('.\testSet.txt') centroids, cluster_ass = Kmeans(data, 4, dis_meas=cal_dist, create_center=rand_center) import matplotlib.pyplot as plt data_0 = data[nonzero(cluster_ass[:, 0].A == 0)[0]] data_1 = data[nonzero(cluster_ass[:, 0].A == 1)[0]] data_2 = data[nonzero(cluster_ass[:, 0].A == 2)[0]] data_3 = data[nonzero(cluster_ass[:, 0].A == 3)[0]] plt.scatter(data_0[:, 0].A[:, 0], data_0[:, 1].A[:, 0]) plt.plot(centroids[0, 0], centroids[0, 1], '*', markersize=20) plt.scatter(data_1[:, 0].A[:, 0], data_1[:, 1].A[:, 0]) plt.plot(centroids[1, 0], centroids[1, 1], '*', markersize=20) plt.scatter(data_2[:, 0].A[:, 0], data_2[:, 1].A[:, 0]) plt.plot(centroids[2, 0], centroids[2, 1], '*', markersize=20) plt.scatter(data_3[:, 0].A[:, 0], data_3[:, 1].A[:, 0]) plt.plot(centroids[3, 0], centroids[3, 1], '*', markersize=20)
2.關於K-means演算法的問題和改進
K-means的損失函式為資料點與資料點所在的聚類中心之間的距離的平方和,也就是:
其中μ為資料點所在的類別的聚類中心,我們期望最小化損失,從而找到最佳的聚類中心和資料所屬的類別。
2.1 陷入區域性最小值問題及改進
然而,上面說到,在K-means演算法的第一步是隨機選取k個位置作為聚類中心,這可能就會導致,不同的初始位置,對最終的聚類結果有著很大的影響,比如當把k設定為2時:
第一次隨機選取的兩個聚類中心為左邊這張圖的位置,那麼結果可能分為上下兩個簇,當第二次選取的為右邊那張圖片的位置時,可能最終的聚類結果為左右兩個簇。
因此,聚類中心的初始位置,對於我們最終的結果影響很大。再比如:
上面通過K-means將資料聚為3類,但由於聚類中心的問題,導致效果不好,“+”為最終聚類中心位置,此時聚類中心已不再更新。
這是因為K-means演算法收斂到了區域性最小值,而非全域性最小值。
因此,我們需要一定的處理方法,來處理這樣的問題:
一種最直觀的做法是,我們通過多次初始化聚類中心,執行K-means聚類,得到多個結果,然後比較最後的損失(上面損失函式計算方法),選擇其中最小的那一個結果。
然而這種對於k取值比較的少的時候可以這麼做,但是如果k值過大,這樣做也不會得到較好的改善。
另一種做法是對生成的簇進行後處理,通過將具有最大的損失的那個簇,再分成兩個簇,也就是對於損失最大的那個簇再運用一次K-means演算法,將k值設為2;
但為了保證簇的總數不變,再將某兩個簇進行合併,比如上面圖中,將下面圓圈和方塊進行合併,但是如果是一個多維資料,無法進行視覺化時,我們無法檢視應該去合併哪幾個簇。
因此,可以有兩種方法去衡量:(1)將最近的聚類中心對應的類進行合併;(2)合併兩個使總的損失增加幅度最小的簇。
第一種方法是計算聚類中心(這裡已經將最大的簇又分為兩個簇)之間的距離,將最近的兩個簇進行合併;
第二種方法需要計算合併兩個簇之後的損失的大小,找出最佳的合併結果。
下面介紹一種利用上面劃分簇的技術所改善的K-means演算法。
二分K-Means演算法
二分K-means演算法是一種能夠解決演算法收斂到區域性最小值的演算法,演算法思想是:首先將所有的點作為一個簇,然後分成2個,接下來,在其中選擇一個簇進行劃分,具體選擇哪個,
要根據選擇劃分的簇能夠使總損失降低程度最大的那個,此時簇被分為3個,然後重複上述過程,直到達到所指定的k個簇即停止。
具體演算法過程如下:
1.將所有資料看做為一個簇;
2.當簇的總數目小於k時:
對於每一個簇:
(1)計算此時的損失;
(2)在此簇上採用K-means聚類(k=2)
(3)計算劃分後的損失;
選擇使劃分後損失最小的簇,將其進行劃分
上述過程中也可以選擇損失最大的那個簇進行劃分。
下面是具體實現過程:
def bi_kmeans(data, k, dist_measure=cal_dist): m = shape(data)[0] cluster_ass = mat(zeros((m, 2))) # 初始化聚類中心,此時聚類中心只有一個,因此對資料取平均 centroid0 = mean(data, axis=0).tolist()[0] # 儲存每個簇的聚類中心的列表 centList = [centroid0] for j in range(m): cluster_ass[j, 1] = dist_measure(mat(centroid0), data[j, :]) ** 2 while (len(centList)) < k: lowestSSE = inf for i in range(len(centList)): # 在當前簇中的樣本點 point_in_current_cluster = data[nonzero(cluster_ass[:, 0].A == i)[0], :] # 在當前簇運用kmeans演算法,分為兩個簇,返回簇的聚類中心和每個樣本點距離其所屬簇的中心的距離 centroid_mat, split_cluster_ass = Kmeans(point_in_current_cluster, 2, dist_measure) # 計算被劃分的簇,劃分後的損失 sse_split = sum(split_cluster_ass[:, 1]) # 計算沒有被劃分的其它簇的損失 sse_not_split = sum(cluster_ass[nonzero(cluster_ass[:, 0].A != i)[0], 1]) # 選擇最小的損失的簇,對其進行劃分 if sse_split + sse_not_split < lowestSSE: # 第i個簇被劃分 best_cent_to_split = i # 第i個簇被劃分後的聚類中心 best_new_centers = centroid_mat # 第i個簇的樣本,距離劃分後所屬的類別(只有0和1)以及距離聚類中心的距離 best_cluster_ass = split_cluster_ass lowestSSE = sse_split + sse_not_split # 把新劃分出來的簇,屬於1類的簇重新進行編號,編號為原先的總類別數目,比如原先有兩類,選擇一個進行劃分後,又分成兩類,等於1的那一類編號為2 best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 1)[0], 0] = len(centList) # 同理,屬於第0類的重新編號,編號為所選的那一類的編號 best_cluster_ass[nonzero(best_cluster_ass[:, 0].A == 0)[0], 0] = best_cent_to_split # 將原來的聚類中心進行替換 centList[best_cent_to_split] = best_new_centers[0, :] # 並加入新的1類的聚類中心 centList.append(best_new_centers[1, :]) # 將之前被選到劃分的那一類的結果全部替換成被劃分後的結果 cluster_ass[nonzero(cluster_ass[:, 0].A == best_cent_to_split[0]), :] = best_cluster_ass return mat(centList), cluster_ass
2.2 K值的影響及其改進
K-means演算法的K值需要人為給出所需要聚類的類別數目k,那麼k的選擇對於結果影響很大,通常k值的選擇通常需要根據實際進行手動選擇,比如給定一組身高和體重的資料,我們可以聚成三類(S、M、L)來指導生產。
還有一種方法用於選擇k值的方法,成為“手肘法則”(elbow method),其原理很簡單,就是通過設定不同的k值,然後畫出每一個k值對應的損失,如圖所示:
然後找出“手肘”的位置,就是最佳的聚類數目k。
這裡有個問題,損失不應該是越小越好嗎?理論上是這樣的,但當我們把n個資料點聚成n個類別,此時損失為0,然而這並沒有什麼意義(類似於過擬合)。
因此我們選擇“手肘”的位置,此時損失下降相較於後面下降速度較大,即k=3。
具體的實現這裡不再說了,就是計算不同k值,然後計算Loss損失就可以了。這裡補充一個關於能夠自動選擇k值的庫:yellowbrick,程式碼很簡單(參考https://www.zhihu.com/question/279825061/answer/1686762604):
from sklearn.cluster import KMeans from yellowbrick.cluster.elbow import kelbow_visualizer from yellowbrick.datasets.loaders import load_nfl x, y = load_nfl() kelbow_visualizer(KMeans(random_state=4), x, k=(2, 10))
可以自動選出k值(k=4),而且畫出的圖也很好看。
上面就是K-means演算法的基本內容,由於演算法比較簡單,內容不多,而且sklearn也帶有Kmeans的工具包(上面那個例子裡就是)。總的來說雖然K-means演算法比較簡單,但是用途還是比較廣泛的。
最近事情比較多,學習進度有點慢~,後面會繼續針對聚類演算法做總結和學習,下一次主要對層次聚類和DBSCAN進行一個回顧和總結。