機器學習實戰筆記--決策樹
本文為《機器學習實戰》學習筆記
1. 決策樹簡介
決策樹可以從資料集合彙總提取一系列的規則,建立規則的過程就是機器學習的過程。在構造決策樹的過程中,不斷選取特徵劃分資料集,直到具有相同型別的資料均在資料子集內。
1.1 劃分資料集
由於不同屬性的資料型別不同,其對應的測試條件也不同。即非葉子節點的每條出邊代表的含義不同。
二元屬性產生兩個可能的輸出。
標稱屬性具有多個屬性值。可以根據屬性值的數量產生多路劃分,每個出邊代表一個屬性值;對於只產生二元劃分的演算法(CART),可以建立k個屬性值二元劃分的所有
序數屬性在保證序數屬性值有序性的同時,可以產生二元或多路劃分。
連續屬性
劃分資料集的最大原則是把無序資料變得有序。可以使用資訊理論量化度量資訊的內容。
如果待分類的事務可能劃分在多個分類中,則符號
其中
集合資訊的度量方式有多種,用於度量資料集的不純性,值越大表示資料集越混亂,混合的資料越多。
其中夏農熵(熵)定義為資訊的期望值。計算熵時,需要計算所有類別所有可能值包含的資訊期望值:
除了熵以外,資料集不純性度量方式還有基尼係數:
分類誤差:
資訊增益是劃分資料集前後資訊發生的變化:
其中,I是給定節點集合的不純性度量,k為屬性的個數,
獲得資訊增益最高的特徵是最好的選擇。
熵和基尼指標等不純性度量有利於具有大量不同值的屬性,如果使用ID進行劃分時,每個樣本在該屬性上的值都是唯一的,得到結果的純度更高,但沒有意義。
解決該問題的策略有兩種:
1)限制測試條件只能是二元劃分,如CART決策樹演算法;
2)修改評估劃分的標準。把屬性測試條件產生的輸出數也考慮進去。如C4.5採用增益率評估劃分。
其中劃分資訊
1.2 構建決策樹
在決策樹的構建過程中,使用貪心的策略和遞迴的方法。每次在所有的屬性中選擇能夠最好的劃分資料集的屬性,將資料集劃分為不同的子集,判斷子集是否為葉子節點,如果是確定葉子節點的類標號並結束遞迴,否則對子集遞迴呼叫該方法繼續劃分。
決策樹過大容易受過擬合的影響,可以通過剪枝減小樹的規模,提高樹的泛化能力。
1.3 決策樹特點
1)非引數方法,不需要先驗假設;
2)NP完全問題,經常採用啟發式方法搜尋假設空間;
3)可快速建立模型,對未知樣本分類快,時間複雜度為樹的深度;
4)對噪聲干擾魯棒性高;
5)冗餘屬性不會對準確率造成不利影響。不相關屬性可能使決策樹過於龐大,因此特徵選擇技術有助於提高決策樹的準確率;
6)子樹可能重複多次,因為採用分治演算法,對不同的資料子集可能採用相同的屬性劃分;
7)涉及單個屬性的測試條件不一定能很好地劃分資料集,可以組合多個屬性劃分
8)不純性度量對決策樹演算法的影響很小,因為度量方法相互一致。
優點:
計算複雜度不高。輸出結果易於理解,對缺失值不敏感,可以處理不相關的特徵資料。
缺點:
可能會產生過擬合問題。
1.4 過擬合
模型訓練誤差小,檢驗誤差大時產生過擬合;模型訓練誤差和檢驗誤差都大產生欠擬合。
噪聲、代表性樣本缺失、包含大量的候選屬性和少量訓練記錄的多重比較都會導致過擬合。
通常通過控制模型的複雜度解決過擬合問題。理想的複雜度是產生最低泛化誤差的模型的複雜度。
1.4.1 泛化誤差估計
1)結合模型複雜度估計
訓練誤差對泛化誤差的估計過於樂觀。通常會結合模型的複雜度估計。奧卡姆剃刀原則認為:兩個具有相同泛華誤差的模型,較簡單的模型比較複雜的模型更可取。
2)計算訓練誤差的上界估計
因為泛化誤差傾向於大於訓練誤差,因此可以計算訓練誤差的上界估計泛化誤差。
3)使用確認集估計
將原始的訓練集分為訓練集和確認集兩個子集,2/3作為訓練集用於訓練模型,1/3作為確認集用於估計泛化誤差。先通過調整模型的引數使得模型在確認集上達到最低錯誤率,在將模型用於位置樣本的訓練。
1.4.2 決策樹過擬合處理
1)先剪枝
構建決策樹時,設定限制條件,當資訊增益或者估計的泛化誤差的改進低於某個確定閾值時,停止擴充套件葉子節點。
該方法避免產生過擬合的複雜子樹,但閾值選取困難,太高導致欠擬合,太低導致過擬合。
2)後剪枝
初始決策樹按照最大規模生長,然後自底向上修剪決策樹。
可以用葉子節點替換子樹。葉子節點的類別為子樹中出現次數最多的類別。也可以用子樹中出現最多的分支代替子樹。
後剪枝的結果更好,但剪枝後浪費了生長完全決策樹的額外計算。
2.2 劃分資料集
針對每個特徵,計算以該特徵劃分資料集前後的熵,得到資訊增益,並選擇資訊增益最高的特徵進行劃分。
定義資料集的夏農熵計算方法:
#定義夏農熵的計算方法
from math import log
def calcShannonEnt(dataSet):
numEntries = len(dataSet) #計算資料集中例項總數
labelCounts = {} #建立資料字典,鍵值為標籤
#將所有可能分類加入字典
for featVec in dataSet: #遍歷資料集中的每一項,featVec可用任意字元表示,區域性變數而已
currentLabel = featVec[-1] #標籤為最後一列
#更新不同標籤對應的例項數
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
#計算資訊熵
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob * log(prob, 2)
return shannonEnt
定義劃分資料集的方法,得到指定特徵為特定值的資料集:
#按照給定特徵劃分資料集
def splitDataSet(dataSet, axis, value): #按照axis對應的特徵值為value對資料進行劃分
retDataSet = [] #由於python函式傳引用,在函式內對列表物件的修改會影響該物件的整個生命週期,
#為了不修改原始資料集,宣告一個新列表物件
for featVec in dataSet:
if featVec[axis] == value: #將符合條件的項儲存並返回,去掉axis列
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:]) #擴充套件得到兩個列表所有元素
retDataSet.append(reducedFeatVec) #為結果資料集新增新的列表
return retDataSet
遍歷資料集,迴圈計算夏農熵和劃分資料集函式,找到最好的特徵劃分方式;
#選擇最好的資料集劃分方式,返回最好劃分方式對應的特徵
def chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0]) - 1 #計算特徵個數
baseEntropy = calcShannonEnt(dataSet) #計算原始資料集的資訊熵
#初始化最佳資訊增益和對應特徵所在的列
bestInfoGain = 0.0
bestFeature = -1
#對所有特徵計算劃分後的資訊增益,選擇最好的特徵
for i in range(numFeature):
featList = [example[i] for example in dataSet] #特徵i的取值列表,使用列表推導實現
uniqueVals = set(featList) #特徵i的取值型別
newEntropy = 0.0
for value in uniqueVals: #統計每種型別的劃分結果並計算資訊熵
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy #得到當前特徵的資訊增益
if infoGain > bestInfoGain: #更新最佳劃分
bestInfoGain = infoGain
bestFeature = i
return bestFeature
2.3 構建決策樹
構建決策樹時,使用遞迴的思想,對於給定資料集,選擇最好的特徵將資料集劃分為若干子集,對於每個子集,可遞迴呼叫該方法再次劃分。遞迴結束的條件為程式遍歷完所有的屬性或者每個分支下所有的例項都有相同的分類。如果所有例項具有相同的分類,即得到葉子節點。
如果程式已經遍歷完所有的屬性,但位於該節點的例項的類別標籤不統一,則使用多數表決的方式決定該葉子節點的類別。
#多數表決
#當處理完所有的屬性後,類標籤仍不唯一,通過多數表決定義葉子節點
def majorityCnt(classList):
classCount = {} #資料字典儲存每個類標籤出現的頻率
for vote in classCount:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
#按照標籤出現的頻率降序排序
sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0] #返回出現次數最多的標籤
構建決策樹時,輸入資料集和包含資料集所有特徵的標籤
#遞迴建立決策樹
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet] #建立包含資料集所有類別的列表變數
if classList.count(classList[0]) == len(classList): #類別完全相同,得到葉子節點,返回類標籤
return classList[0]
if len(dataSet[0]) == 1: #處理完所有屬性後類標籤不唯一,使用多數表決決定類標籤
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #選擇最好的劃分屬性
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}} #使用字典儲存樹結構
del(labels[bestFeat]) #刪除已選擇的最優特徵,更新特徵標籤列表
featValues = [example[bestFeat] for example in dataSet] #得到特徵列所有值的列表
uniqueVals = set(featValues)
for value in uniqueVals: #對每種型別的值,遞迴構建決策樹
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
2.4 使用Matplotlib繪製樹形圖
使用註解繪製節點:
import matplotlib.pyplot as plt
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei'] #動態設定新增中文黑體
mpl.rcParams['axes.unicode_minus'] = False #更改字型導致顯示不出負號,所以設定為true,保證負號的顯示
#定義文字框和箭頭格式
decisionNode = dict(boxstyle = "sawtooth", fc = "0.8")
leafNode = dict(boxstyle = "round4", fc = "0.3")
arrow_args = dict(arrowstyle = "<-")
#繪製帶箭頭的註解
def plotNode(nodeTxt, centerPt, parentPt, nodeType): #文字,center座標,父節點座標,節點型別
createPlot.ax1.annotate(nodeTxt, xy = parentPt, xycoords = 'axes fraction',\
xytext = centerPt, textcoords = 'axes fraction', \
va = "center", ha = "center", bbox = nodeType, arrowprops = arrow_args)
matplotlib.pyplot.annotate(*args, **kwargs)使用文字s註釋節點xy。s為註釋內容,字串型別;xy為長度為2的序列,標明瞭點(x, y),可迭代型別;xytext長度為2的序列,標明文字放置的位置,可迭代型別,可選,預設為xy;xycoords指xy給定的座標系統,可從給定的字串中選擇;textcoords指xytext給定的座標系統;arrowprops,設定xy和xytext間的箭頭屬性。
由於構造註解樹時,需要每個節點的座標,需要根據葉子節點的數量確定x軸的長度,根據樹的高度確定y軸的長度。
#需要通過葉子節點和樹的深度確定x,y軸的長度
#得到葉子節點的數量
def getNumLeafs(myTree):
numLeafs = 0
firstStr = list(myTree.keys())[0] #取字典中的第一個key值
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]) is dict: #子樹型別為字典,遞迴呼叫直至葉子節點
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
#得到樹的深度
def getTreeDepth(myTree):
maxDepth = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]) is dict:
thisDepth = 1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth:
maxDepth = thisDepth
return maxDepth
可以預先儲存樹資訊,在繪圖時直接呼叫,避免每次都要從資料中建立樹。
def retrieveTree(i):
listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}]
return listOfTrees[i]
在繪製樹時,使用全域性變數儲存座標軸的長度,高度和節點在x軸y軸的偏移量,然後遞迴地繪製樹,包括繪製中間節點,葉子節點和父子節點間的文字,並不斷更新節點在x軸和y軸的偏移量。
在父子節點之間填充文字資訊時,輸入為當前節點的座標,父節點的座標和文字內容,根據父子節點的座標取中點求出文字的座標,然後在對應座標處新增文字。
#在父子節點之間填充文字資訊
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0] - cntrPt[0]) / 2.0 + cntrPt[0]
yMid = (parentPt[1] - cntrPt[1]) / 2.0 + cntrPt[1]
createPlot.ax1.text(xMid,yMid, txtString)
matplotlib.pyplot.text(x, y, s, fontdict=None, withdash=False, **kwargs)將文字s新增到座標(x, y)處。
在繪製樹時,首先計算當前樹的葉子節點數和深度,然後根據已繪製節點的偏移量和樹的寬和高計算當前樹根節點的座標,根節點的橫座標位於所有葉子節點的中間,繪製根節點,更新其子節點的y偏移量並處理每個分支節點,如果分支節點為子樹,遞迴呼叫plotTree繪製子樹,否則直接繪製葉子節點,並更新x軸的偏移量。
def plotTree(myTree, parentPt, nodeTxt):
#計算樹的寬和高
numLeafs = getNumLeafs(myTree)
depth = getNumLeafs(myTree)
firstStr = list(myTree.keys())[0]
#變數plotTree.xOff,plotTree.yOff追蹤已繪製節點位置
#變數plotTree.totalW, plotTree.totalD儲存樹的總寬度和總深度
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 /plotTree.totalW, plotTree.yOff) #根節點的橫座標位於對應葉子節點的中間
plotMidText(cntrPt, parentPt, nodeTxt)
plotNode(firstStr, cntrPt, parentPt, decisionNode) #繪製根節點
secondDict = myTree[firstStr]
plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD
#處理每個分支
for key in secondDict.keys():
if type(secondDict[key]) is dict: #遞迴呼叫畫子樹
plotTree(secondDict[key], cntrPt, str(key))
else: #畫葉子節點
plotTree.xOff = plotTree.xOff + 1.0 /