Weka算法Clusterers-DBSCAN源码分析

如果说世界上只能存在一种基于密度的聚类算法的话,那么它必须是DBSCAN(Density-based spatial clustering of applications with noise)。DBSCAN作为基于密度聚类算法的典型,相对于Kmeans,最大优点是可以自己决定聚类数量,同时可以过滤一些噪点,但相对的,对传入的参数较为敏感,并且参数调优全靠经验。

一、算法

对于算法部分只做一些”感性“的分析,具体算法的理论证明以及更精确的形式化描述参考Wiki:http://en.wikipedia.org/wiki/DBSCAN

DBSCAN算法相对于简单,只要弄清几个概念,算法本身是水到渠成的。

(1)几个变量

领域半径e,最小数目minOpt

(2)几个名词

核心对象:若一个对象其领域半径e内的对象数量大于等于minOpt,则称该对象为核心对象。

直接密度可达:若一个核心对象p,其领域半径内有若干点q,则对于每一个q有q从对象p直接密度可达。

(3)算法流程

主流程:输入e,minOpt以及对象集合n

I、找到一个未标记的核心对象k,并设此对象为已标记,若找不到核心对象直接退出

II、扩展此核心对象,expand(k)

III、若所有对象均已标记,则退出,否则转I

expand流程:输入核心对象k

I、初始化一个集合S,放入k

II、遍历该集合元素,对于集合中每一个核心对象,找到其所有未标记的的密度可达对象,放入集合S,并设为已标记

III、若II没有加入任何新对象,则退出,否则转II

在分析Weka的实现时,除了代码本身,着重关心以下几点:

(1)是否使用了特殊的数据结构来提高效率

(2)缺失值的处理

(3)噪声的处理

(4)其它实现技巧

(5)和原始DBSCAN不同之处

二、SequentialDatabase类

在分析具体的buildClusterer方法之前,先分析SequentialDatabase类,该类是DBSCAN方法用到的一个辅助类,封装一个instance并暴露一些定制的查询操作。

(1)epsilonRangeQuery,该函数用于查找离一个给定对象queryDataObject距离epsilon之内的所有对象

public List epsilonRangeQuery(double epsilon, DataObject queryDataObject) {
        ArrayList epsilonRange_List = new ArrayList();
        Iterator iterator = dataObjectIterator();
        while (iterator.hasNext()) {
            DataObject dataObject = (DataObject) iterator.next();
            double distance = queryDataObject.distance(dataObject);//默认的,距离计算器是欧式距离
            if (distance < epsilon) {
                epsilonRange_List.add(dataObject);
            }
        }

        return epsilonRange_List;
    }

可以看出该函数遍历了所有的对象,因此时间复杂度为O(n)

(2)返回一个List,其中Index0是距离最近的k个对象,index1是小于epsilon距离的对象

 public List k_nextNeighbourQuery(int k, double epsilon, DataObject dataObject) {
        Iterator iterator = dataObjectIterator();

        List return_List = new ArrayList();
        List nextNeighbours_List = new ArrayList();
        List epsilonRange_List = new ArrayList();

        PriorityQueue priorityQueue = new PriorityQueue();

        while (iterator.hasNext()) {
            DataObject next_dataObject = (DataObject) iterator.next();
            double dist = dataObject.distance(next_dataObject);

            if (dist <= epsilon) epsilonRange_List.add(new EpsilonRange_ListElement(dist, next_dataObject));

            if (priorityQueue.size() < k) {
                priorityQueue.add(dist, next_dataObject);
            } else {
                if (dist < priorityQueue.getPriority(0)) {
                    priorityQueue.next(); //把最大距离的移除,来实现一个固定长度的队列
                    priorityQueue.add(dist, next_dataObject);
                }
            }
        }

        while (priorityQueue.hasNext()) {
            nextNeighbours_List.add(0, priorityQueue.next());//将优先队列写到list中,每次都添加到index0可以看出这个List是个升序list。
        }

        return_List.add(nextNeighbours_List);
        return_List.add(epsilonRange_List);
        return return_List;
    }

这个函数的设计必须吐槽:第一基于约定的编程,约定了Index0和index1的数据,并且还约定了其中的list所存储的对象,还约定了优先队列中元素升序排列,使得这个函数重用性及其之低。第二和epsilonRangeQuery相比有部分重复的地方(但又不能调用epsilonRangeQuery,因为调用了相当于所有对象遍历两次)。

(3)coreDistance,该函数不仅返回了上面函数的list,还添加了index3为离得最远的并且小于epsilon的对象。

public List coreDistance(int minPoints, double epsilon, DataObject dataObject) {
        List list = k_nextNeighbourQuery(minPoints, epsilon, dataObject);

        if (((List) list.get(1)).size() < minPoints) {
            list.add(new Double(DataObject.UNDEFINED));
            return list;
        } else {
            List nextNeighbours_List = (List) list.get(0);
            PriorityQueueElement priorityQueueElement =
                    (PriorityQueueElement) nextNeighbours_List.get(nextNeighbours_List.size() - 1);
            if (priorityQueueElement.getPriority() <= epsilon) {
                list.add(new Double(priorityQueueElement.getPriority()));
                return list;
            } else {
                list.add(new Double(DataObject.UNDEFINED));
                return list;
            }
        }
    }

三、buildClusterer

接着从buildClusterer说起,该函数是所有聚类器的入口,用于使用已知样本训练一个聚类器。

函数本身是比较简单的。

  public void buildClusterer(Instances instances) throws Exception {
        // 先测一下这个Instance能否用dbscan进行聚类,dbscan几乎可处理所有的类型(枚举、日期、数值、missingValue)
        getCapabilities().testWithFail(instances);

        long time_1 = System.currentTimeMillis();

        processed_InstanceID = 0;
        numberOfGeneratedClusters = 0;
        clusterID = 0;

        replaceMissingValues_Filter = new ReplaceMissingValues();
        replaceMissingValues_Filter.setInputFormat(instances);
        Instances filteredInstances = Filter.useFilter(instances, replaceMissingValues_Filter);

        database = databaseForName(getDatabase_Type(), filteredInstances);
        for (int i = 0; i < database.getInstances().numInstances(); i++) {
            DataObject dataObject = dataObjectForName(getDatabase_distanceType(),
                    database.getInstances().instance(i),
                    Integer.toString(i),
                    database);
            database.insert(dataObject);//插入到数据库
        }
        database.setMinMaxValues();

        Iterator iterator = database.dataObjectIterator();
        while (iterator.hasNext()) {//对于所有节点进行迭代并不是最高效的,如果使用一个变量记录当前unclassfied的数量,当为0的时候直接退出更为高效一些,虽然时间复杂度没有变化。
            DataObject dataObject = (DataObject) iterator.next();
            if (dataObject.getClusterLabel() == DataObject.UNCLASSIFIED) {
                if (expandCluster(dataObject)) {//如果某个点未标记,则尝试进行扩展
                    clusterID++;
                    numberOfGeneratedClusters++;
                }
            }
        }

        long time_2 = System.currentTimeMillis();
        elapsedTime = (double) (time_2 - time_1) / 1000.0;//很奇怪,weka的实现具有不同的编程风格,起码以往的聚类器或者分类器,并没有直接在训练函数中来计算所用时间。
    }

四、expandCluster

扩展核心节点为一个簇的主函数,若成功扩展返回true,否则返回false,如下:

private boolean expandCluster(DataObject dataObject) {
        List seedList = database.epsilonRangeQuery(getEpsilon(), dataObject);//该函数寻找给定对象距离epsilon以内的对象
        if (seedList.size() < getMinPoints()) {
            dataObject.setClusterLabel(DataObject.NOISE);//如果是非核心对象,暂时设置为noise,之后如果不能被核心对象聚类到就一直是noise了。
            return false;
        }

        //走到这里都是核心对象
        for (int i = 0; i < seedList.size(); i++) {
            DataObject seedListDataObject = (DataObject) seedList.get(i);
            seedListDataObject.setClusterLabel(clusterID);//所有seedList里的对象都从属于clusterID,这个clusterID是一个自增量
            if (seedListDataObject.equals(dataObject)) {
                seedList.remove(i);//注意epsilonRangeQueryList会把参数对象本身也放进去,所以这里要移除
                i--;
            }
        }

        for (int j = 0; j < seedList.size(); j++) {
            DataObject seedListDataObject = (DataObject) seedList.get(j);
            List seedListDataObject_Neighbourhood = database.epsilonRangeQuery(getEpsilon(), seedListDataObject);
           //对于seedList中每一个元素都寻找其领域内的元素
            if (seedListDataObject_Neighbourhood.size() >= getMinPoints()) {
                for (int i = 0; i < seedListDataObject_Neighbourhood.size(); i++) {//走到这个循环内说明是核心对象
                    DataObject p = (DataObject) seedListDataObject_Neighbourhood.get(i);
                    if (p.getClusterLabel() == DataObject.UNCLASSIFIED || p.getClusterLabel() == DataObject.NOISE) {<span style="white-space:pre">		</span>
                        if (p.getClusterLabel() == DataObject.UNCLASSIFIED) {
                            seedList.add(p);//如果是未分类的,就加到seedList中,这里使用了unclassified来保证不会添加重复,而且nosie不添加是因为noise肯定不是核心对象(本函数开头逻辑保证)这也算是一个trick,使用了一个list加下标起到了set的效果,如果让我来实现估计我会直接用set吧
                        }
                        p.setClusterLabel(clusterID);//设置成相应的聚类
                    }
                }
            }
            seedList.remove(j);//不是很明白这里为啥要remove,按理说遍历之后不会再访问没必要删除了,或许为了节省内存,也或许是作者强迫症(这段代码的作者貌似不喜欢用迭代器,并且多次使用基于下标的删除,在java中这并不是一个很优雅的编程方式,虽然我也经常这么用)
            j--;
        }

        return true;
    }

五、时间复杂度分析

buildClusterer函数主循环为n,expandCluster函数对list中每个元素调用eplisonRangeQuery,因此是n^2,总结来看是整个算法是n^3,并不是很高效。

优化点:

buildClusterer并不能产生优于O(n)的优化,但可以使用计数器记录未标记的数量来提高一些效率,expandCluster也没什么优化点,但eplisonRangeQuery起码有两个地方可以优化,第一个是使用KDTree(就像Xmean算法一样,参见之前的博客)来更有效寻找离给定点距离最近的距离,其次是使用Cache来缓存一些给定点对的距离,因为考虑到相同的点在程序中其实是被计算了多次的。

六、clusterInstance

这个函数接收一个instance作为参数,理应返回该instance从属的cluster,但DBSCAN貌似并没有这么做。

    public int clusterInstance(Instance instance) throws Exception {
        if (processed_InstanceID >= database.size()) processed_InstanceID = 0;
        int cnum = (database.getDataObject(Integer.toString(processed_InstanceID++))).getClusterLabel();
        if (cnum == DataObject.NOISE)
            throw new Exception();
        else
            return cnum;
    }

依次返回的是id为0,1,2的用例的下标,不知道这么做的用意何在。

而且如果是个noise直接抛出异常,并且根本就不说明为啥抛这个异常。

整个函数意义不明。

七、总结

如果非要写个总结的话,那么我个人对于这段代码是比较失望的,无论是一些函数抽象的设计,数据结构的设计,Java代码风格,都有一种浓浓的”业余“的味道,和之前分类器整洁的代码相比完全是判若两人(好吧本来也不是一个人写的)。

除此之外最后的clusterInstance的行为和注释完全不符,不知道是个bug还是feature还是其它什么原因导致的。

时间: 2024-10-14 04:52:17

Weka算法Clusterers-DBSCAN源码分析的相关文章

OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波

http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 2013-03-23 17:44 16963人阅读 评论(28) 收藏 举报 分类: 机器视觉(34) 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] KAZE系列笔记: OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 OpenCV学习笔记(28)KA

K-近邻算法的Python实现 : 源码分析

网上介绍K-近邻算法的例子很多,其Python实现版本基本都是来自于机器学习的入门书籍<机器学习实战>,虽然K-近邻算法本身很简单,但很多初学者对其Python版本的源代码理解不够,所以本文将对其源代码进行分析. 什么是K-近邻算法? 简单的说,K-近邻算法采用不同特征值之间的距离方法进行分类.所以它是一个分类算法. 优点:无数据输入假定,对异常值不敏感 缺点:复杂度高 好了,直接先上代码,等会在分析:(这份代码来自<机器学习实战>) def classify0(inx, data

OpenStack_Swift源码分析——Ring的rebalance算法源代码详细分析

今天有同学去百度,带回一道面试题,和大家分享一下: 打印: n=1 1 n=2 3 3 2 4 1 1 4 5 5 n=3 7 7 7 7 6 8 3 3 2 6 8 4 1 1 6 8 4 5 5 5 8 9 9 9 9 提供一段参考程序: <pre name="code" class="cpp">// ConsoleApplication1.cpp: 主项目文件. #include "stdafx.h" #include &quo

Openck_Swift源码分析——增加、删除设备时算法具体的实现过程

1 初始添加设备后.上传Object的具体流程 前几篇博客中,我们讲到环的基本原理即具体的实现过程,加入我们在初始创建Ring是执行如下几条命令: ?swift-ring-builder object.builder create 5 3 1 ?swift-ring-builder object.builder add z1-127.0.0.1:6010/sdb1 100 ?swift-ring-builder object.builder add z2-127.0.0.1:6020/sdb2 

WEKA学习——CSVLoader 实例训练 和 源码分析

简介: Weka支持多种数据导入方式,CSVLoader是能从csv文件加载数据集,也可以保存为arff格式文件.官方介绍文件:Converting CSV to ARFF ( http://weka.wikispaces.com/Converting+CSV+to+ARFF) CSVLoader加载文件,关键是对文件字段属性名称和属性的类型需要自己定义,这样才能得到满足自己需要的数据集. CSVLoader通过options设置,可以设置每一列的属性为Nominal,String,Date类型

OpenStack_Swift源码分析——Ring基本原理及一致性Hash算法

1.Ring的基本概念 Ring是swfit中最重要的组件,用于记录存储对象与物理位置之间的映射关系,当用户需要对Account.Container.Object操作时,就需要查询对应的Ring文件(Account.Container.Object都有自己对应的Ring),Ring 使用Region(最近几个版本中新加入的).Zone.Device.Partition和Replica来维护这些信息,对于每一个对象,根据你在部署swift设置的Replica数量,集群中会存有Replica个对象.

SURF算法与源码分析、下

上一篇文章 SURF算法与源码分析.上 中主要分析的是SURF特征点定位的算法原理与相关OpenCV中的源码分析,这篇文章接着上篇文章对已经定位到的SURF特征点进行特征描述.这一步至关重要,这是SURF特征点匹配的基础.总体来说算法思路和SIFT相似,只是每一步都做了不同程度的近似与简化,提高了效率. 1. SURF特征点方向分配 为了保证特征矢量具有旋转不变性,与SIFT特征一样,需要对每个特征点分配一个主方向.为些,我们需要以特征点为中心,以$6s$($s = 1.2 *L /9$为特征点

Mahout源码分析:并行化FP-Growth算法

FP-Growth是一种常被用来进行关联分析,挖掘频繁项的算法.与Aprior算法相比,FP-Growth算法采用前缀树的形式来表征数据,减少了扫描事务数据库的次数,通过递归地生成条件FP-tree来挖掘频繁项.参考资料[1]详细分析了这一过程.事实上,面对大数据量时,FP-Growth算法生成的FP-tree非常大,无法放入内存,挖掘到的频繁项也可能有指数多个.本文将分析如何并行化FP-Growth算法以及Mahout中并行化FP-Growth算法的源码. 1. 并行化FP-Growth 并行

Mahout源码分析-K-means聚类算法

一 算法描述 1.随机选取k个对象作为初始簇中心: 2.计算每个对象到簇中心的距离,将每个对象聚类到离该对象最近的聚簇中去: 3.计算每个聚簇中的簇均值,并将簇均值作为新的簇中心: 4.计算准则函数: 5.重复(2).(3)和(4),直到准则函数不再发生变化. 二 源码分析 Mahout源码分析-K-means聚类算法

【E2LSH源码分析】LSH算法框架分析

位置敏感哈希(Locality Sensitive Hashing,LSH)是近似最近邻搜索算法中最流行的一种,它有坚实的理论依据并且在高维数据空间中表现优异.由于网络上相关知识的介绍比较单一,现就LSH的相关算法和技术做一介绍总结,希望能给感兴趣的朋友提供便利,也希望有兴趣的同道中人多交流.多指正. 1.LSH原理 最近邻问题(nearest neighbor problem)可以定义如下:给定n个对象的集合并建立一个数据结构,当给定任意的要查询对象时,该数据结构返回针对查询对象的最相似的数据