cvCreateCARTClassifier函數在haartraining程序中用於創建CART樹狀弱分類器,但一般只採用單一節點的CART分類器,即樁分類器,一個多節點的CART分類器訓練耗時很多。根據自己的測試,要等差不多10分鐘(2000正樣本、2000負樣本)才能訓練完一個3節點的弱分類器,當然,總體的樹狀弱分類器的數目可能也會減少1/2。之所以將此函數拿出來說說,主要是因爲在網上找不到針對這個函數的詳細說明,同時,CART的應用十分廣泛,自己也趁這個機會好好學學,把自己的一點理解分享給大家。
1. 先說說CART樹的設計問題,也就是CvCARTClassifier這個結構體,結構體中變量的意義着實讓我傷了一番腦筋。現添加其變量含義,如下:
typedef struct CvCARTClassifier
{
CV_CLASSIFIER_FIELDS()
/* number of internal nodes */
int count; // 非葉子節點個數
/* internal nodes (each array of <count> elements) */
int* compidx; // 節點所採用的最優Haar特徵序號
float* threshold; // 節點所採用的最優Haar特徵閾值
int* left; // 非葉子節點的左子節點序號(葉子節點爲負數,非葉子節點爲正數)
int* right; // 非葉子節點的右子節點序號(葉子節點爲負數,非葉子節點爲正數)
/* leaves (array of <count>+1 elements) */
float* val; // 葉子節點輸出置信度
} CvCARTClassifier;
其中,count就是main主函數中的參數nsplits,用於定義的是非葉子節點數,或者叫做中間節點數。個人認爲,這樣設計一棵樹很科學,將非葉子節點與葉子節點分開表述,結構體十分簡潔,只不過當時left的真實含義讓我琢磨了挺長時間。
2. cvCreateCARTClassifier中節點分裂的“候選屬性集合”仍舊是Haar特徵,“分類準則”是分類錯誤率(error)的下降程度,這個函數中分類準則的具體度量是:父節點的子節點自身的error減去 該子節點的兩個子節點的error之和,這個變量在代碼中由errdrop表示。
3. CART樹狀分類器的形式多種多樣,就3個非葉子節點來說,我調試之後,就遇到了如下兩種弱分類器:
4. 不過,該函數的節點添加方式和普通的cart樹有所不同,多了一步對左右兩個子節點分裂進一步篩選的過程。這點我還沒有參透作者的用意,我的意思是,如果不篩選,有何不妥?
5. 可能有童鞋會問,爲什麼要採用樹狀的弱分類器,我的理解是,一個樹狀的分類器在測試過程中,特徵比較的次數相對串行的弱分類器要少很多,比如說,3個串行的Haar特徵,比較次數是3次,但是如果是一顆3節點的CART樹,比較次數可能只需要兩次。並且, 一個樹狀弱分類器中,子節點針對的數據集更加具體,具有針對性,可能精度會更高。
以上就是自己對cvCreateCARTClassifier函數的理解,帶有註釋的源代碼如下所示:
(轉載請註明:http://blog.csdn.net/wsj998689aa/article/details/43411809)
CV_BOOST_IMPL
CvClassifier* cvCreateCARTClassifier( CvMat* trainData, // 訓練樣本特徵值矩陣
int flags, // 樣本按行排列
CvMat* trainClasses, // 訓練樣本類別向量
CvMat* typeMask,
CvMat* missedMeasurementsMask,
CvMat* compIdx, // 特徵序列向量
CvMat* sampleIdx, // 樣本序列向量
CvMat* weights, // 樣本權值向量
CvClassifierTrainParams* trainParams ) // 參數
{
CvCARTClassifier* cart = NULL; // CART樹狀弱分類器
size_t datasize = 0;
int count = 0; // CART中的節點數目
int i = 0;
int j = 0;
CvCARTNode* intnode = NULL; // CART節點
CvCARTNode* list = NULL; // 候選節點鏈表
int listcount = 0; // 候選節點個數
CvMat* lidx = NULL; // 左子節點樣本序列
CvMat* ridx = NULL; // 右子節點樣本序列
float maxerrdrop = 0.0F;
int idx = 0;
// 設置節點分裂函數指針
void (*splitIdxCallback)( int compidx, float threshold,
CvMat* idx, CvMat** left, CvMat** right,
void* userdata );
void* userdata;
// 設置非葉子節點個數
count = ((CvCARTTrainParams*) trainParams)->count;
assert( count > 0 );
datasize = sizeof( *cart ) + (sizeof( float ) + 3 * sizeof( int )) * count +
sizeof( float ) * (count + 1);
cart = (CvCARTClassifier*) cvAlloc( datasize );
memset( cart, 0, datasize );
cart->count = count;
// 輸出當前樣本的置信度
cart->eval = cvEvalCARTClassifier;
cart->save = NULL;
cart->release = cvReleaseCARTClassifier;
cart->compidx = (int*) (cart + 1); // 非葉子節點的最優Haar特徵序號
cart->threshold = (float*) (cart->compidx + count); // 非葉子節點的最優Haar特徵閾值
cart->left = (int*) (cart->threshold + count); // 左子節點序號,包含葉子節點序號
cart->right = (int*) (cart->left + count); // 右子節點序號,包含葉子節點序號
cart->val = (float*) (cart->right + count); // 葉子節點輸出置信度
datasize = sizeof( CvCARTNode ) * (count + count);
intnode = (CvCARTNode*) cvAlloc( datasize );
memset( intnode, 0, datasize );
list = (CvCARTNode*) (intnode + count);
// 節點分裂函數指針,一般爲icvSplitIndicesCallback函數
splitIdxCallback = ((CvCARTTrainParams*) trainParams)->splitIdx;
userdata = ((CvCARTTrainParams*) trainParams)->userdata;
// R代表樣本按行排列,C代表樣本按列排列
if( splitIdxCallback == NULL )
{
splitIdxCallback = ( CV_IS_ROW_SAMPLE( flags ) )
? icvDefaultSplitIdx_R : icvDefaultSplitIdx_C;
userdata = trainData;
}
// 創建CART根節點
intnode[0].sampleIdx = sampleIdx;
intnode[0].stump = (CvStumpClassifier*)
((CvCARTTrainParams*) trainParams)->stumpConstructor( trainData, flags,
trainClasses, typeMask, missedMeasurementsMask, compIdx, sampleIdx, weights,
((CvCARTTrainParams*) trainParams)->stumpTrainParams );
cart->left[0] = cart->right[0] = 0;
// 創建樹狀弱分類器,lerror或者rerror不爲0代表着當前節點爲非葉子節點
listcount = 0;
for( i = 1; i < count; i++ )
{
// 基於當前節點弱分類器閾值,對當前節點進行分裂
// 留意lidx和ridx,它們指的是左右子節點各自的樣本序列
splitIdxCallback( intnode[i-1].stump->compidx, intnode[i-1].stump->threshold,
intnode[i-1].sampleIdx, &lidx, &ridx, userdata );
// 爲分裂之後的非葉子節點計算最優特徵
if( intnode[i-1].stump->lerror != 0.0F )
{
// 小於閾值的樣本集合
list[listcount].sampleIdx = lidx;
// 基於新樣本集合尋找最優特徵
list[listcount].stump = (CvStumpClassifier*)
((CvCARTTrainParams*) trainParams)->stumpConstructor( trainData, flags,
trainClasses, typeMask, missedMeasurementsMask, compIdx,
list[listcount].sampleIdx,
weights, ((CvCARTTrainParams*) trainParams)->stumpTrainParams );
// 計算信息增益(這裏是error的下降程度)
list[listcount].errdrop = intnode[i-1].stump->lerror
- (list[listcount].stump->lerror + list[listcount].stump->rerror);
list[listcount].leftflag = 1;
list[listcount].parent = i-1;
listcount++;
}
else
{
cvReleaseMat( &lidx );
}
// 同上,左分支換成右分支,偏向於右分支
if( intnode[i-1].stump->rerror != 0.0F )
{
list[listcount].sampleIdx = ridx;
list[listcount].stump = (CvStumpClassifier*)
((CvCARTTrainParams*) trainParams)->stumpConstructor( trainData, flags,
trainClasses, typeMask, missedMeasurementsMask, compIdx,
list[listcount].sampleIdx,
weights, ((CvCARTTrainParams*) trainParams)->stumpTrainParams );
list[listcount].errdrop = intnode[i-1].stump->rerror
- (list[listcount].stump->lerror + list[listcount].stump->rerror);
list[listcount].leftflag = 0; // 辨識當前節點是左還是右
list[listcount].parent = i-1;
listcount++;
}
else
{
cvReleaseMat( &ridx );
}
if( listcount == 0 ) break;
idx = 0;
maxerrdrop = list[idx].errdrop;
for( j = 1; j < listcount; j++ )
{
if( list[j].errdrop > maxerrdrop )
{
idx = j; // 這裏被忽略了,樹中真正的節點是由idx決定的
maxerrdrop = list[j].errdrop;
}
}
// 添加新節點
intnode[i] = list[idx];
// 確定當前節點的非葉子子節點的序號
if( list[idx].leftflag )
{
cart->left[list[idx].parent] = i;
}
else
{
cart->right[list[idx].parent] = i;
}
// 此處需要注意,將候選集合中的選定節點刪除,刪除位置用倒數第二個候選節點替代
if( idx != (listcount - 1) )
{
list[idx] = list[listcount - 1];
}
listcount--;
}
// 這段代碼用於確定樹中節點最優特徵序號、閾值與葉子節點序號和輸出置信度
// left與right大於等於0,爲0代表葉子節點
// 就算CART中只有一個節點,仍舊需要設置葉子節點
j = 0;
cart->count = 0;
for( i = 0; i < count && (intnode[i].stump != NULL); i++ )
{
cart->count++;
cart->compidx[i] = intnode[i].stump->compidx; // haar特徵的序號
cart->threshold[i] = intnode[i].stump->threshold;
// 確定葉子序號與葉子的輸出置信度
if( cart->left[i] <= 0 )
{
cart->left[i] = -j;
cart->val[j] = intnode[i].stump->left; // 這個left是float值,不是CVMat*
j++;
}
if( cart->right[i] <= 0 )
{
cart->right[i] = -j;
cart->val[j] = intnode[i].stump->right;
j++;
}
}
// 後續處理
for( i = 0; i < count && (intnode[i].stump != NULL); i++ )
{
intnode[i].stump->release( (CvClassifier**) &(intnode[i].stump) );
if( i != 0 )
{
cvReleaseMat( &(intnode[i].sampleIdx) );
}
}
for( i = 0; i < listcount; i++ )
{
list[i].stump->release( (CvClassifier**) &(list[i].stump) );
cvReleaseMat( &(list[i].sampleIdx) );
}
cvFree( &intnode );
return (CvClassifier*) cart;
}
這段程序有些細節我可能沒有理解正確,比如說左右分支的error同時不爲0時,我的解釋是程序將右分支的優先級設置的更高些,就是一個可能出錯的地方,還想與童鞋們一起探討,謝謝!
後記:上面那個結論的確是說錯了,左右分支的優先級一樣。這個函數後來又看過一次,感覺對代碼又有了重新的認識,將自己之前犯過的錯誤重新修訂,繼續分享給大家,謝謝朋友們繼續挑我的毛病,感激不盡!