2023年随机森林实验报告_第1页
2023年随机森林实验报告_第2页
2023年随机森林实验报告_第3页
2023年随机森林实验报告_第4页
2023年随机森林实验报告_第5页
已阅读5页,还剩8页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

随机森林实验报告

实验目的

实现随机森林模型并测试。

实验问题

Kaggle第二次作业Non-linearclassification

算法分析与设计

算法设计背景:

1.随机森林的原子分类器一般使用决策树,决策树又分为拟合树和分类树。这两者的

区别在于代价估值函数的不同。

2.根据经验,用拟合树做分类的效果比分类树略好。

3.对于一个N分类问题,它总是可以被分解为N个2分类问题,这样分解的好处是其决

策树更加方便构造,更加简朴,且更加有助于用拟合树来构建分类树。对于每一个2分类问

题,构造的树又叫CART树,它是一颗二叉树。

4.将N个2分类树的结果进行汇总即可以得到多分类的结果。

5.CART树构造:

输入:训练数据集D,停止训练条件

输出:CART决策树

根据训练数据集,从根节点开始,递归地对每个节点进行以下操作,递归构造二叉决策树

(1)设节点的训练数据集为D,计算现有特征对该数据集的基尼系数,此时,对每一个特征

A,对于每一个可能的划分值a,根据大于a及小于等于a将训练数据集分成DI,D2两

部分。计算gini系数公式如下:

Giniseparate=^*Gini<Dl)+与*Gini(D2);

Gini(D)=1-Ek=iPk

(2)在所有可能特征A以及其所有可能划分点a中,选择gini系数最小的特征及切分值,将D1

和D2分配到左右两个子节点去。

(3)对左右节点分别递归的调用(1),(2),直到满足一定条件为止。

6.随机森林构造:

(1)随机取样,降低树与树之间的关联性

RF在每次构造一棵决策树时,先随机的有放回的从训练集中抽取(60%)或n(sizeof

trainingset)个样本D,在每个节点分裂前,先随机从总共M个特征中选取m个(m

«M,一般取根号M)作为分裂用的候选特征,这有效降低了树与树之间的关联性。

算法思绪:

将一个N分类问题转化为N个二分类问题。转化方法是:构造N棵二叉拟合树,这里假

设N为26,然后我们给N棵二叉树依次标号为1,2,3...26。1号树的结果相应于该条记

录是不是属于第一类,是则输出1,否则输出0.2号树的结果相应于该条记录是不是属于第二

类,是则1否则0,依此类推。这样,我们的26棵二叉树的结果就相应了26个下标。

例如对于某条记录,这26个二叉树的结果按序号排列为{0,0,0,0,0,0,0,0,0,0,0,0,0,

0,0,...1,0),那么这条记录的分类应当为25。要将一个26维的0,1序列变回

一个索引,我们只需要找出这个序列中值最大的元素的索引,这个索引即是序列号。

我们将上面的26棵分别对26个索引做是否判断的二分类树视为一个整体,在多线程

的环境下,构造多个这样的整体,然后进行求和运算,最后取出每个结果序列中值最大的元

素的下标作为分类值,那么久得到了我们想要的结果,随机森林完毕。

三.算法流程:

1.读入训练集trainset,测试集testset

2.将训练集分割为输入trainIn,输出trainOut

3.这里假设类别数N为26,将trainOut[记录条数]映射为transformTrainOut[训练

记录数][26]

4.初始化transformTestOut[测试记录数][26]所有为0

5.Fori=1:ForestSize:

//对训练集采样,这里要注意输入和输出一致

[samp1eln,transfbrmSampleOut]=TakeSample(trainIn,transformT

rainOut)

Forcategory=1:26:

//CartTree数组存放着26棵二分类树

CartTree[category]=TrainCartTree(sampleln,transformS

ampleOut);

end

//transformTest0ut[测试记录数][26]为承接二分类树输出的容器

foril=1:testSetNum:

Forcategory=1:26:

