机器学习之K均值

1. 聚类:

无监督算法,区别监督学习的各类机器学习算法; 无监督方法有哪些?

聚类 vs 分类

2. K均值算法:

  • 随机初始化K个样本(点),称之为簇中心(cluster centroids);
  • 簇分配: 对于所有的样本,将其分配给离它最近的簇中心;
  • 移动簇中心:对于每一个簇,计算属于该簇的所有样本的平均值,移动簇中心到平均值处;
  • 重复步骤2和3,直到找到我们想要的簇(即优化目标)

3. 优化目标:

重新描述在K均值算法中使用的变量:

c(i) = index of cluster (1,2,…, K ) to which example  is currently assigned

uk= cluster centroid k

 uc(i)= cluster centroid of cluster to which example  has been assigned

使用这些变量,定义我们的cost function如下:

 

所以我们的优化目标就是最小化J

在簇分配步骤中,我们的目标是通过改变最小化 J函数(固定u)

在移动簇中心步骤中,我们的目标通过改变u最小化J函数(固定c)

注意,在K均值算法中,cost function不可能能增加,它应该总是下降的(区别于梯度下降法)。

思考:初始化簇中心的方法

https://stats.stackexchange.com/questions/133656/how-to-understand-the-drawbacks-of-k-means

https://www.zhihu.com/question/31296149

优点:原理简单(实际上就是一个优化去全局MSE(Mean Square Error)和局部MSE的过程),实现容易,收敛快

缺点:

  • 算法复杂度高O(nkt)
  • 不能发现非凸形状的簇,或大小差别很大的簇(对类别规模差异太明显的数据效果不好。比如总共有A、B两类,ground truth显示A类有1000个点,B类有100个点。)
  • 需样本存在均值(限定数据种类)
  • 只应用在连续型的数据
  • 需先确定k的个数
  • Kmeans 其实是在做convex optimization ,遇到non-convex 的distribution 就挂了。怎么办?加Kernel 。加什么Kernel ?慢慢试吧,svm用什么Kernel好不就是这一个个试出来的嘛。
  • 对噪声和离群点敏感(对噪声敏感。比如总共有A、B两类,每类500个点,而且相距不远。此时在离这两类很远的地方加一个点(可以看作噪声),对分类中心会有很大的影响(因为分类中心是取平均)。
  • 最重要是结果不一定是全局最优,只能保证局部最优。

4. K-Means的细节问题

1) K值怎么定?我怎么知道应该几类?

答:这个真的没有确定的做法,分几类主要取决于个人的经验与感觉,通常的做法是多尝试几个K值,看分成几类的结果更好解释,更符合分析目的等。或者可以把各种K值算出的SSE做比较,取最小的SSE的K值。

2) 初始的K个质心怎么选?

答:最常用的方法是随机选,初始质心的选取对最终聚类结果有影响,因此算法一定要多执行几次,哪个结果更reasonable,就用哪个结果。 当然也有一些优化的方法.

第一种是选择彼此距离最远的点,具体来说就是先选第一个点,然后选离第一个点最远的当第二个点,然后选第三个点,第三个点到第一、第二两点的距离之和最小,以此类推。

第二种是先根据其他聚类算法(如层次聚类)得到聚类结果,从结果中每个分类选一个点。

3) K-Means会不会陷入一直选质心的过程,永远停不下来?

答:不会,有数学证明K-Means一定会收敛,大致思路是利用SSE的概念(也就是误差平方和),即每个点到自身所归属质心的距离的平方和,这个平方和是一个函数,然后能够证明这个函数是可以最终收敛的函数。

4) 判断每个点归属哪个质心的距离怎么算?

答:这个问题必须不得不提一下数学了……

