統計學習方法c++實現之四 決策樹


決策樹

前言

決策樹是一種基本的分類和回歸算法,書中主要是討論了分類的決策樹。決策樹在每一個結點分支規則是一種if-then規則,即滿足某種條件就繼續搜索左子樹,不符合就去右子樹,看起來是用二叉樹實現對吧,實際的CART決策樹就是二叉樹,等會再介紹。現在先來看看決策樹的理論部分。代碼地址https://github.com/bBobxx/statistical-learning/blob/master/src/decisiontree.cpp

決策樹相關理論

決策樹的學習通常包括三個部分:特征選擇決策樹生成決策樹修剪

特征選擇

我們拋開煩人的公式和術語,用通俗的思想(沒辦法,本人只有通俗的思想)來理解一下,現在給你很多數據,有很多類,每個數據有n維的特征,怎么分?最簡單的,不如來個全連接神經網絡,把數據丟進去,讓模型自己去學習去,恩.....這個辦法可能是准確率最高的,但是我們這里學習的是決策樹,而且有些場景根本不需要神經網絡也可以分類的很准確,現在讓我們用決策樹解決這個問題。

首先,面對n維特征,和k個類別,仿佛無從下手。咋辦呢,笨一點的辦法,就從第一個特征開始,如果第一個特征有m個不同取值,那我就按這個特征取值把數據分成m份,對這份特征子集,我再選第二個特征,第二個特征比如有l個不同取值,那么對於m個子集,每個又可以最多分出l個子集(最多而不是一定,因為m某個子集中的數據的第二維特征可能取不全l個值),那么現在我們最多有\(m\times l\) 個子集,然后是第三維特征......直到第n維特征或者某個子集中的數據類別幾乎一樣我們就停止。對於這種分法很明顯確實是個樹結構對吧,只不過你的樹可能是這樣子的:

在這里插入圖片描述

不好意思,弄錯了,一般樹結構是這樣子的:

在這里插入圖片描述

思路很簡單,但是過程很復雜對吧,沒錯,這就是決策樹,但是如果真寫成上面這樣也太沒效率了,比如說,現在給你很多人的數據,讓你分出是男是女,特征有這么幾個:身高,體重,頭發長短,身份證上的性別。沒錯最后一個特征一般不會給出的。現在開始按照上面的思路分類,就分10000個數據吧,身高的取值有十種,就當做150到190取十個數吧,體重先不談,如果從身高這個特征開始分就能把你分吐血。聰明的同學(應該是不笨的)一眼就能看出來,我直接用最后一個特征,一下子就分出來了,就算沒有最后這個特征,我用頭發長短這個也可以很好的分。
沒錯,看出特征選擇的重要了吧,這就是決策樹的第一步,要先選擇最具有分類能力的特征,注意每一維特征只用一次。怎么選呢,這就涉及到了信息增益(ID3決策樹), 信息增益比(C4.5決策樹),和基尼指數(CART決策樹)。皮一下,這里就只介紹基尼指數吧,其他的就看書吧。

基尼指數:\(Gini(D,A)=\frac{|D_1|}{|D|}Gini(D_1)+\frac{|D_2|}{|D|}Gini(D_2)\)

其中,A代表某一維特征,D代表的數據集合,根據A是否取a將D分為\(D_1\)\(D_2\)兩個子集,\(|D|,|D_1|,|D_2|\)分別代表各自的數量。

其中,\(Gini(D) = \sum_{k=1}^{K}\frac{|C_k|}{|D|}(1-\frac{|C_k|}{|D|})\)

\(C_k\)代表某一類,\(\frac{|C_k|}{|D|}\)代表這個集合中樣本是第k類的概率。

基尼指數越大,表示樣本集合的不確定性越大,我們在選取A的時候肯定希望分完后集合越確定越好,所以以后在進行特征選擇的時候就需要選取基尼指數最小的那個特征。