transformTestOut[il][category]+=predict(CartTree[cate

gory],testset[il])

end

End

End

6.遍历transformTrainOut□,将其每一行的最大值的下标作为该行记录的索引值。

四.决策树及随机森林的配置

1.决策树

在这里,我们每一次26分类是由26棵CART共同完毕的,CART的costfunction

采用的是gini系数,CART的最大层数为7,分裂停止条件为当前节点GINI为0或者

当前节点所在层数到达了7.

2.随机森林

a.随机森林每次循环的训练集采样为原训练集的05

b.对于森林中每一棵决策树每一次分割点的选取,对属性进行了打乱抽样,抽样数为2

5,即每次分割只在25个属性中寻找最合适的值。并且对于每个选取的属性,我们进行了行

采样。即假如这个属性所拥有的属性值数大于30,我们选取其中30个作为分割候选,假如小

于30,则所有纳入分割候选。

五.代码详解

1.训练集/测试集的读入

a.在dataDefine.h中定义了:

什definenumparametres619//619〃i£录中的字段总数

#definetrainsetNum6239//100〃训瀛集的记录条数

#definetestsetNum1560//100//测触的记点瞰

#definetypesNum26//26〃类型数母

#definenodesNum200//200〃树的总结点数

训练集记录列数numparametres(ID(1)+参数数量(617)+输出(1)=619)

训练集记录条数transe(Num

测试集记录条数testsetNum

分类类型数typesNum

而在main.cpp中,我们声明了全局变量

doubletrainIn[trainsetNum][numparametres-2];〃训输入

doubletrainOut[trainsetNum][l];〃训输出

idoubletestIn[testsetNum][numparametres-2];|〃…

doubletestOut[testsetNn];

)inttrainID[trainsetNum];〃训练集每一行记录的ID,这个看实际情况有没有做决定

.inttestID[testsetNum];/屈I试集每一行记录的ID

trainin用于装教训练集输入,trainOut用于装载训练集的输出(这里trainOut是二维

数组是出于模型假如泛化,那么输出值不一定只有一个的情况,在本次实验中并未派上什么

真正用场,可以将trainOut看作一个普通一维数组)。trainID用于装载训练集中每一行

的第一列ID号。testIn,testID则相应测试集的输入和ID号。这里注意,没有testOut的

因素是测试集的结果理论上应当是不存在的。

然后通过自己编写的读入函数

readDataCtrain.csv","test.csv");

读入测试集合训练集,这个函数将分别装载我们在前面提到的trainIn.trainOut、tra

inlD、

testIn.testIDo这个函数使用的fstream逐行读入的方法,这里不做详述。

2.训练集输出转化为相应的26维01数组transformOut[typesNum]

在dataDefine.h中,我们定义了分类类别数typesNum:

#define露。esNum26“2

在main,cpp中,我们定义了全局变量iransformOut[typesNum]

inttransformOut[trainsetNum][typesNum+1]={0};〃类别数

这里的transformOut是用于储存将trainOut每行的值映射为一行相应的26维01序列

后所

产生的结果。

这里面的相应关系是:例如trainOut[10]中的值是13那么transformOut[10][13]=l,t

ransformOut[10][除13外其他列]=0;假如值是14,那么14列为1,其他列为0,行

号代表的是它们相应的是第几条记录;trainOut[10]和transformOut[l0]都表达的是第

10行的分类值为某个值,只是表达方式不同。前者用数字表达,后者将相应下标的值置1

表达。

转换接口由main.cpp中的函数

voidindexTransform(inttransformres[][typesNum+1],doubleogres[][l]);〃将原始输出转化为转化:

定义,它的输入参数依次为转换输出的承接容器transformres,盛放原始输出的容器。唔

eso

它所做的事情是将transformres[i][orges[i]]的值置1

3.并行构建随机森林

在main.cpp中,我们构建了

doubletrainInPerTime[perTimeNum][numparametres-2];

