K-means聚类算法(事先数据并没有类别之分!所有的数据都是一样的)

1、概述

K-means算法是集简单和经典于一身的基于距离的聚类算法,采用距离作为相似性的评价指标,即认为两个对象的距离越近,其相似度就越大。

该算法认为类簇是由距离靠近的对象组成的,因此把得到紧凑且独立的簇作为最终目标。

2、核心思想

通过迭代寻找k个类簇的一种划分方案,使得用这k个类簇的均值来代表相应各类样本时所得的总体误差最小。

k个聚类具有以下特点:各聚类本身尽可能的紧凑,而各聚类之间尽可能的分开。

k-means算法的基础是最小误差平方和准则,
其代价函数是:

$J(c,\mu)=\sum\limits_{i=1}^{k}\|x^{(i)}-\mu_{c^{(i)}}\|^2$

式中,$kmeans聚类算法python肘部法则 kmeans聚类算法代码python_聚类`表示第i个聚类的均值。

各类簇内的样本越相似,其与该类均值间的误差平方越小,对所有类所得到的误差平方求和,即可验证分为k类时,各聚类是否是最优的。

上式的代价函数无法用解析的方法最小化,只能有迭代的方法。

3、算法步骤图解

下图展示了对n个样本点进行K-means聚类的效果,这里k取2。

![kmeans][base64str]

4、算法实现步骤

k-means算法是将样本聚类成 k个簇(cluster),其中k是用户给定的,其求解过程非常直观简单,具体算法描述如下:

  1. 随机选取 k个聚类质心点
  2. 重复下面过程直到收敛 {
    对于每一个样例 i,计算其应该属于的类:
    $c^{(i)}:=\underset{j}{\arg\min}\Vert x^{(i)}-\mu_{j}\Vert ^2$对于每一个类 j,重新计算该类的质心:
    kmeans聚类算法python肘部法则 kmeans聚类算法代码python_聚类_02

 }

其伪代码如下:


创建k个点作为初始的质心点(随机选择)
当任意一个点的簇分配结果发生改变时
KaTeX parse error: Can't use function '$' in math mode at position 6: \quad$̲`对数据集中的每一个数据点 …\qquad$对每一个质心 $$\qquad\qquad$计算质心与数据点的距离
KaTeX parse error: Can't use function '$' in math mode at position 7: \qquad$̲`将数据点分配到距离最近的簇 …\quad$`对每一个簇,计算簇中所有点的均值,并将均值作为质心

5、K-means聚类算法python实战

需求:对给定的数据集进行聚类

本案例采用二维数据集,共80个样本,有4个类。
testSet.txt

-0.017612	14.053064	0
-1.395634	4.662541	1
-0.752157	6.538620	0
-1.322371	7.152853	0
0.423363	11.054677	0
0.406704	7.067335	1
0.667394	12.741452	0
-2.460150	6.866805	1
0.569411	9.548755	0
-0.026632	10.427743	0
0.850433	6.920334	1
1.347183	13.175500	0
1.176813	3.167020	1
-1.781871	9.097953	0
-0.566606	5.749003	1
0.931635	1.589505	1
-0.024205	6.151823	1
-0.036453	2.690988	1
-0.196949	0.444165	1
1.014459	5.754399	1
1.985298	3.230619	1
-1.693453	-0.557540	1
-0.576525	11.778922	0
-0.346811	-1.678730	1
-2.124484	2.672471	1
1.217916	9.597015	0
-0.733928	9.098687	0
-3.642001	-1.618087	1
0.315985	3.523953	1
1.416614	9.619232	0
-0.386323	3.989286	1
0.556921	8.294984	1
1.224863	11.587360	0
-1.347803	-2.406051	1
1.196604	4.951851	1
0.275221	9.543647	0
0.470575	9.332488	0
-1.889567	9.542662	0
-1.527893	12.150579	0
-1.185247	11.309318	0
-0.445678	3.297303	1
1.042222	6.105155	1
-0.618787	10.320986	0
1.152083	0.548467	1
0.828534	2.676045	1
-1.237728	10.549033	0
-0.683565	-2.166125	1
0.229456	5.921938	1
-0.959885	11.555336	0
0.492911	10.993324	0
0.184992	8.721488	0
-0.355715	10.325976	0
-0.397822	8.058397	0
0.824839	13.730343	0
1.507278	5.027866	1
0.099671	6.835839	1
-0.344008	10.717485	0
1.785928	7.718645	1
-0.918801	11.560217	0
-0.364009	4.747300	1
-0.841722	4.119083	1
0.490426	1.960539	1
-0.007194	9.075792	0
0.356107	12.447863	0
0.342578	12.281162	0
-0.810823	-1.466018	1
2.530777	6.476801	1
1.296683	11.607559	0
0.475487	12.040035	0
-0.783277	11.009725	0
0.074798	11.023650	0
-1.337472	0.468339	1
-0.102781	13.763651	0
-0.147324	2.874846	1
0.518389	9.887035	0
1.015399	7.571882	0
-1.658086	-0.027255	1
1.319944	2.171228	1
2.056216	5.019981	1
-0.851633	4.375691	1
-1.510047	6.061992	0
-1.076637	-3.181888	1
1.821096	10.283990	0
3.010150	8.401766	1
-1.099458	1.688274	1
-0.834872	-1.733869	1
-0.846637	3.849075	1
1.400102	12.628781	0
1.752842	5.468166	1
0.078557	0.059736	1
0.089392	-0.715300	1
1.825662	12.693808	0
0.197445	9.744638	0
0.126117	0.922311	1
-0.679797	1.220530	1
0.677983	2.556666	1
0.761349	10.693862	0
-2.168791	0.143632	1
1.388610	9.341997	0
0.317029	14.739025	0

