一. 決策樹
1. 決策樹:
決策樹算法借助於樹的分支結構實現分類,決策樹在選擇分裂點的時候,總是選擇最好的屬性作為分類屬性,即讓每個分支的記錄的類別盡可能純。
常用的屬性選擇方法有信息增益(Information Gain),增益比例(gain ratio),基尼指數(Gini index)。
其基本思路是不斷選取產生信息增益最大的屬性來划分樣例集和,構造決策樹。
信息增益定義為按某種屬性分裂后,結點與其子結點的信息熵之差。
信息熵是香農提出的,用於描述信息不純度(不穩定性,不確定性),其計算公式是:
其中n為類別個數,Pi為子集合中不同類型(而二元分類即正樣例和負樣例)的樣例的比例。信息熵表示將數據集S不同的類分開需要的信息量,其值越大表示越不純。
這樣信息增益可以定義為樣本按照某屬性划分時造成熵減少的期望,可以區分訓練樣本中正負樣本的能力,其計算公式是:
上面公式實際上就是當前節點的不純度減去子節點不純度的加權平均數,權重由子節點記錄數與當前節點記錄數的比例決定。
--------------------------------------------------------------------------------------------------------
基本步驟:
# ============================================== # 輸入: # 數據集 # 輸出: # 構造好的決策樹(也即訓練集) # ============================================== def 創建決策樹: '創建決策樹' if (數據集中所有樣本分類一致): 創建攜帶類標簽的葉子節點 else: 尋找划分數據集的最好特征 根據最好特征划分數據集 for 每個划分的數據集: 創建決策子樹(遞歸方式)
構建決策樹采用貪心算法,只考慮當前純度差最大的屬性來分裂。
使用信息增益的話其實是有一個缺點,那就是它偏向於具有大量值的屬性--就是說在訓練集中,某個屬性所取的不同值的個數越多,那么越有可能拿它來作為分裂屬性,而這樣做有時候是沒有意義的,
另外ID3不能處理連續分布的數據特征,於是就有了C4.5算法。CART算法也支持連續分布的數據特征。
2. 決策數有兩大優點:
1)決策樹模型可以讀性好,具有描述性,有助於人工分析;
2)效率高,決策樹只需要一次構建,反復使用,每一次預測的最大計算次數不超過決策樹的深度。
3. 過渡擬合:
采用上面算法生成的決策樹在事件中往往會導致過濾擬合。也就是該決策樹對訓練數據可以得到很低的錯誤率,但是運用到測試數據上卻得到非常高的錯誤率。過渡擬合的原因有以下幾點:
- 噪音數據:訓練數據中存在噪音數據,決策樹的某些節點有噪音數據作為分割標准,導致決策樹無法代表真實數據。
- 缺少代表性數據:訓練數據沒有包含所有具有代表性的數據,導致某一類數據無法很好的匹配,這一點可以通過觀察混淆矩陣(Confusion Matrix)分析得出。
優化方案1:修剪枝葉
決策樹過渡擬合往往是因為太過“茂盛”,也就是節點過多,所以需要裁剪(Prune Tree)枝葉。裁剪枝葉的策略對決策樹正確率的影響很大。主要有兩種裁剪策略。
前置裁剪 在構建決策樹的過程時,提前停止。那么,會將切分節點的條件設置的很苛刻,導致決策樹很短小。結果就是決策樹無法達到最優。實踐證明這中策略無法得到較好的結果。
后置裁剪 決策樹構建好后,然后才開始裁剪。采用兩種方法:1)用單一葉節點代替整個子樹,葉節點的分類采用子樹中最主要的分類;
2)將一個子樹完全替代另外一顆子樹。后置裁剪有個問題就是計算效率,有些節點計算后就被裁剪了,導致有點浪費。
優化方案2:K-Fold Cross Validation
首先計算出整體的決策樹T,葉節點個數記作N,設i屬於[1,N]。對每個i,使用K-Fold Validataion方法計算決策樹,並裁剪到i個節點,計算錯誤率,最后求出平均錯誤率。
這樣可以用具有最小錯誤率對應的i作為最終決策樹的大小,對原始決策樹進行裁剪,得到最優決策樹。
優化方案3:Random Forest
Random Forest是用訓練數據隨機的計算出許多決策樹,形成了一個森林。然后用這個森林對未知數據進行預測,選取投票最多的分類。實踐證明,此算法的錯誤率得到了經一步的降低。
這種方法背后的原理可以用“三個臭皮匠定一個諸葛亮”這句諺語來概括。一顆樹預測正確的概率可能不高,但是集體預測正確的概率卻很高。
ID3(C++實現) 參考:http://blog.csdn.net/yangliuy/article/details/7322015#
#include <iostream> #include <string> #include <vector> #include <map> #include <algorithm> #include <cmath> using namespace std; #define MAXLEN 6//輸入每行的數據個數 //多叉樹的實現 //1 廣義表 //2 父指針表示法,適於經常找父結點的應用 //3 子女鏈表示法,適於經常找子結點的應用 //4 左長子,右兄弟表示法,實現比較麻煩 //5 每個結點的所有孩子用vector保存 //教訓:數據結構的設計很重要,本算法采用5比較合適,同時 //注意維護剩余樣例和剩余屬性信息,建樹時橫向遍歷考循環屬性的值, //縱向遍歷靠遞歸調用 vector <vector <string> > state;//實例集 vector <string> item(MAXLEN);//對應一行實例集 vector <string> attribute_row;//保存首行即屬性行數據 string endflag("end");//輸入結束 string yes("yes"); string no("no"); string blank(""); map<string, vector < string > > map_attribute_values;//存儲屬性對應的所有的值 int tree_size = 0; struct Node{//決策樹節點 string attribute;//屬性值 string arrived_value;//到達的屬性值 vector<Node *> childs;//所有的孩子 Node(){ attribute = blank; arrived_value = blank; } }; Node * root; //根據數據實例計算屬性與值組成的map void ComputeMapFrom2DVector(){ unsigned int i, j, k; bool exited = false; vector<string> values; for (i = 1; i < MAXLEN - 1; i++){//按照列遍歷 for (j = 1; j < state.size(); j++){ for (k = 0; k < values.size(); k++){ if (!values[k].compare(state[j][i])) exited = true; } if (!exited){ values.push_back(state[j][i]);//注意Vector的插入都是從前面插入的,注意更新it,始終指向vector頭 } exited = false; } map_attribute_values[state[0][i]] = values; values.erase(values.begin(), values.end()); } } //根據具體屬性和值來計算熵 double ComputeEntropy(vector <vector <string> > remain_state, string attribute, string value, bool ifparent){ vector<int> count(2, 0); unsigned int i, j; bool done_flag = false;//哨兵值 for (j = 1; j < MAXLEN; j++){ if (done_flag) break; if (!attribute_row[j].compare(attribute)){ for (i = 1; i < remain_state.size(); i++){ if ((!ifparent&&!remain_state[i][j].compare(value)) || ifparent){//ifparent記錄是否算父節點 if (!remain_state[i][MAXLEN - 1].compare(yes)){ count[0]++; } else count[1]++; } } done_flag = true; } } if (count[0] == 0 || count[1] == 0) return 0;//全部是正實例或者負實例 //具體計算熵 根據[+count[0],-count[1]],log2為底通過換底公式換成自然數底數 double sum = count[0] + count[1]; double entropy = -count[0] / sum*log(count[0] / sum) / log(2.0) - count[1] / sum*log(count[1] / sum) / log(2.0); return entropy; } //計算按照屬性attribute划分當前剩余實例的信息增益 double ComputeGain(vector <vector <string> > remain_state, string attribute){ unsigned int j, k, m; //首先求不做划分時的熵 double parent_entropy = ComputeEntropy(remain_state, attribute, blank, true); double children_entropy = 0; //然后求做划分后各個值的熵 vector<string> values = map_attribute_values[attribute]; vector<double> ratio; vector<int> count_values; int tempint; for (m = 0; m < values.size(); m++){ tempint = 0; for (k = 1; k < MAXLEN - 1; k++){ if (!attribute_row[k].compare(attribute)){ for (j = 1; j < remain_state.size(); j++){ if (!remain_state[j][k].compare(values[m])){ tempint++; } } } } count_values.push_back(tempint); } for (j = 0; j < values.size(); j++){ ratio.push_back((double)count_values[j] / (double)(remain_state.size() - 1)); } double temp_entropy; for (j = 0; j < values.size(); j++){ temp_entropy = ComputeEntropy(remain_state, attribute, values[j], false); children_entropy += ratio[j] * temp_entropy; } return (parent_entropy - children_entropy); } int FindAttriNumByName(string attri){ for (int i = 0; i < MAXLEN; i++){ if (!state[0][i].compare(attri)) return i; } cerr << "can't find the numth of attribute" << endl; return 0; } //找出樣例中占多數的正/負性 string MostCommonLabel(vector <vector <string> > remain_state){ int p = 0, n = 0; for (unsigned i = 0; i < remain_state.size(); i++){ if (!remain_state[i][MAXLEN - 1].compare(yes)) p++; else n++; } if (p >= n) return yes; else return no; } //判斷樣例是否正負性都為label bool AllTheSameLabel(vector <vector <string> > remain_state, string label){ int count = 0; for (unsigned int i = 0; i < remain_state.size(); i++){ if (!remain_state[i][MAXLEN - 1].compare(label)) count++; } if (count == remain_state.size() - 1) return true; else return false; } //計算信息增益,DFS構建決策樹 //current_node為當前的節點 //remain_state為剩余待分類的樣例 //remian_attribute為剩余還沒有考慮的屬性 //返回根結點指針 Node * BulidDecisionTreeDFS(Node * p, vector <vector <string> > remain_state, vector <string> remain_attribute){ //if(remain_state.size() > 0){ //printv(remain_state); //} if (p == NULL) p = new Node(); //先看搜索到樹葉的情況 if (AllTheSameLabel(remain_state, yes)){ p->attribute = yes; return p; } if (AllTheSameLabel(remain_state, no)){ p->attribute = no; return p; } if (remain_attribute.size() == 0){//所有的屬性均已經考慮完了,還沒有分盡 string label = MostCommonLabel(remain_state); p->attribute = label; return p; } double max_gain = 0, temp_gain; vector <string>::iterator max_it = remain_attribute.begin(); vector <string>::iterator it1; for (it1 = remain_attribute.begin(); it1 < remain_attribute.end(); it1++){ temp_gain = ComputeGain(remain_state, (*it1)); if (temp_gain > max_gain) { max_gain = temp_gain; max_it = it1; } } //下面根據max_it指向的屬性來划分當前樣例,更新樣例集和屬性集 vector <string> new_attribute; vector <vector <string> > new_state; for (vector <string>::iterator it2 = remain_attribute.begin(); it2 < remain_attribute.end(); it2++){ if ((*it2).compare(*max_it)) new_attribute.push_back(*it2); } //確定了最佳划分屬性,注意保存 p->attribute = *max_it; vector <string> values = map_attribute_values[*max_it]; int attribue_num = FindAttriNumByName(*max_it); new_state.push_back(attribute_row); for (vector <string>::iterator it3 = values.begin(); it3 < values.end(); it3++){ for (unsigned int i = 1; i < remain_state.size(); i++){ if (!remain_state[i][attribue_num].compare(*it3)){ new_state.push_back(remain_state[i]); } } Node * new_node = new Node(); new_node->arrived_value = *it3; if (new_state.size() == 0){//表示當前沒有這個分支的樣例,當前的new_node為葉子節點 new_node->attribute = MostCommonLabel(remain_state); } else BulidDecisionTreeDFS(new_node, new_state, new_attribute); //遞歸函數返回時即回溯時需要1 將新結點加入父節點孩子容器 2清除new_state容器 p->childs.push_back(new_node); new_state.erase(new_state.begin() + 1, new_state.end());//注意先清空new_state中的前一個取值的樣例,准備遍歷下一個取值樣例 } return p; } void Input(){ string s; while (cin >> s, s.compare(endflag) != 0){//-1為輸入結束 item[0] = s; for (int i = 1; i < MAXLEN; i++){ cin >> item[i]; } state.push_back(item);//注意首行信息也輸入進去,即屬性 } for (int j = 0; j < MAXLEN; j++){ attribute_row.push_back(state[0][j]); } } void PrintTree(Node *p, int depth){ for (int i = 0; i < depth; i++) cout << '\t';//按照樹的深度先輸出tab if (!p->arrived_value.empty()){ cout << p->arrived_value << endl; for (int i = 0; i < depth + 1; i++) cout << '\t';//按照樹的深度先輸出tab } cout << p->attribute << endl; for (vector<Node*>::iterator it = p->childs.begin(); it != p->childs.end(); it++){ PrintTree(*it, depth + 1); } } void FreeTree(Node *p){ if (p == NULL) return; for (vector<Node*>::iterator it = p->childs.begin(); it != p->childs.end(); it++){ FreeTree(*it); } delete p; tree_size++; } int main(){ Input(); vector <string> remain_attribute; string outlook("Outlook"); string Temperature("Temperature"); string Humidity("Humidity"); string Wind("Wind"); remain_attribute.push_back(outlook); remain_attribute.push_back(Temperature); remain_attribute.push_back(Humidity); remain_attribute.push_back(Wind); vector <vector <string> > remain_state; for (unsigned int i = 0; i < state.size(); i++){ remain_state.push_back(state[i]); } ComputeMapFrom2DVector(); root = BulidDecisionTreeDFS(root, remain_state, remain_attribute); cout << "the decision tree is :" << endl; PrintTree(root, 0); FreeTree(root); cout << endl; cout << "tree_size:" << tree_size << endl; system("pause"); return 0; } /*測試數據: Day Outlook Temperature Humidity Wind PlayTennis 1 Sunny Hot High Weak no 2 Sunny Hot High Strong no 3 Overcast Hot High Weak yes 4 Rainy Mild High Weak yes 5 Rainy Cool Normal Weak yes 6 Rainy Cool Normal Strong no 7 Overcast Cool Normal Strong yes 8 Sunny Mild High Weak no 9 Sunny Cool Normal Weak yes 10 Rainy Mild Normal Weak yes 11 Sunny Mild Normal Strong yes 12 Overcast Mild High Strong yes 13 Overcast Hot Normal Weak yes 14 Rainy Mild High Strong no end */
--------------------------------------------------------------------------------------------------------
2. 建立決策樹的關鍵,即在當前狀態下選擇哪個屬性作為分類依據。
根據不同的目標函數,建立決策樹主要有一下三種算法。
基於信息論的決策樹算法有ID3、CART和C4.5等算法,其中C4.5和CART兩種算法從ID3算法中衍生而來。
CART和C4.5支持數據特征為連續分布時的處理,主要通過使用二元切分來處理連續型變量,即求一個特定的值-分裂值:特征值大於分裂值就走左子樹,或者就走右子樹。這個分裂值的選取的原則是使得划分后的子樹中的“混亂程度”降低,具體到C4.5和CART算法則有不同的定義方式。
ID3算法由Ross Quinlan發明,建立在“奧卡姆剃刀”的基礎上:越是小型的決策樹越優於大的決策樹(be simple簡單理論)。ID3算法中根據信息論的信息增益評估和選擇特征,每次選擇信息增益最大的特征做判斷模塊。ID3算法可用於划分標稱型數據集,沒有剪枝的過程,為了去除過度數據匹配的問題,可通過裁剪合並相鄰的無法產生大量信息增益的葉子節點(例如設置信息增益閥值)。使用信息增益的話其實是有一個缺點,那就是它偏向於具有大量值的屬性--就是說在訓練集中,某個屬性所取的不同值的個數越多,那么越有可能拿它來作為分裂屬性,而這樣做有時候是沒有意義的,另外ID3不能處理連續分布的數據特征,於是就有了C4.5算法。CART算法也支持連續分布的數據特征。
C4.5是ID3的一個改進算法,繼承了ID3算法的優點。C4.5算法用信息增益率來選擇屬性,克服了用信息增益選擇屬性時偏向選擇取值多的屬性的不足在樹構造過程中進行剪枝;能夠完成對連續屬性的離散化處理;能夠對不完整數據進行處理。C4.5算法產生的分類規則易於理解、准確率較高;但效率低,因樹構造過程中,需要對數據集進行多次的順序掃描和排序。也是因為必須多次數據集掃描,C4.5只適合於能夠駐留於內存的數據集。
CART算法的全稱是Classification And Regression Tree,采用的是Gini指數(選Gini指數最小的特征s)作為分裂標准,同時它也是包含后剪枝操作。ID3算法和C4.5算法雖然在對訓練樣本集的學習中可以盡可能多地挖掘信息,但其生成的決策樹分支較大,規模較大。為了簡化決策樹的規模,提高生成決策樹的效率,就出現了根據GINI系數來選擇測試屬性的決策樹算法CART。
3. 其他目標:
1)信息增益率: 信息增益比率度量是用ID3算法中的信息增益Gain(D,X)和分裂信息度量SplitInformation(D,X)來共同定義的。
分裂信息度量SplitInformation(D,X)就相當於特征X的信息熵。
C4.5 (在ID3中用信息增益選擇屬性時偏向於選擇分枝比較多的屬性值,即取值多的屬性,在C4.5中由於除以SplitInformation(D,X)=H(X),可以削弱這種作用。)
2)Gini系數:
4. 基尼系數,熵,分類誤差之間的關系:
從上圖可以看出,基尼系數和熵之半的曲線非常接近,僅僅在45度角附近誤差稍大。因此,基尼系數可以做為熵模型的一個近似替代。而CART分類樹算法就是使用的基尼系數來選擇決策樹的特征。
同時,為了進一步簡化,CART分類樹算法每次僅僅對某個特征的值進行二分,而不是多分,這樣CART分類樹算法建立起來的是二叉樹,而不是多叉樹。這樣一可以進一步簡化基尼系數的計算,二可以建立一個更加優雅的二叉樹模型。
5. 決策樹C4.5算法的改進
ID3算法有四個主要的不足,一是不能處理連續特征,第二個就是用信息增益作為標准容易偏向於取值較多的特征,最后兩個是缺失值處理的問和過擬合問題。
昆蘭在C4.5算法中改進了上述4個問題。
1)對於第一個問題,不能處理連續特征, C4.5的思路是將連續的特征離散化。
2)對於第二個問題,信息增益作為標准容易偏向於取值較多的特征的問題。我們引入一個信息增益率的變量 ,它是信息增益和特征熵的比值。(啟發式方法,從信息增益高於平均值的特征中選擇信息增益率最高的特征)
3)對於第三個缺失值處理的問題,主要需要解決的是兩個問題,一是在樣本某些特征缺失的情況下選擇划分的屬性,二是選定了划分屬性,對於在該屬性上缺失特征的樣本的處理。
4)對於第4個問題,C4.5引入了正則化系數進行初步的剪枝。
C4.5雖然改進或者改善了ID3算法的幾個主要的問題,仍然有優化的空間。
1)由於決策樹算法非常容易過擬合,因此對於生成的決策樹必須要進行剪枝。剪枝的算法有非常多,C4.5的剪枝方法有優化的空間。思路主要是兩種,一種是預剪枝,即在生成決策樹的時候就決定是否剪枝。
另一個是后剪枝,即先生成決策樹,再通過交叉驗證來剪枝。后面在下篇講CART樹的時候我們會專門講決策樹的減枝思路,主要采用的是后剪枝加上交叉驗證選擇最合適的決策樹。
2)C4.5生成的是多叉樹,即一個父節點可以有多個節點。很多時候,在計算機中二叉樹模型會比多叉樹運算效率高。如果采用二叉樹,可以提高效率。
3)C4.5只能用於分類,如果能將決策樹用於回歸的話可以擴大它的使用范圍。
4)C4.5由於使用了熵模型,里面有大量的耗時的對數運算,如果是連續值還有大量的排序運算。如果能夠加以模型簡化可以減少運算強度但又不犧牲太多准確性的話,那就更好了。
6. CART分類樹算法的最優特征選擇方法
我們知道,在ID3算法中我們使用了信息增益來選擇特征,信息增益大的優先選擇。在C4.5算法中,采用了信息增益比來選擇特征,以減少信息增益容易選擇特征值多的特征的問題。
但是無論是ID3還是C4.5,都是基於信息論的熵模型的,這里面會涉及大量的對數運算。能不能簡化模型同時也不至於完全丟失熵模型的優點呢?
有!CART分類樹算法使用基尼系數來代替信息增益比,基尼系數代表了模型的不純度,基尼系數越小,則不純度越低,特征越好。這和信息增益(比)是相反的。
對於CART分類樹連續值的處理問題,其思想和C4.5是相同的,都是將連續的特征離散化。唯一的區別在於在選擇划分點時的度量方式不同,C4.5使用的是信息增益,則CART分類樹使用的是基尼系數。
下面我們看看CART分類樹建立算法的具體流程
算法輸入是訓練集D,基尼系數的閾值,樣本個數閾值。
輸出是決策樹T。
我們的算法從根節點開始,用訓練集遞歸的建立CART樹。
1) 對於當前節點的數據集為D,如果樣本個數小於閾值或者沒有特征,則返回決策子樹,當前節點停止遞歸。
2) 計算樣本集D的基尼系數,如果基尼系數小於閾值,則返回決策樹子樹,當前節點停止遞歸。
3) 計算當前節點現有的各個特征的各個特征值對數據集D的基尼系數,對於離散值和連續值的處理方法和基尼系數的計算見第二節。缺失值的處理方法和上篇的C4.5算法里描述的相同。
4) 在計算出來的各個特征的各個特征值對數據集D的基尼系數中,選擇基尼系數最小的特征A和對應的特征值a。
根據這個最優特征和最優特征值,把數據集划分成兩部分D1和D2,同時建立當前節點的左右節點,做節點的數據集D為D1,右節點的數據集D為D2.
5) 對左右的子節點遞歸的調用1-4步,生成決策樹。
對於生成的決策樹做預測的時候,假如測試集里的樣本A落到了某個葉子節點,而節點里有多個訓練樣本。則對於A的類別預測采用的是這個葉子節點里概率最大的類別。
7. 總結:
首先我們看看決策樹算法的優點:
1)簡單直觀,生成的決策樹很直觀。
2)基本不需要預處理,不需要提前歸一化,處理缺失值。
3)使用決策樹預測的代價低。
4)既可以處理離散值也可以處理連續值。很多算法只是專注於離散值或者連續值。
5)可以處理多維度輸出的分類問題。
6)相比於神經網絡之類的黑盒分類模型,決策樹在邏輯上可以得到很好的解釋。
7)可以交叉驗證的剪枝來選擇模型,從而提高泛化能力。
8) 對於異常點的容錯能力好,健壯性高。
我們再看看決策樹算法的缺點:
1)決策樹算法非常容易過擬合,導致泛化能力不強。可以通過設置節點最少樣本數量和限制決策樹深度來改進。
2)決策樹會因為樣本發生一點點的改動,就會導致樹結構的劇烈改變。這個可以通過集成學習之類的方法解決。
3)尋找最優的決策樹是一個NP難的問題,我們一般是通過啟發式方法,容易陷入局部最優。可以通過集成學習之類的方法來改善。
4)有些比較復雜的關系,決策樹很難學習,比如異或。這個就沒有辦法了,一般這種關系可以換神經網絡分類方法來解決。
5)如果某些特征的樣本比例過大,生成決策樹容易偏向於這些特征。這個可以通過調節樣本權重來改善。
Bootstraping
Bootstraping的名稱來自成語“pull up by your own bootstraps”,意思是依靠你自己的資源,稱為自助法,它是一種有放回的抽樣方法。
pull up by your own bootstraps”即“通過拉靴子讓自己上升”,意思是“不可能發生的事情”。后來意思發生了轉變,隱喻“不需要外界幫助,僅依靠自身力量讓自己變得更好”。
Bagging
二. 隨機森林:
隨機森林就是通過集成學習的思想將多棵樹集成的一種算法,它的基本單元是決策樹,而它的本質屬於機器學習的一大分支——集成學習(Ensemble Learning)方法。
簡單來說,隨機森林就是由多棵CART(Classification And Regression Tree)構成的。對於每棵樹,它們使用的訓練集是從總的訓練集中有放回采樣出來的,這意味着,總的訓練集中的有些樣本可能多次出現在一棵樹的訓練集中,也可能從未出現在一棵樹的訓練集中。在訓練每棵樹的節點時,使用的特征是從所有特征中按照一定比例隨機地無放回的抽取的,根據Leo Breiman的建議,假設總的特征數量為M,這個比例可以是sqrt(M),1/2sqrt(M),2sqrt(M)。
隨機森林的名稱中有兩個關鍵詞,一個是“隨機”,一個就是“森林”。“森林”我們很好理解,一棵叫做樹,那么成百上千棵就可以叫做森林了,這樣的比喻還是很貼切的,其實這也是隨機森林的主要思想--集成思想的體現。“隨機”的含義我們會在下邊部分講到。
其實從直觀角度來解釋,每棵決策樹都是一個分類器(假設現在針對的是分類問題),那么對於一個輸入樣本,N棵樹會有N個分類結果。而隨機森林集成了所有的分類投票結果,將投票次數最多的類別指定為最終的輸出,這就是一種最簡單的 Bagging 思想。
bagging+決策樹=隨機森林
隨機森林的特點
我們前邊提到,隨機森林是一種很靈活實用的方法,它有如下幾個特點:
- 在當前所有算法中,具有極好的准確率/It is unexcelled in accuracy among current algorithms;
- 能夠有效地運行在大數據集上/It runs efficiently on large data bases;
- 能夠處理具有高維特征的輸入樣本,而且不需要降維/It can handle thousands of input variables without variable deletion;
- 能夠評估各個特征在分類問題上的重要性/It gives estimates of what variables are important in the classification;
- 在生成過程中,能夠獲取到內部生成誤差的一種無偏估計/It generates an internal unbiased estimate of the generalization error as the forest building progresses;
- 對於缺省值問題也能夠獲得很好得結果/It has an effective method for estimating missing data and maintains accuracy when a large proportion of the data are missing
- ... ...
實際上,隨機森林的特點不只有這六點,它就相當於機器學習領域的Leatherman(多面手),你幾乎可以把任何東西扔進去,它基本上都是可供使用的。在估計推斷映射方面特別好用,以致都不需要像SVM那樣做很多參數的調試。具體的隨機森林介紹可以參見隨機森林主頁:Random Forest。
集成學習
集成學習通過建立幾個模型組合的來解決單一預測問題。它的工作原理是生成多個分類器/模型,各自獨立地學習和作出預測。這些預測最后結合成單預測,因此優於任何一個單分類的做出預測。
隨機森林是集成學習的一個子類,它依靠於決策樹的投票選擇來決定最后的分類結果。你可以在這找到用python實現集成學習的文檔:Scikit 學習文檔。
將若干個弱分類器的分類結果進行投票選擇,從而組成一個強分類器,這就是隨機森林bagging的思想。
(關於bagging的一個有必要提及的問題:bagging的代價是不用單棵決策樹來做預測,具體哪個變量起到重要作用變得未知,所以bagging改進了預測准確率但損失了解釋性。)。
每棵樹的按照如下規則生成:
有了樹我們就可以分類了,但是森林中的每棵樹是怎么生成的呢?
1)如果訓練集大小為N,對於每棵樹而言,隨機且有放回地從訓練集中的抽取N個訓練樣本(這種采樣方式稱為bootstrap sample方法),作為該樹的訓練集;
從這里我們可以知道:每棵樹的訓練集都是不同的,而且里面包含重復的訓練樣本(理解這點很重要)。
為什么要隨機抽樣訓練集?(add @2016.05.28)
如果不進行隨機抽樣,每棵樹的訓練集都一樣,那么最終訓練出的樹分類結果也是完全一樣的,這樣的話完全沒有bagging的必要;
為什么要有放回地抽樣?(add @2016.05.28)
我理解的是這樣的:如果不是有放回的抽樣,那么每棵樹的訓練樣本都是不同的,都是沒有交集的,這樣每棵樹都是"有偏的",都是絕對"片面的"(當然這樣說可能不對),也就是說每棵樹訓練出來都是有很大的差異的;而隨機森林最后分類取決於多棵樹(弱分類器)的投票表決,這種表決應該是"求同",因此使用完全不同的訓練集來訓練每棵樹這樣對最終分類結果是沒有幫助的,這樣無異於是"盲人摸象"。
2)如果每個樣本的特征維度為M,指定一個常數m<<M,隨機地從M個特征中選取m個特征子集,每次樹進行分裂時,從這m個特征中選擇最優的;
3)每棵樹都盡最大程度的生長,並且沒有剪枝過程。
一開始我們提到的隨機森林中的“隨機”就是指的這里的兩個隨機性。兩個隨機性的引入對隨機森林的分類性能至關重要。由於它們的引入,使得隨機森林不容易陷入過擬合,並且具有很好得抗噪能力(比如:對缺省值不敏感)。
隨機森林分類效果(錯誤率)與兩個因素有關:
- 森林中任意兩棵樹的相關性:相關性越大,錯誤率越大;
- 森林中每棵樹的分類能力:每棵樹的分類能力越強,整個森林的錯誤率越低。
減小特征選擇個數m,樹的相關性和分類能力也會相應的降低;增大m,兩者也會隨之增大。所以關鍵問題是如何選擇最優的m(或者是范圍),這也是隨機森林唯一的一個參數
袋外錯誤率(oob error)
上面我們提到,構建隨機森林的關鍵問題就是如何選擇最優的m,要解決這個問題主要依據計算袋外錯誤率oob error(out-of-bag error)。
隨機森林有一個重要的優點就是,沒有必要對它進行交叉驗證或者用一個獨立的測試集來獲得誤差的一個無偏估計。它可以在內部進行評估,也就是說在生成的過程中就可以對誤差建立一個無偏估計。
我們知道,在構建每棵樹時,我們對訓練集使用了不同的bootstrap sample(隨機且有放回地抽取)。所以對於每棵樹而言(假設對於第k棵樹),大約有1/3的訓練實例沒有參與第k棵樹的生成,它們稱為第k棵樹的oob樣本。
而這樣的采樣特點就允許我們進行oob估計,它的計算方式如下:
(note:以樣本為單位)
1)對每個樣本,計算它作為oob樣本的樹對它的分類情況(約1/3的樹);
2)然后以簡單多數投票作為該樣本的分類結果;
3)最后用誤分個數占樣本總數的比率作為隨機森林的oob誤分率。
oob誤分率是隨機森林泛化誤差的一個無偏估計,它的結果近似於需要大量計算的k折交叉驗證。
(文獻原文:Put each case left out in the construction of the kth tree down the kth tree to get a classification. In this way, a test set classification is obtained for each case in about one-third of the trees. At the end of the run, take j to be the class that got most of the votes every time case n was oob. The proportion of times that j is not equal to the true class of n averaged over all cases is the oob error estimate. This has proven to be unbiased in many tests.)
更多有關隨機森林的代碼:
2)OpenCV版本
3)Matlab版本
4)R版本
AdaBoost
AdaBoost,是英文"Adaptive Boosting"(自適應增強)的縮寫,是一種機器學習方法,由Yoav Freund和Robert Schapire提出
AdaBoost方法的自適應在於:前一個分類器分錯的樣本會被用來訓練下一個分類器。
AdaBoost方法對於噪聲數據和異常數據很敏感。但在一些問題中,AdaBoost方法相對於大多數其它學習算法而言,不會很容易出現過擬合現象。
w(樣本權重)-->e(誤差)-->α(分類器權重)
三. GDBT
Gradient Boost Decision Tree:
GBDT是一個應用很廣泛的算法,可以用來做分類、回歸。在很多的數據上都有不錯的效果。GBDT這個算法還有一些其他的名字,比如說MART(Multiple Additive Regression Tree),GBRT(Gradient Boost Regression Tree),Tree Net等,其實它們都是一個東西(參考自wikipedia – Gradient Boosting),發明者是Friedman
Gradient Boost其實是一個框架,里面可以套入很多不同的算法。
Boost是"提升"的意思,一般Boosting算法都是一個迭代的過程,每一次新的訓練都是為了改進上一次的結果。
原始的Boost算法是在算法開始的時候,為每一個樣本賦上一個權重值,初始的時候,大家都是一樣重要的。在每一步訓練中得到的模型,會使得數據點的估計有對有錯,我們就在每一步結束后,增加分錯的點的權重,減少分對的點的權重,這樣使得某些點如果老是被分錯,那么就會被“嚴重關注”,也就被賦上一個很高的權重。然后等進行了N次迭代(由用戶指定),將會得到N個簡單的分類器(basic learner),然后我們將它們組合起來(比如說可以對它們進行加權、或者讓它們進行投票等),得到一個最終的模型。
而Gradient Boost與傳統的Boost的區別是,每一次的計算是為了減少上一次的殘差(residual),而為了消除殘差,我們可以在殘差減少的梯度(Gradient)方向上建立一個新的模型。所以說,在Gradient Boost中,每個新的模型的簡歷是為了使得之前模型的殘差往梯度方向減少,與傳統Boost對正確、錯誤的樣本進行加權有着很大的區別。
下面舉個例子:
假設輸入數據x可能屬於5個分類(分別為1,2,3,4,5),訓練數據中,x屬於類別3,則y = (0, 0, 1, 0, 0),假設模型估計得到的F(x) = (0, 0.3, 0.6, 0, 0),則經過Logistic變換后的數據p(x) = (0.16,0.21,0.29,0.16,0.16),y - p得到梯度g:(-0.16, -0.21, 0.71, -0.16, -0.16)。觀察這里可以得到一個比較有意思的結論:
假設gk為樣本當某一維(某一個分類)上的梯度:
gk>0時,越大表示其在這一維上的概率p(x)越應該提高,比如說上面的第三維的概率為0.29,就應該提高,屬於應該往“正確的方向”前進
越小表示這個估計越“准確”
gk<0時,越小,負得越多表示在這一維上的概率應該降低,比如說第二維0.21就應該得到降低。屬於應該朝着“錯誤的反方向”前進
越大,負得越少表示這個估計越“不錯誤 ”
總的來說,對於一個樣本,最理想的梯度是越接近0的梯度。所以,我們要能夠讓函數的估計值能夠使得梯度往反方向移動(>0的維度上,往負方向移動,<0的維度上,往正方向移動)最終使得梯度盡量=0),並且該算法在會嚴重關注那些梯度比較大的樣本,跟Boost的意思類似。
得到梯度之后,就是如何讓梯度減少了。這里是用的一個迭代+決策樹的方法,當初始化的時候,隨便給出一個估計函數F(x)(可以讓F(x)是一個隨機的值,也可以讓F(x)=0),然后之后每迭代一步就根據當前每一個樣本的梯度的情況,建立一棵決策樹。就讓函數往梯度的反方向前進,最終使得迭代N步后,梯度越小。
這里建立的決策樹和普通的決策樹不太一樣,首先,這個決策樹是一個葉子節點數J固定的,當生成了J個節點后,就不再生成新的節點了。
算法流程為:
0. 表示給定一個初始值
1. 表示建立M棵決策樹(迭代M次)
2. 表示對函數估計值F(x)進行Logistic變換
3. 表示對於K個分類進行下面的操作(其實這個for循環也可以理解為向量的操作,每一個樣本點xi都對應了K種可能的分類yi,所以yi, F(xi), p(xi)都是一個K維的向量,這樣或許容易理解一點)
4. 表示求得殘差減少的梯度方向
5. 表示根據每一個樣本點x,與其殘差減少的梯度方向,得到一棵由J個葉子節點組成的決策樹
6. 為當決策樹建立完成后,通過這個公式,可以得到每一個葉子節點的增益(這個增益在預測的時候用的)
每個增益的組成其實也是一個K維的向量,表示如果在決策樹預測的過程中,如果某一個樣本點掉入了這個葉子節點,則其對應的K個分類的值是多少。比如 說,GBDT得到了三棵決策樹,一個樣本點在預測的時候,也會掉入3個葉子節點上,其增益分別為(假設為3分類的問題):
(0.5, 0.8, 0.1), (0.2, 0.6, 0.3), (0.4, 0.3, 0.3),那么這樣最終得到的分類為第二個,因為選擇分類2的決策樹是最多的。
7. 的意思為,將當前得到的決策樹與之前的那些決策樹合並起來,作為新的一個模型(跟6中所舉的例子差不多)