参考文献:百度百科 http://baike.baidu.com
我的算法库:https://github.com/linyiqun/lyq-algorithms-lib
说到聚类算法,大家如果有看过我写的一些关于机器学习的算法文章,一定都这类算法不会陌生,之前将的是划分算法(K均值算法)和层次聚类算法(BIRCH算法),各有优缺点和好坏。本文所述的算法是另外一类的聚类算法,他能够克服BIRCH算法对于形状的限制,因为BIRCH算法偏向于聚簇球形的聚类形成,而dbscan采用的是基于空间的密度的原理,所以可以适用于任何形状的数据聚类实现。
在介绍算法原理之前,先介绍几个dbscan算法中的几个概念定义:
Ε领域:给定对象半径为Ε内的区域称为该对象的Ε领域;
核心对象:如果给定对象Ε领域内的样本点数大于等于MinPts,则称该对象为核心对象;
直接密度可达:对于样本集合D,如果样本点q在p的Ε领域内,并且p为核心对象,那么对象q从对象p直接密度可达。
密度可达:对于样本集合D,给定一串样本点p1,p2….pn,p= p1,q= pn,假如对象pi从pi-1直接密度可达,那么对象q从对象p密度可达。
密度相连:存在样本集合D中的一点o,如果对象o到对象p和对象q都是密度可达的,那么p和q密度相联。
下面是算法的过程(可能说的不是很清楚):
1、扫描原始数据,获取所有的数据点。
2、遍历数据点中的每个点,如果此点已经被访问(处理)过,则跳过,否则取出此点做聚类查找。
3、以步骤2中找到的点P为核心对象,找出在E领域内所有满足条件的点,如果个数大于等于MinPts,则此点为核心对象,加入到簇中。
4、再次P为核心对象的簇中的每个点,进行递归的扩增簇。如果P点的递归扩增结束,再次回到步骤2。
5、算法的终止条件为所有的点都被访问(处理过)。
算法可以理解为是一个DFS的深度优先扩展。
算法的输入Input(格式(x, y)):
2 2 3 1 3 4 3 14 5 3 8 3 8 6 9 8 10 4 10 7 10 10 10 14 11 13 12 8 12 15 14 7 14 9 14 15 15 8
坐标点类Point.java:
package DataMining_DBSCAN; /** * 坐标点类 * * @author lyq * */ public class Point { // 坐标点横坐标 int x; // 坐标点纵坐标 int y; // 此节点是否已经被访问过 boolean isVisited; public Point(String x, String y) { this.x = (Integer.parseInt(x)); this.y = (Integer.parseInt(y)); this.isVisited = false; } /** * 计算当前点与制定点之间的欧式距离 * * @param p * 待计算聚类的p点 * @return */ public double ouDistance(Point p) { double distance = 0; distance = (this.x - p.x) * (this.x - p.x) + (this.y - p.y) * (this.y - p.y); distance = Math.sqrt(distance); return distance; } /** * 判断2个坐标点是否为用个坐标点 * * @param p * 待比较坐标点 * @return */ public boolean isTheSame(Point p) { boolean isSamed = false; if (this.x == p.x && this.y == p.y) { isSamed = true; } return isSamed; } }算法工具类DNSCANTool.java:
package DataMining_DBSCAN; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; /** * DBSCAN基于密度聚类算法工具类 * * @author lyq * */ public class DBSCANTool { // 测试数据文件地址 private String filePath; // 簇扫描半径 private double eps; // 最小包含点数阈值 private int minPts; // 所有的数据坐标点 private ArrayList<Point> totalPoints; // 聚簇结果 private ArrayList<ArrayList<Point>> resultClusters; //噪声数据 private ArrayList<Point> noisePoint; public DBSCANTool(String filePath, double eps, int minPts) { this.filePath = filePath; this.eps = eps; this.minPts = minPts; readDataFile(); } /** * 从文件中读取数据 */ public void readDataFile() { File file = new File(filePath); ArrayList<String[]> dataArray = new ArrayList<String[]>(); try { BufferedReader in = new BufferedReader(new FileReader(file)); String str; String[] tempArray; while ((str = in.readLine()) != null) { tempArray = str.split(" "); dataArray.add(tempArray); } in.close(); } catch (IOException e) { e.getStackTrace(); } Point p; totalPoints = new ArrayList<>(); for (String[] array : dataArray) { p = new Point(array[0], array[1]); totalPoints.add(p); } } /** * 递归的寻找聚簇 * * @param pointList * 当前的点列表 * @param parentCluster * 父聚簇 */ private void recursiveCluster(Point point, ArrayList<Point> parentCluster) { double distance = 0; ArrayList<Point> cluster; // 如果已经访问过了,则跳过 if (point.isVisited) { return; } point.isVisited = true; cluster = new ArrayList<>(); for (Point p2 : totalPoints) { // 过滤掉自身的坐标点 if (point.isTheSame(p2)) { continue; } distance = point.ouDistance(p2); if (distance <= eps) { // 如果聚类小于给定的半径,则加入簇中 cluster.add(p2); } } if (cluster.size() >= minPts) { // 将自己也加入到聚簇中 cluster.add(point); // 如果附近的节点个数超过最下值,则加入到父聚簇中,同时去除重复的点 addCluster(parentCluster, cluster); for (Point p : cluster) { recursiveCluster(p, parentCluster); } } } /** * 往父聚簇中添加局部簇坐标点 * * @param parentCluster * 原始父聚簇坐标点 * @param cluster * 待合并的聚簇 */ private void addCluster(ArrayList<Point> parentCluster, ArrayList<Point> cluster) { boolean isCotained = false; ArrayList<Point> addPoints = new ArrayList<>(); for (Point p : cluster) { isCotained = false; for (Point p2 : parentCluster) { if (p.isTheSame(p2)) { isCotained = true; break; } } if (!isCotained) { addPoints.add(p); } } parentCluster.addAll(addPoints); } /** * dbScan算法基于密度的聚类 */ public void dbScanCluster() { ArrayList<Point> cluster = null; resultClusters = new ArrayList<>(); noisePoint = new ArrayList<>(); for (Point p : totalPoints) { if(p.isVisited){ continue; } cluster = new ArrayList<>(); recursiveCluster(p, cluster); if (cluster.size() > 0) { resultClusters.add(cluster); }else{ noisePoint.add(p); } } removeFalseNoise(); printClusters(); } /** * 移除被错误分类的噪声点数据 */ private void removeFalseNoise(){ ArrayList<Point> totalCluster = new ArrayList<>(); ArrayList<Point> deletePoints = new ArrayList<>(); //将聚簇合并 for(ArrayList<Point> list: resultClusters){ totalCluster.addAll(list); } for(Point p: noisePoint){ for(Point p2: totalCluster){ if(p2.isTheSame(p)){ deletePoints.add(p); } } } noisePoint.removeAll(deletePoints); } /** * 输出聚类结果 */ private void printClusters() { int i = 1; for (ArrayList<Point> pList : resultClusters) { System.out.print("聚簇" + (i++) + ":"); for (Point p : pList) { System.out.print(MessageFormat.format("({0},{1}) ", p.x, p.y)); } System.out.println(); } System.out.println(); System.out.print("噪声数据:"); for (Point p : noisePoint) { System.out.print(MessageFormat.format("({0},{1}) ", p.x, p.y)); } System.out.println(); } }测试类Client.java:
package DataMining_DBSCAN; /** * Dbscan基于密度的聚类算法测试类 * @author lyq * */ public class Client { public static void main(String[] args){ String filePath = "C:\\Users\\lyq\\Desktop\\icon\\input.txt"; //簇扫描半径 double eps = 3; //最小包含点数阈值 int minPts = 3; DBSCANTool tool = new DBSCANTool(filePath, eps, minPts); tool.dbScanCluster(); } }算法的输出:
聚簇1:(2,2) (3,4) (5,3) (3,1) (8,3) (8,6) (10,4) (9,8) (10,7) (10,10) (12,8) (14,7) (14,9) (15,8) 聚簇2:(10,14) (11,13) (14,15) (12,15) 噪声数据:(3,14)图示结果如下:
dbscan虽说可以用于任何形状的聚类发现,但是对于密度分布不均衡的数据,变化比较大,分类的性能就不会特别好,还有1点是不能反映高尺寸数据。