之前介绍了haartraining程序中的cvCreateMTStumpClassifier函数,这个函数的功能是计算最优弱分类器,这篇文章介绍一下自己对haartraining中关于强分类器计算的一些理解,也就是程序中的icvCreateCARTStageClassifier函数。
在给出代码之前,说几处自认为值得说说的问题:
1. 由于haartraining是基于HAAR特征进行adaboost训练,对于HAAR特征的处理比较繁琐,采用了奇数弱分类器补充针对翻转特征最优弱分类器计算的代码,所以代码看起来较为冗长。
2. 创建强分类器时,其中包含有样本权值的更新,代码中共提供了四种经典adaboost算法版本,它们是Discrete Adaboost、Real Adaboost、Logit Boost、Gentle Adaboost。每种算法的权值更新策略不同,这方面的知识建议大家下载几篇博士论文看看,也可以看看我之前发的博客。
http://blog.csdn.net/wsj998689aa/article/details/42242565
3. 代码较多地通过函数指针的形式(实际上这也是opencv一直常用的手段)对函数进行回调。
4. 小权值样本需要剔除掉,因为小权值样本对训练结果的影响微乎其微,加了它们反而要耗时不少。这边有一处需要提醒大家,当前分类器剔除掉的小权值样本,仍旧参与下一个分类器中样本权值的剔除,换句话说,每一个弱分类器,其实都要对所有的样本权值进行排序,所以会造成实际训练样本比例出现“跳变”的情况,但是总体走势还是始终下降的。如下图所示:
5. 代码中采用了较多的中间结构体变量,例如CvIntHaarClassifier结构体(用于模拟强分类器结构体CvStageHaarClassifier的父类),CvBoostTrainer结构体(用于初始化,更新样本权值等)等等,看起来比较绕。
6. 关于弱分类器的创建,事先创建的其实是CART分类器,CART分类器就是一棵树,每个节点代表一个最优Haar特征,但是一般程序中的节点个数都设置为1,所以一个CART就相当于stump了,此外,CART的创建涉及到节点的分裂,通过icvSplitIndicesCallback函数实现。
7. 在创建CART的时候,最优Haar特征其实就已经被选择好了,至于下面还有一个stumpConstructor函数,是由于Haar特征被翻转而产生了新特征,所以需要重新寻找最优弱分类器。
8. 有意思的是,函数输入了最小正检率和最大误检率,前者决定了判定样本是正类还是负类的阈值,后者决定了强分类器是否能够收敛,具体来说吧,先计算当前最优弱分类器关于每个正样本的置信度,然后对置信度进行排序,这个阈值就是基于最小正检率选择的置信度,然后在根据这个阈值来计算当前最优弱分类器的误检率(计算每个负样本的置信度),如果大于了输入的最大误检率,那么OK!!一串弱分类器所构成的强分类器诞生了。
以上说的就是icvCreateCARTStageClassifier中值得注意的几点,下面上代码,是根据自己的理解添加的注释,请各位不吝批评指正哈!
转载请注明:http://blog.csdn.net/wsj998689aa/article/details/42398235
static
CvIntHaarClassifier* icvCreateCARTStageClassifier( CvHaarTrainingData* data, // 全部训练样本
CvMat* sampleIdx, // 实际训练样本序列
CvIntHaarFeatures* haarFeatures, // 全部HAAR特征
float minhitrate, // 最小正检率(用于确定强分类器阈值)
float maxfalsealarm, // 最大误检率(用于确定是否收敛)
int symmetric, // HAAR是否对称
float weightfraction, // 样本剔除比例(用于剔除小权值样本)
int numsplits, // 每个弱分类器特征个数(一般为1)
CvBoostType boosttype, // adaboost类型
CvStumpError stumperror, // Discrete AdaBoost中的阈值计算方式
int maxsplits ) // 弱分类器最大个数
{
#ifdef CV_COL_ARRANGEMENT
int flags = CV_COL_SAMPLE;
#else
int flags = CV_ROW_SAMPLE;
#endif
CvStageHaarClassifier* stage = NULL; // 强分类器
CvBoostTrainer* trainer; // 临时训练器,用于更新样本权值
CvCARTClassifier* cart = NULL; // 弱分类器
CvCARTTrainParams trainParams; // 训练参数
CvMTStumpTrainParams stumpTrainParams; // 弱分类器参数
//CvMat* trainData = NULL;
//CvMat* sortedIdx = NULL;
CvMat eval; // 临时矩阵
int n = 0; // 特征总数
int m = 0; // 总样本个数
int numpos = 0; // 正样本个数
int numneg = 0; // 负样本个数
int numfalse = 0; // 误检样本个数
float sum_stage = 0.0F; // 置信度累积和
float threshold = 0.0F; // 强分类器阈值
float falsealarm = 0.0F; // 误检率
//CvMat* sampleIdx = NULL;
CvMat* trimmedIdx; // 剔除小权值之后的样本序列
//float* idxdata = NULL;
//float* tempweights = NULL;
//int idxcount = 0;
CvUserdata userdata; // 训练数据
int i = 0;
int j = 0;
int idx;
int numsamples; // 实际样本个数
int numtrimmed; // 剔除小权值之后的样本个数
CvCARTHaarClassifier* classifier; // 弱分类器
CvSeq* seq = NULL;
CvMemStorage* storage = NULL;
CvMat* weakTrainVals; // 样本类别,只有logitboost才会用到
float alpha;
float sumalpha;
int num_splits; // 弱分类器个数
#ifdef CV_VERBOSE
printf( "+----+----+-+---------+---------+---------+---------+\n" );
printf( "| N |%%SMP|F| ST.THR | HR | FA | EXP. ERR|\n" );
printf( "+----+----+-+---------+---------+---------+---------+\n" );
#endif /* CV_VERBOSE */
n = haarFeatures->count;
m = data->sum.rows;
numsamples = (sampleIdx) ? MAX( sampleIdx->rows, sampleIdx->cols ) : m;
// 样本与HAAR特征
userdata = cvUserdata( data, haarFeatures );
/* 弱分类参数设置 */
stumpTrainParams.type = ( boosttype == CV_DABCLASS )
? CV_CLASSIFICATION_CLASS : CV_REGRESSION; // 分类或者回归
stumpTrainParams.error = ( boosttype == CV_LBCLASS || boosttype == CV_GABCLASS )
? CV_SQUARE : stumperror; // 弱分类器阈值计算方式
stumpTrainParams.portion = CV_STUMP_TRAIN_PORTION; // 每组特征个数
stumpTrainParams.getTrainData = icvGetTrainingDataCallback; // 计算样本的haar值
stumpTrainParams.numcomp = n; // 特征个数
stumpTrainParams.userdata = &userdata;
stumpTrainParams.sortedIdx = data->idxcache; // 特征-样本序号矩阵(排序之后)
// 由于参数众多,所以创建参数结构体
trainParams.count = numsplits; // 弱分类器特征树
trainParams.stumpTrainParams = (CvClassifierTrainParams*) &stumpTrainParams;// 弱分类参数
trainParams.stumpConstructor = cvCreateMTStumpClassifier; // 筛选最优弱分类器
trainParams.splitIdx = icvSplitIndicesCallback; // CART节点分裂函数
trainParams.userdata = &userdata;
// 临时向量,用于存放样本haar特征值
eval = cvMat( 1, m, CV_32FC1, cvAlloc( sizeof( float ) * m ) );
storage = cvCreateMemStorage();
// 最优弱分类器存储序列
seq = cvCreateSeq( 0, sizeof( *seq ), sizeof( classifier ), storage );
// 样本类别,只有logitboost才会用到
weakTrainVals = cvCreateMat( 1, m, CV_32FC1 );
// 初始化样本类别与权重,weakTrainVals为{-1, 1},权重都一样
trainer = cvBoostStartTraining( &data->cls, weakTrainVals, &data->weights,
sampleIdx, boosttype );
num_splits = 0;
sumalpha = 0.0F;
do
{
#ifdef CV_VERBOSE
int v_wt = 0;
int v_flipped = 0;
#endif /* CV_VERBOSE */
// 剔除小权值样本
trimmedIdx = cvTrimWeights( &data->weights, sampleIdx, weightfraction );
// 实际样本总数
numtrimmed = (trimmedIdx) ? MAX( trimmedIdx->rows, trimmedIdx->cols ) : m;
#ifdef CV_VERBOSE
v_wt = 100 * numtrimmed / numsamples;
v_flipped = 0;
#endif /* CV_VERBOSE */
// 重要函数,创建CART树的同时,当前最优弱分类器出炉,一般只有根节点
cart = (CvCARTClassifier*) cvCreateCARTClassifier( data->valcache,
flags,
weakTrainVals, 0, 0, 0, trimmedIdx,
&(data->weights),
(CvClassifierTrainParams*) &trainParams );
// 创建弱分类器
classifier = (CvCARTHaarClassifier*) icvCreateCARTHaarClassifier( numsplits );
// 将CART树转化为弱分类器
icvInitCARTHaarClassifier( classifier, cart, haarFeatures );
num_splits += classifier->count;
cart->release( (CvClassifier**) &cart );
// 为何一定要在奇数个弱分类器处计算?
if( symmetric && (seq->total % 2) )
{
float normfactor = 0.0F;
CvStumpClassifier* stump;
/* 翻转HAAR特征 */
for( i = 0; i < classifier->count; i++ )
{
if( classifier->feature[i].desc[0] == 'h' )
{
for( j = 0; j < CV_HAAR_FEATURE_MAX &&
classifier->feature[i].rect[j].weight != 0.0F; j++ )
{
classifier->feature[i].rect[j].r.x = data->winsize.width -
classifier->feature[i].rect[j].r.x -
classifier->feature[i].rect[j].r.width;
}
}
else
{
int tmp = 0;
/* (x,y) -> (24-x,y) */
/* w -> h; h -> w */
for( j = 0; j < CV_HAAR_FEATURE_MAX &&
classifier->feature[i].rect[j].weight != 0.0F; j++ )
{
classifier->feature[i].rect[j].r.x = data->winsize.width -
classifier->feature[i].rect[j].r.x;
CV_SWAP( classifier->feature[i].rect[j].r.width,
classifier->feature[i].rect[j].r.height, tmp );
}
}
}
// 转化为基于积分图计算的特征
icvConvertToFastHaarFeature( classifier->feature,
classifier->fastfeature,
classifier->count, data->winsize.width + 1 );
// 为了验证最新翻转特征是否为最优特征
stumpTrainParams.getTrainData = NULL;
stumpTrainParams.numcomp = 1;
stumpTrainParams.userdata = NULL;
stumpTrainParams.sortedIdx = NULL;
// 验证是否新生成的特征可作为最优弱分类器
for( i = 0; i < classifier->count; i++ )
{
for( j = 0; j < numtrimmed; j++ )
{
// 获取训练样本
idx = icvGetIdxAt( trimmedIdx, j );
// 对每个训练样本计算Haar特征
eval.data.fl[idx] = cvEvalFastHaarFeature( &classifier->fastfeature[i],
(sum_type*) (data->sum.data.ptr + idx * data->sum.step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step) );
// 归一化因子
normfactor = data->normfactor.data.fl[idx];
// 对Haar特征归一化
eval.data.fl[idx] = ( normfactor == 0.0F )
? 0.0F : (eval.data.fl[idx] / normfactor);
}
// 计算最优弱分类器
stump = (CvStumpClassifier*) trainParams.stumpConstructor( &eval,
CV_COL_SAMPLE,
weakTrainVals, 0, 0, 0, trimmedIdx,
&(data->weights),
trainParams.stumpTrainParams );
classifier->threshold[i] = stump->threshold; // 阈值
if( classifier->left[i] <= 0 )
{
classifier->val[-classifier->left[i]] = stump->left; // 左分支输出置信度
}
if( classifier->right[i] <= 0 )
{
classifier->val[-classifier->right[i]] = stump->right; // 右分支输出置信度
}
stump->release( (CvClassifier**) &stump );
}
// 还原参数,参数支持cvCreateCARTClassifier函数
stumpTrainParams.getTrainData = icvGetTrainingDataCallback;
stumpTrainParams.numcomp = n;
stumpTrainParams.userdata = &userdata;
stumpTrainParams.sortedIdx = data->idxcache;
#ifdef CV_VERBOSE
v_flipped = 1;
#endif /* CV_VERBOSE */
} /* if symmetric */
if( trimmedIdx != sampleIdx )
{
cvReleaseMat( &trimmedIdx );
trimmedIdx = NULL;
}
// 调用icvEvalCARTHaarClassifier函数,计算每个样本的当前最优弱分类器置信度
for( i = 0; i < numsamples; i++ )
{
idx = icvGetIdxAt( sampleIdx, i );
eval.data.fl[idx] = classifier->eval( (CvIntHaarClassifier*) classifier,
(sum_type*) (data->sum.data.ptr + idx * data->sum.step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),
data->normfactor.data.fl[idx] );
}
// 更新样本权重,如果是LogitBoost,也会更新weakTrainVals,函数返回的是弱分类器权重
alpha = cvBoostNextWeakClassifier( &eval, &data->cls, weakTrainVals,
&data->weights, trainer );
// 这个变量没什么用
sumalpha += alpha;
for( i = 0; i <= classifier->count; i++ )
{
if( boosttype == CV_RABCLASS )
{
classifier->val[i] = cvLogRatio( classifier->val[i] );
}
classifier->val[i] *= alpha;
}
// 添加弱分类器
cvSeqPush( seq, (void*) &classifier );
// 正样本个数
numpos = 0;
// 遍历sampleIdx中所有样本,计算每个样本的弱分类器置信度和
for( i = 0; i < numsamples; i++ )
{
// 获得样本序号
idx = icvGetIdxAt( sampleIdx, i );
// 如果样本为正样本
if( data->cls.data.fl[idx] == 1.0F )
{
// 初始化置信度值
eval.data.fl[numpos] = 0.0F;
// 遍历seq中所有弱分类器
for( j = 0; j < seq->total; j++ )
{
// 获取弱分类器
classifier = *((CvCARTHaarClassifier**) cvGetSeqElem( seq, j ));
// 累积当前正样本的弱分类器置信度和
eval.data.fl[numpos] += classifier->eval(
(CvIntHaarClassifier*) classifier,
(sum_type*) (data->sum.data.ptr + idx * data->sum.step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),
data->normfactor.data.fl[idx] );
}
/* eval.data.fl[numpos] = 2.0F * eval.data.fl[numpos] - seq->total; */
numpos++;
}
}
// 对弱分类器输出置信度和进行排序
icvSort_32f( eval.data.fl, numpos, 0 );
// 计算阈值,应该是大于threshold则为正类,小于threshold则为负类
threshold = eval.data.fl[(int) ((1.0F - minhitrate) * numpos)];
numneg = 0;
numfalse = 0;
// 遍历所有样本,统计错分负样本个数
for( i = 0; i < numsamples; i++ )
{
idx = icvGetIdxAt( sampleIdx, i );
// 如果样本为负样本
if( data->cls.data.fl[idx] == 0.0F )
{
numneg++;
sum_stage = 0.0F;
// 遍历seq中所有弱分类器
for( j = 0; j < seq->total; j++ )
{
classifier = *((CvCARTHaarClassifier**) cvGetSeqElem( seq, j ));
// 累积当前负样本的分类器输出结果
sum_stage += classifier->eval( (CvIntHaarClassifier*) classifier,
(sum_type*) (data->sum.data.ptr + idx * data->sum.step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),
data->normfactor.data.fl[idx] );
}
/* sum_stage = 2.0F * sum_stage - seq->total; */
// 因为小于threshold为负类,所以下面是分类错误的情况
if( sum_stage >= (threshold - CV_THRESHOLD_EPS) )
{
numfalse++;
}
}
}
// 计算虚警率
falsealarm = ((float) numfalse) / ((float) numneg);
// 输出内容
#ifdef CV_VERBOSE
{
// 正样本检出率
float v_hitrate = 0.0F;
// 负样本误检率
float v_falsealarm = 0.0F;
/* expected error of stage classifier regardless threshold */
// 这是什么?
float v_experr = 0.0F;
// 遍历所有样本
for( i = 0; i < numsamples; i++ )
{
idx = icvGetIdxAt( sampleIdx, i );
sum_stage = 0.0F;
// 遍历seq中所有弱分类器
for( j = 0; j < seq->total; j++ )
{
classifier = *((CvCARTHaarClassifier**) cvGetSeqElem( seq, j ));
sum_stage += classifier->eval( (CvIntHaarClassifier*) classifier,
(sum_type*) (data->sum.data.ptr + idx * data->sum.step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),
data->normfactor.data.fl[idx] );
}
/* sum_stage = 2.0F * sum_stage - seq->total; */
// 只需要判断单一分支即可
if( sum_stage >= (threshold - CV_THRESHOLD_EPS) )
{
if( data->cls.data.fl[idx] == 1.0F )
{
v_hitrate += 1.0F;
}
else
{
v_falsealarm += 1.0F;
}
}
// 正类样本的sum_stage必须大于0
if( ( sum_stage >= 0.0F ) != (data->cls.data.fl[idx] == 1.0F) )
{
v_experr += 1.0F;
}
}
v_experr /= numsamples;
printf( "|%4d|%3d%%|%c|%9f|%9f|%9f|%9f|\n",
seq->total, v_wt, ( (v_flipped) ? '+' : '-' ),
threshold, v_hitrate / numpos, v_falsealarm / numneg,
v_experr );
printf( "+----+----+-+---------+---------+---------+---------+\n" );
fflush( stdout );
}
#endif /* CV_VERBOSE */
// 两种收敛方式,一种是误检率小于规定阈值,另一种是弱分类器个数小于规定阈值
} while( falsealarm > maxfalsealarm && (!maxsplits || (num_splits < maxsplits) ) );
cvBoostEndTraining( &trainer );
if( falsealarm > maxfalsealarm )
{
// 如果弱分类器达到上限而收敛,则放弃当前强分类器
stage = NULL;
}
else
{
// 创建当前强分类器
stage = (CvStageHaarClassifier*) icvCreateStageHaarClassifier( seq->total,
threshold );
// 保存当前强分类器
cvCvtSeqToArray( seq, (CvArr*) stage->classifier );
}
/* CLEANUP */
cvReleaseMemStorage( &storage );
cvReleaseMat( &weakTrainVals );
cvFree( &(eval.data.ptr) );
return (CvIntHaarClassifier*) stage;
}