前言
本系列為機器學習算法的總結和歸納,目的為了清晰闡述算法原理,同時附帶上手代碼實例,便於理解。
目錄
組合算法(Ensemble Method)
機器學習算法總結
本章為決策樹算法,內容包括基本模型介紹,以及包括ID3、C4.5和CART樹不同樹模型的介紹。同時包括python3下的代碼實戰,代碼實現包括自我實現和基於sklearn的算法,實戰案例有...(參考《機器學習實戰》)。
一、算法簡介
1.1 基本模型介紹
決策樹是一類常見的機器學習方法,可以幫助我們解決分類與回歸兩類問題。模型可解釋性強,模型符合人類思維方式,是經典的樹形結構。分類決策數模型是一種描述對實例進行分類的樹形結構。決策樹由結點 (node) 和有向邊 (directed edge) 組成。結點包含了一個根結點 (root node)、若干個內部結點 (internal node) 和若干個葉結點 (leaf node)。內部結點表示一個特征或屬性,葉結點表示一個類別。
簡單而言,決策樹是一個多層if-else函數,對對象屬性進行多層if-else判斷,獲取目標屬性的類別。由於只使用if-else對特征屬性進行判斷,所以一般特征屬性為離散值,即使為連續值也會先進行區間離散化,如可以采用二分法(bi-partition)。


思考:選哪些特征屬性參與決策樹建模、哪些屬性排在決策樹的頂部,哪些排在底部,對屬性的值該進行什么樣的判斷、樣本屬性的值缺失怎么辦、如果輸出不是分類而是數值能用么、對決策沒有用或沒有多大幫助的屬性怎么辦、什么時候使用決策樹?
1.2 決策樹特點
· 決策樹優點
1)決策樹易於理解和實現,人們在在學習過程中不需要使用者了解很多的背景知識,這同時是它的能夠直接體現數據的特點,只要通過解釋后都有能力去理解決策樹所表達的意義。
2)對於決策樹,數據的准備往往是簡單或者是不必要的,而且能夠同時處理數據型和常規型屬性,在相對短的時間內能夠對大型數據源做出可行且效果良好的結果。
3)易於通過靜態測試來對模型進行評測,可以測定模型可信度;如果給定一個觀察的模型,那么根據所產生的決策樹很容易推出相應的邏輯表達式。
· 決策樹缺點
1)對連續性的字段比較難預測。
2)對有時間順序的數據,需要很多預處理的工作。
3)當類別太多時,錯誤可能就會增加的比較快。
4)一般的算法分類的時候,只是根據一個字段來分類。
二、算法分類和流程
2.1 算法分類
現有的關於決策樹學習的主要思想主要包含以下 3 個研究成果:
由 Quinlan 在 1986 年提出的 ID3 算法
由 Quinlan 在 1993 年提出的 C4.5 算法
由 Breiman 等人在 1984 年提出的 CART 算法
算法比較


2.2 算法流程- 划分選擇
決策樹學習通常包括 3 個步驟:特征選擇、決策樹的生成、決策樹的修剪。最為關鍵的就是如何選擇最優划分屬性。一般而言,隨着划分過程不斷進行,我們希望決策樹的分支結點所包含的樣本盡可能屬於同一類別,即結點的 “純度” (purity) 越來越高。
2.2.1 信息增益(information gain)
信息增益表示得知特征Xj的信息而使所屬分類的不確定性減少的程度。

特征A對訓練數據集D的信息增益g(D,A),定義為集合D的經驗熵H(D)與特征A給定的情況下D的經驗條件熵H(D|A)之差。
假設數據集D有K種分類,特征A有n種取值可能。其中數據集D的經驗熵H(D)為

其中P
k為集合D中的任一樣本數據分類k的概率,或者說屬於分類k的樣本所占的比例。
經驗條件熵H(D|A)為

也可記作



其中P
i為特征取值為第i個可取值的概率。D
i為特征A為第i個可取值的樣本集合。
一般而言,信息增益越大,則意味着使用屬性A來進行划分所獲得的 "純度提升" 越大。因此,我們可以用信息增益來進行決策樹的划分屬性選擇。著名的
ID3 決策樹學習算法就是以信息增益為准則來選擇划分屬性。
2.2.2 信息增益比(information gain ratio)
特征A對訓練數據集D的信息增益gR(D,A),定義為其信息增益g(D,A)與訓練集D的經驗熵H(D)之比。


