MeanShift聚类算法及代码实现

MeanShift

该算法也叫做均值漂移,在目标追踪中应用广泛。本身其实是一种基于密度的聚类算法。
主要思路是:计算某一点A与其周围半径R内的向量距离的平均值M,计算出该点下一步漂移(移动)的方向(A=M+A)。当该点不再移动时,其与周围点形成一个类簇,计算这个类簇与历史类簇的距离,满足小于阈值D即合并为同一个类簇,不满足则自身形成一个类簇。直到所有的数据点选取完毕。

一般形式

对于给定的 n 维空间R^n 中的 m 个样本点X^i,i=1...m,对于其中一个样本X,他的均值漂移向量为:M_h(X)=\frac{1}{K}*\sum_{X^i\in S_h}(X^i-X),其中S_h指的是一个半径为h的球状领域,定义为S_h(X)=\{y|(y-x)(y-x)^T \le h^2\},如下图所示

示例1

蓝色圈内表示半径h的区域
S_h
,黄色箭头尾部指的是计算前的数据点
X
,箭头本身是指的计算后的漂移向量
M_h (X)
。由上图可以看出,均值漂移会不断的往密度较大的区域移动。熟悉的同学可能了解到,一般用的均值漂移都是经过核函数改进的,那为什么要引入核函数呢?
首先,我们再看一下上图和公式:蓝色圈区域内,每一个与
X
相邻的
X^i
在计算过程中对均值漂移向量的贡献都是一样的,不以这个点与X的距离远近而变化。按照我们人类的思想,近朱者赤 近墨者黑,离得中心点越近,受影响/反影响的力度就会越大。比如,都是程序员,但是三线城市程序员和北京程序员在知识广度、能力、成长速度等方面都有较大差距,毕竟北京是互联网行业的中心城市嘛。应用到算法里也是一样的,因此就有人提出邻域内的点需要设置不同的权重来进行漂移计算,故提出了核函数的概念

核函数形式

\Psi是输入空间,是实数空间的一个子集。设H为希尔伯特空间(完备的空间,抽象意义上对有限维欧式空间的扩展),设存在一个映射:\Theta(X):\Psi \to H,此时有函数K(X_1,X_2)=\Theta(X_1)\cdot\Theta(X_2),其中X_1,X_2\in\Psi,K(X_1,X_2)称为核函数,\cdot是内积运算。关于希尔伯特空间和核函数的概念,本人了解的也不深,欢迎探讨。
高斯核函数是一种应用广泛的核函数:K\{\frac{X_1-X_2}{h}\}=\frac{1}{h*\sqrt{2\pi}}*\exp^{-\frac{(X_1-X_2)^2}{2h^2}}
其中h为bandwidth 带宽,不同带宽的核函数形式也不一样

高斯核示例

由上图可以看到,横坐标指的是两变量之间的距离。距离越近(接近于0)则函数值越大,否则越小。h越大,相同距离的情况下 函数值会越小。因此我们可以选取适当的h值,得到满足上述要求的那种权重(两变量距离越近,得到权重越大),故经过核函数改进后的均值漂移为:
M_h(X)=\frac{\sum_{X^i\in S_h}[K\{\frac{X^i-X}{h}\}*(X^i-X)]}{\sum_{X^i\in S_h}[K\{\frac{X^i-X}{h}\}]}

其中
K\{\frac{X^i-X}{h}\}
就是高斯核函数
看到其他的文章说,经过核函数改进后的均值漂移,经过证明(求导),会朝着概率密度上升的区域移动。
上代码及实验结果:

Python代码

