機器學習之決策樹(Decision Tree)
1 引言
決策樹(Decision Tree)是一種非引數的有監督學習方法,它能夠從一系列有特徵和標籤的資料中總結出決策規則,並用樹狀圖的結構來呈現這些規則,以解決分類和迴歸問題。決策樹中每個內部節點表示一個屬性上的判斷,每個分支代表一個判斷結果的輸出,最後每個葉節點代表一種分類結果。
決策樹演算法包括ID3、C4.5以及C5.0等,這些演算法容易理解,適用各種資料,在解決各種問題時都有良好表現,尤其是以樹模型為核心的各種整合演算法,在各個行業和領域都有廣泛的應用。
我們用一個例子來了解決策樹的工作原理以及它能解決什麼問題。下面這個列表是一些動物的特徵資訊,左面第一列是動物的名字,第一行是特徵的名稱。決策樹的本質是一種圖結構,我們可以根據一些問題就可以對動物實現分類。如下表所示是一些已知物種以及特徵,如果要實現一個根據物種特徵將動物分為哺乳類和非哺乳類的決策樹,可以將類別標號這一列作為標籤,其他列作為決策樹的訓練資料,演算法根據這些特徵訓練得到一棵決策樹,然後我們就可以使用不存在列表表中的動物特徵,利用決策樹判斷動物是否為哺乳類動物。
名字 |
體溫 | 表皮覆蓋 | 胎生 | 水生動物 | 飛行動物 | 有腿 | 冬眠 | 類標號 |
人類 | 恆溫 | 毛髮 | 是 | 否 | 否 | 是 | 否 | 哺乳類 |
鮭魚 | 冷血 | 鱗片 | 否 | 是 | 否 | 否 | 否 | 魚類 |
鯨 | 恆溫 | 毛髮 | 是 | 是 | 否 | 否 | 否 | 哺乳類 |
青蛙 | 冷血 | 無 | 否 | 半 | 否 | 是 | 是 | 兩棲類 |
巨蜥 | 冷血 | 鱗片 | 否 | 否 | 否 | 是 | 否 | 爬行類 |
蝙蝠 | 恆溫 | 毛髮 | 是 | 否 | 是 | 是 | 是 | 哺乳類 |
鴿子 | 恆溫 | 羽毛 | 是 | 否 | 是 | 是 | 否 | 鳥類 |
貓 | 恆溫 | 毛髮 | 是 | 否 | 否 | 是 | 否 | 哺乳類 |
豹紋鯊 | 冷血 | 鱗片 | 是 | 是 | 否 | 否 | 否 | 魚類 |
海龜 | 冷血 | 鱗片 | 否 | 半 | 否 | 是 | 否 | 爬行類 |
企鵝 | 恆溫 | 羽毛 | 否 | 半 | 否 | 是 | 否 | 鳥類 |
豪豬 | 恆溫 | 剛毛 | 是 | 否 | 否 | 是 | 是 | 哺乳類 |
鮼 | 冷血 | 鱗片 | 否 | 是 | 否 | 否 | 否 | 魚類 |
嶸螺 | 冷血 | 無 | 否 | 半 | 否 | 是 | 是 | 兩棲類 |
下圖就是一個簡單的決策樹,我們可以根據這各決策樹對新物種進行預測,判斷其是否為哺乳動物。當然這只是一個非常簡單的決策樹,我們可以根據大量的訓練資料來補充完善、簡化我們的決策樹,以便我們的決策樹能判斷各種不同的新物種。
python的sklearn庫中tree模組已經包含了我們平常使用到的決策樹模型,可以直接呼叫,餐後通過調整合適的引數,獲取分類結果比較理想的決策樹。sklearn.tree模組包含以下五個類,接下來我們主要看一下分類樹和迴歸樹是如何使用的。
tree.DecisionTreeClassifier | 分類樹 |
tree.DecisionTreeRegressor | 迴歸樹 |
tree.export_graphviz | 將生成的決策樹匯出為DOT格式,畫圖專用 |
tree.ExtraTreeClassifier | 高隨機版本的分類樹 |
tree.ExtraTreeRegressor | 高隨機版本的迴歸樹 |
利用sklearn中的模型,決策樹的構建流程以及核心程式碼如下:
from sklearn import tree #匯入需要的模組 clf = tree.DecisionTreeClassifier() #例項化 clf = clf.fit(X_train,y_train) #用訓練集資料訓練模型 result = clf.score(X_test,y_test) #匯入測試集,從介面中呼叫需要的資訊
sklearn中我們最常用的兩個模型是分類樹和迴歸樹,分類樹適合於對事物進行分類,一般使用離散的資料;而回歸樹更適合預測連續、具體的數值。下面我們分別學習一下分類樹和迴歸樹原理和使用方法。
2 分類樹—DecisionTreeClassifier
class sklearn.tree.DecisionTreeClassifier (criterion=’gini’, splitter=’best’, max_depth=None,min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None,random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None,class_weight=None, presort=False)
以上是sklearn庫中分類樹的建構函式,其中包含了很多引數,但是在我們實際應用中有些引數的使用頻率並不是很高,我們只需要重點關注一下幾個重要的引數。
2.1 重要引數
2.1.1 criterion
在解釋criterion引數之前,我們需要先了解一下“不純度”的概念。對於決策樹來說,我們需要將樣本轉換為一棵樹,首先要找到最佳結點和分枝方法,對分類樹來說衡量這個“最佳“”的指標叫做“不純度”,通常情況下,不純度越低,決策樹對訓練集的擬合就越好。現在使用的決策樹演算法在分枝方法上的核心大多是圍繞在對某個不純度相關指標的最優化上。
不純度是基於結點計算的,也就是說每一個結點都會有一個不純度,並且子節點的不純度一定是低於父節點的,也就是說,在一棵決策樹上,葉子結點的不純度一定是最低的。
解釋完不純度,我們看 criterion 這個引數時候就容易理解多了,criterion這個引數就是用來決定不純度計算方法的。該引數有兩種選項:“entropy”、“gini”。
criterion | 不純度計算方法 |
"entropy" | 資訊熵(Entropy) |
"gini" | 基尼係數(Gini Impurity) |
公式中 t 代表給定的結點,i 代表標籤的任意分類,p(i | t)代表標籤分類 i 在結點 t 上所佔的比例。資訊熵對不純度更加敏感,對不純度的懲罰最強,在實際應用中資訊熵和基尼係數的效果基本相同,但是資訊熵的計算所要花費的時間要比基尼係數更多,因為基尼係數的計算不涉及對數。在實際應用過程中可以調整引數,兩種計算方式哪一種訓練出的模型評分越高就使用哪一個。
2.1.2 random_state & splitter
random_state用來設定分枝中的隨機模式的引數,預設None,在高維度時隨機性會表現更明顯,低維度的資料(比如鳶尾花資料集),隨機性幾乎不會顯現。輸入任意整數,會一直長出同一棵樹,讓模型穩定下來。
splitter也是用來控制決策樹中的隨機選項的,有兩種輸入值,輸入”best",決策樹在分枝時雖然隨機,但是還是會優先選擇更重要的特徵進行分枝(重要性可以通過屬性feature_importances_檢視),輸入“random",決策樹在分枝時會更加隨機,樹會因為含有更多的不必要資訊而更深更大,並因這些不必要資訊而降低對訓練集的擬合。這也是防止過擬合的一種方式。當你預測到你的模型會過擬合,用這兩個引數來幫助你降低樹建成之後過擬合的可能
2.1.3 剪枝引數
在不加限制的情況下,一棵決策樹會生長到衡量不純度的指標最優,或者沒有更多的特徵可用為止。這樣的決策樹往往會過擬合,這就是說,它會在訓練集上表現很好,在測試集上卻表現糟糕。我們收集的樣本資料不可能和整體的狀況完全一致,因此當一棵決策樹對訓練資料有了過於優秀的解釋性,它找出的規則必然包含了訓練樣本中的噪聲,並使它對未知資料的擬合程度不足。
為了讓決策樹有更好的泛化性,我們要對決策樹進行剪枝。剪枝策略對決策樹的影響巨大,正確的剪枝策略是優化決策樹演算法的核心。關於剪枝策略的引數包括:max_depth、min_samples、min_samples_split、max_features和min_impurity_decrease。
max_depth
max_depth作用在於限制樹的最大深度,超過限定深度的樹枝全部剪掉。這是用得最廣泛的剪枝引數,在高維度低樣本量時非常有效。決策樹多生長一層,對樣本量的需求會增加一倍,所以限制樹深度能夠有效地限制過擬合。在整合演算法中也非常實用。實際使用時,建議從=3開始嘗試,看看擬合的效果再決定是否增加設定深度。下面程式碼是一個max_depth對決策樹模型評分影響的測試程式碼:
import graphviz from sklearn import tree from sklearn.datasets import load_wine from sklearn import model_selection import matplotlib.pyplot as plt wine = load_wine() x_train,x_test,y_train,y_test = model_selection.train_test_split(wine.data,wine.target,test_size=0.3) score = [] for i in range(10): # 例項化 clf = tree.DecisionTreeClassifier(criterion="entropy" ,random_state=30 ,splitter="random" ,max_depth=i+1 # ,min_samples_leaf=10 # ,min_samples_split=10 ) # 用訓練集訓練模型 clf = clf.fit(x_train,y_train) # 用測試資料對模型進行評估 once = clf.score(x_test,y_test) score.append(once) plt.plot(range(1,11),score,color="red",label="max_depth") plt.legend() plt.show()
min_samples &min_samples_split
min_samples_leaf限定,一個節點在分枝後的每個子節點都必須包含至少min_samples_leaf個訓練樣本,否則分枝就不會發生,或者,分枝會朝著滿足每個子節點都包含min_samples_leaf個樣本的方向去發生。一般搭配max_depth使用,在迴歸樹中有神奇的效果,可以讓模型變得更加平滑。這個引數的數量設定得太小會引起過擬合,設定得太大就會阻止模型學習資料。一般來說,建議從=5開始使用。如果葉節點中含有的樣本量變化很大,建議輸入浮點數作為樣本量的百分比來使用。同時,這個引數可以保證每個葉子的最小尺寸,可以在迴歸問題中避免低方差,過擬合的葉子節點出現。對於類別不多的分類問題,=1通常就是最佳選擇。
min_samples_split限定一個節點必須包含至少min_samples_split個訓練樣本,這個節點才允許被分支,否則分支就不會發生。
max_features &min_impurity_decrease
這兩個引數一般和max_depth一起使用,max_features限制分枝時考慮的特徵個數,超過限制個數的特徵都會被捨棄。和max_depth異曲同工,max_features是用來限制高維度資料的過擬合的剪枝引數,但其方法比較暴力,是直接限制可以使用的特徵數量而強行使決策樹停下的引數,在不知道決策樹中的各個特徵的重要性的情況下,強行設定這個引數可能會導致模型學習不足。如果希望通過降維的方式防止過擬合,建議使用PCA,ICA或者特徵選擇模組中的降維演算法。
min_impurity_decrease限制資訊增益的大小,資訊增益小於設定數值的分枝不會發生。
2.1.4 目標權重引數
能夠調整樣本標籤平衡性的引數有兩個:class_weight 和 min_weight_fraction_leaf。由於一些樣本資料中某一類的資料本身就佔比很大或者很小,例如真實網路流量中SQL注入攻擊流量的佔比是很小的,可能只佔1%。如果用一個沒有訓練過的模型,將所有的流量判斷為非SQL注入攻擊流量,那該模型的準確率也是99%。所以我們需要使用class_weight引數對樣本標籤進行一定的均衡,給少量的標籤更多的權重,讓模型更偏向於少數類,向捕獲少數類的方向建模。不設定該引數時,預設為None,資料集中所有標籤的權重一樣。
有了權重之後,樣本量就不再是單純地記錄數目,而是受輸入的權重影響了,因此這時候剪枝,就需要搭配min_weight_fraction_leaf這個基於權重的剪枝引數來使用。另請注意,基於權重的剪枝引數(例如min_weight_fraction_leaf)將比不知道樣本權重的標準(比如min_samples_leaf)更少偏向主導類。如果樣本是加權的,則使用基於權重的預修剪標準來更容易優化樹結構,這確保葉節點至少包含樣本權重的總和的一小部分。
2.2 重要屬性和介面
模型訓練之後我們可以呼叫檢視模型的一些屬性來了解模型的各種性質,其中比較重要的是feature_importances_,該屬性包含了各個特徵對模型的重要性。
除了一些通用的介面fit、score等,決策樹常用的介面還有 apply 和 predict。apply的輸入為測試集,返回值為每個測試樣本所在的葉子結點的索引。predict輸入為測試集,返回值為每個測試贗本的標籤。需要注意的是當介面的輸入是測試集或者訓練集時,輸入的特徵矩陣必須至少是二維矩陣。
2.3 例項:泰坦尼克號倖存者的預測
泰坦尼克號的沉沒是世界上最嚴重的海難事故之一,我們通過分類樹模型來預測一下那些人可能成為倖存者。其中資料集來自著名資料分析競賽平臺kaggle(資料下載連結),train.csv為我們的訓練資料集,test.csv為我們的測試資料集。
2.3.1 資料預處理
建立模型之前我們先對我們的資料進行一下預處理,下圖是原始資料的相關資訊,可以呼叫data.info()檢視。我們的目標是建立一棵預測那些人可能存活的決策樹,那麼我們可以先剔除一些與該判斷完全無關的特徵,也就是剔除與存活完全無關的列。這裡Cabin指的是小倉房間號,Name是乘客名字,Ticket是船票號,這三個資訊並不能為我們預測乘客是否存活做出貢獻,所以我們可以直接將其剔除。
某些列存在缺失值,比如年齡列,大部分列有 891 個數據,而Age列只有 714 個數據,所以可以進行填充,我們這裡為了減小對原始資料的影響,空缺資料我們全部填充為年齡的均值。
由於我們機器學習模型處理的都是數值型資料,但是我們的訓練資料集中包含一些物件資料,比如Sex、Embarked等,這裡可能是字串型別,所以我們需要將字串型別轉換為數值型,我們可以呼叫apply方法實現。程式碼如下:
# 將物件轉換成數值,兩種方法 labels = data["Embarked"].unique().tolist() data["Embarked"] = data["Embarked"].apply(lambda x:labels.index(x)) data.loc[:,"Sex"] = (data.loc[:,"Sex"] == "male").astype("int")
對存在缺失值和物件型資料處理過後我們就可以提取出標籤和特徵矩陣,我們預測的是乘客是否能存活,那麼我們就將“Survived”列作為標籤列,其他列作為特徵。將資料的70%作為訓練集,30%作為測試集。程式碼如下:
# 將標籤和特徵分離 x = data.iloc[:,data.columns != "Survived"] y = data.iloc[:,data.columns == "Survived"] # 劃分訓練集和測試集 xTrain,xTest,yTrain,yTest = train_test_split(x,y,test_size=0.3) for i in [xTrain,xTest,yTrain,yTest]: i = i.reset_index()
2.3.2 調整引數
資料處理之後我們就可以進行正常的例項化模型、訓練資料,評估模型。為了評估比較好的模型,我們對引數需要進行不斷調整。比如樹的深度max_depth等等。對單一引數進行調整我們可以使用學習曲線的方法得到我們的最優引數,對於同時對多引數調整我們可以使用網格搜尋方法進行調整。
學習曲線法的原理非常簡單,通過調整某一引數,然後訓練資料,評估模型,每一個引數對應於一個評估分數,這樣我們可以的得到一條引數取值為橫座標,評估分數為縱座標的曲線,從曲線中我們可以很直觀的得到我們想要的最優引數。程式碼如下:
scoreTest = [] scoreTrain = [] for i in range(10): clf = DecisionTreeClassifier(random_state=0 ,max_depth=i+1 ,criterion="entropy" ) clf = clf.fit(xTrain,yTrain) onceTrain = clf.score(xTrain,yTrain) onceTest = cross_val_score(clf,x,y,cv=10).mean() scoreTest.append(onceTest) scoreTrain.append(onceTrain) print(max(scoreTest)) plt.figure() plt.plot(range(1,11),scoreTrain,color="red",label="train") plt.plot(range(1,11),scoreTest,color="blue",label="test") plt.xticks(range(1,11)) plt.legend() plt.show()
網格搜尋是利用GridSearchCV類,首先構建一個引數字典,將我們要調整的多個引數放在一個字典中,通過GridSearchCV類的例項化物件,對模型進行訓練,最後得出一個高分數的引數列表。通過best_params_檢視最優引數列表,best_score_屬性檢視模型最高分。
網格搜尋存在一個缺點就是不能動態的調整我們要控制的引數的個數,也就是說我們構造的parameters字典中存在的引數,在訓練模型時候都需要用上,此時就會存在一個問題,某些引數使用預設值時候模型的分數比使用該引數時分數還要高,也就是說不能確定該引數是否需要調整。這就導致我們網格搜尋得到的引數列表訓練出來的模型分數並不一定會很高,跟對這一問題,還是需要靠我們的實際應用經驗來得到我們想要的最優引數。
parameters = {"criterion":("gini","entropy") ,"splitter":("best","random") ,"max_depth":[*range(1,10)] ,"min_samples_leaf":[*range(1,50,5)] ,"min_impurity_decrease":[*np.linspace(0.0,0.5,50)] } clf = DecisionTreeClassifier(random_state=0) GS = GridSearchCV(clf,parameters,cv=10) GS = GS.fit(xTrain,yTrain) print(GS.best_params_) print(GS.best_score_)
3 迴歸樹DecisionTreeRegressor
class sklearn.tree.DecisionTreeRegressor (criterion=’mse’, splitter=’best’, max_depth=None,min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None,random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, presort=False)
3.1 重要引數
迴歸樹中的引數大部分和分類樹一樣,比如max_depth、min_samples_split等,但是criterion引數和分類樹的取值並不一樣,它迴歸樹衡量分支質量的指標,包含三種:"mse"、"friedman_mse" 和 "mae",三種引數它們代表三種計算方法。
- "mse"使用均方誤差mean squared error(MSE),父節點和葉子節點之間的均方誤差的差額將被用來作為特徵選擇的標準,這種方法通過使用葉子節點的均值來最小化L2損失
- “friedman_mse”使用費爾德曼均方誤差,這種指標使用弗裡德曼針對潛在分枝中的問題改進後的均方誤差
- "mae"使用絕對平均誤差MAE
3.2 屬性和介面
屬性和介面的話迴歸樹和分類樹基本一樣,屬性最重要的依然是feature_importances_,介面比較常用的是apply、fit、predict、score。
3.3 例項:一維迴歸影象繪製
問題:在二維平面上使用決策樹來擬合一條正弦曲線,並新增一些噪聲來觀察迴歸樹的表現。
1、建立資料
正弦曲線資料我們就用(x,y)表示,x軸座標我們就建立一組隨機分佈在0~5上的數值。y座標資料我們就取x軸座標的sin值,再在y值上新增一部分噪聲。我們使用Python的numpy來生成我們的資料。程式碼如下:
import numpy as np rng = np.random.RandomState(1) x_train = np.sort(5 * rng.rand(80, 1), axis=0) y = np.sin(x_train).ravel() y[::5] += 3 * (0.5 - rng.rand(16))
2、例項化訓練模型
例項化兩個迴歸樹模型,用以我們調整引數,觀察樹的深度對訓練模型的影響,一個模型的max_depth設定為2,另一個設定為5。程式碼如下:
from sklearn.tree import DecisionTreeRegressor
regressor1 = DecisionTreeRegressor(max_depth=2) regressor2 = DecisionTreeRegressor(max_depth=5) regressor1.fit(x_train, y) regressor2.fit(x_train, y)
3、測試集匯入模型,預測結果
x_test = np.arange(0.0,5.0,0.01)[:,np.newaxis] y_1 = regressor1.predict(x_test) y_2 = regressor2.predict(x_test) print(y_1) print(y_2)
4、繪製圖像
將我們的原始訓練資料以散點圖形式展現出來,將深度為2和5的訓練模型的預測結果以折線圖的形式展現出來。程式碼如下:
plt.figure() plt.scatter(x_train, y, s = 20,edgecolors="black", c ="darkorange", label ="data") plt.plot(x_test,y_1,color="cornflowerblue",label="max_depth=2",linewidth=2) plt.plot(x_test,y_2,color="yellowgreen",label="max_depth=5",linewidth=2) plt.xlabel("data") plt.ylabel("target") plt.title("Decision Tree Regression") plt.legend() plt.show()
完整程式碼如下:
import numpy as np from sklearn.tree import DecisionTreeRegressor import matplotlib.pyplot as plt
#建立包含噪聲的sin函式散點資料 rng = np.random.RandomState(1) x_train = np.sort(5 * rng.rand(80, 1), axis=0) y = np.sin(x_train).ravel() y[::5] += 3 * (0.5 - rng.rand(16))
#例項化,並訓練模型 regressor1 = DecisionTreeRegressor(max_depth=3) regressor2 = DecisionTreeRegressor(max_depth=5) regressor1.fit(x_train, y) regressor2.fit(x_train, y) x_test = np.arange(0.0,5.0,0.01)[:,np.newaxis] y_1 = regressor1.predict(x_test) y_2 = regressor2.predict(x_test) #print(y_1) #print(y_2)
#畫圖 plt.figure() plt.scatter(x_train, y, s = 20,edgecolors="black", c ="darkorange", label ="data") plt.plot(x_test,y_1,color="cornflowerblue",label="max_depth=2",linewidth=2) plt.plot(x_test,y_2,color="yellowgreen",label="max_depth=5",linewidth=2) plt.xlabel("data") plt.ylabel("target") plt.title("Decision Tree Regression") plt.legend() plt.show()
結果如下如所示,藍線代表的是決策樹深度為2時,所預測的結果;綠線代表決策樹深度為5時,所預測的結果。從途中我們可以看到深度為5時,決策樹會出現訓練資料的過擬合情況,也就是說模型在訓練資料上表現的很好,但是在測試資料上表現得比較差。所以如果max_depth設定的太高,決策樹就會學習的太過精細,從訓練資料中的更多細節會被包含進去,其中很可能就包含一些噪聲形成過擬合,這樣很不利於我們將決策樹用於測試資料中。對於其他引數我們也可以通過調整引數的不同取值來獲得一個比較友好的引數取值,比如畫學習曲線,我們就可以很直觀的找到最佳引數取值點。
4 總結
4.1 決策樹的優點
1. 易於理解和解釋,因為樹木可以畫出來被看見
2. 需要很少的資料準備。其他很多演算法通常都需要資料規範化,需要建立虛擬變數並刪除空值等。但請注意,sklearn中的決策樹模組不支援對缺失值的處理。
3. 使用樹的成本(比如說,在預測資料的時候)是用於訓練樹的資料點的數量的對數,相比於其他演算法,這是一個很低的成本。
4. 能夠同時處理數字和分類資料,既可以做迴歸又可以做分類。其他技術通常專門用於分析僅具有一種變數型別的資料集。
5. 能夠處理多輸出問題,即含有多個標籤的問題,注意與一個標籤中含有多種標籤分類的問題區別開
6. 是一個白盒模型,結果很容易能夠被解釋。如果在模型中可以觀察到給定的情況,則可以通過布林邏輯輕鬆解釋條件。相反,在黑盒模型中(例如,在人工神經網路中),結果可能更難以解釋。
7. 可以使用統計測試驗證模型,這讓我們可以考慮模型的可靠性。
4.2 決策樹的缺點
1. 決策樹學習者可能建立過於複雜的樹,這些樹不能很好地推廣資料。這稱為過度擬合。修剪,設定葉節點所需的最小樣本數或設定樹的最大深度等機制是避免此問題所必需的,而這些引數的整合和調整對初學者來說會比較晦澀
2. 決策樹可能不穩定,資料中微小的變化可能導致生成完全不同的樹,這個問題需要通過整合演算法來解決。
3. 決策樹的學習是基於貪婪演算法,它靠優化區域性最優(每個節點的最優)來試圖達到整體的最優,但這種做法不能保證返回全域性最優決策樹。這個問題也可以由整合演算法來解決,在隨機森林中,特徵和樣本會在分枝過程中被隨機取樣。
4. 有些概念很難學習,因為決策樹不容易表達它們,例如XOR,奇偶校驗或多路複用器問題。
5. 如果標籤中的某些類占主導地位,決策樹學習者會建立偏向主導類的樹。因此,建議在擬合決策樹之前平衡資料集。