第一种,欧几里德距离(欧几里德这位爷还是很厉害的,《几何原本》被称为古希腊数学的高峰,就是用5个公理推导出了整个平面几何的结论),这个距离就是平时我们理解的距离,如果是两个平面上的点,也就是(X1,Y1),和(X2,Y2),那这俩点距离是多少初中生都会,就是√( (x1-x2)^2+(y1-y2)^2) ,如果是三维空间中呢?√( (x1-x2)^2+(y1-y2)^2+(z1-z2)^2 ;推广到高维空间公式就以此类推。可以看出,欧几里德距离真的是数学加减乘除算出来的距离,因此这就是只能用于连续型变量的原因。

第二种,余弦相似度,余弦相似度用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小。相比距离度量,余弦相似度更加注重两个向量在方向上的差异,而非距离或长度上。下图表示余弦相似度的余弦是哪个角的余弦,A,B是三维空间中的两个向量,这两个点与三维空间原点连线形成的角,如果角度越小,说明这两个向量在方向上越接近,在聚类时就归成一类:

看一个例子(也许不太恰当):歌手大赛,三个评委给三个歌手打分,第一个评委的打分(10,8,9) 第二个评委的打分(4,3,2),第三个评委的打分(8,9,10)

如果采用余弦相似度来看每个评委的差异,虽然每个评委对同一个选手的评分不一样,但第一、第二两个评委对这四位歌手实力的排序是一样的,只是第二个评委对满分有更高的评判标准,说明第一、第二个评委对音乐的品味上是一致的。

因此,用余弦相似度来看,第一、第二个评委为一类人,第三个评委为另外一类。

如果采用欧氏距离, 第一和第三个评委的欧氏距离更近,就分成一类人了,但其实不太合理,因为他们对于四位选手的排名都是完全颠倒的。

总之,如果注重数值本身的差异,就应该用欧氏距离,如果注重的是上例中的这种的差异(我概括不出来到底是一种什么差异……),就要用余弦相似度来计算。

还有其他的一些计算距离的方法,但是都是欧氏距离和余弦相似度的衍生,简单罗列如下:明可夫斯基距离、切比雪夫距离、曼哈顿距离、马哈拉诺比斯距离、调整后的余弦相似度、Jaccard相似系数……

5) 还有一个重要的问题是,大家的单位要一致!

比如X的单位是米,Y也是米,那么距离算出来的单位还是米,是有意义的

但是如果X是米,Y是吨,用距离公式计算就会出现“米的平方”加上“吨的平方”再开平方,最后算出的东西没有数学意义,这就有问题了。

还有,即使X和Y单位一致,但是如果数据中X整体都比较小,比如都是1到10之间的数,Y很大,比如都是1000以上的数,那么,在计算距离的时候Y起到的作用就比X大很多,X对于距离的影响几乎可以忽略,这也有问题。

因此,如果K-Means聚类中选择欧几里德距离计算距离,数据集又出现了上面所述的情况,就一定要进行数据的标准化(normalization),即将数据按比例缩放,使之落入一个小的特定区间。去除数据的单位限制,将其转化为无量纲的纯数值,便于不同单位或量级的指标能够进行计算和比较。

标准化方法最常用的有两种:

  • min-max标准化(离差标准化):对原始数据进行线性变换,是结果落到【0,1】区间,转换方法为 X'=(X-min)/(max-min),其中max为样本数据最大值,min为样本数据最小值。
  • z-score标准化(标准差标准化):处理后的数据符合标准正态分布(均值为0,方差为1),转换公式:X减去均值,再除以标准差

6) 每一轮迭代如何选出新的质心?

答:各个维度的算术平均,比如(X1,Y1,Z1)、(X2,Y2,Z2)、(X3,Y3,Z3),那就新质心就是【(X1+X2+X3)/3,(Y1+Y2+Y3)/3,(Z1,Z2,Z3)/3】,这里要注意,新质心不一定是实际的一个数据点。

7) 关于离群值?

答:离群值就是远离整体的,非常异常、非常特殊的数据点,在聚类之前应该将这些“极大”“极小”之类的离群数据都去掉,否则会对于聚类的结果有影响。但是,离群值往往自身就很有分析的价值,可以把离群值单独作为一类来分析。

8) 用SPSS作出的K-Means聚类结果,包含ANOVA(单因素方差分析),是什么意思?

答:答简单说就是判断用于聚类的变量是否对于聚类结果有贡献,方差分析检验结果越显著的变量,说明对聚类结果越有影响。对于不显著的变量,可以考虑从模型中剔除。

5. 聚类分析中业务专家的作用

业务专家的作用非常大,主要体现在聚类变量的选择和对于聚类结果的解读:

比如要对于现有的客户分群,那么就要根据最终分群的目的选择不同的变量来分群,这就需要业务专家经验支持。如果要优化客户服务的渠道,那么就应选择与渠道相关的数据;如果要推广一个新产品,那就应该选用用户目前的使用行为的数据来归类用户的兴趣。算法是无法做到这一点的。

欠缺经验的分析人员和经验丰富的分析人员对于结果的解读会有很大差异。其实不光是聚类分析,所有的分析都不能仅仅依赖统计学家或者数据工程师。

6. 改进算法

《机器学习实战》中有一个改进的算法,是为了克服Kmeans算法收敛于局部最小值的问题而提出来的:二分K-均值算法

7. 实践应用效果

实现代码:

# -*- coding: utf-8 -*-
#最原始的Kmeans算法
from numpy import *
'''
遇到的问题:numpy的mean函数
inf关键字的含义:无穷值
nonzero(clusterAssment[:, 0].A 
'''
#加载数据,存入矩阵中
def loadDataSet(fileName):
	dataMat = []
	fr = open(fileName)
	for line in fr.readlines():
		curLine = line.strip().split()
		fltLine = map(float, curLine) # map all elements to float()
		dataMat.append(fltLine)
	return dataMat

#利用欧式距离计算样本点之间的距离
def disEclud(vecA, vecB):
	return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB)

#随机初始化中心点
def randCent(dataSet, k):

	n = shape(dataSet)[1]
	#print n
	centroids = mat(zeros((k, n))) #建立K行n列的中心点矩阵,n是特征数(其实就是样本点子集合)
	#print centroids
	for j in range(n):
		minJ = min(dataSet[:,j]) # 找每一维的最小值
		#print "minJ:"
		#print minJ
		#print "$$$$$$$$$$$$$"
		rangeJ = float(max(dataSet[:, j]) - minJ)
		centroids[:,j] = minJ + rangeJ * random.rand(k, 1) # 中心点的位置根据最小值和取值范围的取值来定
		#print centroids[:, j]
	#print "centroids[:, 0]"
	#print centroids[:, 0]
	#print "_________________"
	#print centroids
	#print "_____________________"
	return centroids

# Kmeans (这里就要开始设置参数K了)
def kmeans(dataSet, k, distMeas = disEclud, createCent = randCent):
	m = shape(dataSet)[0]
	clusterAssment = mat(zeros((m, 2))) #簇分配矩阵,一列是记录簇索引值,第二列存储误差(当前点当簇中心的距离)
	centroids = createCent(dataSet, k)
	clusterChanged = True
	while clusterChanged:
		clusterChanged = False
		for i in range(m): # 找距离每个点最近的质心,遍历所有中心点并计算每个中心点和样本点的距离
			minDist = inf; minIndex = -1
			for j in range(k):
				distJI = distMeas(centroids[j,:], dataSet[i, :]) # 计算每个簇和其他样本点的距离. 找到那个离第i个样本点最近的中心点
				if distJI < minDist:
					minDist = distJI; minIndex = j # 找到距离第i个样本点最近的簇
			if clusterAssment[i,0] != minIndex: # 如果这个点距离的簇中心变了,则clusterChanged=True
				clusterChanged = True
			clusterAssment[i, :] = minIndex, minDist ** 2 # 这个索引值是指第几个中心点,将第i和样本点的簇中心标记好
		#print centroids
		# 上面计算好所有点的簇,就是某次聚类完成以后,再重新选择中心点
		for cent in range(k): #重新计算中心点
			ptsInClust = dataSet[nonzero(clusterAssment[:, 0].A == cent)[0]]
			centroids[cent, :] = mean(ptsInClust, axis = 0) # 选择属于该簇的所有点的均值作为该簇新的中心点
			#print centroids[:, j]
	return centroids, clusterAssment

