初识kMeans 聚类

概念引入:

我们前面讲到了knn算法,我们手里都是有一个标签,给出数据我们朝着标签方向优化,使得未知属性的数据预测其属于哪一类标签。现在问题来了,无监督学习,也就是我们手里的数据并没有属于哪一类的标签。其实,这和我们分类的效果是差不多的。我们还是要把杂乱的数据分成很多簇,聚类就是要把相似的东西分到一组。比如说这张图片:

我们可以按照颜色大致分成A,B,C,D四块

这种思想还是挺简单的吧,可是实现起来还是有一些难点:如何评估,如何调参。之前的有监督学习我们还可以做一个验证,然后产生一个评估标准。这些评估标准起码都是基于预测值和正确值之间的一个比较吧。比如 Knn中 我们评估正确率,正确的预测数 / 预测的总数,其中正确的预测数是我们预测的花朵(未知数据需要我们判断是那一朵花)和 标准花朵(这个数据已经有对应的标签)进行比较得来的。

但是在无监督学习中我们手里没有了标签,也就是说一个同学做完一套试卷后没有正确答案,然后叫你评论这个同学做得怎么样,这就非常难了。

K-MEANS算法:

最简单,最实用的一个算法

  • 要得到簇的个数,需要指定k值
  • 质心:均值,即向量各维取平均即可
  • 距离的度量:常用欧几里得距离和余弦相似度(先标准化)
  • 优化目标:min \sum_{i = 1}^{k} \sum_{x \in C_{i}}^{} dist (C_{i},x)^{2}

k值需要我们来指定,在杂乱的数据中,我们指定 K 等于多少,它就会把这堆数据分成几个簇(小的部分)比如上面的图片我们分成四个部分。质心就是在这一簇数据中各维度数据的平均值,比如x方向上的平均值,y方向上的平均值,实际情况维度可能更多,延伸就好了。可以理解类似于各部分的中心坐标。距离的度量,一般我们采用欧氏距离,就是把未知的点和各个质心之间的距离求出来。标准化就是规定我们的数据在那个区间进行浮动比如 x方向上(0,100),y 方向上(0,10)。优化目标就是计算出每一个点和质心的距离,然后比较并判断出它属于那一个质心的类。

优势:

  • 简单
  • 快速
  • 适合常规数据集

劣势:

  • k值难以确定
  • 复杂度与样本呈线性关系
  • 很难发现任意形状的簇

举例:

下面是一些数据分布图,k = 2:

我们选取质心并分类,下面我们要判断黑球属于哪一类

 计算出黑球的到两个质心的距离很容易看出属于蓝色阵营。然后进行质心更新,直到质心的位置不变为止。对分类影响最大的便是初始质心的选择,随机选择不同的位置,那么它的效果会不同的,有时甚至得不到我们想要的结果。

代码:

这里我们用闵教授第56天的代码,里面干货多多,我在这里把注释给好了,大家可以了解一下。

package machinelearning.kmeans;

