Python機器學習基礎教程-第2章-監督學習之決策樹

前言

本系列教程基本就是摘抄《Python機器學習基礎教程》中的例子內容。html

爲了便於跟蹤和學習,本系列教程在Github上提供了jupyter notebook 版本:node

Github倉庫:https://github.com/Holy-Shine/Introduciton-2-ML-with-Python-notebookpython

系列教程總目錄
Python機器學習基礎教程git

引子

先導入必要的包github

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
import os
%matplotlib inline

決策樹是普遍用於分類和迴歸任務的模型。本質上,它從一層層的 if/else 問題中進行學
習,並得出結論。算法

這些問題相似於你在「20 Questions」遊戲中可能會問的問題。想象一下,你想要區分下面這四種動物:熊、鷹、企鵝和海豚。你的目標是經過提出儘量少的 if/else 問題來獲得正確答案。你可能首先會問:這種動物有沒有羽毛,這個問題會將可能的動物減小到只有兩種。若是答案是「有」,你能夠問下一個問題,幫你區分鷹和企鵝。例如,你能夠問這種動物會不會飛。若是這種動物沒有羽毛,那麼多是海豚或熊,因此你須要問一個問題來區分這兩種動物——好比問這種動物有沒有鰭。dom

這一系列問題能夠表示爲一棵決策樹,如圖 2-22 所示。機器學習

mglearn.plots.plot_animal_tree()
圖 2-22:區分幾種動物的決策樹

在這張圖中,樹的每一個結點表明一個問題或一個包含答案的終結點(也叫葉結點)。樹的邊將問題的答案與將問的下一個問題鏈接起來。函數

用機器學習的語言來講就是,爲了區分四類動物(鷹、企鵝、海豚和熊),咱們利用三個特徵(「有沒有羽毛」「會不會飛」和「有沒有鰭」)來構建一個模型。咱們能夠利用監督學習從數據中學習模型,而無需人爲構建模型。post

1. 構造決策樹

咱們在圖 2-23 所示的二維分類數據集上構造決策樹。這個數據集由 2 個半月形組成,每一個類別都包含 50 個數據點。咱們將這個數據集稱爲 two_moons 。

學習決策樹,就是學習一系列 if/else 問題,使咱們可以以最快的速度獲得正確答案。在機器學習中,這些問題叫做測試(不要與測試集弄混,測試集是用來測試模型泛化性能的數據)。數據一般並非像動物的例子那樣具備二元特徵(是 / 否)的形式,而是表示爲連續特徵,好比圖 2-23 所示的二維數據集。用於連續數據的測試形式是:「特徵 i 的值是否大於 a ?」

圖 2-23:用於構造決策樹的 two_moons 數據集

爲了構造決策樹,算法搜遍全部可能的測試,找出對目標變量來講信息量最大的那一個。圖 2-24 展現了選出的第一個測試。將數據集在 x[1]=0.0596 處垂直劃分能夠獲得最多信息,它在最大程度上將類別 0 中的點與類別 1 中的點進行區分。頂結點(也叫根結點)表示整個數據集,包含屬於類別 0 的 50 個點和屬於類別 1 的 50 個點。經過測試 x[1] <=0.0596 的真假來對數據集進行劃分,在圖中表示爲一條黑線。若是測試結果爲真,那麼將這個點分配給左結點,左結點裏包含屬於類別 0 的 2 個點和屬於類別 1 的 32 個點。不然將這個點分配給右結點,右結點裏包含屬於類別 0 的 48 個點和屬於類別 1 的 18 個點。這兩個結點對應於圖 2-24 中的頂部區域和底部區域。儘管第一次劃分已經對兩個類別作了很好的區分,但底部區域仍包含屬於類別 0 的點,頂部區域也仍包含屬於類別 1 的點。咱們能夠在兩個區域中重複尋找最佳測試的過程,從而構建出更準確的模型。圖 2-25 展現了信息量最大的下一次劃分,此次劃分是基於 x[0] 作出的,分爲左右兩個區域。

圖 2-24:深度爲 1 的樹的決策邊界(左)與相應的樹(右)
圖 2-25:深度爲 2 的樹的決策邊界(左)與相應的樹(右)

這一遞歸過程生成一棵二元決策樹,其中每一個結點都包含一個測試。或者你能夠將每一個測試當作沿着一條軸對當前數據進行劃分。這是一種將算法看做分層劃分的觀點。因爲每一個測試僅關注一個特徵,因此劃分後的區域邊界始終與座標軸平行。

對數據反覆進行遞歸劃分,直到劃分後的每一個區域(決策樹的每一個葉結點)只包含單一目標值(單一類別或單一回歸值)。若是樹中某個葉結點所包含數據點的目標值都相同,那麼這個葉結點就是純的(pure)。這個數據集的最終劃分結果見圖 2-26。

圖 2-26:深度爲 9 的樹的決策邊界(左)與相應的樹的一部分(右);完整的決策樹很是大,很難可視化

想要對新數據點進行預測,首先要查看這個點位於特徵空間劃分的哪一個區域,而後將該區域的多數目標值(若是是純的葉結點,就是單一目標值)做爲預測結果。從根結點開始對樹進行遍歷就能夠找到這一區域,每一步向左仍是向右取決因而否知足相應的測試。

決策樹也能夠用於迴歸任務,使用的方法徹底相同。預測的方法是,基於每一個結點的測試對樹進行遍歷,最終找到新數據點所屬的葉結點。這一數據點的輸出即爲此葉結點中全部訓練點的平均目標值。

2. 控制決策樹的複雜度

一般來講,構造決策樹直到全部葉結點都是純的葉結點,這會致使模型很是複雜,而且對訓練數據高度過擬合。純葉結點的存在說明這棵樹在訓練集上的精度是 100%。訓練集中的每一個數據點都位於分類正確的葉結點中。在圖 2-26 的左圖中能夠看出過擬合。你能夠看到,在全部屬於類別 0 的點中間有一塊屬於類別 1 的區域。另外一方面,有一小條屬於類別 0 的區域,包圍着最右側屬於類別 0 的那個點。這並非人們想象中決策邊界的樣子,這個決策邊界過於關注遠離同類別其餘點的單個異常點。

防止過擬合有兩種常見的策略:一種是及早中止樹的生長,也叫預剪枝(pre-pruning);另外一種是先構造樹,但隨後刪除或摺疊信息量不多的結點,也叫後剪枝(post-pruning)或剪枝(pruning)。預剪枝的限制條件可能包括限制樹的最大深度、限制葉結點的最大數目,或者規定一個結點中數據點的最小數目來防止繼續劃分。

scikit-learn 的決策樹在 DecisionTreeRegressor 類和 DecisionTreeClassifier 類中實現。scikit-learn 只實現了預剪枝,沒有實現後剪枝。

咱們在乳腺癌數據集上更詳細地看一下預剪枝的效果。和前面同樣,咱們導入數據集並將其分爲訓練集和測試集。而後利用默認設置來構建模型,默認將樹徹底展開(樹不斷分支,直到全部葉結點都是純的)。咱們固定樹的 random_state ,用於在內部解決平局問題:

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))

[out]:

Accuracy on training set: 1.000
Accuracy on test set: 0.937

不出所料,訓練集上的精度是 100%,這是由於葉結點都是純的,樹的深度很大,足以完美地記住訓練數據的全部標籤。測試集精度比以前講過的線性模型略低,線性模型的精度約爲 95%。

若是咱們不限制決策樹的深度,它的深度和複雜度均可以變得特別大。所以,未剪枝的樹容易過擬合,對新數據的泛化性能不佳。如今咱們將預剪枝應用在決策樹上,這能夠在完美擬合訓練數據以前阻止樹的展開。一種選擇是在到達必定深度後中止樹的展開。這裏咱們設置 max_depth=4 ,這意味着只能夠連續問 4 個問題(參見圖 2-24 和圖 2-26)。限制樹的深度能夠減小過擬合。這會下降訓練集的精度,但能夠提升測試集的精度:

tree=DecisionTreeClassifier(max_depth=4, random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))

[out]:

Accuracy on training set: 0.988
Accuracy on test set: 0.951

3. 分析決策樹

咱們能夠利用 tree 模塊的 export_graphviz 函數來將樹可視化。這個函數會生成一個 .dot 格式的文件,這是一種用於保存圖形的文本文件格式。咱們設置爲結點添加顏色的選項,顏色表示每一個結點中的多數類別,同時傳入類別名稱和特徵名稱,這樣能夠對樹正確標記:

from sklearn.tree import export_graphviz
export_graphviz(tree, out_file="tree.dot", class_names=["malignant","benign"],
    feature_names=cancer.feature_names, impurity=False, filled=True)

咱們能夠利用 graphviz 模塊讀取這個文件並將其可視化(你也可使用任何可以讀取 .dot文件的程序),見圖 2-27:

import graphviz
with open("tree.dot") as f:
    dot_graph = f.read()
    graphviz.Source(dot_graph)
圖 2-27:基於乳腺癌數據集構造的決策樹的可視化

樹的可視化有助於深刻理解算法是如何進行預測的,也是易於向非專家解釋的機器學習算法的優秀示例。不過,即便這裏樹的深度只有 4 層,也有點太大了。深度更大的樹(深度爲 10 並不罕見)更加難以理解。一種觀察樹的方法可能有用,就是找出大部分數據的實際路徑。圖 2-27 中每一個結點的 samples 給出了該結點中的樣本個數, values 給出的是每一個類別的樣本個數。觀察 worst radius <= 16.795 分支右側的子結點,咱們發現它只包含8 個良性樣本,但有 134 個惡性樣本。樹的這一側的其他分支只是利用一些更精細的區別將這 8 個良性樣本分離出來。在第一次劃分右側的 142 個樣本中,幾乎全部樣本(132 個)最後都進入最右側的葉結點中。

再來看一下根結點的左側子結點,對於 worst radius > 16.795 ,咱們獲得 25 個惡性樣本和 259 個良性樣本。幾乎全部良性樣本最終都進入左數第二個葉結點中,大部分其餘葉結點都只包含不多的樣本。

4. 樹的特徵重要性

查看整個樹可能很是費勁,除此以外,我還能夠利用一些有用的屬性來總結樹的工做原理。其中最經常使用的是特徵重要性(feature importance),它爲每一個特徵對樹的決策的重要性進行排序。對於每一個特徵來講,它都是一個介於 0 和 1 之間的數字,其中 0 表示「根本沒用到」,1 表示「完美預測目標值」。特徵重要性的求和始終爲 1:

print("Feature importances:\n{}".format(tree.feature_importances_))

[out]:

Feature importances:
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.01
0.048 0. 0. 0.002 0. 0. 0. 0. 0. 0.727 0.046

    1. 0.014 0. 0.018 0.122 0.012 0. ]

咱們能夠將特徵重要性可視化,與咱們將線性模型的係數可視化的方法相似(圖 2-28):

def plot_feature_importance_cancer(model):
    n_features = cancer.data.shape[1]
    plt.barh(range(n_features), model.feature_importances_, align='center')
    plt.yticks(np.arange(n_features), cancer.feature_names)
    plt.xlabel("Feature importance")
    plt.ylabel("Feature")

plot_feature_importance_cancer(tree)
圖 2-28:在乳腺癌數據集上學到的決策樹的特徵重要性

這裏咱們看到,頂部劃分用到的特徵(「worst radius」)是最重要的特徵。這也證明了咱們在分析樹時的觀察結論,即第一層劃分已經將兩個類別區分得很好。

可是,若是某個特徵的 feature_importance_ 很小,並不能說明這個特徵沒有提供任何信息。這隻能說明該特徵沒有被樹選中,多是由於另外一個特徵也包含了一樣的信息。

與線性模型的係數不一樣,特徵重要性始終爲正數,也不能說明該特徵對應哪一個類別。特徵重要性告訴咱們「worst radius」(最大半徑)特徵很重要,但並無告訴咱們半徑大表示樣本是良性仍是惡性。事實上,在特徵和類別之間可能沒有這樣簡單的關係,你能夠在下面的例子中看出這一點(圖 2-29 和圖 2-30):

tree = mglearn.plots.plot_tree_not_monotone()
圖 2-29:一個二維數據集( y 軸上的特徵與類別標籤是非單調的關係)與決策樹給出的決策邊界
圖 2-30:從圖 2-29 的數據中學到的決策樹

該圖顯示的是有兩個特徵和兩個類別的數據集。這裏全部信息都包含在 X[1] 中,沒有用到X[0] 。但 X[1] 和輸出類別之間並非單調關係,即咱們不能這麼說:「較大的 X[1] 對應類別 0,較小的 X[1] 對應類別 1」(反之亦然)。

雖然咱們主要討論的是用於分類的決策樹,但對用於迴歸的決策樹來講,全部內容都是相似的,在 DecisionTreeRegressor 中實現。迴歸樹的用法和分析與分類樹很是相似。但在將基於樹的模型用於迴歸時,咱們想要指出它的一個特殊性質。 DecisionTreeRegressor(以及其餘全部基於樹的迴歸模型)不能外推(extrapolate),也不能在訓練數據範圍以外進行預測。

咱們利用計算機內存(RAM)歷史價格的數據集來更詳細地研究這一點。圖 2-31 給出了這個數據集的圖像,x 軸爲日期,y 軸爲那一年 1 兆字節(MB)RAM 的價格:

import pandas as pd
ram_prices = pd.read_csv("data/ram_price.csv")

plt.semilogy(ram_prices.date, ram_prices.price)
plt.xlabel('Year')
plt.ylabel("Pirce in $/Mbyte")
圖 2-31:用對數座標繪製 RAM 價格的歷史發展

注意 y 軸的對數刻度。在用對數座標繪圖時,兩者的線性關係看起來很是好,因此預測應該相對比較容易,除了一些不平滑之處以外。

咱們將利用 2000 年前的歷史數據來預測 2000 年後的價格,只用日期做爲特徵。咱們將對比兩個簡單的模型: DecisionTreeRegressor 和 LinearRegression 。咱們對價格取對數,使得兩者關係的線性相對更好。這對 DecisionTreeRegressor 不會產生什麼影響,但對LinearRegression 的影響卻很大(咱們將在第 4 章中進一步討論)。訓練模型並作出預測以後,咱們應用指數映射來作對數變換的逆運算。爲了便於可視化,咱們這裏對整個數據集進行預測,但若是是爲了定量評估,咱們將只考慮測試數據集:

from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression
# 利用歷史數據預測2000年後的價格
data_train = ram_prices[ram_prices.date < 2000]
data_test = ram_prices[ram_prices.date >= 2000]

# 基於日期來預測價格
X_train = data_train.date[:, np.newaxis]
# 咱們利用對數變換獲得數據和目標之間更簡單的關係
y_train = np.log(data_train.price)

tree = DecisionTreeRegressor().fit(X_train, y_train)
linear_reg = LinearRegression().fit(X_train, y_train)

# 對全部數據進行預測
X_all = ram_prices.date[:, np.newaxis]

pred_tree = tree.predict(X_all)
pred_lr = linear_reg.predict(X_all)

# 對數變換逆運算
price_tree = np.exp(pred_tree)
price_lr = np.exp(pred_lr)

這裏建立的圖 2-32 將決策樹和線性迴歸模型的預測結果與真實值進行對比:

plt.semilogy(data_train.date, data_train.price, label="Training data")
plt.semilogy(data_test.date, data_test.price, label="Test data")
plt.semilogy(ram_prices.date, price_tree, label="Tree prediction")
plt.semilogy(ram_prices.date, price_lr, label="Linear prediction")
plt.legend()
圖 2-32:線性模型和迴歸樹對 RAM 價格數據的預測結果對比

兩個模型之間的差別很是明顯。線性模型用一條直線對數據作近似,這是咱們所知道的。這條線對測試數據(2000 年後的價格)給出了至關好的預測,不過忽略了訓練數據和測試數據中一些更細微的變化。與之相反,樹模型完美預測了訓練數據。因爲咱們沒有限制樹的複雜度,所以它記住了整個數據集。可是,一旦輸入超出了模型訓練數據的範圍,模型就只能持續預測最後一個已知數據點。樹不能在訓練數據的範圍以外生成「新的」響應。全部基於樹的模型都有這個缺點。

5. 優勢、缺點和參數

如前所述,控制決策樹模型複雜度的參數是預剪枝參數,它在樹徹底展開以前中止樹的構造。一般來講,選擇一種預剪枝策略(設置 max_depth 、 max_leaf_nodes 或 min_samples_leaf )足以防止過擬合。

與前面討論過的許多算法相比,決策樹有兩個優勢:一是獲得的模型很容易可視化,非專家也很容易理解(至少對於較小的樹而言)二是算法徹底不受數據縮放的影響。因爲每一個特徵被單獨處理,並且數據的劃分也不依賴於縮放,所以決策樹算法不須要特徵預處理,好比歸一化或標準化。特別是特徵的尺度徹底不同時或者二元特徵和連續特徵同時存在時,決策樹的效果很好。決策樹的主要缺點在於,即便作了預剪枝,它也常常會過擬合,泛化性能不好。所以,在大多數應用中,每每使用下面介紹的集成方法來替代單棵決策樹。

相關文章
相關標籤/搜索