您的位置:首页 > 编程语言 > Java开发

模式识别ID3算法实现

2014-04-25 22:01 197 查看
原创作品,出自 “晓风残月xj” 博客,欢迎转载,转载时请务必注明出处(http://blog.csdn.net/xiaofengcanyuexj)。

由于各种原因,可能存在诸多不足,欢迎斧正!
一、决策树ID3算法描述

     
 决策树算法是一种逼近离散函数值的方法。它是一种典型的分类方法,首先对数据进行处理,利用归纳算法生成可读的规则和决策树,然后使用决策对新数据进行分析。本质上决策树是通过一系列规则对数据进行分类的过程。决策树算法中最简单、最经典的就是ID3算法了。下面我从程序员的角度谈谈我对决策树算法的基本理解。在进行节点分裂的过程中,按照一定的贪心策略自顶向下的递归建树。每步使用统计测试来具体确定每个属性单独分类训练样例的能力(ID3算法使用信息熵增量information gain),选择其中分类能力最好的属性作为当前更节点的分类标准;然后按这个属性的可能取值产生分支,把训练样例划分到适当的分支子树中;重复上述过程直到满足某个结束条件。可见ID3形成对决策树的贪心建树,即不回溯重新考虑当前的选择。总的来说,ID3算法是基于信息熵增量最大这一贪心策略进行自适应递归建树的,在建树过程中采用不回溯方法简化算法。决策树ID3算法的优缺点比较明显。优点:算法的理论清晰,方法简单,实现容易,学习能力较强;缺点:只对比较小的数据集有效,对噪声比较敏感,且容易选择属性取值相对较少的属性。

 

二、weka平台ID3算法实现的不足与我的改进思想

         站在一个程序员的角度,追求的是程序的简单高效,我发现weka平台上的决策树算法实现有很多不可取之处,这些不可取之处带来的后果就是增加了算法的时间和空间复杂度或者在相同的时间和空间复杂度内常数数量级增大很多,导致程序跑起来很慢,且在计算机内存消耗殆尽的极限情况下会导致整个程序的崩溃。

     下面我谈谈我发现的不足之处与自己的改进方法:

1.每次分裂时都直接将父亲节点的数据集本身按分裂属性的可能取值划分到对应的子树中

     直接复制数据集很费内存,若一棵树的深度k,原始数据集数据量为n, 每个数据的维数为m,当数据本身为大数据时,m可能很大,则整棵决策树总的空间复杂度为O(n*k*m)(最坏情况为深度为k的满树,即所有叶子节点都在最下面一层,此时每一层为整个数据集的一个划分)。

   (1).n为数据规模,如果数据量足够大,可以采用分批导入内存的方法,但weka平台上关于数据集的读入操作对学习者来说是透明的,这点在没看数据导入实现机制的底层代码是很难采用分批导入内存的,所以不在这上做优化

   (2).k为按照既定决策树算法ID3求得的树的深度,这点可以结合老师课堂讲授的决策树算法来优化,如4.5,4.8算法, 这里很多同学都做过而且weka平台上有现成代码,我认为从理论上发明新的可以明显缩短决策树深度的算法是绝对有效的,效果立竿见影,但对于初学者的我,理论知识储备不足,很难找到一个摒弃经验避免泛泛而谈而是上升到理论层面具有普适性的算法,这点老师可以做到,我就不再班门弄斧了。我将从一个程序员的角度谈谈如何优化算法的空间复杂度,当然也是从算法实现角度立足细节来展开的。

   (3).m为维数,在数据比较复杂时这才是影响算法空间效率的重要因素。试想一下m为10,100,1000…则相应的算法的空间消耗要提高10倍,100倍,1000倍…。在所有节点上都开辟内存储存相应的数据集是很消耗内存的,如何优化呢?可以在所有节点上只存储对应数据集的在原始数据集中的索引,通过索引确定哪些数据被划分到相应的孩子节点,这是绝对可行的。 这样一来算法的空间复杂度有效的降为O(n*k),完全可以回避数据集中数据维数过大导致内存不足的风险,提高程序的健壮性与鲁棒性。而且通过下标索引来查找相应元素、划分元素到相应子树比直接操作整个元素要快,因为我们直接操作的是一个索引(很有可能就是一个int型数据),而不是维数很大的具有复杂结构的自定义对象。无需参与与对象初始化有关的构造函数、 拷贝构造函数、赋值函数等的编写,简化编程;而且对象的实例化要用到new 操作符,无论什么编程语言期都是相当消耗计算机资源的。能在算法的执行时间和占用空间上都有优化,何乐而不为?

2.每次分裂时在选择信息熵增量最大的属性时都在重复试探数据集上的所有属性

     在原weka平台上,不同层次节点求信息熵增量对应的试探属性完全相同,我认为这是不可取的。举个简单例子:

AttributeSet={attr1,attr2,attr3,attr4}

假设属性集中的attr1,attr2属性已经作为了祖先节点的分裂属性,此时就完全可以不用再次试探attr1,attr2了。尽管attr1,attr2由于当前节点在该属性上信息增量为0,不可能再次作为分裂属性,但在计算attr1,attr2对应的分裂属性时会耗费大量的CPU资源且完全是在做无用功。如果不同层次保留的属性集不一样,即后代节点保留的属性集应该剔除已经在祖先节点上作为分类依据的属性,就可以很好的优化程序。即便优化属性集,关于节点上属性集的存储问题也有好有坏,大致有2种想法:

(1)、直接存储尚未选中的属性。此方法会带来时间与空间资源的消耗。若属性很多且单个属性取值很多外加单个属性取值占用存储空间大,则直接存储属性是灾难性的,存储、赋值、复制等操作都会很麻烦,拉大了算法的平摊时间复杂度。而weka平台上ID3算法实现恰恰是这么做—因为其程序保留的是数据集,我认为是不可取的。

(2)、开辟整型数组存储属性的索引。对于每个节点MyID3,开辟整型数组

   private int[] attributeOfNotUsedIndex;

    可以作为当前节点剩余分裂属性的属性索引。每次从中通过索引选择信息增量最大的属性作为分类属性,然后将除去该属性的剩余属性索引复制到孩子节点。这样做简单方便,效率高。何乐而不为?

 

3.关于循环不变量的外移与不外移

       在原weka平台上,ID3算法实现时试图将循环不变量外移,这点是可取的。下面是weka平台上的一段程序片段(摘自ID3中函(private Instances[]splitData(Instances data, Attribute att)):

int numInstances=data.numInstances();

for (int i=0;i<numInstances;i++){

int attVal=(int)data.instance(i).value(att);

splitData[attVal].add(data.instance(i));

}

 

           上述例子尝试将循环不变量data.numInstances()外移,这点在数据集data不会动态变化时是与下面程序等价的—因为决策树ID3算法中作为训练集的数据集在一次建立分类器决策树时肯定是不变的。

for (int i=0;i<data.numInstances();i++){

int attVal=(int)data.instance(i).value(att);

splitData[attVal].add(data.instance(i));

}

程序片段3.2

 

           由于程序片段3.1将data.numInstances()外移,避免了每次都要调用函数计算数据集大小,节省了函数调用的时间与空间开销。但是,在其他有些地方程序却没有将循环不变量外移,尤其是在寻找当前节点信息熵增量最大值对应属性时,没有将需要耗费大量CPU资源的计算当前节点信息熵过程外移。当当前节点对应的数据集很大时(数据多,单个数据维数多(对应属性多)),此时计算一次信息熵是要耗费很多计算机资源的。如当前节点数据集规模为n,属性个数为m,若不外移,则对应每个属性都要求一遍父亲节点信息熵;如外移只需求一次进行预处理,一次计算多次使用,避免每次使用都要计算。虽然对应寻找信息熵增量最大的算法时间复杂度没有得到改善,都为O(n*m),但在常数层面上却带来数量级的优化,且随着数据集规模和单个数据的复杂程度增大这种优化作用也随之放大。何乐而不为?

 

三、改进前后运行结果对比分析

      通过数据可以发现,由于没有对程序的原理进行改进,改进前后的ID3算法程序跑出来的结果完全相同,但是在算法的运行时间上却有很大的区别。

数据audiology.arff(Test-model:10-fold cross-validation)

 

数据集规模

属性数

 建模时间

正确样本

正确分类率

错误样本

错误分类率

原ID3算法

226

70

0.04

177

78.32%

49

21.68%

改进ID3算法

226

70

0.018

177

78.32%

49

21.68%

  

 数据splice.arff(Test-model:10-fold cross-validation)

 

数据集规模

属性数

 建模时间

正确样本

正确分类率

错误样本

错误分类率

原ID3算法

3190

62

1.47

777

24.36%

2413

75.64%

改进ID3算法

3190

62

0.23

777

24.36%

2413

75.64%

 

数据splice.arff(Test-model:split 80% train,remainder test) 

 

数据集规模

属性数

 建模时间

正确样本

正确分类率

错误样本

错误分类率

原ID3算法

3190

62

1.58

144

22.5705%

494

77.4295%

改进ID3算法

3190

62

0.30

144

22.5705%

494

77.4295%

 

      由图可知,在数据集越大、属性越多时改进效果越明显,这种改进体现在建模时间上,改进后的ID3算法平均建模时间比原ID3算法少很多。通过索引比直接操纵元素快,并且控制节点上寻找信息熵增量最大属性时应该剔除已经在祖先节点选为分类依据的属性,同时在保证正确的情况下将循环不变量外移,不但可以较快程序的运行速度,还能有效节省程序占用的内存。

 

四、小结

    下面我谈谈我对《模式识别》课程的学习总结。当下互联网行业大数据等技术快速发展。大数据的背后是新的数据处理方法。在这其中,模式识别扮演着重要的作用。我的理解是模式识别在人工智能、图片搜索、语音识别等方面起着至关重要的作用。在《模式识别》课堂上,我跟着老师学习了两种模式识别的方法:有导师的分类和无导师的聚类。其中分类算法学习了决策树算法、贝叶斯网络算法、k-最近邻算法、人工神经网络算法;聚类算法了解了k-均值算法等。 分类作为一种监督学习方法,要求必须事先明确知道各个类别的信息,并且断言所有待分类项都有一个类别与之对应。但是很多时候上述条件得不到满足,尤其是在处理海量数据的时候,如果通过预处理使得数据满足分类算法的要求,则代价非常大,这时候可以考虑使用聚类算法。

     我的理解是决策树算法是通过贪心策略来建立分类器然后结合AC自动机理论进行分类;贝叶斯网络是基于概率的分类算法;k-最近邻算法通过引入距离来分类;人工神经网络算法是则应用类似于大脑神经突触联接的结构进行信息处理的数学运算模型;聚类算法如k- 均值算法也是通过距离来聚类的。

 

五、代码

package myPatternRecognition;

import weka.classifiers.*;
import weka.core.*;

public class MyID3 extends Classifier {
/**
*
*/
private static final long serialVersionUID = 1L;
private static Instances dataSet;// 静态全局数据集,避免每个对象都包含数据集
private Attribute classifierAttribute;// 分裂属性
private MyID3[] childMyID3;// 孩子节点
private int[] instanceIndex;// 当前节点数据集索引
private int[] attributeOfNotUsedIndex;//可以作为当前节点剩余分裂属性的属性索引

/*
* 参数:待训练数据集
* 返回值:空
* 功能:覆盖父类的buildClassifier函数
*/
public void buildClassifier(Instances data) throws Exception {
dataSet = data;

//计算根节点数据集索引
int numInstances = data.numInstances();// 循环不变量外移
instanceIndex = new int[numInstances];
for (int i = 0; i < numInstances; i++)// 根节点下标索引集
{
instanceIndex[i] = i;
}

//计算根节点可分裂属性索引
int numAttribute =data.numAttributes();// 循环不变量外移
attributeOfNotUsedIndex= new int[numAttribute];
for (int i = 0; i < numAttribute; i++)// 根节点可分裂属性索引集
{
attributeOfNotUsedIndex[i] = i;
}
makeTree();
}

/*
*  参数:选定的分裂属性索引
*  返回值:分裂后个孩子节点数据集索引
*  功能:分裂节点函数(完成将当前节点的索引按既定属性)
*/
private int[][] splitData(int attributeId) {
int numAttValues = dataSet.attribute(attributeId).numValues();// 当前分裂属性的取值可能数
int[][] splitData = new int[numAttValues][];
int[] count = new int[numAttValues];// 统计当前属性不同取值的情况

// 计算每个孩子节点数据集大小
for (int i = 0; i < instanceIndex.length; i++) {
int attVal = (int) dataSet.instance(instanceIndex[i]).value(
dataSet.attribute(attributeId));
count[attVal]++;
}

// 为每个子节点开辟相应大小数组存储下标
for (int i = 0; i < numAttValues; i++) {
splitData[i] = new int[count[i]];
}

int[] lastID = new int[numAttValues];// 统计当前孩子节点下标插入位置
// 计算分裂到每个孩子节点的索引下表集
for (int i = 0; i < instanceIndex.length; i++) {
int attVal = (int) dataSet.instance(instanceIndex[i]).value(
dataSet.attribute(attributeId));
splitData[attVal][lastID[attVal]] = instanceIndex[i];
lastID[attVal]++;
}
return splitData;
}

/*
* 参数:attributeId分裂属性索引,parentEntropyReduce父节点信息熵
* 返回值:信息熵增量
* 功能:计算按某个给定属性分裂的信息熵增量
*/
private double computeEntropyReduce(int attributeId,
double parentEntropyReduce) throws Exception {
double entropyReduce =parentEntropyReduce;
int[][] splitData = splitData(attributeId);
Attribute tempAtt = dataSet.attribute(attributeId);
int numValues=tempAtt.numValues();
for (int j = 0; j < numValues; j++) {//计算信息熵增量
if (splitData[j] != null && splitData[j].length > 0) {
entropyReduce -= ((double) splitData[j].length / (double) instanceIndex.length)
* computeEntropy(splitData[j]);
}
}
return entropyReduce;
}

/*
* 参数:splitData为节点数据集索引
* 返回值:节点信息熵
* 功能:计算当前节点数据集对应的信息熵
*/
private double computeEntropy(int[] splitData) throws Exception {
int numClasses = dataSet.numClasses();// 类标记的取值个
double[] classCounts = new double[numClasses];
for (int i = 0; i < splitData.length; i++) {//统计对应类标记不同取值个数
int classVal = (int) dataSet.instance(splitData[i]).classValue();
classCounts[classVal]++;
}
for (int i = 0; i < numClasses; i++) {
classCounts[i] /= splitData.length;
}
double Entropy = 0;
for (int i = 0; i < numClasses; i++) {
Entropy -= classCounts[i] * log2(classCounts[i], 1);
}
return Entropy;
}

/*
* 参数:x/y为log2的参数
* 返回值:返回log2(x/y)
* 功能:计算log2(x/y)
*/
private double log2(double x, double y) {
if (x < 1e-6 || y < 1e-6)// 控制浮点数精度误差
return 0.0;
else// 换底公式计算
return Math.log(x / y) / Math.log(2);
}

/*
* 参数:无 返回值:空 功能:按照信息熵增量最大这一贪心策略进行递归建立决策树
*/
private void makeTree() throws Exception {

// 空节点必是叶节点
if (instanceIndex == null) {
classifierAttribute = null;
return;
}

// 寻找信息熵增量最大对应的属性索引
double impurityReduce = 0, maxValue = 0;
double parentEntropyReduce = computeEntropy(instanceIndex);// 保存当前节点信息熵
int maxIndex = -1;
for (int i = 0; i < attributeOfNotUsedIndex.length; i++) {// 寻找熵的最大增量
if (attributeOfNotUsedIndex[i] == dataSet.classIndex())
continue;
impurityReduce = computeEntropyReduce(attributeOfNotUsedIndex[i],
parentEntropyReduce);
if (impurityReduce > maxValue) {
maxValue = impurityReduce;
maxIndex = i;
}
}

if (Utils.eq(maxValue, 0)) {// 熵的增量为0
classifierAttribute = null;
return;
} else// 继续递归分裂建立决策树
{
classifierAttribute = dataSet
.attribute(attributeOfNotUsedIndex[maxIndex]);
int[][] splitData = splitData(attributeOfNotUsedIndex[maxIndex]);
int numValues = classifierAttribute.numValues();// 循环不变量外移
childMyID3 = new MyID3[numValues];// 按分裂属性不同取值实例化孩子节点
for (int j = 0; j < numValues; j++) {// 生成对应的孩子节点
childMyID3[j] = new MyID3();
childMyID3[j].instanceIndex = splitData[j];// 赋予孩子节点数据集

// 计算孩子节点可分裂属性
childMyID3[j].attributeOfNotUsedIndex =
new int[attributeOfNotUsedIndex.length - 1];
int numId = 0;
for (int k = 0; k < attributeOfNotUsedIndex.length; k++) {
if (k != maxIndex)// 除去已经使用的属性
childMyID3[j].attributeOfNotUsedIndex[numId++] =
attributeOfNotUsedIndex[k];
}
childMyID3[j].makeTree();// 在孩子节点递归建树
}
}
}

/*
* 参数:待分类样本
* 返回值:对应类标记取值比例
* 功能:分类主函数(递归将当前将数据分裂到叶子节点)
*/
public double[] distributionForInstance(Instance instance) throws Exception {
if (classifierAttribute == null) {
return computeDistribution();
} else {
return childMyID3[(int) instance.value(classifierAttribute)]
.distributionForInstance(instance);
}
}

/*
* 参数:无
* 返回值:对应类标记取值比例
* 功能:在叶子节点进行投票并统计比例
*/
private double[] computeDistribution() throws Exception {
int numClasses = dataSet.numClasses();
double[] probs = new double[numClasses];
double[] classCounts = new double[numClasses];
for (int i = 0; i < instanceIndex.length; i++) {// 将类标记属性按取值分类,此处也是操作下表索引
int classVal = (int) dataSet.instance(instanceIndex[i])
.classValue();
classCounts[classVal]++;
}
for (int i = 0; i < numClasses; i++) {
probs[i] = (classCounts[i] + 1.0)
/ (instanceIndex.length + numClasses);
}
Utils.normalize(probs);
return probs;
}

public static void main(String[] args) {
try {
long beforeTime=System.currentTimeMillis();
System.out.println(Evaluation.evaluateModel(new MyID3(), args));
long afterTime=System.currentTimeMillis();
long timeDistance=afterTime-beforeTime;
System.out.println("改进后ID3算法运行时间"+timeDistance);
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}

 

        在此声明,上面是我模式识别实习报告,分享出来,希望大家斧正!

 

 

 

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息