数据挖掘/机器学习算法--直接聚类算法(k-means)

来源:互联网 发布:js 获取signature 编辑:程序博客网 时间:2024/06/18 00:35

       大家好!我是钱大鑫!本篇博客我主要跟大家探讨一下经典聚类算法——k-means。还是那句话,学习算法不是一件可以瞬间学会的事情,大家慢慢读,慢慢领悟,内容绝大部分为会设计到一些数学知识,算法嘛,正常的!读完记得自己实践一下,好了,废话不多说,我们直接来看k-means算法。

        kmeans 算法是实践中使用最广泛的聚类算法。该算法具有简单、易懂、良好的可伸缩性等显著优点,而且对其稍加改动就能应用于半监督学习或者流数据处理等多种不同应用场景。长期以来,很多人一直在坚持改进和扩展 kmeans 算法,使得该算法充满了活力。

1. 算法基本介绍

       首先我们来理解一下什么是 kmeans 算法。 kmeans 算法是一种直接聚类算法,使用很广泛。给定一个对象(或记录)的集合,所谓聚类(或拆分)就是把这些对象划分为多个组或者“聚簇”,从而使得同组内对象间比较相似而不同组对象间差异比较大。换种说法就是,聚类就是要将相似的对象放入同一聚簇,而将不同的对象分到不同的聚簇中。在这里有一点是值得提一下的,在回归、分类等有监督学习算法中需要定义类别标签或者目标值,但聚类过程的输入对象没有与之关联的目标信息(即没有与之关联的类别标签或者目标值)。因此,我们把聚类算法通常叫做无监督学习算法的一类集合。而恰恰因为无监督学习算法不需要带标签数据,所以适用于许多难于获取带标签数据的应用。而在进行有监督学习任务之前,经常也需要利用聚类等无标签学习方法来探查数据集并且挖掘其特性。另外一点,由于聚类不使用类别标签,所以相似性的概念要基于对象的属性进行定义,也就是说,如何判断数据集中的不同数据属于同一聚簇,得看具体的应用。不同的聚类算法适用的数据集类型和挖掘目的都不一样。

        kmeans 算法是一种简单的迭代型聚类算法,它将一个给定的数据集分为用户指定的 k 个聚簇。实现和运行该算法都很简单 ,它的运行速度也比较快,同时易于修改,所以在实际中使用非常广泛。它可以说是数据挖掘领域中最为重要的算法之一。

       下面,我们就来具体的探讨一下 kmeans 算法的细节以及其局限性。当然我所提到的k-means算法内容不一定完全正确,还请同学们多多讨论,多多思考。

2. 算法描述及伪代码

        kmeans 算法的输入对象是 d 维向量空间中的一些点。因此,他是对一个 d 维向量空间的点集 D={xi|i=1,...,N} 进行聚类,其中 xiRd 表示第 i 个对象(或称“数据点”)。 kmeans 聚类算法会将集合 D 划分为 k 个聚簇。也就是说, kmeans 算法对 D 中所有的数据点进行聚类处理。将每个点 xi 都归于且仅对于 k 个聚簇中的一个。我们可以为每一个点分配一个聚簇标识来记录该点将要被分配到哪一个聚簇中去。拥有相同聚簇标识的点属于同一个聚簇,拥有不同聚簇标识的点属于不同的聚簇。我们可以定义一个长度为 N 的聚簇成员变量 m ,其分量 mi 代表点 xi 的聚簇标识。

        k 值是基本 kmeans 算法的一个关键性的输入。确定 k 值的典型做法是依据某些先验知识,例如集合 D 中实际存在的或当前应用所预期的聚簇数量,当然也可以通过测试不同的 k 值进行探查聚簇的典型信息,从而最终选取合适的 k 值。关于这个 k 值的选取我们一会儿再说。其实, k 值的选取对于理解 kmeans 算法如何对数据进行划分没啥关系。

        kmeans 算法中,每个聚簇都用 Rd 中的一个点来代表。可以将这些聚簇用集合 C={cj|j=1,...,k} 来表示。这 k 个聚簇代表有时也被称为聚簇均值或者聚类中心,下面我们会说说 kmeans 算法的目标函数,理解了目标函数之后大家就明白为啥有这样的名称了。

       聚类算法通常是基于“紧密度”或者“相似度”等概念来对点集进行分组的。具体到 kmeans 算法,默认的紧密度度量标准是欧几里得距离。 kmeans 算法实质上是要最小化一个非负代价函数:

