">

一步步教你轻松学K-means聚类算法

摘要:k-均值算法(英文:k-means clustering),属于比较常用的算法之一,文本首先介绍聚类的理论知识包括什么是聚类、聚类的应用、聚类思想、聚类优缺点等等;然后通过k-均值聚类案例实现及其可视化有一个直观的感受,针对算法模型进行分析和结果优化提出了二分k-means算法。最后我们调用机器学习库函数,很短的代码完成聚类算法。(本文原创,转载必须注明出处.)

理论介绍

聚类

什么是聚类

统计数据分析的一门技术,在许多领域受到广泛应用,包括机器学习,数据挖掘,模式识别,图像分析以及生物信息。聚类是把相似的对象通过静态分类的方法分成不同的组别或者更多的子集(subset),这样让在同一个子集中的成员对象都有相似的一些属性,常见的包括在坐标系中更加短的空间距离等。

聚类的应用

在商务上,聚类能帮助市场分析人员从客户基本库中发现不同的客户群,并且用购买模式来刻画不同的客户群的特征。在生物学上,聚类能用于推导植物和动物的分类,对基因进行分类,获得对种群中固有结构的认识。聚类在地球观测数据库中相似地区的确定,汽车保险单持有者的分组,及根据房子的类型、价值和地理位置对一个城市中房屋的分组上也可以发挥作用。聚类也能用于对Web上的文档进行分类,以发现信息。诸如此类,聚类有着广泛的实际应用。

K-means(k均值)聚类算法

什么是k-means聚类算法

k-平均算法(英文:k-means clustering)源于信号处理中的一种向量量化方法,现在则更多地作为一种聚类分析方法流行于数据挖掘领域。k-平均聚类的目的是:把 n个点划分到k个聚类中,使得每个点都属于离他最近的均值(此即聚类中心)对应的聚类,以之作为聚类的标准。k-平均聚类与k-近邻之间没有任何关系(后者是另一流行的机器学习技术)。

K-Means 是发现给定数据集的 K 个簇的聚类算法, 之所以称之为 K-均值 是因为它可以发现 K 个不同的簇, 且每个簇的中心采用簇中所含值的均值计算而成.簇个数 K 是用户指定的, 每一个簇通过其质心(centroid), 即簇中所有点的中心来描述.
聚类与分类算法的最大区别在于, 分类的目标类别已知, 而聚类的目标类别是未知的.

发展历史

虽然其思想能够追溯到1957年的Hugo Steinhaus,术语“k-均值”于1967年才被James MacQueen 首次使用。标准算法则是在1957年被Stuart Lloyd作为一种脉冲码调制的技术所提出,但直到1982年才被贝尔实验室公开出版。在1965年,E.W.Forgy发表了本质上相同的方法,所以这一算法有时被称为Lloyd-Forgy方法。更高效的版本则被Hartigan and Wong提出。

算法描述

已知观测集\((x_1,x_2,…,x_n)\),其中每个观测都是一个 d-维实向量,k-平均聚类要把这 n个观测划分到k个集合中(k≤n),使得组内平方和最小。换句话说,它的目标是找到使得下式满足的聚类\( S_i\),

其中 \( \mu_i\) 是\(S_i\) 中所有点的均值。

k-means术语

  • 簇: 所有数据的点集合,簇中的对象是相似的。
  • 质心: 簇中所有点的中心(计算所有点的均值而来).
  • SSE: Sum of Sqared Error(误差平方和), 它被用来评估模型的好坏,SSE 值越小,表示越接近它们的质心. 聚类效果越 好。由于对误差取了平方,因此更加注重那些远离中心的点(一般为边界点或离群点)。详情见kmeans的评价标准。
    有关 簇 和 质心 术语更形象的介绍, 请参考下图:

k-means应用场景

kmeans,用于数据集内种类属性不明晰,希望能够通过数据挖掘出或自动归类出有相似特点的对象的场景。其商业界的应用场景一般为挖掘出具有相似特点的潜在客户群体以便公司能够重点研究、对症下药。

