小白都能瞭解的聚類算法之一(Kmeans與GMM)

1.標準Kmeans

經典的標準kmeans算法無需多言,每個無監督學習的開場白一般都是標準kmeans算法。具體的原理不再多言,可以參考之前的文章:
https://blog.csdn.net/bitcarmanlee/article/details/52092288

標準的kmeans的優缺點,上面的文章也有詳細介紹,再詳細說一說kmeans++對於初始中心點的優化

kmeans++中心點初始化步驟
在這裏插入圖片描述
下面舉個例子來說明怎麼優化初始點。
在這裏插入圖片描述
數據集中共有8個樣本,分佈以及對應序號如圖所示。
假設經過圖2的步驟一後6號點被選擇爲第一個初始聚類中心,那在進行步驟二時每個樣本的D(x)和被選擇爲第二個聚類中心的概率如下表所示:
在這裏插入圖片描述
其中的P(x)就是每個樣本被選爲下一個聚類中心的概率。最後一行的Sum是概率P(x)的累加和,用於輪盤法選擇出第二個聚類中心。方法是隨機產生出一個0~1之間的隨機數,判斷它屬於哪個區間,那麼該區間對應的序號就是被選擇出來的第二個聚類中心了。例如1號點的區間爲[0,0.2),2號點的區間爲[0.2, 0.525)。

從上表可以直觀的看到第二個初始聚類中心是1號,2號,3號,4號中的一個的概率爲0.9。而這4個點正好是離第一個初始聚類中心6號點較遠的四個點。這也驗證了K-means的改進思想:即離當前已有聚類中心較遠的點有更大的概率被選爲下一個聚類中心。可以看到,該例的K值取2是比較合適的。當K值大於2時,每個樣本會有多個距離,需要取最小的那個距離作爲D(x)。
(kmeans++優化中心點的例子來自參考文獻1)

2.kmeans python實現

用python實現一個kmeans的例子。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

#Author: WangLei
#date: 2020/3/19

import numpy as np

class KMeansClassifier():

    def __init__(self, k=3, init_cent='random', max_iter=500):
        self._k = k
        self._init_cent = init_cent
        self._max_iter = max_iter
        self._clusterAssment = None
        self._labels = None
        self._sse = None

    def _cal_edist(self, arrA, arrB):
        return np.math.sqrt(sum(np.power(arrA - arrB, 2)))

    def _cal_mdist(self, arrA, arrB):
        return sum(np.abs(arrA, arrB))


    def _rand_cent(self, data_X, k):
        n = data_X.shape[1] # 特徵維度
        centroids = np.empty((k, n)) #使用numpy生成一個k*n的矩陣,用於存儲質心
        for j in range(n):
            minJ = min(data_X[:, j])
            rangeJ = float(max(data_X[:, j]) - minJ)
            centroids[:, j] = (minJ + rangeJ * np.random.rand(k, 1)).flatten()

        return centroids

    def fit(self, data_X):
        if not isinstance(data_X, np.ndarray) or isinstance(data_X, np.matrixlib.defmatrix.matrix):
            try:
                data_X = np.asarray(data_X)
            except:
                raise TypeError("numpy.ndarray resuired for data_X")

        m = data_X.shape[0] #獲取樣本的個數
        self._clusterAssment = np.zeros((m, 2))

        if self._init_cent == 'random':
            self._centroids = self._rand_cent(data_X, self._k)

        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._k):
                    arrA = self._centroids[j,:]
                    arrB = data_X[i,:]
                    distJI = self._cal_edist(arrA, arrB)
                    if distJI < minDist:
                        minDist = distJI
                        minIndex = j
                if self._clusterAssment[i, 0] != minIndex or self._clusterAssment[i, 1] > minDist ** 2:
                    clusterChanged = True
                    self._clusterAssment[i, :] = minIndex, minDist ** 2

            if not clusterChanged:#若所有樣本點所屬的族都不改變,則已收斂,結束迭代
                break

            for i in range(self._k):  # 更新質心,將每個族中的點的均值作爲質心
                index_all = self._clusterAssment[:, 0]  # 取出樣本所屬簇的索引值
                value = np.nonzero(index_all == i)  # 取出所有屬於第i個簇的索引值
                ptsInClust = data_X[value[0]]  # 取出屬於第i個簇的所有樣本點
                self._centroids[i, :] = np.mean(ptsInClust, axis=0)  # 計算均值

        self._labels = self._clusterAssment[:, 0]
        self._sse = sum(self._clusterAssment[:, 1])

    def predict(self, X):  # 根據聚類結果,預測新輸入數據所屬的族
        # 類型檢查
        if not isinstance(X, np.ndarray):
            try:
                X = np.asarray(X)
            except:
                raise TypeError("numpy.ndarray required for X")

        m = X.shape[0]  # m代表樣本數量
        preds = np.empty((m,))
        for i in range(m):  # 將每個樣本點分配到離它最近的質心所屬的族
            minDist = np.inf
            for j in range(self._k):
                distJI = self._calEDist(self._centroids[j, :], X[i, :])
                if distJI < minDist:
                    minDist = distJI
                    preds[i] = j
        return preds

