最近这些天一直在用weka实现一个算法,也是从这次开始接触weka,刚上手难免有些磕磕绊绊,这次实现也是遇到了各种各样的问题,其中的一个就 和weka中的Instances有关,刚刚把程序跑起来了,因为数据比较多,没有个一天两天估计是跑不完了,趁这个空闲时间,把我遇到的问题及解决方法 记录下来,一是这样可能会帮助到其他人,还有就是也方便我自己以后查阅,毕竟俗话说得好,烂笔头胜过好记性。
首先,weka是从事数据挖掘相关研究的人一定会接触到的一个软件,这个软件由新西兰waikato大学编写,里面实现了种类繁多的算法,这里就不一一介绍,相信用过weka的人都知道我在说什么。
这篇随笔将会从三个具体的和属性选择相关的实际问题出发,给出具体的解决办法及代码。在讨论实际问题之前,先来说说为什么需要属性删除,也就是需求的问题。
一、为什么需要属性删除
我们知道,在做交叉验证的时候,在训练集上做过什么处理,这些处理必须原封不动地施加到测试集上,这样分类得到的结果才是可信的。
大 数据时代已经来临,我们经常会遇到那种实例多,维度高(属性多)的大数据,处理这样的数据时,从效果和效率两方面来考虑,我们都需要先对数据进行预处理, 最常见的一种预处理的方式就是属性选择,也就是我现在一直在做的东西,所谓属性选择,从字面上理解,就是从高维的数据中选择出一部分的属性,用这部分属性 代替属性总体,这样一来,很显然能够减少数据处理的时间,另外,如果算法选择得当,我们能得到更好的结果,比如说,分类的精度更高。
如果我 们用属性选择后的训练集构建了一个分类器,我们怎么测试这个分类器的好坏呢,还是要靠实验说话,在测试集上测试这个分类器的精度,前面说过,在分类的时候 在训练集上的处理必须原封不动的施加到测试集上,训练集上属性选择了,测试集也要属性选择,并且选出来的属性必须和训练集上选出来的属性完全一致,为了得 到属性选择后的测试集,显然,除了训练集上选择出来的属性,测试集上的其他属性都要删除。
二、三个具体的问题
为了更好的描述这几个问题,我们定义一些变量,这些变量在三个问题中通用。
train:训练集 test:测试集
trainInstance:在训练集上属性选择之后返回的训练集 testInstances:依据trainInstances在test上删除相应的属性返回的测试集
trainIndex:在训练集上属性选择之后返回的训练集的下标数组
1.属性选择算法返回的是trainInstances,这个时候,为了得到testInstances,可以使用下面这段代码
public static Instances delAttr(Instances model, Instances origin){ boolean flag = false ; ArrayList <Integer> al = new ArrayList<Integer> (); for ( int q = 0; q < origin.numAttributes() - 1; q++ ){ String temp2 = origin.attribute(q).name(); for ( int x = 0; x < model.numAttributes() - 1; x++ ){ String temp1 = model.attribute(x).name(); if (temp1.equals(temp2)){ flag = true ; break ; } } if (flag) { flag = false ; continue ; } else // dataCopy.deleteAttributeAt(q); // you can not do like this al.add( new Integer(q)); } for ( int q = 0; q < al.size(); q++ ){ int deltemp = al.get(q) - q; // pay attention to this line origin.deleteAttributeAt(deltemp); } return origin; }
model相当于trainInstances,origin相当于test,返回的origin相当于testInstances.
上面这段代码的思想无非就是通过属性的名字把需要删除的属性的下标全部存放在一个ArrayList的数据结构中,然后根据这个ArrayList删除对应的属性,返回新构造的Instances.
注意上面我写的两行注释,因为你删除一条属性之后,排在这条属性之后的所有属性的index都是会变化的(-1),这就相当于你从String删除一个字符一样,知道这一点,你就知道为什么第二条注释对应的删除方法可以正常工作了。
注意:如果你用的数据集中有两个或多个属性的名字完全一样,这种方法会把拥有这个名字的所有属性都保留,但是,有可能你真正需要的只是其中一个。
2.直接使用1返回的结果交给分类器,往往会报错,其中的原因有点微妙,解释如下:
假 设train有四个属性,分别是{attr1,attr2,attr3,attr4},attr4是类标签,test也是这样 在train上属性选择,返回trainInstances{attr3,attr2,attr4},把trainInstances和test交给1, 返回的是testInstances{attr2,attr3,attr4},把testInstances交给分类器,报错,报错的原因是 trainInstances和testInstances的属性错位了.
这种情况下,我们需要交换两个属性的位置,下面这段代码解决了这个问题
public static Instances sort(Instances model, Instances process){ Attribute attr; for ( int i = 0; i < model.numAttributes()-1; i++ ){ for ( int j = 0; j < process.numAttributes()-1; j++ ){ if (process.attribute(j).name().equals(model.attribute(i).name())){ if (j!= i){ attr = process.attribute(j); process.insertAttributeAt(attr, i); for ( int k = 0; k < process.numInstances(); k++ ){ process.instance(k).setValue(i, process.instance(k).stringValue(j +1 )); //pay attention to j+1 } break ; } } } } for ( int i = process.numAttributes() - 2; i > model.numAttributes()-2; i-- ){ process.deleteAttributeAt(i);; } return process; }
model相当于trainInstances,process相当于1中返回的testInstances,2中返回的process就是能和trainInstances的每个属性对应起来的testInstances。
这段代码的思想是找到 process中每个属性在model中的对应位置,然后再process的对应位置插入这个属性,插入属性的时候需要把属性值复制过去。
注意因为我用到的数据都是nominal类型的,所以在复制属性值的时候选的是stringValue这个方法,如果是numeric或者其他的类型需要选择对应的方法,这个在weka的Instance类中可以找到。
3.属性选择算法返回的是下标数组trainIndex
3和1的不同之处在于省去了自己用属性名去匹配的这个步骤,也因此,即使数据集中有多个属性的名字完全一致,也没有影响,因为返回的是下标。
这种情况可以用下面的代码:
public static Instances delAttr(ArrayList<Integer> al, Instances inst){ for ( int i = inst.numAttributes() - 1; i > -1; i--){ // delete from back to forward if (! al.contains(i)) inst.deleteAttributeAt(i); } return inst; }
注意:这段代码我是从后往前删的,这种删除方法不会出现问题,因为每次删除的都是需要删除的属性的最后一个,对前面需要删除的属性的下标不会有任何影响。
注意:拿到属性下标之后,先看看是不是排序过,没有排序的话,最好先排序一下,按从小到大的顺序排,这样虽然需要花费一点额外的时间,有时却能避免意想不到的错误,具体原因我说不清楚,但是我遇到过。
三、总结
很显然,第三种情况最简单,代码清晰,花费的时间也相对较少,所以在属性选择之后能够直接拿到下标数组最好,否则将拿到的结果转化成下标数组也可以。