例如,在2000年和2004年的美国总统大选中,候选人的得票数比较接近或者说非常接近。任一候选人得到的普选票数的最大百分比为50.7%而最小百分比为47.9% 如果1%的选民将手中的选票投向另外的候选人,那么选举结果就会截然不同。 实际上,如果妥善加以引导与吸引,少部分选民就会转换立场。尽管这类选举者占的比例较低,但当候选人的选票接近时,这些人的立场无疑会对选举结果产生非常大的影响。如何找出这类选民,以及如何在有限的预算下采取措施来吸引他们? 答案就是聚类(Clustering)。

那么,具体如何实施呢?首先,收集用户的信息,可以同时收集用户满意或不满意的信息,这是因为任何对用户重要的内容都可能影响用户的投票结果。然后,将这些信息输入到某个聚类算法中。接着,对聚类结果中的每一个簇(最好选择最大簇 ), 精心构造能够吸引该簇选民的消息。最后, 开展竞选活动并观察上述做法是否有效。

另一个例子就是产品部门的市场调研了。为了更好的了解自己的用户,产品部门可以采用聚类的方法得到不同特征的用户群体,然后针对不同的用户群体可以对症下药,为他们提供更加精准有效的服务。

k-means算法思想

先随机选取K个对象作为初始的聚类中心。然后计算每个对象与各个种子聚类中心之间的距离,把每个对象分配给距离它最近的聚类中心。聚类中心以及分配给它们的对象就代表一个聚类。一旦全部对象都被分配了,每个聚类的聚类中心会根据聚类中现有的对象被重新计算。这个过程将不断重复直到满足某个终止条件。终止条件可以是以下任何一个:

  • 没有(或最小数目)对象被重新分配给不同的聚类。
  • 没有(或最小数目)聚类中心再发生变化。
  • 误差平方和局部最小。

得到相互分离的球状聚类,在这些聚类中,均值点趋向收敛于聚类中心。 一般会希望得到的聚类大小大致相当,这样把每个观测都分配到离它最近的聚类中心(即均值点)就是比较正确的分配方案。

k-means工作流程

创建 k 个点作为起始质心(通常是随机选择)
当任意一个点的簇分配结果发生改变时(不改变时算法结束)
    对数据集中的每个数据点
        对每个质心
            计算质心与数据点之间的距离
        将数据点分配到距其最近的簇
    对每一个簇, 计算簇中所有点的均值并将均值作为质心

k-means开发流程

收集数据:使用任意方法
准备数据:需要数值型数据类计算距离, 也可以将标称型数据映射为二值型数据再用于距离计算
分析数据:使用任意方法
训练算法:不适用于无监督学习,即无监督学习不需要训练步骤
测试算法:应用聚类算法、观察结果.可以使用量化的误差指标如误差平方和(后面会介绍)来评价算法的结果.
使用算法:可以用于所希望的任何应用.通常情况下, 簇质心可以代表整个簇的数据来做出决策.

k-means评价标准

k-means算法因为手动选取k值和初始化随机质心的缘故,每一次的结果不会完全一样,而且由于手动选取k值,我们需要知道我们选取的k值是否合理,聚类效果好不好,那么如何来评价某一次的聚类效果呢?也许将它们画在图上直接观察是最好的办法,但现实是,我们的数据不会仅仅只有两个特征,一般来说都有十几个特征,而观察十几维的空间对我们来说是一个无法完成的任务。因此,我们需要一个公式来帮助我们判断聚类的性能,这个公式就是SSE (Sum of Squared Error, 误差平方和 ),它其实就是每一个点到其簇内质心的距离的平方值的总和,这个数值对应kmeans函数中clusterAssment矩阵的第一列之和。 SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。 因为对误差取了平方,因此更加重视那些远离中心的点。一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。

k-means优缺点

  • 优点:

    属于无监督学习,无须准备训练集
    原理简单,实现起来较为容易
    结果可解释性较好

  • 缺点:

    聚类数目k是一个输入参数。选择不恰当的k值可能会导致糟糕的聚类结果。这也是为什么要进行特征检查来决定数据集的聚类数目了。
    可能收敛到局部最小值, 在大规模数据集上收敛较慢
    对于异常点、离群点敏感

使用数据类型 : 数值型数据


k-means聚类算法实现

案例描述

