1. 程式人生 > >機器學習 聚類(Clustering)____K-均值聚類演算法(K-means Clustering) 層次聚類(Hierarchical Clustering)

機器學習 聚類(Clustering)____K-均值聚類演算法(K-means Clustering) 層次聚類(Hierarchical Clustering)

____tz_zs學習筆記

聚類(Clustering) 顧名思義,就是將相似樣本聚合在一起,屬於機器學習中的非監督學習 (unsupervised learning) 問題。聚類的目標是找到相近的資料點,並將相近的資料點聚合在一起。

實現聚類的演算法主要有:

1.K-均值聚類演算法

2.層次聚類

K-均值聚類演算法(K-means Clustering)

K-means是機器學習中一個比較常用的演算法,屬於無監督學習演算法,其常被用於資料的聚類,只需為它指定簇的數量即可自動將資料聚合到多類中,相同簇中的資料相似度較高,不同簇中資料相似度較低。

優點:

  • 原理簡單
  • 速度快
  • 對大資料集有比較好的伸縮性

缺點:

  • 需要指定聚類 數量K
  • 對異常值敏感
  • 對初始值敏感

演算法接受引數 k ;然後將事先輸入的n個數據物件劃分為 k個聚類,以便使得所獲得的聚類滿足:同一聚類中的物件相似度較高;而不同聚類中的物件相似度較小。

演算法思想:

以空間中k個點為中心進行聚類,對最靠近他們的物件歸類。通過迭代的方法,逐次更新各聚類中心的值,直至得到最好的聚類結果

演算法描述:

(1)適當選擇c個類的初始中心;

(2)在第k次迭代中,對任意一個樣本,求其到c各中心的距離,將該樣本歸到距離最短的中心所在的類;

(3)利用均值等方法更新該類的中心值;

(4)對於所有的c個聚類中心,如果利用(2)(3)的迭代法更新後,值保持不變,則迭代結束,否則繼續迭代。

演算法流程:

輸入:k, data[n];

(1) 選擇k個初始中心點,例如c[0]=data[0],…c[k-1]=data[k-1];

(2) 對於data[0]….data[n], 分別與c[0]…c[k-1]比較,假定與c[i]差值最少,就標記為i;

(3) 對於所有標記為i點,重新計算c[i]={ 所有標記為i的data[j]之和}/標記為i的個數;

(4) 重複(2)(3),直到所有c[i]值的變化小於給定閾值。

·

# -*- coding: utf-8 -*-
"""
@author: tz_zs
"""
import numpy as np


# Function: K Means
# -------------
# K-Means is an algorithm that takes in a dataset and a constant
# k and returns k centroids (which define clusters of data in the
# dataset which are similar to one another).

# 定義kmeans方法 (資料集,劃分為k類,停止的條件)
def kmeans(X, k, maxIt):
    
    numPoints, numDim = X.shape # 獲取多少行(例項數) 多少列(維度)
    
    dataSet = np.zeros((numPoints, numDim + 1))
    dataSet[:, :-1] = X
    
    # Initialize centroids randomly 隨機的初始化中心點
    centroids = dataSet[np.random.randint(numPoints, size = k), :]
#    centroids = dataSet[0:2, :]
    #Randomly assign labels to initial centorid 分配標籤
    centroids[:, -1] = range(1, k +1)
    
    # Initialize book keeping vars.
    iterations = 0
    oldCentroids = None
    
    # Run the main k-means algorithm
    while not shouldStop(oldCentroids, centroids, iterations, maxIt):
        print "iteration: \n", iterations # 迭代次數
        print "dataSet: \n", dataSet
        print "centroids: \n", centroids
        # Save old centroids for convergence test. Book keeping.
        oldCentroids = np.copy(centroids)
        iterations += 1
        
        # Assign labels to each datapoint based on centroids 更新歸類
        updateLabels(dataSet, centroids)
        
        # Assign centroids based on datapoint labels 更新中心點
        centroids = getCentroids(dataSet, k)
        
    # We can get the labels too by calling getLabels(dataSet, centroids)
    return dataSet


# Function: Should Stop
# -------------
# Returns True or False if k-means is done. K-means terminates either
# because it has run a maximum number of iterations OR the centroids
# stop changing.
def shouldStop(oldCentroids, centroids, iterations, maxIt):
    if iterations > maxIt:
        return True
    return np.array_equal(oldCentroids, centroids)  


# Function: Get Labels
# -------------
# Update a label for each piece of data in the dataset. 
def updateLabels(dataSet, centroids):
    # For each element in the dataset, chose the closest centroid. 
    # Make that centroid the element's label.
    numPoints, numDim = dataSet.shape
    for i in range(0, numPoints):
        dataSet[i, -1] = getLabelFromClosestCentroid(dataSet[i, :-1], centroids)
    
# (資料集中的一行,中心點)  
def getLabelFromClosestCentroid(dataSetRow, centroids):
    label = centroids[0, -1];
    minDist = np.linalg.norm(dataSetRow - centroids[0, :-1])
    for i in range(1 , centroids.shape[0]):
        dist = np.linalg.norm(dataSetRow - centroids[i, :-1])
        if dist < minDist:
            minDist = dist
            label = centroids[i, -1]
    print "minDist:", minDist
    return label
    
        
    
# Function: Get Centroids
# -------------
# Returns k random centroids, each of dimension n.
def getCentroids(dataSet, k):
    # Each centroid is the geometric mean of the points that
    # have that centroid's label. Important: If a centroid is empty (no points have
    # that centroid's label) you should randomly re-initialize it.
    result = np.zeros((k, dataSet.shape[1]))
    for i in range(1, k + 1):
        oneCluster = dataSet[dataSet[:, -1] == i, :-1]
        result[i - 1, :-1] = np.mean(oneCluster, axis = 0)
        result[i - 1, -1] = i
    
    return result
    
    
x1 = np.array([1, 1])
x2 = np.array([2, 1])
x3 = np.array([4, 3])
x4 = np.array([5, 4])
testX = np.vstack((x1, x2, x3, x4)) # 組成矩陣


result = kmeans(testX, 2, 10)
print "final result:"
print result

·

Python中sklearn.cluster.KMeans

·

# -*- coding: utf-8 -*-
"""
@author: tz_zs
k-means
"""

sklearn.cluster.KMeans(
    n_clusters=8,
    init='k-means++',
    n_init=10,
    max_iter=300,
    tol=0.0001,
    precompute_distances='auto',
    verbose=0,
    random_state=None,
    copy_x=True,
    n_jobs=1,
    algorithm='auto'
)
'''
n_clusters:     簇的個數,即你想聚成幾類
init:           初始簇中心的獲取方法
n_init:         獲取初始簇中心的更迭次數,為了彌補初始質心的影響,演算法預設會初始10個質心,實現演算法,然後返回最好的結果。
max_iter:       最大迭代次數(因為kmeans演算法的實現需要迭代)
tol:            容忍度,即kmeans執行準則收斂的條件
precompute_distances:   是否需要提前計算距離,這個引數會在空間和時間之間做權衡,如果是True 會把整個距離矩陣都放到內
                        存中,auto 會預設在資料樣本大於featurs*samples 的數量大於12e6 的時候False,False 時核心實
                        現的方法是利用Cpython 來實現的
verbose:        冗長模式(不太懂是啥意思,反正一般不去改預設值)
random_state:   隨機生成簇中心的狀態條件。
copy_x:  對是否修改資料的一個標記,如果True,即複製了就不會修改資料。bool 在scikit-learn 很多介面中都
        會有這個引數的,就是是否對輸入資料繼續copy 操作,以便不修改使用者的輸入資料。這個要理解Python 的內
        存機制才會比較清楚。
n_jobs:     並行設定
algorithm:  kmeans的實現演算法,有:’auto’, ‘full’, ‘elkan’, 其中 ‘full’表示用EM方式實現

雖然有很多引數,但是都已經給出了預設值。所以我們一般不需要去傳入這些引數,引數的。可以根據實際需要來呼叫。
'''

·

sklearn.cluster.KMeans的使用1

# -*- coding: utf-8 -*-
"""
@author: tz_zs
sklearn.cluster.KMeans的使用1
"""
from sklearn.cluster import KMeans
import numpy as np

# 生成10*3的矩陣
data = np.random.rand(10, 3)
print(data)

# 聚類為4
estimator = KMeans(n_clusters=4)  # 返回KMeans物件

# fit_predict表示擬合+預測,也可以分開寫
res = estimator.fit_predict(data)  # 返回預測類別標籤結果
'''
fit = estimator.fit(data)  # 返回此KMeans物件,fit==estimator
res2 = estimator.predict(data)  # 返回預測類別標籤結果
print("product:", res2)

# res3 = estimator.fit(data).predict(data)  # 返回預測類別標籤結果
'''

# 預測類別標籤結果
lable_pred = estimator.labels_
# 各個類別的聚類中心值
centroids = estimator.cluster_centers_
# 聚類中心均值向量的總和
inertia = estimator.inertia_

print("lable_pred:", lable_pred)
print("centroids:", centroids)
print("inertia:", inertia)

'''
結果:

[[ 0.84806828  0.12835109  0.49866577]
 [ 0.05612908  0.60932968  0.18321125]
 [ 0.06626165  0.88874957  0.99578568]
 [ 0.44475794  0.0689524   0.5608716 ]
 [ 0.70856172  0.35226933  0.12178557]
 [ 0.04791486  0.66868388  0.01066914]
 [ 0.72677149  0.46552559  0.61079303]
 [ 0.82953259  0.88425252  0.52384649]
 [ 0.87558294  0.3417068   0.20297633]
 [ 0.86283938  0.51308292  0.85950813]]
lable_pred: [1 0 2 1 1 0 3 3 1 3]
centroids: [[ 0.05202197  0.63900678  0.0969402 ]
 [ 0.71924272  0.22281991  0.34607482]
 [ 0.06626165  0.88874957  0.99578568]
 [ 0.80638115  0.62095368  0.66471589]]
inertia: 0.512747537743
'''

·

sklearn.cluster.KMeans的使用2

# -*- coding: utf-8 -*-
"""
@author: tz_zs
sklearn.cluster.KMeans的使用2
"""

from sklearn.cluster import KMeans
from sklearn.externals import joblib
import numpy as np
import matplotlib.pyplot as plt

data = np.random.rand(100, 2)
estimator = KMeans(n_clusters=3)
res = estimator.fit_predict(data)
lable_pred = estimator.labels_
centroids = estimator.cluster_centers_
inertia = estimator.inertia_

print("lable_pred:", lable_pred)
print("centroids:", centroids)
print("inertia:", inertia)
'''
lable_pred: [2 0 2 0 1 0 2 2 1 2 0 1 2 2 0 0 0 2 2 1 2 0 2 0 1 0 2 2 0 0 0 0 0 1 0 1 1
 2 2 0 2 1 0 1 2 0 0 1 0 0 1 1 0 1 1 0 2 0 1 2 1 2 1 1 0 2 1 1 1 0 2 2 1 2
 0 1 0 2 0 0 1 0 1 0 1 1 0 1 2 0 0 0 0 2 1 0 0 0 0 2]
centroids: [[ 0.27187029  0.77241536]
 [ 0.39703852  0.23291867]
 [ 0.83250094  0.59010582]]
inertia: 6.54249440001
'''

for i in range(len(data)):
    if int(lable_pred[i]) == 0:
        plt.scatter(data[i][0], data[i][1], color='red')
    if int(lable_pred[i]) == 1:
        plt.scatter(data[i][0], data[i][1], color='black')
    if int(lable_pred[i]) == 2:
        plt.scatter(data[i][0], data[i][1], color='blue')

plt.show()

#  儲存機器學習演算法模型
joblib.dump(value=estimator, filename="./km_model.m")
load = joblib.load("./km_model.m")
print(load)
'''
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
    n_clusters=3, n_init=10, n_jobs=1, precompute_distances='auto',
    random_state=None, tol=0.0001, verbose=0)
'''

·


層次聚類(Hierarchical Clustering)

假設有N個待聚類的樣本,對於層次聚類來說,步驟:

1、(初始化)把每個樣本歸為一類,計算每兩個類之間的距離,也就是樣本與樣本之間的相似度;

2、尋找各個類之間最近的兩個類,把他們歸為一類(這樣類的總數就少了一個);

3、重新計算新生成的這個類與各個舊類之間的相似度;

4、重複2和3直到所有樣本點都歸為一類,結束


整個聚類過程其實是建立了一棵樹,在建立的過程中,可以通過在第二步上設定一個閾值,當最近的兩個類的距離大於這個閾值,則認為迭代可以終止。另外關鍵的一步就是第三步,如何判斷兩個類之間的相似度有不少種方法。這裡介紹一下三種:
SingleLinkage:又叫做 nearest-neighbor ,就是取兩個類中距離最近的兩個樣本的距離作為這兩個集合的距離,也就是說,最近兩個樣本之間的距離越小,這兩個類之間的相似度就越大。容易造成一種叫做 Chaining 的效果,兩個 cluster 明明從“大局”上離得比較遠,但是由於其中個別的點距離比較近就被合併了,並且這樣合併之後 Chaining 效應會進一步擴大,最後會得到比較鬆散的 cluster 。CompleteLinkage:這個則完全是 Single Linkage 的反面極端,取兩個集合中距離最遠的兩個點的距離作為兩個集合的距離。其效果也是剛好相反的,限制非常大,兩個 cluster 即使已經很接近了,但是隻要有不配合的點存在,就頑固到底,老死不相合並,也是不太好的辦法。這兩種相似度的定義方法的共同問題就是指考慮了某個有特點的資料,而沒有考慮類內資料的整體特點。Average-linkage:這種方法就是把兩個集合中的點兩兩的距離全部放在一起求一個平均值,相對也能得到合適一點的結果。average-linkage的一個變種就是取兩兩距離的中值,與取均值相比更加能夠解除個別偏離樣本對結果的干擾。
# -*- coding: utf-8 -*-
"""
@author: tz_zs
"""
from numpy import *


"""
Code for hierarchical clustering, modified from 
Programming Collective Intelligence by Toby Segaran 
(O'Reilly Media 2007, page 33). 
"""


class cluster_node:
    def __init__(self,vec,left=None,right=None,distance=0.0,id=None,count=1):
        self.left=left
        self.right=right
        self.vec=vec # vector向量 代表一個例項
        self.id=id
        self.distance=distance
        self.count=count #only used for weighted average 


def L2dist(v1,v2):
    return sqrt(sum((v1-v2)**2))
    
def L1dist(v1,v2):
    return sum(abs(v1-v2))


# def Chi2dist(v1,v2):
#     return sqrt(sum((v1-v2)**2))


def hcluster(features,distance=L2dist):
    #cluster the rows of the "features" matrix
    distances={}
    currentclustid=-1


    # clusters are initially just the individual rows  每個例項作為一個聚類
    clust=[cluster_node(array(features[i]),id=i) for i in range(len(features))]

    
    while len(clust)>1:
        lowestpair=(0,1) # 最近的一對
        closest=distance(clust[0].vec,clust[1].vec) # 最近的距離
    
        # loop through every pair looking for the smallest distance
        # 迴圈每一對 找出最小距離的
        for i in range(len(clust)):
            for j in range(i+1,len(clust)):
                # distances is the cache of distance calculations
                # distances是距離計算結果的儲存集合
                if (clust[i].id,clust[j].id) not in distances: 
                    distances[(clust[i].id,clust[j].id)]=distance(clust[i].vec,clust[j].vec)
        
                d=distances[(clust[i].id,clust[j].id)]
        
                if d<closest:
                    closest=d
                    lowestpair=(i,j)
        
        # calculate the average of the two clusters 
        # 合併vec 計算最近的這一對的每個vec值平均值
        mergevec=[(clust[lowestpair[0]].vec[i]+clust[lowestpair[1]].vec[i])/2.0 \
            for i in range(len(clust[0].vec))]
        
        # create the new cluster 以這個合併的向量 建立新的節點
        newcluster=cluster_node(array(mergevec),left=clust[lowestpair[0]],
                             right=clust[lowestpair[1]],
                             distance=closest,id=currentclustid)
        
        # cluster ids that weren't in the original set are negative
        currentclustid-=1
        del clust[lowestpair[1]]
        del clust[lowestpair[0]]
        clust.append(newcluster)


    return clust[0]



# 遞迴的取出樹形結構結果
def extract_clusters(clust,dist):
    # extract list of sub-tree clusters from hcluster tree with distance<dist
    clusters = {}
    if clust.distance<dist:
        # we have found a cluster subtree
        return [clust] 
    else:
        # check the right and left branches
        cl = []
        cr = []
        if clust.left!=None: 
            cl = extract_clusters(clust.left,dist=dist)
        if clust.right!=None: 
            cr = extract_clusters(clust.right,dist=dist)
        return cl+cr 

# 迭代 取出聚類中的元素id   
def get_cluster_elements(clust):
    # return ids for elements in a cluster sub-tree
    if clust.id>=0:
        # positive id means that this is a leaf
        return [clust.id]
    else:
        # check the right and left branches
        cl = []
        cr = []
        if clust.left!=None: 
            cl = get_cluster_elements(clust.left)
        if clust.right!=None: 
            cr = get_cluster_elements(clust.right)
        return cl+cr



# 打印出
def printclust(clust,labels=None,n=0):
    # indent to make a hierarchy layout 用n控制縮排層級
    for i in range(n): print ' ',
    if clust.id<0:
        # negative id means that this is branch
        print '-'
    else:
        # positive id means that this is an endpoint
        if labels==None: print clust.id
        else: print labels[clust.id]
    
    # now print the right and left branches
    if clust.left!=None: printclust(clust.left,labels=labels,n=n+1)
    if clust.right!=None: printclust(clust.right,labels=labels,n=n+1)





# 得到樹的高度 多少個末端
def getheight(clust):
    # Is this an endpoint? Then the height is just 1
    if clust.left==None and clust.right==None: return 1
    
    # Otherwise the height is the same of the heights of
    # each branch
    return getheight(clust.left)+getheight(clust.right)

# 得到樹的深度 
def getdepth(clust):
    # The distance of an endpoint is 0.0
    if clust.left==None and clust.right==None: return 0
    
    # The distance of a branch is the greater of its two sides
    # plus its own distance
    return max(getdepth(clust.left),getdepth(clust.right))+clust.distance