python機器學習——kmeans聚類算法

背景與原理:

聚類問題與分類問題有一定的區別,分類問題是對每個訓練數據,我給定了類別的標籤,現在想要訓練一個模型使得對於測試數據能輸出正確的類別標籤,更多見於監督學習;而聚類問題則是我們給出了一組數據,我們並沒有預先的標籤,而是由機器考察這些數據之間的相似性,將相似的數據聚爲一類,是無監督學習的一個典型應用。

而k-means算法則是非常常見的聚類算法,其思想是如果我們想把這些數據聚爲k類,那麼我們預先選擇k箇中心,然後計算每個數據點與這k箇中心之間的“距離”(也就是這個數據點與這個中心的“相似度”),那麼非常自然地,每個數據點應當被劃分進離他距離最近的那個中心點對應的類。

但是這是最優的聚類方法嗎?如果我們初始選的k個點很糟糕,其實有些數據點離這k個點都很遠,而這種劃分只是一種“矮子裏面拔將軍”的劃分,因此可能會把兩個差異巨大的點劃分到一個聚類裏面去,因此我們需要迭代上述過程,即當我們選中了一些數據點聚爲一類之後,我們取這些數據點的“質心”作爲新的中心點,這樣我們會得到k個新的中心點,然後我們重複上述過程,直到中心點不再移動爲止。

那麼我們就要解決幾個問題:

一.我們如何度量“距離”或“相似度”?

距離的度量其實有很多方式,對於兩個數據點$(x_{1},x_{2},...,x_{n}),(y_{1},y_{2},...,y_{n})$,常見的距離度量有如下的方式:

歐氏距離:$d(x,y)=\sqrt{\sum_{i=1}^{n}(x_{i}-y_{i})^{2}}$

曼哈頓距離:$d(x,y)=\sum_{i=1}^{n}|x_{i}-y_{i}|$

切比雪夫距離:$d(x,y)=max_{i=1}^{n}|x_{i}-y_{i}|$

餘弦距離:$d(x,y)=\cos \theta =\dfrac{\sum_{i=1}^{n}x_{i}y_{i}}{\sqrt{\sum_{i=1}^{n}x_{i}^{2}} \sqrt{\sum_{i=1}^{n}y_{i}^{2}}}$

相關係數:$\rho_{XY}=\dfrac{Cov(X,Y)}{\sqrt{D(X)} \sqrt{D(Y)}}=\dfrac{E((X-EX)(Y-EY))}{\sqrt{D(X)} \sqrt{D(Y)}}$

在這裏我們選用歐氏距離來度量點之間的距離,選取SSE(誤差平方和)作爲損失函數,即設我們有$k$類,第$i$類的中心點爲$c_{i}$,那麼損失函數爲:

$J(c)=\sum_{i=1}^{k}\sum_{x\in C_{i}}d(x,c_{i})^{2}$

那麼此時我們每次迭代過程中選取的新中心點是什麼呢?

我們對第$i$箇中心點$c_{i}$求偏導:

$\dfrac{\partial J(c)}{\partial c_{i}}=\dfrac{\partial \sum_{x\in C_{i}}|x-c_{i}|^{2}}{\partial c_{i}}=-2\sum_{x\in C_{i}} (x-c_{i})$

而取得極小值時,上述偏導數爲零,即:

$c_{i}=\dfrac{\sum_{x\in C_{i}}x}{|C_{i}|}$

也即新的中心點應該是聚在這一類裏的所有點的算術平均值

設計、調整與評價:

如何選取初始聚類中心?

(1)憑經驗直接選取

(2)將數據隨機分成k類,計算每類中心作爲初始聚類中心

(3)求以每個數據點爲球心,某個半徑內的特徵點個數,選取密度最大的特徵點爲第一個聚類中心,然後在離這個聚類中心距離大於某個距離$d$的特徵點中選取另一個密度最大的特徵點,以此類推直至選出k個點

(4)用距離最遠的k個點作爲初始中心

(5)n較大時,先隨機選出一部分聚成k類,再將這k箇中心作爲初始聚類中心

如何選取聚類個數?

(1)按聚類目標(比如手寫數字識別)等先驗知識確定k

(2)讓k從小到大增加,那麼損失函數顯然在減少,選擇損失函數下降的拐點對應的k

如何評價聚類效果?

一般我們可以用幾個指標來評價,比如$P$值(純度)和$F$值

所謂純度,是指如果我們已知每個數據點的類別,我們不妨假設一共有$k$類,那麼對於聚出的第$r$類,其純度$P(S_{r})=\dfrac{max_{i=1}^{k}n_{ri}}{n_{r}}$,所謂$n_{ri}$,就是被聚在第$r$類的數據中心原本屬於第$i$類的數據點個數,而$n_{r}$就是整個被聚出的第$r$類的元素個數,而我們整個聚類過程的總純度爲:

$P=\sum_{r=1}^{k}\dfrac{n_{r}}{n}P(S_{r})$

其中$n$爲所有數據點的總數。

 而所謂$F$值,是準確率(precision)和召回率(recall)的調和平均值。

這裏要解決的其實是一個問題:我們已知原數據有$k$類,而我們聚出了$k$個類,那...我們怎麼把聚出的$k$個類和預先想分出的$k$類對應起來?

舉個例子:手寫數字識別,我們要識別手寫的0~9十個數字,而我們聚出了十個類,那我們要怎麼知道每個類對應的是哪個數字呢?

那麼我們定義$precision(i,r)=\dfrac{n_{ri}}{n_{r}}$,即如果我們認爲聚出的第$r$類對應於原數據中的第$i$類,那麼其準確率即爲這個類裏確實屬於第$i$類的數據佔比

而$recall(i,r)=\dfrac{n_{ri}}{n_{i}}$,其中$n_{i}$表示在原標籤中屬於第$i$類的數據個數,即如果我們認爲聚出的第$r$類對應於原數據中的第$i$類,那麼召回率即爲這個類裏確實屬於第$i$類裏的數據佔所有第$i$類數據的佔比

那麼我們定義$f(i,r)=\dfrac{2*precision(i,r)*recall(i,r)}{precision(i,r)+recall(i,r)}$,而整個數據集的$F$定義爲:

$F=\sum_{i=1}^{k}\dfrac{n_{i}}{n}f(i,r)$

當然,這個值取決於如何對聚類和原始類別對應,因此我們想最大化這個值,我們就要使用一種二分圖匹配算法,常用的是KM算法。

KM算法可以查看別的介紹,這裏簡要介紹下其功能:我們把聚出的k類和原始分好的k類分在兩側,那麼這可以看做一個二分圖模型,而我們構造的聚類與分好類的對應關係就是一種二分圖的匹配,而這個匹配過程的要求是我們要最大化$F$值,那麼如果我們設聚出的第$r$類和原始的第$i$類之間的邊權爲$n_{i}f(i,r)$,我們進行的就是二分圖最佳匹配,而這個匹配可以用KM算法計算出來。

代碼實現:

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

plt.figure()
img = plt.imread('./cat.jpeg')
plt.imshow(img)

def kmeans_iteration(l):
    oril=[]
    for i in l:
        oril.append(i)
    flag=0
    for i in dic:
        p=0
        mind=10000000
        for j in range(0,16):
            d=pow((i[0]-l[j][0]),2)+pow((i[1]-l[j][1]),2)+pow((i[2]-l[j][2]),2)
            if d<mind:
                p=j
                mind=d
        if dic[i]!=p:
            flag=1
            dic[i]=p
    if flag==0:
        return l
    else:for i in range(0,16):
            cnt=0
            r=0
            b=0
            g=0
            for j in dic:
                if dic[j]==i:
                    r+=j[0]
                    b+=j[1]
                    g+=j[2]
                    cnt+=1
            r/=cnt
            b/=cnt
            g/=cnt
            l[i]=(r,b,g)
        
        for i in range(0,16):
            d=pow((oril[i][0]-l[i][0]),2)+pow((oril[i][1]-l[i][1]),2)+pow((oril[i][2]-l[i][2]),2)
            if d>1:
                flag=0
        if flag==1:
            return l
        else:
            return kmeans_iteration(l)

templ=[]
for i in range(1,17):
    templ.append((i*10,i*10,i*10))
    
retl=kmeans_iteration(templ)
re=np.zeros((1080,1080,3))
for i in range(0,1080):
    for j in range(0,1080):
        p=0
        mind=1000000
        for k in range(0,16):
            d=pow((img[i][j][0]-retl[k][0]),2)+pow((img[i][j][1]-retl[k][1]),2)+pow((img[i][j][2]-retl[k][2]),2)
            if d<mind:
                p=k
                mind=d
        for k in range(0,3):
            re[i][j][k]=retl[p][k]
plt.imshow(re/255)

這段代碼是kmeans的一個手寫實現,實現了將一張圖片壓縮至16色,原理是將所有顏色點聚成16類,每個顏色用其聚類中心取代

這是原始圖片:

 

 這是處理後的圖片:

 

 可以看到壓縮效果還是很不錯的

from sklearn import datasets, preprocessing
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans, MeanShift
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
X = pd.read_csv('./train_X.csv') # 爲了方便起見,這裏只採用前6000個MNIST數據
y = pd.read_csv('./train_y.csv')
X, y = np.array(X), np.array(y)
print(X.shape)
print(y.shape)

pca2d = PCA(n_components=2)
X_std = preprocessing.scale(X) # 數據標準化
X_2d = pca2d.fit_transform(X_std)# 數據降維至兩維便於可視化
plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y)

y_pred_std = KMeans(n_clusters=10, random_state=9).fit_predict(X_std)
plt.scatter(X_2d[:,0],X_2d[:,1],c=y_pred_std)
l2=[]
for i in range(0,10):
    l2.append(dict())
    for j in range(0,10):
        l2[i][j]=0
for i in range(0,6000):
    l2[y_pred_std[i]][y[i][0]]+=1

P2=0
for i in range(0,10):
    p=0
    for j in range(1,10):
        if l2[i][j]>l2[i][p]:
            p=j
    P2+=l2[i][p]/6000
print(P2)

 這個代碼展示了使用sklearn裏面的KMeans包直接進行kmeans聚類,而同樣還進行了一個PCA降維,這個降維的過程主要是用來可視化,即把聚類結果花在一張圖上,同時這裏還計算了P值

而如果實現類別的對應,我們可以這樣寫:

dic1=dict()#ni
dic3=dict()#nr
graph2=[]
for i in range(0,10):
    dic1[i]=0
    dic3[i]=0
    graph2.append([])

for i in range(0,6000):
    dic1[y[i][0]]+=1
    dic3[y_pred_std[i]]+=1

for i in range(0,10):
    for j in range(0,10):
        graph2[i].append(2*l2[j][i]*dic1[i]/(dic1[i]+dic3[j]))


def find_path(graph,i, visited_left, visited_right, slack_right):
    visited_left[i] = True
    for j, match_weight in enumerate(graph[i]):
        if visited_right[j]:
            continue
        gap = label_left[i] + label_right[j] - match_weight
        if abs(gap)<1e-3 :
            visited_right[j] = True
            if j not in T or find_path(graph,T[j], visited_left, visited_right, slack_right):
                T[j] = i
                S[i] = j
                return True

        else:
            slack_right[j] = min(slack_right[j], gap)
    return False

def KM(graph):
    m = len(graph)
    for i in range(m):
        # 重置輔助變量
        slack_right = [float('inf') for _ in range(m)]
        while True:
            visited_left = [False for _ in graph]
            visited_right = [False for _ in graph]
            if find_path(graph,i,visited_left,visited_right, slack_right):
                break
            d = float('inf')
            for j, slack in enumerate(slack_right):
                if not visited_right[j] and slack < d:
                    d = slack
            for k in range(m):
                if visited_left[k]:
                    label_left[k] -= d
                if visited_right[k]:
                    label_right[k] += d
    return S, T

label_left, label_right = [max(g) for g in graph2], [0 for _ in graph2]
S, T = {}, {}

visited_left = [False for _ in graph2]
visited_right = [False for _ in graph2]
slack_right = [float('inf') for _ in graph2]
KM(graph2)
ans=0
for i in S:
    ans+=graph2[i][S[i]]
print(ans/6000)

這段代碼用KM算法計算了上述手寫數字識別kmeans的F值。

小結與優化:

kmeans算法的優點是顯著的:算法簡單易於實現;如果類密集且類與類之間區別明顯時聚類效果很好;算法複雜度爲$O(Nkt)$,對大數據集而言相對高效

但是其缺點也是顯著的:結果與初始質心的選取有關(如果初始質心選取的不好迭代次數會很多,同時效果可能會比較差);必須預先給出要聚類的個數k作爲超參數(因此需要調參);對噪聲和孤立數據點敏感,少量這樣的數據就會對平均值產生較大的影響;不適合發現非凸形狀的聚類;在大數據集上收斂比較慢;可能達到局部極小值。

常見的改進有kmeans++算法,即初始選取聚類中心時要求聚類中心離得越遠越好;ISODATA算法(迭代自組織數據分析法):可以調整的kmeans方法,當屬於某個類的數據點過少就刪掉這個類,而當屬於某個類的數據過多、分散程度較大時將這個類分成兩個子類

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