二分K-mean均值算法原理讲解和代码实战

二分K-mean均值算法原理讲解和代码实战

前言

上一篇我们简单给大家介绍了一下K-mean均值的原理和详细代码实践。由于普通的K-mean均值算法很容易陷入局部最优解的情况,这一篇,给大家介绍一个在前面的基础上的优化算法,能够很好的解决陷入局部最优解的情况。

GitHub地址:二分K-mean算法实战

K-mean算法回顾:K-mean均值算法原理讲解和代码实战

二分K-mean均值算法原理解析

K-mean局限性分析

在我们写完上一节的K-mean算法后,我们可以会惊讶于算法实现方式的简单和效果的强大,但是也同样存在这样的问题,咱们看下面一张图:
在这里插入图片描述
图1 该图片来自《机器学习实战》

从这个图中,我们可以看到,图中的数据分为了三类,但是这样的分类明显不是一个好的分类。好的分类应该是上面三组数据各属于一类,比如下图:

在这里插入图片描述
图2 该图片来自《机器学习实战》

是什么原因造成出现第图1的情况呢?

是因为K-mean是采用随机初始化K点的方法,所以极有可能由于初始化点的位置,导致出现图1的情况

目前比较好的解决办法:

  1. 运行多次K-mean算法,取使得损失值最小的K点。(数据量比较小的时候效果比较好)
  2. 二分K-mean均值 -这个方法很好的解决了这个问题,下面重点讲解

二分K-mean均值原理

  1. 首先我们将整个数据集当成一簇,取当前所有数据点的均值作为这个簇的点坐标
  2. 对该簇使用K-mean进行二分类,计算分类后损失值(也就是数据点距所属簇的欧几里得距离)的大小。选取使得误差最小的那个簇进行划分。
  3. 不断循环2,直到当前族的个数等于K值。

伪代码(摘抄《机器学习实战》):

	将所有点看成一个簇

​	当簇数目小于K时

​			对于每一个簇

​					计算总误差

​					在给定的簇上面进行K-mean均值聚类(k=2)

​					计算将该簇一分为二之后的总误差

​			选择使得误差最小的那个簇进行划分操作

二分K-mean均值算法代码实战

一、生成数据

def CreatData():
    x1 = np.random.rand(50)*3#0-3
    y1 = [i+np.random.rand()*2-1 for i in x1]
    with open('data.txt','w') as f:
        for i in range(len(x1)):
            f.write(str(x1[i])+'\t'+str(y1[i])+'\n')

二、读取数据

def loadDateSet(fileName):
    dataMat=[]
    fr = open(fileName)
    for line in fr.readlines():
        curline = line.strip().split('\t')
        #map函数 对指定的序列做映射,第一个参数是function 第二个是序列
        #此方法可以理解为进行字符串格式转换.这个函数可以深究
        fltLine = map(float,curline)
        dataMat.append(list(fltLine))
    return dataMat

这里主要是说一下map这个方法

map(function,list) 此方法的作用是将第二个参数(列表或者迭代器)对前面的方法进行一一映射。

举个例子:

>>>def square(x) :            # 计算平方数
...     return x ** 2
... 
>>> map(square, [1,2,3,4,5])   # 计算列表各个元素的平方
[1, 4, 9, 16, 25]

map(float,curline) 的作用可以理解为将curline 列表中的数字格式转换成float类型的。

三、欧几里得距离计算

def distEclud(vecA,vecB):
    return np.sqrt(np.sum(np.power((vecA-vecB),2)))

四、显示变化数据变化过程

def showProcess(clusterAssment,centroids):
    #显示过程
    Index1 = np.nonzero(clusterAssment[:,0]==0)[0]
    Index2 = []
    spotstyle=['ro','go','yo','bo','po']
    scattercolor=['red','green','yellow','blue','pink']
    for i in range(len(clusterAssment)):
        if i not in Index1:
            Index2.append(i)
    for i in range(len(centroids)):
        Index1 = np.nonzero(clusterAssment[:,0]==i)[0]
        plt.plot(datamat[Index1,0],datamat[Index1,1],spotstyle[i])
        plt.scatter([centroids[i][0]],[centroids[i][1]],color='',marker='o',edgecolors=scattercolor[i],linewidths=3)
    plt.show()

五、初始化K点

def randCent(dataSet,k):
    n = np.shape(dataSet)[1]#获取维度数
    centroids = np.array(np.zeros((n,2)))#创建一个k*n的矩阵,初始值为0
    for j in range(n):
        minJ = np.min(dataSet[:,j])#获取每一维度的最小值
        rangeJ = float(np.max(dataSet[:,j])-minJ)#获得最大间隔,最大值➖最小值
        centroids[:,j] = minJ+rangeJ*np.random.rand(k,1)#最小值加上间隔*[0,1]范围的数
        #每进行一次循环,给每一整列赋值。
    return centroids

六、K-mean算法分类

