無監督學習 k-means算法

一、無監督學習

無監督學習是機器學習算法中的一種。監督學習的目的主要是對數據進行分類和迴歸預測,它主要是通過已知推測未知,大部分監督學習算法有一個訓練模型的過程;相對於監督學習,無監督學習則是主要着重於數據的分佈特點,與有監督學習不同,無監督學習並沒有訓練的過程。

二、 聚類

針對給定的樣本數據,聚類算法會根據它們的特徵相似度或距離,把相似的數據劃分爲若干個簇中。相似的樣本劃分到相同的簇中, 不相似的樣本劃分到不同的簇。聚類處理數據的過程也是從未知到已知的過程,因爲聚類算法在事先並不知道聚類得到的簇的特點和樣本的分佈。

k-means屬於無監督學習中聚類算法的一種。

三、 k均值聚類算法

3.1 k-means

k均值聚類算法將樣本集劃分爲k個簇,把每個樣本數據劃分到距離最近的簇中,且每個樣本僅屬於一個類,此爲k均值聚類算法。k-mean算法屬於硬聚類,即每個樣本只能屬於一個簇。

聚合聚類需要預先考慮以下幾個要素:

  1. 距離或相似度;
  2. 合併規則;
  3. 停止條件。

3.2 步驟

  1. 隨機選擇k個簇作爲質心(需要保證這k個簇的特徵值在樣本集特徵值的範圍之內);
  2. 把n個樣本點劃分到距離最近的簇中;
  3. 計算每個簇的平均值。
  4. 判斷是否收斂,如果沒有收斂,則從步驟2開始。

 

圖1 k-means流程圖

 

圖2 k均值聚類算法 迭代過程

通過圖2可以瞭解到,由於第三張圖和第四張圖未發生變化,則表示此時的簇已經收斂。

3.3 實現

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


def load_data_set(filename):
    data_matrix = []
    with open(filename) as fp:
        for line in fp.readlines():
            # 多個浮點型的列
            data = line.strip().split('\t')
            float_data = [float(datum) for datum in data]
            data_matrix.append(float_data)
    return data_matrix

 load_data_set函數用於將文本文件導入成一個列表中。

def euclidean_distance(vec_a, vec_b):
    """計算兩個向量的歐式距離"""
    return sqrt(sum(power(vec_a - vec_b, 2)))

 距離的計算有很多種選擇,比如閔可夫斯基距離、馬氏距離等,不同的距離選擇會使得計算頁會有所不同。這裏選擇的是歐氏距離,即各個特徵差值的平方再開方(勾股定理的計算就用到了歐式距離),馬氏距離是歐式距離的推廣

def random_centroids(data_set, k):
    """
    返回k個隨機的質心 保證這些質心隨機並且不超過整個數據集的邊界
    :param data_set: 數據集
    :param k: 質心的數量
    :return: k個隨機的質心
    """
    # 創建一個k行n列的矩陣,用於保存隨機質心
    n = shape(data_set)[1]
    centroids = mat(zeros((k, n)))
    # 對n個特徵進行遍歷
    for j in range(n):
        minJ = min(data_set[:, j])
        rangeJ = float(max(data_set[:, j]) - minJ)
        centroids[:, j] = minJ + rangeJ * random.rand(k, 1)
    return centroids

 random_centroids的作用就是隨機創建k個質心。data_set[:, j]獲取的是第j列的所有元素,這種用法是numpy對__getitem__()即切片方法的重寫。

