Kmeans是最经典的聚类算法之一,它的优美简单、快速高效被广泛使用。
Kmeans算法描述
输入:簇的数目k;包含n个对象的数据集D。
输出:k个簇的集合。
方法:
- 从D中任意选择k个对象作为初始簇中心;
- repeat;
- 根据簇中对象的均值,将每个对象指派到最相似的簇;
- 更新簇均值,即计算每个簇中对象的均值;
- 计算准则函数;
- until准则函数不在发生变化。
Kmeans 算法的优缺点:
针对算法存在的问题,对K-means算法提出一些改进:一是数据预处理,二是初始聚类中心选择,三是迭代过程中聚类种子的选择。
首先对样本数据进行正规化处理,这样就能防止某些大值属性的数据左右样本间的距离。给定一组含有n个数据的数据集,每个数据含有m个属性,分别计算每一个属性的均值、标准差对每条数据进行标准化。
其次,初始聚类中心的选择对最后的聚类效果有很大的影响,原K-means算法是随机选取k个数据作为聚类中心,而聚类的结果要是同类间尽可能相似,不同 类间尽可能相异,所以初始聚类中心的选取要尽可能做到这一点。采用基于距离和的孤立点定义来进行孤立点的预先筛选,并利用两两数据之间的最大距离在剩余数 据集合中寻找初始聚类中心。但对于实际数据,孤立点个数往往不可预知。在选择初始聚类中心时,先将孤立点纳入统计范围,在样本中计算对象两两之间的距离, 选出距离最大的两个点作为两个不同类的聚类中心,接着从其余的样本对象中找出已经选出来的所有聚类中心的距离和最大的点为另一个聚类中心,直到选出k个聚 类中心。这样做就降低了样本输入顺序对初始聚类中心选择的影响。
聚类中心选好以后,就要进行不断的迭代计算,在K-means算法中,是将聚类均值点(类中所有数据的几何中心点)作为新的聚类种子进行新一轮的聚类计 算,在这种情况下,新的聚类种子可能偏离真正的数据密集区,从而导致偏差,特别是在有孤立点存在的情况下,有很大的局限性。在选择初始中心点时,由于将孤 立点计算在内,所以在迭代过程中要避免孤立点的影响。这里根据聚类种子的计算时,采用簇中那些与第k-1轮聚类种子相似度较大的数据,计算他们的均值点作 为第k轮聚类的种子,相当于将孤立点排除在外,孤立点不参与聚类中心的计算,这样聚类中心就不会因为孤立点的原因而明显偏离数据集中的地方。在计算聚类中 心的时候,要运用一定的算法将孤立点排除在计算均值点那些数据之外,这里主要采用类中与聚类种子相似度大于某一阈值的数据组成每个类的一个子集,计算子集 中的均值点作为下一轮聚类的聚类种子。为了能让更多的数据参与到聚类中心的计算种去,阈值范围要包含大多数的数据。在第k-1轮聚类获得的类,计算该类中 所有数据与该类聚类中心的平均距离S,选择类中与聚类种子相似度大于2S的数据组成每个类的一个子集,以此子集的均值点作为第k轮聚类的聚类种子。在数据 集中无论是否有明显的孤立点存在,两倍的平均距离都能包含大多数的数据。
对孤立点的改进:
经典k均值算法中没有考虑孤立点。所谓孤立点都是基于距离的, 是数据U集中到U中最近邻居的距离最大的对象, 换言之, 数据集中与其最近邻居的平均距离最大的对象。针对经典k均值算法易受孤立点的影响这一问题, 基于距离法移除孤立点, 具体过程如下:
首先扫描一次数据集, 计算每一个数据对象与其临近对象的距离, 累加求其距离和, 并计算出距离和均值。如果某个数据对象的距离和大于距离和均值, 则视该点为孤立点。把这个对象从数据集中移除到孤立点集合中, 重复直到所有孤立点都找到。最后得到新的数据集就是聚类的初始集合。
对初始点的选择和K值选择的改进:
经典的kmeans 初值选择K值是很难确定的。由于kmeans是局部最优,所以对于初始中心选择很敏感,一方面影响聚类的速度,另一方面影响聚类的质量。一种思路是和其他 的聚类算法联合使用,比如Canopy ,谱聚类等。将Canopy和谱聚类执行的结果作为kmeans聚类的输入,这样效果就有了明显的提升。
Mahout Kmeans 聚类:
// 基于内存的 K 均值聚类算法实现 public static void kMeansClusterInMemoryKMeans(){ // 指定需要聚类的个数,这里选择 2 类 int k = 2; // 指定 K 均值聚类算法的最大迭代次数 int maxIter = 3; // 指定 K 均值聚类算法的最大距离阈值 double distanceThreshold = 0.01; // 声明一个计算距离的方法,这里选择了欧几里德距离 DistanceMeasure measure = new EuclideanDistanceMeasure(); // 这里构建向量集,使用的是清单 1 里的二维点集 List<Vector> pointVectors = SimpleDataSet.getPointVectors(SimpleDataSet.points); // 从点集向量中随机的选择 k 个作为簇的中心 List<Vector> randomPoints = RandomSeedGenerator.chooseRandomPoints(pointVectors, k); // 基于前面选中的中心构建簇 List<Cluster> clusters = new ArrayList<Cluster>(); int clusterId = 0; for(Vector v : randomPoints){ clusters.add(new Cluster(v, clusterId ++, measure)); } // 调用 KMeansClusterer.clusterPoints 方法执行 K 均值聚类 List<List<Cluster>> finalClusters = KMeansClusterer.clusterPoints(pointVectors, clusters, measure, maxIter, distanceThreshold); // 打印最终的聚类结果 for(Cluster cluster : finalClusters.get(finalClusters.size() -1)){ System.out.println("Cluster id: " + cluster.getId() + " center: " + cluster.getCenter().asFormatString()); System.out.println(" Points: " + cluster.getNumPoints()); } } // 基于 Hadoop 的 K 均值聚类算法实现 public static void kMeansClusterUsingMapReduce () throws Exception{ // 声明一个计算距离的方法,这里选择了欧几里德距离 DistanceMeasure measure = new EuclideanDistanceMeasure(); // 指定输入路径,如前面介绍的一样,基于 Hadoop 的实现就是通过指定输入输出的文件路径来指定数据源的。 Path testpoints = new Path("testpoints"); Path output = new Path("output"); // 清空输入输出路径下的数据 HadoopUtil.overwriteOutput(testpoints); HadoopUtil.overwriteOutput(output); RandomUtils.useTestSeed(); // 在输入路径下生成点集,与内存的方法不同,这里需要把所有的向量写进文件,下面给出具体的例子 SimpleDataSet.writePointsToFile(testpoints); // 指定需要聚类的个数,这里选择 2 类 int k = 2; // 指定 K 均值聚类算法的最大迭代次数 int maxIter = 3; // 指定 K 均值聚类算法的最大距离阈值 double distanceThreshold = 0.01; // 随机的选择 k 个作为簇的中心 Path clusters = RandomSeedGenerator.buildRandom(testpoints, new Path(output, "clusters-0"), k, measure); // 调用 KMeansDriver.runJob 方法执行 K 均值聚类算法 KMeansDriver.runJob(testpoints, clusters, output, measure, distanceThreshold, maxIter, 1, true, true); // 调用 ClusterDumper 的 printClusters 方法将聚类结果打印出来。 ClusterDumper clusterDumper = new ClusterDumper(new Path(output, "clusters-" + maxIter -1), new Path(output, "clusteredPoints")); clusterDumper.printClusters(null); } //SimpleDataSet 的 writePointsToFile 方法,将测试点集写入文件里 // 首先我们将测试点集包装成 VectorWritable 形式,从而将它们写入文件 public static List<VectorWritable> getPoints(double[][] raw) { List<VectorWritable> points = new ArrayList<VectorWritable>(); for (int i = 0; i < raw.length; i++) { double[] fr = raw[i]; Vector vec = new RandomAccessSparseVector(fr.length); vec.assign(fr); // 只是在加入点集前,在 RandomAccessSparseVector 外加了一层 VectorWritable 的包装 points.add(new VectorWritable(vec)); } return points; } // 将 VectorWritable 的点集写入文件,这里涉及一些基本的 Hadoop 编程元素,详细的请参阅参考资源里相关的内容 public static void writePointsToFile(Path output) throws IOException { // 调用前面的方法生成点集 List<VectorWritable> pointVectors = getPoints(points); // 设置 Hadoop 的基本配置 Configuration conf = new Configuration(); // 生成 Hadoop 文件系统对象 FileSystem FileSystem fs = FileSystem.get(output.toUri(), conf); // 生成一个 SequenceFile.Writer,它负责将 Vector 写入文件中 SequenceFile.Writer writer = new SequenceFile.Writer(fs, conf, output, Text.class, VectorWritable.class); // 这里将向量按照文本形式写入文件 try { for (VectorWritable vw : pointVectors) { writer.append(new Text(), vw); } } finally { writer.close(); } }