摘 要
本文是汽車評估系統的核心算法,利用決策樹進行分類,本文對決策樹進行了介紹,同時比較C4.5和ID3算法的不同,對C4.5提出隨機深林的想法提高分類預測的准確性。
關鍵詞:汽車評估,決策樹,C4.5
決策樹(Decision tree)
它從一組無次序、無規則的元組中推理出決策樹表示形式的分類規則。它采用自頂向下的遞歸方式,在決策樹的內部結點進行屬性值的比較,並根據不同的屬性值從該結點向下分支,葉結點是要學習划分的類。從根到葉結點的一條路徑就對應着一條合取規則,整個決策樹就對應着一組析取表達式規則。1986年 Quinlan提出了著名的ID3算法。在ID3算法的基礎上,1993年Quinlan又提出了C4.5算法。為了適應處理大規模數據集的需要,后來又提出了若干改進的算法,其中SLIQ(super-vised learning in quest)和SPRINT(scalable parallelizableinduction of decision trees)是比較有代表性的兩個算法,本文主要用C4.5算法對汽車評估系統建立分類決策樹,同時比較C4.5和ID3算法的不同,對C4.5提出隨機深林的想法提高分類預測的准確性。
信息熵的含義及分類
信息熵是信息論中的一個重要的指標,是由香農在1948年提出的。香農借用了熱力學中熵的概念來描述信息的不確定性。因此信息學中的熵和熱力學的熵是有聯系的。根據Charles H. Bennett對Maxwell’s Demon的重新解釋,對信息的銷毀是一個不可逆過程,所以銷毀信息是符合熱力學第二定律的。而產生信息,則是為系統引入負(熱力學)熵的過程。所以信息熵的符號與熱力學熵應該是相反的。
簡單的說信息熵是衡量信息的指標,更確切的說是衡量信息的不確定性或混亂程度的指標。信息的不確定性越大,熵越大。決定信息的不確定性或者說復雜程度主要因素是概率。決策樹中使用的與熵有關的概念有三個:信息熵,條件熵和互信息。下面分別來介紹這三個概念的含義和計算方法。
1、信息熵
信息熵是用來衡量一元模型中信息不確定性的指標。信息的不確定性越大,熵的值也就越大。而影響熵值的主要因素是概率。這里所說的一元模型就是指單一事件,而不確定性是一個事件出現不同結果的可能性。例如拋硬幣,可能出現的結果有兩個,分別是正面和反面。而每次拋硬幣的結果是一個非常不確定的信息。因為根據我們的經驗或者歷史數據來看,一個均勻的硬幣出現正面和反面的概率相等,都是50%。因此很難判斷下一次出現的是正面還是反面。這時拋硬幣這個事件的熵值也很高。而如果歷史數據告訴我們這枚硬幣在過去的100次試驗中99次都是正面,也就是說這枚硬幣的質量不均勻,出現正面結果的概率很高。那么我們就很容易判斷下一次的結果了。這時的熵值很低,只有0.08。
我們把拋硬幣這個事件看做一個隨機變量S,它可能的取值有2種,分別是正面x1和反面x2。每一種取值的概率分別為P1和P2。我們要獲得隨機變量S的取值結果至少要進行1次試驗,試驗次數與隨機變量S可能的取值數量(2種)的對數函數Log有聯系。Log2=1(以2為底),其計算公式是:

Pi為子集合中不同性(而二元分類即正樣例和負樣例)的樣例的比例。
在拋硬幣的例子中,我們借助一元模型自身的概率,也就是前100次的歷史數據來消除了判斷結果的不確定性。而對於很多現實生活中的問題,則無法僅僅通過自身概率來判斷。例如:對於天氣情況,我們無法像拋硬幣一樣通過晴天,雨天和霧霾在歷史數據中出現的概率來判斷明天的天氣,因為天氣的種類很多,並且影響天氣的因素也有很多。同理,對於網站的用戶我們也無法通過他們的歷史購買頻率來判斷這個用戶在下一次訪問時是否會完成購買。因為用戶是的購買行為存在着不確定性,要消除這些不確定性需要更多的信息。例如用戶歷史行為中的廣告創意,促銷活動,商品價格,配送時間等信息。因此這里我們不能只借助一元模型來進行判斷和預測了,需要獲得更多的信息並通過二元模型或更高階的模型了解用戶的購買行為與其他因素間的關系來消除不確定性。衡量這種關系的指標叫做條件熵。
2、條件熵
條件熵是通過獲得更多的信息來消除一元模型中的不確定性。也就是通過二元或多元模型來降低一元模型的熵。我們知道的信息越多,信息的不確定性越小。例如,只使用一元模型時我們無法根據用戶歷史數據中的購買頻率來判斷這個用戶本次是否也會購買。因為不確定性太大。在加入了促銷活動,商品價格等信息后,在二元模型中我們可以發現用戶購買與促銷活動,或者商品價格變化之間的聯系。並通過購買與促銷活動一起出現的概率,和不同促銷活動時購買出現的概率來降低不確定性。
計算條件熵時使用到了兩種概率,分別是購買與促銷活動的聯合概率P(c),和不同促銷活動出現時購買也出現的條件概率E(c)。以下是條件熵E(T,X)的計算公式。條件熵的值越低說明二元模型的不確定性越小。

3、互信息(信息增益)
互信息是用來衡量信息之間相關性的指標。當兩個信息完全相關時,互信息為1,不相關時為0。在前面的例子中用戶購買與促銷活動這兩個信息間的相關性究竟有多高,我們可以通過互信息這個指標來度量。具體的計算方法就熵與條件熵之間的差。用戶購買的熵E(T)減去促銷活動出現時用戶購買的熵E(T,X)。以下為計算公式:

熵,條件熵和互信息是構建決策樹的三個關鍵的指標。下面我們將通過信息增益划分決策樹,即ID3算法。
ID3算法
在決策樹各級節點選擇屬性時,以信息熵增益(Information gain)作為屬性的選擇標准,在檢測所有屬性值時,選擇信息熵增益最大的屬性產生決策樹的節點,由該屬性的不同取值作為分支,然后遞歸建立分支,最后等到一個完整的決策樹,可以用來對新的樣本進行分類。
信息熵增益定義為樣本按照某屬性划分時造成熵減少的期望,可以區分訓練樣本中正負樣本的能力,其公式是:
ID3算法存在的缺點
(1)ID3算法在選擇根節點和各內部節點中的分支屬性時,采用信息增益作為評價標准。信息增益的缺點是傾向於選擇取值較多的屬性,在有些情況下這類屬性可能不會提供太多有價值的信息。
(2)ID3算法只能對描述屬性為離散型屬性的數據集構造決策樹。
C4.5算法做出的改進
(1)用信息增益率來選擇屬性,選擇信息增益率大的產生決策樹的節點,克服了用信息增益來選擇屬性時偏向選擇值多的屬性的不足。信息增益率定義為:

分子表示信息增益,和ID3算法一樣,分母表示分裂因子,代表按照屬性A分裂樣本集S的廣度和均勻性。分裂因子公式如下:

(2)可以處理連續數值型屬性
C4.5既可以處理離散型描述屬性,也可以處理連續性描述屬性。在選擇某節點上的分枝屬性時,對於離散型描述屬性,C4.5的處理方法與ID3相同,按照該屬性本身的取值個數進行計算;對於某個連續性描述屬性Ac,假設在某個結點上的數據集的樣本數量為total,C4.5將作以下處理。
Ø 將該結點上的所有數據樣本按照連續型描述屬性的具體數值,由小到大進行排序,得到屬性值的取值序列{A1c,A2c,……Atotalc}。
Ø 在取值序列中生成total-1個分割點。第i(0<i<total)個分割點的取值設置為Vi=(Aic+A(i+1)c)/2,它可以將該節點上的數據集划分為兩個子集。
Ø 從total-1個分割點中選擇最佳分割點。對於每一個分割點划分數據集的方式,C4.5計算它的信息增益率,並且從中選擇信息增益比最大的分割點來划分數據集。
(3)采用悲觀剪枝(Pessimistic Error Pruning (PEP)),避免樹的高度無節制的增長,避免過度擬合數據。
PEP后剪枝技術是由大師Quinlan提出的。它不需要像REP(錯誤率降低修剪)樣,需要用部分樣本作為測試數據,而是完全使用訓練數據來生成決策樹,又用這些訓練數據來完成剪枝。決策樹生成和剪枝都使用訓練集, 所以會產生錯分。現在我們先來介紹幾個定義:
| 符號 |
含義 |
| T1 |
決策樹T的所有內部節點(非葉子節點) |
| T2 |
決策樹T的所有葉子節點 |
| T3 |
決策樹T的所有節點,T3=T1∪T2 |
| n(t) |
節點t的所有樣本數 |
| ni(t) |
節點t中類別i的所有樣本數 |
| e(t) |
t中不屬於節點t所標識類別的樣本數 |
在剪枝時,我們使用r(t)=e(t)/n(t),就是當節點被剪枝后在訓練集上的錯誤率,而下面公式表示具體的計算公式,其中s為t節點的葉子節點。

在此,我們把錯誤分布看成是二項式分布,由上面“二項分布的正態逼近”相關介紹知道,上面的式子是有偏差的,因此需要連續性修正因子來矯正數據,有 r‘(t)=[e(t) + 1/2]/n(t):

其中s為t節點的葉子節點,你不認識的那個符號為 t的所有葉子節點的數目。
為了簡單,我們就只使用錯誤數目而不是錯誤率了,如下e'(t) = [e(t) + 1/2]:

接着求e'(Tt)的標准差,由於誤差近似看成是二項式分布,根據u = np, σ2=npq可以得到

當節點t滿足下面公式是,Tt子樹就會被剪掉:

(4)對於缺失值的處理
在某些情況下,可供使用的數據可能缺少某些屬性的值。假如〈x,c(x)〉是樣本集S中的一個訓練實例,但是其屬性A的值A(x)未知。處理缺少屬性值的一種策略是賦給它結點n所對應的訓練實例中該屬性的最常見值;另外一種更復雜的策略是為A的每個可能值賦予一個概率。例如,給定一個布爾屬性A,如果結點n包含6個已知A=1和4個A=0的實例,那么A(x)=1的概率是0.6,而A(x)=0的概率是0.4。於是,實例x的60%被分配到A=1的分支,40%被分配到另一個分支。這些片斷樣例(fractional examples)的目的是計算信息增益,另外,如果有第二個缺少值的屬性必須被測試,這些樣例可以在后繼的樹分支中被進一步細分。C4.5就是使用這種方法處理缺少的屬性值。
C4.5算法的優缺點
優點:產生的分類規則易於理解,准確率較高。
缺點:在構造樹的過程中,需要對數據集進行多次的順序掃描和排序,因而導致算法的低效。此外,C4.5只適合於能夠駐留於內存的數據集,當訓練集大得無法在內存容納時程序無法運行。
汽車評估系統決策樹的建立
本文所用數據來自某汽車評估系統的一部分,下載地址:http://archive.ics.uci.edu/ml/machine-learning-databases/car/
訓練集的數據表示法
| 變量 |
變量取值域 |
變量含義 |
| buying |
vhigh, high, med, low |
購買價格 |
| maint |
vhigh, high, med, low. |
維修價格 |
| doors |
2, 3, 4, 5more |
門有多少 |
| persons |
2, 4, more |
載人數 |
| lug_boot |
small, med, big |
載行李能力 |
| safety |
low, med, high |
安全性 |
將數據可以分為四類:unacc, acc, good, vgood
部分訓練數據集
| buying |
maint |
doors |
persons |
lug_boot |
safety |
class |
| med |
med |
5more |
more |
small |
low |
unacc |
| med |
med |
5more |
more |
small |
med |
acc |
| med |
med |
5more |
more |
small |
high |
acc |
| med |
med |
5more |
more |
med |
low |
unacc |
| med |
med |
5more |
more |
med |
med |
acc |
| med |
med |
5more |
more |
med |
high |
vgood |
| med |
med |
5more |
more |
big |
low |
unacc |
| med |
med |
5more |
more |
big |
med |
acc |
| med |
med |
5more |
more |
big |
high |
vgood |
| med |
low |
2 |
2 |
med |
low |
unacc |
| med |
low |
2 |
2 |
med |
med |
unacc |
| med |
low |
2 |
2 |
med |
high |
unacc |
| med |
low |
2 |
2 |
big |
high |
unacc |
| med |
low |
2 |
4 |
small |
low |
unacc |
| med |
low |
2 |
4 |
small |
med |
acc |
| med |
low |
2 |
4 |
small |
high |
good |
| med |
low |
2 |
4 |
med |
low |
unacc |
| med |
low |
2 |
4 |
med |
med |
acc |
| med |
low |
2 |
4 |
med |
high |
good |
| med |
low |
2 |
4 |
big |
low |
unacc |
| med |
low |
2 |
4 |
big |
med |
good |
| med |
low |
2 |
4 |
big |
high |
vgood |
核心代碼:
1 #include<bits/stdc++.h> 2 using namespace std; 3 struct Node 4 { 5 string attribute; 6 string attribute_value; 7 vector<Node*> child; 8 Node() 9 { 10 attribute = ""; 11 attribute_value=""; 12 } 13 }; 14 Node * root = new Node(); 15 vector<string> item; 16 map<string,vector<string> >item_range; 17 vector<vector<string> > states; 18 int item_num = 0; 19 void Input() 20 { 21 22 ifstream myfile("C:/Users/Administrator/Desktop/data4.csv"); 23 if(!myfile) 24 { 25 cout<<"unalbe to open myfile"; 26 exit(1); 27 } 28 char buff[1000]; 29 myfile.getline(buff,1000); 30 string temp = ""; 31 int bufflen = strlen(buff); 32 for(int i = 0; i <= bufflen; i++) 33 { 34 if(buff[i] == ',' || i == bufflen) 35 { 36 item.push_back(temp); 37 item_num ++; 38 temp =""; 39 } 40 else 41 temp +=buff[i]; 42 } 43 while(!myfile.eof()) 44 { 45 vector<string> row; 46 myfile.getline(buff,1000); 47 int bufflen = strlen(buff); 48 for(int i = 0; i <= bufflen; i++) 49 { 50 if(buff[i] == ',' || i == bufflen) 51 { 52 row.push_back(temp); 53 temp =""; 54 } 55 else 56 temp +=buff[i]; 57 } 58 states.push_back(row); 59 } 60 } 61 void computeAttributeRange() 62 { 63 int states_num = states.size(); 64 for(int i = 1; i < item_num; i++) 65 { 66 vector<string> valuetemp; 67 vector<string>::iterator it; 68 for(int j = 0; j < states_num; j++) 69 { 70 it = find(valuetemp.begin(),valuetemp.end(),states[j][i]); 71 if(it == valuetemp.end()) 72 valuetemp.push_back(states[j][i]); 73 } 74 item_range[item[i]] = valuetemp; 75 } 76 } 77 78 double computeEntropy(vector<vector<string> > remain_states,string attribute,string attribute_value) 79 { 80 vector<string> lastItem = item_range[item[item_num -1]]; 81 int Size = remain_states.size(); 82 vector<string>::iterator it; 83 int P[10],cnt,cntnum = 0; 84 memset(P,0,sizeof(P)); 85 it = find(item.begin(),item.end(),attribute); 86 if(it != item.end()) 87 cnt = it - item.begin(); 88 for(int i = 0; i < Size; i++) 89 { 90 if(remain_states[i][cnt] == attribute_value) 91 { 92 cntnum ++; 93 it = find(lastItem.begin(),lastItem.end(),remain_states[i][item_num - 1]); 94 if(it != lastItem.end()) 95 P[it - lastItem.begin()] ++; 96 } 97 } 98 double ans = 0.0; 99 int lastItem_size = lastItem.size(); 100 for(int i = 0; i < lastItem_size ; i++) 101 { 102 double temp = cntnum == 0 ? 0.0 : double(P[i])/cntnum; 103 ans -=temp != 0.0 ? temp * log(temp)/log(2.0) : 0.0; 104 } 105 return ans*cntnum; 106 } 107 double computeEntropy(vector<vector<string> > remain_states) 108 { 109 vector<string> lastItem = item_range[item[item_num -1]]; 110 int Size = remain_states.size(); 111 vector<string>::iterator it; 112 int P[10]; 113 memset(P,0,sizeof(P)); 114 for(int i = 0; i < Size; i++) 115 { 116 it = find(lastItem.begin(),lastItem.end(),remain_states[i][item_num - 1]); 117 if(it != lastItem.end()) 118 P[it - lastItem.begin()] ++; 119 } 120 double ans = 0.0; 121 int lastItem_size = lastItem.size(); 122 for(int i = 0; i < lastItem_size ; i++) 123 { 124 125 double temp = Size == 0 ? 0.0 : double(P[i])/Size; 126 ans -=temp == 0 ? 0.0 : temp * log(temp)/log(2.0); 127 } 128 return ans; 129 } 130 131 double computeGain(vector<vector<string> > remain_states,string attribute) 132 { 133 int Size = remain_states.size(); 134 vector<string> cntItem = item_range[attribute]; 135 double ans = computeEntropy(remain_states); 136 int cntItem_num = cntItem.size(); 137 for(int i = 0; i < cntItem_num; i++) 138 { 139 ans -= Size== 0 ? 0.0 :computeEntropy(remain_states,attribute,cntItem[i])/Size; 140 } 141 return ans; 142 } 143 144 string allSameOfLastItem(vector<vector<string> > remain_states,bool& ok) 145 { 146 ok = true; 147 string lastItem = remain_states[0][item_num - 1]; 148 for(int i = 1; i < remain_states.size(); i++) 149 if(remain_states[i][item_num-1] != lastItem) 150 { 151 ok = false; 152 break; 153 } 154 return lastItem; 155 } 156 157 string mostCommonValue(vector<vector<string> > remain_states) 158 { 159 int p[10]; 160 memset(p,0,sizeof(p)); 161 vector<string> lastItems = item_range[item[item_num - 1]]; 162 for(int i = 0; i < remain_states.size(); i++) 163 p[ find(lastItems.begin(),lastItems.end(),remain_states[i][item_num - 1]) - lastItems.begin()] ++; 164 int Max = 0,maxIndex = 0,lastItems_num = lastItems.size(); 165 for(int i = 0; i < lastItems_num; i++) 166 { 167 if(Max < p[i]) 168 p[i] = Max,maxIndex = i; 169 } 170 return lastItems[maxIndex]; 171 } 172 173 Node* BuildDecisionTree(Node * p,vector<vector<string> > remain_states,vector<string> remain_item) 174 { 175 if(p == NULL) 176 { 177 p = new Node(); 178 } 179 bool Ok = true; 180 string lastItem = allSameOfLastItem(remain_states,Ok); 181 if(Ok == true) 182 { 183 p->attribute = lastItem; 184 return p; 185 } 186 if(remain_item.size() == 2) 187 { 188 p->attribute = mostCommonValue(remain_states); 189 return p; 190 } 191 double Max = computeGain(remain_states,remain_item[1]); 192 string maxAttribute = remain_item[1]; 193 for(int i = 1; i < remain_item.size()- 1; i++) 194 { 195 double temp = computeGain(remain_states,remain_item[i]); 196 if(temp > Max) 197 { 198 Max = temp; 199 maxAttribute = remain_item[i]; 200 } 201 } 202 p->attribute = maxAttribute; 203 vector<string> maxAttribute_range = item_range[maxAttribute]; 204 int maxAttribute_rangeNum = maxAttribute_range.size(); 205 int cnt = find(item.begin(),item.end(),maxAttribute) - item.begin(); 206 for(int i = 0; i < maxAttribute_rangeNum; i++) 207 { 208 vector<vector<string> > newRemain_states; 209 Node* childNode = new Node(); 210 childNode->attribute_value = maxAttribute_range[i]; 211 for(int j = 0; j < remain_states.size(); j++) 212 { 213 if(remain_states[j][cnt] == childNode->attribute_value) 214 newRemain_states.push_back(remain_states[j]); 215 } 216 if(newRemain_states.size() == 0) 217 { 218 childNode->attribute = mostCommonValue(remain_states); 219 } 220 else 221 { 222 223 vector<string>::iterator it = find(remain_item.begin(),remain_item.end(),maxAttribute); 224 if(it != remain_item.end()) 225 remain_item.erase(it); 226 BuildDecisionTree(childNode,newRemain_states,remain_item); 227 } 228 229 p->child.push_back(childNode); 230 } 231 return p; 232 } 233 void printTree(Node* p, int dep) 234 { 235 for(int i = 0; i < dep; i++) 236 printf("\t"); 237 if(p->attribute_value != "") 238 { 239 cout<<p->attribute_value<<endl; 240 for(int i = 0; i <dep+1; i++) 241 printf("\t"); 242 } 243 cout<<p->attribute<<endl; 244 for(vector<Node*>::iterator it = p->child.begin(); it != p->child.end(); it ++) 245 printTree(*it,dep+1); 246 247 } 248 bool traceTree(vector<string> state,Node *p) 249 { 250 //cout<<p->attribute<<" "; 251 if(p->child.size() <= 0 ) 252 { 253 //cout<<p->attribute<< " " << state[item_num-1]<<endl; 254 //cout<<(p->attribute == state[item_num - 1])<<endl; 255 //cout<<"return"<<endl; 256 return p->attribute == state[item_num - 1]; 257 } 258 vector<string>::iterator it = find(item.begin(),item.end(),p->attribute); 259 if(it != item.end()) 260 { 261 for(int i = 0; i < p->child.size(); i++) 262 if(p->child[i]->attribute_value == state[it-item.begin()] ) 263 return traceTree(state,p->child[i]); 264 } 265 //cout<<"erro"<<endl; 266 return 0; 267 } 268 void test() 269 { 270 int fm = 0,fz = 0; 271 ifstream testfile("C:/Users/Administrator/Desktop/data5.csv"); 272 char buff[1000]; 273 testfile.getline(buff,1000); 274 string temp = ""; 275 while(!testfile.eof()) 276 { 277 vector<string> row; 278 testfile.getline(buff,1000); 279 int bufflen = strlen(buff); 280 for(int i = 0; i <= bufflen; i++) 281 { 282 if(buff[i] == ',' || i == bufflen) 283 { 284 row.push_back(temp); 285 temp =""; 286 } 287 else 288 temp +=buff[i]; 289 } 290 if(traceTree(row,root)) 291 fz++; 292 fm++; 293 294 } 295 cout<<fz<<" "<<fm<<endl; 296 cout<<double(fz)/double(fm)<<endl; 297 } 298 299 int main() 300 { 301 freopen("C:/Users/Administrator/Desktop/res1.txt", "w", stdout); 302 Input(); 303 computeAttributeRange(); 304 BuildDecisionTree(root,states,item); 305 printTree(root,0); 306 test(); 307 return 0; 308 }
通過隨機森林提高准確率
決策樹是建立在已知的歷史數據及概率上的,一課決策樹的預測可能會不太准確,提高准確率最好的方法是構建隨機森林(Random Forest)。所謂隨機森林就是通過隨機抽樣的方式從歷史數據表中生成多張抽樣的歷史表,對每個抽樣的歷史表生成一棵決策樹。由於每次生成抽樣表后數據都會放回到總表中,因此每一棵決策樹之間都是獨立的沒有關聯。將多顆決策樹組成一個隨機森林。當有一條新的數據產生時,讓森林里的每一顆決策樹分別進行判斷,以投票最多的結果作為最終的判斷結果。以此來提高正確的概率。