# 二分法的k均值
def biKmeans(dataSet, k, distMeas = disEclud):
	m = shape(dataSet)[0] #样本点的数目
	clusterAssment = mat(zeros((m, 2))) # 所有样本点的簇属性标记(m行2列(列属性同上))
	centroid0 = mean(dataSet, axis = 0).tolist()[0] # 将所有点分为一个簇,计算簇中心
	centList = [centroid0]
	for j in range(m):
		clusterAssment[i, 1] = distMeas(mat(centroid0), dataSet[j, :])**2 # 计算样本点到簇中心的距离,就是计算误差平方和
	while(len(centList) < k): # 结束的条件是:当簇个数达到k以后停止
		lowestSSE = inf
		for i in range(len(centList)):
			ptsInCurClust = dataSet[nonzero(clusterAssment[:, 0].A == i)[0], :]
			centroidMat, splitClustAss = kmeans(ptsInCurClust, 2, distMeas) # 利用kmeans分成两个簇,再分别计算着两个簇的误差值
			sseSplit = sum(splitClustAss[:, 1])
			sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:, 0].A != i), i])
			print "sseSplit, and sseNotSplit:", sseSplit, sseNotSplit
			if(sseSplit + sseNotSplit < lowestSSE):
				bestCentToSplit = i
				bestNewCents = centroidMat
				bestClustAss = splitClustAss.copy()
				lowestSSE = sseNotSplit + sseSplit
		bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0], 0] = len(centList)
		bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestCentToSplit
		print 'the bestCentToSplit is: ',bestCentToSplit
        print 'the len of bestClustAss is: ', len(bestClustAss)
        centList[bestCentToSplit] = bestNewCents[0, :].tolist()[0]
        centList.append(bestNewCents[1, :].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss#reassign new clusters, and SSE
	return mat(centList), clusterAssment
import urllib
import json
def geoGrab(stAddress, city):
    apiStem = 'http://where.yahooapis.com/geocode?'  #create a dict and constants for the goecoder
    params = {}
    params['flags'] = 'J'#JSON return type
    params['appid'] = 'aaa0VN6k'
    params['location'] = '%s %s' % (stAddress, city)
    url_params = urllib.urlencode(params)
    yahooApi = apiStem + url_params      #print url_params
    print yahooApi
    c=urllib.urlopen(yahooApi)
    return json.loads(c.read())
if __name__ == "__main__":
	dataMat = mat(loadDataSet("testSet.txt"))
	geoGrab('1 VA Center', 'Augusta')
	#print dataMat[:, 0] == -5.379713
	#print dataMat[:, 0].A == -5.379713
	#rint (10000000 > inf)
	#centroid0 = mean(dataMat, axis = 0).tolist()[0]
	#print [centroid0]
	#print dataMat
	#print dataMat
	# print min(dataMat[:,0])
	#rand = randCent(dataMat, 4)
	#print shape(rand)
	#print "*********************"
	#print rand
	#print disEclud(dataMat[0], dataMat[1])
	#print centroids()
	#kmeans(dataMat, 4)

8. 聚类效果评估

SSE:Sum of Square Error(误差平方和),SSE越小离中心店越近,聚类效果更好。

如何降低SSE:一种是:增加簇的个数(k),另一种是:将具有最大的SSE的簇拆开,为了将簇个数保持不变,可以将某两个簇合并,合并的方法有:合并最近的两个中心点,第二种是合并然后计算SSE,直到扎到两个簇合并后有最小SSE的值。

posted @ 2017-09-05 13:27  张不  阅读(914)  评论(0编辑  收藏  举报