机器学习实战----Kmeans(地图点聚类的没做)

今天是周天呢~还是逼着自己出来写完...

原理

聚类是一种无监督的学习,它将相似的对象归到同一簇中。聚类的方法几乎可以应用所有对象,簇内的对象越相似,聚类的效果就越好。K-means算法中的k表示的是聚类为k个簇,means代表取每一个聚类中数据值的均值作为该簇的中心,或者称为质心,即用每一个的类的质心对该簇进行描述。

  聚类和分类最大的不同在于,分类的目标是事先已知的,而聚类则不一样,聚类事先不知道目标变量是什么,类别没有像分类那样被预先定义出来,所以,聚类有时也叫无监督学习。

  聚类分析试图将相似的对象归入同一簇,将不相似的对象归为不同簇,那么,显然需要一种合适的相似度计算方法,我们已知的有很多相似度的计算方法,比如欧氏距离,余弦距离,汉明距离等。事实上,我们应该根据具体的应用来选取合适的相似度计算方法。

  当然,任何一种算法都有一定的缺陷,没有一种算法时完美的,有的只是人类不断追求完美,不断创新的意志。K-means算法也有它的缺陷,但是我们可以通过一些后处理来得到更好的聚类结果,这些在后面都会一一降到。

  K-means算法虽然比较容易实现,但是其可能收敛到局部最优解,且在大规模数据集上收敛速度相对较慢。

2 K-means算法的工作流程

  首先,随机确定k个初始点的质心;然后将数据集中的每一个点分配到一个簇中,即为每一个点找到距其最近的质心,并将其分配给该质心所对应的簇;该步完成后,每一个簇的质心更新为该簇所有点的平均值。伪代码如下:

复制代码

创建k个点作为起始质心,可以随机选择(位于数据边界内)
  当任意一个点的簇分配结果发生改变时
    对数据集中每一个点
        对每个质心
          计算质心与数据点之间的距离
        将数据点分配到距其最近的簇
    对每一个簇,计算簇中所有点的均值并将均值作为质心

下面是实现代码:

#这是将文件数据导入的函数,需要注意的就是在python3中map的返回值是iterators,不再是列表。

def loadDataSet(filename):
    dataSet = []
    with open(filename,'r') as f:
        lines =f.readlines()
        for line in lines:
            line = line.strip().split("\t")
            #python3中map的返回值是iterators,不再是list
            line = list(map(float,line))
            dataSet.append(line)
    return dataSet

#计算两个向量之间的欧氏距离
def distEclud(vecA,vecB):
    return sqrt(sum(power(vecA-vecB,2)))

#随机生成k个质心
def randCent(dataSet,k):
    n = dataSet.shape[1]
    randMat = mat(zeros((k,n)))
    for i in range(n):
        mindig = min(dataSet[:,i])
        #print(mindig)
        #千万不能落下这个float,因为这个数组max和min生成的仍然是一个数组。如果不用float,后面进行实数乘矩阵会变成维度不同的矩阵相乘。
        rangedig = float(max(dataSet[:,i])-mindig)
        #print(rangedig),不加mat的话输出聚类矩阵时,k值有问题
        randMat[:,i] = mat(mindig + rangedig * random.rand(k,1))
    return randMat

