聚類算法K-Means原理及 Python 實現

聚類

一、聚類任務

在無監督的學習中,訓練樣本的標記信息是未知的,目標是通過對無標記訓練樣本的學習來揭露數據的內在性質及規律,爲進一步的數據的分析提供基礎,此類學習任務中研究最多、應用最廣泛的是聚類。

聚類試圖將數據集中的樣本劃分爲若干個通常是不相交的子集,每個子集稱爲一個簇,通過這樣的劃分,每個簇可能對應於一些潛在的類別。聚類過程僅能自動形成簇結構,簇對應的概念語義需由使用者來把握和命名。

下面討論聚類算法涉及的兩個基本問題——性能度量和距離計算

二、性能度量

性能度量也稱爲聚類的 “有效性指標”,在聚類結果中,我們需通過某種性能度量來評估其好壞,另一方面,若明確了最終將要使用的性能度量,則可直接將其作爲聚類過程的優化目標,從而得到符合要求的聚類結果。聚類將樣本集 D 劃分爲若干互不相交的子集,即樣本簇,我們希望同一簇的樣本儘可能彼此相似,不同簇的樣本儘可能不同。

聚類性能指標大致分爲兩類——外部指標、內部指標

1.外部指標

指將聚類結果與某個 “參考模型” 進行比較。

則外部指標主要有:

  • Jaccard 係數(簡稱 JC )

                                              JC=\frac{a}{a+b+c}

  • FM 指數(簡稱 FMI )

                                           FMI=\sqrt{\frac{a}{a+b}\cdot \frac{a}{a+c}}

  • Rand 指數(簡稱 RI )

                                                RI=\frac{2(a+b)}{m(m-1)}

上述性能度量的結果值均在 [0,1] 區間,值越大越好。

2.內部指標

則內部指標主要有:

  • DB 指數(簡稱 DBI )

                                           

  • Dunn 指數(簡稱 DI )

                             

DBI 的值越小越好,DI 的值越大越好。

三、距離計算

距離度量需要滿足一些基本性質:

  • 非負性
  • 同一性(兩點相同時,距離爲0)
  • 對稱性
  • 直遞性(也稱爲三角不等式)

我們常將屬性劃分爲 ”連續屬性“ 和 ”離散屬性“,前者在定義域有無窮多個取值,後者在定義域有有限個取值。在討論距離計算時,可以再屬性上計算距離的爲 ”有序屬性“,例如定義域 {1,2,3} 的離散屬性和連續屬性的性質更接近一些,可以直接計算。而定義域 {飛機、火車、輪船} 這樣的離散屬性不能直接計算距離,稱爲 ”無序屬性“。 

 常用的有序屬性:

  • 閔可夫斯基距離

                                                   

  • p=2 時,歐氏距離

                                                  dist_{ed}(x_{i},x_{j})=\left \| x_{i}-x_{j} \right \|_{2}=\sqrt{\sum_{u=1}^{n}\left | x_{iu}-x_{ju} \right |^{2}}

  • p=1 時,曼哈頓距離

                                                dist_{man}(x_{i},x_{j})=\left \| x_{i}-x_{j} \right \|_{1}=\sum_{u=1}^{n}\left | x_{iu}-x_{ju} \right |

常用的無序屬性:

  • m_{u,a} 表示在屬性 u 上取值爲 a 的樣本數,m_{u,a,i} 表示在第 i 個樣本簇中在屬性 u 上取值爲 a 的樣本數,k 爲樣本簇數,則屬性u上兩個離散值 a 與 b 的 VDM 距離爲:

                                                         

  • 將閔可夫斯基距離和 VDM 結合即可處理混合屬性,假定有 n_{c} 個有序屬性,n-n_{c} 個無序屬性,不失一般性,令有序屬性排在無序屬性之前,則:

                                        

  • 當樣本空間中不同屬性的重要性不同時, 可使用 ” 加權距離“ ,以加權閔夫斯基距離爲例:

                                                    dist_{wmk}\left ( x_{i},x_{j} \right )=(w_{1}\cdot \left | x_{i1}-x_{j1} \right |^{p}+...+w_{n}\cdot \left | x_{in}-x_{jn} \right |^{p})^{\frac{1}{p}}

K-Means算法 