#!/usr/bin/env python
# -*- coding: utf-8 -*-

#Author: WangLei
#date: 2020/3/19

from cluster.kmeans import KMeansClassifier

import numpy as np
import pandas as pd


def loadDataSet(infile):
    df = pd.read_csv(infile, sep='\t', header=None, dtype=str, na_filter=False)
    return np.array(df).astype(np.float)

def main():
    data_X = loadDataSet("data/testSet.txt")
    print(data_X.shape)
    k = 3
    clf = KMeansClassifier(k)
    clf.fit(data_X)
    cents = clf._centroids
    labels = clf._labels
    sse = clf._sse

    print(cents)
    print(labels)
    print(len(labels))
    print(sse)

main()

數據如下

1.658985	4.285136
-3.453687	3.424321
4.838138	-1.151539
-5.379713	-3.362104
0.972564	2.924086
-3.567919	1.531611
0.450614	-3.302219
-3.487105	-1.724432
2.668759	1.594842
-3.156485	3.191137
3.165506	-3.999838
-2.786837	-3.099354
4.208187	2.984927
-2.123337	2.943366
0.704199	-0.479481
-0.392370	-3.963704
2.831667	1.574018
-0.790153	3.343144
2.943496	-3.357075
-3.195883	-2.283926
2.336445	2.875106
-1.786345	2.554248
2.190101	-1.906020
-3.403367	-2.778288
1.778124	3.880832
-1.688346	2.230267
2.592976	-2.054368
-4.007257	-3.207066
2.257734	3.387564
-2.679011	0.785119
0.939512	-4.023563
-3.674424	-2.261084
2.046259	2.735279
-3.189470	1.780269
4.372646	-0.822248
-2.579316	-3.497576
1.889034	5.190400
-0.798747	2.185588
2.836520	-2.658556
-3.837877	-3.253815
2.096701	3.886007
-2.709034	2.923887
3.367037	-3.184789
-2.121479	-4.232586
2.329546	3.179764
-3.284816	3.273099
3.091414	-3.815232
-3.762093	-2.432191
3.542056	2.778832
-1.736822	4.241041
2.127073	-2.983680
-4.323818	-3.938116
3.792121	5.135768
-4.786473	3.358547
2.624081	-3.260715
-4.009299	-2.978115
2.493525	1.963710
-2.513661	2.642162
1.864375	-3.176309
-3.171184	-3.572452
2.894220	2.489128
-2.562539	2.884438
3.491078	-3.947487
-2.565729	-2.012114
3.332948	3.983102
-1.616805	3.573188
2.280615	-2.559444
-2.651229	-3.103198
2.321395	3.154987
-1.685703	2.939697
3.031012	-3.620252
-4.599622	-2.185829
4.196223	1.126677
-2.133863	3.093686
4.668892	-2.562705
-2.793241	-2.149706
2.884105	3.043438
-2.967647	2.848696
4.479332	-1.764772
-4.905566	-2.911070

最後代碼運行的結果爲

(80, 2)
[[ 2.99405094 -0.1605263 ]
 [-1.6334182   3.03655888]
 [-3.01169468 -3.01238673]]
[1. 1. 0. 2. 1. 1. 2. 2. 0. 1. 0. 2. 0. 1. 0. 2. 0. 1. 0. 2. 0. 1. 0. 2.
 1. 1. 0. 2. 0. 1. 2. 2. 0. 1. 0. 2. 1. 1. 0. 2. 1. 1. 0. 2. 0. 1. 0. 2.
 0. 1. 0. 2. 0. 1. 0. 2. 0. 1. 0. 2. 0. 1. 0. 2. 0. 1. 0. 2. 0. 1. 0. 2.
 0. 1. 0. 2. 0. 1. 0. 2.]
80
136.84604817873276

3.kmeans算法複雜度分析

假設有k個聚類中心,總共有n個點,迭代t輪,那麼每一輪迭代都需要算n個點到k箇中心點的距離,算法的總複雜度爲O(tnk)O(tnk)

4.GMM(Gaussian Mixed Model)

GMM混合高斯分佈是多個高斯分佈函數的線性組合,假設有隨機變量XX,GMM模型可以用如下公式表示
p(x)=k=1KπkN(xμk,Σk)p(\boldsymbol{x}) = \sum_{k=1}^K\pi_k \mathcal{N}(\boldsymbol{x}|\boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k)
其中,N(xμk,Σk)\mathcal{N}(\boldsymbol{x}|\boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k)
是模型中的第k個分量(component)。p(x)p(\boldsymbol{x})是混合係數(mixture coefficient),而且有
k=1Kπk=1\sum_{k=1}^K\pi_k = 1
0πk10 \leq \pi_k \leq 1

如果樣本XX是一維數據(Univariate),高斯分佈的概率密度函數PDF(Probability Density Function)爲:
P(xθ)=12πσ2exp((xμ)22σ2)P(x|\theta) = \frac{1}{\sqrt{2\pi\sigma^{2}}} exp(-\frac{(x-\mu)^2}{2\sigma^{2}})
XX爲多維數據(Multivariate)時,其PDF如下:
P(xθ)=1(2π)D2Σ12exp((xμ)TΣ1(xμ)2)P(x|\theta) = \frac{1}{(2\pi)^{\frac{D}{2}}\left| \Sigma \right|^{\frac{1}{2}}}exp(-\frac{(x-\mu)^{T}\Sigma^{-1}(x-\mu)}{2})
其中,μ\mu爲期望,Σ\Sigma爲協方差矩陣,是一個對稱矩陣。D爲數據的維度。

協方差:
如果用xkix_{ki}表示隨機變量xkx_k的第i個樣本,n表示樣本總數,則xx第m維與第k維的協方差爲
σ(xm,xk)=1n1i=1n(xmixˉm)(xkixˉk)\sigma\left(x_m,x_k\right)=\frac{1}{n-1}\sum_{i=1}^n\left(x_{mi}-\bar{x}_m\right)\left(x_{ki}-\bar{x}_k\right)

Σ=[σ(x1,x1)σ(x1,xd)σ(xd,x1)σ(xd,xd)]\Sigma=\left [ \begin{matrix} \sigma(x_1, x_1) & \cdots & \sigma(x_1, x_d) \\ \cdots & \cdots & \cdots \\ \sigma(x_d, x_1) & \cdots & \sigma(x_d, x_d) \\ \end{matrix} \right ]
其中,對角線上的元素尾各隨機變量的方差,而非對角線上的元素爲兩兩隨機變量之間的協方差。由協方差矩陣的定義易知,協方差矩陣爲方針,大小爲d*d,且爲對稱矩陣(symmetric matrix)。

5.GMM聚類

GMM聚類的核心思想是假設所有的數據點來自多個參數不同的高斯分佈,來自同一分佈的數據點被劃分爲同一類。算法結果返回的是數據點屬於不同類別的概率。

提GMM,必然繞不過EM算法。反過來,提EM算法,一般也會提GMM,這兩跟孿生兄弟一樣經常成對出現,下面我們試着用簡單的方式來描述一下EM算法。

假設我們得到一組樣本xtx_t,而且xtx_t服從高斯分佈,但是高斯分佈的參數未知,就是第四小節裏提到的xN(μ,Σ)x \sim N(\boldsymbol{\mu}, \boldsymbol{\Sigma})。我們的目標是找一個合適的高斯分佈,確定分佈參數,使得這個高斯分佈能產生這組樣本的可能性儘可能大。

學過概率統計的同學應該有感覺,產生這組樣本的可能性儘可能大,這個時候就輪到極大似然估計(Maximum Likehood Estimate, MLE)大顯身手的時候了。

假設樣本集X=x1,x2,,xnX = x_1, x_2, \cdots, x_n,而p(xnμ,Σ)p(x_n | \boldsymbol{\mu}, \boldsymbol{\Sigma})是高斯分佈的概率分佈函數。如果假設樣本抽樣獨立,樣本集XX的聯合概率就是似然函數:
L(μ,Σ)=L(x1,x2,,xn;μ,Σ)=i=0np(xn;μ,Σ)L(\boldsymbol{\mu}, \boldsymbol{\Sigma}) = L(x_1, x_2, \cdots, x_n; \boldsymbol{\mu}, \boldsymbol{\Sigma}) = \prod_{i=0}^n p(x_n; \boldsymbol{\mu}, \boldsymbol{\Sigma})