#k均值算法
def KMeans(dataSet,k,distMeas=distEclud,createCent=randCent):
    m = dataSet.shape[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):
                temp = distMeas(dataSet[i,:],centroids[j,:])
                if(temp

有时候当我们观察聚类的结果图时,发现聚类的效果没有那么好,如上图所示,K-means算法在k值选取为3时的聚类结果,我们发现,算法能够收敛但效果较差。显然,这种情况的原因是,算法收敛到了局部最小值,而并不是全局最小值,局部最小值显然没有全局最小值的结果好。

  那么,既然知道了算法已经陷入了局部最小值,如何才能够进一步提升K-means算法的效果呢?

  一种用于度量聚类效果的指标是SSE,即误差平方和, 为所有簇中的全部数据点到簇中心的误差距离的平方累加和。SSE的值如果越小,表示数据点越接近于它们的簇中心,即质心,聚类效果也越好。因为,对误差取平方后,就会更加重视那些远离中心的数据点。

  显然,我们知道了一种改善聚类效果的做法就是降低SSE,那么如何在保持簇数目不变的情况下提高簇的质量呢?

  一种方法是:我们可以将具有最大SSE值得簇划分为两个簇(因为,SSE最大的簇一般情况下,意味着簇内的数据点距离簇中心较远),具体地,可以将最大簇包含的点过滤出来并在这些点上运行K-means算法,其中k设为2.

  同时,当把最大的簇(上图中的下半部分)分为两个簇之后,为了保证簇的数目是不变的,我们可以再合并两个簇。具体地:

  一方面我们可以合并两个最近的质心所对应的簇,即计算所有质心之间的距离,合并质心距离最近的两个质心所对应的簇。

  另一方面,我们可以合并两个使得SSE增幅最小的簇,显然,合并两个簇之后SSE的值会有所上升,那么为了最好的聚类效果,应该尽可能使总的SSE值小,所以就选择合并两个簇后SSE涨幅最小的簇。具体地,就是计算合并任意两个簇之后的总得SSE,选取合并后最小的SSE对应的两个簇进行合并。这样,就可以满足簇的数目不变。

  上面,是对已经聚类完成的结果进行改善的方法,在不改变k值的情况下,上述方法能够起到一定的作用,会使得聚类效果得到一定的改善。那么,下面要讲到的是一种克服算法收敛于局部最小值问题的K-means算法。即二分k-均值算法。

三,二分K-means算法

  二分K-means算法首先将所有点作为一个簇,然后将簇一分为二。之后选择其中一个簇继续进行划分,选择哪一个簇取决于对其进行划分是否能够最大程度的降低SSE的值。上述划分过程不断重复,直至划分的簇的数目达到用户指定的值为止。

  二分K-means算法的伪代码如下:

复制代码

将所有点看成一个簇
当簇数目小于k时
对于每一个簇
    计算总误差
    在给定的簇上面进行k-均值聚类(k=2)
    计算将该簇一分为二之后的总误差
选择使得总误差最小的簇进行划分

复制代码

  当然,也可以选择SSE最大的簇进行划分,知道簇数目达到用户指定的数目为止。下面看具体的代码:

def biKMeans(dataSet,k,distMeas=distEclud):
    m = dataSet.shape[0]
    centroido = mean(dataSet,axis=0).tolist()[0]
    clusterAssment = mat(zeros((m,2)))
    centList = [centroido]
    for j in range(m):
        clusterAssment[j,1] = distMeas(dataSet[j,:],mat(centroido))**2
    while(len(centList) < k):
        lowestSSE = inf
        for i in range(len(centList)):
            #首先找到属于该类的数据集
            IClassIndex = nonzero(clusterAssment[:,0].A == i)[0]
            IdataSet = dataSet[IClassIndex,:]
            centroidMat,splitClustAss = KMeans(IdataSet,2,distMeas)
            sseSplit = sum(splitClustAss[:,1])
            NotClassIndex = nonzero(clusterAssment[:,0].A != i)[0]
            sseNotSplit = sum(clusterAssment[NotClassIndex,1])
            if(sseNotSplit+sseSplit < lowestSSE):
                bestClassToSplite = i
                bestNewCents = centroidMat
                bestClustAss = splitClustAss.copy() #不知道为什么要用copy
                lowestSSE = sseSplit+sseNotSplit
        #接下来更新簇的分配结果,将新分类的加为两类,类别为1的类改为类的长度,类别为0的类为原被分的类。
        bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList)
        bestClustAss[nonzero(bestClustAss[:, 0].A == 0)[0], 0] = bestClassToSplite
        centList[bestClassToSplite] = bestNewCents[0,:].tolist()[0]
        centList.append(bestNewCents[1,:].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:,0].A == bestClassToSplite)[0],:] = bestClustAss
    return mat(centList),clusterAssment

写代码时对于nonzero(clusterAssment[:,0].A == i)[0]不是很理解,下面链接是分步讲该语句的实现的

https://blog.csdn.net/xinjieyuan/article/details/81477120

然后还有 return mat(centList)时报错

将centList[bestClassToSplite] = bestNewCents[0,:];centList.append(bestNewCents[1,:])改成centList[bestClassToSplite] = bestNewCents[0,:].tolist()[0] centList.append(bestNewCents[1,:].tolist()[0])就可以了。

 

你可能感兴趣的:(机器学习)