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还是其他什么原因导致的。
Weka算法Clusterers-DBSCAN源代码分析的更多相关文章
- STL源代码分析——STL算法remove删除算法
前言 因为在前文的<STL算法剖析>中,源代码剖析许多.不方便学习,也不方便以后复习,这里把这些算法进行归类.对他们单独的源代码剖析进行解说.本文介绍的STL算法中的remove删除算法. ...
- STL源代码分析——STL算法merge合并算法
前言 因为在前文的<STL算法剖析>中.源代码剖析许多.不方便学习.也不方便以后复习,这里把这些算法进行归类.对他们单独的源代码剖析进行解说.本文介绍的STL算法中的merge合并算法. ...
- STL源代码分析——STL算法sort排序算法
前言 因为在前文的<STL算法剖析>中,源代码剖析许多,不方便学习,也不方便以后复习.这里把这些算法进行归类,对他们单独的源代码剖析进行解说.本文介绍的STL算法中的sort排序算法,SG ...
- 变动性算法源代码分析与使用示例(copy_backward、 transform、 replace_copy_if 等)
首先回顾前面的文章,我们把for_each 归类为非变动性算法,实际上它也可以算是变动性算法,取决于传入的第三个参数,即函数 指针.如果在函数内对容器元素做了修改,那么就属于变动性算法. 变动性算法源 ...
- OpenStack_Swift源代码分析——Ring的rebalance算法源代码具体分析
1 Command类中的rebalnace方法 在上篇文章中解说了,创建Ring已经为Ring加入设备.在加入设备后须要对Ring进行平衡,平衡 swift-ring-builder object.b ...
- K-近邻算法的Python实现 : 源代码分析
网上介绍K-近邻算法的样例非常多.其Python实现版本号基本都是来自于机器学习的入门书籍<机器学习实战>,尽管K-近邻算法本身非常easy,但非常多刚開始学习的人对其Python版本号的 ...
- Openck_Swift源代码分析——添加、删除设备时算法详细的实现过程
1 初始加入设备后.上传Object的详细流程 前几篇博客中,我们讲到环的基本原理即详细的实现过程,加入我们在初始创建Ring是执行例如以下几条命令: •swift-ring-builder obj ...
- redis 源代码分析(一) 内存管理
一,redis内存管理介绍 redis是一个基于内存的key-value的数据库,其内存管理是很重要的,为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中 ...
- Raid1源代码分析--开篇总述
前段时间由于一些事情耽搁了,最近将raid1方面的各流程整理了一遍.网上和书上,能找到关于MD下的raid1的文档资料比较少.决定开始写一个系列的关于raid1的博客,之前写过的一篇读流程也会在之后加 ...
- Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6664554 在上一文章Android系统匿名共 ...
随机推荐
- HTML5和CSS3实例教程[总结一]
关于onclick的行为与内容分离 通过链接触发弹出窗口方式 (不推荐使用此方法!!!) <a href='#' onclcik = "window.open('holiday_pay ...
- linux的sudo apt-get install 和dpkg -i <package.deb>命令
ubuntu统一的安装软件命令 sudo apt-get install ** sudo dpkg -i <package.deb>
- EF中使用Contains方法
第一种情况 var db=new ECEntities(); var list=new []{"8","9"}; var result=from a in db ...
- Objective-C 笔记 字符串操作
这次总结下OC里一些对字符串的一些操作. 创建字符串对象时,会创建一个内容不可更改的对象,称为不可变对象.可以使用NSString类处理不可变字符串.你经常需要处理字符串并更改字符串中的字符.例如,可 ...
- C/C++默认浮点型
代码: #include <iostream> #include <cstdio> using namespace std; void test(int a){ cout< ...
- 【C++学习之路】派生类的构造函数(一)
一.简单派生类的构造函数 1.所谓简单派生类,就是指派生类中不包含基类的内嵌对象的派生类. 2.一般来说,这样的派生类的构造函数的形式是: student( int i, string nam, in ...
- STL 之 vector 用法
一.头文件 #include<vector> 二.常用方法: // 在这个向量的尾部插入x的考贝,平均时间为常数,最坏时间为O(n): 1: void push_back(const T& ...
- CentOS5.5下安装Ant
从yum源直接下ant: [root@master local]$ yum install ant 运行ant,发现报错: java.lang.NoClassDefFoundError: org/ap ...
- 数据库 数据库SQL语句五
集合运算 union 并集(两个集合如果有重复部分,那么只显示一次重复部分) union all 并集(两个集合如果有重复部分,那么重复部分显示两次) intersect 交集 minus 差集 -- ...
- android 写文件权限
首先,在manifest.xml中添加user permission:<uses-permission android:name="android.permission.WRITE_E ...