是為了矯正在訓練數據集的經驗熵大時,信息增益值會偏大,反之,信息增益值會偏小的問題。即特征選擇使用了一個啟發式策略:先從候選划分屬性中找出信息增益高於平均水平的屬性,再從中選擇增益率最高的。
C4.5算法是使用該方式來划分屬性。
2.2.3 基尼指數(Gini index)
ID3還是C4.5都是基於信息論的熵模型的,這里面會涉及大量的對數運算,為了簡化模型同時也完全丟失熵模型的優點,在CART算法中使用基尼系數來代替信息增益比,基尼系數代表了模型的不純度,基尼系數越小,則不純度越低,特征越好。這和信息增益(比)是相反的。通過子集計算基尼不純度,即隨機放置的數據項出現於錯誤分類中的概率。以此來評判屬性對分類的重要程度。


其中p
k為任一樣本點屬於第k類的概率,也可以說成樣本數據集中屬於k類的樣本的比例。
集合D的基尼指數為Gini(D),在特征A條件下的集合D的基尼指數為


其中 |D
i|為特征A取第i個值時對應的樣本個數。|D|為總樣本個數
CART算法中對於分類樹采用的是上述的基尼指數最小化准則。對於回歸樹,CART采用的是平方誤差最小化准則。
2.3 剪枝
剪枝 (pruning) 是決策樹學習算法對付 “過擬合” 的主要手段。在決策樹學習中,為了盡可能正確分類訓練樣本,結點划分過程將不斷重復,有時會造成決策樹分支過多,這時就可能因針對訓練樣本學得 “太好” 了,以至於把訓練集自身的一些特點當作所有數據都具有的一般性質而導致過擬合。因此,可通過主動去掉一些分支來降低過擬合的風險。
決策樹剪枝的基本策略有 “預剪枝” (prepruning) 和 “后剪枝” (postpruning)。
· 預剪枝 是指在決策樹生成過程中,對每個結點在划分前先進行估計,若當前節點的划分不能帶來決策樹泛化性能的提升,則停止划分並將當前結點標記為葉結點;
· 后剪枝 則是先從訓練集生成一顆完整的決策樹,然后自底向上地對非葉結點進行考察,若將該結點的子樹替換為葉結點能帶來決策樹泛化性能的提升,則將該子樹替換為葉結點。
四、案例
本篇文章將在基本概念基礎上,以實際案例熟悉決策樹構建、可視化和分類預測等。
決策樹算法訓練流程主要包括:收集數據- 准備數據- 分析數據- 訓練算法- 測試算法
4.1 基於python3的代碼實現
基於ID3算法,實現預測貸款用戶是否具有償還貸款的能力
1)創建數據集

""" 函數說明:創建測試數據集 Parameters: 無 Returns: dataSet - 數據集 labels - 特征標簽 """ def createDataSet(): dataSet = [[0, 0, 0, 0, 'no'], #數據集 [0, 0, 0, 1, 'no'], [0, 1, 0, 1, 'yes'], [0, 1, 1, 0, 'yes'], [0, 0, 0, 0, 'no'], [1, 0, 0, 0, 'no'], [1, 0, 0, 1, 'no'], [1, 1, 1, 1, 'yes'], [1, 0, 1, 2, 'yes'], [1, 0, 1, 2, 'yes'], [2, 0, 1, 2, 'yes'], [2, 0, 1, 1, 'yes'], [2, 1, 0, 1, 'yes'], [2, 1, 0, 2, 'yes'], [2, 0, 0, 0, 'no']] labels = ['年齡', '有工作', '有自己的房子', '信貸情況'] #特征標簽 return dataSet, labels #返回數據集和分類屬性
2)划分數據集

