系列文章:《機器學習實戰》學習筆記html
決策樹git
咱們常用決策樹處理分類問題,它的過程相似二十個問題的遊戲:參與遊戲的一方在腦海裏想某個事物,其餘參與者向他提出問題,只容許提20個問題,問題的答案也只能用對或錯回答。問問題的人經過推斷分解,逐步縮小帶猜想事物的範圍。如圖1所示的流程圖就是一個決策樹,長方形表明判斷模塊(decision block),橢圓形表明終止模塊(terminating block),表示已經得出結論,能夠終止運行。從判斷模塊引出的左右箭頭稱做分支(branch),它能夠到達另外一個判斷模塊或終止模塊。github
圖1構造了一個假象的郵件分類系統,它首先檢測發送郵件域名地址。若是地址爲myEmployer.com,則將其放在分類"無聊時須要閱讀的郵件"中。若是郵件不是來自這個域名,則檢查內容是否包括單詞曲棍球,若是包含則將郵件歸類到"須要及時處理的朋友郵件",不然將郵件歸類到"無須閱讀的垃圾郵件"。算法
圖1 流程圖形式的決策樹數據庫
第2章介紹的k-近鄰算法能夠完成不少分類任務,可是它最大的缺點就是沒法給出數據的內在含義,決策樹的主要優點就在於數據形式很是容易理解。數據結構
本章構造的決策樹算法可以讀取數據集合,構建相似圖1的決策樹。決策樹能夠在數據集合中提取出一系列規則,規則建立的過程就是機器學習的過程。如今咱們已經大體瞭解決策樹能夠完成哪些任務,接下來咱們將學習如何從一堆原始數據中構造決策樹。首先咱們討論構造決策樹的方法,以及如何編寫構造樹的Python代碼;接着提出一些度量算法成功率的方法;最後使用遞歸創建分類器。app
在構造決策樹時,咱們須要解決的第一個問題就是,當前數據集上哪一個特徵在劃分數據分類時起決定性做用。爲了找到決定性的特徵,劃分出最好的結果,咱們必須評估每一個特徵。咱們假設已經根據必定的方法選取了待劃分的特徵,則原始數據集將根據這個特徵被劃分爲幾個數據子集。這數據子集會分佈在決策點(關鍵特徵)的全部分支上。若是某個分支下的數據屬於同一類型,則無需進一步對數據集進行分割。若是數據子集內的數據不屬於同一類型,則須要遞歸地重複劃分數據子集的過程,直到每一個數據子集內的數據類型相同。機器學習
建立分支的過程用僞代碼表示以下:函數
檢測數據集中的每一個子項是否屬於同一類型:
若是是,則返回類型標籤
不然:
尋找劃分數據集的最好特徵
劃分數據集
建立分支節點
對劃分的每一個數據子集:
遞歸調用本算法並添加返回結果到分支節點中
返回分支節點學習
決策樹的通常流程:
一些決策樹算法使用二分法劃分數據,本書並不採用這種方法。若是依據某個屬性劃分數據將會產生4個可能的值,咱們將把數據劃分紅四塊,並建立四個不一樣的分支。
本書將使用ID3算法劃分數據集,該算法處理如何劃分數據集,什麼時候中止劃分數據集(進一步的信息能夠參見http://en.wikipedia.org/wiki/ID3_algorithm)。每次劃分數據集咱們只選取一個特徵屬性,那麼應該選擇哪一個特徵做爲劃分的參考屬性呢?
表1的數據包含5個海洋動物,特徵包括:不浮出水面是否能夠生存,以及是否有腳噗。咱們能夠將這些動物分紅兩類:魚類和非魚類。
表1 海洋生物數據
不浮出水面是否能夠生存 | 是否有腳蹼 | 屬於魚類 | |
1 | 是 | 是 | 是 |
2 | 是 | 是 | 是 |
3 | 是 | 否 | 否 |
4 | 否 | 是 | 否 |
5 | 否 | 是 | 否 |
劃分數據集的大原則是:將無序的數據變得更加有序。咱們可使用多種方法劃分數據集,可是每種方法都有各自的優缺點。組織雜亂無章數據的一種方法就是使用信息論度量信息,信息論是量化處理信息的分支科學。咱們能夠在劃分數據以前或以後使用信息論量化度量信息的內容。
在劃分數據集以前以後信息發生的變化成爲信息增益,咱們能夠計算每一個特徵劃分數據集得到的信息增益,得到信息增益最高的特徵就是最好的選擇。
集合信息的度量方式成爲香農熵或者簡稱爲熵。
熵定義爲信息的指望值。咱們先肯定信息的定義:
若是待分類的事務可能劃分在多個分類之中,則符號$ x_i $定義爲:
$ l(x_i)=-log_2{p(x_i)} $
其中$ p(x_i) $是選擇該分類的機率。
爲了計算熵,咱們須要計算全部類型全部可能值包含的信息的指望值,經過下面的公式獲得:
$ H=-\sum_{i=1}^{n}{p(x_i)\log_{2}{p(x_i)}} $
其中n是分類的數目。
下面給出計算信息熵的Python函數,建立名爲trees.py文件,添加以下代碼:
from math import log def calcShannonEnt(dataSet): 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) return shannonEnt
代碼說明:
在trees.py文件中,咱們利用createDateSet()函數獲得一些樣例數據:
def createDataSet(): dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] labels = ['no sufacing', 'flippers'] return dataSet, labels
熵越高,則混合的數據也越多。獲得熵以後,咱們就能夠按照得到最大信息增益的方法劃分數據集。
另外一個度量集合無序程度的方法是基尼不純度(Gini impurity),簡單地說就是從一個數據集中隨機選取子項,度量其被錯誤分類到其餘分組裏的機率。
咱們將對每一個特徵劃分數據集的結果計算一次信息熵,而後判斷按照哪一個特徵劃分數據集市最好的劃分方法。
添加劃分數據集的代碼:
def splitDataSet(dataSet, axis, value): retDataSet = [] for featVec in dataSet: if featVac[axis] == value: reducedFeatVec = featVec[:axis] reducedFeatVec.extend(featVec[axis+1:]) retDataSet.append(reducedFeatVec) return retDataSet
該函數使用了三個輸入參數:帶劃分的數據集、劃分數據集的特徵、須要返回的特徵的值。函數先選取數據集中第axis個特徵值爲value的數據,從這部分數據中去除第axis個特徵,並返回。
測試這個函數,效果以下:
>>> myDat, labels = trees.createDataSet() >>> myDat [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']] >>> trees.splitDataSet(myDat, 0, 1) [[1, 'yes'], [1, 'yes'], [0, 'no']] >>> trees.splitDataSet(myDat, 0, 0) [[1, 'no'], [1, 'no']]
接下來咱們將遍歷整個數據集,循環計算香農熵和splitDataSet()函數,找到最好的特徵劃分方式。
def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 baseEntropy = calcShannonEnt(dataSet) bestInfoGain = 0.0 bestFeature = -1 for i in range(numFeatures): featList = [example[i] for example in dataSet] uniqueVals = set(featList) newEntropy = 0.0 for value in uniqueValue: 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
注意數據的最後一列或者每一個實例的最後一個元素是當前實例的類別標籤,而不是特徵;baseEntropy保存了整個數據集的原始香農熵。
測試上面代碼的實際輸出結果:
>>> myData, labels = trees.createDataSet() >>> trees.chooseBestFeatureToSplit(myDat) 0
函數選取了第一個特徵用於劃分。
構造決策樹所需的子功能模塊已經介紹完畢,構建決策樹的算法流程以下:
參加圖2所示:
圖2 劃分數據集時的數據路徑
添加下面的程序代碼:
import operator def majorityCnt(classList): 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): 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[:] # 複製labels列表 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels) # 遞歸構造子樹 return myTree
majorityCnt函數統計classList列表中每一個類型標籤出現頻率,返回出現次數最多的分類名稱。
createTree函數使用兩個輸入參數:數據集dataSet和標籤列表labels。標籤列表包含了數據集中全部特徵的標籤,算法自己並不須要這個變量,可是爲了給出數據明確的含義,咱們將它做爲一個輸入參數提供。上述代碼首先建立了名爲classList的列表變量,其中包含了數據集的全部類標籤。列表變量classList包含了數據集的全部類標籤。遞歸函數的第一個中止條件是全部類標籤徹底相同,則直接返回該類標籤。遞歸函數的第二個中止條件是使用完了全部特徵,仍然不能將數據集劃分紅僅包含惟一類別的分組。這裏使用majorityCnt函數挑選出現次數最多的類別做爲返回值。
下一步程序開始建立樹,這裏直接使用Python的字典類型存儲樹的信息。字典變量myTree存儲樹的全部信息。當前數據集選取的最好特徵存儲在變量bestFeat中,獲得列表中包含的全部屬性值。
最後代碼遍歷當前選擇特徵包含的全部屬性值,在每一個數據集劃分上遞歸待用函數createTree(),獲得的返回值將被插入到字典變量myTree中,所以函數終止執行時,字典中將會嵌套不少表明葉子節點信息的字典數據。
注意其中的subLabels = labels[:]複製了類標籤,由於在遞歸調用createTree函數中會改變標籤列表的值。
測試這些函數:
>>> myDat, labels = trees.createDataSet() >>> myTree = trees.createTree(myDat, labels) >>> myTree {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
依靠訓練數據構造了決策樹以後,咱們能夠將它用於實際數據的分類。在執行數據分類時,須要決策樹以及用於決策樹的標籤向量。而後,程序比較測試數據與決策樹上的數值,遞歸執行該過程直到進入葉子結點;最後將測試數據定義爲葉子結點所屬的類型。
使用決策樹分類的函數:
def classify(inputTree, featLabels, testVec): firstStr = inputTree.keys()[0] 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
測試上面的分類函數:
>>> myDat, labels = trees.createDataSet() >>> myTree = trees.createTree(myDat, labels[:]) >>> trees.classify(myTree, labels, [1, 0]) 'no' >>> trees.classify(myTree, labels, [1, 1]) 'yes'
可使用Python模塊pickle序列化對象,參見下面的程序。序列化對象能夠在磁盤上保存對象,並在須要的時候讀取出來。
def storeTree(inputTree, filename): import pickle fw = open(filename, 'w') pickle.dump(inputTree, fw) fw.close() def grabTree(filename): import pickle fr = open(filename) return pickle.load(fr)
隱形眼鏡數據集市很是著名的數據集,它包含不少患者眼部狀態的觀察條件以及醫生推薦的因性眼睛類型。隱形眼鏡類型包括硬材質、軟材質以及不適合佩戴隱形眼鏡。數據來源於UCI數據庫,爲了更容易顯示數據,本書對數據作了簡單的更改,數據存儲在源代碼下載路徑的文本文件中。
在Python命令提示符中輸入下列命令加載數據:
>>> fr = open('lenses.txt') >>> lensens = [inst.strip().split('\t') for inst in fr.readlines()] >>> lensensLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] >>> lensesTree = trees.createTree(lenses, lensesLabels) >>> lensensTree
決策樹很好地匹配了實驗數據,然而這些匹配選項可能太多了。咱們將這種問題稱之爲過分匹配(overfitting)。爲了減小過分匹配問題,咱們能夠裁剪決策樹,去掉一些沒必要要的葉子結點。若是葉子結點只能增長少量信息,則能夠刪除該節點,將他併入到其餘葉子結點中。第9章將進一步討論這個問題。
第九章將學習另外一個決策樹構造算法CART,本章使用的算法成爲ID3,它是一個好的算法但並不完美。ID3算法沒法直接處理數值型數據,儘管咱們能夠經過量化的方法將數值型數據轉化爲標稱型數據,可是若是存在太多的特徵劃分,ID3算法仍然會面臨其餘問題。
附錄:
1. 關於基尼不純度(Gini impurity)的更多信息,請參考Pan-Ning Tan, Vipin Kumar and Michael Steinbach, Introduction to Data Mineing. Pearson Eduction (Addison-Wesley, 2005), 158.
2. 隱形眼鏡數據集:The dataset is a modified version of the Lenses dataset retrieved from the UCI Machine Learning Repository November 3, 2001 [http://archive.ics.uci.edu/ml/machine-learning-databases/lenses/]. The source of the data is Jadzia Cendrowska and was originally published in 「PRISM: An algorithm for inducing modular rules,」 in International Journal of Man-Machine Studies (1987), 27, 349-70. 本書使用的數據的下載連接在:[連接]。