我们的问题是:怎么给一堆数据分类?
首先,每一类都叫一个簇(Cluster),如何才能算作是同一类能?
我们有K-means聚类,DBSCAN(Density-Based Spatial Clustering of Application with Noise),hierarchical clustering等等这些聚类算法,这些算法区分同一类的方式都不同,比如DBSCAN,它是以一定的密度进行传播,将传播到的点都聚成一类,在某些场景,比如簇与簇之间有明显分隔的,DBSCAN显然要比k-means好。
下面是用k-means和DBSCAN划分一个笑脸的不同效果。

k-means算法

DBSCAN算法
不过对于均匀分布的数据,指定簇个数的k-means就要优于依靠密度发展的DBSCAN了。

k-means算法

DBSCAN算法
这些动画的演示你可在https://www.naftaliharris.com/blog/visualizing-k-means-clustering/上做到
使用pycharm手写k-means聚类算法
思路:
①随机生成1000个二维坐标的样本点
②指定划分成5类
③确定收敛阈值为0.2
④从1000个样本点中随机选出5个样本点,作为5个簇的中心
⑤每个中心为一个Cluster
⑥遍历1000个点,分别计算它们到5个中心点之间的距离,对每个点,我们得到5个距离,选择最小的,加入那个簇。
⑦第一次循环结束,我们分配好了1000个点的归属,得到了5个Cluster,对每个Cluster,计算里面包含的所有的点的均值,
也就是所有的x加起来除以总个数,所有的y加起来除以总个数,得到一个均值点。
⑧算出均值点到原来的中心点之间的距离,可以记为shift。对5个Cluster都这么做,我们会得到5个shift,挑出里边最大的biggest_shift
⑨如果biggest_shift小于收敛阈值0.2,跳出loop,否则,回到⑥
请注意,每个循环的时候,我们都要更新中心点,这是在第⑧步做的。代码十分详细,请看——
kmeans_tools.py
1 # -*- coding: utf-8 -*-
2
3 import math
4 import random
5
6
7 class Cluster(object):
8 """
9 聚类
10 """
11
12 def __init__(self, samples):
13 if len(samples) == 0:
14 # 如果聚类中无样本点
15 raise Exception("错误:一个空的聚类!")
16
17 # 属于该聚类的样本点
18 self.samples = samples
19
20 # 该聚类中样本点的维度
21 self.n_dim = samples[0].n_dim
22
23 # 判断该聚类中所有样本点的维度是否相同
24 for sample in samples:
25 if sample.n_dim != self.n_dim:
26 raise Exception("错误: 聚类中样本点的维度不一致!")
27
28 # 设置初始化的聚类中心
29 self.centroid = self.cal_centroid()
30
31 def __repr__(self):
32 """
33 输出对象信息
34 """
35 return str(self.samples)
36
37 def update(self, samples):
38 """
39 计算之前的聚类中心和更新后聚类中心的距离
40 """
41
42 old_centroid = self.centroid
43 #这里lists[i]中的所有数据都赋给clusters[i]了
44 self.samples = samples
45 #这里把clusters[i]的centroid也更新了
46 self.centroid = self.cal_centroid()
47 shift = get_distance(old_centroid, self.centroid)
48 return shift
49
50 def cal_centroid(self):
51 """
52 对于一组样本点计算其中心点
53 """
54 #获取样本点的个数
55 n_samples = len(self.samples)
56 # 获取所有样本点的坐标(特征)
57 coords = [sample.coords for sample in self.samples]
58 # 将所有sample的横坐标合起来作为一个tuple,所有的纵坐标合起来作为一个tuple
59 unzipped = zip(*coords)
60 # 计算每个维度的均值
61 centroid_coords = [math.fsum(d_list)/n_samples for d_list in unzipped]
62 #将新得到的中心点封装成一个Sample
63 return Sample(centroid_coords)
64
65
66 class Sample(object):
67 """
68 样本点类
69 """
70 def __init__(self, coords):
71 self.coords = coords # 样本点包含的坐标
72 self.n_dim = len(coords) # 样本点维度
73
74 # 将coords转成str输出
75 # 方便查看
76 def __repr__(self):
77
78 return str(self.coords)
79
80
81 def get_distance(a, b):
82
83 #使用两点间的距离公式计算两个sample之间的距离
84 if a.n_dim != b.n_dim:
85 # 如果样本点维度不同
86 raise Exception("错误: 样本点维度不同,无法计算距离!")
87
88 acc_diff = 0.0
89 for i in range(a.n_dim):
90 square_diff = pow((a.coords[i]-b.coords[i]), 2)
91 acc_diff += square_diff
92 distance = math.sqrt(acc_diff)
93
94 return distance
95
96
97 def gen_random_sample(n_dim, lower, upper):
98 """
99 生成随机样本
100 """
101 # [random.uniform(lower, upper) for _ in range(n_dim)]得到的是一个二维的点,我们将其传入Sample
102 #得到一个Sample对象
103 sample = Sample([random.uniform(lower, upper) for _ in range(n_dim)])
104 return sample
Sample类做样本点,样本点提供给Cluster做簇,gen_random_sample方法生成随机点,get_distance方法计算两点间的距离
main.py
1 # -*- coding: utf-8 -*-
2
3 import random
4 from kmeans_tools import Cluster, get_distance, gen_random_sample
5 import matplotlib.pyplot as plt
6 from matplotlib import colors as mcolors
7
8
9 def kmeans(samples, k, cutoff):
10
11 # 随机选k个样本点作为初始聚类中心
12 #random.sample返回的是samples中任选的5个sample,以它们作为初始的重心
13 init_samples = random.sample(samples, k)
14
15 # 创建k个聚类,聚类的中心分别为随机初始的样本点
16 #最初,clusters是有5个Cluster的list
17 #每个Cluster的centroid都是init_sample中的值,因为只有一个中心点嘛,它的中心自然就是自己了
18 #这也可以看成是初始化Cluster
19 clusters = [Cluster([sample]) for sample in init_samples]
20
21
22 # 迭代循环直到聚类划分稳定
23
24 #记录循环次数
25 n_loop = 0
26 while True:
27 # 初始化一组空列表用于存储每个聚类内的样本点
28 #注意:我们现在是要把所有的sample分开来存在这以下的5个空的list中
29 #但是,我们最终要得到的是分好类的clusters,并非分好的lists,所以还会有将lists中的值转移到clusters中的步骤
30 lists = [[] for _ in clusters]
31
32 # 开始迭代
33 n_loop += 1
34 # 遍历样本集中的每个样本
35 for sample in samples:
36 # 计算样本点sample和第一个聚类中心的距离
37 smallest_distance = get_distance(sample, clusters[0].centroid)
38 # 初始化属于聚类 0
39 cluster_index = 0
40
41 # 计算和其他聚类中心的距离
42 #分别算出sample和另外四个cluster的centroid的距离
43 for i in range(k - 1):
44 # 计算样本点sample和聚类中心的距离
45 distance = get_distance(sample, clusters[i+1].centroid)
46 # 如果存在更小的距离,更新距离
47 if distance < smallest_distance:
48 smallest_distance = distance
49 cluster_index = i + 1
50
51 # 找到最近的聚类中心,更新所属聚类
52 lists[cluster_index].append(sample)
53
54 #运行至此,1000个样本点根据距离最小的分类方案分别存在了5个list当中
55 # 初始化最大移动距离
56 biggest_shift = 0.0
57
58 # 计算本次迭代中,聚类中心移动的距离
59 for i in range(k):
60 #注意:这个update方法不仅计算出了原来的聚类中心和新的聚类中心的距离
61 #而且,他把list中的值更新到cluster当中了,并且每个cluster的centroid也在这一步更新了
62 shift = clusters[i].update(lists[i])
63 # 记录最大移动距离
64 biggest_shift = max(biggest_shift, shift)
65
66 # 如果聚类中心移动的距离小于收敛阈值,即:聚类稳定
67 if biggest_shift < cutoff:
68 print("第{}次迭代后,聚类稳定。".format(n_loop))
69 break
70 # 返回聚类结果
71 return clusters
72
73
74 def run_main():
75 """
76 主函数
77 """
78 # 样本个数
79 n_samples = 1000
80
81 # 维度
82 n_feat = 2
83
84 # 生成的点的范围,也就是0到200
85 lower = 0
86 upper = 200
87
88 # 聚类个数
89 n_cluster = 5
90
91 # 生成随机样本
92 samples = [gen_random_sample(n_feat, lower, upper) for _ in range(n_samples)]
93
94 # 收敛阈值
95 cutoff = 0.2
96
97 #将1000个随机sample,聚类个数,和收敛阈值传入kmeans方法,
98 #返回的是经过无监督学习后自动分好类的5个Cluster
99 #此函数是理解kmeans的关键
100 clusters = kmeans(samples, n_cluster, cutoff)
101
102 # 输出结果
103 for i, c in enumerate(clusters):
104 for sample in c.samples:
105 print('聚类--{},样本点--{}'.format(i, sample))
106
107 # ---------------------算法结束,以下是画图-----------------------------------------------------------------
108
109 # 可视化结果
110 plt.subplot()
111 color_names = list(mcolors.cnames)
112 # 将5个Cluster以scatter的方式显示
113 for i, c in enumerate(clusters):
114 x = []
115 y = []
116
117 # 这里i是循环次数,是固定的,所以从color_names中取出的颜色也是固定的,这个你可以所以改动
118 color = [color_names[i]] * len(c.samples)
119 for sample in c.samples:
120 x.append(sample.coords[0])
121 y.append(sample.coords[1])
122 plt.scatter(x, y, c=color)
123 plt.show()
124
125 if __name__ == '__main__':
126 run_main()
kmeans方法的是算法的核心,我的注释已经写得十分详细了。
我们看看运行结果吧:



我在这里只是随机运行了三次,其实改变的只是样本点不同罢了,结果还是很满意的。
你当然可以改变k以及收敛阈值的值,观察结果的改变。