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. 如果標簽中的某些類占主導地位,決策樹學習者會創建偏向主導類的樹。因此,建議在擬合決策樹之前平衡數據集。