接下來的求解過程就是求極值了,學過高數的同學都知道,對上式求導,並令導數爲0,就可以求得高斯分佈的參數。
所以最大化似然函數的意義在於:通過使得樣本集的聯合概率最大來對參數進行估計,從而選擇最佳的分佈模型。

回到GMM。上面是隻有一個高斯分佈的情況,在GMM中,有多個高斯分佈,我們並不知道樣本來自哪個高斯分佈。換句話說,
p(x)=k=1KπkN(xμk,Σk)p(\boldsymbol{x}) = \sum_{k=1}^K\pi_k \mathcal{N}(\boldsymbol{x}|\boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k)
中,p(x)p(x)的值我們並不知道,我們不光需要求解不同高斯分佈的參數,還需要求解不同高斯分佈的概率。

我們引入一個隱變量γ\gamma,它是一個K維二值隨機變量,在它的K維取值中只有某個特定的元素γk\gamma_k的取值爲1,其它元素的取值爲0。實際上,隱變量描述的就是:每一次採樣,選擇第k個高斯模型的概率,故有:
p(γk=1)=πkp({\gamma _k} = 1) = {\pi _k}
當給定了γ\gamma的一個特定的值之後(也就是知道了這個樣本從哪一個高斯模型進行採樣),可以得到樣本x的條件分佈是一個高斯分佈,滿足:
p(xγk=1)=N(xμk,Σk)p(x|{\gamma _k} = 1) = N(x|{\mu _k},{\Sigma _k})

而實際上,每個樣本到底是從這K個高斯模型中哪個模型進行採樣的,是都有可能的。故樣本y的概率爲:
p(x)=γp(γ)p(yγ)=k=1KπkN(xμk,Σk)p(x) = \sum\nolimits_\gamma {p(\gamma )} p(y|\gamma ){\rm{ = }}\sum\limits_{{\rm{k}} = 1}^K {{\pi _k}N(x|{\mu _k},{\Sigma _k})}

樣本集X(n個樣本點)的聯合概率爲:
L(μ,Σ,π)=L(x1,x2...xN;μ,Σ,π)=n=1Np(xn;μ,Σ,π)=n=1Nk=1KπkN(xnμk,Σk)L(\mu ,\Sigma ,\pi ) = L({x_1},{x_2}...{x_N};\mu ,\Sigma ,\pi ) = \prod\limits_{n = 1}^N {p({x_n};\mu ,\Sigma ,\pi )} = \prod\limits_{n = 1}^N {\sum\limits_{{\rm{k}} = 1}^K {{\pi _k}N({x_n}|{\mu _k},{\Sigma _k})} }
  
對數似然函數表示爲:
lnL(μ,Σ,π)=n=1Nlnk=1KπkN(xnμk,Σk)\ln L(\mu ,\Sigma ,\pi ) = \sum\limits_{n = 1}^N {\ln \sum\limits_{{\rm{k}} = 1}^K {{\pi _k}N({x_n}|{\mu _k},{\Sigma _k})} }

接下來再求導,令導數爲0,可以得到模型參數(μ,Σ,π)(\mu ,\Sigma ,\pi)
到此爲止,貌似問題已經解決了。但是在對數似然函數裏面,裏面還有求和。實際上沒有辦法通過求導的方法來求這個對數似然函數的最大值。

具體的EM算法步驟參見參考文獻2,公式比較多,就不復制粘貼了。

6.GMM代碼示例

import numpy as np
import itertools

from scipy import linalg
import matplotlib.pyplot as plt
import matplotlib as mpl

from sklearn import mixture

# Number of samples per component
n_samples = 500

# Generate random sample, two components
np.random.seed(0)
C = np.array([[0., -0.1], [1.7, .4]])
X = np.r_[np.dot(np.random.randn(n_samples, 2), C),
          .7 * np.random.randn(n_samples, 2) + np.array([-6, 3])]

lowest_bic = np.infty
bic = []
n_components_range = range(1, 7)
cv_types = ['spherical', 'tied', 'diag', 'full']
for cv_type in cv_types:
    for n_components in n_components_range:
        # Fit a Gaussian mixture with EM
        gmm = mixture.GaussianMixture(n_components=n_components,
                                      covariance_type=cv_type)
        gmm.fit(X)
        bic.append(gmm.bic(X))
        if bic[-1] < lowest_bic:
            lowest_bic = bic[-1]
            best_gmm = gmm

bic = np.array(bic)
color_iter = itertools.cycle(['navy', 'turquoise', 'cornflowerblue',
                              'darkorange'])
clf = best_gmm
bars = []

# Plot the BIC scores
plt.figure(figsize=(8, 6))
spl = plt.subplot(2, 1, 1)
for i, (cv_type, color) in enumerate(zip(cv_types, color_iter)):
    xpos = np.array(n_components_range) + .2 * (i - 2)
    bars.append(plt.bar(xpos, bic[i * len(n_components_range):
                                  (i + 1) * len(n_components_range)],
                        width=.2, color=color))
plt.xticks(n_components_range)
plt.ylim([bic.min() * 1.01 - .01 * bic.max(), bic.max()])
plt.title('BIC score per model')
xpos = np.mod(bic.argmin(), len(n_components_range)) + .65 +\
    .2 * np.floor(bic.argmin() / len(n_components_range))
plt.text(xpos, bic.min() * 0.97 + .03 * bic.max(), '*', fontsize=14)
spl.set_xlabel('Number of components')
spl.legend([b[0] for b in bars], cv_types)

# Plot the winner
splot = plt.subplot(2, 1, 2)
Y_ = clf.predict(X)
for i, (mean, cov, color) in enumerate(zip(clf.means_, clf.covariances_,
                                           color_iter)):
    v, w = linalg.eigh(cov)
    if not np.any(Y_ == i):
        continue
    plt.scatter(X[Y_ == i, 0], X[Y_ == i, 1], .8, color=color)

    # Plot an ellipse to show the Gaussian component
    angle = np.arctan2(w[0][1], w[0][0])
    angle = 180. * angle / np.pi  # convert to degrees
    v = 2. * np.sqrt(2.) * np.sqrt(v)
    ell = mpl.patches.Ellipse(mean, v[0], v[1], 180. + angle, color=color)
    ell.set_clip_box(splot.bbox)
    ell.set_alpha(.5)
    splot.add_artist(ell)

plt.xticks(())
plt.yticks(())
plt.title('Selected GMM: full model, 2 components')
plt.subplots_adjust(hspace=.35, bottom=.02)
plt.show()

關鍵步驟有註釋,參考註釋就好。
最後出來的效果
在這裏插入圖片描述

7.高斯分佈於混合高斯分佈流程小結

對於一個高斯分佈,參數估計的流程一般如下:
1.首先得到一些符合高斯分佈的採樣數據。
2.假設有一個高斯分佈最有可能得到這些採樣數據,接下來求解該高斯分佈的參數。
3.求採樣數據產生的聯合概率P
4.使用極大似然估計最大化P獲得高斯分佈的參數。

如果是一個混合高斯分佈,一般的求解流程如下
1.首先得到一些符合混合高斯分佈的採樣數據。
2.假設有一組高斯分佈最有可能得到這些採樣數據,接下來求解該高斯分佈的參數。
3.1 各個採樣數據最有可能是從哪一個高斯分佈產生的,相當於對採樣數據做分類劃分。
3.2 各個類對劃分到的樣本,求樣本產生的概率P(xt,γtμ,Σ,π)P(x_t, \gamma_t| \boldsymbol{\mu}, \boldsymbol{\Sigma}, \pi)
3.3 求採樣數據產生的概率(Q函數)
3.4 最大化Q函數來優化混合高斯分佈參數。

8 Kmeans與GMM的區別與聯繫

相同點
兩者都是迭代算法,並且迭代策略大致相同:算法開始對需要計算的參數賦初值,然後交替執行兩個步驟。第一個步驟是對數據的估計,k-means是估計每個點所屬簇,GMM是計算隱含變量的期望。第二個步驟是用第一步的估計值重新計算參數值,更新目標參數。其中k-means是計算簇心位置;GMM是計算各個高斯分佈的中心位置和協方差矩陣。
不同點
1.k-means是計算簇心位置,GMM是計算各個高斯分佈的參數。
2.k-means是計算當前簇中所有元素的位置的均值,GMM是通過計算似然函數的最大值實現分佈參數的求解的。

參考文獻

1.https://www.cnblogs.com/yixuan-xu/p/6272208.html
2.https://blog.csdn.net/lin_limin/article/details/81048411

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