Cost=i=1N(arg minj || xicj ||22)

       专业一点说,就是 kmeans 算法要最小化的目标是:每个点 xi 和离它最近的聚簇代表 cj 之间的欧几里得距离的平方和。上面提到的这个非负代价函数就是 kmeans 的目标函数。

       讲了这么多了,我们来具体看一看 kmeans 算法的伪代码:

k-means算法输入:数据集D,聚簇数k输出:聚簇代表集合C,聚簇成员向量m/*初始化聚簇代表C*/从数据集D中随机挑选k个数据点使用这k个数据点构成初识聚簇代表集合Crepeat    /*再分数据*/    将D中的每个数据点重新分配至与之最近的聚簇均值    更新m(mi表示D中第i个点的聚簇标识)    /*重定均值*/    更新C(cj表示第j个聚簇均值)until 目标函数收敛

       以上就是 kmeans 算法的具体中文伪代码,伪代码中将会描述k-means算法通过迭代的方式对点集进行聚类,该迭代过程主要包括了交替执行的两个步骤:(1)重新确定点集 D 中每个数据点的聚簇标识;(2)基于每个聚簇内的所有数据点算出新的聚簇代表。完整的算法流程包括:首先,用 Rd 中的 k 个数据点作为初始的聚簇代表。挑选这些初始种子的方法可以是从数据集中随机抽取样本,或者在数据集的某个子集上聚类得到聚簇代表(上面伪代码中就是选择了随机挑选样本的方式确定初始聚簇代表)。然后该算法依次执行以下两个迭代步骤直至算法收敛。

       步骤1:再分数据。将每个数据点分配到当前与之最近的那个聚簇中心,同时打破了上次迭代确定的归属关系。
       步骤2:重定均值。重新确定每一个聚簇代表,即计算所有分配给该聚簇代表的数据中心(如算数平均值)。

       算法收敛有两种表现形式:1. 目标函数值不断缩小至一定值并且不再发生明显变化;2. 聚簇代表集合 C={cj|j=1,...,k} 不再发生变化。其实数学功底强悍的同学可以证明一下有限步的迭代之后该算法时候必定收敛,能力有限,我证不出来。。。。

       到这里,我们可以考虑一下它的算法复杂度。注意到每一次迭代都需要 Nk 次比较,这也就决定了每次迭代的时间复杂度。 kmeans 算法收敛所需的迭代次数可能依赖于 N ,直觉上应该与数据集的大小成线性关系,当然根据不同具体条件会有所变化。同时,由于迭代中的比较操作次数关于 d 也是线性相关的,所以 kmeans 算法的复杂度关于数据维度也是线性相关的。

       当然,这样的 kmeans 算法也是有一定局限性的。 kmeans 算法本身是一种面向非凸代价函数优化的贪婪下降求解算法,也就是说,上面的那个目标函数本身是一个非凸函数,而我们要做的事儿其实就是不断的将该函数值进行优化,已达到最小的目的,所以, kmeans 算法仅能获得局部最优解(这里有点讲不清,大家可能要理解一下非凸函数和贪婪下降的概念)。此外,大家不知道有没有发现, kmeans 算法对初始聚簇中心的位置比较敏感,也就是说,即便是同一种数据集,如果聚簇代表集合 C 的初始化值不同,最终获得的聚簇之间差异也会比较大。一个糟糕的初始化聚簇代表集合会导致一个糟糕的最终聚簇集合。后面讲到 kmeans 算法实例的时候我们再具体讨论。为了解决局部最优的问题,我们可以基于不同的初始聚簇中心多次运行该算法,从中挑选一个最好的结果。

       前面提到, k 值是基本 kmeans 算法的一个关键性的输入,然而对于 kmeans 算法而言, k 值的选择也是比较困难的。如果我们事先可以取得一些关于数据集的先验知识,比如知道数据集可以分为多少个部分,那么自然就可以将 k 指定为这个数量值。否则我们必须使用其他准则来选择 k ,这就是模型选择的问题了。为此很多人都能想到一个最简单的办法来解决 k 的问题,像上面提到的一样,尝试多个不同的 k 值,然后选择 kmeans 目标函数最小化这个 k 值(也就是对目标函数进行优化),待多个不同 k 值的目标函数都优化成最小值后,比较所有的目标函数最小值,选取目标函数最小这种情况的那个 k 就可以了。但其实,这个方法行不通,目标函数的值对 k 值选择的问题并不适用,也就是说,目标函数优化到最小,也不一定保证这种情况下的 k 值是合适的。举个例子:我们说目标函数最优情况的表现形式是什么?不就是聚类效果最好么。但其实当 k 值越大,即聚簇数目越大时,聚类效果会越精确。大家想想是不是,如果 k 的值等于点集中点的个数,也就是点集中每个点都归为一类,这样是最精确的聚类效果了,这个时候目标函数就成了最优值,且值为0,但这样就没有意义了。这就意味着不能使用目标函数并不能得到以下目标:

       1. 直接比较具有不同聚簇数量的聚类效果;
       2. 寻找出最优的 k 值。

       所以,如果实现无法知道理想的 k 值,人们都会在尝试不同的 k 值后,再用其他方法判断其聚类效果。至于一般会用一些什么方法,我这里大致介绍几种,但这些方法都涉及到一些理论较深的知识,所以我就不具体展开了。比如说,SAS使用了立方聚类的准则,其实就是在原来 kmeans 算法的代价函数上增加一个复杂控制项(这个复杂控制项的值会随着 k 值的增大而增大,这样的话,就可以避免上面的缺陷)。这种方法在一定程度上降低和减轻了 kmeans 算法事先需要确定 k 值的要求。这些我会给出一些参考文献,大家自己去研究一下,我这里就不讲了。

       除了上面提到的这些局限,还有一个 kmeans 算法的经典局限。那就是噪声点的问题。k-means算法对噪声点非常敏感,也就是说多个噪声点会对聚类的效果产生很大的影响,归根结底,这实际上是因为常用的 kmeans 手段都是求均值,而均值从本质上讲并不是一种稳健性的统计量。在用 kmeans 聚类前,通过预处理移除噪声点往往是非常有用的,同样,在聚类后,对聚类结果进行一些后处理对于聚类的结果也有很大的好处。如,删除过小的聚簇,或将彼此接近的一些聚簇合并成一个较大的聚簇。

       讲了这么多理论,下面我们讲个实例。

