一,引言:python
上一章咱們講的kNN算法,雖然能夠完成不少分類任務,但它最大的缺點是沒法給出數據的內在含義,而決策樹的主要優點就在於數據形式很是容易理解。決策樹算法可以讀取數據集合,決策樹的一個重要任務是爲了數據所蘊含的知識信息,所以,決策樹可使用不熟悉的數據集合,並從中提取一系列規則,在這些機器根據數據集建立規則是,就是機器學習的過程。算法
二,相關知識數據結構
1 決策樹算法app
在構造決策樹時,第一個須要解決的問題就是,如何肯定出哪一個特徵在劃分數據分類是起決定性做用,或者說使用哪一個特徵分類能實現最好的分類效果。這樣,爲了找到決定性的特徵,劃分川最好的結果,咱們就須要評估每一個特徵。當找到最優特徵後,依此特徵,數據集就被劃分爲幾個數據子集,這些數據本身會分佈在該決策點的全部分支中。此時,若是某個分支下的數據屬於同一類型,則該分支下的數據分類已經完成,無需進行下一步的數據集分類;若是分支下的數據子集內數據不屬於同一類型,那麼就要重複劃分該數據集的過程,按照劃分原始數據集相同的原則,肯定出該數據子集中的最優特徵,繼續對數據子集進行分類,直到全部的特徵已經遍歷完成,或者全部葉結點分支下的數據具備相同的分類。機器學習
建立分支的僞代碼函數createBranch()以下:函數
檢測數據集中的每個子項是否屬於同一分類: 工具
if so return 類標籤; else 尋找劃分數據集的最好特徵 劃分數據集 建立分支結點 for 每一個分支結點 調用函數createBranch並增長返回結點到分支結點中//遞歸調用createBranch() return 分支結點
瞭解瞭如何劃分數據集後,咱們能夠總結出決策樹的通常流程:學習
(1)收集數據測試
(2)準備數據:構造樹算法只適用於標稱型數據,所以數值型數據必須離散化spa
(3)分析數據
(4)訓練數據:上述的構造樹過程構造決策樹的數據結構
(5)測試算法:使用經驗樹計算錯誤率
(6)使用算法:在實際中更好地理解數據內在含義
2 最好特徵選取的規則:信息增益
劃分數據集的大原則是:將無序的數據變得更加有序。在劃分數據集先後信息發生的變化稱爲信息增益,若是咱們知道如何計算信息增益,就能夠計算每一個特徵值劃分數據集得到的信息增益,而獲取信息增益最高的特徵就是最好的特徵。
接下來,咱們講學習如何計算信息增益,而提到信息增益咱們又不得不提到一個概念"香農熵",或者簡稱熵。熵定義爲信息的指望值。
若是待分類的事物可能會出現多個結果x,則第i個結果xi發生的機率爲p(xi),那麼咱們能夠由此計算出xi的信息熵爲l(xi)=p(xi)log(1/p(xi))=-p(xi)log(p(xi))
那麼,對於全部可能出現的結果,事物所包含的信息但願值(信息熵)就爲:H=-Σp(xi)log(p(xi)),i屬於全部可能的結果
這樣,假設利用數據集中某一特徵A對數據集D(D的分類類別有n種)進行分類,而特徵A取值有k種,那麼此時,利用特徵A對數據集進行分類的信息增益爲:
信息增益H(D,A)=原始數據集的信息熵H(D)-特徵A對數據集進行劃分後信息熵H(D/A)
其中H(D/A)=∑|Aj|/|D|*H(Aj),j屬於A的k種取值,|Aj|和|D|分別表示,特徵A第j種取值的樣本數佔全部取值樣本總數的比例,以及數據集的樣本總數
三,構造決策樹
在知道了如何選取劃分數據的最優特徵後,咱們就能夠依據此來構建決策樹了。
1 因爲要屢次使用香農熵的公式,因此咱們寫出計算給定數據集的熵的公式:
#計算給定數據集的熵 #導入log運算符 from math import log def calEnt(dataSet): #獲取數據集的行數 numEntries=len(dataSet) #設置字典的數據結構 labelCounts={} #提取數據集的每一行的特徵向量 for featVec in dataSet: #獲取特徵向量的最後一列的標籤 currentLabel=featVec[-1] #檢測字典的關鍵字key中是否存在該標籤 #若是不存在keys()關鍵字 if currentLabel not in labelCounts.keys(): #將當前標籤/0鍵值對存入字典中 labelCounts[currentLabel]=0 #不然將當前標籤對應的鍵值加1 labelCounts[currentLabel]+=1 #初始化熵爲0 Ent=0.0 #對於數據集中全部的分類類別 for key in labelCounts: #計算各個類別出現的頻率 prob=float(labelCounts[key])/numEntries #計算各個類別信息指望值 Ent-=prob*log(prob,2) #返回熵 return Ent
2 咱們固然須要構建決策樹的數據集:
#建立一個簡單的數據集 #數據集中包含兩個特徵'no surfacing','flippers'; #數據的類標籤有兩個'yes','no' def creatDataSet(): dataSet=[[1,1,'yes'], [1,1,'yes'], [1,0,'no'], [0,1,'no'], [0,1,'no']] labels=['no surfacing','flippers'] #返回數據集和類標籤 return dataSet,labels
須要說明的是,熵越高,那麼混合的數據就越多,若是咱們在數據集中添加更多的分類,會致使熵結果增大
3 接下來咱們就要經過上面講到的信息增益公式獲得劃分數據集的最有特徵,從而劃分數據集
首先劃分數據集的代碼:
#劃分數據集:按照最優特徵劃分數據集 #@dataSet:待劃分的數據集 #@axis:劃分數據集的特徵 #@value:特徵的取值 def splitDataSet(dataSet,axis,value): #須要說明的是,python語言傳遞參數列表時,傳遞的是列表的引用 #若是在函數內部對列表對象進行修改,將會致使列表發生變化,爲了 #不修改原始數據集,建立一個新的列表對象進行操做 retDataSet=[] #提取數據集的每一行的特徵向量 for featVec in dataSet: #針對axis特徵不一樣的取值,將數據集劃分爲不一樣的分支 #若是該特徵的取值爲value if featVec[axis]==value: #將特徵向量的0~axis-1列存入列表reducedFeatVec reducedFeatVec=featVec[:axis] #將特徵向量的axis+1~最後一列存入列表reducedFeatVec #extend()是將另一個列表中的元素(以列表中元素爲對象)一一添加到當前列表中,構成一個列表 #好比a=[1,2,3],b=[4,5,6],則a.extend(b)=[1,2,3,4,5,6] reducedFeatVec.extend(featVec[axis+1:]) #簡言之,就是將原始數據集去掉當前劃分數據的特徵列 #append()是將另一個列表(以列表爲對象)添加到當前列表中 ##好比a=[1,2,3],b=[4,5,6],則a.extend(b)=[1,2,3,[4,5,6]] retDataSet.append(reducedFeatVec) return retDataSet
須要說明的是:
(1)在劃分數據集函數中,傳遞的參數dataSet列表的引用,在函數內部對該列表對象進行修改,會致使列表內容發生改變,因而,爲了消除該影響,咱們應該在函數中建立一個新的列表對象,將對列表對象操做後的數據集存入新的列表對象中
(2)須要區分一下append()函數和extend()函數
這兩種方法的功能相似,都是在列表末尾添加新元素,可是在處理多個列表時,處理結果有所不一樣:
好比:a=[1,2,3],b=[4,5,6]
那麼a.append(b)的結果爲:[1,2,3,[4,5,6]],即便用append()函數會在列表末尾添加人新的列表對象b
而a.extend(b)的結果爲:[1,2,3,4,5,6],即便用extend()函數
接下來,咱們再看選取最優特徵的代碼:
#如何選擇最好的劃分數據集的特徵 #使用某一特徵劃分數據集,信息增益最大,則選擇該特徵做爲最優特徵 def chooseBestFeatureToSplit(dataSet): #獲取數據集特徵的數目(不包含最後一列的類標籤) numFeatures=len(dataSet[0])-1 #計算未進行劃分的信息熵 baseEntropy=calEnt(dataSet) #最優信息增益 最優特徵 bestInfoGain=0.0;bestFeature=-1 #利用每個特徵分別對數據集進行劃分,計算信息增益 for i in range(numFeatures): #獲得特徵i的特徵值列表 featList=[example[i] for example in dataSet] #利用set集合的性質--元素的惟一性,獲得特徵i的取值 uniqueVals=set(featList) #信息增益0.0 newEntropy=0.0 #對特徵的每個取值,分別構建相應的分支 for value in uniqueVals: #根據特徵i的取值將數據集進行劃分爲不一樣的子集 #利用splitDataSet()獲取特徵取值Value分支包含的數據集 subDataSet=splitDataSet(dataSet,i,value) #計算特徵取值value對應子集佔數據集的比例 prob=len(subDataSet)/float(len(dataSet)) #計算佔比*當前子集的信息熵,並進行累加獲得總的信息熵 newEntropy+=prob*calEnt(subDataSet) #計算按此特徵劃分數據集的信息增益 #公式特徵A,數據集D #則H(D,A)=H(D)-H(D/A) infoGain=baseEntropy-newEntropy #比較此增益與當前保存的最大的信息增益 if (infoGain>bestInfoGain): #保存信息增益的最大值 bestInfoGain=infoGain #相應地保存獲得此最大增益的特徵i bestFeature=i #返回最優特徵 return bestFeature
在函數調用中,數據必須知足必定的要求,首先,數據必須是由列表元素組成的列表,並且全部的列表元素具備相同的數據長度;其次,數據的最後一列或者每一個實例的最後一個元素是當前實例的類別標籤。這樣,咱們才能經過程序統一完成數據集的劃分
4,在經過以上的各個模塊學習以後,咱們接下來就要真正構建決策樹,構建決策樹的工做原理爲:首先獲得原始數據集,而後基於最好的屬性劃分數據集,因爲特徵值可能多於兩個,所以可能存在大於兩個分支的數據集劃分。第一次劃分以後,數據將向下傳遞到樹分支的下一個結點,在該結點上,咱們能夠再次劃分數據。所以,咱們能夠採用遞歸的方法處理數據集,完成決策樹構造。
遞歸的條件是:程序遍歷完全部劃分數據集的屬性,或者每一個分之下的全部實例都具備相同的分類。若是全部的實例具備相同的分類,則獲得一個葉子結點或者終止塊。
固然,咱們可能會遇到,當遍歷完全部的特徵屬性,可是某個或多個分支下實例類標籤仍然不惟一,此時,咱們須要肯定出如何定義該葉子結點,在這種狀況下,經過會採起多數表決的原則選取分支下實例中類標籤種類最多的分類做爲該葉子結點的分類
這樣,咱們就須要先定義一個多數表決函數majorityCnt()
#當遍歷完全部的特徵屬性後,類標籤仍然不惟一(分支下仍有不一樣分類的實例) #採用多數表決的方法完成分類 def majorityCnt(classList): #建立一個類標籤的字典 classCount={} #遍歷類標籤列表中每個元素 for vote in classList: #若是元素不在字典中 if vote not in classCount.keys(): #在字典中添加新的鍵值對 classCount[vote]=0 #不然,當前鍵對於的值加1 classCount[vote]+=1 #對字典中的鍵對應的值所在的列,按照又大到小進行排序 #@classCount.items 列表對象 #@key=operator.itemgetter(1) 獲取列表對象的第一個域的值 #@reverse=true 降序排序,默認是升序排序 sortedClassCount=sorted(classCount.items,\ key=operator.itemgetter(1),reverse=true) #返回出現次數最多的類標籤 return sortedClassCount[0][0]
好了,考慮了這種狀況後,咱們就能夠經過遞歸的方式寫出決策樹的構建代碼了
#建立樹 def createTree(dataSet,labels): #獲取數據集中的最後一列的類標籤,存入classList列表 classList=[example[-1] for example in dataSet] #經過count()函數獲取類標籤列表中第一個類標籤的數目 #判斷數目是否等於列表長度,相同表面全部類標籤相同,屬於同一類 if classList.count(classList[0])==len(classList): return classList[0] #遍歷完全部的特徵屬性,此時數據集的列爲1,即只有類標籤列 if len(dataSet[0])==1: #多數表決原則,肯定類標籤 return majorityCnt(classList) #肯定出當前最優的分類特徵 bestFeat=chooseBestFeatureToSplit(dataSet) #在特徵標籤列表中獲取該特徵對應的值 bestFeatLabel=labels[bestFeat] #採用字典嵌套字典的方式,存儲分類樹信息 myTree={bestFeatLabel:{}} ##此位置書上寫的有誤,書上爲del(labels[bestFeat]) ##至關於操做原始列表內容,致使原始列表內容發生改變 ##按此運行程序,報錯'no surfacing'is not in list ##如下代碼已改正 #複製當前特徵標籤列表,防止改變原始列表的內容 subLabels=labels[:] #刪除屬性列表中當前分類數據集特徵 del(subLabels[bestFeat]) #獲取數據集中最優特徵所在列 featValues=[example[bestFeat] for example in dataSet] #採用set集合性質,獲取特徵的全部的惟一取值 uniqueVals=set(featValues) #遍歷每個特徵取值 for value in uniqueVals: #採用遞歸的方法利用該特徵對數據集進行分類 #@bestFeatLabel 分類特徵的特徵標籤值 #@dataSet 要分類的數據集 #@bestFeat 分類特徵的標稱值 #@value 標稱型特徵的取值 #@subLabels 去除分類特徵後的子特徵標籤列表 myTree[bestFeatLabel][value]=createTree(splitDataSet\ (dataSet,bestFeat,value),subLabels) return myTree
須要說明的是,此時參數dataSet爲列表的引用,咱們不能在函數中直接對列表進行修改,可是在書中代碼中有del(labels[bestFeat])的刪除列表某一列的操做,顯然不可取,應該建立新的列表對象subLabels=labels[:],再調用函數 del(subLabels[bestFeat])
好了,接下來運行代碼:
5 接下來,咱們能夠經過決策樹進行實際的分類了,利用構建好的決策樹,輸入符合要求的測試數據,比較測試數據與決策樹上的數值,遞歸執行該過程直到葉子結點,最後將測試數據定義爲葉子結點全部的分類,輸出分類結果
決策樹分類函數代碼爲:
#------------------------測試算法------------------------------ #完成決策樹的構造後,採用決策樹實現具體應用 #@intputTree 構建好的決策樹 #@featLabels 特徵標籤列表 #@testVec 測試實例 def classify(inputTree,featLabels,testVec): #找到樹的第一個分類特徵,或者說根節點'no surfacing' #注意python2.x和3.x區別,2.x可寫成firstStr=inputTree.keys()[0] #而不支持3.x firstStr=list(inputTree.keys())[0] #從樹中獲得該分類特徵的分支,有0和1 secondDict=inputTree[firstStr] #根據分類特徵的索引找到對應的標稱型數據值 #'no surfacing'對應的索引爲0 featIndex=featLabels.index(firstStr) #遍歷分類特徵全部的取值 for key in secondDict.keys(): #測試實例的第0個特徵取值等於第key個子節點 if testVec[featIndex]==key: #type()函數判斷該子節點是否爲字典類型 if type(secondDict[key]).__name__=='dict': #子節點爲字典類型,則從該分支樹開始繼續遍歷分類 classLabel=classify(secondDict[key],featLabels,testVec) #若是是葉子節點,則返回節點取值 else: classLabel=secondDict[key] return classLabel
輸入實例,經過分類函數獲得預測結果,能夠與實際結果比對,計算錯誤率
6 咱們說一個好的分類算法要可以完成實際應用的須要,決策樹算法也不例外,一個算法好很差,仍是須要實際應用的檢驗才行,接下來咱們會經過一個實例來使用決策樹預測隱形眼鏡的類型
首先,咱們知道構建決策樹是很是耗時的任務,即便很小的數據集,也要花費幾秒的時間來構建決策樹,這樣顯然耗費計算時間。因此,咱們能夠將構建好的決策樹保存在磁盤中,這樣當咱們須要的時候,再從磁盤中讀取出來使用便可。
如何進行對象的序列化操做,python的pickle模塊足以勝任該任務,任何對象均可以經過pickle模塊執行序列化操做,字典也不例外,使用pickle模塊存儲和讀取決策樹文件的代碼以下:
#決策樹的存儲:python的pickle模塊序列化決策樹對象,使決策樹保存在磁盤中 #在須要時讀取便可,數據集很大時,能夠節省構造樹的時間 #pickle模塊存儲決策樹 def storeTree(inputTree,filename): #導入pickle模塊 import pickle #建立一個能夠'寫'的文本文件 #這裏,若是按樹中寫的'w',將會報錯write() argument must be str,not bytes #因此這裏改成二進制寫入'wb' fw=open(filename,'wb') #pickle的dump函數將決策樹寫入文件中 pickle.dump(inputTree,fw) #寫完成後關閉文件 fw.close() #取決策樹操做 def grabTree(filename): import pickle #對應於二進制方式寫入數據,'rb'採用二進制形式讀出數據 fr=open(filename,'rb') return pickle.load(fr)
這裏,文件的寫入操做爲'wb'或'wb+',表示以byte的形式寫入數據,相應'rb'以byte形式讀入數據
接下來,咱們將經過隱形眼鏡數據集構建決策樹,從而預測患者須要佩戴的隱形眼鏡的類型,步驟以下:
(1)收集數據:文本數據集'lenses.txt'
(2)準備數據:解析tab鍵分隔開的數據行
(3)分析數據:快速檢查數據,確保正確地解析數據內容
(4)訓練算法:構建決策樹
(5)測試算法:經過構建的決策樹比較準確預測出分類結果
(6)算法的分類準確類知足要求,將決策樹存儲下來,下次須要時讀取使用
其中,解析文本數據集和構建隱形眼鏡類型決策樹的函數代碼以下:
#------------------------示例:使用決策樹預測隱形眼鏡類型---------------- def predictLensesType(filename): #打開文本數據 fr=open(filename) #將文本數據的每個數據行按照tab鍵分割,並依次存入lenses lenses=[inst.strip().split('\t') for inst in fr.readlines()] #建立並存入特徵標籤列表 lensesLabels=['age','prescript','astigmatic','tearRate'] #根據繼續文件獲得的數據集和特徵標籤列表建立決策樹 lensesTree=createTree(lenses,lensesLabels) return lensesTree
固然,咱們還能夠經過python的matplotlib工具繪製出決策樹的樹形圖,因爲內容太多,就不一一講解啦
接下來,補充一下決策樹算法可能或出現的過分匹配(過擬合)的問題,當決策樹的複雜度較大時,極可能會形成過擬合問題。此時,咱們能夠經過裁剪決策樹的辦法,下降決策樹的複雜度,提升決策樹的泛化能力。好比,若是決策樹的某一葉子結點只能增長不多的信息,那麼咱們就可將該節點刪掉,將其併入到相鄰的結點中去,這樣,下降了決策樹的複雜度,消除過擬合問題。