一、K-Means 算法的基本原理及步驟

K-Means 算法是基於數據劃分的無監督聚類算法, 首先定義常數 k , 常數 k 表示最終的聚類的類別數。 在確定類別數 k 後, 隨機初始化 k 個類的聚類中心, 通過計算每一個樣本與聚類中心之間的相似度, 將樣本劃分到相似的類別中。

K-Means 算法的步驟:

1. 初始化常數k, 隨機初始化 k 個聚類中心;

2. 重複計算以下過程, 直到聚類中心不再改變; 

  • 計算每個樣本與每個聚類中心之間的相似度, 將樣本劃分到最相似的類別中;
  • 計算劃分到每個類別中的所用樣本特徵的均值, 並將該均值作爲每個類新的聚類中心。

3. 輸出最終的聚類中心以及每個樣本所屬的類別。

二、K-Means 算法與矩陣分解

在 K-Means 算法中,假設訓練數據集 X 有 m 個樣本 \left \{ X^{(1)},X^{(2)} ,...,X^{(m)}\right \},其中,每一個樣本 Xi 爲 n 維的向量。此時的樣本爲一個 m×n 的矩陣:

                                  

k-Means 算法通過歐式距離的度量方法計算每一個樣本 Xi 到質心之間的距離,並將其劃分到較近的質心所屬的類別中並重新計算質心,重複以上的過程,直到質心不再改變爲止。

K-Means 算法的目標是使每一個樣本 Xi 被劃分到最相似的類別中,利用每個類別中的樣本重新計算聚類中心 ui:

證明如下:

三、K-Means 算法的實踐

# -*- coding: utf-8 -*-
"""
Created on Wed Mar 27 16:18:21 2019

@author: 2018061801
"""
import numpy as np

def load_data(file_path):
    '''導入數據
    input:  file_path(string):文件的存儲位置
    output: data(mat):數據
    '''
    f = open(file_path)
    data = []
    for line in f.readlines():
        row = []  # 記錄每一行
        lines = line.strip().split("\t")
        for x in lines:
            row.append(float(x)) # 將文本中的特徵轉換成浮點數
        data.append(row)
    f.close()
    return np.mat(data)

def distance(vecA, vecB):
    '''計算vecA與vecB之間的歐式距離的平方
    input:  vecA(mat)A點座標
            vecB(mat)B點座標
    output: dist[0, 0](float)A點與B點距離的平方
    '''
    dist = (vecA - vecB) * (vecA - vecB).T
    return dist[0, 0]

def randCent(data, k):
    '''隨機初始化聚類中心
    input:  data(mat):訓練數據
            k(int):類別個數
    output: centroids(mat):聚類中心
    '''
    n = np.shape(data)[1]  # 屬性的個數
    centroids = np.mat(np.zeros((k, n)))  # 初始化k個聚類中心
    for j in range(n):  # 初始化聚類中心每一維的座標
        minJ = np.min(data[:, j])
        rangeJ = np.max(data[:, j]) - minJ
        # 在最大值和最小值之間隨機初始化
        centroids[:, j] = minJ * np.mat(np.ones((k , 1))) \
                        + np.random.rand(k, 1) * rangeJ

        #計算隨機值的公式:c=min+rand(0,1)*(max-min)
    return centroids
 
def kmeans(data, k, centroids):
    '''根據KMeans算法求解聚類中心
    input:  data(mat):訓練數據
            k(int):類別個數
            centroids(mat):隨機初始化的聚類中心
    output: centroids(mat):訓練完成的聚類中心
            subCenter(mat):每一個樣本所屬的類別
    '''
    m, n = np.shape(data) # m:樣本的個數,n:特徵的維度
    subCenter = np.mat(np.zeros((m, 2)))  # 初始化每一個樣本所屬的類別
    change = True  # 判斷是否需要重新計算聚類中心
    while change == True:
        change = False  # 重置
        for i in range(m):
            minDist = np.inf  # 設置樣本與聚類中心之間的最小的距離,初始值爲正無窮
            minIndex = 0  # 所屬的類別
            for j in range(k):
                # 計算i和每個聚類中心之間的距離
                dist = distance(data[i, ], centroids[j, ])
                if dist < minDist:
                    minDist = dist
                    minIndex = j
            # 判斷是否需要改變
            if subCenter[i, 0] < minIndex:  # 需要改變
                change = True
                subCenter[i, ] = np.mat([minIndex, minDist])
        # 重新計算聚類中心
        for j in range(k):
            sum_all = np.mat(np.zeros((1, n)))
            r = 0  # 每個類別中的樣本的個數
            for i in range(m):
                if subCenter[i, 0] == j:  # 計算第j個類別
                    sum_all += data[i, ]
                    r += 1
            for z in range(n):
                try:
                    centroids[j, z] = sum_all[0, z] / r
                except:
                    print (" r is zero")   
    return subCenter

