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-08-14 06:55:49

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

Weka算法Clusterers-Xmeans源码分析

</pre><p></p><p><span style="font-size:18px">上几篇博客都是分析的分类器算法(有监督学习),这次就分析一个聚类算法(无监督学习).</span></p><p><span style="font-size:18px"></span></p><p><span style=&quo

Weka算法Clusterers-DBSCAN源码分析

如果说世界上只能存在一种基于密度的聚类算法的话,那么它必须是DBSCAN(Density-based spatial clustering of applications with noise).DBSCAN作为基于密度聚类算法的典型,相对于Kmeans,最大优点是可以自己决定聚类数量,同时可以过滤一些噪点,但相对的,对传入的参数较为敏感,并且参数调优全靠经验. 一.算法 对于算法部分只做一些"感性"的分析,具体算法的理论证明以及更精确的形式化描述参考Wiki:http://en.wi

Weka算法Classifier-meta-AdditiveRegression源码分析

博主最近迷上了打怪物猎人,这片文章拖了很久才开始动笔 一.算法 AdditiveRegression,换个更出名一点的叫法可以称作GBDT(Grandient Boosting Decision Tree)梯度下降分类树,或者GBRT(Grandient Boosting Regression Tree)梯度下降回归树,是一种多分类器组合的算法,更确切的说,是属于Boosting算法. 谈到Boosting算法,就不能不提AdaBoost,参见之前我写的博客,可以看到AdaBoost的核心是级联

Weka算法Classifier-tree-RandomForest源码分析(二)代码实现

RandomForest的实现异常的简单,简单的超出博主的预期,Weka在实现方式上组合了Bagging和RandomTree. 一.RandomForest的训练 构建RandomForest的代码如下: public void buildClassifier(Instances data) throws Exception { // can classifier handle the data? getCapabilities().testWithFail(data); // remove

Weka算法Classifier-tree-J48源码分析(三)ModelSelection

ModelSelection主要是用于选择合适的列对数据集进行分割,结合上一篇J48的主流程,发现用到的ModelSelection有 C45ModelSelection以及BinC45ModelSelection,先来分析C45ModelSelection. 一.C45ModelSelection 首先作为一个ModelSelection接口,实现的主要方法有两个,分别是selectModel(Instances)和selectionModel(Instances,Instances).C45

Weka算法Classifier-meta-AdaBoostM1源码分析(一)

多分类器组合算法简单的来讲常用的有voting,bagging和boosting,其中就效果来说Boosting略占优势,而AdaBoostM1算法又相当于Boosting算法的"经典款". Voting思想是使用多分类器进行投票组合,并按照少数服从多数(大多数情况)来决定最终的分类,缺点是少数服从多数的规则往往只能避免达到最差的情况却也很难达到最少的情况. Bagging思想是有放回的随机抽样来训练多个分类器,最后使用voting来进行投票决策,经典算法如RandomForest(之

Weka算法Classifier-trees-REPTree源码分析(一)

一.算法 关于REPTree我实在是没找到什么相关其算法的资料,或许是Weka自创的一个关于决策树的改进,也许是其它某种决策树方法的别名,根据类的注释:Fast decision tree learner. Builds a decision/regression tree using information gain/variance and prunes it using reduced-error pruning (with backfitting).  Only sorts values

Weka算法Classifier-meta-Bagging源码分析

Bagging部分比较简单,算法和代码放到一起写了. 一.Bagging算法 严格来看Bagging并不能算是一种分类算法,Bagging和Boosting一样,是一种组合基本分类器的方法,也就是使用多个基分类器来获取更为强大的分类器,其核心思想是有放回的抽样. Bagging算法的训练流程: 1.从样本集中有放回的抽样M个样本. 2.用这M个样本训练基分类器C. 3.重复这个过程X次,得到若干个基分类器. Bagging算法的预测流程: 1.对于新传入实例A,用这X个新分类器得到一个分类结果的

Weka算法Classifier-meta-AdaBoostM1源码分析(二)

三.基分类器 AdaBoostM1使用的默认基分类器是weka.classifiers.trees.DecisionStump,名字直译过来就是决策桩(这什么名字?!),其分类方法类似于ID3算法的节点分裂算法,如果是枚举型的,遍历所有属性,选出其中一个属性,使使用该属性进行分类后的熵增益最大,如果是数值型的,选择一个节点做二分,使分类后方差最小. 但和决策树不同的是,并不做递归的树生长,只做一次节点选择并分裂(所以叫桩而不是树). 这个基分类器大体思路就是这样,代码较简单并且没有什么巧妙的算法

Weka算法Classifier-tree-J48源码分析(四)总结

一.ClassifyInstance 首先先说一下构造好的分类树是如何对一个新的Instance进行区分. 直观上,会对树进行一个检索,从根节点根据属性的不同,最终走到叶子节点,得到具体的分类. 但Weka在实现上,是遍历了这个Instance属于不同的class的可能性,并从中选出了一个最大的,代码如下: public double classifyInstance(Instance instance) throws Exception { double maxProb = -1; doubl