赞
踩
监督式学习:训练集有明确答案,监督学习就是寻找问题(又称输入、特征、自变量)与答案(又称输出、目标、因变量)之间关系的学习方式。监督学习模型有两类,分类和回归。
• 分类模型:目标变量是离散的分类型变量;
• 回归模型:目标变量是连续性数值型变量。
无监督学习:只有数据,无明确答案,即训练集没有标签。常见的无监督学习算法有聚类(clustering),由计算机自己找出规律,把有相似属性的样本放在一组,每个组也称为簇(cluster)。
最早的聚类分析是在考古分类、昆虫分类研究中发展起来的,目的是找到隐藏于数据中客观存在的“自然小类”,“自然小类”具有类内结构相似、类间结构差异显著的特点,通过刻画“自然小类”可以发现数据中的规律、揭示数据的内在结构。
回归算法中超级典型的线性回归,分类算法中非常难懂的SVM,这两都是有监督学习中的模型,那今天就来看看无监督学习中最最基础的聚类算法——K-Means Cluster吧。
K-Means聚类步骤是一个循环迭代的算法,非常简单易懂:
1.假定我们要对N个样本观测做聚类,要求聚为K类,首先选择K个点作为初始中心点;
2.接下来,按照距离初始中心点最小的原则,把所有观测分到各中心点所在的类中;
3.每类中有若干个观测,计算K个类中所有样本点的均值,作为第二次迭代的K个中心点;
4.然后根据这个中心重复第2、3步,直到收敛(中心点不再改变或达到指定的迭代次数),聚类过程结束。
以二维平面中的点
为例,用图片展示K=2时的迭代过程:
1.现在我们要将(a)图中的n个绿色点聚为2类,先随机选择蓝叉和红叉分别作为初始中心点;
2.分别计算所有点到初始蓝叉和初始红叉的距离,
距离蓝叉更近就涂为蓝色,距离红叉更近就涂为红色,遍历所有点,直到全部都染色完成,如图(b);
3.现在我们不管初始蓝叉和初始红叉了,对于已染色的红色点计算其红色中心,蓝色点亦然,得到第二次迭代的中心,如图©;
4.重复第2、3步,直到收敛,聚类过程结束。
怎么样,很简单吧?看完K-Means算法步骤的文字描述,我们可能会有以下疑问:
1.第一步中的初始中心点怎么确定?随便选吗?不同的初始点得到的最终聚类结果也不同吗?
2.第二步中点之间的距离用什么来定义?
3.第三步中的所有点的均值(新的中心点)怎么算?
4.K怎么选择?
我们先解答第2个和第3个问题,其他两个问题放到后面小节中再说。
聚类是把相似的物体聚在一起,这个相似度(或称距离)是用什么来度量的呢?这又得提到我们的老朋友——欧氏距离。
给定两个样本
与
,其中n表示特征数 ,X和Y两个向量间的欧氏距离(Euclidean Distance)表示为:
k-means算法是把数据给分成不同的簇,目标是同一个簇中的差异小,不同簇之间的差异大,这个目标怎么用数学语言描述呢?我们一般用误差平方和作为目标函数(想想线性回归中说过的残差平方和、损失函数,是不是很相似),公式如下:
其中C表示聚类中心,如果x属于Ci这个簇,则计算两者的欧式距离,将所有样本点到其中心点距离算出来,并加总,就是k-means的目标函数。实现同一个簇中的样本差异小,就是最小化SSE。
可以通过求导来求函数的极值,我们对SSE求偏导看看能得到什么结果:
式中m是簇中点的数量,发现了没有,这个C的解,就是X的均值点。多点的均值点应该很好理解吧,给定一组点 X1,…,Xm,其中Xi=(xi1,xi2,…,xin) ,这组点的均值向量表示为:
在k-means算法步骤中,有两个地方降低了SSE:
1.把样本点分到最近邻的簇中,这样会降低SSE的值;
2.重新优化聚类中心点,进一步的减小了SSE。
这样的重复迭代、不断优化,会找到局部最优解(局部最小的SSE),如果想要找到全局最优解需要找到合理的初始聚类中心。
那合理的初始中心怎么选?
方法有很多,譬如先随便选个点作为第1个初始中心C1,接下来计算所有样本点与C1的距离,距离最大的被选为下一个中心C2,直到选完K个中心。这个算法叫做K-Means++,可以理解为 K-Means的改进版,它可以能有效地解决初始中心的选取问题,但无法解决离群点问题。
我自己也想了一个方法,先找所有样本点的均值点,计算每个点与均值点的距离,选取最远的K个点作为K个初始中心。当然,如果样本中有离群点,这个方法也不佳。
总的来说,最好解决办法还是多尝试几次,即多设置几个不同的初始点,从中选最优,也就是具有最小SSE值的那组作为最终聚类。
要知道,K设置得越大,样本划分得就越细,每个簇的聚合程度就越高,误差平方和SSE自然就越小。所以不能单纯像选择初始点那样,用不同的K来做尝试,选择SSE最小的聚类结果对应的K值,因为这样选出来的肯定是你尝试的那些K值中最大的那个。
确定K值的一个主流方法叫“手肘法”。
如果我们拿到的样本,客观存在J个“自然小类”,这些真实存在的小类是隐藏于数据中的。三维以下的数据我们还能画图肉眼分辨一下J的大概数目,更高维的就不能直观地看到了,我们只能从一个比较小的K,譬如K=2开始尝试,去逼近这个真实值J。
1.当K小于样本真实簇数J时,K每增大一个单位,就会大幅增加每个簇的聚合程度,这时SSE的下降幅度会很大;
2.当K接近J时,再增加K所得到的聚合程度回报会迅速变小,SSE的下降幅度也会减小;
3.随着K的继续增大,SSE的变化会趋于平缓。
例如下图,真实的J我们事先不知道,那么从K=2开始尝试,发现K=3时,SSE大幅下降,K=4时,SSE下降幅度稍微小了点,K=5时,下降幅度急速缩水,再后面就越来越平缓。所以我们认为J应该为4,因此可以将K设定为4。
叫“手肘法”可以说很形象了,因为SSE和K的关系图就像是手肘的形状,而肘部对应的K值就被认为是数据的真实聚类数。
当然还有其他设定K值的方法,这里不赘述,总的来说还是要结合自身经验多做尝试,要知道没有一个方法是完美的。
而且,聚类有时是比较主观的事,比如下面这组点,真实簇数J是几呢?我们既可以说J=3,也可以就把它分成2个簇。
K-Means优点在于原理简单,容易实现,聚类效果好。
当然,也有一些缺点:
1.K值、初始点的选取不好确定;
2.得到的结果只是局部最优;
3.受离群值影响大。
完整代码:
package machinelearning.kmeans; import java.io.FileReader; import java.util.Arrays; import java.util.Random; import weka.core.Instances; /** * kMeans clustering. * @author Rui Chen 1369097405@qq.com. */ public class KMeans { /** * Manhattan distance. */ public static final int MANHATTAN = 0; /** * Euclidean distance. */ public static final int EUCLIDEAN = 1; /** * The distance measure. */ public int distanceMeasure = EUCLIDEAN; /** * A random instance; */ public static final Random random = new Random(); /** * The data. */ Instances dataset; /** * The number of clusters. */ int numClusters = 2; /** * The clusters. */ int[][] clusters; /** ******************************* * The first constructor. * * @param paraFilename * The data filename. ******************************* */ 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 /** ******************************* * A setter. ******************************* */ public void setNumClusters(int paraNumClusters) { numClusters = paraNumClusters; }// Of the setter /** ********************* * Get a random indices for data randomization. * * @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. Initialize. for (int i = 0; i < paraLength; i++) { resultIndices[i] = i; } // Of for i // Step 2. Randomly swap. int tempFirst, tempSecond, tempValue; for (int i = 0; i < paraLength; i++) { // Generate two random indices. 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 /** ********************* * The distance between two instances. * * @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() { int[] tempOldClusterArray = new int[dataset.numInstances()]; tempOldClusterArray[0] = -1; int[] tempClusterArray = new int[dataset.numInstances()]; Arrays.fill(tempClusterArray, 0); double[][] tempCenters = new double[numClusters][dataset.numAttributes() - 1]; // Step 1. Initialize centers. int[] tempRandomOrders = getRandomIndices(dataset.numInstances()); 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 Minimization. Assign cluster to each instance. 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 tempClusterArray[i] = tempNearestCenter; } // Of for i // Step 2.2 Mean. Find new centers. tempClusterLengths = new int[numClusters]; Arrays.fill(tempClusterLengths, 0); double[][] tempNewCenters = new double[numClusters][dataset.numAttributes() - 1]; // Arrays.fill(tempNewCenters, 0); for (int i = 0; i < dataset.numInstances(); i++) { for (int j = 0; j < tempNewCenters[0].length; j++) { tempNewCenters[tempClusterArray[i]][j] += dataset.instance(i).value(j); } // Of for j tempClusterLengths[tempClusterArray[i]]++; } // Of for i // Step 2.3 Now average 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. Form clusters. clusters = new int[numClusters][]; int[] tempCounters = new int[numClusters]; for (int i = 0; i < numClusters; i++) { clusters[i] = new int[tempClusterLengths[i]]; } // Of for i 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 /** ************************* * A testing method. ************************* */ public static void main(String arags[]) { testClustering(); }// Of main }// Of class KMeans
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。