1.运行结果:(注:图中方块标注的点为随机选取的初始样本点)
k=2时:
本次选取的2个初始向量为[[0.243, 0.267], [0.719, 0.103]]
共进行61轮
共耗时0.10s
k=3时:
本次选取的3个初始向量为[[0.343, 0.099], [0.719, 0.103], [0.774, 0.376]]
共进行64轮
共耗时0.10s
k=4时:
本次选取的4个初始向量为[[0.339, 0.241], [0.748, 0.232], [0.608, 0.318], [0.725, 0.445]]
共进行10轮
共耗时0.02s
2.结果分析:
k-means算法选的初始点离得越远越容易收敛,聚类效果也越好。
因此k-means算法的好坏与初始样本的选取有很大关系。
3.k-means改进:
(1)K-means++:
K-means++按照如下的思想选取K个聚类中心:
假设已经选取了n个初始聚类中心(0<n<K),则在选取第n+1个聚类中心时,距离当前n个聚类中心越远的点会有更高的概率被选为第n+1个聚类中心。在选取第一个聚类中心(n=1)时同样通过随机的方法。可以说这也符合我们的直觉:聚类中心当然是互相离得越远越好。
(2) ISODATA:
ISODATA的全称是迭代自组织数据分析法。在K-means中,K的值需要预先人为地确定,并且在整个算法过程中无法更改。而当遇到高维度、海量的数据集时,人们往往很难准确地估计出K的大小。ISODATA就是针对这个问题进行了改进,它的思想也很直观:
当属于某个类别的样本数过少时把这个类别去除,当属于某个类别的样本数过多、分散程度较大时把这个类别分为两个子类别。
(3) Kernel K-means:
传统K-means采用欧式距离进行样本间的相似度度量,显然并不是所有的数据集都适用于这种度量方式。参照支持向量机中核函数的思想,将所有样本映射到另外一个特征空间中再进行聚类,就有可能改善聚类效果。
4.代码清单:(详见注释)
# coding=utf-8
# author:yjy
# date:2019/12/1
import numpy as np # 扩展程序库,针对数组运算提供大量的数学函数库
import pandas as pd # 加强版numpy,pandas拥有种数据结构:Series和DataFrame
import matplotlib.pyplot as plt # 绘图库,一种 MatLab 开源替代方案
import random # 随机数模块
import time # 时间模块,时间戳时间: float数据类型,给机器用
# 西瓜数据集4.0: 密度 含糖率 标签
data = [[0.697, 0.460, 1],
[0.774, 0.376, 1],
[0.634, 0.264, 1],
[0.608, 0.318, 1],
[0.556, 0.215, 1],
[0.430, 0.237, 1],
[0.481, 0.149, 1],
[0.437, 0.211, 1],
[0.666, 0.091, 0],
[0.243, 0.267, 0],
[0.245, 0.057, 0],
[0.343, 0.099, 0],
[0.639, 0.161, 0],
[0.657, 0.198, 0],
[0.360, 0.370, 0],
[0.593, 0.042, 0],
[0.719, 0.103, 0],
[0.359, 0.188, 0],
[0.339, 0.241, 0],
[0.282, 0.257, 0],
[0.748, 0.232, 0],
[0.714, 0.346, 1],
[0.483, 0.312, 1],
[0.478, 0.437, 1],
[0.525, 0.369, 1],
[0.751, 0.489, 1],
[0.532, 0.472, 1],
[0.473, 0.376, 1],
[0.725, 0.445, 1],
[0.446, 0.459, 1]]
# 多维数组中创建DataFrame(二维表),需要为DataFrame赋值columns和index(默认为数字)
column = ['density', 'sugar_rate', 'label']
dataSet = pd.DataFrame(data, columns=column)
# 创建类K_means
class K_means(object):
# 创建__init__方法,在面向对象编程中,给未来创建的对象所定义的进行初始化属性
# 当对象一旦被创建,Python将会自动调用__init__方法,里面的属性将会赋予这个对象
def __init__(self, k, data, loop_times, error): # self只有在类的方法中才会有,指向类的实例对象,而非类本身
self.k = k
self.data = data
self.loop_times = loop_times
self.error = error
def distance(self, p1, p2):
# linalg=linear(线性)+algebra(代数),norm则表示范数
# 求p = 2 时的闵可夫斯基距离,即欧氏距离
return np.linalg.norm(np.array(p1) - np.array(p2))
def fitting(self):
time1 = time.perf_counter() # 返回性能计数器的值(以分秒为单位),表示程序开始运行到调用这个语句所经历的时间
mean_vectors = random.sample(self.data, self.k) # 随机选取k个初始样本
initial_main_vectors = mean_vectors
for vec in mean_vectors :
plt.scatter(vec[0], vec[1], s=100, color = 'black', marker='s') # 画出初始聚类中心,以黑色正方形(square)表示
times = 0
# map(),高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f 依次作用在 list 的每个元素上,得到一个新的 list 并返回
# lambda:返回可调用的函数对象,通常是在需要一个函数,但又不想命名一个函数时使用,lambda x : [x] 表示输入x,输出为[x]
clusters = list(map((lambda x:[x]), mean_vectors))
while times < self.loop_times:
change_flag = 1 # 标记簇均值向量是否改变
for sample in self.data:
dist = []
for vec in mean_vectors:
dist.append(self.distance(vec, sample)) # 计算样本到每个聚类中心的距离
clusters[dist.index(min(dist))].append(sample) # 找到离该样本最近的聚类中心,并将它放入该簇
new_mean_vectors = []
for c,v in zip(clusters, mean_vectors): # zip()将两个对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表
cluster_num = len(c)
cluster_array = np.array(c)
new_mean_vector = sum(cluster_array) / cluster_num # 计算出新的聚类簇均值向量
mean_vector = np.array(v)
# np.divide和np.true_divide结果一样(python3.7.2),np.floor_divide只保留整数结果
# all(iterable):如果iterable(元组或者列表)的所有元素不为0、False或者iterable为空,all(iterable)返回True,否则返回False
if all(np.true_divide((new_mean_vector - mean_vector), mean_vector) < np.array([self.error, self.error])):
new_mean_vectors.append(mean_vector) # 均值向量未改变
change_flag = 0
else:
# dataFrame转List(),括号不能忘
new_mean_vectors.append(new_mean_vector.tolist()) # 均值向量发生改变
if change_flag == 1:
mean_vectors = new_mean_vectors
else:
break
times += 1
time2 = time.perf_counter()
# str.format(),基本语法是通过 {} 和 : 来代替以前的 %
print ('本次选取的{}个初始向量为{}'.format(self.k, initial_main_vectors))
print ('共进行{}轮'.format(times))
print ('共耗时{:.2f}s'.format(time2 - time1)) # 取2位小数
for cluster in clusters:
x = list(map(lambda arr: arr[0], cluster))
y = list(map(lambda arr: arr[1], cluster))
plt.scatter(x, y, marker = 'o', label = clusters.index(cluster)+1)
plt.xlabel('密度')
plt.ylabel('含糖率')
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.legend(loc='upper left')
plt.show()
for i in [2, 3, 4]:
# 调用K_means,执行方法fitting()
k_means = K_means(i, dataSet[['density', 'sugar_rate']].values.tolist(), 1000, 0.0000001)
k_means.fitting()