聚类分析:K-Means
软件及版本:Jupyter Notebook (Anaconda3)
作者:落寞红颜玉玫瑰
编程语言:python
1. 关于聚类
“物以类聚,人以群分”,聚类(Clustering)是人类认识大自然的一种重要方法。聚类就是按照事物的某些属性,把事物聚成簇,使簇类元素具有较高的相似性,使不同簇间的相似较差。
聚类属于无监督学习。它与分类的根本区别在于:分类是已知对象特征,根据特征来整理对象,而聚类则是找到对象特征。
聚类分析通常被作为数据预处理,是进一步分析和处理数据的基础。在生物学及商业分析上具有广泛的应用。在商业上,其可以帮助市场业务人员分析客户,并对客户进行分群;在互联网应用上,聚类分析被用来在网上进行文档归类,等等。
聚类分析算法取决于数据的类型。聚类的目的和应用。按照其主要思路的不同,可分为:划分方法,层次方法,基于密度的算法,基于网格的算法,基于模型的算法。
基于聚类分析的数据挖掘已经取得了很好的效果,但由于要处理巨大的,复杂的数据集,对数据分析也提出了特殊的挑战,根据其要求不同,主要要求有:可伸缩,可处理高维数据,可理解,发现任意形状的簇,可处理噪声数据,对数据顺序不敏感等。
2. K-Means 简介
此方法属于划分方法,同属于划分方法的还有:K中心点,PAM(Parting Around Medoid,围绕中心点的划分),CLARA(Clustering LARge Applications,大型应用中的聚类方法),CLARRANS(Clustering LARge Applications based upRANdomized Search,用于空间数据库的聚类算法)等。
其是一种古老的,最广泛使用的聚类方法。k均值用质心来表示一个簇,其中质心是一组数据的平均值。此算法以k为输入参数,将n个数据分为k个簇。
算法思想:
- 输入目标簇个数值:k;
- 随机在包含n个数据的数据集中选择k个对象,以此作为初始组,开始迭代计算;
- 对剩余对象:计算其与各个初始组中心点的距离(本文为欧式距离),将其分配到最近的簇,计算其判别函数。
- 重复上述第二步,直至簇不在发生变化。
判别函数(目标函数):是评判分组是否是当前数据集最佳的重要表达式。由k均值的定义及实现步骤可以得出其判别函数,不至一个,下文使用的为簇间距离最小模型,还有加法模型,乘法模型等。
依据1:簇内距离最小,dis_in=sum(x(i)-x)^2,其中i ~ {1-m},所有簇内点到中心点的欧式距离和。
依据2:簇间距离最大,dis_out = sum(k(j)-X)^2,其中j~{1,k},各个簇到总数据中心点的距离和,簇的代表元素为其簇中心点。
构建判别函数:1.min=sum(dis_in(j)),j~{1,k},k个组单独簇类距离和最小。
2.max=dis_out,k个组,簇间距离最大。
3.sum(dis_in)/dis_out,簇内距离越小效果越好,簇间距离越大越优,故此值越小越好,可使其等于某个收敛值。
3. 算法实现
## k-means 聚类算法。
# 组内距离最小,组间距离最大化。
#定义函数。写出伪代码:
#设分为 m 组,第一个组第一个元素为X11,平均数means为X1,以此类推;
#则公式为:min=E{1,m}(X1i-X1)^2,其中X1=(X11+X12+...+X1n)/n
#上面伪代码看出,这是一个迭代算法,故在复杂度方面需注意。
#引入需要的模块
import numpy
import time
import math
import json
#根据自己需要,可引进其他模块,比如:
import peewee
from concurrent.futures import ThreadingPoolExecutor
import matlibplot.pyplot as plt
自己定义一个平均值函数,更加适合本算法。
#自己定义一个平均值函数。
# 输入列表,输出中心点元组
def get_mean(*arges):
if len(*arges) == 0:
return "list is empty,please sure list is true,or not"
elif len(*arges) == 1:
return list(*arges)[0]
else:
temp_tuple=list(list(*arges)[0])
num_weight = len(*arges)
for i in range(len(list(*arges)[0])):
temp_single_mean=0
for item in list(*arges):
temp_single_mean += item[i]
temp_tuple[i]=round(temp_single_mean/num_weight,2)
return tuple(temp_tuple)
#or can writer code like this: return sum(*arges)/len(*arges)
定义一个类,方便储存,和查看各项参数。
import json
# 定义一个类,用来输出分类的组的各项属性
class KMeansGroup():
def __init__(self,row_list = None,group_num = None,convergence_val = None,iter_frequency = None,group_class = None):
self.rowList = row_list
self.groupClass = group_class
self.groupNum = group_num
self.iterFrequency = iter_frequency
self.convergenceVal = convergence_val
#重新构建str函数数,使输出的信息是分类信息
def __str__(self):
return json.dumps(self.groupClass,ensure_ascii=False)
def __repr__(self):
return str(self.groupClass)
#内置一个分类函数,调用时可对当前类重新再次分类
def group_class_again(self,convergence_val = 10**(-3),iter_frequency = 10):
iter_fun(self.groupClass)
定义一个迭代函数,为核心函数
#定义一个迭代函数,方便使用
def iter_fun(group_class):
#基本组分好了,我们进行迭代。
#1.先求出所有组的平均值
mean_list=[get_mean(group_class[item]) for item in group_class.keys()]
#print(mean_list)
#对每个组循环。重新分配变量
for i in range(len(group_class.keys())):
#长度为1的组不需要参与此次重新分配
if len(group_class[list(group_class.keys())[i]])>1:
#拿到当前组的中心点平均变量
item_mean = mean_list[i]
#当前组中的所有元素,单独进行比较
for item in group_class[list(group_class.keys())[i]]:
#距离列表,方便计算总距离和单个组的距离,方便分组。
distince_list=[]
#计算当前元素到各个组的距离
for ele in mean_list:
#单个双元素的距离:欧式距离。
both_distince=0
#计算欧式距离,每个方向上。
for j in range(len(item)):
both_distince += round((float(item[j])-ele[j])**2,2)
#将元素加入列表
distince_list.append(both_distince)
#重新分配值,得到最短距离所在的位置,并于当前所在位置作比较。
index=distince_list.index(min(distince_list))
#如果不等于当前所在位置,说明不是最合适的组,移动位置
if index != i:
#先添加到最短距离组,然后在当前组删除元素。
group_class["第【{}】组".format(index+1)].append(item)
group_class["第【{}】组".format(i+1)].remove(item)
else:
continue
return group_class
计算簇内距离和。
def groupMidDistinS(init_group):
temp_sum = 0
for item in init_group.keys():
temp_mean = get_mean(init_group[item])
for ele in init_group[item]:
both_distince=0
#计算欧式距离,每个方向上。
for j in range(len(ele)):
both_distince += round((float(ele[j])-temp_mean[j])**2,2)
temp_sum += both_distince
#print(temp_mean,ele,both_distince,temp_sum)
return temp_sum
主函数:进行分簇。
#输入项:类数目,收敛值,
#输出项:分类结果,组内距离和,各类中心点,及类元素数目
def class_by_kmeans(init_list,group_num,iter_frequency=0,convergence_val=10):
#引入一个类,让所有信息储存在类中
group = KMeansGroup(row_list=init_list,group_num=group_num,iter_frequency=iter_frequency,convergence_val=convergence_val)
if group_num > len(init_list):
return "类数目多余列表元素数目相等,不符合逻辑,输入有误,请重新输入。"
else:
start_num = 0
group_class={}
square_list = []
for i in range(1,group_num+1):
group_class["第【{}】组".format(i)]=[]
group_class["第【{}】组".format(i)].append(init_list[i-1])
if group_num == len(init_list):
group.groupClass=group_class
return group
elif group_num == len(init_list)-1:
for ele in init_list[:-1]:
square_sum = 0
for i in range(len(init_list[-1])):
square_sum += (float(list(ele)[i])-init_list[-1][i])**2
square_list.append(square_sum)
index=square_list.index(min(square_list))
group_class["第【{}】组".format(index+1)].append(init_list[-1])
group.groupClass = group_class
return group
else:
#随机分配。先确定大组,然后进行迭代
for item in init_list[group_num:]:
distince_list=[]
for ele in init_list[:group_num]:
both_distince=0
for i in range(len(item)):
both_distince += (float(item[i])-ele[i])**2
distince_list.append(both_distince)
#print(min(distince_list))
index=distince_list.index(min(distince_list))
group_class["第【{}】组".format(index+1)].append(item)
#print(group_class)
front_sub=groupMidDistinS(group_class)
convergence_count=0
flag = True
#迭代函数,传入可接受迭代次数,当达到迭代次数后,无论是否是最佳分组,都将返回分组
while flag:
#接受返回参数,相当于更新group_class。
group_class=iter_fun(group_class)
now_sub=groupMidDistinS(group_class)
if front_sub-now_sub < convergence_val:
convergence_count += 1
if convergence_count > iter_frequency:
flag = False
group.groupClass = group_class
return group
front_sub = now_sub
以上便是算法的雏形,完善及优化在下面开始。
4. 算法调教
先输入一个一维数据,来测试一下各种条件。
#一维数据输入,应该遵循下面格式,当然可以加判别,数据的元素,
lst=[(2,),(4,),(10,),(12,),(3,),(20,),(30,),(11,),(25,)]
"""
init_list:初始数据集,应该按照所规定的格式输入:列表输入,单个元素为元组;
group_num:期望的簇数,不应该超过列表长度;
iter_frewuency:迭代次数,当判别函数小于收敛值的次数,大于此值时,认为当前为最优分组,停止迭代;
convergence_val:收敛值,当判别函数小于此值时,认为目前分组暂时最优,有待进一步判断。(1太大了,不过目前不影响)
"""
print(class_by_kmeans(lst,group_num=9,iter_frequency=10,convergence_val=1).groupClass)
"""
output:
{"第【1】组": [[2]], "第【2】组": [[4]], "第【3】组": [[10]], "第【4】组": [[12]], "第【5】组": [[3]], "第【6】组": [[20]], "第【7】组": [[30]], "第【8】组": [[11]], "第【9】组": [[25]]}
"""
验证正确,我们输入一个长于列表长度的值。
a=class_by_kmeans(lst,group_num=12,iter_frequency=10,convergence_val=1)
print(a)
#output:类数目多余列表元素数目相等,不符合逻辑,输入有误,请重新输入。
验证正确,将其簇数改为2,看看。
a=class_by_kmeans(lst,group_num=2,iter_frequency=10,convergence_val=1)
print(a)
#output:{"第【1】组": [[2], [3], [4], [10], [12], [11]], "第【2】组": [[20], [30], [25]]}
接下来升级一下难度,输入二维数组。
example_list=[(1,2),(2,2),(12,13),(5,6),(20,9)]
class_by_kmeans(example_list,group_num=3,convergence_val=1)
#output:{'第【1】组': [(1, 2), (2, 2)], '第【2】组': [(5, 6)], '第【3】组': [(12, 13), (20, 9)]}
多输入几组二维数组,发现分组的可靠度很高。
接下来,输入高维数据,大数据(本例中最多为10000条6维的数据)。
构建一个产生随机高维数组的简单函数。
#可以通过改变j来改变数据维度,i来改变数据条数。
exp_list=[tuple([np.random.randint(1000) for j in range(5)]) for i in range(50000)]
5. 算法优化
5.1 时间复杂度
我们来看看不同长度元素分类所需要的时间。for z in range(10,200):
exp_list=[tuple([np.random.randint(1000) for j in range(5)]) for i in range(z*10)]
#print(exp_list)
a=class_by_kmeans(exp_list,group_num=3,iter_frequency=10,convergence_val=10**(-2))
从上图可以看出,其时间复杂度为O(nkt),即随着数据的增大,时间成线性增加。在数据长度为200时,分簇时长为:0.48s,而当数据长度为2000时,时间为:1.152。由此推测当数据长度为50000时,时间为60s,实际运行时长124s。此处可用多线程加网格算法优化内部结构。
我们从维度方向来看,时间复杂度。
#实际应用中维度达到30的很少吧,所以以长度为1000的数据来测试。
for z in range(1,30):
exp_list=[tuple([np.random.randint(1000) for j in range(z)]) for i in range(1000)]
#print(exp_list)
a=class_by_kmeans(exp_list,group_num=3,iter_frequency=10,convergence_val=10**(-2))
从上图可以看出,当维度为1时,对1000条数据进行分簇所用时长为:0.26s,而当维度达到29时,其时间达到了恐怖的5.84s。
从上面实验可以看出,当数据长度达到上万条时,就已经有了明显的压力,如果再加上维度的处理,那么分簇时长将会成倍叠加,这不符合我们设计算法的初衷。优化其算法可通过多线程,或者网格算法。
最后,我们看看分簇组数的变化与分簇时长的关系。
#对同一个数组进行不同组数的分簇
exp_list=[tuple([np.random.randint(1000) for j in range(5)]) for i in range(1000)]
for z in range(1,100):
#print(exp_list)
a=class_by_kmeans(exp_list,group_num=z,iter_frequency=10,convergence_val=10**(-2))
可以看到,分组的时间复杂度不亚于维度的时间复杂度,分为一个组时,所需时长:0.16s,而当分为1000组时,其所需时间为:16.70s。在此处采取的策略时:当组的元素个数达到一定值时,采取深度优先原则,先大组,后小组,即先将列表分为几个大组,然后对单个大组进行分类,分为几个小组。
空间复杂度目前不需要优化。
以上便是所以内容,欢迎指正错误。优化算法后面有时间就优化了。
源代码详见资源。资源中为未优化的K-means算法,但对于10000条,单个数据拥有7个变量及以下的数据进行分簇是完全够用的。
另附工作中的数据对其分类结果,以及其准确性的报告。作为算法实际应用的实验。