doubletrainInPerTimeTl[perTimeNum][numparametres-2];

doubletrainInPerTimeT2[perTimeNum][numparametres-2];

doubletrainInPerTimeT3[perTimeNum][numparametres-2];

doubletrainInPerTimeT4[perTimeNum][numparametres-2];

inttransformOutPerTime[perTimeNum][typesNum+1]={0};

inttransformOutPerTimeTl[perTimeNum][typesNum+1]={0};

inttransformOutPerTimeT2[perTimeNum][typesNum+1]={0};

inttransformOutPerTimeT3[perTimeNum][typesNum+1]={0};

inttransformOutPerTimeT4[perTimeNum][typesNum+1]={0};

doubletransformTestOutTl[testsetNum][typesNum+1]={0};

doubletransformTestOutT2[testsetNum][typesNum+1]={0};

doubletransformTestOutT3[testsetNum][typesNum+1]={0};

doubletransformTestOutT4[?s:setNum][typesNum+1]={0};

trainInperTime代表的是随机森林算法中通过采样环节后选取的训练输入,

TransformOutPerTime代表的是与trainInpeiTime相应的转换输出

transformtestOut是承接本支线程的所有CART树的决策值之和的结构,这与算法思绪

是相应的,我们将所有CART树的预测结果在意个转换输出容器上累加,然后对于每行取

该行最大列的下标,即可得到由随机森林得到的分类结果。

我们可以看出,这几个变量都是只有最后的TX有区别,事实上,反复的创建相似的变量只

是为了方便多线程操作不会冲突。

多线程入口:

decisionTree&tasd=Treel;

threadthreadl(mainInThread,transformOutPerTimeTl,trainlnPerTimeTl,transformTestOutTl,&Treel);

threadthread2(mainInThread,transformOutPerTimeT2,trainInPerTimeT2,transformTestOutT2,&Tree2);

threadthread3(mainInThread,transformOutPerTimeT3,trainInPerTimeT3,transformTestOutT3,&Tree3);

threadthread4(mainInThread,transformOutPerTimeT4,trainInPerTimeT4,transformTestOutT4,&Tree4);

这里使用的是C++11的〈thread〉库,简朴好用。

每一个线程的随机森林框架定义在main.cpp的

|intmainInThread(inttransformOutPerTime[][t.):、+1],doubletrainInPerTimeHn「::[-2],doubletransformTestOut

这个函数采用循环的方式,每次循环,对训练集及相应转换输出进行打乱后采样,然后输入

TRAIN(trainInPerTime_,transformOutPerTime_,transformTestOut_,Tree);

中进行一轮决策树的训练,这一轮训练将会生成26棵CART树,相应26个分类值。这里输

入的参数Tree就是我们所用的决策树容器,这里注意,我们一个线程中只需要公用一个决

策树结构即足够了.

在训练完毕后,我们用transformTestOut[i][j]=陵加训练结果。

4.一轮训练26棵树

由于26棵CART树才干完整的等价于一棵26分类树,因此我们将构建这26棵CART树

的过程当作是一个整体。这个过程由函数

TRAIN(trainInPerTime_,transformOutPerTime_,transformTestOut_,Tree);

实现。它的输入依次是本轮的训练输入(通过了下采样,随机森林规定的),相应的转换训练

输出,以及一个决策树容器Tree。决策树的定义我们将在下文中描述。

这个函数有一个栈

stack<int>trace;〃用于追踪树的遍历过程,这里我们假定用的是先根遍历;

并且有一个从1:26的循环

for(inttypesN=1;typesN<=typesNum;typesN++){

每次循环会建立一棵关于相应的分类值得CART树,CART树的构造是由栈trace维护

的,trace维护的是一个先序的遍历顺序。

当循环完毕后,将会计算本轮的转换输出结果的变更:

for(inti=0;i<testsetNum;++i){

//testresPerTime[i][typesN]=TputeRes(testIn[i]);

transformTestOut_[i][typesN]+=(*Tree).computeRes(testIn[i]);

}

)

