機器學習之路--決策樹
一,引言:
上一章我們講的kNN算法,雖然可以完成很多分類任務,但它最大的缺點是無法給出數據的內在含義,而決策樹的主要優勢就在於數據形式非常容易理解。決策樹算法能夠讀取數據集合,決策樹的一個重要任務是為了數據所蘊含的知識信息,因此,決策樹可以使用不熟悉的數據集合,並從中提取一系列規則,在這些機器根據數據集創建規則是,就是機器學習的過程。
二,相關知識
1 決策樹算法
在構造決策樹時,第一個需要解決的問題就是,如何確定出哪個特征在劃分數據分類是起決定性作用,或者說使用哪個特征分類能實現最好的分類效果。這樣,為了找到決定性的特征,劃分川最好的結果,我們就需要評估每個特征。當找到最優特征後,依此特征,數據集就被劃分為幾個數據子集,這些數據自己會分布在該決策點的所有分支中。此時,如果某個分支下的數據屬於同一類型,則該分支下的數據分類已經完成,無需進行下一步的數據集分類;如果分支下的數據子集內數據不屬於同一類型,那麽就要重復劃分該數據集的過程,按照劃分原始數據集相同的原則,確定出該數據子集中的最優特征,繼續對數據子集進行分類,直到所有的特征已經遍歷完成,或者所有葉結點分支下的數據具有相同的分類。
創建分支的偽代碼函數createBranch()如下:
檢測數據集中的每一個子項是否屬於同一分類:
if so return 類標簽; else 尋找劃分數據集的最好特征 劃分數據集 創建分支結點 for 每個分支結點 調用函數createBranch並增加返回結點到分支結點中//遞歸調用createBranch() return 分支結點
了解了如何劃分數據集後,我們可以總結出決策樹的一般流程:
(1)收集數據
(2)準備數據:構造樹算法只適用於標稱型數據,因此數值型數據必須離散化
(3)分析數據
(4)訓練數據:上述的構造樹過程構造決策樹的數據結構
(5)測試算法:使用經驗樹計算錯誤率
(6)使用算法:在實際中更好地理解數據內在含義
2 最好特征選取的規則:信息增益
劃分數據集的大原則是:將無序的數據變得更加有序。在劃分數據集前後信息發生的變化稱為信息增益,如果我們知道如何計算信息增益,就可以計算每個特征值劃分數據集獲得的信息增益,而獲取信息增益最高的特征就是最好的特征。
接下來,我們講學習如何計算信息增益,而提到信息增益我們又不得不提到一個概念"香農熵",或者簡稱熵。熵定義為信息的期望值。
如果待分類的事物可能會出現多個結果x,則第i個結果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工具繪制出決策樹的樹形圖,由於內容太多,就不一一講解啦
接下來,補充一下決策樹算法可能或出現的過度匹配(過擬合)的問題,當決策樹的復雜度較大時,很可能會造成過擬合問題。此時,我們可以通過裁剪決策樹的辦法,降低決策樹的復雜度,提高決策樹的泛化能力。比如,如果決策樹的某一葉子結點只能增加很少的信息,那麽我們就可將該節點刪掉,將其並入到相鄰的結點中去,這樣,降低了決策樹的復雜度,消除過擬合問題。
機器學習之路--決策樹