1.算法介紹
分類回歸樹算法:CART(Classification And Regression Tree)算法采用一種二分遞歸分割的技術,將當前的樣本集分為兩個子樣本集,使得生成的的每個非葉子節點都有兩個分支。因此,CART算法生成的決策樹是結構簡潔的二叉樹。
分類樹兩個基本思想:第一個是將訓練樣本進行遞歸地划分自變量空間進行建樹的想法,第二個想法是用驗證數據進行剪枝。
建樹:在分類回歸樹中,我們把類別集Result表示因變量,選取的屬性集attributelist表示自變量,通過遞歸的方式把attributelist把p維空間划分為不重疊的矩形,具體建樹的基本步驟參見:http://baike.baidu.com/view/3075445.htm。
CART算法是怎樣進行樣本划分的呢?它檢查每個變量和該變量所有可能的划分值來發現最好的划分,對離散值如{x,y,x},則在該屬性上的划分有三種情況({{x,y},{z}},{{x,z},y},{{y,z},x}),空集和全集的划分除外;對於連續值處理引進“分裂點”的思想,假設樣本集中某個屬性共n個連續值,則有n-1個分裂點,每個“分裂點”為相鄰兩個連續值的均值 (a[i] + a[i+1]) / 2。將每個屬性的所有划分按照他們能減少的雜質(合成物中的異質,不同成分)量來進行排序,雜質的減少被定義為划分前的雜質減去划分之后每個節點的雜質量*划分所占樣本比率之和,目前最流行的雜質度量方法是:GINI指標,如果我們用k,k=1,2,3……C表示類,其中C是類別集Result的因變量數目,一個節點A的GINI不純度定義為:
其中,Pk表示觀測點中屬於k類得概率,當Gini(A)=0時所有樣本屬於同一類,當所有類在節點中以相同的概率出現時,Gini(A)最大化,此時值為(C-1)C/2。
對於分類回歸樹,A如果它不滿足“T都屬於同一類別or T中只剩下一個樣本”,則此節點為非葉節點,所以嘗試根據樣本的每一個屬性及可能的屬性值,對樣本的進行二元划分,假設分類后A分為B和C,其中B占A中樣本的比例為p,C為q(顯然p+q=1)。則雜質改變量:Gini(A) -p*Gini(B)-q*Gini(C),每次划分該值應為非負,只有這樣划分才有意義,對每個屬性值嘗試划分的目的就是找到雜質該變量最大的一個划分,該屬性值划分子樹即為最優分支。
剪枝:在CART過程中第二個關鍵的思想是用獨立的驗證數據集對訓練集生長的樹進行剪枝。
分析分類回歸樹的遞歸建樹過程,不難發現它實質上存在着一個數據過度擬合問題。在決策樹構造時,由於訓練數據中的噪音或孤立點,許多分枝反映的是訓練數據中的異常,使用這樣的判定樹對類別未知的數據進行分類,分類的准確性不高。因此試圖檢測和減去這樣的分支,檢測和減去這些分支的過程被稱為樹剪枝。樹剪枝方法用於處理過分適應數據問題。通常,這種方法使用統計度量,減去最不可靠的分支,這將導致較快的分類,提高樹獨立於訓練數據正確分類的能力。
決策樹常用的剪枝常用的簡直方法有兩種:事前剪枝和事后剪枝,CART算法經常采用事后剪枝方法:該方法是通過在完全生長的樹上剪去分枝實現的,通過刪除節點的分支來剪去樹節點。最下面未被剪枝的節點成為樹葉。
CART用的成本復雜性標准是分類樹的簡單誤分(基於驗證數據的)加上一個對樹的大小的懲罰因素。懲罰因素是有參數的,我們用a表示,每個節點的懲罰。成本復雜性標准對於一個數來說是Err(T)+a|L(T)|,其中Err(T)是驗證數據被樹誤分部分,L(T)是樹T的葉節點樹,a是每個節點的懲罰成本:一個從0向上變動的數字。當a=0對樹有太多的節點沒有懲罰,用的成本復雜性標准是完全生長的沒有剪枝的樹。在剪枝形成的一系列樹中,從其中選擇一個在驗證數據集上具有最小誤分的樹是很自然的,我們把這個樹成為最小誤分樹。
2.算法實現
本文根據一個樣本集,進行了CART算法的簡單實現。該樣本集中每個樣本有十六個特征屬性和一個結果屬性,為了降低划分的難度,每個特征屬性取兩個不同的離散值,結果屬性有兩個離散值:Yes和No。
數據結構定義:在該算法中定義了三種數據結構:存儲樣本屬性名稱及取值的Node屬性,存儲單個樣本的EXampleSet屬性,樹的節點屬性dataNode;存放在DataStructure.h中,代碼如下:
- <span style="font-size: 18px;">typedef struct tagNode
- {//存儲屬性
- string name;//屬性的名稱
- string value;//屬性取值
- }Node;
- typedef struct tagExampleSet
- {//樣本存儲
- string example[16];//樣本的每個屬性上的屬性值
- string decision;//樣本的結果類
- }ExampleSet;
- typedef struct Data_Node{
- //節點的數據結構,結果分為兩類yes類和No類
- int Yesnum;//類yes得樣本數目
- int Nonum;//類no得樣本數
- vector<ExampleSet> myVector;//存儲樣本
- Data_Node *LeftNode;//左子樹
- Data_Node *RightNode;//右子樹
- int Property;//划分選取的屬性
- string Proper_value;//所選的屬性的值
- int nodenum;//標示節點
- bool leavenode;//標示葉節點
- }dataNode;</span>
- <span style="font-size:18px;">typedef struct tagNode
- {//存儲屬性
- string name;//屬性的名稱
- string value;//屬性取值
- }Node;
- typedef struct tagExampleSet
- {//樣本存儲
- string example[16];//樣本的每個屬性上的屬性值
- string decision;//樣本的結果類
- }ExampleSet;
- typedef struct Data_Node{
- //節點的數據結構,結果分為兩類yes類和No類
- int Yesnum;//類yes得樣本數目
- int Nonum;//類no得樣本數
- vector<ExampleSet> myVector;//存儲樣本
- Data_Node *LeftNode;//左子樹
- Data_Node *RightNode;//右子樹
- int Property;//划分選取的屬性
- string Proper_value;//所選的屬性的值
- int nodenum;//標示節點
- bool leavenode;//標示葉節點
- }dataNode;</span>
樣本讀取及處理:用兩個文件分別存儲樣本的屬性及所有樣本。文件t存儲樣本的十六個自變量屬性、類別屬性的名稱和離散值集合,文件t1是所有樣本的集合,用ReadFile類讀取文件,並把它們分別存儲在兩個向量中。建樹的過程在MySufan類中,該類地方法列表如下:
- <span style="font-size: 18px;">MySuanfa();
- ~MySuanfa();
- void Method();//調用建樹、剪枝方法
- void BuildTree(Data_Node*thisNode);//建樹方法,每次調用DeviceTree對非葉節點進行划分
- void DeviceTree(Data_Node*thisNode,int i);//對非葉結點進行划分,分出左節點,有節點
- int Choose_Property(Data_Node* thisNode);//返回選擇的屬性值
- double pure(int i1,int i2,int i3);//純度計算函數,每次計算最優划分時用
- void Deal(Data_Node* d);//剪枝函數,此函數對建好的樹用測試樣本進行剪枝
- void levelorder(Data_Node * p);//層次遍歷,此方法按曾給決策點分配序號,用於剪枝
- void inorder(Data_Node *p);//中序遍歷,和建樹的前序遍歷用於確定樹的結構
- void BuildTest(Data_Node *d,int t);//此方法用於計算當取不同決策點時,建樹樣本的錯誤樣本數,t為決策點數目
- void CutTree(Data_Node *d,int k,int t);//k為單個樣本,t為決策點數,根據決策點對測試樣本集進行測試
- void ClassOfNode(vector<ExampleSet>);//本方法用於切割原始樣本集,將樣本分為測試樣本和建樹樣本</span>
- <span style="font-size:18px;">MySuanfa();
- ~MySuanfa();
- void Method();//調用建樹、剪枝方法
- void BuildTree(Data_Node*thisNode);//建樹方法,每次調用DeviceTree對非葉節點進行划分
- void DeviceTree(Data_Node*thisNode,int i);//對非葉結點進行划分,分出左節點,有節點
- int Choose_Property(Data_Node* thisNode);//返回選擇的屬性值
- double pure(int i1,int i2,int i3);//純度計算函數,每次計算最優划分時用
- void Deal(Data_Node* d);//剪枝函數,此函數對建好的樹用測試樣本進行剪枝
- void levelorder(Data_Node * p);//層次遍歷,此方法按曾給決策點分配序號,用於剪枝
- void inorder(Data_Node *p);//中序遍歷,和建樹的前序遍歷用於確定樹的結構
- void BuildTest(Data_Node *d,int t);//此方法用於計算當取不同決策點時,建樹樣本的錯誤樣本數,t為決策點數目
- void CutTree(Data_Node *d,int k,int t);//k為單個樣本,t為決策點數,根據決策點對測試樣本集進行測試
- void ClassOfNode(vector<ExampleSet>);//本方法用於切割原始樣本集,將樣本分為測試樣本和建樹樣本</span>
遞歸建樹:建樹按照遞歸方式進行建樹,采用全部樣本的2/3進行建樹,首先找到一個划分值,如果不存在返回-1,然后判斷一個樹是否為葉子節點,不為葉子節點按照划分值進行划分,關鍵代碼如下:
- <span style="font-size: 18px;">void MySuanfa::BuildTree(Data_Node* thisNode)
- {
- if(thisNode!=NULL){// //節點不為空
- nodenum++;
- thisNode->nodenum=nodenum;
- int getProperty=Choose_Property(thisNode);//找到划分
- thisNode->Property=getProperty;
- if((thisNode->Yesnum*thisNode->Nonum==0)||getProperty==-1)
- {//如果划分為-1,則無法再次划分
- thisNode->Property=-1;
- thisNode->leavenode=true;
- }
- else
- {//遞歸建樹
- thisNode->leavenode=false;
- DeviceTree(thisNode,getProperty);//將父節點按照划分屬性進行划分
- BuildTree(thisNode->LeftNode);//遞歸建立左子樹
- BuildTree(thisNode->RightNode);//遞歸建立右子樹
- }
- }
- }</span>
- <span style="font-size:18px;">void MySuanfa::BuildTree(Data_Node* thisNode)
- {
- if(thisNode!=NULL){// //節點不為空
- nodenum++;
- thisNode->nodenum=nodenum;
- int getProperty=Choose_Property(thisNode);//找到划分
- thisNode->Property=getProperty;
- if((thisNode->Yesnum*thisNode->Nonum==0)||getProperty==-1)
- {//如果划分為-1,則無法再次划分
- thisNode->Property=-1;
- thisNode->leavenode=true;
- }
- else
- {//遞歸建樹
- thisNode->leavenode=false;
- DeviceTree(thisNode,getProperty);//將父節點按照划分屬性進行划分
- BuildTree(thisNode->LeftNode);//遞歸建立左子樹
- BuildTree(thisNode->RightNode);//遞歸建立右子樹
- }
- }
- }</span>
分析上面代碼,Choose_Property(thisNode);函數的作用是將thisNode中的樣本嘗試進行最優划分,划分的依據就是雜質最大該變量,如果划分成功返回屬性下標,否則返回-1,我們在樣本中每個屬性默認取兩個離散值。注意到方法中對書中定義的leavenode和nodenum兩個變量的操作,他們的用途是什么呢?nodenum的第一個作用是樹的遍歷,將每一個節點賦予一個唯一的值,建樹的過程是前序建樹,建樹結束后根據樹的中序遍歷可以唯一確定樹的結構,nodenum的第二個作用和leavenode的作用將會在剪枝過程中用到,后面將會提到。
當建樹結束后,樹的前序即為nodenum從小到大的排序,然后通過調用中序遍歷函數輸出樹的中序序列,確定樹的結構。該樹含有17個決策點(非葉子節點),18個葉子節點。