import java.io.FileReader;
import java.util.Arrays;
import java.util.Random;
import weka.core.Instances;

 public class KMeans {

	/**
	 * 曼哈顿距离
	 */
	public static final int MANHATTAN = 0;

	/**
	 * 欧式距离
	 */
	public static final int EUCLIDEAN = 1;

	/**
	 * 距离量度默认是欧式距离
	 */
	public int distanceMeasure = EUCLIDEAN;

	/**
	 * 随机距离
	 */
	public static final Random random = new Random();

	/**
	 * 数据
	 */
	Instances dataset;

	/**
	 * 簇值也就是k值
	 */
	int numClusters = 2;

	/**
	 * 簇
	 */
	int[][] clusters;

	/**
	 * KMeans:主要负责传入数据
	 */
	public KMeans(String paraFilename) {
		dataset = null;
		try {
			FileReader fileReader = new FileReader(paraFilename);
			dataset = new Instances(fileReader);
			fileReader.close();
		} catch (Exception ee) {
			System.out.println("Cannot read the file: " + paraFilename + "\r\n" + ee);
			System.exit(0);
		} // Of try
	}// Of the first constructor

	/**
	 ******************************* 
	 * 设置k的值
	 ******************************* 
	 */
	public void setNumClusters(int paraNumClusters) {
		numClusters = paraNumClusters;
	}// Of the setter

	/**
	 *********************
	 * 数据随机化获得随机索引
	 * 
	 * @param paraLength
	 *            The length of the sequence.
	 * @return An array of indices, e.g., {4, 3, 1, 5, 0, 2} with length 6.
	 *********************
	 */
	public static int[] getRandomIndices(int paraLength) {
		int[] resultIndices = new int[paraLength];

		// Step 1. 初始化数组
		for (int i = 0; i < paraLength; i++) {
			resultIndices[i] = i;
		} // Of for i

		// Step 2. 随机交换
		int tempFirst, tempSecond, tempValue;
		for (int i = 0; i < paraLength; i++) {
			// 这里随机生成两个索引,保证了每次随机数组不同值。
			tempFirst = random.nextInt(paraLength);
			tempSecond = random.nextInt(paraLength);

			// Swap.
			tempValue = resultIndices[tempFirst];
			resultIndices[tempFirst] = resultIndices[tempSecond];
			resultIndices[tempSecond] = tempValue;
		} // Of for i

		return resultIndices;
	}// Of getRandomIndices

	/**
	 *********************
	 * 两点间的距离:欧式距离、曼哈顿距离
	 * 
	 * @param paraI
	 *            The index of the first instance.
	 * @param paraArray
	 *            The array representing a point in the space.
	 * @return The distance.
	 *********************
	 */
	public double distance(int paraI, double[] paraArray) {
		int resultDistance = 0;
		double tempDifference;
		switch (distanceMeasure) {
		case MANHATTAN:
			for (int i = 0; i < dataset.numAttributes() - 1; i++) {
				tempDifference = dataset.instance(paraI).value(i) - paraArray[i];
				if (tempDifference < 0) {
					resultDistance -= tempDifference;
				} else {
					resultDistance += tempDifference;
				} // Of if
			} // Of for i
			break;

		case EUCLIDEAN:
			for (int i = 0; i < dataset.numAttributes() - 1; i++) {
				tempDifference = dataset.instance(paraI).value(i) - paraArray[i];
				resultDistance += tempDifference * tempDifference;
			} // Of for i
			break;
		default:
			System.out.println("Unsupported distance measure: " + distanceMeasure);
		}// Of switch

		return resultDistance;
	}// Of distance

	/**
	 ******************************* 
	 * Clustering.
	 ******************************* 
	 */
	public void clustering() {
		// 生成一个数组tempOldClusterArray,大小是数据的长度
		int[] tempOldClusterArray = new int[dataset.numInstances()];
		tempOldClusterArray[0] = -1;
		// 生成数组tempClusterArray,大小是数据长度
		int[] tempClusterArray = new int[dataset.numInstances()];
		// tempClusterArray数组的值全部为0
		Arrays.fill(tempClusterArray, 0);
		// 生成过程的中心点,每一个中心点应该具有除标签以外的全部属性,所以-1.
		double[][] tempCenters = new double[numClusters][dataset.numAttributes() - 1];
														
		// 第一步初始化中心点
		int[] tempRandomOrders = getRandomIndices(dataset.numInstances());
		
		// 理解:把数据进行了随机化排序,然后选择前 K 个作为第一次的随机点
		for (int i = 0; i < numClusters; i++) {
			// 每一个结点的分量(除去标签)赋值了
			for (int j = 0; j < tempCenters[0].length; j++) {
				tempCenters[i][j] = dataset.instance(tempRandomOrders[i]).value(j);
			} // Of for j
		} // Of for i
		

		int[] tempClusterLengths = null;
		while (!Arrays.equals(tempOldClusterArray, tempClusterArray)) {
			System.out.println("New loop ...");
			tempOldClusterArray = tempClusterArray;
			tempClusterArray = new int[dataset.numInstances()];

			// Step 2.1 细化 Assign 把集群分给每一个数据。
			int tempNearestCenter;
			double tempNearestDistance;
			double tempDistance;

			// 每一个数据结点
			for (int i = 0; i < dataset.numInstances(); i++) {
				tempNearestCenter = -1;
				// 将距离置为最大
				tempNearestDistance = Double.MAX_VALUE;
				
				// 计算出每一个数据结点到中心的距离
				for (int j = 0; j < numClusters; j++) {
					tempDistance = distance(i, tempCenters[j]);
					// 如果这个距离更短那么就把最短的距离中心索引记下来
					if (tempNearestDistance > tempDistance) {
						tempNearestDistance = tempDistance;
						tempNearestCenter = j;
					} // Of if
				} // Of for j
				// 把最近的中心索引放入簇的数组中,开始全为0,这里开始有变化了。
				tempClusterArray[i] = tempNearestCenter;
			} // Of for i

			// Step 2.2 找到新的中心
			tempClusterLengths = new int[numClusters];
			Arrays.fill(tempClusterLengths, 0);
			double[][] tempNewCenters = new double[numClusters][dataset.numAttributes() - 1];
			// 外层循环是每一个数据结点循环
			for (int i = 0; i < dataset.numInstances(); i++) {
				// 内层是这个数据的属性出去标签的循环。
				for (int j = 0; j < tempNewCenters[0].length; j++) {
					// 把属性值存进去求和 这个数组:a[中心值][对应属性的和]
					tempNewCenters[tempClusterArray[i]][j] += dataset.instance(i).value(j);
				} // Of for j
				// 让0变为1,这个数组是储存属性长度设计几个数目,不同的簇中成员可能不同。
				tempClusterLengths[tempClusterArray[i]]++;
			} // Of for i
			
			// Step 2.3 求平均值
			// 外层循环是中心结点数目的循环
			for (int i = 0; i < tempNewCenters.length; i++) {
				// 内层循环是中心结点对应的属性循环
				for (int j = 0; j < tempNewCenters[0].length; j++) {
					tempNewCenters[i][j] /= tempClusterLengths[i];
				} // Of for j
			} // Of for i
			
			System.out.println("Now the new centers are: " + Arrays.deepToString(tempNewCenters));
			// 把中心换了。
			tempCenters = tempNewCenters;
		} // Of while

		// Step 3.生成新的簇,先生成空间
		clusters = new int[numClusters][];
		// 这个数组大小是 k 个空间,开始的值都为0
		int[] tempCounters = new int[numClusters];
		for (int i = 0; i < numClusters; i++) {
			clusters[i] = new int[tempClusterLengths[i]];
		} // Of for i
		
		// 把值传递进去,在数据中的索引tempClusterArray数组装的是该数据的索引,长度是所有的数据的总量
		for (int i = 0; i < tempClusterArray.length; i++) {
			clusters[tempClusterArray[i]][tempCounters[tempClusterArray[i]]] = i;
			tempCounters[tempClusterArray[i]]++;
		} // Of for i

		System.out.println("The clusters are: " + Arrays.deepToString(clusters));
	}// Of clustering

	/**
	 ******************************* 
	 * Clustering.
	 ******************************* 
	 */
	public static void testClustering() {
		KMeans tempKMeans = new KMeans("D:/data/iris.arff");
		tempKMeans.setNumClusters(3);
		tempKMeans.clustering();
	}// Of testClustering

	/**
	 ************************* 
	 * 测试方法
	 ************************* 
	 */
	public static void main(String arags[]) {
		testClustering();
	}// Of main

}// Of class KMeans

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值