我的機器學習之旅(六):決策樹
決策樹概念:
分類決策樹模型是一種描述對實例進行分類的樹形結構。決策樹由結點和有向邊組成。結點有兩種類型:內部節點和葉節點,內部節點表示一個特征或屬性,葉節點表示一個類。
分類的時候,從根節點開始,對實例的某一個特征進行測試,根據測試結果,將實例分配到其子結點;此時,每一個子結點對應著該特征的一個取值。如此遞歸向下移動,直至達到葉結點,最後將實例分配到葉結點的類中。
例如判斷某款物品的潛在買家:
決策樹可以看成一個if-then規則的集合:由決策樹的根結點到葉結點的每一條路徑構建一條規則;路徑上的內部結點的特征對應著規則的條件,而葉結點對應著分類的結論。決策樹的路徑和其對應的if-then規則集合是等效的,它們具有一個重要的性質:互斥並且完備。這裏的意思是說:每一個實例都被一條路徑或一條規則所覆蓋,而且只被一條規則所覆蓋。
幾個基本概念
特征選擇問題希望選取對訓練數據具有良好分類能力的特征,這樣可以提高決策樹學習的效率。如果利用一個特征進行分類的結果與隨機分類的結果沒有很大差別,則稱這個特征是沒有分類能力的(對象是否喜歡打遊戲應該不會成為關鍵特征吧,也許也會……)。為了解決特征選擇問題,找出最優特征,先要介紹一些信息論裏面的概念。
1.信息熵(香農熵)
熵是表示隨機變量不確定性的度量。設X是一個取有限個值的離散隨機變量,其概率分布為:
隨機變量的熵為:
熵越大,隨機變量的不確定性就越大。
數據集熵值計算函數:
from math import log
def calcShannonEnt(dataSet):"""dataSet 為前n-1列為特征,最後一列為類別的數據集 """ numEntries = len(dataSet) labelCounts = {} for featVec in dataSet: # 遍歷每個實例,統計標簽的頻數 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) # 以2為底的對數 return shannonEnt
2.條件熵
設有隨機變量(X,Y),其聯合概率分布為
條件熵H(Y|X)表示在已知隨機變量XX的條件下隨機變量Y的不確定性。隨機變量X給定的條件下隨機變量Y的條件熵H(Y|X),定義為X給定條件下Y的條件概率分布的熵對X的數學期望:
這裏,pi=P(X=xi),i=1,2,?,n.pi=P(X=xi),i=1,2,?,n.
3.信息熵
信息增益表示得知特征XX的信息而使得類Y的信息的不確定性減少的程度。特征A對訓練數據集D的信息增益g(D,A),定義為集合D的經驗熵H(D)與特征A給定條件下D的經驗條件熵H(D|A)之差,即
這個差又稱為互信息。信息增益大的特征具有更強的分類能力。
根據信息增益準則的特征選擇方法是:對訓練數據集(或子集)計算其每個特征的信息增益,選擇信息增益最大的特征。
計算信息增益的算法如下:
通過劃分數據集,並計算信息增益
def splitDataSet(dataSet, axis, value): ‘‘‘ 按照給定特征劃分數據集 :param dataSet:待劃分的數據集 :param axis:劃分數據集的特征 :param value: 需要返回的特征的值 :return: 劃分結果列表 ‘‘‘ retDataSet = [] for featVec in dataSet: if featVec[axis] == value: reducedFeatVec = featVec[:axis] #chop out axis used for splitting reducedFeatVec.extend(featVec[axis+1:]) retDataSet.append(reducedFeatVec) return retDataSet def calcConditionalEntropy(dataSet, i, featList, uniqueVals): ‘‘‘ 計算X_i給定的條件下,Y的條件熵 :param dataSet:數據集 :param i:維度i :param featList: 數據集特征列表 :param uniqueVals: 數據集特征集合 :return: 條件熵 ‘‘‘ conditionEnt = 0.0 for value in uniqueVals: subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet) / float(len(dataSet)) # 極大似然估計概率 conditionEnt += prob * calcShannonEnt(subDataSet) # 條件熵的計算 return conditionEnt def calcInformationGain(dataSet, baseEntropy, i): ‘‘‘ 計算信息增益 :param dataSet:數據集 :param baseEntropy:數據集的信息熵 :param i: 特征維度i :return: 特征i對數據集的信息增益g(D|X_i) ‘‘‘ featList = [example[i] for example in dataSet] # 第i維特征列表 uniqueVals = set(featList) # 轉換成集合 newEntropy = calcConditionalEntropy(dataSet, i, featList, uniqueVals) infoGain = baseEntropy - newEntropy # 信息增益,就yes熵的減少,也就yes不確定性的減少 return infoGain
兩種算法
1,ID3算法
ID3算法由Ross Quinlan發明,建立在“奧卡姆剃刀”的基礎上:越是小型的決策樹越優於大的決策樹(be simple簡單理論)。ID3算法中根據信息增益評估和選擇特征,每次選擇信息增益最大的特征作為判斷模塊建立子結點。ID3算法可用於劃分標稱型數據集,沒有剪枝的過程,為了去除過度數據匹配的問題,可通過裁剪合並相鄰的無法產生大量信息增益的葉子節點(例如設置信息增益閥值)。使用信息增益的話其實是有一個缺點,那就是它偏向於具有大量值的屬性。就是說在訓練集中,某個屬性所取的不同值的個數越多,那麽越有可能拿它來作為分裂屬性,而這樣做有時候是沒有意義的,另外ID3不能處理連續分布的數據特征,於是就有了C4.5算法。CART算法也支持連續分布的數據特征。
步驟:
ID3算法思想描述:
a.對當前例子集合,計算屬性的信息增益;
b.選擇信息增益最大的屬性Ai(關於信息增益後面會有詳細敘述)
c.把在Ai處取值相同的例子歸於同於子集,Ai取幾個值就得幾個子集
d.對依次對每種取值情況下的子集,遞歸調用建樹算法,即返回a,
e.若子集只含有單個屬性,則分支為葉子節點,判斷其屬性值並標上相應的符號,然後返回調用處。
尋找最優劃分方式代碼
def chooseBestFeatureToSplitByID3(dataSet): ‘‘‘ 選擇最好的數據集劃分方式 :param dataSet:數據集 :return: 劃分結果 ‘‘‘ numFeatures = len(dataSet[0]) - 1 # 最後一列yes分類標簽,不屬於特征向量 baseEntropy = calcShannonEnt(dataSet) bestInfoGain = 0.0 bestFeature = -1 for i in range(numFeatures): # 遍歷所有特征 infoGain = calcInformationGain(dataSet, baseEntropy, i) # 計算信息增益 if (infoGain > bestInfoGain): # 選擇最大的信息增益 bestInfoGain = infoGain bestFeature = i return bestFeature # 返回最優特征對應的維度
2.C4.5算法
C4.5算法是用於生成決策樹的一種經典算法,是ID3算法的一種延伸和優化。C4.5算法對ID3算法主要做了一下幾點改進:
??(1)通過信息增益率選擇分裂屬性,克服了ID3算法中通過信息增益傾向於選擇擁有多個屬性值的屬性作為分裂屬性的不足;
??(2)能夠處理離散型和連續型的屬性類型,即將連續型的屬性進行離散化處理;
??(3)構造決策樹之後進行剪枝操作;
??(4)能夠處理具有缺失屬性值的訓練數據。
- 優點:
(1)通過信息增益率選擇分裂屬性,克服了ID3算法中通過信息增益傾向於選擇擁有多個屬性值的屬性作為分裂屬性的不足;
(2)能夠處理離散型和連續型的屬性類型,即將連續型的屬性進行離散化處理;
(3)構造決策樹之後進行剪枝操作;
(4)能夠處理具有缺失屬性值的訓練數據。 - 缺點:
(1)算法的計算效率較低,特別是針對含有連續屬性值的訓練樣本時表現的尤為突出。
(2)算法在選擇分裂屬性時沒有考慮到條件屬性間的相關性,只計算數據集中每一個條件屬性與決策屬性之間的期望信息,有可能影響到屬性選擇的正確性。
步驟:
代碼:
創建樹
def majorityCnt(classList): ‘‘‘ 采用多數表決的方法決定葉結點的分類 :param: 所有的類標簽列表 :return: 出現次數最多的類 ‘‘‘ classCount={} for vote in classList: # 統計所有類標簽的頻數 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): ‘‘‘ 創建決策樹 :param: dataSet:訓練數據集 :return: 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 = chooseBestFeatureToSplitByID3(dataSet) # 最優劃分特征 bestFeat = chooseBestFeatureToSplitByC45(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
測試分類
構造了決策樹之後,我們就可以將它用於實際數據的分類,在執行分類時,需要輸入決策樹和用於構造樹的所有類標簽向量。然後,程序比較測試數據與決策樹上的數值,遞歸執行該過程直到進入葉結點;最後將測試數據定義為葉結點所屬的類型。
def classify(inputTree,featLabels,testVec): ‘‘‘ 利用決策樹進行分類 :param: inputTree:構造好的決策樹模型 :param: featLabels:所有的類標簽 :param: testVec:測試數據 :return: 分類決策結果 ‘‘‘ firstStr = inputTree.keys()[0] secondDict = inputTree[firstStr] featIndex = featLabels.index(firstStr) key = testVec[featIndex] valueOfFeat = secondDict[key] if isinstance(valueOfFeat, dict): classLabel = classify(valueOfFeat, featLabels, testVec) else: classLabel = valueOfFeat return classLabel
可視化樹
import matplotlib.pyplot as plt import tree # 定義文本框和箭頭格式 decisionNode = dict(boxstyle="round4", color=‘#3366FF‘) # 定義判斷結點形態 leafNode = dict(boxstyle="circle", color=‘#FF6633‘) # 定義葉結點形態 arrow_args = dict(arrowstyle="<-", color=‘g‘) # 定義箭頭 #計算葉結點數 def getNumLeafs(myTree): numLeafs = 0 firstStr = list(myTree.keys())[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__==‘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]).__name__==‘dict‘:# 測試結點的數據類型是否為字典 thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth # 繪制帶箭頭的註釋 def plotNode(nodeTxt, centerPt, parentPt, nodeType): createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords=‘axes fraction‘, xytext=centerPt, textcoords=‘axes fraction‘, va="center", ha="center", bbox=nodeType, arrowprops=arrow_args ) # 在父子結點間填充文本信息 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, va="center", ha="center", rotation=30) def plotTree(myTree, parentPt, nodeTxt): numLeafs = getNumLeafs(myTree) # 計算寬與高 depth = getTreeDepth(myTree) firstStr = list(myTree.keys())[0] 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 # 減少y偏移 for key in secondDict.keys(): if type(secondDict[key]).__name__==‘dict‘: plotTree(secondDict[key],cntrPt,str(key)) #recursion else: #it‘s a leaf node print the leaf node plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode) plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key)) plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD #if you do get a dictonary you know it‘s a tree, and the first element will be another dict def createPlot(inTree): fig = plt.figure(1, facecolor=‘white‘) fig.clf() axprops = dict(xticks=[], yticks=[]) createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) plotTree.totalW = float(getNumLeafs(inTree)) plotTree.totalD = float(getTreeDepth(inTree)) plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; plotTree(inTree, (0.5,1.0), ‘‘) plt.show()
兩個測試用例
1.
myDat = [[‘youth‘, ‘no‘, ‘no‘, ‘1‘, ‘refuse‘], [‘youth‘, ‘no‘, ‘no‘, ‘2‘, ‘refuse‘], [‘youth‘, ‘yes‘, ‘no‘, ‘2‘, ‘agree‘], [‘youth‘, ‘yes‘, ‘yes‘, ‘1‘, ‘agree‘], [‘youth‘, ‘no‘, ‘no‘, ‘1‘, ‘refuse‘], [‘mid‘, ‘no‘, ‘no‘, ‘1‘, ‘refuse‘], [‘mid‘, ‘no‘, ‘no‘, ‘2‘, ‘refuse‘], [‘mid‘, ‘yes‘, ‘yes‘, ‘2‘, ‘agree‘], [‘mid‘, ‘no‘, ‘yes‘, ‘3‘, ‘agree‘], [‘mid‘, ‘no‘, ‘yes‘, ‘3‘, ‘agree‘], [‘elder‘, ‘no‘, ‘yes‘, ‘3‘, ‘agree‘], [‘elder‘, ‘no‘, ‘yes‘, ‘2‘, ‘agree‘], [‘elder‘, ‘yes‘, ‘no‘, ‘2‘, ‘agree‘], [‘elder‘, ‘yes‘, ‘no‘, ‘3‘, ‘agree‘], [‘elder‘, ‘no‘, ‘no‘, ‘1‘, ‘refuse‘], ] labels = [‘age‘, ‘working?‘, ‘house?‘, ‘credit_situation‘] myTree = createTree(myDat, labels) createPlot(myTree) labels = [‘age‘, ‘working?‘, ‘house?‘, ‘credit_situation‘] print(classify(myTree,labels ,[‘youth‘,‘no‘,‘no‘,1]))
###結果為refuse
2.網上數據
import pandas as pd import numpy as np name_c=[‘sample code number‘,‘clump thickness‘, ‘uniformity of cell size‘, ‘uniformity of cell shape‘,‘marginal adhesion‘,‘single epithelial cell‘, ‘bare nuclei‘,‘bland chromatin‘,‘normal nucleoli‘,‘mitoses‘,‘class‘ ] data=pd.read_csv(‘http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data‘,names=name_c) data=data.replace(to_replace=‘?‘,value=np.nan) data=data.dropna(how=‘any‘) myDat=data[name_c[1:]].values[:].tolist() myTree = createTree(myDat, name_c[1:]) createPlot(myTree) print( classify(myTree,name_c[1:],[5,3,2,1,3,‘1‘,1,1,1]))
###結果為2
我的機器學習之旅(六):決策樹