底层实现K-means++算法并运用于寻找数据异常点
前言
本篇文章,我们基于自己定义的方法而非调用现成模块来解决运用整体维度的数据做异常点筛选问题,并最终对结果进行可视化展示。
岁月如云,匪我思存,写作不易,望路过的朋友们点赞收藏加关注哈,在此表示感谢!
一:聚类算法的介绍
聚类算法大致介绍
与分类算法不同,在没有给定划分类别的情况下,根据数据相似度进行样本分组的一种方法。根据原始样本的数据分布特征,可以分为半监督聚类和无监督聚类。
- 半监督聚类:充分利用已知标签信息再对未知标签信息进行分类划分
- 无监督聚类:没有任何已知标签信息的进行相似性分类划分
常用聚类方法为:
- 划分聚类:K-Means(++),K-Medoids,Clarans等
- 层次聚类:Birch,Cure,AgglomerativeClustering等
- 基于密度方法:DBSCAN,DENCLUE,OPTICS等
- 基于网格:STING,CLIOUE等
- 其他方法:统计学,深度学习等
当然方法很多,实际中能运用好理解好其中几个,就能解决很多问题了。
聚类效果的评价方法也很多,其中最简便实用的是purity方法,当然还有一些高级判别方法如轮廓系数,肘方法,calinski_harabaz值等等,
-
purity方法指的是只计算正确聚类占总数的比例即
p u r i t y ( X , Y ) = 1 n ∑ i k ( x i ∩ y i ) purity(X,Y)=\frac{1}{n}\sum_{i}^{k}{(x_i\cap y_i)} purity(X,Y)=n1∑ik(xi∩yi),其中
x = ( x 1 , x 2 , . . . x k ) x=(x_1,x_2,...x_k) x=(x1,x2,...xk) 是已经聚类好的集合, y = ( y 1 , y 2 , . . . , y k ) y=(y_1,y_2,...,y_k) y=(y1,y2,...,yk)
表示需要被聚类的原始数据集合, n n n 表示被聚类的对象总数, -
一些高级判别法在后面做些介绍,这里不再过多描述。
- 底层编写K-means代码实现
本文主要通过K-means++算法找出数据的多个聚类中心,然后找出数据的异常点,所以我们先用简单的语句回顾下K-mean算法的底层原理。
- 初始化聚类种类个数常数 k ,随机选取初始点为数据中心,
- 定义每一样本数据跟每个中心的距离运算(这里的每个样本点维度跟每个中心点维度必然一致,可以做矩阵运算),将样本归类到最相似的类中,
- 重新计算中心值并带入上述步骤2中,直到每个样本属于哪个类别不再改变为止(当然我们也可以最开始设定迭代多少次运算才退出逻辑),
- 输出最终的中心值以及每个类。
根据以上步骤我们来实现K-means算法:
- 定义计算样本与中心点的距离函数
import numpy as np
def distance(vA,vB):
d_t = vA - np.array(vB)[0]
dist = np.power(sum(np.power(d_t, 4)), 1 / 4)
# dist = np.dot((vA - vB),(vA - vB).T)[0,0]##两种计算距离公式
return dist
- 定义初始化随机聚类中心函数
def init_randCent(data,K):
n = np.shape(data)[1]##属性的个数
cent_value = np.mat(np.zeros((K,n)))
for j in range(n):
min_t = np.min(data[:,j])
range_t = np.max(data[:,j]) - min_t
##在最大值和最小值之间随机初始化
cent_value[:,j] = min_t * np.mat(np.ones((K,1))) + np.random.rand(K,1) * range_t
return cent_value
- 进行主要逻辑编写,本文采用递归方法编写
def K_means(data, K, cent_value):###K为我们设定的聚类数据
m = np.shape(data)[0]
n = np.shape(data)[1]
subcenter = np.mat(np.zeros((m, 2))) ##初始化默认所有样本都是属于第1个类的
count = 0
ls_r = []
def main_process(tag, count, ls_r):##定义递归函数
if tag == False:##定一个触发事件,初始为True,执行else
df1 = pd.DataFrame(subcenter,columns=['聚类类别','与中心距离(0代表初始化距离,一直为这个类不曾变过)'])
df2 = pd.DataFrame(cent_value,index=['类别1','类别2','类别3'])
ls_rr = []
for i in ls_r:
ls_rr.append(round(i / sum(ls_r),3))
df3 = pd.DataFrame([ls_r,ls_rr],columns=['类别1','类别2','类别3'],index=['样本个数','占比%']).T
df4 = pd.concat([df2,df3],axis=1)
print(df1)
print(df4)
print('\033[1;38m共迭代次数:%s\033[0;m'%count)
return 'END!!'
else:
ls_r = []
tag = False
count = count + 1
for i in range(m):
minDist = np.inf
minIndex = 0
for j in range(K):
###计算i跟每个聚类之间的距离
dist = distance(data[i, :], cent_value[j, :])
if dist < minDist:
minDist = dist
minIndex = j
###判断是否需要改变
if subcenter[i, 0] != minIndex: ##需要改变
subcenter[i, :] = np.mat([minIndex, minDist])
tag = True
###重新计算聚类中心
for j in range(K):
sum_all = np.mat(np.zeros((1, n)))
r = 0
for i in range(m):
if subcenter[i, 0] == j: ###计算第j个类别
sum_all = sum_all + data[i, :]##属于相同类别的中心值相加
r = r + 1
for k in range(n):
try:
cent_value[j, k] = sum_all[0, k] / r###同种类的所有特征数据取均值
except ZeroDivisionError:
print('r is zero!')
ls_r.append(r)
return main_process(tag, count, ls_r)
main_process(True, count, ls_r)#调用递归函数
K_means(data, 3, init_randCent(data, 3))
PS:以上代码经过本人调试复制粘贴后直接可以使用的,其中数据的输入为数据框形式!
基于上篇文章数据,我们测试下结果
运用进阶版3sigm准则处理实际问题
聚类类别 与最短距离(0代表初始化距离,一直为这个类不曾变过)
0 2.0 0.434378
1 2.0 0.301428
2 1.0 0.192687
3 2.0 0.427841
4 2.0 0.233552
.. ... ...
935 2.0 0.347604
936 2.0 0.334781
937 2.0 0.299281
938 2.0 0.378599
939 2.0 0.433147
[940 rows x 2 columns]
0 1 2 样本个数 占比%
类别1 0.505249 0.272886 0.258199 37.0 0.039
类别2 0.120483 0.553747 0.151894 325.0 0.346
类别3 0.125011 0.123172 0.115365 578.0 0.615
共迭代次数:10
我们可以在聚类前对数据进行标准化处理来降低量级带来的差异,避免某个属性值过大而距离运算完全取决于这某一属性。标准化方法可以采用最大最小标准化,即 ( V − m i n ( V ) ) / ( m a x ( V ) − m i n ( V ) ) (V-min(V))/(max(V)-min(V)) (V−min(V))/(max(V)−min(V)) 。
上述结果类别列的右侧为数据集的最终聚类中心。通过底层代码我们知道,每次的初始化中心是随机得来的,所以每次的聚类结果会有所不同,有可能会出现在距离运算中存在局部最优而不再迭代更新的情况,尽管数据标准化了,但有些聚类结果还是会有所倾斜,解决这种情况可以用蒙特卡洛求期望的方法,本篇先暂不展开叙述。
二:实现K-means++算法
接下来,我们底层理解和实现K-means++算法,并最终用此算法进行系统化异常值筛选。
- 底层理解K-means++算法
K-means++算法主要是对初始化的数据中心做处理,在排除异常点的情况下,我们希望初始化中心点尽量远一点,具体步骤如下:
- 先确定聚类中心的个数 k 值,从输入数据中随机选择第一个聚类中心,
- 对数据集中的每一个点,计算它与已经存在的聚类中心的距离,我们设为 D ( x e x i s t ) D(x_{exist}) D(xexist) ,
- 再选择新的数据点作为新的聚类中心,选择的要求是 D ( x e x i s t ) D(x_{exist}) D(xexist) 较大的点,被选择的概率也较大,
- 重复上述步骤2和3,直到 k k k 个中心被选择出来,剩下的操作跟一般K-means一样。
注意,这里异常点的存在对新的聚类中心选择的影响。因为异常点存在, D ( x e x i s t ) D(x_{exist}) D(xexist) 会非常大,我们有两种方法处理这种情况,1:在建模前处理掉异常点,但如果我们就是要通过聚类方法来处理异常点的,那就行不通,2:在距离值集合 D ( x ) = { D ( x 1 e x i s t ) , D ( x 2 e x i s t ) . . . , D ( x n e x i s t ) } D(x)=\{D(x_{1exist}),D(x_{2exist})...,D(x_{nexist})\} D(x)={ D(x1exist),D(x2exist)...,D(xnexist)} 中( n n n 是数据个数),我们不能直接选择最大的距离值,而是选择较大的那个数据点,这里的“较大的”,可以通过“面积概率思想”选择,即把这些距离“接在一块”,那么随机点落在较大的 D ( x i e x i s t ) D(x_{iexist}) D(xiexist) 内的概率也较大。
我们用代码实现上述过程
init_value = np.inf#先定义为无穷大
def nest_dist(point,clc):
min_dist = init_value
m = np.shape(clc)[0]###当前已经初始化的聚类中心个数
for i in range(m):
d = distance(point,clc[i,:])##计算与每个聚类中心的距离
###选择最短距离
if min_dist > d:
min_dist = d
return min_dist
def get_cent_value(data,K):
m,n = np.shape(data)###m为数据个数
clc = np.mat(np.zeros((K,n)))
###随机选择一个样本点为第一个聚类中心
index = np.random.randint(0,m)
clc[0,:] = np.copy(data[index,:])
##初始化一个距离序列
d = [0 for i in range(m)]
for i in range(1,K):
sum_all = 0
for j in range(m):
##计算每一个样本跟已找到的聚类中心做距离运算,并返回最近的一个距离值
d[j] = nest_dist(data[j,:],clc[0:i,:])
##将所有最短距离相加
sum_all = sum_all + d[j]
sum_all = sum_all * np.random.random()###取
##获得距离最远的样本点作为聚类中心点
for j,dist in enumerate(d):
sum_all = sum_all - dist
if sum_all > 0:
continue
else:
clc[i] = np.copy(data[j,:])
break
return clc
K_means(data, 3, get_cent_value(data,3))
我们看下运行结果:
聚类类别 与中心距离(0代表初始化距离,一直为这个类不曾变过)
0 2.0 0.127224
1 2.0 0.172709
2 0.0 0.000000
3 1.0 0.208919
4 1.0 0.157283
.. ... ...
935 2.0 0.136519
936 2.0 0.137244
937 1.0 0.155543
938 2.0 0.194231
939 2.0 0.126017
[940 rows x 2 columns]
0 1 2 样本个数 占比%
类别1 0.118565 0.621701 0.162584 231.0 0.246
类别2 0.188617 0.313694 0.151047 265.0 0.282
类别3 0.133774 0.077739 0.108143 444.0 0.472
共迭代次数:14
同样数据进行标准化处理后再运用K-means++算法,我们发展聚类结果比较”平均”,而且多次运行结果也比较稳定。主要因为K-means++能在开始聚类时候尽可能优化初始中心点值,使各聚类中心尽可能远一些,而不是基于更多的偶然性造成的局部最优。
三:基于K-means++算法进行数据异常值筛选
- 此篇文章从底层实现了K-means++算法,当然需要运用一番
基于上一篇文章,我们仅需要在做每一行 p 阶范数时稍加修改就行,改为for循环形式即可,因为现在需要每一个类数据分别要跟每一个类的中心做运算,最后再把所有结果合并就行了,代码比较简单,这里不再过多叙述,另外我们选择的聚类数目为3。
运用进阶版3sigmsigm准则处理实际数据及异常点的进阶处理
四:总结
- 本篇文章从算法底层原理出发,自己实现了k-means++算法,并最终用于异常值的筛选上,理论上k-means++算法是优于普通k-means算法的,
- 尽管如此,我们没有解决一个重要问题,那就是使用聚类算法时(无论是层次聚类还是划分聚类等等),没有事先规定到底聚多少类最好。一般聚类类别数的的确定采用“后验”方法确定,比如肘方法、CH值等,同时为防止局部最优现象,可以采用蒙特卡洛取期望的思想。这些内容我们将在下一篇具体呈现。