K-Means 算法聚类
非监督学习: 从数据中发现隐含的关系 对数据进行聚类 cluster
监督学习: 根据已有的历史数据 对数据进行分类 classification
K-Means 算法
问题:如何对数据进行聚类?
- 假设数据集T中, 由K类的数据, 但是如何确定这些数据之间存在关系
损失函数: 平方误差函数 我们可以以它们之间距离度量确定数据之间存在关系,越是相似,那么距离度量就越近
算法思路:
确定K个质心, 将数据分为k类, 实质上质心的值就是数据集中所属于第k个质心的样本数据子集的均值
确定算法思路之后,尝试实现算法
已知条件: 数据集 损失函数 需要选择质心 遍历数据集与每个质心的距离 根据损失函数进行重新分配质心 遍历执行直到聚类效果达到最优
分解问题:
step1: 读取数据集
step2: 创建损失函数 SSE sum of Square
step3: 初始化质心选择 选择k个质心
step4: k-means 算法实现
算法主体step4
step4 算法实现伪代码;
数据集维度为mxn
创建K个点作为启始质心
创建维度为(mx2)的矩阵clusterment 第一列记录分配的质心 第二列记录与该质心的损失函数度量
while True 循环 (有且仅当数据分配不改变时,停止循环)
遍历数据集中的每一个点
遍历每一个质心
计算每一个点与每一个质心的损失函数大小
比较大小,记录min_error and best_k 最小损失函数 以及对应的质心
将数据点分配到距离最近的簇中
并且检查数据分配是否仍然变动 没有变动了就停止循环
同时更新每个质心的值
每个质心的都是数据集中所属于该质心的样本数据集的均值 (因此才是k-Means算法)
返回 更新后的质心以及 clusterments
K-Means算法实现
import numpy as np
def load_data(filename):
fr = open(filename)
data_set = list()
for lines in fr.readlines():
line = lines.strip().split('\t')
line_list = list()
for i in line:
line_list.append(float(i))
data_set.append(line_list)
data_set = np.mat(data_set)
return data_set
def calc_dist_sse(data_set, centroids):
"""
k-means 距离度量方式 sum of square error
:param data_set:
:param centroids:
:return:
"""
# print('data set', type(data_set))
# print('centriods', type(centroids))
return np.sqrt(np.sum(np.power(data_set - centroids, 2)))
def create_centroids(data_set, k):
"""
k means 算法 随机选取质心
:param data_set: 数据集
:param k: k个质心
:return
centroids
"""
m, n = np.shape(data_set)
# print('shape data set', np.shape(data_set))
centroids = np.mat(np.zeros((k, n)))
# print('centroids', np.shape(centroids))
for row in range(k):
for index in range(n):
range_min = np.min(data_set[:, index])
range_of = np.max(data_set[:, index]) - range_min
centroids[row, index] = range_min + np.random.uniform(range_of)
# print('centroids\n', centroids)
return centroids
def k_means(data_set, k, calc_dist, create_centroids=create_centroids):
"""
k means 算法主体
计算质心——分配——重新计算
实现重点:
part1: 通过对每个点遍历所有质心并计算点到每个置信的距离
part2: 遍历所有质心并更新它们的取值
:param data_set:数据集
:param k:k个簇
:param calc_dist: 指向计算距离的函数
:param create_centroids: 指向创建质心的函数
:return:
centroids: 最终确定的质心
cluster_ment: 聚类的情况(class, dist) 分配到哪个质心附近 以及dist距离
"""
m, n = np.shape(data_set)
cluster_ment = np.mat(np.zeros((m, 2)))
centroids = create_centroids(data_set, k)
print('init centriods', centroids)
cluster_changed = True
count = 0
while cluster_changed:
count += 1
cluster_changed = False
# step1 通过对每个点遍历所有质心并计算每个点到每个质心的距离
for i in range(m): # 循环每一个数据点并分配到最近的质心中去
# print(np.inf, -1)
min_dist = np.inf
min_index = -1
for j in range(k):
dist_ji = calc_dist(data_set[i, :], centroids[j, :]) # 计算数据点到质心的最小距离
if dist_ji < min_dist: # 如果距离比最小距离还小, 就更新最小距离和最小质心的index
min_dist = dist_ji
min_index = j
if cluster_ment[i, 0] != min_index: # 簇分配结果改变
cluster_changed = True
cluster_ment[i, :] = min_index, min_dist**2 # 更新簇分配结果为最小质心的索引,最小距离
# step2 遍历所有质心并更新它们的取值
for cent in range(k):
pts_in_cluster = data_set[np.nonzero(cluster_ment[:, 0].A == cent)[0]] # 获取该簇中的所有点
centroids[cent, :] = np.mean(pts_in_cluster, axis=0) # 将质心修改为簇中所有点的平均值
print('centriods changed', centroids)
print('count', count)
return centroids, cluster_ment
K-Means聚类算法缺陷
缺陷:当面对数据分布过于’散乱’的时候,以及初始质心选择不合理(靠的太近), k取值不合理(不好把握数据聚类中到底几类), 损失函数不合理
当出现上述情况的时候,k-means算法可能会陷入局部最优,而不是全局最优
解决问题的思路:
- 什么时候才是全局最优?
- 已知损失函数 J(θ)=(y(i)−y^)2−−−−−−−−√ J ( θ ) = ( y ( i ) − y ^ ) 2
- 有且仅当存在 argmin∑i∈kNJ(θ) arg min ∑ i ∈ k N J ( θ )
- 即: 所有样本所分配的质心与其损失函数之和 最小时才是达到了最优化
算法实现思路:
对生成后的簇进行处理
1.’大簇’,第k个质心点若是分配的样本数据点较多,就尝试将该簇分解为两个小簇并计算最大SSE 与没有改变之前相比较, 小就分解成功
2.对两个’小簇’进行合并,并计算SEE之和, 与未合并之前比较, 若是小于就合并成功,反之不需要分解
针对这一思路: 处理生成后的簇较为复杂,因此出现了K-Means算法的优化算法: b-k-means算法 二分k-Means算法
二分K-Means算法
解决问题: 为克服K-Means算法收敛于局部最小值的问题
优化思路;
在上述解决K-Means算法缺陷的思路中,我们是对应生成后的簇进行优化
那么如果在生成之前进行优化呢
即:算法一开始, 就将数据集看做一个大簇,然后再分解优化!!!
算法思路:
首先将所有点看做一个簇, 然后将该簇一分为二
之后选择其中的一个簇,继续划分
问题? 如果选择? 取决于对其划分是否可以最大程度的降低SSE的值
迭代上述步骤, 直到得到用户指定的簇数目
算法实现:
- 以矩阵形式处理数据集 维度为mxn
- 一开始将所有数据点看做一个簇, 因此初始化质心 该质心为所有数据点的均值
- 创建维度为mx2的矩阵 记录分配的质心以及与相应质心的距离
计算所有数据点到初始质心的距离平方误差
当质心数量小于k时: 遍历每一个质心 调用K-Means算法 算法参数为(与该质心对应的样本数据集, k=2, 损失函数)====>二分K-Means算法 返回本次二分的 质心与簇分配效果 计算本次被二分的质心对于数据集的损失函数之和 与没有被二分的质心对应数据集的损失函数之和(2+1) ————————> 总的误差和越小,越相似, 效果越优化, 划分的效果更好 记录本次优化的原质心index, 优化拆分出的质心集, 优化拆分出簇分配效果 #更新最好的簇分配结果 调用K-means分配结构 最好的簇分配默认为0,1 原本为1的更新为 len(centList) 原本为0的更新为最佳质心 # 更新质心列表 更新原质心List中的第i个质心为使用二分K-Means算法优化后的第1个质心 更新原质心List中的第2个质心为二分K-Means算法优化后的第2个质心 重新分配最好簇下的数据(质心)以及SSE 返回 质心 与 簇分配结果
算法实现 以及测试
def biKMeans(data_set, k, calc_dist=calc_dist_sse):
m = np.shape(data_set)[0]
clusterAssment = np.mat(np.zeros((m,2))) # 保存每个数据点的簇分配结果和平方误差
centroid0 = np.mean(data_set, axis=0).tolist()[0] # 质心初始化为所有数据点的均值
centList =[centroid0] # 初始化只有 1 个质心的 list
for j in range(m): # 计算所有数据点到初始质心的距离平方误差
clusterAssment[j,1] = calc_dist(np.mat(centroid0), data_set[j,:])**2
while (len(centList) < k): # 当质心数量小于 k 时
lowestSSE = np.inf
for i in range(len(centList)): # 对每一个质心
ptsInCurrCluster = data_set[np.nonzero(clusterAssment[:,0].A==i)[0],:] # 获取当前簇 i 下的所有数据点
centroidMat, splitClustAss = k_means(ptsInCurrCluster, 2, calc_dist) # 将当前簇 i 进行二分 kMeans 处理
sseSplit = sum(splitClustAss[:,1]) # 将二分 kMeans 结果中的平方和的距离进行求和
sseNotSplit = sum(clusterAssment[np.nonzero(clusterAssment[:,0].A!=i)[0],1]) # 将未参与二分 kMeans 分配结果中的平方和的距离进行求和
print("sseSplit, and notSplit: ",sseSplit,sseNotSplit)
if (sseSplit + sseNotSplit) < lowestSSE: # 总的(未拆分和已拆分)误差和越小,越相似,效果越优化,划分的结果更好(注意:这里的理解很重要,不明白的地方可以和我们一起讨论)
bestCentToSplit = i
bestNewCents = centroidMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
# 找出最好的簇分配结果
bestClustAss[np.nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) # 调用二分 kMeans 的结果,默认簇是 0,1. 当然也可以改成其它的数字
bestClustAss[np.nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit # 更新为最佳质心
print('the bestCentToSplit is: ',bestCentToSplit)
print('the len of bestClustAss is: ', len(bestClustAss))
# 更新质心列表
centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0] # 更新原质心 list 中的第 i 个质心为使用二分 kMeans 后 bestNewCents 的第一个质心
centList.append(bestNewCents[1,:].tolist()[0]) # 添加 bestNewCents 的第二个质心
clusterAssment[np.nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss # 重新分配最好簇下的数据(质心)以及SSE
return np.mat(centList), clusterAssment
def main():
filename = 'testSet.txt'
data_set = load_data(filename)
print('mean data set ', np.mean(data_set, axis=0).tolist()[0])
# print('*****data set*****\n', data_set)
# create_centroids(data_set, k=3)
#
# centriods, cluster_ment = k_means(data_set, k=3, calc_dist=calc_dist_sse, create_centroids=create_centroids)
# print('result centriods,', centriods)
# print("result cluster_ment", cluster_ment)
centroids, cluster_ment = biKMeans(data_set, 3, calc_dist=calc_dist_sse)
print('centroids', centroids)
print('cluster ment', cluster_ment)
if __name__ == '__main__':
main()
参考文献
《机器学习实战》