def kMeans(dataSet,k,distMeans=distEclud,createCent = randCent):
    m = np.shape(dataSet)[0]#获取数据的个数
    clusterAssment = np.array(np.zeros((m,2)))#创建一个m行2列的矩阵用于存储索引值和距离
    centroids = createCent(dataSet,k)#随机选取两个点
    plt.scatter([centroids[0][0]],[centroids[0][1]],color='',marker='o',edgecolors='red',linewidths=3)
    plt.scatter([centroids[1][0]],[centroids[1][1]],color='',marker='o',edgecolors='green',linewidths=3)
    plt.plot(dataSet[:,0],dataSet[:,1],'o',color='yellow')
    plt.show()
    clusterChanged = True#标志符,判定数据点的所属关系有没有发生变化
    flag=1
    while clusterChanged:
        print("当前迭代次数为:{}".format(flag))
        flag+=1
        clusterChanged=False
        for i in range(m):#m为数据量的个数
            minDist = 10000#设置一个最大值
            minIndex = -1#初始化索引
            for j in range(k):#k为划分的种类数 此for循环给数据点分配所属关系
                distJI = distMeans(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):#这个for循环是用来移动分类点的位置,将其移动到所属点的平均值位置
            # print("输出1:",clusterAssment[:,0])
            # print("输出2:",np.nonzero(clusterAssment[:,0]==cent))
            #.A 是将矩阵转化为数组
            ptsInClust = dataSet[np.nonzero(clusterAssment[:,0]==cent)[0]]#取出相同簇的点进行取平均,这里[0]是因为参数的形状为(n,1)
            #np.nonzero 取值不为0的索引值
            centroids[cent,:] = np.mean(ptsInClust,axis=0)#取平均
        showProcess(clusterAssment,centroids)
    return centroids,clusterAssment
    

七、二分K-mean均值算法

def biKmeans(dataSet,k,distMeans=distEclud):
    m = np.shape(dataSet)[0]
    clusterAssment =np.array(np.zeros((m,2)))#初始化一个形状为m*2的数组
    centroid0 = np.mean(dataSet,axis=0).tolist()[0]#这里获取已求平均的x、y值
    # print(np.mean(dataSet,axis=0))输出形状为:[[1.51081424 1.55366839]]
    centList = [centroid0]#这里放置的是划分簇的中心点
    #print("cenlist:",np.shape(centList))
    for j in range(m):#m次循环,计算每个数据与中心点的距离。这里多了个平方,目的是增加对距离远的数据的敏感度
        clusterAssment[j,1] = distMeans(np.mat(centroid0),dataSet[j,:])**2
    while(len(centList)<k):#结束循环条件,满足条件的中心点数等于K值
        lowestSSE = 100000#初始化一个最大值
        for i in range(len(centList)):#循环每一个中心点
            #这里是取出属于第i类的点数据
            ptsInCurrCluster = dataSet[np.nonzero(clusterAssment[:,0]==i)[0],:]
            #通过k-mean  将当前类的数据集进行2分类
            centroidMat,splitClustAss = kMeans(ptsInCurrCluster,2,distMeans)
            #计算分类后所有数据的距离值(误差值)
            sseSplit = np.sum(splitClustAss[:,1])
            #计算除了该类剩余数据的误差值
            sseNotSplit = np.sum(clusterAssment[np.nonzero(clusterAssment[:,0]!=i)[0],1])
            print("sseSplit,and notSplit:",sseSplit,sseNotSplit)
            #进行比较,若分类之后损失值小于lowestSSE,则可分
            if(sseSplit+sseNotSplit)<lowestSSE:
                bestCentToSplit = i
                bestNewCents = centroidMat#经过k-mean计算出的两个簇点
                bestClustAss = splitClustAss.copy()#拷贝一份经过k-mean计算的数据所属关系
                lowestSSE = sseSplit+sseNotSplit#总的损失值大小
        #对当前已经经过k-mean计算的数据进行重新分类
        #将类型==1的分给最大长度,因为是从0开始计算的,所以不用+1.
        bestClustAss[np.nonzero(bestClustAss[:,0]==1)[0],0]=len(centList)
        #等于0的还是分配原来的。
        bestClustAss[np.nonzero(bestClustAss[:,0]==0)[0],0]=bestCentToSplit
        #此时bestClustAss 数组中存储的是经过2分之后的数据标签归属和误差值
        print("the bestCentTopSplit is:",bestCentToSplit)
        print("the len of bestClustAss is :",len(bestClustAss))
        #簇点进行替换
        centList[bestCentToSplit] = bestNewCents[0,:]
        print("step1:",np.shape(centList))
        #产生的新的簇点加在列表后面
        centList.append(bestNewCents[1,:])
        print("step2:",np.shape(centList))
        #直接将上面分好的数据标签归属进行替换
        #print("ttest---:",clusterAssment[[1,2,3,4,5],:])允许这样的数据访问形式。
        clusterAssment[np.nonzero(clusterAssment[:,0]==bestCentToSplit)[0],:] = bestClustAss
    return centList,clusterAssment

总结

二分K-mean算法很好的解决了K-mean算法的缺陷,每次都能够很好的进行数据的分类。在这里博主多说一句话,我们学习算法,一定要注重算法思路,多去思考为什么这样为什么不这样。而且多去注意算法的优缺点,这对于以后我们思考别的问题很有帮助,甚至这些思想稍加修改就能解决一些棘手的问题。

原创文章 94 获赞 96 访问量 27万+

猜你喜欢

转载自blog.csdn.net/lzx159951/article/details/105764762
今日推荐