我们假设这样的一个案例需求:某公司发布一批新型手机,根据客户热衷度进行投放。公司市场人员收集其中四个地区用户对手机的满意程度(由两个特征决定的)。分析哪个区域对手机产品比较热衷,对应的进行市场销售工作。这里就用到k-means聚类算法。

从文件加载数据集

上文中我们收集好四个地区用户对产品满意的特征数据值,转化为向量预先保存到文本中(关于词向量转化及其词袋模型问题,参考:决策树算法模型研究与案例分析一文)。我们加载文件并以数据矩阵形式返回数据集,代码实现如下:


'''加载数据集'''
def loadDataSet(fileName):
    dataSet = [] # 初始化一个空列表
    fr = open(fileName)
    for line in fr.readlines():
        # 切割每一行的数据
        curLine = line.strip().split('\t')
        # 将数据追加到dataMat,映射所有的元素为 float类型
        fltLine = list(map(float,curLine))    
        dataSet.append(fltLine)
    return mat(dataSet)
我们打印看下结果: ![](https://i.imgur.com/zItcW74.png) ## 计算两个向量的欧氏距离 上文在k均值算法思想和工作流程都提到过,我们一个重要的方法就是随机设置质心,然后比较每条数据(可以理解为单一客户的特征数据)与质心之间的距离。这里距离公式包括很多,本文采用的是欧式距离计算,其代码实现如下:
'''欧氏距离计算函数'''
def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2)))

构建一个包含 K 个随机质心的集合

接下来,我们构建随机质心(中心点),这里的K值是经过数据观察随机设置的值,假如k=3,代表我们将数据集分为3个簇,也就是说分为3个部分。我们随机质心在整个数据集的边界之内,这可以通过找到数据集每一维的最小和最大值来完成,然后生成0到1.0之间的随机数并通过取值范围和最小值,以便确保随机点在数据的边界之内。


‘’’
随机质心
‘’’
def randCent(dataMat, k):

# 获取样本数与特征值
m, n = shape(dataMat)
# 初始化质心,创建(k,n)个以零填充的矩阵
centroids = mat(zeros((k, n)))
# 循环遍历特征值
for j in range(n):
    # 计算每一列的最小值
    minJ = min(dataMat[:, j])
    # 计算每一列的范围值
    rangeJ = float(max(dataMat[:, j]) - minJ)
    # 计算每一列的质心,并将值赋给centroids
    centroids[:, j] = mat(minJ + rangeJ * random.rand(k, 1))
# 返回质心
return centroids 

</pre>
我们测试下k=3的随机质心结果:

K-Means 聚类算法

我们基于以上算法构建k均值算法,该算法会创建k个质心,然后将每个点分配到最近的质心,再重新计算质心。这个过程重复数次,直到数据点的簇分配结果不再改变位置。返回类质心与点分配结果(多次运行结果可能会不一样,可以试试,原因为随机质心的影响,但总的结果是对的,因为数据足够相似,也可能会陷入局部最小值),代码实现如下:


‘’’
创建K个质心,然后将每个点分配到最近的质心,再重新计算质心。
这个过程重复数次,直到数据点的簇分配结果不再改变为止
‘’’
def kMeans(dataMat, k, distMeas=distEclud, createCent=randCent):

# 获取样本数和特征数
m, n = shape(dataMat)
# 初始化一个矩阵来存储每个点的簇分配结果
# clusterAssment包含两个列:一列记录簇索引值,第二列存储误差(误差是指当前点到簇质心的距离,后面会使用该误差来评价聚类的效果)
clusterAssment = mat(zeros((m, 2)))
# 创建质心,随机K个质心
centroids = createCent(dataMat, k)
# 初始化标志变量,用于判断迭代是否继续,如果True,则继续迭代
clusterChanged = True
while clusterChanged:
    clusterChanged = False
    # 遍历所有数据找到距离每个点最近的质心,
    # 可以通过对每个点遍历所有质心并计算点到每个质心的距离来完成
    for i in range(m):
        minDist = inf # 正无穷
        minIndex = -1
        for j in range(k):
            # 计算数据点到质心的距离
            # 计算距离是使用distMeas参数给出的距离公式,默认距离函数是distEclud
            distJI = distMeas(centroids[j, :], dataMat[i, :])
            # 如果距离比minDist(最小距离)还小,更新minDist(最小距离)和最小质心的index(索引)
            if distJI < minDist:
                minDist = distJI
                minIndex = j
        # 如果任一点的簇分配结果发生改变,则更新clusterChanged标志
        if clusterAssment[i, 0] != minIndex:
            # print(clusterAssment[i, 0],minIndex)
            clusterChanged = True
        # 更新簇分配结果为最小质心的index(索引),minDist(最小距离)的平方
        clusterAssment[i, :] = minIndex, minDist ** 2
    # print(centroids)
    # 遍历所有质心并更新它们的取值
    for cent in range(k):
        # 通过数据过滤来获得给定簇的所有点
        ptsInClust = dataMat[nonzero(clusterAssment[:, 0].A == cent)[0]]
        # 计算所有点的均值,axis=0表示沿矩阵的列方向进行均值计算
        centroids[cent, :] = mean(ptsInClust, axis=0)# axis=0列方向
# 返回所有的类质心与点分配结果
return centroids, clusterAssment

</pre>
测试查看下运行结果:

分析数据:聚类可视化

通过上文返回的数据结果,似乎我们还不能直观感受,接下来我们采用可视化分析方法直观感受下,代码实现如下:

'''
可视化展示
'''
def kmeanShow(dataMat,centers,clusterAssment):
    plt.scatter(np.array(dataMat)[:, 0], np.array(dataMat)[:, 1], c=np.array(clusterAssment)[:, 0].T)
    plt.scatter(centers[:, 0].tolist(), centers[:, 1].tolist(), c="r")
    plt.show()

测试查看可视化结果:

结果讨论与分析

局部最小值(局部最优的结果,但不是全局最优的结果)

上文可视化结果显示,其中两个簇聚集在一起,也就说说没有达到我们预期的效果。出现这个问题有很多原因,可能是k值取的不合适,可能是距离函数不合适,可能是最初随机选取的质心靠的太近,也可能是数据本身分布的问题。

为了解决这个问题,我们可以对生成的簇进行后处理,一种方法是将具有最大SSE值的簇划分成两个簇。具体实现时可以将最大簇包含的点过滤出来并在这些点上运行K-均值算法,令k设为2。

为了保持簇总数不变,可以将某两个簇进行合并。从上图中很明显就可以看出,应该将上图下部两个出错的簇质心进行合并。那么问题来了,我们可以很容易对二维数据上的聚类进行可视化, 但是如果遇到40维的数据应该如何去做?

有两种可以量化的办法:合并最近的质心,或者合并两个使得SSE增幅最小的质心。 第一种思路通过计算所有质心之间的距离, 然后合并距离最近的两个点来实现。第二种方法需要合并两个簇然后计算总SSE值。必须在所有可能的两个簇上重复上述处理过程,直到找到合并最佳的两个簇为止。

因为上述后处理过程实在是有些繁琐,所以有更厉害的大佬提出了另一个称之为二分K-均值(bisecting K-Means)的算法.

二分 K-Means 聚类算法

算法描述

该算法首先将所有点作为一个簇,然后将该簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇进行划分取决于对其划分时候可以最大程度降低 SSE(平方和误差)的值。上述基于 SSE 的划分过程不断重复,直到得到用户指定的簇数目为止。

二分 K-Means 聚类算法伪代码

将所有点看成一个簇
当簇数目小于 k 时
对于每一个簇
    计算总误差
    在给定的簇上面进行 KMeans 聚类(k=2)
    计算将该簇一分为二之后的总误差
选择使得误差最小的那个簇进行划分操作

另一种做法是选择 SSE 最大的簇进行划分,直到簇数目达到用户指定的数目位置。

二分 K-Means 聚类算法代码

根据算法思想,我们基于k均值算法做了少许的改动,代码实现如下:


‘’’在给定数据集,所期望的簇数目和距离计算方法的条件下,函数返回聚类结果’’’
def biKmeans(dataMat, k, distMeas=distEclud):
m, n = shape(dataMat)