""" 函數說明:按照給定特征划分數據集 Parameters: dataSet - 待划分的數據集 axis - 划分數據集的特征 value - 需要返回的特征的值 Returns: 無 """ def splitDataSet(dataSet, axis, value): retDataSet = [] #創建返回的數據集列表 for featVec in dataSet: #遍歷數據集 if featVec[axis] == value: reducedFeatVec = featVec[:axis] #去掉axis特征 reducedFeatVec.extend(featVec[axis+1:]) #將符合條件的添加到返回的數據集 retDataSet.append(reducedFeatVec) return retDataSet #返回划分后的數據集
3)計算香儂熵

""" 函數說明:計算給定數據集的經驗熵(香農熵) Parameters: dataSet - 數據集 Returns: shannonEnt - 經驗熵(香農熵) """ def calcShannonEnt(dataSet): numEntires = len(dataSet) #返回數據集的行數 labelCounts = {} #保存每個標簽(Label)出現次數的字典 for featVec in dataSet: #對每組特征向量進行統計 currentLabel = featVec[-1] #提取標簽(Label)信息 if currentLabel not in labelCounts.keys(): #如果標簽(Label)沒有放入統計次數的字典,添加進去 labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 #Label計數 shannonEnt = 0.0 #經驗熵(香農熵) for key in labelCounts: #計算香農熵 prob = float(labelCounts[key]) / numEntires #選擇該標簽(Label)的概率 shannonEnt -= prob * log(prob, 2) #利用公式計算 return shannonEnt #返回經驗熵(香農熵)
4)選擇最優特征

""" 函數說明:選擇最優特征 Parameters: dataSet - 數據集 Returns: bestFeature - 信息增益最大的(最優)特征的索引值 """ def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 #特征數量 baseEntropy = calcShannonEnt(dataSet) #計算數據集的香農熵 bestInfoGain = 0.0 #信息增益 bestFeature = -1 #最優特征的索引值 for i in range(numFeatures): #遍歷所有特征 #獲取dataSet的第i個所有特征 featList = [example[i] for example in dataSet] uniqueVals = set(featList) #創建set集合{},元素不可重復 newEntropy = 0.0 #經驗條件熵 for value in uniqueVals: #計算信息增益 subDataSet = splitDataSet(dataSet, i, value) #subDataSet划分后的子集 prob = len(subDataSet) / float(len(dataSet)) #計算子集的概率 newEntropy += prob * calcShannonEnt(subDataSet) #根據公式計算經驗條件熵 infoGain = baseEntropy - newEntropy #信息增益 # print("第%d個特征的增益為%.3f" % (i, infoGain)) #打印每個特征的信息增益 if (infoGain > bestInfoGain): #計算信息增益 bestInfoGain = infoGain #更新信息增益,找到最大的信息增益 bestFeature = i #記錄信息增益最大的特征的索引值 return bestFeature
5)統計類標簽

""" 函數說明:統計classList中出現此處最多的元素(類標簽) Parameters: classList - 類標簽列表 Returns: sortedClassCount[0][0] - 出現此處最多的元素(類標簽) """ def majorityCnt(classList): classCount = {} for vote in classList: #統計classList中每個元素出現的次數 if vote not in classCount.keys():classCount[vote] = 0 classCount[vote] += 1 sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True) #根據字典的值降序排序 return sortedClassCount[0][0] #返回classList中出現次數最多的元素 """
6)創建決策樹

""" 函數說明:創建決策樹 Parameters: dataSet - 訓練數據集 labels - 分類屬性標簽 featLabels - 存儲選擇的最優特征標簽 Returns: myTree - 決策樹 """ def createTree(dataSet, labels, featLabels): classList = [example[-1] for example in dataSet] #取分類標簽(是否放貸:yes or no) if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續划分 return classList[0] if len(dataSet[0]) == 1: #遍歷完所有特征時返回出現次數最多的類標簽 return majorityCnt(classList) bestFeat = chooseBestFeatureToSplit(dataSet) #選擇最優特征 bestFeatLabel = labels[bestFeat] #最優特征的標簽 featLabels.append(bestFeatLabel) myTree = {bestFeatLabel:{}} #根據最優特征的標簽生成樹 del(labels[bestFeat]) #刪除已經使用特征標簽 featValues = [example[bestFeat] for example in dataSet] #得到訓練集中所有最優特征的屬性值 uniqueVals = set(featValues) #去掉重復的屬性值 for value in uniqueVals: #遍歷特征,創建決策樹。 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels) return myTree
遞歸創建決策樹時,遞歸有兩個終止條件:第一個停止條件是所有的類標簽完全相同,則直接返回該類標簽;第二個停止條件是使用完了所有特征,仍然不能將數據划分僅包含唯一類別的分組,即決策樹構建失敗,特征不夠用。此時說明數據緯度不夠,由於第二個停止條件無法簡單地返回唯一的類標簽,這里挑選出現數量最多的類別作為返回值。
7)樹的可視化