def save_result(file_name, source):
    '''保存source中的結果到file_name文件中
    input:  file_name(string):文件名
            source(mat):需要保存的數據
    output: 
    '''
    m, n = np.shape(source)
    f = open(file_name, "w")
    for i in range(m):
        tmp = []
        for j in range(n):
            tmp.append(str(source[i, j]))
        f.write("\t".join(tmp) + "\n")
    f.close()
   
if __name__ == "__main__":
    k = 4  # 聚類中心的個數
    file_path = "D:/anaconda4.3/spyder_work/data4.txt"
    # 1、導入數據
    print ("---------- 1.load data ------------")
    data = load_data(file_path)
    # 2、隨機初始化k個聚類中心  
    print ("---------- 2.random center ------------")
    centroids = randCent(data, k)
    # 3、聚類計算  
    print ("---------- 3.kmeans ------------")
    subCenter = kmeans(data, k, centroids)  
    # 4、保存所屬的類別文件
    print ("---------- 4.save subCenter ------------")
    save_result("sub", subCenter)
    # 5、保存聚類中心
    print ("---------- 5.save centroids ------------")
    save_result("center", centroids) 

預測數據二維,80個樣本,4類:

-2.29693419524775	5.59018489409611
-1.65945644047067	1.24998175001933
-7.11109228594546	6.94390514108144
-1.60636900702686	7.53795273430285
-3.57348527642213	5.75114608400441
-7.31721716500413	6.30418091404833
-6.05051246793066	6.20192727687441
-4.17182936556511	3.74558913673918
-1.29745215195992	5.58834523124290
-1.24578025360506	2.19830681468093
-6.89670842825716	5.94232261613726
-1.20585052767569	1.22282992464194
-1.29983136229938	2.93846089472623
-4.60237045894011	1.32319973441808
-2.39803671777840	1.67992246865093
-7.00679562960949	6.76420479829105
-5.04767102161608	5.86380036083072
-1.58985132367653	3.21969636042602
-2.45454869308312	7.65155434186849
-1.28355301524968	1.24112256352036
4.07121051759479	6.25886941513957
3.67090919965106	2.78566580821488
6.35861751704302	4.54169936165600
6.56639930795944	5.89353705859680
2.30810823188065	7.23632276775059
4.42835077051762	7.71503997643811
4.11910340497630	4.83050870974662
5.52419107077885	1.97037109980075
5.96555381600651	2.04505803891340
6.28280677387653	2.80255777886616
2.93217553899005	6.88502079188564
5.75791873797572	2.77997525280072
5.58568602781689	6.69999378248171
2.13828214636241	2.70467478107493
1.83298377090864	7.50484536231059
4.48854836387500	3.44988636189366
7.71820770961257	2.37616675301846
3.38270008666293	2.75758700583222
5.09687425685844	5.31231273302647
2.56668357643796	4.31302194231911
-5.53838345055902	-6.86472384264730
-2.18419960472596	-2.44000821521265
-3.90315136193093	-5.82149470568637
-4.15193474196202	-4.30026805145651
-1.57964435319133	-6.84045889350153
-5.99912686825739	-3.78612641018854
-2.69959839622495	-6.15920100821899
-2.72389634005053	-3.42144631066252
-5.33687907117250	-3.17549847801995
-4.02524851492345	-2.76293885023403
-7.46901997305856	-4.84620881048252
-7.62234916933375	-7.41325035402147
-4.28441712893719	-6.39716121898227
-2.54582938928592	-1.60663846948831
-1.46192521039572	-6.93335386721544
-7.09065654068389	-2.21928115757317
-4.01823437389465	-4.23160295317960
-4.71426551259256	-1.02705698361180
-7.91668551349131	-7.45277129872771
-5.64014148920783	-4.90125211157188
1.74656939126409	-2.02878217594675
7.73328656598538	-3.64561407960454
1.03243956893847	-5.54333333375410
6.42437325298052	-4.40725322093063
6.72112254457403	-5.18734376373641
7.08086293754457	-7.46823315816411
1.59105091857637	-6.32058692512439
3.79847854369228	-7.13676745615384
2.81909281995458	-6.71264548202308
6.60047936157015	-6.32033232034568
4.01989679224481	-5.07913051640941
7.37453316100666	-7.65241898771981
2.27292919811997	-1.68098723059303
2.84662041565393	-1.38648967194848
2.01877286269302	-4.56395135272344
1.95247991096065	-4.57523153119987
7.08504545348063	-5.63596413125036
5.05793211155899	-1.69962307507637
4.84902141285432	-5.41527253215850
2.01468358756609	-7.22158071294349

