本文将大致梳理K-Means算法的流程, 并且使用python3实现kmeans算法对简单欧式空间数据集的聚类问题,以及结果评价算法—轮廓系数的实现,最后将提供本次实例的详细注释代码
1.算法原理:
K-means算法是最为经典的基于划分的聚类方法,是十大经典数据挖掘算法之一。K-means算法的基本思想是:以空间中k个点为中心进行聚类,对最靠近他们的对象归类。通过迭代的方法,逐次更新各聚类中心的值,直至得到最好的聚类结果。
选择K个不相同的点作为初始质心
repeat
将每个点指派到最近的质心,形成K个簇
重新计算每个簇的质心
until 簇不发生变化或达到最大迭代次数
1.1.质心计算:
对于分类后的产生的k个簇,分别计算到簇内其他点距离均值最小的点作为质心(对于拥有坐标的簇可以计算每个簇坐标的均值作为质心)
1.2.距离度量:
将对象点分到距离聚类中心最近的那个簇中需要最近邻的度量策略,在欧式空间中采用的是欧式距离,在处理文档中采用的是余弦相似度函数,有时候也采用曼哈顿距离作为度量,不同的情况实用的度量公式是不同的。
欧式距离
余弦相似度
曼哈顿距离
1.3.聚类效果评价
轮廓系数(Silhouette Coefficient)结合了聚类的凝聚度(Cohesion)和分离度(Separation),用于评估聚类的效果。该值处于-1~1之间,值越大,表示聚类效果越好。具体计算方法如下:
- 对于每个样本点i,计算点i与其同一个簇内的所有其他元素距离的平均值,记作a(i),用于量化簇内的凝聚度。
- 选取i外的一个簇b,计算i与b中所有点的平均距离,遍历所有其他簇,找到最近的这个平均距离,记作b(i),即为i的邻居类,用于量化簇之间分离度。
- 对于样本点i,轮廓系数s(i) = (b(i) – a(i))/max{a(i),b(i)}
- 计算所有i的轮廓系数,求出平均值即为当前聚类的整体轮廓系数,度量数据聚类的紧密程度
从上面的公式,不难发现若s(i)小于0,说明i与其簇内元素的平均距离小于最近的其他簇,表示聚类效果不好。如果a(i)趋于0,或者b(i)足够大,即a(i)远远小于b(i),那么s(i)趋近与1,说明聚类效果比较好。
2.上手实例
下面将通过从准备数据到完成聚类并寻找最佳参数等一系列完整的步骤来讲解第一个算法实现的示例,最后我将提供此次示例的完整项目代码以及基于上一篇博客的VSM模型的计算结果来进行文档的聚类项目代码,本次示例使用语言为python3
2.1.准备数据
作为算法实现的测试数据,最好能够了解数据的分布情况,以便结合代码的运行结果来检测代码实现的正确与否。因此,我准备了一组基于欧式空间的坐标集数据,数据大致分布如下:
2.2.数据预处理
初始数据只有点的坐标和序号,因此需要计算出每两点之间的欧式距离并保存为距离表,方便后续计算时查找距离避免重复计算
# 各点间距离计算
def cal_dis(path, data):
tes_dis = {}
for i in range(0, len(data) - 1):
for j in range(i + 1, len(data)):
name = str(i+1) + '-' + str(j+1)
# 欧式距离计算公式
dist = math.sqrt(math.pow(data[i]["x"] - data[j]["x"], 2) + math.pow(data[i]["y"] - data[j]["y"], 2))
tes_dis[name] = int(round(dist))
save_file(path, tes_dis)
pass
# 数据转换为距离表形式
def data_transform(data, path):
points = {}
point_name = []
for i in data:
name = i.split('-')
if name[0] not in point_name:
point_name.append(int(name[0]))
if name[1] not in point_name:
point_name.append(int(name[1]))
if name[0] not in points:
points[name[0]] = {}
points[name[0]][name[1]] = data[i]
else:
points[name[0]][name[1]] = data[i]
points[str(max(point_name))] = {} # 将最后一个点加入到距离表中
save_file(path, points)
最终计算出的距离表样例:
"1": {
"2": 2,
"3": 2,
"4": 70,
"5": 4,
"6": 72,
"7": 99,
"8": 70,
"9": 102,
"10": 103,
"11": 102,
"12": 67,
"13": 74
}
2.3.编写K-Means主函数
在参数部分选择使用k值的范围可以避免多次的手动调节,函数会自动遍历k值范围并找到最优解,根据肘部法则默认k值从2开始;对于每个k值最好进行多次重复计算,因为kmeans算法对初始点的选取十分敏感,离群点以及噪点对算法结果影响很大,算法很容易陷入局部最优,局部最优问题将会在结果部分做进一步分析。
# 参数:k值的范围上限(默认从2开始遍历),每个k值重复计算次数,每次计算迭代次数,距离表数据
def k_means(k_range, repetition, iterations, data):
coefficient_list = [] # 记录每次计算轮廓系数
result_centers = [] # 记录每次计算结果中心点
# k值遍历
for k in range(2, k_range + 1):
# 每次计算重复多次,避免局部最优解
for times in range(0, repetition):
center_point = []
for i in range(0, k): # 初始化随机选取k个不同中心点
random_center = str(random.randint(0, len(data) - 1))
while random_center in center_point:
random_center = str(random.randint(0, len(data) - 1))
center_point.append(random_center)
for index in range(0, iterations): # 迭代计算
# 将每次重新计算得到的中心点作为下一次参数
center_point = cal_k_means(center_point, data)
# 计算每次聚类后的轮廓系数
coefficient = silhouette_coefficient(center_point, data)
coefficient_list.append(coefficient)
result_centers.append(center_point)
# 以轮廓系数最大的结果作为最优解
best_coef = max(coefficient_list)
best_centers = result_centers[coefficient_list.index(best_coef)]
# 结果输出
cout_best_result(repetition * iterations * k_range, best_coef, best_centers, data)
pass
# k-means 计算主函数,根据中心点划分簇,再重新计算每个簇的中心点
def cal_k_means(center_point_list, data):
# 根据中心点划分簇
cluster = divide_cluster(center_point_list, data)
center_point = []
for each in cluster: # 重新计算中心点
center_num = each['center']
center_dis_count = 0 # 中心点到其他点距离和
each_dis = [] # 每个点到其他点的距离和
for each_point in each['member']:
center_dis = search_distance(center_num, each_point, data)
dis_count = center_dis # 到其他点距离和
center_dis_count += center_dis
for target_point in each['member']: # 计算到簇内其他点的距离和
if target_point == each_point:
continue
dis_count += search_distance(target_point, each_point, data) # 查询距离表
each_dis.append(dis_count)
each_dis.append(center_dis_count)
min_point = each_dis.index(min(each_dis)) # 选取距离和最小的点为中心点
# 更新中心点
if min_point == (len(each_dis) - 1):
center_point.append(each['center'])
else:
center_point.append(each['member'][min_point])
return center_point
# 根据中心点划分簇
def divide_cluster(center_list, data):
cluster = []
center_point = []
for i in center_list:
temp = {'center': i, 'member': []}
cluster.append(temp)
center_point.append(i)
for point in data: # 计算每点到每个中心点距离
dis = []
if point in center_point:
continue
for j in cluster:
center_num = j['center']
temp_dis = search_distance(center_num, point, data)
dis.append(temp_dis)
point_cluster = dis.index(min(dis)) # 划分到最小的簇
cluster[point_cluster]['member'].append(point)
return cluster
# 在距离表中查询距离
def search_distance(a, b, data):
if a in data[b]:
distance = data[b][a]
else:
distance = data[a][b]
return distance
到现在就算完成了kmeans函数的编写,但是我们无法判断每一次聚类结果的好坏,因此,我们引入了轮廓系数来量化聚类结果。
2.4.评价函数:轮廓系数计算函数
轮廓系数计算的原理以及流程已经在上文介绍过了,以下是具体的实现:
# 计算轮廓系数
def silhouette_coefficient(center_list, data):
cohesion = 0.0 # 簇内凝聚度
separation = 0.0 # 簇间分离度
separation_list = []
coefficient_list = []
cluster = divide_cluster(center_list, data)
for point in data:
this_cluster = ''
for i in cluster:
# 计算簇内凝聚度
if point in i['member'] or point == i['center']:
this_cluster = i['center']
if point != i['center']:
center_dis = search_distance(point, i['center'], data)
else:
center_dis = 0
for j in i['member']:
if j == point:
continue
cohesion += search_distance(point, j, data)
cohesion += center_dis
if len(i['member']) != 0: # 取平均值
cohesion = float(cohesion/len(i['member']))
else:
cohesion = float(cohesion / 1.0)
break
# 簇间分离度计算
for other in cluster:
if other['center'] == this_cluster:
continue
center_dis = search_distance(point, other['center'], data)
for other_point in other['member']:
separation += search_distance(point, other_point, data)
separation += center_dis
if len(other['member']) != 0: # 取平均值
separation = float(separation / (len(other['member']) + 1))
else:
separation = float(separation / 1.0)
separation_list.append(separation)
# 计算每个点的轮廓系数
separation = min(separation_list)
coefficient = (separation - cohesion)/max(separation, cohesion)
coefficient_list.append(coefficient)
# 返回平均值作为此次分类的轮廓系数
coefficient = float(sum(coefficient_list)/len(coefficient_list))
return coefficient
2.5.结果分析
如果不对每一个k值进行多次重复计算就会有很大几率得到局部最优解:
# 参数:k值的范围上限(默认从2开始遍历),每个k值重复计算次数,每次计算迭代次数,距离表数据
k_means(3, 1, 20, dta)
进一步分析发现:对于这个小型的数据集,函数在第一次迭代完成后中心点就基本稳定了,因此,如果初始化选取的中心点位置不佳的话函数就会陷入局部最优,对此,最简单的解决方法就是多次计算,然后选取最优解。对K-Means算法的进一步优化请查看下面的参考资料。
示例的工程代码下载链接:
3.参考资料
https://blog.csdn.net/u013719780/article/details/78413770
https://www.cnblogs.com/dudumiaomiao/p/5839905.html
https://blog.csdn.net/qll125596718/article/details/8243404/
https://blog.csdn.net/github_36326955/article/details/54999612