5.每科CART树的构造

CART树的数据结构如下:

structdecisionTree{

doubletrainIn[perTimeNum][numparametres-2];

inttrainOut[perTimeNum];

nodeNodesfnodesNum];

〃在建树时需要使用的可用节点索引记录

intusableNode;

trainIntrainOut相应于输入该树的输入输出集,Nodes表达的是节点序列,在这里我们

的树的构造使用的是数组,且树的节点间的索引是通过索引值维护的,这颗树非常紧密(假如

只看NODES是看不出节点间的层级关系的)。

它有如下成员函数:

decisionTreeOO;

voidsetDecisionTree(doubletrainlnj](numparametres-2],inttrainOut_[]);

boolgetPartition(intindex,intcontainer!]);〃用于做树的节点分割

doublecomputeGini(intindex,intlabel,doublevalue);〃用7Hl声分划台点时所需要的gini值

//doublecomputeNodeGini(intindex);〃计算某一节点的Gini

doublecomputeRes(double[numparametres-2]);

voidgetNodesSequence(nodel[]);

〃初始化树

voidinitialize(nodeele);

〃对于每个节点的一些操作

voidgetNodeAttr(vector<int>selectedCols,intindex);

voidcomputePerNodeGini(intindex);

voidcomputeNodeValue(intindex);

);

setDecisionTree用于给trainIn和trainOut赋值

getNodeSequence(nodel[])本来是用来输出节点参数的,这里不做详述

initia1ize用于初始化决策树。

getNodeAttr用于得到某一节点的备选属性分割值

computePerNodeGini用于计算某一节点的GINI值,这在停止节点分割时有用

ComputeN。deValue是用于计算某一叶子节点的拟合值的。

我们再说一下Nodes节点,它的结构如下

vector<int>datalndex;〃用于装该节点的数据的

〃用于记录i亥节点的分割点

vector<double>attributes[SelectedColumns];

intleftChild,rightchild;〃子节点的Index

intisLeaf;//0表示内部节点,1表示外部节点

intsplitLabel;〃记录当前的分割属性的位置

doublelabelvalue;/僦节点分割值

doublevalue;〃该节点的平均值,用于拟合时的运算

intlayer;〃用于记录所在层数

doublegini;〃当前节点的GINI值

AttrbutesEselectedColumns]是用于存放候选的分割值的容器

其余变量的功能见图片中的文字注释

这里我们用datalndex存放相应记录所在索引的方法取代了直接存放记录,这里是一个巨

大的改善,将程序的执行速度提高了至少10倍。

在构造一棵决策树时,当train函数相应的trace栈的栈顶非空时,我们会不断的取出栈顶

元素,对其进行