""" 函數說明:獲取決策樹葉子結點的數目 Parameters: myTree - 決策樹 Returns: numLeafs - 決策樹的葉子結點的數目 """ def getNumLeafs(myTree): numLeafs = 0 #初始化葉子 firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結點屬性,可以使用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 """ 函數說明:獲取決策樹的層數 Parameters: myTree - 決策樹 Returns: maxDepth - 決策樹的層數 """ def getTreeDepth(myTree): maxDepth = 0 #初始化決策樹深度 firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結點屬性,可以使用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 """ 函數說明:繪制結點 Parameters: nodeTxt - 結點名 centerPt - 文本位置 parentPt - 標注的箭頭位置 nodeType - 結點格式 Returns: 無 """ def plotNode(nodeTxt, centerPt, parentPt, nodeType): arrow_args = dict(arrowstyle="<-") #定義箭頭格式 font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14) #設置中文字體 createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', #繪制結點 xytext=centerPt, textcoords='axes fraction', va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font) """ 函數說明:標注有向邊屬性值 Parameters: cntrPt、parentPt - 用於計算標注位置 txtString - 標注的內容 Returns: 無 """ 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) """ 函數說明:繪制決策樹 Parameters: myTree - 決策樹(字典) parentPt - 標注的內容 nodeTxt - 結點名 Returns: 無 """ def plotTree(myTree, parentPt, nodeTxt): decisionNode = dict(boxstyle="sawtooth", fc="0.8") #設置結點格式 leafNode = dict(boxstyle="round4", fc="0.8") #設置葉結點格式 numLeafs = getNumLeafs(myTree) #獲取決策樹葉結點數目,決定了樹的寬度 depth = getTreeDepth(myTree) #獲取決策樹層數 firstStr = next(iter(myTree)) #下個字典 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)) #不是葉結點,遞歸調用繼續繪制 else: #如果是葉結點,繪制葉結點,並標注有向邊屬性值 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 """ 函數說明:創建繪制面板 Parameters: inTree - 決策樹(字典) Returns: 無 """ def createPlot(inTree): fig = plt.figure(1, facecolor='white') #創建fig fig.clf() #清空fig axprops = dict(xticks=[], yticks=[]) createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #去掉x、y軸 plotTree.totalW = float(getNumLeafs(inTree)) #獲取決策樹葉結點數目 plotTree.totalD = float(getTreeDepth(inTree)) #獲取決策樹層數 plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; #x偏移 plotTree(inTree, (0.5,1.0), '') #繪制決策樹 plt.show() #顯示繪制結果
8)執行決策樹
依靠訓練數據構造好決策樹之后可以用於實際數據分類。

""" 函數說明:使用決策樹分類 Parameters: inputTree - 已經生成的決策樹 featLabels - 存儲選擇的最優特征標簽 testVec - 測試數據列表,順序對應最優特征標簽 Returns: classLabel - 分類結果 """ def classify(inputTree, featLabels, testVec): firstStr = next(iter(inputTree)) #獲取決策樹結點 secondDict = inputTree[firstStr] #下一個字典 featIndex = featLabels.index(firstStr) for key in secondDict.keys(): if testVec[featIndex] == key: if type(secondDict[key]).__name__ == 'dict': classLabel = classify(secondDict[key], featLabels, testVec) else: classLabel = secondDict[key] return classLabel
9)決策樹存儲
可以調用python模塊中的pickle序列化對象,這樣能夠在每次執行時調用已經構造好的決策樹

