重点介绍下K-means聚类算法。K-means算法是比较经典的聚类算法,算法的基本思想是选取K个点(随机)做为中心进行聚类,而后对聚类的结果计算该类的质心,经过迭代的方法不断更新质心,直到质心不变或稍微移动为止,则最后的聚类结果就是最后的聚类结果。下面首先介绍下K-means具体的算法步骤。java
K-means算法算法
在前面已经大概的介绍了下K-means,下面就介绍下具体的算法描述:数组
1)选取K个点做为初始质心;app
2)对每一个样本分别计算到K个质心的类似度或距离,将该样本划分到类似度最高或距离最短的质心所在类;dom
3)对该轮聚类结果,计算每个类别的质心,新的质心做为下一轮的质心;ide
4)判断算法是否知足终止条件,知足终止条件结束,不然继续第二、三、4步。this
在介绍算法以前,咱们首先看下K-means算法聚类平面200,000个点聚成34个类别的结果(以下图)spa

算法实现.net
K-means聚类算法总体思想比较简单,下面 就分步介绍如何用Java来实现K-means算法。code
1、K-means算法基础属性
在K-means算法中,有几个重要的指标,好比K值、最大迭代次数等,对于这些指标,咱们统一把它们设置为类的属性,以下:
[java] view plain copy
print?

- private List<T> dataArray;//待分类的原始值
- private int K = 3;//将要分红的类别个数
- private int maxClusterTimes = 500;//最大迭代次数
- private List<List<T>> clusterList;//聚类的结果
- private List<T> clusteringCenterT;//质心
2、初始质心的选择
K-means聚类算法的结果很大程度收到初始质心的选取,这了为了保证有充分的随机性,对于初始质心的选择这里采用彻底随机的方法,先把待分类的数据随机打乱,而后把前K个样本做为初始质心(经过屡次迭代,会减小初始质心的影响)。
[java] view plain copy
print?

- List<T> centerT = new ArrayList<T>(size);
- //对数据进行打乱
- Collections.shuffle(dataArray);
- for (int i = 0; i < size; i++) {
- centerT.add(dataArray.get(i));
- }
3、一轮聚类
在K-means算法中,大部分的时间都在作一轮一轮的聚类,具体功能也很简单,就是对每个样本分别计算和全部质心的类似度或距离,找到与该样本最类似的质心或者距离最近的质心,而后把该样本划分到该类中,具体逻辑介绍参照代码中的注释。
[java] view plain copy
print?

- private void clustering(List<T> preCenter, int times) {
- if (preCenter == null || preCenter.size() < 2) {
- return;
- }
- //打乱质心的顺序
- Collections.shuffle(preCenter);
- List<List<T>> clusterList = getListT(preCenter.size());
- for (T o1 : this.dataArray) {
- //寻找最类似的质心
- int max = 0;
- double maxScore = similarScore(o1, preCenter.get(0));
- for (int i = 1; i < preCenter.size(); i++) {
- if (maxScore < similarScore(o1, preCenter.get(i))) {
- maxScore = similarScore(o1, preCenter.get(i));
- max = i;
- }
- }
- clusterList.get(max).add(o1);
- }
- //计算本次聚类结果每一个类别的质心
- List<T> nowCenter = new ArrayList<T> ();
- for (List<T> list : clusterList) {
- nowCenter.add(getCenterT(list));
- }
- //是否达到最大迭代次数
- if (times >= this.maxClusterTimes || preCenter.size() < this.K) {
- this.clusterList = clusterList;
- return;
- }
- this.clusteringCenterT = nowCenter;
- //判断质心是否发生移动,若是没有移动,结束本次聚类,不然进行下一轮
- if (isCenterChange(preCenter, nowCenter)) {
- clear(clusterList);
- clustering(nowCenter, times + 1);
- } else {
- this.clusterList = clusterList;
- }
- }
4、质心是否移动
在第三步中,提到了一个重要的步骤:每轮聚类结束后,都要从新计算质心,而且计算质心是否发生移动。对于新质心的计算、样本之间的类似度和判断两个样本是否相等这几个功能因为并不知道样本的具体数据类型,所以把他们定义成抽象方法,供子类来实现。下面就重点介绍如何判断质心是否发生移动。
[java] view plain copy
print?

