在(3)中把opencv_traincascade在使用LBP特征的时候的训练准备工作的代码总结了下。下面开始硬着头皮看训练里面的部分了。介于这部分实在是没怎么找到网上介绍的帖子(为啥呢???)所以总结的大部分内容是自己猜测的。以后再回头慢慢完善。
接着上次结束的部分,训练一个强分类器的代码是:
bool CvCascadeBoost::train( const CvFeatureEvaluator* _featureEvaluator,//包含了sum,tilted,特征的位置等信息
int _numSamples,
int _precalcValBufSize, int _precalcIdxBufSize,
const CvCascadeBoostParams& _params )
{
CV_Assert( !data );
clear();
data = new CvCascadeBoostTrainData( _featureEvaluator, _numSamples,
_precalcValBufSize, _precalcIdxBufSize, _params );//目前暂且知道这里是调用preCalculate计算所有的特征值
CvMemStorage *storage = cvCreateMemStorage();
weak = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvBoostTree*), storage );
storage = 0;
set_params( _params );
if ( (_params.boost_type == LOGIT) || (_params.boost_type == GENTLE) )
data->do_responses_copy();
update_weights( 0 );
cout << "+----+---------+---------+" << endl;
cout << "| N | HR | FA |" << endl;
cout << "+----+---------+---------+" << endl;
do
{
CvCascadeBoostTree* tree = new CvCascadeBoostTree;
if( !tree->train( data, subsample_mask, this ) )//应该是训练一个弱分类器tree
{
delete tree;
break;
}
cvSeqPush( weak, &tree );//把弱分类器添加到强分类器里面
update_weights( tree );//AdaBoost里面更新weight部分。
trim_weights();
if( cvCountNonZero(subsample_mask) == 0 )
break;
}
while( !isErrDesired() && (weak->total < params.weak_count) );
data->is_classifier = true;
data->free_train_data();
return true;
}
这里面主要分为
调用preCalculate计算所有样本的所有的特征值
训练一个弱分类器
根据AdaBoost里面的步骤把样本的weight都更新一遍
主要看如何训练一个弱分类器。因为在(4)中总结output的xml文件结构的时候发现每一个node里面都包含了8个很大的数,后来在predict函数里看到这8个数是属于subset,在使用xml文件的时候用里面每一个feature算出来的特征值转换为二进制后是256个bit,然后跟这8个大数的最后32个位进行比较,如果有至少一个bit都是1的话那么就输出一个值,反之另一个值。在这需要指出的是,如果是haar-like feature的话用的不是subset而是threshold,后面比较后就一样了。而且haar-like基本上是一个feature一个弱分类器,而这里subset里可以有多个1出现,所以我理解着就是指有多个feature组成的subset。
而这些subset是如何求出来的呢,我跟踪程序总结如下。(里面用到好多opencv里面ml的数据结构,到现在感觉还是没有入门啊)
boost.cpp里:
bool
CvBoostTree::train( CvDTreeTrainData* _train_data,
const CvMat* _subsample_idx, CvBoost* _ensemble )
{
clear();
ensemble = _ensemble;
data = _train_data;
data->shared = true;
return do_train( _subsample_idx );//去CvDTree里训练弱分类器去
}
bool CvDTree::do_train( const CvMat* _subsample_idx )
{
bool result = false;
CV_FUNCNAME( "CvDTree::do_train" );
__BEGIN__;
root = data->subsample_data( _subsample_idx );
CV_CALL( try_split_node(root));
//在这把root的value啊,爹妈孩子都算出来的,其中跟分类器的子集(threshold)有关的split->subset就在此计算的。
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;
}
__END__;
return result;
}
在这里面主要包括:
找root(回头再研究)
分裂root。 try_split_node(root),就是在这把我想知道的那些数给算出来的,在opencv文档上看到这么一段话应该是对此的描述“决策树是从根结点递归构造的。用所有的训练数据(特征向量和对应的响应)来在根结点处进行分裂。在每个结点处,优化准则(比如最优分裂)是基于一些基本原则来确定的(比如ML中的“纯度purity”原则被用来进行分类,方差之和用来进行回归)。”
prune_cv() 估计是剪枝(回头再研究)
void CvDTree::try_split_node( CvDTreeNode* node )
{
CvDTreeSplit* best_split = 0;
int i, n = node->sample_count, vi;
bool can_split = true;
double quality_scale;
calc_node_value( node );//计算node->value
if( node->sample_count <= data->params.min_sample_count ||
node->depth >= data->params.max_depth )
can_split = false;
if( can_split && data->is_classifier )
{
// check if we have a "pure" node,
// we assume that cls_count is filled by calc_node_value()
int* cls_count = data->counts->data.i;
int nz = 0, m = data->get_num_classes();
for( i = 0; i < m; i++ )
nz += cls_count[i] != 0;
if( nz == 1 ) // there is only one class
can_split = false;
}
else if( can_split )
{
if( sqrt(node->node_risk)/n < data->params.regression_accuracy )
can_split = false;
}
if( can_split )
{
best_split = find_best_split(node);//可能是在这计算split->subset[]
// TODO: check the split quality ...
node->split = best_split;
}
if( !can_split || !best_split )
{
data->free_node_data(node);
return;
}
quality_scale = calc_node_dir( node );
if( data->params.use_surrogates )
{
// find all the surrogate splits
// and sort them by their similarity to the primary one
for( vi = 0; vi < data->var_count; vi++ )
{
CvDTreeSplit* split;
int ci = data->get_var_type(vi);
if( vi == best_split->var_idx )
continue;
if( ci >= 0 )
split = find_surrogate_split_cat( node, vi );
else
split = find_surrogate_split_ord( node, vi );
if( split )
{
// insert the split
CvDTreeSplit* prev_split = node->split;
split->quality = (float)(split->quality*quality_scale);
while( prev_split->next &&
prev_split->next->quality > split->quality )
prev_split = prev_split->next;
split->next = prev_split->next;
prev_split->next = split;
}
}
}
split_node_data( node );
try_split_node( node->left );///////
try_split_node( node->right );//////
/*
算法回归的继续分裂左右孩子结点。在以下情况下算法可能会在某个结点停止(i.e. stop splitting the node further):
树的深度达到了指定的最大值
在该结点训练样本的数目少于指定值,比如,没有统计意义上的集合来进一步进行结点分裂了。
在该结点所有的样本属于同一类(或者,如果是回归的话,变化已经非常小了)
跟随机选择相比,能选择到的最好的分裂已经基本没有什么有意义的改进了。
*/
}
CvDTreeSplit* CvDTree::find_best_split( CvDTreeNode* node )
{
DTreeBestSplitFinder finder( this, node );
cv::parallel_reduce(cv::BlockedRange(0, data->var_count), finder);//调用本cpp文件中的DTreeBestSplitFinder::operator()
CvDTreeSplit *bestSplit = 0;
if( finder.bestSplit->quality > 0 )
{
bestSplit = data->new_split_cat( 0, -1.0f );
memcpy( bestSplit, finder.bestSplit, finder.splitSize );
}
return bestSplit;
}
void DTreeBestSplitFinder::operator()(const BlockedRange& range)
{
int vi, vi1 = range.begin(), vi2 = range.end();
int n = node->sample_count;
CvDTreeTrainData* data = tree->get_data();
AutoBuffer inn_buf(2*n*(sizeof(int) + sizeof(float)));
for( vi = vi1; vi < vi2; vi++ )//{0,8464},就是一共有多少个feature,这个属于外面的循环,对于每一个feature
{
CvDTreeSplit *res;
int ci = data->get_var_type(vi);
if( node->get_num_valid(vi) <= 1 )
continue;
if( data->is_classifier )
{
if( ci >= 0 )
res = tree->find_split_cat_class( node, vi, bestSplit->quality, split, (uchar*)inn_buf );
else
res = tree->find_split_ord_class( node, vi, bestSplit->quality, split, (uchar*)inn_buf );
}
else
{
if( ci >= 0 )
res = tree->find_split_cat_reg( node, vi, bestSplit->quality, split, (uchar*)inn_buf );//计算split 调用的是CvDTreeSplit* CvBoostTree::find_split_cat_reg
else
res = tree->find_split_ord_reg( node, vi, bestSplit->quality, split, (uchar*)inn_buf );
}
if( res && bestSplit->quality < split->quality )
memcpy( (CvDTreeSplit*)bestSplit, (CvDTreeSplit*)split, splitSize );//更新bestSplit选取那个最好的feature
}
}
CvDTreeSplit*
CvBoostTree::find_split_cat_reg( CvDTreeNode* node, int vi, float init_quality, CvDTreeSplit* _split, uchar* _ext_buf )//cat好像是categary的意思,reg是regression的意思。
{
const double* weights = ensemble->get_subtree_weights()->data.db;
int ci = data->get_var_type(vi);
int n = node->sample_count;//正负样本个数
int mi = data->cat_count->data.i[ci];
int base_size = (2*mi+3)*sizeof(double) + mi*sizeof(double*);
cv::AutoBuffer inn_buf(base_size);
if( !_ext_buf )
inn_buf.allocate(base_size + n*(2*sizeof(int) + sizeof(float)));
uchar* base_buf = (uchar*)inn_buf;
uchar* ext_buf = _ext_buf ? _ext_buf : base_buf + base_size;
int* cat_labels_buf = (int*)ext_buf;
const int* cat_labels = data->get_cat_var_data(node, vi, cat_labels_buf);//返回的是1 x n的矩阵,里面是所有样本的第vi个feature的特征值
float* responses_buf = (float*)(cat_labels_buf + n);
int* sample_indices_buf = (int*)(responses_buf + n);
const float* responses = data->get_ord_responses(node, responses_buf, sample_indices_buf);//response就是类别{1,-1}
double* sum = (double*)cv::alignPtr(base_buf,sizeof(double)) + 1;
double* counts = sum + mi + 1;
double** sum_ptr = (double**)(counts + mi);
double L = 0, R = 0, best_val = init_quality, lsum = 0, rsum = 0;
int i, best_subset = -1, subset_i;
for( i = -1; i < mi; i++ )//mi 256,LBP一共256个category
sum[i] = counts[i] = 0;
// calculate sum response and weight of each category of the input var
for( i = 0; i < n; i++ )//对于所有正负样本
{
int idx = ((cat_labels[i] == 65535) && data->is_buf_16u) ? -1 : cat_labels[i];//对于LBP来说,这个idx都属于[0,255]
double w = weights[i];
double s = sum[idx] + responses[i]*w;//把weight给乘以(+1)或(-1)求和
double nc = counts[idx] + w;//纯粹weight求和
sum[idx] = s;
counts[idx] = nc;
}
// calculate average response in each category
for( i = 0; i < mi; i++ )//对于LBP的256个category
{
R += counts[i];
rsum += sum[i];
sum[i] = fabs(counts[i]) > DBL_EPSILON ? sum[i]/counts[i] : 0;//求category里的平均值,要是这个category不太重要的话那么这个值估计会比较小(极端点考虑如果一半是正样本,一半是负样本那正好为0)
sum_ptr[i] = sum + i;
}
icvSortDblPtr( sum_ptr, mi, 0 );//把256个值进行升序排序,注意sum_ptr里存的是sum[i]的地址
// revert back to unnormalized sums
// (there should be a very little loss in accuracy)
for( i = 0; i < mi; i++ )
sum[i] *= counts[i];
for( subset_i = 0; subset_i < mi-1; subset_i++ )
{
int idx = (int)(sum_ptr[subset_i] - sum);//表示排序之后的第subset_i个在排序前是第idx个
double ni = counts[idx];
if( ni > FLT_EPSILON )
{
double s = sum[idx];
lsum += s; L += ni;
rsum -= s; R -= ni;
if( L > FLT_EPSILON && R > FLT_EPSILON )
{
double val = (lsum*lsum*R + rsum*rsum*L)/(L*R);//要赋值给下面的quality,然后外层的每个feature的循环里还要用到
if( best_val < val )
{
best_val = val;
best_subset = subset_i;
}
}
}
}
CvDTreeSplit* split = 0;
if( best_subset >= 0 )
{
split = _split ? _split : data->new_split_cat( 0, -1.0f);
split->var_idx = vi;//表示这是利用的第几个feature
split->quality = (float)best_val;//上层的每个feature的循环里要用来比较好选择哪个feature
memset( split->subset, 0, (data->max_c_count + 31)/32 * sizeof(int));
for( i = 0; i <= best_subset; i++ )
{
int idx = (int)(sum_ptr[i] - sum);
split->subset[idx >> 5] |= 1 << (idx & 31);
//在这里把256个bin放到8个subset中(8 x 32 = 256),每个subset里面包含32位信息来表示这32个情况是存在与否,
//但是我的问题是为啥subset定义的大小是2,根本不够使的啊。
}
}
return split;
}
个人感觉sum[i] = fabs(counts[i]) > DBL_EPSILON ? sum[i]/counts[i] : 0;是[1] 里的Eq.4,但是double val = (lsum*lsum*R + rsum*rsum*L)/(L*R);这个公式从哪来的不明白,[1]里对应的应该是Eq.2才对啊。
在xml文件里除了subset还有threshold和output值,关于threshold的思路跟haartraing差不多,关于output值(即left->value 和 right->value)是在boost.cpp的calc_node_value(CvDTreeNode *n)中的。主要代码是:
{
// in case of regression tree:
// * node value is 1/n*sum_i(Y_i), where Y_i is i-th response,
// n is the number of samples in the node.
// * node risk is the sum of squared errors: sum_i((Y_i - )^2)
double sum = 0, sum2 = 0, iw;
float* values_buf = (float*)(labels_buf + n);
int* sample_indices_buf = (int*)(values_buf + n);
const float* values = data->get_ord_responses(node, values_buf, sample_indices_buf);
for( i = 0; i < n; i++ )
{
int idx = labels[i];
double w = weights[idx]/*priors[values[i] > 0]*/;
double t = values[i];//{-1,1}
rcw[0] += w;
subtree_weights[i] = w;
sum += t*w;
sum2 += t*t*w;
}
iw = 1./rcw[0];
node->value = sum*iw;
node->node_risk = sum2 - (sum*iw)*sum;
// renormalize the risk, as in try_split_node the unweighted formula
// sqrt(risk)/n is used, rather than sqrt(risk)/sum(weights_i)
node->node_risk *= n*iw*n*iw;
}
【1】 Face Detection Based on Multi-Block LBP Representation, L.Zhang