boolgetPartition(intindex,intcontainer(1);

操作,Index指的是节点所在的索引值,container用于存放这个节点的左右叶子索引,由于

树的构建是由外部栈维护的,所以这个container是必不可少的,在当前节点分割完毕后,

我们会将这个节点的索引值出栈,假如container⑼的值不是-1,我们会将container[0],co

ntainer[1]入栈。建树的相应模块在main,cpp下的train函数中的

trace.push(U);

〃树的初始化(插入头节点)完成后,正式开始决策树的构建

while(!trace.emptyO){

intcurrent_node=trace.topO;

trace.popO;

intcontainer[2]={0};

(*Tree).getPartition(current_node,container);

〃判断当前节点是否成功分割___________________________

if(container[0]!=-1){

trace.push(container[l]);

trace.push(container[0]);

}

}

〃训练完成,计算输出

下面再重点说一下函数:

booldecisionTree::getPartition(intindex,intcontainer[2]){

这个函数是单棵决策树构造的核心,调用这个函数,假如当前节点的Gini值已经为0,那么这

个函数会计算当前节点的拟合值:

if(Nodes[index].gini==0||Nodes[index].layer>=MaxLayerNum/*||/*Nodes[index].dataSet.sizeO<10*/){

Nodes[mdex].isLeaf=1;

〃计算叶子节点的拟合值

doublesum=0;

for(inti=0;i<Nodes[mdex].datalndex.size();++i){

sum+=trainOut[Nodes[index].dataIndex[i]];//Nodes[index].res[ij,

)

Nodes[index].value=sum/Nodes[index].datalndex.size();

container[0]=container[l]=-1;

returnfalse;

结束条件是gini==0||层数等于10

假如当前节点不满足结束分割条件,那么函数将对属性进行抽样,抽样的方法是打乱后取前s

e1ectedColumns列I。然后调用getNodeAttr(s,index)获取当前节点的备选分割值,这里

的s是抽取的属性的列号的集合。

/献得打乱后的索引,并选取前面的25个作为选取的列的索引,传递给getAttr,获得所需要的分割值

vector<pairl>sequence(numparametres-2);

srand((unsigned)time(NULL));

for(i=0;i<sequence.sizeO;++i){

sequence[i].index=i;

sequence[i].value=randQ;

)

std::sort(sequence.beginO,sequence.endO.cmp);

vector<int>s(SelectedColumns);

for(inti=0;i<SelectedColumns;++i){

s[i]=sequence[i].index;

)

aetNodeAtths.index):

在得到备选的属性分割值后,将进入循环,寻找最优分割点

for(i=0;i<SelectedColumns;++i){

vector<double>::iteratorcursor;

for(cursor=Nodes[index].attributes[i].beginQ;cursor!=Nodes[index].attributes[i].end();++cursor){

temp_gini=computeGini(index,s[i],*cursor);

if(min_gini>temp_gini){

min_gini=temp_gini;

par_label=s[i];

par_value=*cursor;

)

)

)

6.最终结果计算

在main函数中,我们将四个线程所得的transformOutT相加,最后遍历取每一行最大值的

下标,即可得到最终结果。

六.算法优化

1.应用了数组+栈建树取代了普通的函数递归建树,加快了建树速度。

2.在传递每个节点的节点数据集时,使用了传递数据集的索引而非数据自身,这样做的好处

是,本来假如传递一条数据需要复制617个double类型的数量,而现在只需要传递一个

Int型的索引,这种快了617倍的数据集传递方式使程序运营效率提高了10倍以上。

3.在每个属性中选择备选分割值的时候,采用了一种下采样的策略。即:假如该节点的数据

集大小小于某一数值,则将这个数据集的这个属性的所有值都纳入候选分割值列表。但是假

如大于了这个阈值,则将属性所相应的列进行排序后再进行等间距采样得到样本数等于阈值

的子集作为候选分割集。代码详见getPartition().这样做的好处是需要计算的分割gini

值大大减少了(本人取的采样阈值时100,相比原数据集,样本空间缩小了尽30倍),这里也再

一次加速了程序运营。但是这个优化随机而来的一个问题是:有也许每次分割都不是最佳分

割。

4.使用了C++11的〈thread〉库进行了并行实现,开出4个线程,程序相比单线程加速了4

倍。

七.并行实现

C++11〈thread〉库创建线程,为每个线程赋予独立的数据容器,并将随机森林提成等量

的4部分(由于我使用的是4个线程)。即,每个线程中执行的函数承担1/4规模的随机森

林的构造,实现代码如下:

threadthreadl(mainInThread,transformOutPerTimeTl,trainlnPerTimeTl,transformTestOutTl,&Treel);

threadthread2(mainInThread,transformOutPerTimeT2,trainInPerTimeT2,transformTestOutT2,&Tree2);

threadthread3(mainInThread,transformOutPerTimeT3,trainInPerTimeT3,transformTestOutT3,&Tree3);

threadthread4(mainInThread,transformOutPerTimeT4,trainInPerTimeT4,transformTestOutT4,&Tree4);

threadl.joinO;

thread2.join0;

thread3.join0;

thread4.joinQ;

intmain!n

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论