『ML』利用K-Means聚类算法对未标注数据分组——《机器学习实战》学习笔记(Ch10)
本节用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 * 2 的 0 矩阵,第一列存放的是该点所在簇的索引值,第二列存放该点与簇质心的距离。 - 接下来创建创建一个标志变量
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]) # 返回第一行即它们所在的行数
效果如下:
第一个是判断,返回的是 True 和 False ,我们发现,第一列为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.sum
和sum
使用会有小小差别。最后二分法我的运算结果与书上不太一致,差了一点点。我下载下了作者提供的源代码,运算结果和我目前的一样,不太清楚作者是否更改了数据集,还是我自身代码有误,还望各位指点。(已解决,K-Means是随机生成质心的,因此不同的初始质心会有不同的结果。)
关于后期做K-Means的一点小小的心得,请看 K-Means聚类时可能存在的问题——薛定谔的最优解
系列文章目录
利用K-Means聚类算法对未标注数据分组——《机器学习实战》学习笔记(Ch10)
利用SVD(奇异值分解)实现推荐系统及图像压缩——《机器学习实战》学习笔记(Ch14)
注:本系列主要参考《机器学习实战》—— Peter Harrington著,行文思路与书籍基本一致,在此也推荐大家购买正版书籍。本系列目的在于更加详细地解释书籍中一些难点,同时做一些扩展,可以看做对于书籍内容的补充;另一方面,也可以对于自己的想法以及学习历程做一个记录。转载请注明出处。
『ML』利用K-Means聚类算法对未标注数据分组——《机器学习实战》学习笔记(Ch10)相关推荐
- 机器学习实战(十)——利用K-均值聚类算法对未标注数据分组
机器学习实战(十)--利用K-均值聚类算法对未标注数据分组 聚类是一种无监督的学习,即对于训练数据来说并没有已知标签,我们是根据样本之间的相似程度将其划分为多个类. 一.K-均值聚类算法 K-均值算法 ...
- 机器学习——利用K-均值聚类算法对未标注数据分组
聚类是一种无监督的学习,它将相似的对象归到同一簇中.它有点像全自动分类.聚类方法几乎可以应用到所有对象,簇内的对象越相似,聚类的效果越好. K-均值(K-means)聚类算法,之所以称之为K-均值是因 ...
- 利用K-均值聚类算法对未标注数据分组
无监督学习简介 无监督学习是一种机器学习的训练方式,它本质上是一个统计方法,在没有标签的数据里可以发现潜在的一些结构的一种训练方式. 无监督学习主要具备3个特点: 无监督学习没有明确的目的 无监督学习 ...
- 《机器学习实战》——第10章 利用K-均值聚类算法对未标注数据分组
聚类是一种无监督的学习,它将相似的对象归到同一个簇中.它有点像全自动分类.聚类方法几乎可以应用于所有对象,簇内的对象越相似,聚类的效果越好. 簇识别给出聚类结果的含义.假定有一些数据,现在将相似数据归 ...
- K-均值聚类算法对未标注数据分组(1)
1. K-均值聚类算法 优点:容易实现. 缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢. 适用数据类型:数值型数据. K-均值算法的工作流程是这样的.首先,随机确定&个初始点作为质心. ...
- 机器学习实战(十)利用K-means算法对未标注数据分组
第十章 利用K-means算法对未标注数据分组 10.1 K-均值聚类算法 10.2 使用后处理来提高聚类性能 10.3 二分K-均值算法 10.4 总结 第十章 利用K-means算法对未标注数据分 ...
- k means聚类算法_一文读懂K-means聚类算法
1.引言 什么是聚类?我们通常说,机器学习任务可以分为两类,一类是监督学习,一类是无监督学习.监督学习:训练集有明确标签,监督学习就是寻找问题(又称输入.特征.自变量)与标签(又称输出.目标.因变量) ...
- k means聚类算法_K-Means 聚类算法 20210108
说到聚类,应先理解聚类和分类的区别 聚类和分类最大的不同在于:分类的目标是事先已知的,而聚类则不一样,聚类事先不知道目标变量是什么,类别没有像分类那样被预先定义出来. K-Means 聚类算法有很多种 ...
- 机器学习实战学习笔记 一 k-近邻算法
k-近邻算法很简单,这里就不赘述了,主要看一下python实现这个算法的一些细节.下面是书中给出的算法的具体实现. def clssify(inX,dataset,label,k):#计算距离data ...
最新文章
- python history没有定义_python – AttributeError:’Tensor’对象没有属性’_keras_history’...
- 游戏产品开发流程-leangoo
- linux检查正则表达式,正则表达式及Linux文本检查工具
- HDU.3177Crixalis's Equipment(贪心)
- 一个c语言构造函数调用的问题(有趣)
- android jsoup简书,jsoup爬虫简书首页数据做个小Demo
- EcStore中的App是什么东西?
- 13. Roman to Integer
- Spring Cloud构建微服务架构—服务网关过滤器
- 速修复!CISA警告称 Zoho 服务器0day已遭在野利用
- mschart mysql_在VB mschart里面可以一个MSCHART同时显示曲线和状图吗?
- 在属性级情感分析中结合BERT和语法信息
- 关于Java字符串的几个重点
- 人件管理与中国古代史:程序员豫让
- 后危机时代,DCS的新征程
- Linux如何自定义屏幕分辨率,Linux手动设置屏幕分辨率的办法
- hist seg, find peaks, tps, pava单调拟合, isotonic-regression,REGULARIZED DISCRETE OPTIMAL TRANSPORT
- visual studio 调试python_visual studio code 里调试运行 Python代码
- APP小程序网站搭建需要什么样的服务器
- 经典的经济、金融、投资书籍