Opencv研讀筆記:haartraining程序之cvCreateCARTClassifier函數詳解(CART樹狀弱分類器創建)~

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時,我的解釋是程序將右分支的優先級設置的更高些,就是一個可能出錯的地方,還想與童鞋們一起探討,謝謝!

後記:上面那個結論的確是說錯了,左右分支的優先級一樣。這個函數後來又看過一次,感覺對代碼又有了重新的認識,將自己之前犯過的錯誤重新修訂,繼續分享給大家,謝謝朋友們繼續挑我的毛病,感激不盡!


發佈了42 篇原創文章 · 獲贊 193 · 訪問量 50萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章