代码如下:

#!/usr/bin/python
# coding=utf-8
import copy
import numpy as np
# 加载数据
def loadDataSet(fileName):  # 解析文件,按tab分割字段,得到一个浮点数字类型的矩阵
    dataMat = []              # 文件的最后一个字段是类别标签
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = list(map(float, curLine))    # 将每个元素转成float类型
        dataMat.append(fltLine)
    return np.array(dataMat)

# 计算欧几里得距离
def distEclud(vecA, vecB):
    return np.sqrt(np.sum(np.power(vecA - vecB, 2))) # 求两个向量之间的距离

# 构建聚簇中心,取k个(此例中为4)随机质心
def randCent(dataSet, k):
    centroid = np.array(np.zeros((k,2)))   # 每个质心有n个坐标值,总共要k个质心
    for j in range(2):
        minJ = min(dataSet[:,j])
        maxJ = max(dataSet[:,j])
        rangeJ = float(maxJ - minJ)
        centroid[:,j] = minJ + rangeJ * np.random.rand(k)
    return centroid

# k-means 聚类算法
def kMeans(dataSet, k, distMeans =distEclud, createCent = randCent):
    m = dataSet.shape[0];counts=[]
    clusterAssments=[];centroids=[]
    clusterAssment = np.array(np.zeros((m,2)))    # 用于存放该样本属于哪类及质心距离
    # clusterAssment第一列存放该数据所属的中心点,第二列是该数据到中心点的距离
    centroid = createCent(dataSet, k)
    clusterChanged = True   # 用来判断聚类是否已经收敛
    while clusterChanged:
        clusterChanged = False
        count=0
        for i in range(m):  # 把每一个数据点划分到离它最近的中心点
            minDist = np.inf; minIndex = -1;
            for j in range(k):
                distJI = distMeans(centroid[j,:], dataSet[i,:-1])
                if distJI < minDist:
                    minDist = distJI; minIndex = j  # 如果第i个数据点到第j个中心点更近,则将i归属为j
            if clusterAssment[i,0] != minIndex: 
                clusterChanged = True;count+=1  # 如果分配发生变化,则需要继续迭代
                #print(clusterAssment[i,0],'-->',minIndex)
            clusterAssment[i,:] = minIndex,minDist**2   # 并将第i个数据点的分配情况存入字典

        for cent in range(k):   # 重新计算中心点
            ptsInClust = dataSet[clusterAssment[:,0] == cent][:,:-1]   # 去第一列等于cent的所有列
            centroid[cent,:] = np.mean(ptsInClust, axis = 0)  # 算出这些数据的中心点

        
        #此处为坑
#         centroids.append(centroid)
#         clusterAssments.append(clusterAssment)
        if clusterChanged==True:
            centroids.append(copy.copy(centroid))
            clusterAssments.append(copy.copy(clusterAssment))
            counts.append(count)
    return centroids, clusterAssments,counts
# --------------------测试----------------------------------------------------
# 用测试数据及测试kmeans算法
datMat=loadDataSet('f:/testSet.txt')
myCentroids,clustAssings,counts = kMeans(datMat,4)
print(myCentroids)
len(clustAssings)
counts # [58, 23, 3, 1]

运行结果:
![kmean2][base64str2]

画出每步迭代的图:

import matplotlib.pyplot as plt
fig1=plt.figure(1,figsize=(15,20))
for i in range(4):
    ax=fig1.add_subplot(32*10+i+1)
    s=clustAssings[i][:,0]+30
    c=clustAssings[i][:,0]+20
    ax.scatter(datMat[:,0],datMat[:,1],s,c)
    ax.scatter(myCentroids[i][:,0],myCentroids[i][:,1],s=150,c='r',marker='+')

plt.show()

结果共迭代了4步,每一步变动的点数分别为58, 23, 3, 1,画图如下:

kmeans聚类算法python肘部法则 kmeans聚类算法代码python_迭代_03

6、K-means算法补充

K-means算法的缺点及改进方法

(1)值的选择是用户指定的,不同的k得到的结果会有挺大的不同,如下图所示,上边是k=3的结果,这个就太稀疏了,蓝色的那个簇其实是可以再划分成两个簇的。而下图是k=5的结果,可以看到紫色和黄色这两个簇应该是可以合并成一个簇的:

![kmeans4][base64str4]

![kmeans5][base64str5]

kmeans聚类算法python肘部法则 kmeans聚类算法代码python_迭代_04

kmeans聚类算法python肘部法则 kmeans聚类算法代码python_聚类_05

改进:

对k的选择可以先用一些算法分析数据的分布,如重心和密度等,然后选择合适的k

(2)对k个初始质心的选择比较敏感,容易陷入局部最小值。例如,我们上面的算法运行的时候,有可能会得到不同的结果,如下面这两种情况。K-means也是收敛了,只是收敛到了局部最小值.

改进:

有人提出了另一个成为二分k均值(bisecting k-means)算法,它对初始的k个质心的选择就不太敏感.

(3)存在局限性,如下面这种非球状的数据分布就搞不定了:

kmeans聚类算法python肘部法则 kmeans聚类算法代码python_聚类_06

(4)数据集比较大的时候,收敛会比较慢.