# K-means Algorithm is a clustering algorithm
import matplotlib.pyplot as plt
from matplotlib import colors as mcolors
import math
import random
class Cluster(object):
def __init__(self, samples):
if len(samples) == 0:
raise Exception("错误,样本数为0,为空聚类")
self.samples = samples
self.n_dim = samples[0].n_dim
# 检查是否所有样本点的纬度数都一样
for sample in samples: # 这里的 sample 相当于新定义的变量名,类似于int i,再加上for循环
if sample.n_dim != self.n_dim:
raise Exception("错误,聚类中的样本点的纬度数不一致")
self.centroid = self.cal_centroid() # 初始化聚类中心
def __repr__(self):
return str(self.samples)
def update(self, samples):
old_centroid = self.centroid
self.samples = samples
self.centroid = self.cal_centroid() # 牛逼!又学了一招https://www.cnblogs.com/IT-LearnHall/p/9426327.html
# 报错原因1.调用方法时忘记加上括号,导致参数无法传入
# 一、不带括号时,调用的是这个函数本身 ,是整个函数体,是一个函数对象,不须等该函数执行完成
# 二、带括号(参数或者无参),调用的是函数的执行结果,须等该函数执行完成的结果
shift = get_distance(old_centroid, self.centroid)
return shift
def cal_centroid(self): # 对于一组样本点,计算其样本中心点
n_samples = len(self.samples)
coords = [sample.coords for sample in self.samples] # a.coords[i] 包含的是该层维度的具体数值
unzipped = zip(*coords)
centroid_coords = [math.fsum(d_list) / n_samples for d_list in unzipped]
return Sample(centroid_coords)
class Sample(object):
def __init__(self, coords):
self.coords = coords # 参数个数
self.n_dim = len(coords) # 维度
def __repr__(self):
return str(self.coords)
def get_distance(a, b):
if a.n_dim != b.n_dim:
raise Exception("错误!样本点纬度不同")
acc_diff = 0.0
for i in range(a.n_dim):
square_diff = pow(a.coords[i] - b.coords[i], 2)
acc_diff += square_diff
distance = math.sqrt(acc_diff)
return distance
def gen_random_sample(n_dim, low, high): # 产生随机样本点
sample = Sample([random.uniform(low, high) for _ in range(n_dim)])
return sample
def kmeans(samples, k, cutoff):
init_samples = random.sample(samples, k) # 初始化K个样本中心点,从samples中选择K个样本点
clusters = [Cluster([sample]) for sample in init_samples] # 以init_sample中的样本点作为聚类中心,先创建K个聚类
num_loop = 0.0 # Q:python 中这里写为0和0.0有什么区别呢?
while True:
lists = [[] for _ in clusters] # 创建K个空列表待用
num_loop += 1 # 循环次数计数
for sample in samples:
min_distance = get_distance(sample, clusters[0].centroid)
cluster_index = 0
for i in range(k - 1):
distance = get_distance(sample, clusters[i + 1].centroid)
if distance < min_distance:
min_distance = distance
cluster_index = i + 1
lists[cluster_index].append(sample)
max_shift = 0
for i in range(k):
shift = clusters[i].update(lists[i])
# if shift > max_shift:
# max_shift = shift
# 可再优化:
max_shift = max(shift, max_shift)
if max_shift < cutoff:
print("第{}次迭代后,聚类稳定".format(num_loop))
break
return clusters
def run_main():
# 这些main函数里面的参数名都是自己定义的,和上面的类还有方法都没有关系,只要在对应位置放进去就ok
n_samples = 1000 # 样本数
n_feat = 2 # 样本维度
n_cluster = 100 # 聚类个数 <=> K
cutoff = 0.2 # 聚类结束标志
lower = 0 # 样本空间范围
upper = 200
samples = [gen_random_sample(n_feat, lower, upper) for _ in range(n_samples)] # 创造样本列表
clusters = kmeans(samples, n_cluster, cutoff) # 对创造出来的样本点列表进行第一次聚类
for i, c in enumerate(clusters): # i:簇编号,c:簇集合
for sample in c.samples:
print("聚类--{},样本点--{}".format(i, sample))
plt.subplot()
color_names = list(mcolors.cnames) # 元组转化为列表
for i, c in enumerate(clusters):
x = []
y = []
color = [color_names[i]] * len(c.samples)
for sample in c.samples: # 报错原因 2.这里也别忘记了 c. ,因为这里的元素是从当前c簇中选取的
x.append(sample.coords[0])
y.append(sample.coords[1])
plt.scatter(x, y, c=color) # 必须严格写 c = color,不然就有错误
# 实际上,c=color 这里不写也可以,采用默认值,但是有一个不好的地方就是,有可能会出现各种相同颜色的簇,不利于分类效果的显示
plt.show()
if __name__ == '__main__':
run_main()
借鉴的版本
# K-means Algorithm is a clustering algorithm
import matplotlib.pyplot as plt
from matplotlib import colors as mcolors
import math
import random
class Cluster(object):
# 类继承了object对象,拥有了好多可操作对象,这些都是类中的高级特性
# https: // blog.csdn.net / DeepOscar / article / details / 80947155
"""
聚类
"""
def __init__(self, samples):
if len(samples) == 0:
# 如果聚类中无样本点
raise Exception("错误:一个空的聚类!")
# 类中的非静态方法第一个参数是 self 的才可以被实例调用
# 类中带 self 的参数都是 实例 的,实例对这个参数拥有所有权,即实例中所有的方法都可以使用实例的参数
# raise 手动抛出一个异常
# 属于该聚类的样本点
self.samples = samples
# 该聚类中样本点的维度
self.n_dim = samples[0].n_dim # samples 是一个簇的样本点集合,其中每一个的取值通过[i]来索引查阅
# 判断该聚类中所有样本点的维度是否相同
for sample in samples:
if sample.n_dim != self.n_dim:
raise Exception("错误: 聚类中样本点的维度不一致!")
# 设置初始化的聚类中心
self.centroid = self.cal_centroid()
def __repr__(self):
"""
输出对象信息
"""
return str(self.samples)
def update(self, samples):
"""
计算之前的聚类中心和更新后聚类中心的距离
"""
old_centroid = self.centroid
self.samples = samples
self.centroid = self.cal_centroid()
shift = get_distance(old_centroid, self.centroid)
return shift
def cal_centroid(self):
"""
对于一组样本点计算其中心点
"""
n_samples = len(self.samples) # 求样本点总数
# 获取所有样本点的坐标(特征),并用列表给它装起来[];
coords = [sample.coords for sample in self.samples] # 列表; coords : 坐标;
unzipped = zip(*coords) # zip <=> 压缩; zip(*)<=> 解压
# 计算每个维度的均值
centroid_coords = [math.fsum(d_list) / n_samples for d_list in unzipped]
# math.fsum() 对迭代器里的每个元素进行求和操作
return Sample(centroid_coords)
class Sample(object):
"""
样本点类
"""
def __init__(self, coords): # coords相当于一个参数,因为这一层是函数,是包在class里面的!
self.coords = coords # 样本点包含的坐标: 这里是在 初始化属性 coords 和 n_dim
self.n_dim = len(coords) # 样本点维度
def __repr__(self): # 如果用户需要自定义类能实现“自我描述”的功能,就必须重写 __repr__() 方法
"""
输出对象信息
"""
return str(self.coords) # 表明在调用该方法时,会返回;;转化成字符串
def get_distance(a, b):
"""
返回样本点a, b的欧式距离
参考:https://en.wikipedia.org/wiki/Euclidean_distance#n_dimensions
"""
if a.n_dim != b.n_dim:
# 如果样本点维度不同
raise Exception("错误: 样本点维度不同,无法计算距离!")
acc_diff = 0.0
for i in range(a.n_dim): # 对点坐标的每一个纬度进行遍历,其中a.coords[i]包含的是该层维度的数值
square_diff = pow((a.coords[i] - b.coords[i]), 2)
acc_diff += square_diff
distance = math.sqrt(acc_diff)
return distance
def gen_random_sample(n_dim, lower, upper):
"""
生成随机样本
"""
# 下面 [ ] 中是一个链表推导式
# 下面的lower和upper是对应这个函数的形参来讲的,所以同名;
sample = Sample([random.uniform(lower, upper) for _ in range(n_dim)]) # Sample()是一个类,在上一个类中,是样本电类
return sample
# uniform() 方法将随机生成下一个实数,它在 [x, y) 范围内(左闭右开)
# '_'是一个循环标志,也可以用i,j 等其他字母代替,下面的循环中不会用到,起到的只是控制循环次数的作用
def kmeans(samples, k, cutoff):
# kmeans函数
# 随机选k个样本点作为初始聚类中心
# random.sample(list, 5) # 从list中随机获取5个元素,作为一个片断返回;(前提是list必须已知);
# 变形:random.sample(range(0, 9), 4) => 使得能够满足 从选定范围中选取任意个元素 的需求
init_samples = random.sample(samples, k) # random.sample 是一个固定方法,学一下
# 创建k个聚类,聚类的中心分别为随机初始的样本点
# 总的clusters结果是一个列表,每个 Cluster类 的参数是一个列表[sample];最后相当于"列表中含列表"?[ [] [] []...]
clusters = [Cluster([sample]) for sample in init_samples] # 初始聚类,每类中只含有一个点,最后输出的时候是分类好的样子
# 迭代循环直到聚类划分稳定
n_loop = 0
while True: # while循环体中使用 True 或者 False ,首字母大写~
# 初始化一组空列表用于存储每个聚类内的样本点
lists = [[] for _ in clusters] # ***************************************
# 开始迭代
n_loop += 1
# 遍历样本集中的每个样本
for sample in samples:
# 计算样本点sample和 第一个聚类の中心 的距离
smallest_distance = get_distance(sample, clusters[0].centroid)
# 初始化属于聚类 0
cluster_index = 0
# 计算和其他聚类中心的距离
for i in range(k - 1): # 从0到(k-1),相比于外层的0地址,要再加一,等价于<=>地址从1到k;
# 计算样本点sample和聚类中心的距离(欧式距离)
distance = get_distance(sample, clusters[i + 1].centroid)
# 如果存在更小的距离,更新距离
if distance < smallest_distance:
smallest_distance = distance
cluster_index = i + 1 # 这是用来定位的,表明最后会把这个点归类到哪一个样本中心点去,
# 这里i因为是第二层循环的缘故,所以0实际对应着下标1,物理实际位置为2
# 找到最近的聚类中心,更新所属聚类
lists[cluster_index].append(sample) # list.append 方法
# 初始化最大移动距离
biggest_shift = 0.0
# 计算本次迭代中,聚类中心移动的距离
# range(k)是左闭右开:[0,k)
for i in range(k): # 对 K 个聚类都更新一下聚类中心,并求出K个聚类中,聚类中心移动距离最大的一个
# update() (line44) : 计算之前的聚类中心和更新后聚类中心的距离
# line128:clusters就是由Cluster而定义的列表,所以应该能直接用Cluster类里面的方法吧???
# update方法 是求取一组样本点的样本中心点,所以参数是一簇样本点;line134,lists结构为[ [][][]... ]的形式
shift = clusters[i].update(lists[i]) #
# 记录最大移动距离
biggest_shift = max(biggest_shift, shift)
# 如果聚类中心移动的距离小于收敛阈值,即:聚类稳定
if biggest_shift < cutoff:
print("第{}次迭代后,聚类稳定。".format(n_loop))
break # 跳出while True这一循环体
# 返回聚类结果
return clusters
def run_main():
"""
主函数
"""
# 样本个数
n_samples = 1000
# 特征个数 (特征维度)
n_feat = 2
# 特征数值范围
lower = 0
upper = 200
# 聚类个数
n_cluster = 100
# 生成随机样本 '_'是个"占位符",循环中不关心具体的元素内容,就是简单的“让它循环这么多次”
samples = [gen_random_sample(n_feat, lower, upper) for _ in range(n_samples)]
# 收敛阈值
cutoff = 0.2
clusters = kmeans(samples, n_cluster, cutoff)
# 输出结果 enumerate(sequence, [start=0])
# 用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,
# 同时列出数据和数据下标,一般用在 for 循环当中 : (下标,数据)
#
# 格式化字符串的函数 str.format()
# "{1} {0} {1}".format("hello", "world") # 设置指定位置
for i, c in enumerate(clusters): # 按簇,把每一簇内含有的元素都打印出来;enumerate:枚举
for sample in c.samples:
print('聚类--{},样本点--{}'.format(i, sample)) # 这里不设置指定位置
# 可视化结果,直接学就好了
# https://blog.csdn.net/tefuirnever/article/details/88944438
# c可以是一个RGB或RGBA二维行数组
plt.subplot() # subplot() 函数允许你在同一图中绘制不同的东西:matplotlib.pyplot
color_names = list(mcolors.cnames) # list() 方法用于将 元组 转换为 列表 ,所以这里现在是一个列表 [ i ]
for i, c in enumerate(clusters):
x = [] # 学习python中数组的定义方式
y = []
# random.choice
color = [color_names[i]] * len(c.samples)
# 这里按照当前簇c中的元素个数,对color列表进行增殖
# 以期在plt.scatter函数中,对应的是一组x和对应的一组y,则带入一个颜色列表,方能使得最后画图时,同一组点都是同颜色的
# 用一个列表list1乘一个数字n 会得到一个新的列表list2, 这个列表的元素是list1的元素重复n次
for sample in c.samples:
x.append(sample.coords[0]) # 用于在列表末尾添加新的对象
y.append(sample.coords[1]) # coords是坐标,后面的 [] 是指维度
plt.scatter(x, y, c=color) # 绘制散点图,其中X、Y是相同长度的数组序列; 一簇一簇地画
plt.show()
if __name__ == '__main__':
run_main()