""" 函數說明:存儲決策樹 Parameters: inputTree - 已經生成的決策樹 filename - 決策樹的存儲文件名 Returns: 無 """ def storeTree(inputTree, filename): with open(filename, 'wb') as fw: pickle.dump(inputTree, fw)
4.2 基於sklearn的代碼實現
同樣,python的sklearn庫也提供了決策樹的模型-DecisionTreeClassifier,可以直接調用,使用方便。具體介紹參見
官方文檔
class sklearn.tree.DecisionTreeClassifier(criterion=’gini’, splitter=’best’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, class_weight=None, presort=False)
參數介紹:
· criterion: 特征選擇標准,默認值為‘gini’,即CART算法。(entropy, gini)
· splitter: 特征划分標准,默認值為‘best’。(best, random) best在特征的所有划分點中找出最優的划分點,random隨機的在部分划分點中找局部最優的划分點。默認的‘best’適合樣本量不大的時候,而如果樣本數據量非常大,此時決策樹構建推薦‘random’。
· max_depth: 決策樹最大深度。默認值是‘None’。(int, None)常用的可以取值10-100之間,常用來解決過擬合。
· min_samples_split: 內部節點再划分所需最小樣本數。默認值為2。(int, float)
· min_samples_leaf: 葉子節點最少樣本數。
· min_weight_fraction_leaf: 葉子節點最小的樣本權重和。默認為0。(float)
· max_features: 在划分數據集時考慮的最多的特征值數量。
· random_state: 默認是None(int, randomSate instance, None)
· max_leaf_nodes: 最大葉子節點數。默認為None。(int, None)通過設置最大葉子節點數,可以防止過擬合。
· min_impurity_decrease: 節點划分最小不純度。默認值為‘0’。(float,)
· min_impurity_split: 信息增益的閥值。
· class_weight: 類別權重。默認為None,(dict, list of dicts, balanced)
· presort: bool,默認是False,表示在進行擬合之前,是否預分數據來加快樹的構建。
實例:項目采用用決策樹預測隱形眼鏡類型,數據集下載地址:
https://github.com/Jack-Cherish/Machine-Learning/blob/master/Decision%20Tree/classifierStorage.txt
模型構建之后可以使用Graphviz可視化樹,pydotplus和Grphviz。確定好決策樹后可以進行預測。項目代碼如下:

# -*- coding: UTF-8 -*- from sklearn.preprocessing import LabelEncoder, OneHotEncoder from sklearn.externals.six import StringIO from sklearn import tree import pandas as pd import numpy as np import pydotplus if __name__ == '__main__': with open('lenses.txt', 'r') as fr: #加載文件 lenses = [inst.strip().split('\t') for inst in fr.readlines()] #處理文件 lenses_target = [] #提取每組數據的類別,保存在列表里 for each in lenses: lenses_target.append(each[-1]) print(lenses_target) lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] #特征標簽 lenses_list = [] #保存lenses數據的臨時列表 lenses_dict = {} #保存lenses數據的字典,用於生成pandas for each_label in lensesLabels: #提取信息,生成字典 for each in lenses: lenses_list.append(each[lensesLabels.index(each_label)]) lenses_dict[each_label] = lenses_list lenses_list = [] # print(lenses_dict) #打印字典信息 lenses_pd = pd.DataFrame(lenses_dict) #生成pandas.DataFrame # print(lenses_pd) #打印pandas.DataFrame le = LabelEncoder() #創建LabelEncoder()對象,用於序列化 for col in lenses_pd.columns: #序列化 lenses_pd[col] = le.fit_transform(lenses_pd[col]) # print(lenses_pd) #打印編碼信息 clf = tree.DecisionTreeClassifier(max_depth = 4) #創建DecisionTreeClassifier()類 clf = clf.fit(lenses_pd.values.tolist(), lenses_target) #使用數據,構建決策樹 dot_data = StringIO() tree.export_graphviz(clf, out_file = dot_data, #繪制決策樹 feature_names = lenses_pd.keys(), class_names = clf.classes_, filled=True, rounded=True, special_characters=True) graph = pydotplus.graph_from_dot_data(dot_data.getvalue()) graph.write_pdf("tree.pdf") #保存繪制好的決策樹,以PDF的形式存儲。
參考: