【机器学习系列】聊聊决策树
決策樹是簡單易學且具有良好解釋性的模型,但實話說,我在工作中用的不多,通常會選擇更加復雜一些的模型,如隨機森林、XGBoots之類的模型,但要理解這些模型,對決策樹的學習是必不可少的,所以本文就基于sklearn(Scikit Learn)來討論一下決策樹相關的內容。
決策樹基本使用與可視化
為了方便,我們直接使用sklearn提供的鳶尾花數據集來展示決策樹的使用。
首先,導入鳶尾花數據集:
from?sklearn.datasets?import?load_iris iris?=?load_iris()如果第一次使用,sklearn會自動幫我們下載,你需要等待一下則可。
獲得鳶尾花數據集后,我們使用數據集中花瓣的長度和寬度作為特征,將花瓣的種類作為target,然后使用sklearn的DecisionTreeClassifier構建分類決策樹,該決策樹會基于特征,對花瓣的種類進行分類,代碼如下:
from?sklearn.tree?import?DecisionTreeClassifierX?=?iris.data[:,?2:]??#?花瓣的長與寬 y?=?iris.target??#?花的種類#?分類決策樹,max_depth=2表示決策樹最大高度為2 tree_clf?=?DecisionTreeClassifier(max_depth=2) tree_clf.fit(X,?y)構建完后,可以通過sklearn提供的plot_tree方法可視化決策樹:
from?sklearn?import?tree tree.plot_tree(tree_clf)???圖1
如果覺得sklearn的plot_tree方法繪制出的決策樹不太美觀,可以甩graphviz進行繪制,graphviz是一個繪圖軟件,需要先自行安裝,在MacOS下,安裝簡單:
brew?install?graphviz pip?install?graphviz安裝完后,變可以進行決策樹的繪制了:
from?graphviz?import?Source from?sklearn.tree?import?export_graphvizexport_graphviz(tree_clf,out_file=os.path.join(IMAGES_PATH,?"iris_tree.dot"),feature_names=iris.feature_names[2:],??#?特征class_names=iris.target_names,??#?分類rounded=True,??#?圓角filled=True,??#?顏色填充)Source.from_file(os.path.join(IMAGES_PATH,?"iris_tree.dot"))效果如圖:
圖2
怎么使用這棵決策樹?
假設你手里有一朵剛摘的鳶尾花,要對其進行分類,你會從決策樹的根節點開始(深度為0),判斷該花的花瓣寬度是否小于0.8cm,如果小于,那么就來到根的左子節點(圖中橙色節點,深度為1),該節點是葉子節點(沒有其他子節點),葉子節點的class值便是當前這棵決策樹對當前這朵花的預測,它認為這朵花的種類是setosa。
假設你還有另外一朵花,發現它的花瓣寬度大于0.8cm,那么你還需要繼續判斷花瓣寬度是否小于等于1.75cm,如果小于,那么就來的圖中綠色的葉子節點,此時,決策樹預測這朵花的種類是versicolor。
純度
先不糾結決策樹的細節,來思考一個關鍵問題:決策樹是基于什么來做決策的?,比如圖2中的根節點,分裂成了2個子節點,左邊的子節點,不再分裂,而右邊的子節點繼續分裂,這里的分裂的依據是什么?
這就涉及到純度的概念,對于分類決策樹而言,通常會使用基尼系數或熵來判斷某個節點的純度,而對于回歸決策樹而言,通常會使用均方誤差(MSE)來判斷某節點純度。
圖3
基尼系數
我們先從基尼系數聊起。
百度搜索基尼系數,會得到如下定義:
基尼系數(英文:Gini index、Gini Coefficient)是指國際上通用的、用以衡量一個國家或地區居民收入差距的常用指標。基尼系數最大為“1”,最小等于“0”。
在決策樹中,基本性質不變,只是它被用來表示決策樹中節點的純度,這里的純度正如其字面意思,如果決策樹的葉子節點里,只有一種類別,那么它就是純的,該葉子節點的基尼系數為0。
我們看回圖2,圖2中的gini表示當前節點的基尼系數,看到橙色節點(深度為1的左節點),可以發現gini為0,這是因為該節點的訓練實例數量(節點中的samples屬性描述)為50個,而這50個都是種類setosa,如果訓練實例的個數不全部屬于同一類,那么節點就是不純的,使用基尼系數表示不純度的公式為:
公式中表示決策樹第i個節點中實例數量與k類實例的比率。
還是以圖2為例,就公式代入,我們來計算一下圖2中綠色節點的gini。
In?[1]:?import?numpyIn?[2]:?1?-?numpy.square(49/54)?-?numpy.square(5/54) Out[2]:?0.1680384087791495熵
熵的概念源于熱力學,很多硬科幻小說中也會出現它以及它涉及的熱力學第二原理。
在熱力學領域,熵主要用于描述分子混亂程度,而這里我們提及的熵主要是在信息論中的定義。在信息論里,熵被用于衡量一條信息的平均信息內容,一條有價值的信息會讓決策的可能性(混亂程度)降低,這便是熵減的過程,在信息論里,將熵減過程稱為信息增益。
決策樹也會使用熵作為不純度的一種指標,如果節點的數據集中只包含一種類型的實例,那么熵為0,純度為0,用熵表示不純度的公式為:
sklearn中使用的決策樹默認會使用基尼系數來作為節點不純度的指標,但我們可以將criterion參數設置為「entropy」,讓sklearn使用熵來作為不純度計算方式,代碼如下:
from?sklearn.tree?import?DecisionTreeClassifierX?=?iris.data[:,?2:]??#?花瓣的長與寬 y?=?iris.target??#?花的種類#?分類決策樹,max_depth=2表示決策樹最大高度為2 tree_clf2?=?DecisionTreeClassifier(max_depth=2,?criterion="entropy") tree_clf2.fit(X,?y)相同的方式繪制一下:
圖4
獲得圖4后,我們來使用一下熵的公式來手動計算一下紫色節點的熵(entropy)。
In?[4]:?import?mathIn?[5]:?-(1/46)*math.log((1/46),?2)-(45/46)*math.log((45/46),2) Out[5]:?0.15109697051711368從圖2和圖4看,使用基尼系數或熵訓練出的分類決策樹是一樣的,那我們該使用基尼系數還是使用熵呢?一旦腦海里出現這種問題,一律使用默認的,sklearn默認使用基尼系數,那我們就使用基尼系數,這是經驗之談,sklearn的開發者有我們目前知識量暫時無法理解的考慮。
其實呢,大多數情況下,使用基尼系數還是熵,差異不大,都會產生相似的決策樹,只是基尼系數計算會快一些,所以作為sklearn的默認值,當然,差異還是有的,使用基尼系數會讓你的決策樹從樹枝中分裂出最常見的類別,而使用熵則會傾向于生成更平衡的決策樹。
均方誤差(MSE)
當我們通過決策樹來解決回歸類型的問題時,通常會使用MSE來作為節點是否向下分裂的依據,這里已經跟純度這個概念沒啥關系了,所謂純度其樸素的理解就建立在類別上的,某個數據集中,都是同一類數據,那么就是高純度的,但回歸問題上,沒有類別的概念,所以也與純度的概念無關。
首先,我們利用sklearn構建一顆回歸決策樹:
import?numpy?as?np from?sklearn.tree?import?DecisionTreeRegressornp.random.seed(42) m?=?200 #?隨機生成特征數據 X?=?np.random.rand(m,?1) y?=?4?*?(X?-?0.5)?**?2 #?隨機生成對應的目標 y?=?y?+?np.random.randn(m,?1)?/?10#?構建回歸決策樹 tree_reg?=?DecisionTreeRegressor(max_depth=2,?random_state=42) tree_reg.fit(X,?y)然后用老方法,將其可視化顯示出來:
from?graphviz?import?Source from?sklearn.tree?import?export_graphvizexport_graphviz(tree_reg,out_file=os.path.join(IMAGES_PATH,?"reg_tree.dot"),feature_names='X',??#?特征class_names='y',??#?目標rounded=True,??#?圓角filled=True,??#?顏色填充)Source.from_file(os.path.join(IMAGES_PATH,?"reg_tree.dot"))圖5
回歸決策樹長的很像分類決策樹,主要的差別是,回歸決策樹中的節點不再預測某個分類,而是預測一個具體的值,比如你想對一個的實例進行預測,那么從根節點看,最后會來到value=0.111的葉子節點上,在該葉子節點上,均分誤差為0.015。
CART訓練算法
sklearn使用分類和回歸樹(Classification and Regression Tree,CART)算法來訓練決策樹。
對于分類決策樹,CART工作原理為:
先使用單個特征和閾值(如:花瓣長度)將訓練集劃分為兩個子集,如何選擇與呢?
算法會對與進行搜索比較,然后找到可以產生最純子集的一對與,其最小化的成本函數為:
其中:
、測量左右子集的不純度
、策略左右子集的實例數
CART算法通過上述邏輯將訓練集分成兩部分后,會使用相同的邏輯對子集進行分割,然后一直分割下去,直到達到最大深度(由max_depath定義)
對于回歸決策樹,CART算法通過最小化MSE的方式來拆分訓練集,公式如下:
其中:
從原理上理解,很容易發現CART算法是一種貪心算法,它會從最頂層開始搜索最優的分裂方式,然后對子集也進行同樣的處理,多次分裂后,CART算法不會審視自己目前這樣的分裂產出的不純度是否全局最優的。
通常,貪心算法可以獲得一個不錯的解,但不能保證該解是最優解,為了便于理解,我再舉一個例子,假設你要從廣州去上海,你在每個節點上都選擇最短的路,但這樣選擇下來的總路徑可能不是最短,這便是貪心算法面臨的情況。
決策邊界可視化
除了前文中提及的將決策樹本身可視化外,還有另一種常見的可視化方式,那便是將決策樹的決策邊界可視化出來。當然,如果數據量很大,還需要對數據進行采樣后在進行可視化處理。
編寫一個用于可視化決策邊界的函數:
#?To?plot?pretty?figures %matplotlib?inline import?matplotlib?as?mpl import?matplotlib.pyplot?as?plt mpl.rc('axes',?labelsize=14) mpl.rc('xtick',?labelsize=12) mpl.rc('ytick',?labelsize=12)from?matplotlib.colors?import?ListedColormapdef?plot_decision_boundary(clf,?X,?y,?axes=[0,?7.5,?0,?3],?iris=True,?legend=False,?plot_training=True):#?指定間隔內,返回均勻的數字x1s?=?np.linspace(axes[0],?axes[1],?100)x2s?=?np.linspace(axes[2],?axes[3],?100)#?meshgrid函數:用兩個坐標軸上的點在平面上畫網格,其實返回的是矩陣x1,?x2?=?np.meshgrid(x1s,?x2s)#?按行連接兩個矩陣,就是把兩矩陣左右相加,要求行數相等X_new?=?np.c_[x1.ravel(),?x2.ravel()]#?預測y_pred?=?clf.predict(X_new).reshape(x1.shape)custom_cmap?=?ListedColormap(['#fafab0','#9898ff','#a0faa0'])plt.contourf(x1,?x2,?y_pred,?alpha=0.3,?cmap=custom_cmap)if?not?iris:custom_cmap2?=?ListedColormap(['#7d7d58','#4c4c7f','#507d50'])plt.contour(x1,?x2,?y_pred,?cmap=custom_cmap2,?alpha=0.8)if?plot_training:plt.plot(X[:,?0][y==0],?X[:,?1][y==0],?"yo",?label="Iris?setosa")plt.plot(X[:,?0][y==1],?X[:,?1][y==1],?"bs",?label="Iris?versicolor")plt.plot(X[:,?0][y==2],?X[:,?1][y==2],?"g^",?label="Iris?virginica")plt.axis(axes)if?iris:plt.xlabel("Petal?length",?fontsize=14)plt.ylabel("Petal?width",?fontsize=14)else:plt.xlabel(r"$x_1$",?fontsize=18)plt.ylabel(r"$x_2$",?fontsize=18,?rotation=0)if?legend:plt.legend(loc="lower?right",?fontsize=14)基于鳶尾花數據集進行可視化,代碼如下:
plt.figure(figsize=(8,?4)) X?=?iris.data[:,?2:]??#?花瓣的長與寬 y?=?iris.target??#?花的種類 plot_decision_boundary(tree_clf,?X,?y) plt.plot([2.45,?2.45],?[0,?3],?"k-",?linewidth=2) plt.plot([2.45,?7.5],?[1.75,?1.75],?"k--",?linewidth=2) plt.plot([4.95,?4.95],?[0,?1.75],?"k:",?linewidth=2) plt.plot([4.85,?4.85],?[1.75,?3],?"k:",?linewidth=2) plt.text(1.40,?1.0,?"Depth=0",?fontsize=15) plt.text(3.2,?1.80,?"Depth=1",?fontsize=13) plt.text(4.05,?0.5,?"(Depth=2)",?fontsize=11) plt.show()效果:
決策樹的問題
容易過擬合
決策樹的特點是,它極少對訓練數據本身做出假設,對比看線性模型,如果你選擇使用線性模型,其實你就假設了訓練數據是線性變化的,否則線性模型不可能得出好的結果,而決策樹不會有這樣的假設,這個特點容易讓決策樹出現過擬合的問題。
以一個具體的例子來展示決策樹過擬合的情況:
首先,我通過sklearn的make_moons方法生成半環形分布式的數據集,直觀理解如下:
from?sklearn.datasets?import?make_moons?plt.subplot(122)?? x1,y1=make_moons(n_samples=1000,noise=0.1)?? plt.title('make_moons?function?example')?? plt.scatter(x1[:,0],x1[:,1],marker='o',c=y1)?? plt.show()上述代碼效果如下圖:
有了半環形分布的數據后,訓練分類決策樹,代碼如下:
決策樹對半環形分布的數據,其決策邊界如下:
從圖可以看出,決策樹對該數據有明顯的過擬合情況。
解決方法也很簡單,就是使用各種超參數對模型進行正則化調整,即限制模型的擬合能力,從而希望獲得具有更好泛化的模型。sklearn對決策樹提供了max_depth(最大深度)、min_samples_split(分裂前節點必須有的最小樣本數)、min_samples_leaf(葉節點必須要有的最小樣本數),等等超參數用于正則化。
這里我使用max_depth和min_samples_leaf對分類決策樹做了相應的正則化。
from?sklearn.datasets?import?make_moons Xm,?ym?=?make_moons(n_samples=100,?noise=0.25,?random_state=53)#?分類決策樹 tree_clf?=?DecisionTreeClassifier(random_state=42,?max_depth=5,?min_samples_leaf=4) tree_clf.fit(Xm,?ym)#?可視化 plt.figure(figsize=(8,?4)) plot_decision_boundary(tree_clf,?Xm,?ym,?axes=[-1.5,?2.4,?-1,?1.5],?iris=False) plt.title("regularization",?fontsize=16) plt.show()可視化效果如圖:
過擬合的問題不只是在分類決策樹上,在回歸決策樹上也會有,這里可視化的展示一下,讓你有更直觀的了解,如下圖:
上圖中,左半部分,無疑是過擬合的,而右半部分,使用了min_smaples_leaf做正則化的限制,效果還可以。
此外,從右半部分的圖也可以看出,因為回歸決策樹使用MSE來做節點分裂標準,所以決策樹的預測值都是對應區域內實例的目標平均值。
不穩定性
前文中,我們已經展示,決策樹對多種數據類型的處理情況并可視化的展示出其決策邊界,仔細觀察會發現,無論是分類決策樹還是回歸決策樹,其決策邊界都喜歡垂直于X軸或Y軸,這使得他們對訓練集數據的旋轉產生的變化特別敏感,一個具體的例子:
np.random.seed(6) Xs?=?np.random.rand(100,?2)?-?0.5 ys?=?(Xs[:,?0]?>?0).astype(np.float32)?*?2angle?=?np.pi?/?4 rotation_matrix?=?np.array([[np.cos(angle),?-np.sin(angle)],?[np.sin(angle),?np.cos(angle)]]) Xsr?=?Xs.dot(rotation_matrix)tree_clf_s?=?DecisionTreeClassifier(random_state=42) tree_clf_s.fit(Xs,?ys) tree_clf_sr?=?DecisionTreeClassifier(random_state=42) tree_clf_sr.fit(Xsr,?ys)fig,?axes?=?plt.subplots(ncols=2,?figsize=(10,?4),?sharey=True) plt.sca(axes[0]) plot_decision_boundary(tree_clf_s,?Xs,?ys,?axes=[-0.7,?0.7,?-0.7,?0.7],?iris=False) plt.sca(axes[1]) plot_decision_boundary(tree_clf_sr,?Xsr,?ys,?axes=[-0.7,?0.7,?-0.7,?0.7],?iris=False) plt.ylabel("") plt.show()從上圖可以看出,左邊的圖,決策樹使用一條線就將數據做好了分類,但我們將數據旋轉一下,獲得右邊的圖,再使用決策樹去處理,會發現,決策樹需要繪制多條線才能將數據做好分類,即右邊數據訓練出的決策樹模型,可能無法很好的泛化。
更概括的說,決策樹對訓練集中微小的數據變化都非常敏感,一個具體例子,如果我們直接使用鳶尾花數據集進行可視化,效果如圖(前面展示過):
但我們使用相同的數據、類似的代碼,會得到與上圖大為不同的效果:
上述代碼可視化的效果如下:
從上圖可知,即便是相同的訓練數據上,如果random_state不同(sklearn選擇特征集的算法是隨機的,通過random_state參數控制),獲得的決策樹模型也完全不同了。
隨機森林可以通過對多個樹進行平均預測來限制這種不穩定性,關于隨機森林的內容,我們后面的文章會討論。
結尾
本文涉及代碼已提交到:https://github.com/ayuLiao/machine_learning_interstellar_journey 項目中。
我是二兩,下篇文章見。
總結
以上是生活随笔為你收集整理的【机器学习系列】聊聊决策树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 听说学习是件苦差事——Linux第一天
- 下一篇: 程序员的奋斗史(十二)——谈信念