本节用Python实现K-Means算法,对未标注的数据进行聚类。

在做完聚类后,如何对于聚类结果进行评估?请看 用Python实现聚类效果的评估(轮廓系数、互信息)


导航

  • K-Means简介
  • 代码实现
    • (一)数据集读入
    • (二)距离计算
    • (三)构建随机质心
    • (四)数据聚类
    • (五)完整代码
  • 改进:采用二分法
    • (一)简介
    • (二)代码
  • 最后
  • 系列文章目录

K-Means简介

这里参考了大三专业课老师的PPT,现在回过头来看,老师当初讲得特别透彻,可惜没好好听,老师dbq (*>﹏<*)。

k-means算法,也被称为k-平均或k-均值算法,是一种使用最广泛的聚类算法。根据个体到每个类中心的距离进行划分,而类中心用类中所有个体的均值来度量。

思路及步骤:

  • 随机或按某种策略从 n 个对象中选择 k 个对象作为初始的类中心(Centriod,Mean Point);
  • 计算每个对象与这 k 个类中心的距离;
  • 将每个对象划分/分配到与其距离最近的类中心所在的类中;并重新计算每个类的类中心。
  • 回到第 2 步,直到和前一次划分/分配结果无差异,停止。


代码实现

(一)数据集读入

先查看一下 testSet.txt 数据集的格式,每一行有两个数据,用空格间隔开。

我们首先要将每一行用空格 split , 用 map 函数进行数值类型转化(转为 float 型),再保存到一个名为 dataMat 的列表中。代码如下所示:

# 读入数据,保存到列表
def loadDataSet(fileName):dataMat = []with open(fileName, "r", encoding='utf-8') as fn:for line in fn.readlines():curLine = line.strip().split('\t')fltLine = list(map(float, curLine))  # 数值类型转化,map()会根据提供的函数对指定序列做映射dataMat.append(fltLine)return dataMat

注:map ( ) 函数在 Python 2.x 返回列表,在 Python 3.x 返回迭代器,因此代码中用list()进行转换。效果如下:

(二)距离计算

采用欧式距离进行计算,也可以使用其他计算方法。

# 计算距离
def distCal(vecA, vecB):return np.sqrt(np.sum(np.power(vecA - vecB, 2)))# vecA, vecB都为数组的形式,类似于[1 2]# power(x1, x2)数组的元素分别求n次方。x2可以是数字,也可以是数组,但是x1和x2的列数要相同。

(三)构建随机质心

这一步用来随机生成质心。

  • 质心的表示与原数据相同,原数据保存的格式类似为[[1, 4], [-3, 3], [4, -1], ...] ,因此首先获取到每一行的数据有几个,用 shape [1] 来获取,得到 n 为2。
  • 接下来生成 k * n 的矩阵,用来保存 k 个质心。
  • 最后是表示质心的值,随机生成处于最大最小之间的值。获取第 j 列的最小值和幅度值,则质心的第 j 列的值 = 最小值 + (0-1随机数)* 幅度值。
# 为给定数据集构建一个随机质心矩阵
def randCent(dataSet, k):n = np.shape(dataSet)[1]  # shape函数的功能是读取矩阵的长度,比如shape[0]就是读取矩阵第一维度的长度。# shape[0]返回的是有多少行,shape[1]返回的是每一行有几个数据centroids = np.mat(np.zeros((k, n)))  # 生成k*n的矩阵,用0初始化for j in range(n):minJ = min(dataSet[:, j])rangeJ = float(max(dataSet[:, j]) - minJ)centroids[:, j] = minJ + rangeJ * np.random.rand(k, 1)  # random.rand(k, 1)生成k行1列的随机数组return centroids

(四)数据聚类

  • 首先找到dataSet一共有 m 行,构建 m * 20 矩阵,第一列存放的是该点所在簇的索引值,第二列存放该点与簇质心的距离。
  • 接下来创建创建一个标志变量clusterChanged,若为真,则继续迭代。
  • 对于每一行数据(视作一个点),分别计算与质心的距离,如果该距离小于之前的最小距离,说明该点离这个质心更近,同时更新簇的索引(或质心的索引)和距离质心的距离。判断该点簇的索引是否发生了变化。
  • 在每一行计算结束后,更新质心的位置。首先要进行数组的过滤。例如,cent = 1时,抽取簇索引为1的行保存在ptsInClust中,对于每一列的值求平均,获得新的质心的位置。
