机器学习(二):二分K-means算法
在前一节的内容已经介绍了k-means算法的原理和代码实现,如果没有了解过K-means的同学建议先了解机器学习(二):k-means算法(基础篇)
二分k-means是k-means算法的一种优化,二分k-means算法很好的解决了k-means算法的局部最优的问题。接下来我们来了解一下二分k-means的神奇之处
二分k-means算法
二分k-means算法是分层聚类(Hierarchical clustering)的一种,分层聚类是聚类分析中常用的方法。
分层聚类的策略一般有两种:
- 聚合。这是一种自底向上的方法,每一个观察者初始化本身为一类,然后两两结合
- 分裂。这是一种自顶向下的方法,所有观察者初始化为一类,然后递归地分裂它们
二分k-means算法是分裂法的一种。
二分k-means算法的优点
二分k-means算法是k-means算法的改进算法,相比k-means算法,它有如下优点:
- 二分k-means算法可以加速k-means算法的执行速度,因为它的相似度计算少了
- 能够克服k-means收敛于局部最小的缺点
二分k-means算法的步骤
二分k-means算法的一般流程如下所示:
- 把所有数据初始化为一个簇,将这个簇分为两个簇。
- 选择满足条件的可以分解的簇。选择条件综合考虑簇的元素个数以及聚类代价(也就是误差平方和SSE),误差平方和的公式如下所示,其中w(i) 表示权重值,y∗该簇所有点的平均值。
- 使用k-means算法将可分裂的簇分为两簇
- 一直重复(2)(3)步,直到满足迭代结束条件。
以上过程隐含着一个原则是:因为聚类的误差平方和能够衡量聚类性能,该值越小表示数据点越接近于它们的质心,聚类效果就越好。
所以我们就需要对误差平方和最大的簇进行再一次的划分,因为误差平方和越大,表示该簇聚类越不好,越有可能是多个簇被当成一个簇了,所以我们首先需要对这个簇进行划分。
二分K-means算法的实践
扩展任务
- 现二分 K-means 代码并进行测试(会在下一节中提到)
数据集
- 网址 http://archive.ics.uci.edu/ml/index.php
- 内容:Iris 数据集
数据集介绍:
Iris.data 数据集主要有如下:
sl | 花萼长度 |
---|---|
sw | 花萼宽度 |
pl | 花瓣长度 |
pw | 花瓣宽度 |
variety | 花的品种 |
实现二分k-means算法
(1) 算法思路:
二分 k-means 算法,此算法不需要标签变量,在 k-means 算法的基础上需要通过四个特征变量将 Iris 进行聚类。目标:通过 Iris 的四个特征值进行聚类,得到每个聚类中的质心,并把聚类结果写入文件中。
(2) 算法原理基础:
在原理上跟二分 k-means 上差不多相同。
(3) 算法步骤:
- 把整个数据集看成一个簇,计算质心
- 将这个簇分成两个簇
- 选择满足条件的可以分解的簇,选择条件为簇元素的个数和 SSE 大小
- 使用 k-mean 算法将可分裂的簇分成两个簇
- 重复(2)(3)步,直到满足 k 值
(4) 算法相关函数的实现:
- loadDataSet(): 读入数据,得到四个特征变量
- distEclud(): 欧式距离公式
- randCent(): 生成随机 k 个质心
- Kmeans(): k-means 函数
- chooseK(): 画出肘部图
- biKmeans():二分 k-means 的主函数,主要算法
- writeTxt(): 写入文件
(5)代码实现:
# -*- coding:utf-8 -*-
import warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
warnings.filterwarnings('ignore') #忽略警告
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
#伪代码如下
# 将所有点看成一个簇
# 当簇数目小于k时
# 对于每一个簇
# 计算总误差
# 在给定的簇上面进行k-均值聚类(k=2)
# 计算将该簇一分为二之后的总误差
# 选择使得总误差最小的簇进行划分
def loadDataSet(filename):
"""
函数说明:从文件中下载数据,并将分离除连续型变量和标签变量
:parameter:
data - Iris数据集
attributes - 鸢尾花的属性
type - 鸢尾花的类别
sl-花萼长度 , sw-花萼宽度, pl-花瓣长度, pw-花瓣宽度
:return:
"""
iris_data = pd.read_csv(filename) #打开文件
iris_data = pd.DataFrame(data=np.array(iris_data), columns=['sl', 'sw', 'pl', 'pw', 'type'], index=range(149)) #给数据集添加列名,方便后面的操作
attributes = iris_data[['sl', 'sw', 'pl', 'pw']] #分离出花的属性
iris_data['type'] = iris_data['type'].apply(lambda x: x.split('-')[1]) # 最后类别一列,感觉前面的'Iris-'有点多余即把class这一列的数据按'-'进行切分取切分后的第二个数据
labels = iris_data['type'] #分理出花的类别
attriLabels = [] #建立一个标签列表
for label in labels: #为了更方便操作,将三中不同的类型分别设为1,2,3
if label == 'setosa': #如果类别为setosa的话,设为1
attriLabels.append(1)
elif label == 'versicolor': #如果是versicolor的时候设为2
attriLabels.append(2)
elif label == 'virginica': #如果是virginica的时候设为3
attriLabels.append(3)
attributes = attributes.values
return attributes, attriLabels
def randCent(dataSet, k):
"""
函数说明:随机初始化k个质心(质心满足数据边界之内)
:param dataSet:数据集
:param k: 质点个数
:return:centroids 返回初始化得到的k个质心向量
"""
m, n = dataSet.shape # 得到行列数目
centroids = np.zeros((k, n)) # 定义一个k行n列的全为0的质点集合
for j in range(n):
#得到该列数据的最小值
minJ = min(dataSet[:, j])
#得到该列数据的范围
rangeJ = float(max(dataSet[:, j]) - minJ)
#k个质心向量的第j维数据值随机为位于范围内的某一个值
centroids[:, j] = np.mat(minJ + rangeJ * np.random.rand(k))
return centroids
def distEclud(vecA,vecB):
"""
函数说明:欧式距离公式
:param vecA:样本点
:param vecB:质心
:return:距离
"""
return np.sqrt(np.sum((vecA - vecB) ** 2))
def KMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
"""
函数说明:k均值算法
:param dataSet: 特征集
:param k: k个值
:return:centroids, clusterAssment
"""
m = np.shape(dataSet)[0] #得到行的数目
clusterAssment = np.mat(np.zeros((m, 2))) #初始化一个(m,2)矩阵
clusterChange = True #定义聚类是否发生变化
centroids = createCent(dataSet, k) #创建初始化的k个质心向量
while clusterChange: #只要聚类结果一直发生变化,就一直执行聚类算法,直到所有数据点聚类结果不发生变化
clusterChange = False #聚类结果定义为False
for i in range(m): #遍历数据集中的每一个样本
minDist = np.inf #初始化最小距离为100000
minIndex = -1 #最小距离索引定为-1
for j in range(k): #循环k个类的质心
distance = distMeas(centroids[j, :], dataSet[i, :]) #计算数据点到质心的欧式距离
if distance < minDist: #如果当前距离少于最小距离
minDist = distance#当前距离定为最小距离,最小距离的索引定为j
minIndex = j
if clusterAssment[i, 0] != minIndex: #当前聚类结果中第i个样本的结果发生变化
clusterChange = True #把clusterChange定义为Ture,代表发生了变化
clusterAssment[i, :] = minIndex, minDist**2 #更新当前新变化的聚类结果和错误平方
for j in range(k): #遍历每一个质心
#因此首先先比较clusterAssment[:,0].A==cent的真假,如果为真则记录了他所在的行,因此在用切片进行取值。
# print(clusterAssment[:, 0].A == j)
if (clusterAssment[:, 0].A == j).all() == False: #再chooseK防止报错
continue
pointsInCluster = dataSet[np.nonzero(clusterAssment[:, 0].A == j)[0]]
# 计算这些数据的均值(axis=0:求列的均值),作为该类质心向量
centroids[j, :] = np.mean(pointsInCluster, axis=0)
# #返回k个聚类,聚类结果和误差
return centroids, clusterAssment
def biKmeans(dataSet, k, distMeans = distEclud):
"""
函数说明:二分k-均值聚类算法
:param dataSet: 待聚类的数据集
:param k: 聚类的个数
:param distMeans: 用户指定的距离计算方法,这里为欧式距离公式
:return: np.mat(centList)-质心向量
clusterAssment - 聚类结果
"""
m = np.shape(dataSet)[0] #获得数据集的样本数
clusterAssment = np.mat(np.zeros((m, 2))) #初始化一个元素全为0的(m,2)的矩阵
centroid0 = np.mean(dataSet, axis=0).tolist()[0] #获取数据每一列的均值,组成一个一维数组
centList = [centroid0] #当前聚类列表将数据聚为一类
for j in range(m): #遍历数据中的每个数据集样本
#计算当前聚类为一类时各个数据点距离质心的平方距离
clusterAssment[j, 1] = distMeans(np.mat(centroid0), np.mat(centroid0)) ** 2
while (len(centList) < k): #循环,直到达到k类
lowestSSE = np.inf #将当前最小误差设置为正无穷大
for i in range(len(centList)): #遍历每个聚类
##因此首先先比较clusterAssment[:,0].A==cent的真假,如果为真则记录了他所在的行,因此在用切片进行取值。
ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:, 0].A==i)[0], :]
# 对该类利用二分k-均值算法进行划分,返回划分后结果,及误差
centroidMat, splitClustAss = KMeans(ptsInCurrCluster, 2, distMeans)
#计算该划分后两个类的误差平方和
sseSplit = np.sum(splitClustAss[:, 1])
#计算数据集中不属于该类的数据的误差平方和
sseNotSplit = np.sum(clusterAssment[np.nonzero(clusterAssment[:, 0].A!=i)[0], 1])
#划分第i类后总误差小于当前最小总误差
if(sseSplit + sseNotSplit) < lowestSSE:
# 第i类作为本次划分类
bestCentToSplit = i
# 第i类划分后得到的两个质心向量
bestNewCents = centroidMat.copy()
# 复制第i类中数据点的聚类结果即误差值
bestClustAss = splitClustAss.copy()
# 将划分第i类后的总误差作为当前最小误差
lowestSSE = sseSplit + sseNotSplit
# 数组过滤筛选出本次2-均值聚类划分后类编号为1数据点,将这些数据点类编号变为1
# 当前类个数+1,作为新的一个聚类
bestClustAss[np.nonzero(bestClustAss[:, 0].A == 1)[0], 0] = len(centList)
# 同理,将划分数据集中类编号为0的数据点的类编号仍置为被划分的类编号,使类编号
# 连续不出现空缺
bestClustAss[np.nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
# # 更新质心列表中的变化后的质心向量
centList[bestCentToSplit] = bestNewCents[0, :]
# 添加新的类的质心向量
centList.append(bestNewCents[1, :])
# 更新clusterAssment列表中参与2-均值聚类数据点变化后的分类编号,及数据该类的误差平方
clusterAssment[np.nonzero(clusterAssment[:, 0].A == bestCentToSplit)[0], :] = bestClustAss.copy()
#返回聚类结果
return np.mat(centList), clusterAssment
def chooseK(dataSet, i):
"""
函数说明:肘部图的绘画
:param dataSet: 数据集
:param i:k从1开始迭代到k次
:return:none
"""
list = [] #定义存放距离的列表,即y轴
x= [] #定义存放1,2,3,4,5,即x轴
for j in range(1, i):
#得到聚点, 聚类结果,和误差
cenList, clusterAssment = KMeans(dataSet, j)
#计算每个点的误差平方和,并加入到列表中
list.append(sum(clusterAssment[:, 1]))
#添加x轴
x.append(j)
#将list转变为列表
a = np.array(list)
list = a.reshape(-1)
#以x为画图
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, list)
plt.show()
def writeTxt(cenList1, clusterAssment, type=1):
"""
函数说明:将质心和分类结果写入文件的操作。
:param cenList1:质心
:param clusterAssment:分类结果
:param type: 1为二分k-means, 2为k-means
:return: none
"""
#将clusterAssment变成列表
clusterAssment = clusterAssment.tolist()
#获得cenList的长度
n = len(cenList1)
#获得clusterAssment的长度
m = len(clusterAssment)
#判断是否为k-means
if type == 1:
file = open('consequeue01.txt', mode='w') #如果是二分k-means则打开01文件
file.write("二分k-means的聚类后的质心")
else:
file = open('consequeue02.txt', mode='w') #如果是k-means则打开02文件
file.write("k-means的聚类后的质心")
for j in range(n):
file.write("\n第%d个质心为:" % (j+1)) #输入质心
file.write(str(cenList1[j])) #输入质心
file.write("\n聚类结果:\n")
for i in range(m):
file.write('第%d个属性被归类为:' % (i+1)) #输入类别
file.write(str(int(clusterAssment[i][0])))
file.write(" ")
file.write("距离为:")
file.write(str(clusterAssment[i][1])) #输入距离
file.write("\n")
file.close()
#二分k-means算法
if __name__ == '__main__':
filename = "iris.data" #文件路径
attributes, labels = loadDataSet(filename) #得到数据
centList, clusterAssment = biKmeans(attributes, 3) #k=3时得到质心和分类结果
writeTxt(centList, clusterAssment, 1) #写入文件
运行结果如下:
有图可看出每个Iris都被到较为可能的类别。到这里,二分k-means已经实现。