class MeanShift(object):
    """
    均值漂移聚类-基于密度
    """
    def __init__(self,radius = 0.5,distance_between_groups = 2.5,bandwidth = 1,use_gk = True):
        self._radius = radius
        self._groups = []
        self._bandwidth = bandwidth
        self._distance_between_groups = distance_between_groups
        self._use_gk = use_gk #是否启用高斯核函数

    def _find_nearst_indexes(self,xi,XX):
        if XX.shape[0] == 0:
            return []
        distances= eculide(xi,XX)
        nearst_indexes = np.where(distances <= self._distance_between_groups)[0].tolist()
        return nearst_indexes

    def _compute_mean_vector(self,xi,datas):
        distances = datas-xi
        if self._use_gk:
            sum1 = self.gaussian_kernel(distances)
            sum2 = sum1*(distances)
            mean_vector = np.sum(sum2,axis=0)/np.sum(sum1,axis=0)
        else:
            mean_vector = np.sum(datas - xi, axis=0) / datas.shape[0]
        return mean_vector

    def fit(self,X):
        XX = X
        while(XX.shape[0]!=0):
            # 1.从原始数据选取一个中心点及其半径周边的点 进行漂移运算
            index = np.random.randint(0,XX.shape[0],1).squeeze()
            group = Group()
            xi = XX[index]
            XX = np.delete(XX,index,axis=0) # 删除XX中的一行并重新赋值
            nearest_indexes = self._find_nearst_indexes(xi, XX)
            nearest_datas = None
            mean_vector = None
            if len(nearest_indexes) != 0:
                nearest_datas = None
                # 2.不断进行漂移,中心点达到稳定值
                epos = 1.0
                while (True):
                    nearest_datas = XX[nearest_indexes]
                    mean_vector = self._compute_mean_vector(xi,nearest_datas)
                    xi = mean_vector + xi
                    nearest_indexes = self._find_nearst_indexes(xi, XX)
                    epos = np.abs(np.sum(mean_vector))
                    if epos < 0.00001 : break
                    if len(nearest_indexes) == 0 : break
                # 有些博客说在一次漂移过程中 每个漂移点周边的点都需要纳入该类簇中,我觉得不妥,此处不是这样实现的,
                # 只把稳定点周边的数据纳入该类簇中
                group.members = nearest_datas.tolist()
                group.center = xi
                XX = np.delete(XX, nearest_indexes, axis=0)
            else:
                group.center = xi
            # 3.与历史类簇进行距离计算,若小于阈值则加入历史类簇,并更新类簇中心及成员
            for i in range(len(self._groups)):
                h_group = self._groups[i]
                distance = eculide(h_group.center,group.center)
                if distance <= self._distance_between_groups:
                    h_group.members = group.members
                    h_group.center = (h_group.center+group.center)/2
                else:
                    group.name = len(self._groups) + 1
                    self._groups.append(group)
                    break
            if len(self._groups) == 0:
                group.name = len(self._groups) + 1
                self._groups.append(group)
            # 4.从余下的点中重复1-3的计算,直到所有数据完成选取

    def plot_example(self):
        figure = plt.figure()
        ax = figure.add_subplot(111)
        ax.set_title("MeanShift Iris Example")
        plt.xlabel("first dim")
        plt.ylabel("third dim")
        legends = []
        cxs = []
        cys = []
        for i in range(len(self._groups)):
            group = self._groups[i]
            members = group.members
            x = [member[0] for member in members]
            y = [member[2] for member in members]
            cx = group.center[0]
            cy = group.center[2]
            cxs.append(cx)
            cys.append(cy)
            ax.scatter(x, y, marker='o')
            #ax.scatter(cx,cy,marker='+',c='r')
            legends.append(group.name)
        plt.scatter(cxs,cys,marker='+',c='k')
        plt.legend(legends, loc="best")
        plt.show()

    def gaussian_kernel(self,distances):
        """
        高斯核函数
        :param distances:
        :param h:
        :return:
        """
        left = 1/(self._bandwidth*np.sqrt(2*np.pi))
        right = np.exp(-np.power(distances,2)/(2*np.power(self._bandwidth,2)))
        return left*right

def test_meanshift(use_gk = False):
    data,t,tn=load_data()
    ms = MeanShift(radius=0.66,distance_between_groups=1.4,use_gk=use_gk)
    ms.fit(data)
    ms.plot_example()

test_meanshift(use_gk = True)

上述定义的Group类及一些import导入包,参见K均值聚类及代码实现
实验结果还是利用了iris数据集,结果如下,第一幅图是一般形式,第二幅图是高斯核函数。黑色“+”代表的是聚类中心

一般形式

高斯核函数

与KMeans相比较而言,meashift可以不用指定类簇的个数,自动发现类簇结构。
但是Kmeans也类似,发现的类簇多为球状类簇,不能发现一些混合度较高,非球状类簇。
下面是经过调参得到的分为3个类图像。此时
MeanShift(radius=1.5,distance_between_groups=2.3,use_gk=use_gk)
此处实现的与sklearn中的MeanShift不同,后续会研究一下sklearn的实现方法。


聚类结果

参考文献

1.简单易学的机器学习算法——Mean Shift聚类算法
2.python机器学习算法-赵志勇
1中的文章也是2作者写的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342