def kMeans(dataSet, k):m = np.shape(dataSet)[0]  # 一共有m行数据clusterAssment = np.mat(np.zeros((m, 2)))  # 生成m*2的0矩阵,第一列存放该点所在簇的索引,第二列存放该点与簇质心的距离centroids = randCent(dataSet, k)  # 调用函数,构建质心矩阵clusterChanged = True  # 创建一个标志变量,若为真,则继续迭代count = 0while clusterChanged:count += 1clusterChanged = False# 寻找最近的质心for i in range(m):  # 对于每一行的数据minIndex = -1  # 在未迭代之前,将簇的索引设为-1minDist = np.inf  # 在未迭代之前,将最小距离设为无穷大for j in range(k):  # 对于当前每一个质心distJI = distCal(centroids[j, :], dataSet[i, :])  # 计算第i个数据与第j个质心之间的距离if distJI < minDist:minIndex = j  # 若i与j之间距离小于目前的最小值,则更新当前点的索引为jminDist = distJI  # 若i与j之间距离小于目前的最小值,则更新当前最小值if clusterAssment[i, 0] != minIndex:  # 如果第i行的第一个值(簇的索引)不为当前的minIndex,说明簇发生了改变clusterChanged = True  # 继续迭代clusterAssment[i, :] = int(minIndex), minDist**2print(centroids)# 更新质心的位置for cent in range(k):category = np.nonzero(clusterAssment[:, 0].A == cent)  # 得到簇索引为cent的值的位置ptsInClust = dataSet[category[0]]centroids[cent, :] = np.mean(ptsInClust, axis=0)  # axis=0表示沿着矩阵列的方向进行均值计算print('*********************************************************************************')print('经过%d次迭代,最终的质心坐标为' % count)print(centroids)print('*********************************************************************************')print('打印所有点的索引及距离')print(clusterAssment)return centroids, clusterAssment

由于最后的更新质心的位置代码可能比较抽象,我做了一个小小的例子,帮助理解。

首先 clusterAssment = numpy.mat([[1, 0], [0, 1], [2, 0], [0, 3]]) 得到这样一个矩阵:

执行以下代码(判断第一列的值是否为0):

print(clusterAssment[:, 0].A == 0)  # 判断每一行第一列值是否为0
print('---------------')
print(nonzero(clusterAssment[:, 0].A == 0))  # 找到判断为True的索引
print('---------------')
print(nonzero(clusterAssment[:, 0].A == 0)[0])  # 返回第一行即它们所在的行数

效果如下:

第一个是判断,返回的是 TrueFalse ,我们发现,第一列为0对应第2行(索引1)和第4行(索引3);第二个是使用nonzero,找到显示为True的项,发现返回的有两行,[1, 3]表示行数,[0, 0]表示相对应的列,即[1, 0] [3, 0]位置的值为0;第三个直接返回第一行的值,即它们的行的索引。

因此,要想知道clusterAssment矩阵第一列为0的行并且打印出来,我们可以这样做:

category = nonzero(clusterAssment[:, 0].A == 0)[0]
print(clusterAssment[category])

(五)完整代码

import numpy as np# 读入数据,保存到列表
def loadDataSet(fileName):dataMat = []with open(fileName, "r", encoding='utf-8') as fn:for line in fn.readlines():curLine = line.strip().split('\t')fltLine = list(map(float, curLine))  # 数值类型转化,map()会根据提供的函数对指定序列做映射dataMat.append(fltLine)return dataMat# 计算距离
def distCal(vecA, vecB):return np.sqrt(np.sum(np.power(vecA - vecB, 2)))# vecA, vecB都为数组的形式,类似于[1 2]# power(x1, x2)数组的元素分别求n次方。x2可以是数字,也可以是数组,但是x1和x2的列数要相同。# 为给定数据集构建一个随机质心矩阵
def randCent(dataSet, k):n = np.shape(dataSet)[1]  # shape函数的功能是读取矩阵的长度,比如shape[0]就是读取矩阵第一维度的长度。# shape[0]返回的是有多少行,shape[1]返回的是每一行有几个数据centroids = np.mat(np.zeros((k, n)))  # 生成k*n的矩阵,用0初始化for j in range(n):minJ = min(dataSet[:, j])rangeJ = float(max(dataSet[:, j]) - minJ)centroids[:, j] = minJ + rangeJ * np.random.rand(k, 1)  # random.rand(k, 1)生成k行1列的随机数组return centroids# 数据聚类
def kMeans(dataSet, k):m = np.shape(dataSet)[0]  # 一共有m行数据clusterAssment = np.mat(np.zeros((m, 2)))  # 生成m*2的0矩阵,第一列存放该点所在簇的索引,第二列存放该点与簇质心的距离centroids = randCent(dataSet, k)  # 调用函数,构建质心矩阵clusterChanged = True  # 创建一个标志变量,若为真,则继续迭代count = 0while clusterChanged:count += 1clusterChanged = False# 寻找最近的质心for i in range(m):  # 对于每一行的数据minIndex = -1  # 在未迭代之前,将簇的索引设为-1minDist = np.inf  # 在未迭代之前,将最小距离设为无穷大for j in range(k):  # 对于当前每一个质心distJI = distCal(centroids[j, :], dataSet[i, :])  # 计算第i个数据与第j个质心之间的距离if distJI < minDist:minIndex = j  # 若i与j之间距离小于目前的最小值,则更新当前点的索引为jminDist = distJI  # 若i与j之间距离小于目前的最小值,则更新当前最小值if clusterAssment[i, 0] != minIndex:  # 如果第i行的第一个值(簇的索引)不为minIndex,说明簇发生了改变clusterChanged = True  # 继续迭代clusterAssment[i, :] = int(minIndex), minDist**2print(centroids)# 更新质心的位置for cent in range(k):category = np.nonzero(clusterAssment[:, 0].A == cent)  # 得到簇索引为cent的值的位置ptsInClust = dataSet[category[0]]centroids[cent, :] = np.mean(ptsInClust, axis=0)  # axis=0表示沿着矩阵列的方向进行均值计算print('*********************************************************************************')print('经过%d次迭代,最终的质心坐标为' % count)print(centroids)print('*********************************************************************************')print('打印所有点的索引及距离')print(clusterAssment)return centroids, clusterAssmentif __name__ == '__main__':data = np.mat(loadDataSet('testSet.txt'))kMeans(data, 4)

经过了五次迭代,效果如图:


改进:采用二分法

(一)简介

由于参数 k 的值是用户预先给定的,因此会出现 K-means 只收敛于局部最优,还存在有最好的结果,因此采用二分法(bisecting K-means)。

首先将所有的点看作一个簇,然后将该簇一分为二。之后选择其中一个簇继续划分,选择哪一个簇基于是否可以最大程度降低SSE的值。上述过程不断重复。

SSE(Sum of Squared Error,误差平方和),对应上文中 clusterAssment 第2列(索引为1)的值,SSE越小代表数据点越接近于它们的质心。对距离取平方,会放大那些距离较远的值。

伪代码如下:

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

(二)代码

在之前代码基础上,新增如下代码:

# 二分聚类
def biKMeans(dataSet, k):global bestCentToSplit, bestClustAss, bestNewCentsm = np.shape(dataSet)[0]  # 一共有m行数据clusterAssment = np.mat(np.zeros((m, 2)))  # 生成m*2的0矩阵,第一列存放该点所在簇的索引,第二列存放该点与簇质心的距离centroid0 = np.mean(dataSet, axis=0).tolist()[0]  # 计算整个数据集的质心,并且将之转换为列表centList = [centroid0]  # 用来存放质心的列表for j in range(m):clusterAssment[j, 1] = distCal(np.mat(centroid0), dataSet[j, :])**2  # 计算每一行与当前质心的距离的平方while len(centList) < k:# 寻找一个最佳的簇,使得分完后SSE最小lowestSSE = np.inf  # 设置初始最小SSE为无穷大for i in range(len(centList)):  # 对于质心的列表中的每一个簇ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:, 0].A == i)[0], :]  # 找到簇索引为i的行,其值保存在ptsInCurrClustercentroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2)  # 对这个簇进行k=2的聚类sseSplit = np.sum(splitClustAss[:, 1])  # 这个簇二分后,新的SSE值sseNotSplit = np.sum(clusterAssment[np.nonzero(clusterAssment[:, 0].A != i)[0], 1])  # 除了该簇的其他簇的SSE值print("新簇二分后 sseSplit = " + str(sseSplit))print("原先不含该簇 sseNotSplit = " + str(sseNotSplit))if (sseSplit + sseNotSplit) < lowestSSE:  # 如果二分后,SSE值小于目前的最小值bestCentToSplit = i  # 最应该被二分的簇的索引为ibestNewCents = centroidMat  # 新的质心bestClustAss = splitClustAss.copy()  # 新的点矩阵,第一列为索引,第二列为距离 [0, x] [1, y] ...lowestSSE = sseSplit + sseNotSplit  # 更新最小SSE# 到目前为止,已有一个最佳的簇等待被分类,接下来开始实行分类bestClustAss[np.nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit  # 将索引值为0的改为当前被二分的簇的索引bestClustAss[np.nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)  # 将索引值为1的改为centList的长度(实际就是新赋一个值)print("最佳二分的簇索引为:%d" % bestCentToSplit)print("该簇共有%d行" % len(bestClustAss))print('*****************************************************************')centList[bestCentToSplit] = bestNewCents[0, :]  # 将索引值为i的质心加入列表centList.append(bestNewCents[1, :])  # 将另一个质心加入列表clusterAssment[np.nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss  # 更新点矩阵print(centList)return centList, clusterAssment

运行 main 函数:

if __name__ == '__main__':data = np.mat(loadDataSet('testSet2.txt'))biKMeans(data, 3)

结果如下图所示:

用该代码跑第一次的数据集 testSet.txt ,发现结果与之前相同:


最后

  • 书上引进包的形式为from numpy import *,而我使用的是import numpy as np,从而使得所有numpy的函数都需要加一个np.,这样做一来是自己希望能够熟悉 numpy 的运算函数,另一方面,也看到博文说不建议使用前者,因为会与内置的函数混淆,例如numpy.sumsum使用会有小小差别。

  • 最后二分法我的运算结果与书上不太一致,差了一点点。我下载下了作者提供的源代码,运算结果和我目前的一样,不太清楚作者是否更改了数据集,还是我自身代码有误,还望各位指点。(已解决,K-Means是随机生成质心的,因此不同的初始质心会有不同的结果。)

  • 关于后期做K-Means的一点小小的心得,请看 K-Means聚类时可能存在的问题——薛定谔的最优解


系列文章目录

  利用K-Means聚类算法对未标注数据分组——《机器学习实战》学习笔记(Ch10)
  利用SVD(奇异值分解)实现推荐系统及图像压缩——《机器学习实战》学习笔记(Ch14)

  注:本系列主要参考《机器学习实战》—— Peter Harrington著,行文思路与书籍基本一致,在此也推荐大家购买正版书籍。本系列目的在于更加详细地解释书籍中一些难点,同时做一些扩展,可以看做对于书籍内容的补充;另一方面,也可以对于自己的想法以及学习历程做一个记录。转载请注明出处。

『ML』利用K-Means聚类算法对未标注数据分组——《机器学习实战》学习笔记(Ch10)相关推荐

  1. 机器学习实战(十)——利用K-均值聚类算法对未标注数据分组

    机器学习实战(十)--利用K-均值聚类算法对未标注数据分组 聚类是一种无监督的学习,即对于训练数据来说并没有已知标签,我们是根据样本之间的相似程度将其划分为多个类. 一.K-均值聚类算法 K-均值算法 ...

  2. 机器学习——利用K-均值聚类算法对未标注数据分组

    聚类是一种无监督的学习,它将相似的对象归到同一簇中.它有点像全自动分类.聚类方法几乎可以应用到所有对象,簇内的对象越相似,聚类的效果越好. K-均值(K-means)聚类算法,之所以称之为K-均值是因 ...

  3. 利用K-均值聚类算法对未标注数据分组

    无监督学习简介 无监督学习是一种机器学习的训练方式,它本质上是一个统计方法,在没有标签的数据里可以发现潜在的一些结构的一种训练方式. 无监督学习主要具备3个特点: 无监督学习没有明确的目的 无监督学习 ...

  4. 《机器学习实战》——第10章 利用K-均值聚类算法对未标注数据分组

    聚类是一种无监督的学习,它将相似的对象归到同一个簇中.它有点像全自动分类.聚类方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好. 簇识别给出聚类结果的含义.假定有一些数据,现在将相似数据归 ...

  5. K-均值聚类算法对未标注数据分组(1)

    1. K-均值聚类算法 优点:容易实现. 缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢. 适用数据类型:数值型数据. K-均值算法的工作流程是这样的.首先,随机确定&个初始点作为质心. ...

  6. 机器学习实战(十)利用K-means算法对未标注数据分组

    第十章 利用K-means算法对未标注数据分组 10.1 K-均值聚类算法 10.2 使用后处理来提高聚类性能 10.3 二分K-均值算法 10.4 总结 第十章 利用K-means算法对未标注数据分 ...

  7. k means聚类算法_一文读懂K-means聚类算法

    1.引言 什么是聚类?我们通常说,机器学习任务可以分为两类,一类是监督学习,一类是无监督学习.监督学习:训练集有明确标签,监督学习就是寻找问题(又称输入.特征.自变量)与标签(又称输出.目标.因变量) ...

  8. k means聚类算法_K-Means 聚类算法 20210108

    说到聚类,应先理解聚类和分类的区别 聚类和分类最大的不同在于:分类的目标是事先已知的,而聚类则不一样,聚类事先不知道目标变量是什么,类别没有像分类那样被预先定义出来. K-Means 聚类算法有很多种 ...

  9. 机器学习实战学习笔记 一 k-近邻算法

    k-近邻算法很简单,这里就不赘述了,主要看一下python实现这个算法的一些细节.下面是书中给出的算法的具体实现. def clssify(inX,dataset,label,k):#计算距离data ...

最新文章

  1. python history没有定义_python – AttributeError:’Tensor’对象没有属性’_keras_history’...
  2. 游戏产品开发流程-leangoo
  3. linux检查正则表达式,正则表达式及Linux文本检查工具
  4. HDU.3177Crixalis's Equipment(贪心)
  5. 一个c语言构造函数调用的问题(有趣)
  6. android jsoup简书,jsoup爬虫简书首页数据做个小Demo
  7. EcStore中的App是什么东西?
  8. 13.  Roman to Integer
  9. Spring Cloud构建微服务架构—服务网关过滤器
  10. 速修复!CISA警告称 Zoho 服务器0day已遭在野利用
  11. mschart mysql_在VB mschart里面可以一个MSCHART同时显示曲线和状图吗?
  12. 在属性级情感分析中结合BERT和语法信息
  13. 关于Java字符串的几个重点
  14. 人件管理与中国古代史:程序员豫让
  15. 后危机时代,DCS的新征程
  16. Linux如何自定义屏幕分辨率,Linux手动设置屏幕分辨率的办法
  17. hist seg, find peaks, tps, pava单调拟合, isotonic-regression,REGULARIZED DISCRETE OPTIMAL TRANSPORT
  18. visual studio 调试python_visual studio code 里调试运行 Python代码
  19. APP小程序网站搭建需要什么样的服务器
  20. 经典的经济、金融、投资书籍

热门文章

  1. detail texture与splat texture
  2. 令人惊讶的胃灼热原因
  3. 在计算机上收回光驱快捷键,如何在Windows中使用键盘快捷键打开光驱
  4. 黑马程序员linux运维上半部分笔记
  5. 十大运动蓝牙耳机品牌排行榜,排名最靠前的运动耳机推荐
  6. 内网服务器管理面板安装教程
  7. 【论文阅读】使用周期一致的对抗网络的非匹配的图片到图片的翻译
  8. WIn10+Ubuntu 双系统时间同步和默认启动项设置
  9. android 数组转换成list,数组转换成List集合
  10. Java 集合转数组,数组转集合