結果:

---------- 1.load data ------------
---------- 2.random center ------------
---------- 3.kmeans ------------
---------- 4.save subCenter ------------
---------- 5.save centroids ------------

四個聚類中心的具體值:

A:(-5.232158745136095,-5.3149499495898445)
B:(3.714649607552847,4.85395509083394)
C:(5.257939367323595,-4.624586594288424)
D:(-2.4477179599509813,0.5190348172066764)

使用python 顯示聚類結果:

from sklearn.cluster import KMeans
import numpy
import matplotlib.pyplot as plt  

print('---------- 1.load data ------------') 
#讀取數據並存儲到dataSet中
dataSet = []
fileIn = open('D:/anaconda4.3/spyder_work/data4.txt')
for line in fileIn.readlines():  
    lineArr = line.strip().split()
    dataSet.append('%0.6f' % float(lineArr[0]))
    dataSet.append('%0.6f' % float(lineArr[1]))

print('---------- 2.clustering ------------')

#調用sklearn.cluster中的KMeans類
dataSet = numpy.array(dataSet).reshape(80,2)
kmeans = KMeans(n_clusters=4, random_state=0).fit(dataSet)

#求出聚類中心
center=kmeans.cluster_centers_
center_x=[]
center_y=[]
for i in range(len(center)):
    center_x.append('%0.6f' % center[i][0])
    center_y.append('%0.6f' % center[i][1])

#標註每個點的聚類結果
labels=kmeans.labels_
type1_x = []
type1_y = []
type2_x = []
type2_y = []
type3_x = []
type3_y = []
type4_x = []
type4_y = []
for i in range(len(labels)):
    if labels[i] == 0:
        type1_x.append(dataSet[i][0])
        type1_y.append(dataSet[i][1])
    if labels[i] == 1:
        type2_x.append(dataSet[i][0])
        type2_y.append(dataSet[i][1])
    if labels[i] == 2:
        type3_x.append(dataSet[i][0])
        type3_y.append(dataSet[i][1])
    if labels[i] == 3:
        type4_x.append(dataSet[i][0])
        type4_y.append(dataSet[i][1])

#畫出四類數據點及聚類中心
plt.figure(figsize=(10,8), dpi=80,facecolor='gray')
axes = plt.subplot(111)
type1 = axes.scatter(type1_x, type1_y, s=30, c='red')
type2 = axes.scatter(type2_x, type2_y, s=30, c='green')
type3 = axes.scatter(type3_x, type3_y,s=30, c='pink' )
type4 = axes.scatter(type4_x, type4_y, s=30, c='yellow')
type_center = axes.scatter(center_x, center_y, s=40, c='blue',marker='*')
plt.xlabel('x')
plt.ylabel('y')
plt.title("KMeans")

axes.legend((type1, type2, type3, type4,type_center), ('0','1','2','3','center'),loc=2)
plt.show()  

聚類結果如下圖所示:

對比結果圖:

K-Means++ 算法

一、K-Means算法存在的問題

K-Means算法中的聚類的個數 k 需要事先指定,這一點對於未知數據存在很大的侷限,其次,在利用K-Means算法進行聚類之前,需要初始化 k 個聚類中心,在上述的K-Means算法的過程中,使用的是在數據集中隨機選擇最大值和最小值之間的數作爲其初始化的聚類中心,但在聚類中心選擇不好時,對於K-Means算法有很大的影響。

