Python機器學習算法之二元決策樹

二元決策樹就是基於屬性做一系列的二元(是/否)決策。每次決策對應於從兩種可能性中選擇一個。每次決策後,要麼引出另外一個決策,要麼生成最終的結果。一個實際訓練決策樹的例子有助於加強對這個概念的理解。瞭解了訓練後的決策樹是什麼樣的,就學會了決策樹的訓練過程。

代碼清單6-1爲使用Scikitlearn的DecisionTreeRegressor工具包針對紅酒口感數據構建二元決策樹的代碼。圖6-1爲代碼清單6-1生成的決策樹。

代碼清單6-1 構建一個決策樹預測紅酒口感-winTree.py

__author__ = 'mike-bowles'

import urllib2
import numpy
from sklearn import tree
from sklearn.tree import DecisionTreeRegressor
from sklearn.externals.six import StringIO
from math import sqrt
import matplotlib.pyplot as plot

#read data into iterable
target_url = ("http://archive.ics.uci.edu/ml/machine-learning-"
"databases/wine-quality/winequality-red.csv")
data = urllib2.urlopen(target_url)

xList = []
labels = []
names = []
firstLine = True
for line in data:
    if firstLine:
        names = line.strip().split(";")
        firstLine = False
    else:
        #split on semi-colon
        row = line.strip().split(";")
        #put labels in separate array
        labels.append(float(row[-1]))
        #remove label from row
        row.pop()
        #convert row to floats
        floatRow = [float(num) for num in row]
        xList.append(floatRow)

nrows = len(xList)
ncols = len(xList[0])

wineTree = DecisionTreeRegressor(max_depth=3)

wineTree.fit(xList, labels)

with open("wineTree.dot", 'w') as f:
    f = tree.export_graphviz(wineTree, out_file=f)
#Note: The code above exports the trained tree info to a
#Graphviz "dot" file.
#Drawing the graph requires installing GraphViz and the running the
#following on the command line
#dot -Tpng wineTree.dot -o wineTree.png
# In Windows, you can also open the .dot file in the GraphViz
#gui (GVedit.exe)]

圖6-1爲針對紅酒數據的訓練結果,即一系列的決策。決策樹框圖顯示了一系列的方框,這些方框稱作節點(nodes)。有兩類節點,一種針對問題輸出“是”或者“否”,另外一種是終止節點,輸出針對樣本的預測結果,並終止整個決策的過程。終止節點也叫作葉子節點(leaf)。在圖6-1中,終止節點處在框圖底部,它們下面沒有分支或者進一步的決策節點。

圖片 13{}

圖6-1 確定紅酒口感的決策樹

1.1 如何利用二元決策樹進行預測

當一個觀察(或一行數據)被傳送到一個非終止節點時,此行數據要回答此節點的問題。如果回答“是”,則該行數據進入節點下面的左側節點。如果回答”否“,則此行數據進入節點下面的右側節點。該過程持續進行,直到到達一個終止節點(即葉子節點),葉子節點給該行數據分配預測值。葉子節點分配的預測值是所有到達此節點的訓練數據結果的均值。

儘管此決策樹的第二個決策層在兩個分支中都考慮了變量X[9],這兩個決策也可以是針對不同屬性所做的判斷(可以參看第三個決策層的例子)。

最上面的節點又叫根節點(root node)。這個節點提出的問題是“X[10]<=10.525”。在二元決策樹中,越是重要的變量越早用來分割數據(越接近決策樹的頂端),因此決策樹認爲變量X[10],也就是酒精含量屬性很重要。這點決策樹與第5章的懲罰線性迴歸是一致的。第5章“用懲罰線性方法構建預測模型”也認爲酒精含量是決定紅酒口感最重要的屬性。

圖6-1所示決策樹的深度爲3。決策樹的深度定義爲從上到下遍歷樹的最長路徑(所經過的決策的數目)。在“決策樹的訓練等價於分割點的選擇”小節的關於訓練的討論中,可以看到沒有理由要求到達終止節點的所有路徑具有相同的長度(見圖6-1)。

現在已經知道一個訓練好的決策樹是什麼樣的,也看到了如何使用一個決策樹來進行預測。下面介紹如何訓練決策樹。

1.2 如何訓練一個二元決策樹

瞭解如何訓練決策樹最簡單的方法就是通過一個具體的例子。代碼清單6-2爲給定一個實數屬性如何預測一個實數標籤的例子。數據集在代碼中產生(也叫作合成數據)。生成過程是把−0.5~+0.5等分成100份,單一實數屬性x就是這些等分數。標籤y等於x加上隨機噪聲。

代碼清單6-2 簡單迴歸問題的決策樹訓練-simpleTree.py

__author__ = 'mike-bowles'

import numpy
import matplotlib.pyplot as plot
from sklearn import tree
from sklearn.tree import DecisionTreeRegressor
from sklearn.externals.six import StringIO

#Build a simple data set with y = x + random
nPoints = 100

#x values for plotting
xPlot = [(float(i)/float(nPoints) - 0.5) for i in range(nPoints + 1)]

#x needs to be list of lists.
x = [[s] for s in xPlot]

#y (labels) has random noise added to x-value
#set seed
numpy.random.seed(1)
y = [s + numpy.random.normal(scale=0.1) for s in xPlot]

plot.plot(xPlot,y)
plot.axis('tight')
plot.xlabel('x')
plot.ylabel('y')
plot.show()

simpleTree = DecisionTreeRegressor(max_depth=1)
simpleTree.fit(x, y)

#draw the tree
with open("simpleTree.dot", 'w') as f:
    f = tree.export_graphviz(simpleTree, out_file=f)

#compare prediction from tree with true values

yHat = simpleTree.predict(x)

plot.figure()
plot.plot(xPlot, y, label='True y')
plot.plot(xPlot, yHat, label='Tree Prediction ', linestyle='--')
plot.legend(bbox_to_anchor=(1,0.2))
plot.axis('tight')
plot.xlabel('x')
plot.ylabel('y')
plot.show()

simpleTree2 = DecisionTreeRegressor(max_depth=2)
simpleTree2.fit(x, y)

#draw the tree
with open("simpleTree2.dot", 'w') as f:
    f = tree.export_graphviz(simpleTree2, out_file=f)

#compare prediction from tree with true values

yHat = simpleTree2.predict(x)

plot.figure()
plot.plot(xPlot, y, label='True y')
plot.plot(xPlot, yHat, label='Tree Prediction ', linestyle='--')
plot.legend(bbox_to_anchor=(1,0.2))
plot.axis('tight')
plot.xlabel('x')
plot.ylabel('y')
plot.show()

#split point calculations - try every possible split point to
#find the best one
sse = []
xMin = []
for i in range(1, len(xPlot)):
    #divide list into points on left and right of split point
    lhList = list(xPlot[0:i])
    rhList = list(xPlot[i:len(xPlot)])

    #calculate averages on each side
    lhAvg = sum(lhList) / len(lhList)
    rhAvg = sum(rhList) / len(rhList)

    #calculate sum square error on left, right and total
    lhSse = sum([(s - lhAvg) * (s - lhAvg) for s in lhList])
    rhSse = sum([(s - rhAvg) * (s - rhAvg) for s in rhList])

    #add sum of left and right to list of errors

    sse.append(lhSse + rhSse)
    xMin.append(max(lhList))

plot.plot(range(1, len(xPlot)), sse)
plot.xlabel('Split Point Index')
plot.ylabel('Sum Squared Error')
plot.show()

minSse = min(sse)
idxMin = sse.index(minSse)
print(xMin[idxMin])

#what happens if the depth is really high?
simpleTree6 = DecisionTreeRegressor(max_depth=6)
simpleTree6.fit(x, y)

#too many nodes to draw the tree
#with open("simpleTree2.dot", 'w') as f:
# f = tree.export_graphviz(simpleTree6, out_file=f)

#compare prediction from tree with true values

yHat = simpleTree6.predict(x)

plot.figure()
plot.plot(xPlot, y, label='True y')
plot.plot(xPlot, yHat, label='Tree Prediction ', linestyle='–')
plot.legend(bbox_to_anchor=(1,0.2))
plot.axis('tight')
plot.xlabel('x')
plot.ylabel('y')
plot.show()

圖6-2爲屬性x和標籤y的關係圖。正如預期,y值大致上一直跟隨x值變化,但是有些隨機的小擾動。

圖片 14{67%}

圖6-2 標籤與屬性的關係圖

1.3 決策樹的訓練等同於分割點的選擇

代碼清單6-2的第一步是運行scikitlearn的regression tree包,並指定決策樹的深度爲1。此處理過程的結果如圖6-3所示。圖6-3爲深度爲1的決策樹的框圖。深度爲1的樹又叫作樁(stumps)。在根節點的決策就是將屬性值與−0.075比較。這個值叫作分割點(split point),因爲它把數據分割成兩部分。由根節點發散出去的兩個方框可知,101個實例中有43個到了根節點的左分支,剩下的58個實例到了根節點的右分支。如果屬性值小於分割點,則此決策樹的預測值就是方框裏指明的值,大約就是−0.302。

圖片 15{61%}

圖6-3 一個簡單問題的解:深度爲1的決策樹的框圖

分割點的選擇如何影響預測效果

審視決策樹的另一個方法就是將預測值與真實的標籤值進行對比。這個簡單的合成數據只有一個屬性,由決策樹產生的預測值一直跟隨着實際的標籤值,從中也能看出這個簡單的決策樹的訓練是如何完成的。如圖6-4所示,預測的值是基於一個簡單的判斷方法。預測值實際上是屬性值的階梯函數。這個“階梯”就發生在分割點。

圖片 16{67%}

圖6-4 預測值與實際值的比較

分割點選擇算法

這個簡單的決策樹需要確定3個變量:分割點的值、分割後生成的兩組數據的預測值。決策樹的訓練過程就是要完成這個任務。下面介紹上述目標如何達到。訓練此決策樹的目標是使預測值的誤差平方最小。首先假設分割點已經確認。一旦給定分割點,分配給兩個組的預測值就可以確定下來。分配的值就是使均方誤差最小的那個值。那麼剩下的問題就是如何確定分割點的值。代碼清單6-2有一小段代碼用來確定分割點。這個過程是嘗試每一個可能的分割點,然後把數據分成2組,取每組數值的均值作爲分配的預測值,然後計算相應的誤差平方和。

圖6-5展示了誤差平方和作爲分割點的函數是如何變化的。大概在數據的中心,可以明確地取到最小的誤差平方。訓練一個決策樹需要窮盡地搜索所有可能的分割點來確定哪個值可以使誤差平方和最小化。這是這個簡單的例子需要注意的地方。

圖片 17{67%}

圖6-5 每個可能的分割點對應的誤差平方和

多變量決策樹的訓練-選擇哪個屬性進行分割

如果問題含有多個屬性該怎麼辦?算法會對所有的屬性檢查所有可能的分割點,對每個屬性找到誤差平方和最小的分割點,然後找到哪個屬性對應的誤差平方和最小。

在訓練決策樹的過程中,每個計算週期都要對分割點進行計算。同樣地,訓練基於決策樹的集成算法時,每個週期也要對分割點進行計算。如果屬性沒有重複值,每個數據點對應的屬性值都要作爲分割點進行測試(則分割點的測試次數等於數據點數目減1)。

隨着數據規模的增大,分割點的計算量也成比例增加。測試的分割點彼此可能非常近。因此設計針對大規模數據的算法時,分割點的檢測通常要比原始數據的粒度粗糙得多。論文“PLANET:Massively Parallel Learning of Tree Ensembles with MapReduce” 提出一種方法,是谷歌工程師針對大規模數據集構建決策樹時採用的方法,他們使用決策樹來實現梯度提升(gradient boosting) 算法(本章將會學到該集成方法)。

通過遞歸分割獲得更深的決策樹

代碼清單6-2展示了當決策樹深度從1增加到2時,預測曲線會發生什麼變化。預測曲線如圖6-6所示。決策樹的框圖如圖6-7所示。深度爲1的決策樹只有一步,這個預測曲線有3步。第2決策層分割點的確定與第1個分割點的方法完全一樣。決策樹的每個節點處理基於上個分割點生成的數據子集。每個節點中分割點的選擇是使下面2個節點的誤差平方和最小。圖6-6的曲線非常接近一個實際的階梯函數曲線。決策樹深度的增加意味着更細小的步長、更高的保真度(準確性)。但是如果這個過程無限地繼續下去會怎樣?

圖片 18{67%}

圖6-6 深度爲2的決策樹的預測曲線

圖片 19{514}

圖6-7 深度爲2的決策樹的框圖

隨着分割的繼續,決策樹深度增加,最深節點包含的數據(實例數)會減少。這將導致在達到特定的深度之前,這種分割就終止了。如果決策樹的節點只有一個數據實例,就不需要分割了。決策樹訓練算法通常有一個參數來控制節點包含的數據實例最小到什麼規模就不再分割。節點包含的數據實例太少會導致預測結果發生劇烈震盪。

1.4 二元決策樹的過擬合

上節介紹瞭如何訓練任意深度的二元決策樹。那麼有沒有可能過擬合一個二元決策樹?本節介紹如何度量和控制二元決策樹的過擬合。二元決策樹的過擬合原因與第4章和第5章的有所不同。但是過擬合的表現以及如何度量過擬合過程還是比較相似的。二元決策樹的參數(樹的深度、最小葉節點規模等等)可以用來控制模型的複雜度,類似過程已經在第4章和第5章看到。

二元決策樹過擬合的度量

圖6-8展示了決策樹的深度增加到6會發生什麼。在圖6-8中,很難看出真實值與預測值之間的差別。預測值幾乎完全跟隨每一個觀察值的變化。這就開始暗示此模型已經過擬合了。數據產生方式表明最佳預測就是讓預測值等於對應的屬性值。添加到屬性上的噪音是不可預測的,然而過擬合的預測結果是實際值加上噪聲產生的偏差。合成數據的好處就是可以事先知道正確答案。

另外一個檢查過擬合的方法是比較決策樹中終止節點的數目與數據的規模。生成圖6-8所示的預測曲線的決策樹的深度是6。這意味着它有64個終止節點(2^6)。數據集中共有100個數據點。這意味着大量的數據單獨佔據一個終止節點,因此它們的預測值與觀察值完全匹配。這就不奇怪預測曲線完全跟隨着噪聲的“扭動”。

圖片 20{67%}

圖6-8 深度爲6的決策樹的預測曲線

權衡二元決策樹複雜度以獲得最佳性能

在實際問題中,使用交叉驗證(cross-validation)來控制過擬合。代碼清單6-3展示了針對此問題使用不同深度的決策樹運行10折交叉驗證。代碼顯示了2層循環,外層循環定義了內層交叉驗證的決策樹深度,內層循環將數據分割爲訓練數據和測試數據後計算10輪測試誤差。不同深度的決策樹對應的均方誤差(MSE, mean squared error)如圖6-9所示。

代碼清單6-3 不同深度決策樹的交叉驗證-simpleTreeCV.py

__author__ = 'mike-bowles'

import numpy
import matplotlib.pyplot as plot
from sklearn import tree
from sklearn.tree import DecisionTreeRegressor
from sklearn.externals.six import StringIO

#Build a simple data set with y = x + random
nPoints = 100

#x values for plotting
xPlot = [(float(i)/float(nPoints) - 0.5) for i in range(nPoints + 1)]

#x needs to be list of lists.
x = [[s] for s in xPlot]

#y (labels) has random noise added to x-value
#set seed
numpy.random.seed(1)
y = [s + numpy.random.normal(scale=0.1) for s in xPlot]

nrow = len(x)

#fit trees with several different values for depth and use
#x-validation to see which works best.

depthList = [1, 2, 3, 4, 5, 6, 7]
xvalMSE = []
nxval = 10

for iDepth in depthList:

    #build cross-validation loop to fit tree and evaluate on
    #out of sample data
    for ixval in range(nxval):

        #Define test and training index sets
        idxTest = [a for a in range(nrow) if a%nxval == ixval%nxval]
        idxTrain = [a for a in range(nrow) if a%nxval != ixval%nxval]

        #Define test and training attribute and label sets
        xTrain = [x[r] for r in idxTrain]
        xTest = [x[r] for r in idxTest]
        yTrain = [y[r] for r in idxTrain]
        yTest = [y[r] for r in idxTest]

        #train tree of appropriate depth and accumulate
        #out of sample (oos) errors
        treeModel = DecisionTreeRegressor(max_depth=iDepth)
        treeModel.fit(xTrain, yTrain)

        treePrediction = treeModel.predict(xTest)
        error = [yTest[r] - treePrediction[r] \
            for r in range(len(yTest))]

        #accumulate squared errors
        if ixval == 0:
            oosErrors = sum([e * e for e in error])
        else:
        #accumulate predictions
        oosErrors += sum([e * e for e in error])

    #average the squared errors and accumulate by tree depth

    mse = oosErrors/nrow
    xvalMSE.append(mse)

plot.plot(depthList, xvalMSE)
plot.axis('tight')
plot.xlabel('Tree Depth')
plot.ylabel('Mean Squared Error')
plot.show()

圖片 21{69%}

圖6-9 簡單問題的測試數據均方誤差與決策樹深度的關係

決策樹的深度控制二元決策樹模型的複雜度。它的效果類似於第4章和第5章中懲罰迴歸模型的懲罰係數項。決策樹深度的增加意味着在付出額外的複雜度的基礎上,可以從數據中提取出更復雜的行爲。圖6-9說明決策樹深度爲3時,可以獲得基於代碼清單6-2生成的數據的最佳均方誤差(MSE)。此決策樹深度體現了重現屬性與標籤的內在關係和過擬合風險之間的最佳權衡。

回顧第3章,最佳模型的複雜度是數據集規模的函數。合成數據問題提供了觀察這個關係是如何起作用的機會。當數據點增加到1 000時,最佳模型複雜度和性能發生的變化如圖6-10所示。

圖片 22{69%}

圖6-10 1 000個數據點時,測試數據均方誤差與決策樹深度關係

可以修改代碼清單6-3中的變量nPoints爲1000,然後運行代碼。增加數據時,會發生兩件事情:第一件事是最佳決策樹深度會從3增加到4。增加的數據支持更復雜的模型。另外一件事是均方誤差有輕微的下降。增加的決策樹深度允許在逼近真實模型時提供更精細的“臺階”,面向真實的大規模數據場景也可以提供更好的保真度。

1.5 針對分類問題和類別特徵所做的修改

爲了提供關於決策樹是如何訓練的完整場景,還有一些細節問題需要討論。一個問題就是:如何應用決策樹解決分類問題?上述判斷分割點的均方誤差只對迴歸問題有意義。正如你在本書其他部分看到的,分類問題與迴歸問題有不同的評價標準。分類問題在判斷分割點時可以使用多個評價標準來代替均方誤差。一個是很熟悉的misclassification error(誤分類錯誤)。另外兩個比較通用的是基尼不純性度量(Gini impurity measure)和信息增益(information gain)。詳細內容可以參考http://en.wikipedia.org/wiki/Decision_tree_learning#Gini_impurity。這兩個度量指標與誤分類錯誤有一些不同特性,但在概念上沒有差別。