- private boolean isCenterChange(List<T> preT, List<T> nowT) {
- if (preT == null || nowT == null) {
- return false;
- }
- for (T t1 : preT) {
- boolean bol = true;
- for (T t2 : nowT) {
- if (equals(t1, t2)) {//t1在t2中有相等的,认为该质心未移动
- bol = false;
- break;
- }
- }
- //有一个质心发生移动,认为须要进行下一次计算
- if (bol) {
- return bol;
- }
- }
- return false;
- }
从上述代码能够看到,算法的思想就是对于先后两个质心数组分别前一组的质心是否在后一个质心组中出现,有一个没有出现,就认为质心发生了变更。
完整代码
上面四步已经完整的介绍了K-means算法的具体算法思想,下面就看下完整的代码实现。
[java] view plain copy
print?

- /**
- *@Description: K-means聚类
- */
- package com.lulei.datamining.knn;
-
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
-
- public abstract class KMeansClustering <T>{
- private List<T> dataArray;//待分类的原始值
- private int K = 3;//将要分红的类别个数
- private int maxClusterTimes = 500;//最大迭代次数
- private List<List<T>> clusterList;//聚类的结果
- private List<T> clusteringCenterT;//质心
-
- public int getK() {
- return K;
- }
- public void setK(int K) {
- if (K < 1) {
- throw new IllegalArgumentException("K must greater than 0");
- }
- this.K = K;
- }
- public int getMaxClusterTimes() {
- return maxClusterTimes;
- }
- public void setMaxClusterTimes(int maxClusterTimes) {
- if (maxClusterTimes < 10) {
- throw new IllegalArgumentException("maxClusterTimes must greater than 10");
- }
- this.maxClusterTimes = maxClusterTimes;
- }
- public List<T> getClusteringCenterT() {
- return clusteringCenterT;
- }
- /**
- * @return
- * @Author:lulei
- * @Description: 对数据进行聚类
- */
- public List<List<T>> clustering() {
- if (dataArray == null) {
- return null;
- }
- //初始K个点为数组中的前K个点
- int size = K > dataArray.size() ? dataArray.size() : K;
- List<T> centerT = new ArrayList<T>(size);
- //对数据进行打乱
- Collections.shuffle(dataArray);
- for (int i = 0; i < size; i++) {
- centerT.add(dataArray.get(i));
- }
- clustering(centerT, 0);
- return clusterList;
- }
-
- /**
- * @param preCenter
- * @param times
- * @Author:lulei
- * @Description: 一轮聚类
- */
- private void clustering(List<T> preCenter, int times) {
- if (preCenter == null || preCenter.size() < 2) {
- return;
- }
- //打乱质心的顺序
- Collections.shuffle(preCenter);
- List<List<T>> clusterList = getListT(preCenter.size());
- for (T o1 : this.dataArray) {
- //寻找最类似的质心
- int max = 0;
- double maxScore = similarScore(o1, preCenter.get(0));
- for (int i = 1; i < preCenter.size(); i++) {
- if (maxScore < similarScore(o1, preCenter.get(i))) {
- maxScore = similarScore(o1, preCenter.get(i));
- max = i;
- }
- }
- clusterList.get(max).add(o1);
- }
- //计算本次聚类结果每一个类别的质心
- List<T> nowCenter = new ArrayList<T> ();
- for (List<T> list : clusterList) {
- nowCenter.add(getCenterT(list));
- }
- //是否达到最大迭代次数
- if (times >= this.maxClusterTimes || preCenter.size() < this.K) {
- this.clusterList = clusterList;
- return;
- }
- this.clusteringCenterT = nowCenter;
- //判断质心是否发生移动,若是没有移动,结束本次聚类,不然进行下一轮
- if (isCenterChange(preCenter, nowCenter)) {
- clear(clusterList);
- clustering(nowCenter, times + 1);
- } else {
- this.clusterList = clusterList;
- }
- }
-
- /**
- * @param size
- * @return
- * @Author:lulei
- * @Description: 初始化一个聚类结果
- */
- private List<List<T>> getListT(int size) {
- List<List<T>> list = new ArrayList<List<T>>(size);
- for (int i = 0; i < size; i++) {
- list.add(new ArrayList<T>());
- }
- return list;
- }
-
- /**
- * @param lists
- * @Author:lulei
- * @Description: 清空无用数组
- */
- private void clear(List<List<T>> lists) {
- for (List<T> list : lists) {
- list.clear();
- }
- lists.clear();
- }
-
- /**
- * @param value
- * @Author:lulei
- * @Description: 向模型中添加记录
- */
- public void addRecord(T value) {
- if (dataArray == null) {
- dataArray = new ArrayList<T>();
- }
- dataArray.add(value);
- }
-
- /**
- * @param preT
- * @param nowT
- * @return
- * @Author:lulei
- * @Description: 判断质心是否发生移动
- */
- private boolean isCenterChange(List<T> preT, List<T> nowT) {
- if (preT == null || nowT == null) {
- return false;
- }
- for (T t1 : preT) {
- boolean bol = true;
- for (T t2 : nowT) {
- if (equals(t1, t2)) {//t1在t2中有相等的,认为该质心未移动
- bol = false;
- break;
- }
- }
- //有一个质心发生移动,认为须要进行下一次计算
- if (bol) {
- return bol;
- }
- }
- return false;
- }
-
- /**
- * @param o1
- * @param o2
- * @return
- * @Author:lulei
- * @Description: o1 o2之间的类似度
- */
- public abstract double similarScore(T o1, T o2);
-
- /**
- * @param o1
- * @param o2
- * @return
- * @Author:lulei
- * @Description: 判断o1 o2是否相等
- */
- public abstract boolean equals(T o1, T o2);
-
- /**
- * @param list
- * @return
- * @Author:lulei
- * @Description: 求一组数据的质心
- */
- public abstract T getCenterT(List<T> list);
- }
二维数聚类实现
在算法描述中,介绍了一个200,000个点聚成34个类别的效果图,下面就针对二维坐标数据实现其具体子类。
1、类似度
对于二维坐标的类似度,这里咱们采起两点间聚类的相反数,具体实现以下:
[java] view plain copy
print?

- @Override
- public double similarScore(XYbean o1, XYbean o2) {
- double distance = Math.sqrt((o1.getX() - o2.getX()) * (o1.getX() - o2.getX()) + (o1.getY() - o2.getY()) * (o1.getY() - o2.getY()));
- return distance * -1;
- }
2、样本/质心是否相等
判断样本/质心是否相等只须要判断两点的坐标是否相等便可,具体实现以下:
[java] view plain copy
print?

- @Override
- public boolean equals(XYbean o1, XYbean o2) {
- return o1.getX() == o2.getX() && o1.getY() == o2.getY();
- }
3、获取一个分类下的新质心
对于二维坐标数据,可使用全部点的重心做为分类的质心,具体以下:
[java] view plain copy
print?

- @Override
- public XYbean getCenterT(List<XYbean> list) {
- int x = 0;
- int y = 0;
- try {
- for (XYbean xy : list) {
- x += xy.getX();
- y += xy.getY();
- }
- x = x / list.size();
- y = y / list.size();
- } catch(Exception e) {
-
- }
- return new XYbean(x, y);
- }
4、main方法
对于具体二维坐标的源码这里就再也不贴出来,就是实现前面介绍的抽象类,并实现其中的3个抽象方法,下面咱们就随机产生200,000个点,而后聚成34个类别,具体代码以下:
[java] view plain copy
print?

- public static void main(String[] args) {
-
- int width = 600;
- int height = 400;
- int K = 34;
- XYCluster xyCluster = new XYCluster();
- for (int i = 0; i < 200000; i++) {
- int x = (int)(Math.random() * width) + 1;
- int y = (int)(Math.random() * height) + 1;
- xyCluster.addRecord(new XYbean(x, y));
- }
- xyCluster.setK(K);
- long a = System.currentTimeMillis();
- List<List<XYbean>> cresult = xyCluster.clustering();
- List<XYbean> center = xyCluster.getClusteringCenterT();
- System.out.println(JsonUtil.parseJson(center));
- long b = System.currentTimeMillis();
- System.out.println("耗时:" + (b - a) + "ms");
- new ImgUtil().drawXYbeans(width, height, cresult, "d:/2.png", 0, 0);
- }
对于这随机产生的200,000个点聚成34类,总耗时5485ms。(计算机配置:i5 + 8G内存)