# 创建一个矩阵来存储数据集中每个点的簇分配结果及平方误差
clusterAssment = mat(zeros((m, 2)))
# 计算整个数据集的质心,并使用一个列表来保留所有的质心
centroid0 = mean(dataMat, axis=0).tolist()[0]
centList = [centroid0] # [-0.15772275000000002, 1.2253301166666664]
# 遍历数据集中所有点来计算每个点到质心的误差值
for j in range(m):
    clusterAssment[j, 1] = distMeas(mat(centroid0), dataMat[j, :]) ** 2
# 对簇不停的进行划分,直到得到想要的簇数目为止
while (len(centList) < k):
    # 初始化最小SSE为无穷大,用于比较划分前后的SSE
    lowestSSE = inf
    # 通过考察簇列表中的值来获得当前簇的数目,遍历所有的簇来决定最佳的簇进行划分
    for i in range(len(centList)):
        # 对每一个簇,将该簇中的所有点堪称一个小的数据集
        ptsInCurrCluster = dataMat[nonzero(clusterAssment[:, 0].A == i)[0], :]
        # 将ptsInCurrCluster输入到函数kMeans中进行处理,k=2,
        # kMeans会生成两个质心(簇),同时给出每个簇的误差值
        centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)
        # 将误差值与剩余数据集的误差之和作为本次划分的误差
        sseSplit = sum(splitClustAss[:, 1])
        sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:, 0].A != i)[0], 1])
        print('sseSplit, and notSplit: ', sseSplit, sseNotSplit)
        # 如果本次划分的SSE值最小,则本次划分被保存
        if (sseSplit + sseNotSplit) < lowestSSE:
            bestCentToSplit = i
            bestNewCents = centroidMat
            bestClustAss = splitClustAss.copy()
            lowestSSE = sseSplit + sseNotSplit
    # 找出最好的簇分配结果
    # 调用kmeans函数并且指定簇数为2时,会得到两个编号分别为0和1的结果簇
    bestClustAss[nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
    # 更新为最佳质心
    bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
    print('the bestCentToSplit is: ', bestCentToSplit)
    print('the len of bestClustAss is: ', len(bestClustAss))
    # 更新质心列表
    # 更新原质心list中的第i个质心为使用二分kMeans后bestNewCents的第一个质心
    centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
    # 添加bestNewCents的第二个质心
    centList.append(bestNewCents[1, :].tolist()[0])
    # 重新分配最好簇下的数据(质心)以及SSE
    clusterAssment[nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss
return mat(centList), clusterAssment

</pre>

测试二分 KMeans 聚类算法

经过改进后,我们运行biKmeans函数得到可视化结果如下:

总结:如此我们得到预想的结果,解决了局部最优的问题,聚类会收敛到全局最小值。而原始的 kMeans() 函数偶尔会陷入局部最小值。

调用机器学习库sklearn实现k-means 聚类

加载数据集

加载数据集

dataMat = []
fr = open(“./testSet2.txt”) # 注意,这个是相对路径
for line in fr.readlines():
curLine = line.strip().split(‘\t’)
fltLine = list(map(float,curLine)) # 映射所有的元素为 float(浮点数)类型
dataMat.append(fltLine)
</pre>

训练k-means算法模型

km = KMeans(n_clusters=3) # 初始化
km.fit(dataMat) # 拟合
km_pred = km.predict(dataMat) # 预测
centers = km.cluster_centers_ # 质心

可视化结果

plt.scatter(np.array(dataMat)[:, 1], np.array(dataMat)[:, 0], c=km_pred)
plt.scatter(centers[:, 1], centers[:, 0], c="r")
plt.show()

聚类结果

参考文献

  1. scikit中文社区:http://sklearn.apachecn.org/cn/0.19.0/
  2. 中文维基百科:https://zh.wikipedia.org/wiki/K-%E5%B9%B3%E5%9D%87%E7%AE%97%E6%B3%95
  3. GitHub:https://github.com/BaiNingchao/MachineLearning-1
  4. 图书:《机器学习实战》
  5. 图书:《自然语言处理理论与实战》

完整代码下载

源码请进【机器学习和自然语言QQ群:436303759】文件下载:

作者声明

本文版权归作者所有,旨在技术交流使用。未经作者同意禁止转载,转载后需在文章页面明显位置给出原文连接,否则相关责任自行承担。

白宁超 wechat
扫一扫关注微信公众号,机器学习和自然语言处理,订阅号datathinks!