最後一個部分是當屬性是類別屬性而非數值屬性時,如何訓練決策樹。決策樹中的非終止節點提出一個yes/no的問題。對應數值屬性,問題是判斷屬性是否小於某一值的這種形式。把一個類別屬性(變量)分割成兩個子集需要嘗試所有分成2個子集的可能性。假設一個類別屬性包含A、B、C三類,可能的分割方式是:A在一個子集,B、C在另外一個子集,或者B在一個子集,A、C在另外一個子集,諸如此類。在某些環境下,可以直接使用相關數學結果簡化這個過程。

本節了提供二元決策樹的背景知識,二元決策樹本身就是一個很好的預測工具,值得深入研究。但是這裏提出的目的是將其作爲集成方法的背景。集成方法包含了大量的二元決策樹。在當成千上萬個決策樹組合到一起時,使用單個決策樹時出現的問題(如需要調整多個參數、結果的不穩定性、決策樹深度加深導致的過擬合等)就會減弱。這也是提出集成方法的原因,集成方法更加魯棒、易於訓練、更加準確。下面討論三個主流的集成方法。


本文摘自《Python機器學習:預測分析核心算法》,作者: 【美】Michael Bowles(鮑爾斯),譯者: 沙嬴 , 李鵬 ,由人民郵電出版社異步社區出版,定價:69,320頁。點擊【閱讀原文】可實現購買。

圖片描述

作者簡介

圖片描述

Michael Bowles擁有機械工程學士和碩士學位、儀器儀表博士學位以及MBA學位。他的履歷涉及學術界、工業界以及商業界。他目前在一家初創公司工作,其中機器學習技術至關重要。他是多個管理團隊的成員、諮詢師以及顧問。他也曾經在加州山景城的黑客道場、創業公司孵化器和辦公場所教授機器學習課程。

他出生於俄克拉荷馬州並在那裏獲得學士和碩士學位。在東南亞待了一段時間後,他前往劍橋攻讀博士學位,畢業後任職於MIT的Charles Stark Draper實驗室。之後他離開波士頓前往南加州的休斯飛機公司開發通信衛星。在UCLA獲得MBA學位後,他前往舊金山的灣區工作。作爲創始人以及CEO,他目前經營兩家公司,這兩家公司都已獲風險投資。

內容提要

本書遵循了着手解決一個預測問題的基本流程。開始階段包括對數據的理解、如何形式化表示問題,然後開始嘗試使用算法解決問題,評估其性能。

在這個過程中,本書將概要描述每一步採用的方法及其原因。第1章給出本書涵蓋的問題和所用方法的完整描述,本書使用來自UC Irvine數據倉庫的數據集作爲例子;第2章展示了一些數據分析的方法和工具,幫助讀者對新數據集具有一定的洞察力。第3章“預測模型的構建:平衡性能、複雜性以及大數據”主要介紹由上述三者帶給預測分析技術的困難以及所採用的技術,勾勒了問題複雜度、模型複雜度、數據規模和預測性能之間的關係,討論了過擬合問題以及如何可靠地感知到過擬合,以及不同類型問題下的性能評價標準。第4章、第5章分別介紹懲罰線性迴歸的背景及其應用,即如何解決第2章所述的問題。第6章、第7章分別介紹集成方法的背景及其應用。

面向的讀者羣

本書主要面向想提高機器學習技能的Python開發人員,不管是針對某一特定的項目,還是隻想提升相關技能。開發人員很可能在工作中遇到新問題需要使用機器學習的方法來解決。當今機器學習的應用領域如此之廣,使其已成爲簡歷中一項十分有用的技能。

本書爲Python開發人員提供如下內容:

  • 機器學習所解決的基本問題的描述;

  • 當前幾種最先進的算法;

  • 這些算法的應用原則;
  • 一個機器學習系統的選型、設計和評估的流程;
  • 流程、算法的示例;
  • 可進一步修改擴展的代碼。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章