k_means为k均值聚类,是一种聚类算法。
本文主要讲解:
1.如何自己用python编写k_means
2.实例演练
k_means算法流程:
1.对于数据集X选择要将数据集分为几类,设为k
2.初始化k个中心点(中心点的初始化方法有很多种比如:(1)随机选取样本X中每个特征最大值最小值之间的任意数据、(2)在样本中随机选取k个点,(3)人工根据经验选取k个中心点传入程序中,(4)对于比较大的样本可以选择几个子样本进行分类,根据子样本最终的中心点的均值作为原始样本的初始化的k个中心点,(5)k-means++ 该文中有介绍等等),本文的初始化方法为第一种方法。
3.计算X中每一个样本点对每一个中心点的距离,将距离该中心点最小距离的样本加入该中心点,(例如:有两个中心点center1与center2,X={x1,x2,x3…},如果x1距离center1的距离为2,距离center2的距离为5,那么就将x1归属于center1类。相应的x2距离center1的距离为28,距离center2的距离为1,那么x2就归属于center2)。本文所用的距离为欧氏距离。
4.将数据集X中的样本根据距离分别归类到k个中心点中后,计算每个中心点中的样本的质心(也就是该中心点样本的每个特征的平均值),将新算出来的质心代替先前的中心点。
例如:
# center1中的样本点为:
data_center1 = [[1,2,3]
[4,5,6]
[7,8,9]]
# 该样本的质心point为
point = [(1+4+7)/3, (2+5+8)/3, (3+6+9)/3]
5.根据第4步算出来的新的中心点,计算新的中心点与旧的中心点的欧氏距离;选取所有新的中心点与相对应旧的中心点欧氏距离最大的值,将最大值与所定的收敛条件比较(比如:最大值为max,收敛条件为e = 0.01,若max<e,则退出循环,否则继续3、4、5步的运算)
6.重复3、4、5步直到max<e为止。
python实现k_means:
class K_Means:
"""
k 均值聚类
"""
def __init__(self, X, k, e=0.01):
self.X = np.array(X)
self.k = k
self.e = e
def init_random(self):
"""
随机初始化k个中心点
:return:
"""
# 计算X中的每一个特征的最大与最小值,使初始化的点在最大值与最小值之间
# 注:将计算出的最大最小值分为self.k份,每一个初始化质心在每一份中随机选取或选取平均值
self.X_max = []
self.X_min = []
for temp in range(self.X.shape[1]):
self.X_max.append(max(self.X[:, temp]))
self.X_min.append(min(self.X[:, temp]))
# 将self.X_min与self.X_max分为self.k份
split = (np.array(self.X_max) - np.array(self.X_min))/self.k
# 随机初始化self.k个点
init_point = []
for temp in range(self.k):
point = []
for temp1 in range(self.X.shape[1]):
point.append(random.uniform(self.X_min[temp1] + temp*split[temp1], self.X_min[temp1] + (temp + 1)*split[temp1]))
init_point.append(point)
return init_point
def EuchdeanDistance(self, a, b):
"""欧氏距离"""
return np.sqrt(np.dot(a - b, a - b))
def center_point(self, x):
"""计算一群样本中心点"""
center = []
x = np.array(x)
point = []
if x.ndim == 1:
for temp1 in range(self.X.shape[1]):
point.append(random.uniform(self.X_min[temp1], self.X_max[temp1]))
return point
for temp in range(x.shape[1]):
center.append(np.average(x[:, temp]))
return center
def sample_allocation(self, init_point):
"""
将样本X中的点划分给,距离k个点中最近的点,更新中心点,直到满足要求
:return:
"""
# 定义一个previous_point用于存储上一个init_point,用于比较两个点的差值,用于终止程序
previous_point = []
for i in range(self.k):
previous_point.append([0]*self.X.shape[1])
# 定义一个字典,用于存储每个self.k中做具有的数据点的索引
k_include = {}
# max_loop 最大循环次数防止死循环
max_loop = 500
# loop 当前循环次数
loop = 0
# 循环终止条件,中心点差值小于0.01(previous_point与init_point差值的欧氏距离的最大值小于0.01,为了防止样本数据太小的影响可以用相对误差)
e = self.e
# 循环整个迭代过程,直到收敛为止
while True:
for temp in range(self.k):
k_include[temp] = []
# 计算每一个样本点到init_point每个点的距离,将距离self.k中最近的点加入到k_include中相应的k中
for index, temp in enumerate(self.X):
distance = []
for i in init_point:
distance.append(self.EuchdeanDistance(temp, i))
k_include[distance.index(min(distance))].append(index)
# 计算每一个k_include样本中的中心点并更新到init_point中
previous_point = init_point
for temp in range(self.k):
temp_value = []
for i in k_include[temp]:
temp_value.append(self.X[i, :])
center = self.center_point(temp_value)
init_point[temp] = center
# 计算init_point 与 previous_point 之间的差值(用欧式距离表示),存储在diff中
diff = []
for temp in range(self.k):
diff.append(self.EuchdeanDistance(np.array(init_point)[temp, :], np.array(previous_point)[temp, :]))
# 计算k_include中的元素长度,防止k_include有空列表(这只是防止陷入局部最优解,但有可能loop > max_loop还是有空列表,那么这就是选取的k只有问题)
min_length = 1
for temp in range(self.k):
if min_length > np.array(k_include[temp]).shape[0]:
min_length = np.array(k_include[temp]).shape[0]
# 如果min_length=0,从新初始化init_point
if min_length == 0:
init_point = self.init_random()
loop += 1
# 判断循环是否终止
if (loop > max_loop) or ((max(diff) < e) and (min_length != 0)):
break
return k_include, init_point
def main(self):
"""调用该函数运行kmeans"""
init_point = self.init_random()
k_include, init_point = self.sample_allocation(init_point)
return k_include, init_point
算法数据实战演练:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 解决画图中文字体乱码问题
plt.rcParams['font.sans-serif'] = ['SimHei']
# 解决画图负号(-)显示不出来问题
plt.rcParams['axes.unicode_minus'] = False
# 制作一个二维数据,该数据集有两个特征a,b,该数据集最好的结果是被分为5类
x = []
for i in range(5):
for _ in range(20):
a = np.random.normal(i, 0.2)
b = np.random.normal(i + 5, 0.2)
x.append([a, b])
# 将数据集x画出来
plt.scatter(np.array(x)[:, 0], np.array(x)[:, 1])
plt.xlabel("a")
plt.ylabel("b")
plt.show()
当k=5时的分类情况
k = 5
for _ in range(3):
km = K_Means(x, k)
k_include, init_point = km.main()
# 将k_include中的x的标签的数据加入到D中
D = {}
for i in range(k):
D[i] = []
for j in k_include[i]:
D[i].append(x[j])
plt.figure()
for i in range(k):
# 防止D为空
if np.array(D[i]).ndim == 2:
plt.scatter(np.array(D[i])[:, 0], np.array(D[i])[:, 1])
plt.show()
运行结果:
这是运行三次k=5状态下的运行结果,从图中可见三次分类结果有些许不同,最后一次结果有些误差绿色点与红色点分类效果不好,这是由于,初始参数是随机选取的所以可能在一些情况下分类效果不是很好。
由于k值是人工选取的,所以依赖于人工,下面将介绍一种k值选择方法:
该方法是通过,计算在k值确定的情况下,利用最终迭代所得的中心点以及每个中心点中的数据,计算每个中心点下的数据点到中心点的欧式距离并取平均值。将每个中心点所计算的平均值相加在平均得到“最终结果”。利用不同k下的“最终结果”画出曲线,可以看出,如果分类效果好那么“最终结果”越小。
代码如下:
x = []
for i in range(5):
for _ in range(20):
a = np.random.normal(i, 0.2)
b = np.random.normal(i + 5, 0.2)
x.append([a, b])
k = 2
# 存储“最终结果”
result = {}
# 存储不同的k值
result["k"] = []
# 存储不同k值下的“最终结果”
result["dis"] = []
for _ in range(12):
km = K_Means(x, k)
k_include, init_point = km.main()
D = {}
for i in range(k):
D[i] = []
for j in k_include[i]:
D[i].append(x[j])
# 计算最优k值,利用每个质心到该质心所对应的点的距离的平均值
SUM = 0
for i in range(k):
sum = 0
for j in D[i]:
sum += np.sqrt(np.dot(np.array(j) - np.array(init_point[i]), np.array(j) - np.array(init_point[i])))
if len(D[i]) != 0:
SUM += sum/(len(D[i]))
result["k"].append(k)
result["dis"].append(SUM/k)
k += 1
# 画出不同k值下的最终结果
plt.figure()
plt.title("最佳k值")
plt.plot(result["k"], result["dis"])
plt.show()
最终结果:
从上图中可以看出,在k=5往后k值的变化就很缓慢,所以k=5的时候最合适。原因如下:从极限的方面考虑,当k=1时此时只有一个中心点,所以此时距离(也就是y轴 )应该最大。当k的值与样本数目相同时,此时距离应该为0,。所以当k值越大时距离的变化趋势应该是逐渐减小。但k值并不是越大越好,这和实际需求有关。
上面的数据分布的较好,还有一些数据变化极小,如下所示
| | |
这类数据画散点图如下:
所有数据在图中可以显示为3个点,这时由于数据变化极小导致的,这些数据用上述自己编写的代码运行如下:
虽然对应于不同的k值但最终的分类结果相同,都分为3类。如下代码所示,这是由于在程序运行期间k_include中有空列表,即使loop达到最大值max_loop,程序k_include中仍有空列表,所以为了防止死循环,程序运行结束。
# 计算k_include中的元素长度,防止k_include有空列表(这只是防止陷入局部最优解,但有可能loop > max_loop还是有空列表,那么这就是选取的k只有问题)
min_length = 1
for temp in range(self.k):
if min_length > np.array(k_include[temp]).shape[0]:
min_length = np.array(k_include[temp]).shape[0]
# 如果min_length=0,从新初始化init_point
if min_length == 0:
init_point = self.init_random()
loop += 1
# 判断循环是否终止
if (loop > max_loop) or ((max(diff) < e) and (min_length != 0)):
break
对于这类数据k值与距离变化如下:
虽然随着k值的增加,距离在减小,但在k=3时,距离几乎为0,所以最佳k=3