def kMeans(data_set, k, distance_measure=euclidean_distance, create_centroids=random_centroids):
    """
    k means 算法
    :param data_set: 數據集
    :param k: k means算法的k 即要生成幾個簇
    :param distance_measure: 計算距離函數
    :param create_centroids: 創建質心的函數
    :return:
    """
    m = shape(data_set)[0]
    # 向量分配到某一個(簇索引值,誤差)
    cluster_assment = mat(zeros((m, 2)))
    # 創建k個質心
    centroids = create_centroids(data_set, k)
    cluster_changed = True

    while cluster_changed:
        cluster_changed = False
        # 計算該點離哪個質心最近
        for i in range(m):
            min_index, min_dist = -1, inf
            # 遍歷k個質心 獲取一個最近的質心
            for j in range(k):
                # 計算該點和質心j的距離
                distJI = distance_measure(centroids[j, :], data_set[i, :])
                if distJI < min_dist:
                    min_dist, min_index = distJI, j
            # 分配質心索引發生了變化 則仍然需要迭代
            if cluster_assment[i, 0] != min_index:
                cluster_changed = True
            # 不斷更新最小值
            cluster_assment[i, :] = min_index, min_dist ** 2
        print(centroids)
        # 更新質心
        for cent in range(k):
            # 獲取屬於該簇的所有點
            ptsInClust = data_set[nonzero(cluster_assment[:, 0].A==cent)[0]]
            # 按矩陣的列進行均值計算
            centroids[cent, :] = mean(ptsInClust, axis=0)
        # 顯示每一次迭代後的簇的情況
        # show_image(data_set, centroids, cluster_assment)
    return centroids, cluster_assment

 kMeans函數就是之前流程圖的實現,它會迭代到簇收斂爲止。

def show_image(data_set, centroids, clustAssing):
    colors = 'bgrcmykb'
    markers = 'osDv^p*+'
    for index in range(len(clustAssing)):
        datum = data_set[index]
        j = int(clustAssing[index, 0])
        flag = markers[j] + colors[j]
        plt.plot(datum[:, 0], datum[:, 1], flag)
    # 質心
    plt.plot(centroids[:, 0], centroids[:, 1], '+k')

 show_image函數用於點的分類的顯示,它目前最多顯示4個簇,過多的時候需要再sign和color上添加;除此之外,這個函數目前只能顯示兩個特徵值,當有多個特徵值的時候,可以考慮進行降維處理。

接着是主函數和數據:

if __name__ == '__main__':
    data_mat = mat(load_data_set('testSet.txt'))
    centroids, clustAssing = kMeans(data_mat, 4)
    print('----')
    # print(clustAssing)
    show_image(data_mat, centroids, clustAssing)
    plt.show()

 數據集:dataSet.txt

把上述數據保存爲dataSet.txt和之前的kMeans.py放入同一文件夾下運行即可得到圖 2所示的收斂圖。

3.4 sklearn實現

python有着完整的機器學習的庫,通過調用這些庫,可以在很大程度上減少代碼的編寫和錯誤率。

"""
使用sklearn提供的K均值聚類算法
"""
import numpy
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

from kMeans import load_data_set


def show_image(data_set, centroids, cluster_centers):
    colors = 'bgrcmykb'
    markers = 'osDv^p*+'
    for index in range(len(cluster_centers)):
        datum = data_set[index]
        j = int(cluster_centers[index])
        flag = markers[j] + colors[j]
        plt.plot(datum[:, 0], datum[:, 1], flag)
    # 質心
    plt.plot(centroids[:, 0], centroids[:, 1], '+k')


if __name__ == '__main__':
    data_set = numpy.mat(load_data_set('testSet.txt'))
    kmeans_model = KMeans(n_clusters=4).fit(data_set)
    # 顯示模型
    show_image(data_set, kmeans_model.cluster_centers_, kmeans_model.labels_)
    plt.show()

運行結果如下:

圖3 K均值聚類算法運行結果

 

四、二分K-均值算法

把數據集切換爲dataSet2.txt後,再次運行之前的kMeans算法可能會得到下圖的結果:

圖4 質心的隨機性導致了k-均值算法效果不好

k均值聚類算法能夠保證收斂局部最優性,它的聚類效果在很大程度上依賴於隨機質心的選擇,這會造成算法收斂但聚類效果卻較爲一般。二分K-均值算法可以避免此類問題。

一種用於度量聚類效果的指標是SSE(Sum of Square Error, 誤差平方和),它等於所有的樣本點到對應的簇的距離的平方的和,也就是上面的kMeans函數內部cluster_assment的第一列的和(用代碼實現就是sum(cluster_assment[:, 1]))。SSE越小表示數據點越接近於它們的質心,聚類效果也就越好。

4.1 思路

該算法首先將所有的樣本點作爲一個簇,然後將該簇一分爲二;之後選擇其中一個可以在最大程度上降低SSE值的簇進行劃分,直到滿足停止條件爲止。這裏的停止條件是劃分的簇的數量達到了k後則停止劃分。

  1. 把所有的點作爲一個簇;
  2. 判斷當前簇的數量是否大於等於k,如果是則直接退出,否則向下執行;
  3. 對於每一個簇,在給定的簇上進行K-均值聚類(k=2),並計算將該簇一分爲二之後的總誤差;
  4. 選擇使得誤差最小的那個簇進行劃分操作;跳轉到步驟2。
圖5 二分K-均值算法

4.2 代碼實現

def binary_kmeans(data_set, k, distance_measure=euclidean_distance):
    """
    二分 K-均值算法
    :param data_set: 樣本集
    :param k: 要劃分的簇的數量
    :param distance_measure: 距離計算函數
    :return: 返回同kMeans()函數
    """
    m = shape(data_set)[0]
    cluster_assment = mat(zeros((m, 2)))
    # 按照列求平均數並轉換成列表
    centroid0 = mean(data_set, axis=0).tolist()[0]
    # 劃分的簇
    cent_list = [centroid0]
    # 計算點當前簇的誤差
    for j in range(m):
        cluster_assment[j, 1] = distance_measure(mat(centroid0), data_set[j, :]) ** 2
    # 劃分的簇小於選定值時 繼續劃分
    while len(cent_list) < k:
        lowestSSE = inf
        # 找到一個劃分後SSE最小的簇
        for i in range(len(cent_list)):
            # 獲取該簇的所有數據
            ptsInCurrCluster = data_set[nonzero(cluster_assment[:, 0].A == i)[0], :]
            # 經過劃分 一個簇得到編號分別爲0和1的兩個簇
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distance_measure)
            # 計算分簇之後的Sum of Square Error
            sseSplit = sum(splitClustAss[:, 1])
            sseNotSplit = sum(cluster_assment[nonzero(cluster_assment[:, 0].A != i)[0], 1])

            print('sseSplit, and not split', sseSplit, sseNotSplit)
            if sseSplit + sseNotSplit < lowestSSE:
                bestCentToSplit = i
                bestNewCents = centroidMat
                bestClustAss = splitClustAss.copy()
                lowestSSE = sseSplit + sseNotSplit
        # 重新編排編號
        bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(cent_list)
        bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit

        print('the bestCentToSplit is:', bestCentToSplit)
        print('the len of bestClustAss is', len(bestClustAss))
        cent_list[bestCentToSplit] = bestNewCents.tolist()[0]
        cent_list.append(bestNewCents.tolist()[1])
        cluster_assment[nonzero(cluster_assment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
        # 顯示每一次迭代後的簇的情況
        show_image(data_set, mat(cent_list), cluster_assment)

    return mat(cent_list), cluster_assment

在二分K-均值算法中主要有兩個循環,第一重循環用於確定簇的個數,第二重循環用於選定一個能使得SSE降到最低的劃分。在劃分完成後,一個簇被劃分成了兩個簇,需要對這個簇進行一些後續的操作:編排序號(序號是由k均值聚類算法給定的)和添加到簇列表中。

 二分K-均值算法可以很好地解決k均值聚類算法的問題,不過目前的k值仍然需要預先給定。

 

參考:

  • 《機器學習實戰》
  • 《統計學習方法》
  • 《Python 機器學習及實踐》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章