引言
k-means是一种聚类算法。聚类与之前讲过的分类很相似但实质不一样,分类是已有了明确的类别,然后将样本分到不同类别中去;聚类是事先没有明确的类别,将特征各自相近的样本分别聚在一起。k-means则是原理比较简单的聚类算法。
k-means原理
k-means的原理可以用一个拟人化的场景来理解:
有三个传教士来到了一个小镇,各自选了个地点布道。小镇的居民们都很想听传教士的布道,但三个传教士有的近有的远,于是居民们都不约而同选了离自己最近的传教士听布道,没多久每家每户的居民都有了自己的传教士。
没过多久,传教士们发现有的居民每天要跑来大老远才能到自己这,很累。于是每个传教士都移动了自己的位置,让听自己布道的居民们的家离自己位置的总距离最短。
但是传教士们如此一移动,让有些部分居民不高兴了,因为自己的传教士照顾了其他居民,但却离自己远了,还不如去另一个传教士那才是离自己最近的。
于是这部分居民不再听原来的传教士布道了,而选择了目前离自己最近的另一个。
传教士们也发现听自己布道的居民发生了变化,为了照顾到这些新听众,传教士们再一次移动了自己的位置。部分居民的选择传教士也随之再次改变。
经过几次更迭之后,居民们不再更换自己的传教士,传教士们也不再移动自己的位置,各自形成了三个稳定聚落,达成完美的平衡。
以上就是k-means聚类过程的拟人化叙述。接下来我们用图来解释:
我们有一堆样本,特征值形成的散点图如下:
此时我们欲将样本聚类,比如聚成三个簇,那么我们就在样本中随机选取三个初始点。样本点会选择离自己最近的初始点,就分别形成了不同颜色的集合:
三个初始点移动至各自簇的质心:
达成稳态,中心点不再移动,形成稳定的三个簇。
手写k-means算法
k-means聚类过程首先要根据簇的数量随机选取初始点,于是先实现初始点选取的函数:
def __pick_start_point(self,ndarray,cluster_num):
'''选取初始点
'''
if cluster_num <0 or cluster_num > ndarray.shape[0]:
raise Exception("簇数设置有误")
# 随机点的下标
indexes=random.sample(np.arange(0,ndarray.shape[0],step=1).tolist(),cluster_num)
points=[]
for index in indexes:
points.append(ndarray[index].tolist())
return np.array(points)
k-means中涉及到的距离度量一般采用欧氏距离,因此可以先写出坐标点之间欧氏距离的计算函数:
def __distance(self,p1,p2):
'''计算两点间距
'''
tmp=0
for i in range(len(p1)):
tmp += pow(p1[i]-p2[i],2)
return pow(tmp,0.5)
中心点的坐标会随着加入自己的样本点的变化而发生变化,于是我们实现计算一组坐标的中心点的函数:
def __center(self,list):
'''计算一组坐标的中心点
'''
# 计算每一列的平均值
return np.array(list).mean(axis=0)
接下来是最核心的函数,迭代计算中心点和簇的形成。
def cluster(self):
result = []
for i in range(self.cluster_num):
result.append([])
for item in self.ndarray:
distance_min = sys.maxsize
index=-1
# 计算离自己最近的中心点
for i in range(len(self.points)):
distance = self.__distance(item,self.points[i])
if distance < distance_min:
distance_min = distance
index = i
# 根据中心点形成不同的簇
result[index] = result[index] + [item.tolist()]
# 新的中心点
new_center=[]
for item in result:
new_center.append(self.__center(item).tolist())
# 中心点未改变,说明达到稳态,结束递归
if (self.points==new_center).all():
return result
# 新中心点覆盖旧中心点
self.points=np.array(new_center)
# 继续迭代
return self.cluster()
封装好的完整算法代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import random
import sys
import time
class KMeansClusterer:
def __init__(self,ndarray,cluster_num):
self.ndarray = ndarray
self.cluster_num = cluster_num
self.points=self.__pick_start_point(ndarray,cluster_num)
print("初始点"+str(self.points))
def cluster(self):
result = []
for i in range(self.cluster_num):
result.append([])
for item in self.ndarray:
distance_min = sys.maxsize
index=-1
for i in range(len(self.points)):
distance = self.__distance(item,self.points[i])
if distance < distance_min:
distance_min = distance
index = i
result[index] = result[index] + [item.tolist()]
new_center=[]
for item in result:
new_center.append(self.__center(item).tolist())
print("中心点:"+str(new_center))
for item in result:
plt.scatter([x[0] for x in item],[x[1] for x in item])
plt.scatter([x[0] for x in self.points],[x[1] for x in self.points],marker="*",s=100)
plt.show()
# 中心点未改变,说明达到稳态,结束递归
if (self.points==new_center).all():
return result
self.points=np.array(new_center)
# print(self.points)
return self.cluster()
def __center(self,list):
'''计算一组坐标的中心点
'''
# 计算每一列的平均值
return np.array(list).mean(axis=0)
def __distance(self,p1,p2):
'''计算两点间距
'''
tmp=0
for i in range(len(p1)):
tmp += pow(p1[i]-p2[i],2)
return pow(tmp,0.5)
def __pick_start_point(self,ndarray,cluster_num):
if cluster_num <0 or cluster_num > ndarray.shape[0]:
raise Exception("簇数设置有误")
# 随机点的下标
indexes=random.sample(np.arange(0,ndarray.shape[0],step=1).tolist(),cluster_num)
points=[]
for index in indexes:
points.append(ndarray[index].tolist())
return np.array(points)
调用方式
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from K_means import KMeansClusterer
if __name__ == "__main__":
pdData=pd.read_csv("datas.csv")
data=pdData[["density","sugercontent"]].values
clusterer=KMeansClusterer(data,3)
result=clusterer.cluster()
for item in result:
plt.scatter([x[0] for x in item],[x[1] for x in item])
plt.show()
输出结果: