一个K-means聚类算法的实现代码和分析

最近做聚类实验,实现了几个简单的聚类算法,其中基于最大最小的顺序聚类算法MBSAS就不贴了,受异常点影响很大,其实这个也很好理解,因为选最大距离进行分类(MBSAS先确定类,再根据类里的向量将剩下的样本分到某个类中)的时候,很容易就将异常点作为一个类分出去,那样在分2类的过程中,很明显会出现某个类占95%+,另一个类只有5%以下样本的情况,后者显然就是个异常点,这对我们的聚类是非常不利的,当然用来找异常点还是很有效的。

在实现K-mean算法的时候,也需要找初始点,如果采用最大最小的话还是会遇到这个问题,那么怎么解决这个问题呢?

我的想法是多次分类:如果某个初始点找到之后,第一次分类的结果有一个类的元素的数目特别小,那么就假定这个类的元素是噪声集的一部分,注意噪声集不等于噪声点,但是噪声点一定在噪声集中,而且最重要的是噪声集中的点都不适合作为初始点。然后第二次找初始点,再分类,再判断是否需要重新找,直到找到的所有按照初始点的第一次划分的集合的数目都不是特别小。

执行流程如下:

  1. 按照最大最小进行分类得到需要的n个初始点(噪声集的点不能被选作初始点)
  2. 按照这n个初始点对所有的元素进行分类,得到n个类,每个类有一些元素
  3. 如果这n个类中的某个类,假设为nk的元素的数目小于t,那么将nk中的元素都加到噪集当中
  4. 重复1 直到所有的类的元素的数目都大于等于t

那么这里又有一个问题:t是多少?

从直观的角度来想,t应该和样本数目N有关,样本数目越大,t越大,一个1000的样本的某个类有100个元素可能不是噪声,但是一个100000的样本的某个有100个元素的类则可能是噪声。同样,t还要和分类的数目C有关,C越大,t应该越小,1000个元素分为2个类,则一个含有50个元素的类就可能是噪声,但是如果把1000个元素分为10个类,则含有50个元素的类则不太可呢个是噪声,所以综合这两个因素,我设计了一个阈值函数:

 

t = N / C ^ 2

 

也就是说当N = 1000,C = 4 的时候, t = 1000/16 = 62,如果某个类小于62个元素,就认为是噪声集,关于这个函数,可以有很多种,比如t = N / (C * 4),这里面有什么倾向,读者可以自己思考,但是最终结果还是和数据相关。

 

补:

昨天老师公布了实验结果,我们组的成绩不错,尤其是分两类的情况,基本达到的最优值,但是八类的情况就不是那么好了,我想了想,是阈值函数的问题,因为上交的结果中使用的阈值函数是t = N / C ^ 3,C=8的时候,阈值失去了意义,所以一定程度上会受到噪声点的影响。

 

今天又想了想,觉得应该使用一个如下的函数:

t = N/(C * asym)   asym = 2,4,8….

这个函数中asym系数是不对乘系数,反应的是最小(数目)的类和最大的类的不对称比例,asym最小也应该为2,如果为1,那么就是均分,不现实,当为2的时候,1000个样本分两类,最小的类是250,如果觉得这个比例还是不够悬殊,可以另asym = 4,也就是最小的类为125,这个看上去就比较合适了,当然如果你比较极端,可以选择更大的不对称系数,但是注意,这里影响的只是对噪声点的判定,和最后的聚类结果的大小没有关系,也就是说如果asym = 8, N =1000,C = 2,那么以一个初始点M为中心的第一次划分的类的数目只要大于 1000/2/8 = 62,就不认为这个点是噪声,显然这看起来有些不合理,一般来说asym=4应该是一个比较好的情况。

 

完整的代码如下(阈值函数没有引入不对乘系数,读者请自己修改):

package cn.edu.pku.ss.dm.cluster; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; /** * K-means算法实现 * * @author eagle * */ public class KMeans { //聚类的数目 final static int ClassCount = 2; //样本数目(测试集) final static int InstanseNumber = 1000; //样本属性数目(测试) final static int FieldCount = 57; //设置异常点阈值参数(每一类初始的最小数目为InstanseNumber/ClassCount^t) final static double t = 2.0; //存放数据的矩阵 private float[][] data; //每个类的均值中心 private float[][] classData; //噪声集合索引 private ArrayList<Integer> noises; //存放每次变换结果的矩阵 private ArrayList<ArrayList<Integer>> result; /** * 构造函数,初始化 */ public KMeans() { //最后一位用来储存结果 data = new float[InstanseNumber][FieldCount + 1]; classData = new float[ClassCount][FieldCount]; result = new ArrayList<ArrayList<Integer>>(ClassCount); noises = new ArrayList<Integer>(); } /** * 主函数入口 * 测试集的文件名称为”测试集.data”,其中又1000*57大小的数据 * 每一行为一个样本,有57个属性 * 主要分为两个步骤 * 1.读取数据 * 2.进行聚类 * 最后统计了运行的时间和消耗的内存 * @param args */ public static void main(String[] args) { long startTime = System.currentTimeMillis(); KMeans cluster = new KMeans(); //读取数据 cluster.readData(“测试集.data”); //聚类过程 cluster.cluster(); //输出结果 cluster.printResult(“clusterResult.data”); long endTime = System.currentTimeMillis(); System.out.println(“Total Time: ” + (endTime – startTime)/1000 + ” s”); System.out.println(“Memory Consuming: ” + (float)(Runtime.getRuntime().totalMemory() – Runtime.getRuntime().freeMemory())/1000000 + ” MB”); } /** * 读取测试集的数据 * * @param trainingFileName 测试集文件名 */ public void readData(String trainingFileName){ try { FileReader fr = new FileReader(trainingFileName); BufferedReader br = new BufferedReader(fr); //存放数据的临时变量 String lineData = null; String[] splitData = null; int line = 0; //按行读取 while(br.ready()) { //得到原始的字符串 lineData = br.readLine(); splitData = lineData.split(“,”); //转化为数据 for(int i = 0;i<splitData.length;i++) data[line][i] = Float.parseFloat(splitData[i]); line++; } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * 聚类过程,主要分为两步 * 1.循环找初始点 * 2.不断调整直到分类不再发生变化 */ public void cluster() { //数据归一化 normalize(); //标记是否需要重新找初始点 boolean needUpdataInitials = true; //找初始点的迭代次数 int times = 1; //找初始点 while(needUpdataInitials) { needUpdataInitials = false; result.clear(); System.out.println(“Find Initials Iteration ” + (times++) + ” time(s) “); //一次找初始点的尝试和根据初始点的分类 findInitials(); firstClassify(); //如果某个分类的数目小于特定的阈值、则认为这个分类中的所有样本都是噪声点 //需要重新找初始点 for(int i = 0; i < result.size();i++){ if(result.get(i).size() < InstanseNumber / Math.pow(ClassCount, t)){ needUpdataInitials = true; noises.addAll(result.get(i)); } } } //找到合适的初始点后 //不断的调整均值中心和分类、直到不再发生任何变化 Adjust(); } /** * 对数据进行归一化 * 1.找每一个属性的最大值 * 2.对某个样本的每个属性除以其最大值 */ public void normalize(){ //找最大值 float[] max = new float[FieldCount]; for(int i = 0;i<InstanseNumber;i++){ for(int j = 0;j < FieldCount ;j++){ if(data[i][j] > max[j]) max[j] = data[i][j]; } } //归一化 for(int i = 0;i<InstanseNumber;i++){ for(int j = 0;j < FieldCount ;j++){ data[i][j] = data[i][j]/max[j]; } } } /** * 关于初始向量的一次找寻尝试 */ public void findInitials(){ //a,b为标志距离最远的两个向量的索引 int i,j,a,b; i = j = a = b = 0; //最远距离 float maxDis = 0; //已经找到的初始点个数 int alreadyCls = 2; //存放已经标记为初始点的向量索引 ArrayList<Integer> initials = new ArrayList<Integer>(); //从两个开始 for(;i < InstanseNumber;i++) { //噪声点 if(noises.contains(i)) continue; //long startTime = System.currentTimeMillis(); j = i + 1; for(; j < InstanseNumber;j++) { //噪声点 if(noises.contains(j)) continue; //找到最大的距离并记录下来 float newDis = calDis(data[i], data[j]); if( maxDis < newDis) { a = i; b = j; maxDis = newDis; } } //long endTime = System.currentTimeMillis(); //System.out.println(i + ” Vector Caculation Time:” + (endTime – startTime) + ” ms”); } //将前两个初始点记录下来 initials.add(a); initials.add(b); classData[0] = data[a]; classData[1] = data[b]; //在结果中新建存放某类样本索引的对象,并把初始点添加进去 ArrayList<Integer> resultOne = new ArrayList<Integer>(); ArrayList<Integer> resultTwo = new ArrayList<Integer>(); resultOne.add(a); resultTwo.add(b); result.add(resultOne); result.add(resultTwo); //找到剩余的几个初始点 while( alreadyCls < ClassCount){ i = j = 0; float maxMin = 0; int newClass = -1; //找最小值中的最大值 for(;i < InstanseNumber;i++){ float min = 0; float newMin = 0; //找和已有类的最小值 if(initials.contains(i)) continue; //噪声点去除 if(noises.contains(i)) continue; for(j = 0;j < alreadyCls;j++){ newMin = calDis(data[i], classData[j]); if(min == 0 || newMin < min) min = newMin; } //新最小距离较大 if(min > maxMin) { maxMin = min; newClass = i; } } //添加到均值集合和结果集合中 //System.out.println(“NewClass ” + newClass); initials.add(newClass); classData[alreadyCls++] = data[newClass]; ArrayList<Integer> rslt = new ArrayList<Integer>(); rslt.add(newClass); result.add(rslt); } } /** * 第一次分类 */ public void firstClassify() { //根据初始向量分类 for(int i = 0;i < InstanseNumber;i++) { float min = 0f; int clsId = -1; for(int j = 0;j < classData.length;j++){ //欧式距离 float newMin = calDis(classData[j], data[i]); if(clsId == -1 || newMin < min){ clsId = j; min = newMin; } } //本身不再添加 if(!result.get(clsId).contains(i)) { result.get(clsId).add(i); } } } /** * 迭代分类、直到各个类的数据不再变化 */ public void Adjust() { //记录是否发生变化 boolean change = true; //循环的次数 int times = 1; while(change){ //复位 change = false; System.out.println(“Adjust Iteration ” + (times++) + ” time(s) “); //重新计算每个类的均值 for(int i = 0;i < ClassCount; i++){ //原有的数据 ArrayList<Integer> cls = result.get(i); //新的均值 float[] newMean = new float[FieldCount ]; //计算均值 for(Integer index:cls){ for(int j = 0;j < FieldCount ;j++) newMean[j] += data[index][j]; } for(int j = 0;j < FieldCount ;j++) newMean[j] /= cls.size(); if(!compareMean(newMean, classData[i])){ classData[i] = newMean; change = true; } } //清空之前的数据 for(ArrayList<Integer> cls:result) cls.clear(); //重新分配 for(int i = 0;i < InstanseNumber;i++) { float min = 0f; int clsId = -1; for(int j = 0;j < classData.length;j++){ float newMin = calDis(classData[j], data[i]); if(clsId == -1 || newMin < min){ clsId = j; min = newMin; } } data[i][FieldCount] = clsId; result.get(clsId).add(i); } //测试聚类效果(训练集) // for(int i = 0;i < ClassCount;i++){ // int positives = 0; // int negatives = 0; // ArrayList<Integer> cls = result.get(i); // for(Integer instance:cls) // if (data[instance][FieldCount – 1] == 1f) // positives ++; // else // negatives ++; // System.out.println(” ” + i + ” Positive: ” + positives + ” Negatives: ” + negatives); // } // System.out.println(); } } /** * 计算a样本和b样本的欧式距离作为不相似度 * * @param a 样本a * @param b 样本b * @return 欧式距离长度 */ private float calDis(float[] aVector,float[] bVector) { double dis = 0; int i = 0; //最后一个数据在训练集中为结果,所以不考虑 for(;i < aVector.length;i++) dis += Math.pow(bVector[i] – aVector[i],2); dis = Math.pow(dis, 0.5); return (float)dis; } /** * 判断两个均值向量是否相等 * * @param a 向量a * @param b 向量b * @return */ private boolean compareMean(float[] a,float[] b) { if(a.length != b.length) return false; for(int i =0;i < a.length;i++){ if(a[i] > 0 &&b[i] > 0&& a[i] != b[i]){ return false; } } return true; } /** * 将结果输出到一个文件中 * * @param fileName */ public void printResult(String fileName) { FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(fileName); bw = new BufferedWriter(fw); //写入文件 for(int i = 0;i < InstanseNumber;i++) { bw.write(String.valueOf(data[i][FieldCount]).substring(0, 1)); bw.newLine(); } //统计每类的数目,打印到控制台 for(int i = 0;i < ClassCount;i++) { System.out.println(“第” + (i+1) + “类数目: ” + result.get(i).size()); } } catch (IOException e) { e.printStackTrace(); } finally{ //关闭资源 if(bw != null) try { bw.close(); } catch (IOException e) { e.printStackTrace(); } if(fw != null) try { fw.close(); } catch (IOException e) { e.printStackTrace(); } } } }  

 

    原文作者:聚类算法
    原文地址: https://blog.csdn.net/eaglex/article/details/6376533
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