爲了解決K-Means算法帶來的問題,K-Means++算法被提出。K-Means++算法主要是爲了能夠在聚類中心的選擇過程中選擇較優的聚類中心。

二、K-Means++算法的基本思想

K-Means++算法在聚類中心的初始化過程中的基本原則是使得初始的聚類中心之間的相互距離儘可能遠,K-Means++算法的初始化過程如下:

  1. 在數據集隨機選擇一個樣本點作爲第一個初始化的聚類中心;
  2. 選擇出其餘的聚類中心:
  • 計算樣本中的每一個樣本點與已經初始化的聚類中心之間的距離,並選擇其最短的距離,記爲 d_{i}
  • 以概率選擇距離最大的樣本作爲新的聚類中心,重複上述過程,直到 k 個聚類中心都被確定。

    3.  對 k 個初始化的聚類中心,利用K-Means算法計算最終的聚類中心。

三、K-Means++算法的實現

import numpy as np
from random import random
from KMeans import load_data, kmeans, distance, save_result

FLOAT_MAX = 1e100 # 設置一個較大的值作爲初始化的最小的距離

def nearest(point, cluster_centers):
    '''計算point和cluster_centers之間的最小距離
    input:  point(mat):當前的樣本點
            cluster_centers(mat):當前已經初始化的聚類中心
    output: min_dist(float):點point和當前的聚類中心之間的最短距離
    '''
    min_dist = FLOAT_MAX
    m = np.shape(cluster_centers)[0]  # 當前已經初始化的聚類中心的個數
    for i in range(m):
        # 計算point與每個聚類中心之間的距離
        d = distance(point, cluster_centers[i, ])
        # 選擇最短距離
        if min_dist > d:
            min_dist = d
    return min_dist

def get_centroids(points, k):
    '''KMeans++的初始化聚類中心的方法
    input:  points(mat):樣本
            k(int):聚類中心的個數
    output: cluster_centers(mat):初始化後的聚類中心
    '''
    m, n = np.shape(points)
    cluster_centers = np.mat(np.zeros((k , n)))
    # 1、隨機選擇一個樣本點爲第一個聚類中心
    index = np.random.randint(0, m)
    cluster_centers[0, ] = np.copy(points[index, ])
    # 2、初始化一個距離的序列
    d = [0.0 for _ in range(m)]
 
    for i in range(1, k):
        sum_all = 0
        for j in range(m):
            # 3、對每一個樣本找到最近的聚類中心點
            d[j] = nearest(points[j, ], cluster_centers[0:i, ])
            # 4、將所有的最短距離相加
            sum_all += d[j]
        # 5、取得sum_all之間的隨機值
        sum_all *= random()
        # 6、獲得距離最遠的樣本點作爲聚類中心點
        for j, di in enumerate(d):
            sum_all -= di
            if sum_all > 0:
                continue
            cluster_centers[i] = np.copy(points[j, ])
            break
    return cluster_centers

if __name__ == "__main__":
    k = 4#聚類中心的個數
    file_path = "D:/anaconda4.3/spyder_work/data4.txt"
    # 1、導入數據
    print ("---------- 1.load data ------------")
    data = load_data(file_path)
    # 2、KMeans++的聚類中心初始化方法
    print ("---------- 2.K-Means++ generate centers ------------")
    centroids = get_centroids(data, k)
    # 3、聚類計算
    print ("---------- 3.kmeans ------------")
    subCenter = kmeans(data, k, centroids)
    # 4、保存所屬的類別文件
    print ("---------- 4.save subCenter ------------")
    save_result("sub_pp", subCenter)
    # 5、保存聚類中心
    print ("---------- 5.save centroids ------------")
    save_result("center_pp", centroids)

結果:

---------- 1.load data ------------
---------- 2.K-Means++ generate centers ------------
---------- 3.kmeans ------------
---------- 4.save subCenter ------------
---------- 5.save centroids ------------

訓練數據

 

參考文獻:趙志勇《python 機器學習算法》(原理+程序)

周志華《機器學習》(原理)
 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章