前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《机器学习实战》(十)——k-means、k-means++、二分K-means

《机器学习实战》(十)——k-means、k-means++、二分K-means

作者头像
小爷毛毛_卓寿杰
发布2019-02-13 11:47:48
9630
发布2019-02-13 11:47:48
举报
文章被收录于专栏:Soul Joy Hub

k-means

原理

  • 创建K个点作为起始质点。每次迭代如下:
    • 将各个数据点分配到离它距离最近的质点的簇。
    • 全部分配后,用各个簇中的数据点的位置均值来更新质点的位置。
  • 直到达到迭代次数,或者所有的数据点所在的簇不再改变。

可参阅:http://blog.csdn.net/u011239443/article/details/51707802#t0

实现

Python

  • 支持函数
代码语言:javascript
复制
# 加载数据
def loadDataSet(fileName):      
    dataMat = []                
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = map(float,curLine) 
        dataMat.append(fltLine)
    return dataMat
代码语言:javascript
复制
# 计算距离
def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB)

# 随机创建质点
def randCent(dataSet, k):
    n = shape(dataSet)[1]
    centroids = mat(zeros((k,n)))
    for j in range(n):
        minJ = min(dataSet[:,j]) 
        rangeJ = float(max(dataSet[:,j]) - minJ)
        centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
    return centroids
  • 聚类算法
代码语言:javascript
复制
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    m = shape(dataSet)[0]
    # 存储聚类结果 (质点,离该质点的距离)
    clusterAssment = mat(zeros((m,2)))
    # 随机创建k个质点
    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,:])
                if distJI < minDist:
                    minDist = distJI; minIndex = j
            if clusterAssment[i,0] != minIndex: clusterChanged = True
            clusterAssment[i,:] = minIndex,minDist**2
        print centroids
        # 更新质点
        for cent in range(k):
            ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]
            centroids[cent,:] = mean(ptsInClust, axis=0) 
    return centroids, clusterAssment

Scala

代码语言:javascript
复制
package Kmeans

import scala.io.Source
import scala.collection.mutable.ArrayBuffer
import scala.util.Random

object Kmeans {

  def loadDataSet(fileName: String) = {
    var dataMat = ArrayBuffer[Array[Double]]()
    val fr = Source.fromFile(fileName)
    for (line <- fr.getLines()) {
      val fltLine = line.trim().split("\t").map(_.toDouble)
      dataMat.append(fltLine)
    }
    dataMat.toArray
  }

  def distEclud(vecA: Array[Double], vecB: Array[Double]) = {
    vecA.zip(vecB).map(x => math.pow(x._1 - x._2, 2)).sum
  }

  def randCent(dataSet: Array[Array[Double]], k: Int) = {
    val n = dataSet(0).length
    var centroids = Array.fill(k)(Array.fill(n)(0.0))
    for (j <- 0 to n - 1) {
      val minJ = dataSet.map(_(j)).min
      val rangeJ = dataSet.map(_(j) - minJ).max
      for (i <- 0 to k - 1) {
        Random.setSeed(i)
        centroids(i)(j) = minJ + rangeJ * Random.nextDouble()
      }
    }
    centroids
  }

  def MykMeans(dataSet: Array[Array[Double]],
               k: Int,
               distMeas: (Array[Double], Array[Double]) => Double = distEclud,
               createCent: (Array[Array[Double]], Int) => Array[Array[Double]] = randCent) = {
    val m = dataSet.length
    // 存储聚类结果 (质点,离该质点的距离)
    var clusterAssment = Array.fill(m)((0, 0.0))
    // 随机创建k个质点
    var centroids = createCent(dataSet, k)
    // 用于判断所有的数据点所在的簇是否不再改变
    var clusterChanged = true
    while (clusterChanged) {
      clusterChanged = false
      // 对每个数据点
      for (i <- 0 to m - 1) {
        var minDist = Double.MaxValue
        var minIndex = -1
        // 求与之最近的质点
        for (j <- 0 to k - 1) {
          val distJI = distMeas(centroids(j), dataSet(i))
          if (distJI < minDist) {
            minDist = distJI
            minIndex = j
          }
        }
        if (clusterAssment(i)._1 != minIndex)
          clusterChanged = true
        // 笔者认为这里应该的开方 而不不是平方
        clusterAssment(i) = (minIndex, math.sqrt(minDist))
      }
      centroids.foreach(x => println(x.mkString("|")))
      println("——————————————————————————————————————————————")
      // 更新质点
      for (cent <- 0 to k - 1) {
        val centDataSet = dataSet.zip(clusterAssment).filter(_._2._1 == cent)
        val len = centDataSet.length
        // 保证簇中有数据点,才更新质点
        if (len > 0)
          centroids(cent) = centDataSet.map(_._1).reduce((a, b) => Array(a(0) + b(0), a(1) + b(1))).map(_ / len)
      }
    }
    (centroids, clusterAssment)
  }

  def main(args: Array[String]) {
    val dataSet = loadDataSet("testSet.txt")
    MykMeans(dataSet, 4)
  }
}

/*
运行结果:

2.089206937214362 2.6553132269012165
2.0882914517558113 2.6544689587024406
2.0910374513579297 2.657001342058469
2.090122270415068 2.6561573546865596
——————————————————————————————————————————————
2.089206937214362 2.6553132269012165
-0.8102053124999998 -0.777700796875
2.7227551874999993 3.3823091875
2.090122270415068 2.6561573546865596
——————————————————————————————————————————————
1.3357000833333332 1.7167295833333334
-1.3054142499999999 -1.3533385769230764
2.6742446666666666 3.7408964166666667
2.86828675 2.3065474999999998
——————————————————————————————————————————————
-2.002083882352941 3.0127088823529418
-1.1042668249999996 -2.4955051999999998
2.4340693 3.9126998
3.5058729999999994 1.0631734615384618
——————————————————————————————————————————————
-2.46154315 2.7873755500000006
-1.2792010625 -3.04600065625
2.5212765 3.4946472500000003
3.461318 -0.8204791666666668
——————————————————————————————————————————————
-2.46154315 2.7873755500000006
-3.011694681818182 -3.012386727272727
2.54391447368421 3.2129961052631577
3.0981428421052626 -2.4304122631578946
——————————————————————————————————————————————
-2.46154315 2.7873755500000006
-3.38237045 -2.9473363000000004
2.6265298999999995 3.10868015
2.80293085 -2.7315145999999997
——————————————————————————————————————————————

*/