決策樹(CART)生成算法

  1. 對於當前根節點Root,對現有的樣本集D,對所有的特征\(A_i\)的所有可能取值\(a_j\)計算基尼指數,選擇使基尼指數最小的\(A_i\)\(a_j\),根據樣本點對\(A_i=a_j\)的測試為“是”或“否”將D分為\(D_1\)\(D_2\)
  2. \(D_1\)作為根節點Root的左子樹的根節點Root_L的樣本集,\(D_2\)作為根節點Root的右子樹的根節點Root_R的樣本集。
  3. 重復1,2直到結點中樣本個數小於閾值,或樣本集基本屬於同一類,或者沒有更多特征(代表已經將所有的特征都過一遍了)。

CART剪枝

請自行看書,反正我也沒實現。

決策樹的c++實現

代碼結構

在這里插入圖片描述

實現

這里只展示如何確定分割的特征和值

pair<int, double> DecisionTree::createSplitFeature(vector<vector<double >>& valRange){
    priority_queue<pair<double, pair<int, double>>, vector<pair<double, pair<int, double>>>, std::greater<pair<double, pair<int, double>>>> minheap;
      //pair<double, pair<int, double>> first value is Gini value, second pair (pair<int, double>) first value is split
      //axis, second value is split value
    vector<map<double, int>> dataDivByFeature(indim);  //vector size is num of axis, map's key is the value of feature, map's value is
      //num belong to feature'value
    vector<set<double>> featureVal(indim);  //store value for each axis
    vector<map<pair<double, double>, int>> datDivByFC(indim);  //vector size is num of axis, map's key is the feature value and class value, map's value is
      //num belong to that feature value and class
    set<double> cls;  //store num of class
    for(const auto& featureId:features) {
        if (featureId<0)
            continue;
        map<double, int> dataDivByF;
        map<pair<double, double>, int> dtDivFC;
        set<double> fVal;
        for (auto& data:valRange){  //below data[featureId] is the value of one feature axis, data.back() is class value
            cls.insert(data.back());
            fVal.insert(data[featureId]);
            if (dataDivByF.count(data[featureId]))
                dataDivByF[data[featureId]] += 1;
            else
                dataDivByF[data[featureId]] = 0;
            if (dtDivFC.count(std::make_pair(data[featureId], data.back())))
                dtDivFC[std::make_pair(data[featureId], data.back())] += 1;
            else
                dtDivFC[std::make_pair(data[featureId], data.back())] = 0;
        }
        featureVal[featureId] = fVal;
        dataDivByFeature[featureId] = dataDivByF;
        datDivByFC[featureId] = dtDivFC;
    }
    for (auto& featureId: features) {  // for each feature axis
        if (featureId<0)
            continue;
        for (auto& feVal: featureVal[featureId]){  //for each feature value
            double gini1 = 0 ;
            double gini2 = 0 ;

            double prob1 = dataDivByFeature[featureId][feVal]/double(valRange.size());
            double prob2 = 1 - prob1;
            for (auto& c : cls){  //for each class
                double pro1 = double(datDivByFC[featureId][std::make_pair(feVal, c)])/dataDivByFeature[featureId][feVal];
                gini1 += pro1*(1-pro1);
                int numC = 0;
                for (auto& feVal2: featureVal[featureId])
                    numC += datDivByFC[featureId][std::make_pair(feVal2, c)];
                double pro2 = double(numC-datDivByFC[featureId][std::make_pair(feVal, c)])/(valRange.size()-dataDivByFeature[featureId][feVal]);
                gini2 += pro2*(1-pro2);
            }
            double gini = prob1*gini1+prob2*gini2;

            minheap.push(std::make_pair(gini, std::make_pair(featureId, feVal)));
        }
    }
    features[minheap.top().second.first]=-1;
    return minheap.top().second;
}

這里使用循環嵌套計算符合條件的數據的數量,效率很低,有更好方法的同學麻煩告知一下,叩拜~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM