决策树,随机森林,boost小结
决策树(CvDTree)是最基础的,是CvForestTree和CvBoostTree的父类。
决策树的生成,一般资料中都是基于ID3算法(熵增益),即ID3算法在每个节点分裂时,选择使gain(A)最大的特征分裂。
Opencv中决策树的生成是基于吉尼不纯度最小的原则生成的。即选择 最小的分裂。其中,m为样本的种类,如只有正负样本,则m为2,p1,p2分别代表某个节点中正样本的比例和负样本的比例。j为使用特征A分裂出的节点数目,如二叉树,每次只分裂出左右两个节点,则j为2,D1,D2分别代表左右节点的样本个数。
决策树的训练接口中,核心是使用try_split_node函数进行递归,直至最终分裂出来的节点中样本的个数小于指定的个数,或者决策树的深度达到指定的最大深度,或者节点中的样本标签纯净(即节点中所有样本属于同一类)。
每调用一次try_split_node函数,会选择一个最优的特征,即找出所有特征中吉尼不纯度最小的特征(find_best_split)。
find_best_split函数会遍历所有特征(如果train函数指定了某些特征,则只遍历指定的特征),分别计算每个特征对应的吉尼不纯度(find_split_cat_class)。然后选择出吉尼不纯度最小的特征来分裂该节点。即find_best_split 函数会调用find_split_cat_class,后者用于计算指定的某个特征的吉尼不纯度。
那么,find_split_cat_class函数是如何计算某个指定特征的吉尼不纯度呢?
假设某个特征的取值有很多个(如颜色特征,对应的取值有红,黄,蓝,绿四种,则颜色特征对应的取值个数为4),则具体计算过程如下:
1.统计每个特征值对应的cjk。cjk为第j个特征值对应的k样本的个数。(如颜色特征,特征值总个数为4,j为0~3;二分类问题,分类总数为2,即k为正样本或负样本, cjk对应4*2矩阵)。
2.统计每个特征值对应的weight。weight[j]即为第 j个特征值对应的cjk中正样本个数和负样本个数分别和其权重的乘积再求和。
3.将cjk按照正样本的个数从大到小重新排序。
4.按照排序后的cjk顺序,依次按照不同的特征值进行分裂,并计算每次分裂对应的吉尼不纯度,最终选出吉尼不纯度最小时对应的特征值域的分裂。
为了更直观地说明如何求某一个指定特征的最小吉尼不纯度的过程,现在使用具体的例子展示,如指定颜色这个特征,同时颜色特征对应的取值有红,黄,蓝,绿四种)
(1)统计每个特征值对应的cjk,cjk为第j个特征值对应的k样本的个数。
CJK | 负样本个数 | 正样本个数 |
0(红) | 100 | 200 |
1(黄) | 50 | 600 |
2(蓝) | 200 | 100 |
3(绿) | 30 | 300 |
(2)统计每个特征值对应的weight。weight[j]为第 j个特征值对应的cjk中正样本个数和负样本个数分别和其权重的乘积再求和。(如果负样本权重和正样本权重比例为[9:1])。
weight | 正负样本权重个数和 |
0(红) | (100*9+200*1)/10=110 |
1(黄) | (50*9+600*1)/10=105 |
2(蓝) | (200*9+100*1)/10=190 |
3(绿) | (30*9+300*1)/10=57 |
(3)将cjk按照正样本的个数从大到小重新排序。这个排序大大简化了特征值域组合的统计的个数,这是精妙之处。否则,按照原始的方法,需要统计很多次。比如,于颜色特征的取值为红,黄,蓝,绿四种,它们组合的值域应该有2的4次方减2,但是为了简化,我们将cjk按照正样本个数排序,即sort后得到从大到小的cjk”。然后按照新排序的特征值,只需要分别统计下面三种特征值域的组合,即{黄},{黄,绿},{黄,绿,红}。也就是按照cjk”的顺序,每次值域组合中依次多加入一个特征值。
CJK" | 负样本个数 | 正样本个数 |
1(黄) | 50 | 600 |
3(绿) | 30 | 300 |
0(红) | 100 | 200 |
2(蓝) | 200 | 100 |
(4)初始的L,R,lc,rc,初始认为L, lc为0,R, rc为总体。其中L为左边分支节点中正负样本个数和其相应权重的乘积和。R为右边分支节点中正负样本个数和其相应权重的乘积和。lc为左边分支节点中正样本个数和负样本个数,rc为右边分支节点中正样本个数和负样本个数。
L | 0 |
R | ∑weight=462 |
LC | 负样本个数 | 正样本个数 |
0 | 0 |
RC | 负样本个数 | 正样本个数 |
380 | 1200 |
(5)按照排序后的cjk顺序,依次按照不同的特征值进行分裂。
按照某个特征值域分裂后的吉尼不纯度为:
lsum2为分裂后左边节点的正,负样本个数和相应权重的乘积的平方和
rsum2为分裂后右边节点的正,负样本个数和相应权重的乘积的平方和。即:
lsum2=
rsum2=
(p0,p1分别为对应的负样本的权重和正样本权重,这里p0为9/10,p1为1/10)
注意,要选择出分类对应的吉尼不纯度最小,即为选出最大的值(L+R是固定值),因此我们用代表分裂的质量,其值越大,代表分裂质量越高。
a) 先按照黄色的特征值分裂,如果特征为黄色,进入左边分支,否则,仍在右边分支。
L | 105 |
R | 357(即462-105) |
LC | 负样本个数 | 正样本个数 |
50 | 600 |
RC | 负样本个数 | 正样本个数 |
330(380-50) | 600(1200-600) |
计算 = 310.74 (L = 105,R=357)
lsum2 = = 5625
rsum2= = 91809
b) 再按照绿色的特征值分裂,如果特征为绿色,进入左边分支,否则,仍在右边分支。
L | 162(105+57) |
R | 300(357-57) |
LC | 负样本个数 | 正样本个数 |
80(50+30) | 900(600+300) |
RC | 负样本个数 | 正样本个数 |
300(330-30) | 300(600-300) |
此时,再次计算 = 328 (L = 162,R=300)
lsum2 = = 13284
rsum2= = 73800
c) 再按照红色的特征值分裂,如果特征为红绿色,进入左边分支,否则,仍在右边分支。
L | 272(162+110) |
R | 190(300-110) |
LC | 负样本个数 | 正样本个数 |
180(80+100) | 1100(900+200) |
RC | 负样本个数 | 正样本个数 |
200(300-100) | 100(300-200) |
此时,再次计算 = 312.02 (L =272,R=190)
lsum2 = = 38344
rsum2= = 32500
(6)最终从步骤5中的所有分类组合中选出质量最高对应的分裂,也就是最终选出最小的吉尼不纯度的分裂。从上面计算的 可以得到5(b)步骤的分裂质量为328,在这三种分裂中质量最高。那么,颜色特征最终选择的分裂方式为(b),即颜色值为{黄,绿}进入左分支,颜色值为{红,蓝}进入右分支。这样颜色特征的分裂质量也为328。
【注】由于决策树和随机森林中所有正样本的权重一样,所有负样本的权重一样,因此决策树和随机森林都是用决策树的find_split_cat_class函数。然而boost中每个样本都各自的权重,因此,boost会重载find_split_cat_class函数,在boost重载的函数中cjk统计的第j个特征值对应的所有正样本的权重和和所有负样本的权重和,而不再是第j个特征值对应的所有正样本的个数和所有负样本的个数。在boost重载的函数中lcw统计分裂的左分支对应的正样本的权重和和对应负样本的权重和,rcw统计分裂的右分支对应的正样本的权重和和对应负样本的权重和,而不再是lc,rc中正样本的个数和负样本的个数。L为左边分支节点中正负样本个数和其相应权重的乘积和。R为右边分支节点中正负样本个数和其相应权重的乘积和。即L,R的统计含义没有变化。
其实,理论上boost这种统计是更符合实际定义的,只不过在决策树和随机森林中,由于决策树和随机森林中所有正样本的权重一样,所有负样本的权重一样,才简化了计算。决策树和随机森林中,虽然cjk统计的是个数,但是在最后计算分裂质量时,又将lc,rc中的正负样本个数分别乘以对应的权重,最终也转化为相应特征值对应正样本的权重和和对应负样本的权重和。
我们当然也可以在决策树和随机森林中使用boost的这种统计,这样就不需要在最后计算分裂质量时再将lc,rc中的正负样本个数分别乘以对应的权重了。
下面将分别展示一下决策树和boost中find_split_cat_class函数。
首先,决策树的find_split_cat_class函数,注意圈出的部分,cjk统计正样本个数和负样本个数。lsum2和rsum2取lc 和rc中正负样本的个数后,需要分别乘以对应的权重。(下面代码中m=2,代表分为正负样本)
下面是boost重载的find_split_cat_class函数,注意圈出的部分,cjk是统计第j个特征值对应的所有正样本的权重和以及所有负样本的权重和。
lsum2和rsum2直接取lcw 和rcw中正负样本的权重和
下面将决策树,随机森林,boost中重载函数一一说明。
决策树CvDTree类是父类,其他都是继承了这个类的。
决策树CvDTree的train函数核心是调用其do_train函数。
do_train函数又调用try_split_node函数, try_split_node函数是不断递归,从而完成决策树的训练。try_split_node函数每被调用一次,就会使用一次find_best_split函数寻找最优的特征进行分裂。
find_best_split函数会在所有特征中(也可以指定一些特征)寻找最优的一个特征进行分裂,即调用find_split_cat_class计算某个指定的特征的最优分裂。
bool CvDTree::train( CvDTreeTrainData* _data,const CvMat* _subsample_idx )
{
。。。。。。
CV_CALL(result = do_train(_subsample_idx));
}
bool CvDTree::do_train( const CvMat* _subsample_idx)
{
。。。。。。
root= data->subsample_data( _subsample_idx );
CV_CALL(try_split_node(root));
if(root->split )
{
CV_Assert( root->left );
CV_Assert( root->right );
if( data->params.cv_folds > 0 )
CV_CALL( prune_cv() );
if( !data->shared )
data->free_train_data();
result = true;
}
。。。。。。
}
void CvDTree::try_split_node( CvDTreeNode* node)
{
。。。。。。
calc_node_value(node );
。。。。。。。
if(can_split )
{
best_split = find_best_split(node);
node->split = best_split;
}
quality_scale= calc_node_dir( node );
。。。。。。
split_node_data(node );
try_split_node(node->left );
try_split_node(node->right );
}
CvDTreeSplit* CvDTree::find_best_split( CvDTreeNode*node )
{
// DTreeBestSplitFinder的operate接口中是对所有的的特征进行选择
//每个特征都调用find_split_cat_class接口,计算各自分裂的质量
DTreeBestSplitFinderfinder( this, node );
cv::parallel_reduce(cv::BlockedRange(0,data->var_count), finder);
CvDTreeSplit*bestSplit = 0;
if(finder.bestSplit->quality > 0 )
{
bestSplit = data->new_split_cat( 0, -1.0f );
memcpy( bestSplit, finder.bestSplit, finder.splitSize );
}
returnbestSplit;
}
find_split_cat_class函数的重载就不再列出,前面已经详细比较过了。
随机森林CvRTrees的train函数的核心是调用CvRTrees类的grow_forest函数。
grow_forest函数的核心是不断地在全部样本中随机抽样出nsamples个样本,然后将每次随机抽出的样本做为根节点数据训练出一棵棵随机决策树。每棵决策树的训练是使用CvForestTree的train函数。
CvForestTree的train函数核心使用了do_train函数,do_train函数并未重载,即使用CvDTree类的do_train。(因为决策树的do_train函数支持在所有样本中选择某些样本参与训练,即不用重载)
前面已经说过do_train函数是使用try_split_node函数不断递归完成决策树的训练。这里try_split_node函数没有被重载,仍使用决策树的try_split_node。
try_split_node函数每被调用一次,就会使用一次find_best_split函数寻找最优的特征进行分裂。由于随机森林的随机,不仅仅是样本的选择随机,还包括特征的选择随机,因此这里重载了find_best_split函数,在重载的函数中完成了特征的随机选择,最后在随机选择的特征中选择最优的特征进行分裂。
由于随机森林和决策树中正样本中各个样本的权重一样,负样本中各个样本的权重一样,因此寻找指定特征的最优分裂函数并未重载,即find_split_cat_class无需重载。
总之,重载的函数只有find_best_split函数。
bool CvRTrees::train( const CvMat*_train_data, int _tflag,
const CvMat*_responses, const CvMat* _var_idx,const CvMat* _sample_idx, const CvMat*_var_type, const CvMat* _missing_mask, CvRTParams params )
{
。。。。。。
return grow_forest( params.term_crit );
}
bool CvRTrees::grow_forest( constCvTermCriteria term_crit )
{
。。。。。。
trees= (CvForestTree**)cvAlloc( sizeof(trees[0])*max_ntrees );
while( ntrees < max_ntrees )
{
//一共生成nsamples个样本,有可能有重复样本。
//每次都是在所有的样本中随机抽样,因此可能某个样本被抽样了多次
for(i = 0; i < nsamples; i++ ) {
int idx = (*rng)(nsamples);
sample_idx_for_tree->data.i[i] = idx;//每次采样到的样本序号, sample_idx_mask_for_tree->data.ptr[idx]= 0xFF;//标识选中的样本
}
CvForestTree* tree = 0;
trees[ntrees] = new CvForestTree();
tree = trees[ntrees];
tree->train( data, sample_idx_for_tree, this );
ntrees++;
。。。。。。
}
}
bool CvForestTree::train( CvDTreeTrainData*_data,
const CvMat* _subsample_idx, CvRTrees*_forest )
{
。。。。。。
return do_train(_subsample_idx);
}
// do_train未重载,仍使用决策树的do_train函数
bool CvDTree::do_train( const CvMat*_subsample_idx )
{
。。。。。。
root = data->subsample_data( _subsample_idx );
CV_CALL(try_split_node(root));
}
// try_split_node函数中的find_best_split函数被重载
CvDTreeSplit* CvForestTree::find_best_split(CvDTreeNode* node )
{
。。。。。。
CvRNG* rng = forest->get_rng();
//随机选取m个特征,一般m是总特征数目的平方根
for( int vi = 0; vi < var_count;vi++ ){
uchar temp;
int i1 = cvRandInt(rng) % var_count;
int i2 = cvRandInt(rng) % var_count;
CV_SWAP( active_var_mask->data.ptr[i1],
active_var_mask->data.ptr[i2], temp );
}
//ForestTreeBestSplitFinder的operate接口中是对active_var_mask为1的特征
//进行选择,每个特征都调用find_split_cat_class接口,计算各自分裂的质量
cv::ForestTreeBestSplitFinderfinder( this, node );
cv::parallel_reduce(cv::BlockedRange(0,data->var_count), finder);
。。。。。。
}
CvBoost的train函数核心是不断更新每个样本的权重,然后用不同权重的样本训练决策树;接着再根据新训练的决策树对每个样本的预测结果和样本原来的标签比较,如果预测错误,则提高对应样本的权重,反之,则降低对应样本的权重。然后再次将更新了权重的样本重新训练(如果某些样本的权重小于指定的阈值权重,则这些样本不参与本轮的训练,但是并不代表这些样本不参与下一轮的训练,如果下次某些样本权重提高,则有可能参与下一轮训练),如此反复,直至决策树个数达到指定的个数,或者没有样本参加训练。这里每棵决策树的训练是使用CvBoostTree类的train函数。
CvBoostTree类的train函数核心使用了do_train函数,do_train函数并未重载,即使用CvDTree类的do_train。
前面已经说过do_train函数是使用try_split_node函数不断递归完成决策树的训练。这里try_split_node函数虽然被重载了,但是CvBoostTree类的try_split_node函数的核心仍然是调用CvDTree类的try_split_node,只是在CvDTree类的try_split_node调用完毕后,将参与训练的所有样本对应的分裂结果存储下来(如果一个样本分裂到某个叶子节点,则分裂结果为该叶子结对对应的 )。
try_split_node函数每被调用一次,就会使用一次find_best_split函数寻找最优的特征进行分裂。这里无需重载find_best_split函数。
由于boost中各个样本的权重都不一样,因此寻找指定特征的最优分裂函数必须,即重载了find_split_cat_class函数。
总之,重载的函数只有try_split_node函数,find_split_cat_class函数。
bool CvBoost::train( const CvMat* _train_data,int _tflag,const CvMat* _responses,
const CvMat* _var_idx, const CvMat* _sample_idx,const CvMat* _var_type,
const CvMat*_missing_mask, CvBoostParams _params, bool _update )
{
。。。。。。
update_weights(0 );
for(i = 0; i < params.weak_count; i++ ) {
CvBoostTree* tree = new CvBoostTree;
if( !tree->train( data, subsample_mask, this ) ) {
delete tree;
break;
}
cvSeqPush( weak, &tree );
update_weights( tree );
trim_weights();
if( cvCountNonZero(subsample_mask) == 0 )
break;
}
}
CvBoostTree::train( CvDTreeTrainData* _train_data,
const CvMat* _subsample_idx, CvBoost* _ensemble)
{
clear();
ensemble= _ensemble;
data= _train_data;
data->shared= true;
returndo_train( _subsample_idx );
}
// do_train未重载,仍使用决策树的do_train函数
bool CvDTree::do_train( const CvMat*_subsample_idx )
{
。。。。。。
root = data->subsample_data( _subsample_idx );
CV_CALL(try_split_node(root));
}
// try_split_node函数被重载
void CvBoostTree::try_split_node(CvDTreeNode* node )
{
CvDTree::try_split_node( node );
if( !node->left )
{
// if the node has not been split,
// store the responses for the corresponding training samples
double* weak_eval = ensemble->get_weak_response()->data.db;
cv::AutoBuffer<int> inn_buf(node->sample_count);
const int* labels = data->get_cv_labels( node, (int*)inn_buf );
int i, count = node->sample_count;
double value = node->value;
for( i = 0; i < count; i++ )
weak_eval[labels[i]] = value;
}
}
find_split_cat_class函数的重载就不再列出,前面已经详细比较过了。