k-means++

k-means++算法选择初始seeds的基本思想就是:初始的聚类中心之间的相互距离要尽可能的远。该算法的描述是如下:

1.从输入的数据点集合中随机选择一个点作为第一个聚类中心 2.对于数据集中的每一个点x,计算它与最近聚类中心(指已选择的聚类中心)的距离D(x) 3.选择一个新的数据点作为新的聚类中心,选择的原则是:D(x)较大的点,被选取作为聚类中心的概率较大 4.重复2和3直到k个聚类中心被选出来 5.利用这k个初始的聚类中心来运行标准的k-means算法

从上面的算法描述上可以看到,算法的关键是第3步,如何将D(x)反映到点被选择的概率上,一种算法如下:

1.先从我们的数据库随机挑个随机点当“种子点” 2.对于每个点,我们都计算其和最近的一个“种子点”的距离D(x)并保存在一个数组里,然后把这些距离加起来得到Sum(D(x))。 3.然后,再取一个随机值,用权重的方式来取计算下一个“种子点”。这个算法的实现是,先取一个能落在Sum(D(x))中的随机值Random,然后用Random -= D(x),直到其<=0,此时的点就是下一个“种子点”。 4.重复2和3直到k个聚类中心被选出来 5.利用这k个初始的聚类中心来运行标准的k-means算法

可以看到算法的第三步选取新中心的方法,这样就能保证距离D(x)较大的点,会被选出来作为聚类中心了。至于为什么原因很简单,如下图 所示:

bisecting k-meas

为了克服K-Means算法收敛于局部最小值的问题,提出了一种二分K-均值(bisecting K-means) 算法的伪代码如下:

代码语言:javascript
复制
将所有的点看成是一个簇
当簇小于数目k时
    对于每一个簇
        计算总误差
        在给定的簇上进行K-均值聚类,k值为2
        计算将该簇划分成两个簇后总误差
    选择使得误差最小的那个簇进行划分

实现

首先我们来讲解下代码中的这句:

代码语言:javascript
复制
ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A == i)[0],:]

clusterAssment 存储的(所属的中心编号,距中心的距离)的列表,clusterAssment[:,0].A就是把clusterAssment各个所属的中心编号抽出来形成数组。clusterAssment[:,0].A == i就形成的布尔数组,如果编号等于i,则为true,反之为false。nonzero(clusterAssment[:,0].A == i),会得到非零或者非false的索引列表,列表中第一个元素是按行的索引,第二个元素是按列的索引。所以,dataSet[nonzero(clusterAssment[:,0].A == i)[0],:]就得到了dataSet中行号与clusterAssment所属的中心编号i的行号对应的子集数据。

接下来,我们来分析全部的代码:

代码语言:javascript
复制
def biKmeans(dataSet,k,distMeas=distEclud):
    m = shape(dataSet)[0]
    # 将所有的点看成是一个簇
    # clusterAssment 存储 (所属的中心编号,距中心的距离)的列表
    clusterAssment = mat(zeros((m,2)))
    # centList 存储聚类中心
    centroid0 = mean(dataSet,axis=0).tolist()[0]
    centList = [centroid0]
    for j in range(m):
        clusterAssment[j,1] = distMeas(mat(centroid0),dataSet[j,:]) ** 2
    # 当簇小于数目k时
    while len(centList) < k:
        lowestSSE = inf
        for i in range(len(centList)):
            # 得到dataSet中行号与clusterAssment中所属的中心编号为i的行号对应的子集数据。
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A == i)[0],:]
            # 在给定的簇上进行K-均值聚类,k值为2
            centroidMat,splitClustAss = kMeans(ptsInCurrCluster,2,distMeas)
           # 计算将该簇划分成两个簇后总误差
            sseSplit = sum(splitClustAss[:,1])
            sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A != i)[0],1])
           # 选择使得误差最小的那个簇进行划分
            if sseSplit + sseNotSplit < lowestSSE:
                bestCentToSplit = i
                bestNewCents = centroidMat.copy()
                bestClustAss = splitClustAss.copy()
                lowestSSE = sseSplit + sseNotSplit

       # 将需要分割的聚类中心下的点进行1划分
       # 新增的聚类中心编号为len(centList)
        bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList)
        bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit
        clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:] = bestClustAss

        # 更新被分割的聚类中心的坐标
        centList[bestCentToSplit] = bestNewCents[0,:]
        # 增加聚类中心
        centList.append(bestNewCents[1,:])

    return centList,clusterAssment

测试

代码语言:javascript
复制
import myKmeans
from numpy import *
dataMat = mat(myKmeans.loadDataSet('testSet2.txt'))
centList,myNewAssments = myKmeans.biKmeans(dataMat,3)

print(centList)

# [matrix([[ 2.93386365,  3.12782785]]), matrix([[-2.94737575,  3.3263781 ]]), matrix([[-0.45965615, -2.7782156 ]])]
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017年03月30日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • k-means
    • 原理
      • 实现
        • Python
        • Scala
    • k-means++
    • bisecting k-meas
      • 实现
        • 测试
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档