3.  kmeans 算法实现

Java实现 kmeans 算法

1. 数据获取类

package kmeans;/** 1.  数据获取类 2. @param <b>data</b> <i>in double[length][dim]</i><br/>length个instance的坐标,第i(0~length-1)个instance为data[i] 3. @param <b>length</b> <i>in</i> instance个数 4. @param <b>dim</b> <i>in</i> instance维数 5. @param <b>label</b> <i>out int[length]</i><br/>聚类后,instance所属的聚类标号(0~k-1) 6. @param <b>centers</b> <i>in out double[k][dim]</i><br/>k个聚类中心点的坐标,第i(0~k-1)个中心点为centers[i] 7.  */public class kmeans_data {    public double[][] data;    public int length;    public int dim;    public int[] labels;    public double[][] centers;    public int[] centerCounts;    public kmeans_data(double[][] data, int length, int dim) {        this.data = data;        this.length = length;        this.dim = dim;    }}

2. 参数初始化类

package kmeans;/** 1.  2. 参数初始化类 3.  */public class kmeans_param {    public static final int CENTER_ORDER = 0;    public static final int CENTER_RANDOM = 1;    public static final int MAX_ATTEMPTS = 4000;    public static final double MIN_CRITERIA = 1.0;    public double criteria = MIN_CRITERIA; //阈值    public int attempts = MAX_ATTEMPTS; //尝试次数    public int initCenterMehtod = CENTER_ORDER; //初始化聚类中心点方式    public boolean isDisplay = true; //是否直接显示结果}

3. 结果类

package kmeans;/** *  * 结果类 *  */public class kmeans_result {    public int attempts; // 退出迭代时的尝试次数    public double criteriaBreakCondition; // 退出迭代时的最大距离(小于阈值)    public int k; // 聚类数}

4. KMeans计算类

package kmeans;import java.util.Arrays;import java.util.Collections;import java.util.LinkedList;import java.util.List;import java.util.Random;/** 1.  2. KMeans计算类 3.  */public class kmeans {    /**     * double[][] 元素全置0     *      * @param matrix     *            double[][]     * @param highDim     *            int     * @param lowDim     *            int     *            double[highDim][lowDim]     */    private static void setDouble2Zero(double[][] matrix, int highDim, int lowDim) {        for (int i = 0; i < highDim; i++) {            for (int j = 0; j < lowDim; j++) {                matrix[i][j] = 0;            }        }    }    /**     * 拷贝源二维矩阵元素到目标二维矩阵。 foreach (dests[highDim][lowDim] = sources[highDim][lowDim]);     *      * @param dests     *            double[][]     * @param sources     *            double[][]     * @param highDim     *            int     * @param lowDim     *            int     */    private static void copyCenters(double[][] dests, double[][] sources, int highDim, int lowDim) {        for (int i = 0; i < highDim; i++) {            for (int j = 0; j < lowDim; j++) {                dests[i][j] = sources[i][j];            }        }    }    /**     * 更新聚类中心坐标     *      * @param k     *            int 分类个数     * @param data     *            kmeans_data     */    private static void updateCenters(int k, kmeans_data data) {        double[][] centers = data.centers;        setDouble2Zero(centers, k, data.dim);        int[] labels = data.labels;        int[] centerCounts = data.centerCounts;        for (int i = 0; i < data.dim; i++) {            for (int j = 0; j < data.length; j++) {                centers[labels[j]][i] += data.data[j][i];            }        }        for (int i = 0; i < k; i++) {            for (int j = 0; j < data.dim; j++) {                centers[i][j] = centers[i][j] / centerCounts[i];            }        }    }    /**     * 计算两点的欧几里得距离     *      * @param pa     *            double[]     * @param pb     *            double[]     * @param dim     *            int 维数     * @return double 距离     */    public static double dist(double[] pa, double[] pb, int dim) {        double rv = 0;        for (int i = 0; i < dim; i++) {            double temp = pa[i] - pb[i];            temp = temp * temp;            rv += temp;        }        return Math.sqrt(rv);    }    /**     * 做Kmeans运算     *      * @param k     *            int 聚类个数     * @param data     *            kmeans_data kmeans数据类     * @param param     *            kmeans_param kmeans参数类     * @return kmeans_result kmeans运行信息类     */    public static kmeans_result doKmeans(int k, kmeans_data data, kmeans_param param) {        // 预处理        double[][] centers = new double[k][data.dim]; // 聚类中心点集        data.centers = centers;        int[] centerCounts = new int[k]; // 各聚类的包含点个数        data.centerCounts = centerCounts;        Arrays.fill(centerCounts, 0);        int[] labels = new int[data.length]; // 各个点所属聚类标号        data.labels = labels;        double[][] oldCenters = new double[k][data.dim]; // 临时缓存旧的聚类中心坐标        // 初始化聚类中心(随机或者依序选择data内的k个不重复点)        if (param.initCenterMehtod == kmeans_param.CENTER_RANDOM) { // 随机选取k个初始聚类中心            Random rn = new Random();            List<Integer> seeds = new LinkedList<Integer>();            while (seeds.size() < k) {                int randomInt = rn.nextInt(data.length);                if (!seeds.contains(randomInt)) {                    seeds.add(randomInt);                }            }            Collections.sort(seeds);            for (int i = 0; i < k; i++) {                int m = seeds.remove(0);                for (int j = 0; j < data.dim; j++) {                    centers[i][j] = data.data[m][j];                }            }        } else { // 选取前k个点位初始聚类中心            for (int i = 0; i < k; i++) {                for (int j = 0; j < data.dim; j++) {                    centers[i][j] = data.data[i][j];                }            }        }        // 第一轮迭代        for (int i = 0; i < data.length; i++) {            double minDist = dist(data.data[i], centers[0], data.dim);            int label = 0;            for (int j = 1; j < k; j++) {                double tempDist = dist(data.data[i], centers[j], data.dim);                if (tempDist < minDist) {                    minDist = tempDist;                    label = j;                }            }            labels[i] = label;            centerCounts[label]++;        }        updateCenters(k, data);        copyCenters(oldCenters, centers, k, data.dim);        // 迭代预处理        int maxAttempts = param.attempts > 0 ? param.attempts : kmeans_param.MAX_ATTEMPTS;        int attempts = 1;        double criteria = param.criteria > 0 ? param.criteria : kmeans_param.MIN_CRITERIA;        double criteriaBreakCondition = 0;        boolean[] flags = new boolean[k]; // 标记哪些中心被修改过        // 迭代        iterate: while (attempts < maxAttempts) { // 迭代次数不超过最大值,最大中心改变量不超过阈值            for (int i = 0; i < k; i++) { // 初始化中心点“是否被修改过”标记                flags[i] = false;            }            for (int i = 0; i < data.length; i++) { // 遍历data内所有点                double minDist = dist(data.data[i], centers[0], data.dim);                int label = 0;                for (int j = 1; j < k; j++) {                    double tempDist = dist(data.data[i], centers[j], data.dim);                    if (tempDist < minDist) {                        minDist = tempDist;                        label = j;                    }                }                if (label != labels[i]) { // 如果当前点被聚类到新的类别则做更新                    int oldLabel = labels[i];                    labels[i] = label;                    centerCounts[oldLabel]--;                    centerCounts[label]++;                    flags[oldLabel] = true;                    flags[label] = true;                }            }            updateCenters(k, data);            attempts++;            // 计算被修改过的中心点最大修改量是否超过阈值            double maxDist = 0;            for (int i = 0; i < k; i++) {                if (flags[i]) {                    double tempDist = dist(centers[i], oldCenters[i], data.dim);                    if (maxDist < tempDist) {                        maxDist = tempDist;                    }                    for (int j = 0; j < data.dim; j++) { // 更新oldCenter                        oldCenters[i][j] = centers[i][j];                    }                }            }            if (maxDist < criteria) {                criteriaBreakCondition = maxDist;                break iterate;            }        }        // 输出信息        kmeans_result rvInfo = new kmeans_result();        rvInfo.attempts = attempts;        rvInfo.criteriaBreakCondition = criteriaBreakCondition;        if (param.isDisplay) {            System.out.println("k=" + k);            System.out.println("attempts=" + attempts);            System.out.println("criteriaBreakCondition=" + criteriaBreakCondition);            System.out.println("The number of each classes are: ");            for (int i = 0; i < k; i++) {                System.out.print(centerCounts[i] + " ");            }            System.out.print("\n\n");        }        return rvInfo;    }}

5. KMeans主运行类

package com.dataMiningAlgorithms.kmeans;import kmeans.kmeans;import kmeans.kmeans_data;import kmeans.kmeans_param;/** 1.  2. KMeans主运行类 3.  */public class KMeans {    public static void main(String[] args) {        //测试数据,四个二维的点        double[][] points = {{0, 0}, {4, 10}, {1, 1}, {5, 8}};         //初始化数据结构        kmeans_data data = new kmeans_data(points, 4, 2);         //初始化参数结构        kmeans_param param = new kmeans_param();         //设置聚类中心点的初始化模式为随机模式        param.initCenterMehtod = kmeans_param.CENTER_RANDOM;         //做kmeans计算,设置k为2        kmeans.doKmeans(2, data, param);        //查看每个点的所属聚类标号        System.out.print("The labels of points is: ");        for (int lable : data.labels) {            System.out.print(lable + "  ");        }    }}

6. 程序输出结果

k=2attempts=3criteriaBreakCondition=0.0The number of each classes are: 2 2 The labels of points is: 0  1  0  1 

       以上是Java对二维数据点简单 kmeans 处理。我们也可以使用MatLab实现 kmeans 算法,并画出图像,直观地展现效果。

MatLab实现 kmeans 算法
       在MatLab实现 kmeans 算法的过程中,我们利用高斯分布产生了三维数据,测试 kmeans 算法对多维数据的聚类效果。

1. MatLab主运行脚本

clear all;close all;clc;%第一类数据%均值mu1=[0 0 0];  %协方差S1=[0.3 0 0;0 0.35 0;0 0 0.3];  %产生高斯分布数据data1=mvnrnd(mu1,S1,100);   %%第二类数据mu2=[1.25 1.25 1.25];S2=[0.3 0 0;0 0.35 0;0 0 0.3];data2=mvnrnd(mu2,S2,100);%第三个类数据mu3=[-1.25 1.25 -1.25];S3=[0.3 0 0;0 0.35 0;0 0 0.3];data3=mvnrnd(mu3,S3,100);%显示数据plot3(data1(:,1),data1(:,2),data1(:,3),'+');hold on;plot3(data2(:,1),data2(:,2),data2(:,3),'r+');plot3(data3(:,1),data3(:,2),data3(:,3),'g+');grid on;%三类数据合成一个不带标号的数据类%这里的data是不带标号的data=[data1;data2;data3];%k-means聚类%最后产生带标号的数据,标号在所有数据的最后,意思就是数据再加一维度[u re]=KMeans(data,3);[m n]=size(re);%最后显示聚类后的数据figure;hold on;for i=1:m     if re(i,4)==1            plot3(re(i,1),re(i,2),re(i,3),'ro');     elseif re(i,4)==2         plot3(re(i,1),re(i,2),re(i,3),'go');     else          plot3(re(i,1),re(i,2),re(i,3),'bo');     endendgrid on;

2. Kmeans脚本

%N是数据一共分多少类%data是输入的不带分类标号的数据%u是每一类的中心%re是返回的带分类标号的数据function [u re]=KMeans(data,N)       [m n]=size(data);   %m是数据个数,n是数据维数    ma=zeros(n);        %每一维最大的数    mi=zeros(n);        %每一维最小的数    u=zeros(N,n);       %随机初始化,最终迭代到每一类的中心位置    for i=1:n       ma(i)=max(data(:,i));    %每一维最大的数       mi(i)=min(data(:,i));    %每一维最小的数       for j=1:N            u(j,i)=ma(i)+(mi(i)-ma(i))*rand();  %随机初始化,不过还是在每一维[min max]中初始化好些       end          end    while 1        pre_u=u;            %上一次求得的中心位置        for i=1:N            tmp{i}=[];      % 公式一中的x(i)-uj,为公式一实现做准备            for j=1:m                tmp{i}=[tmp{i};data(j,:)-u(i,:)];            end        end        quan=zeros(m,N);        %目标函数的实现        for i=1:m                    c=[];            for j=1:N                c=[c norm(tmp{j}(i,:))];            end            [junk index]=min(c);            quan(i,index)=norm(tmp{index}(i,:));                   end        %距离公式的实现        for i=1:N                       for j=1:n                u(i,j)=sum(quan(:,i).*data(:,j))/sum(quan(:,i));           end                   end        if norm(pre_u-u)<0.1  %不断迭代直到位置不再变化            break;        end    end    re=[];    for i=1:m        tmp=[];        for j=1:N            tmp=[tmp norm(data(i,:)-u(j,:))];        end        [junk index]=min(tmp);        re=[re;data(i,:) index];    endend

3. 实验结果
       初始数据在三维空间中的状态显示:

初始数据.jpg

        kmeans 聚类后将结果映射到二维坐标轴上的结果显示(同种颜色的点表示聚类到同一类):
结果显示.jpg

4. 算法扩展

       我们顺便也讲讲 kmeans 算法的扩展。主要扩展什么呢?还不就是 kmeans 算法的推广、与其他算法的关联这些东西,以下的言语基本都是我在一些中英文论文文献中看来的,很多东西我都没有实现过,大致聊聊,大家看看就好。

       一般的数据分析岗位都涉及到大数据集的分析建模,因此让 kmeans 算法适应大数据集的规模还是相当有必要的。为了有效处理超大数据集,必须对 kmeans 算法加速。很多学者在这方面做出了突出的成就,例如通过使用 kd 或利用三角不等式,避免在“数据再分”过程中计算所有 <> 对的距离。

       我们还是主要说说 kmeans 算法的扩展吧,主要说两个:1. 柔性 kmeans ;2.  kmeans 与半监督学习相关。

        柔性 kmeans 
       在标准 kmeans 算法中,每个点 xi 属于且仅属于一个聚簇,但柔性 kmeans 算法放松了这个约束,在柔性 kmeans 算法中,每个点 xi 依概率赋给一个聚簇。柔性 kmeans 算法中,每个点 xi 都有一个 k 维概率(或权重)向量用来描述该点属于每个聚簇的可能性。这些权重是以点 xi  C 中每个聚簇代表的距离为基础的, xi 来自聚簇 j 的概率正比于 xi  cj 之间的相似性。这种情况下,聚簇代表是基于数据集 D 的所有点(而不是一个聚簇的点)关于聚簇均值的期望的计算来得到。

         kmeans 与半监督学习相关扩展
        学习算法一般情况下大致分为两种:有监督学习和无监督学习。简单地说,有监督学习需要使用类别标签,而无监督学习不用类别标签, kmeans 算法就是一种无监督学习算法。除了这两种学习算法外,其实还有一种学习算法——半监督学习算法,它能够同时使用已标记和未标记的数据。可见,半监督学习结合了监督学习和无监督学习的优势。有监督学习通常要求有大量的有标记数据,而当有标记数据很少的情况下,半监督学习就可以发挥用处了。无监督学习案发虽然不用类别标签,但是由它学习而来的模型通常不符合我们面对的应用。在执行 kmeans 算法过程中,我们实际上对最终聚簇并没有相应的控制机制,所以这些聚簇就可能不对应我们期望挖掘出的隐含概念。而半监督学习方法借助有标记数据的指导,就更有可能挖掘出同类别标签对应良好的聚簇。
       对半监督 kmeans 算法的研究现在已经有不少了,一会儿给出两篇文献,其中有一篇文献中提出的算法称为 seededkmeans 算法,它比较简单,使用已标记的数据帮助初始化 k 值和聚簇代表 C 。在这种方法中, k 值设定为已标记数据中的类别数,聚簇代表 Cj 根据第 j 个类别之间的对应关系是已知的。除了初始化方法的不同外, seededkmeans 算法和标准的 kmeans 算法是一样的,也是要执行那两个步骤(再分数据和重定均值)的迭代直至收敛。

5. 总结

       最后总结一下吧, kmeans 算法使用简单 迭代将数据集聚成 k 个类,迭代的核心步骤有两个:(1)数据再分;(2)重定均值。 kmeans 算法的局限主要是算法对初始化条件、 k 值的选取以及噪声点都很敏感。

6. 参考文献

[1] P. S. Bradley, K. P. Bennett, and A. Demiriz. "Constrained k-means clustering", Technical Report MSR-TR-2000-65, 2000.[2] E. Forgey. "Cluster analysis of multivariate data: Efficiency vs. interpretability of classification", Biometrics, 21, pp. 758, 1965.[3] H. P. Friedman and J. Rubin. "Onsome invariant criteria for grouping data", Journal of American Statistical Association, 62, pp. 1159-1178, 1967.[4] T. Kanungo, et al. "A local search approximation algorithm for k-means clustering", Computational Geometry: Theory and Application, 28(2004), pp. 89-112, 2004.
0 0