圖1. 結構
樹中決策點的划分代碼對應的屬性名稱:
0————handicapped-infants ; 1————water-project-cost-sharing
2————adoption-of-the-budget-resolution ; 3————physician-fee-freeze
4————el-salvador-aid ; 5————religious-groups-in-schools
6————anti-satellite-test-ban; 7————aid-to-nicaraguan-contras
8————mx-missile ; 9————immigration
10————synfuels-corporation-cutback ; 11————education-spending
12————superfund-right-to-sue ; 13————crime
14————duty-free-exports ; 15—export-administration-act-south-africa
按照遞歸分類的算法,最終生成的樹的葉子節點中或者同屬一類或者只有一個樣本,分析樹的結構我們可以發現,有兩個葉子節點8和23不符合這種情況,卻成了葉子節點。這與所選樣本有關,在這兩個葉節點中兩個樣本的十六個特征屬性值都相同,只有所屬類別不同,所以無法根據遞歸算法進行分類。另當選取physician-fee-freeze 和adoption-of-the-budget-resolution兩種屬性進行決策時,樣本所屬的類別已經基本判定,造成這種情況我們可認為這兩種屬性在樣本中所占的權重很大,只要確定這兩種情況,樹的大部分樣本的分類就已確定。
剪枝:用訓練樣本建樹結束后,就是進行樹的剪枝階段,本算法采用樣本集的后1/3作為測試進行剪枝。
樹的決策點:如果一個節點為非葉節點,則稱該節點為一個樹的決策點。樹的剪枝就是減去過分擬合給樹帶來的的冗余,用盡可能少的決策點、盡可能低的樹高獲取盡可能大的正確率。
如何獲取樹的決策點?逐層確定樹的決策點,並根據決策點數目進行剪枝是剪枝的關鍵。
根據二叉樹的特性可知樹的非葉節點=葉節點-1;所以可以從樹的節點數中得知樹種非葉結點的數量。本程序根據這一特性將樹的決策點逐層賦值,根節點賦值1,根節點的左節點賦值2……,這一過程通過層次遍歷實現。並將該值賦給nodenum,對於葉子節點nodenum為0關鍵代碼如下:
- <span style="font-size: 18px;">void MySuanfa::levelorder(Data_Node* p)
- {
- int node=1;
- list<Data_Node *>q;
- if(p)q.push_back(p);
- p->nodenum=node;
- while(!q.empty())
- {
- p=q.front();
- q.pop_front();
- if(p->LeftNode)
- {
- if(p->LeftNode->leavenode)
- {//如果該節點的左節點是子節點,則將nodenum賦0
- p->LeftNode->nodenum=0;
- }
- else
- {//否則將該節點賦一個node值,該值表示此決策點的順序
- node++;
- p->LeftNode->nodenum=node;
- q.push_back(p->LeftNode);
- }
- }
- if(p->RightNode)
- {
- if(p->RightNode->leavenode)//
- {//如果該節點的右節點是子節點,則將nodenum賦0
- p->RightNode->nodenum=0;
- }
- else
- {//否則將該節點賦一個node值,該值表示此決策點的順序
- node++;
- p->RightNode->nodenum=node;
- q.push_back(p->RightNode);
- }
- }
- }
- }
- </span>
- <span style="font-size:18px;">void MySuanfa::levelorder(Data_Node* p)
- {
- int node=1;
- list<Data_Node *>q;
- if(p)q.push_back(p);
- p->nodenum=node;
- while(!q.empty())
- {
- p=q.front();
- q.pop_front();
- if(p->LeftNode)
- {
- if(p->LeftNode->leavenode)
- {//如果該節點的左節點是子節點,則將nodenum賦0
- p->LeftNode->nodenum=0;
- }
- else
- {//否則將該節點賦一個node值,該值表示此決策點的順序
- node++;
- p->LeftNode->nodenum=node;
- q.push_back(p->LeftNode);
- }
- }
- if(p->RightNode)
- {
- if(p->RightNode->leavenode)//
- {//如果該節點的右節點是子節點,則將nodenum賦0
- p->RightNode->nodenum=0;
- }
- else
- {//否則將該節點賦一個node值,該值表示此決策點的順序
- node++;
- p->RightNode->nodenum=node;
- q.push_back(p->RightNode);
- }
- }
- }
- }
- </span>
遍歷結束后,每一個決策點數目可以確定一個樹,我們就可以根據樹的決策點數對訓練樣本和測試樣本的誤差進行統計,怎樣根據決策點數確定樹的結構?可以將樹的前序遍歷進行改進,對於t個決策點,節點為0或大於t的都是葉子節點,一旦確定葉子節點,樹的結構就清楚了,下圖為重新賦值后的樹,在該圖中,如當有3個決策點時,2的子節點和3的子節點都是葉子節點,當用改進的前序遍歷便立時會輸出有3個決策點:(1,2,3);4個葉子節點(4,5,0,6)的子樹:

圖2給樹的節點重新賦值
不同決策點可對應不同子樹,通過前序遍歷可以將葉子節點中的錯誤樣本統計出來計算該樹情況下錯誤樣本的個數,然后再用測試樣本遍歷樹,統計測試樣本再改樹下錯誤樣本個數最后得出結果集如下:
圖3 不同決策點時建樹誤差與測試誤差
通過比較可知當樹有8和9個決策點時,測試誤差最小,我們取8,因為此時樹比9個決策點簡單,我們取含有8個決策點為最小誤分樹。最小誤分樹結構如下:
圖4 最小誤分樹
上圖中最小誤分樹非葉節點中的兩個值,第一個表示決策點表示,第二個表示選擇的屬性的代碼,葉子節點中兩數表示每一類的數目。
我們定義最優剪枝的方法是在剪枝序列中含有誤差在最小誤差樹的一個標准差之內的最小樹,算出的最小誤差率被砍做一個帶有標准差等於的隨機變量的觀測值,其中Emin對最小誤差樹的錯誤率,Nval是驗證集的個數:Emin=5.41%,Nval=148,所以到當樹有4個決策點時,為最優剪枝。
圖5 最優剪枝樹