機器學習-決策樹

基本原理:
  kNN算法可以完成很多分類任務,但是它的最大的缺點就是無法給出數據的內在含義,決策樹的主要優點就在於數據形式非常容易理解。
  決策樹的一般流程:

  1. 收集數據
  2. 準備數據:樹構造算法只適用於標稱型數據,因此數值型必須離散化
  3. 分析數據:可以使用任何方法,在樹構造完成之後,我們可以檢查圖形是否符合預期
  4. 訓練算法:構造樹的數據結構
  5. 測試算法:使用經驗樹計算錯誤率
  6. 使用算法:使用訓練好的樹模型進行分類
      這裏使用ID3算法,首先根據信息增益劃分數據集,然後使用遞歸構建決策樹。當匹配選項太多的時候,就變成了過度匹配,爲了解決這個問題,我們可以裁剪決策樹,去掉一些不必要的葉子結點。如果葉子節點只能增加少許信息,則可以刪除該節點,並將它併入其他節點中。
      另外還有C4.5和CART決策樹構造算法,在後面的博客中會對這兩種算法進行代買實現,下面的代碼是使用ID3進行決策樹算法的實現。其中各個函數的功能以及測試在代碼中都有寫,其打包程序:https://download.csdn.net/download/pcb931126/10859675


"""
機器學習之決策樹
姓名:pcb
日期:2018.12.16
"""

from math import log
import operator
import matplotlib.pyplot as plt
import pickle                               #利用pickle模塊存儲決策樹


#-*-coding:utf-8-*-

"""
計算給定數據的香農熵
"""
def calcShannonEnt(dataSet):
    numEntries=len(dataSet)                                   #計算數據中的總數
    labelCounts={}                                            #爲所有可能分類創建字典
    for featVec in dataSet:
        currentLabel=featVec[-1]                              #鍵值是字典的最後一列數值
        if currentLabel not in labelCounts.keys():            #如果當前的鍵值不存在,則擴展字典,並將當前的鍵值加入字典
            labelCounts[currentLabel]=0                       #將該鍵加入到字典中,並給值附爲0
        labelCounts[currentLabel]+=1                          #將該鍵的值+1,最終得到每種分類的次數
    shannonEnt=0.0                                            #計算香農熵
    for key in labelCounts:                                   #得到字典中的鍵
        prob=float(labelCounts[key])/numEntries               #根據鍵得到值,並計算該分類的值佔中分類數量的比例
        shannonEnt-=prob*log(prob,2)                          #計算熵-計算所有類別所有可能值包含的信息期望值
    return shannonEnt


"""
劃分數據集,當我們按照某個特徵劃分數據集時,就需要將所有符合要求的元素提取出來
"""
def splitDataSet(dataSet,axis,value):
    """
    :param dataSet:待劃分的數據集
    :param axis:   劃分數據集的特徵
    :param value:  需要返回特徵的值
    :return:
    """
    #Python語言在函數中傳遞的是列表的引用,在函數內部對列表的修改,會影響到該列表對象的整個生存週期,
    #爲了消除這個不良的影響需要在函數開始聲明一個新列表對象
    retDataSet=[]                                             #創建新的list對象
    for featVec in dataSet:
        if featVec[axis]==value:
            reducedFeatVec=featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])           #將所得到的列表合併,元素個數相加
            retDataSet.append(reducedFeatVec)                 #將該列表作爲一個元素添加到列表中,列表中的元素數量+1
    return retDataSet


"""
#遍歷整個數據集,循環計算香農熵和splitDataSet(),找到最好的特徵劃分方式來劃分數據集
#該函數實現了選取特徵,劃分數據集,計算得出最好的劃分數據集的特徵
#數據集dataSet必須滿足兩個要求:
   1.數據必須是一種由列表元素組成的列表,而且所有的列表元素都要具有相同的數據長度
   2.數據的最後一列或者每個實例的最後一個元素是當前實例的標籤
"""
def chooseBestFeatureToSplit(dataSet):

    numFeatures=len(dataSet[0])-1
    baseEntropy=calcShannonEnt(dataSet)  #計算數據集的原始香農熵,保存最初的無序度量值,用於與劃分完之後的數據集計算的熵值進行比較
    bestInfoGain=0.0;bestFeature=-1

    # 遍歷數據集中的所有特徵,使用列表推導創建新的列表,將數據集中所有第i個特徵或者所有可能存在的值寫入新的list中
    for i in range(numFeatures):
        featList=[example[i] for example in dataSet]           #提取特徵的每列數據
        uniqueVals=set(featList)
        newEntropy=0.0

        #遍歷當前特徵值中所有唯一屬性值,對每個唯一屬性值劃分一次數據集
        for value in uniqueVals:
            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

"""
多數表決:如果數據集已經處理了所有屬性,但是類標籤依然不唯一,此時通常會採用多數表決的方式決定改葉子結點的分類
"""
def majorityCnt(classList):
    classCount={}
    for vote in classList:
        if vote not in classList.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: 數據集 ,前面提到的數據集的要求必須滿足
    :param labels:  標籤列表,標籤列表中包含了數據集中所有特徵的標籤,算法本身並不需要這個變量
    :return:
    """
    classList=[example[-1] for example in dataSet] #創建classList列表變量,其中包含了數據集中的所有類標籤

    #遞歸停止的第一個條件就是所有的類標籤完全相同
    if classList.count(classList[0])==len(classList):  #統計classList中的類標籤是否是classList的長度
        return classList[0]

    #遞歸停止的第二個條件使用完了所有特徵,仍然不能將數據集換分成僅僅包含唯一類別的分組
    #採用選取次數最多的類別作爲返回值
    if len(dataSet[0])==1:
        return majorityCnt(classList)

    #選取當前數據集中最好的特徵變量存儲在bestFeat中
    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[:]    #複製類標籤,並將其存儲在新的列表變量subLabels中,使用subLabels代替原始列表
        #在每個數據集劃分上遞歸調用函數createTree,得到的返回值插入到字典變量myTree中
        myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
    return myTree


"""
驗證香農熵的計算
"""
def createDataSet():
    dataSet=[[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]  #熵越高,表明混合的數據越多
    labels=['no surfacing','flippers']                        #我們按照獲取最大信息增益的方法劃分數據集
    return dataSet,labels

"""
-------------------------使用文本註解繪製樹節點----------------------------------------------------------------------------
"""


decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
"""
繪製帶箭頭的註解
"""
def plotNode(nodeTxt,centrPt,parentPt,nodeType):
    createPlot.ax1.annotate(nodeTxt,xy=parentPt,xycoords="axes fraction",xytext=centrPt,textcoords="axes fraction",\
                            va="center",ha="center",bbox=nodeType,arrowprops=arrow_args)
"""
在父子節點中填充文本信息
"""
def plotMindText(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)

"""
計算寬和高
"""
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)
    plotMindText(cntrpt,parentPt,nodeTxt)
    plotNode(firstStr,cntrpt,parentPt,decisionNode)
    secondDict=myTree[firstStr]
    plotTree.yOff=plotTree.yOff-1.0/plotTree.totalD

    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)
            plotMindText((plotTree.xOff,plotTree.yOff),cntrpt,str(key))

    plotTree.yOff=plotTree.yOff+1.0/plotTree.totalD


"""
實際繪圖函數
"""

def createPlot():

    plt.rcParams['font.sans-serif'] = ['SimHei']  # 用來正常顯示中文標籤
    plt.rcParams['axes.unicode_minus'] = False  # 用來正常顯示負號
    fig=plt.figure(1,facecolor="white")
    fig.clf()                                                   #清空圖像區
    createPlot.ax1=plt.subplot(111,frameon=False)
    plotNode(u"決策節點",(0.5,0.1),(0.1,0.5),decisionNode)
    plotNode(u"葉節點",(0.8,0.1),(0.3,0.8),leafNode)
    plt.show()

"""
繪圖
"""
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()


"""
獲取樹的葉節點的數目
"""
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]    #在python中dict.key()不是list類型,也不支持索引了,,解決的辦法就是使用list()
    secondDict=myTree[firstStr]

    #計算遍歷過程中遇到判斷節點的個數,終止條件是葉子節點,一旦達到葉子結點,則從遞歸抵用中返回,並將樹的深度+1
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':       #type()函數判斷子節點是否爲字典類型
            thisDepth=1+getTreeDepth(secondDict[key])    #節點是字典類型,需要遞歸調用getTreeDepth()
        else:
            thisDepth=1
        if thisDepth>maxDepth:
            maxDepth=thisDepth
    return maxDepth


"""
創建一個預先儲存的樹,避免每次測試代碼都要重數據中創建樹的麻煩
"""
def retrieveTree(i):
    listOfTrees=[{'no surfacing':{0:'no',1:{'flippers':{0:'no',1:'yes'}}}},\
                 {'no surfacing':{0:'no',1:{'flippers':{0:{'head':{0:'no',1:'yes'}},1:'yes'}}}}]

    return listOfTrees[i]

"""
------------------------------------------------------------------------------------------------------------------------
"""

"""
使用決策樹進行分類
"""
def classify(inputTree,featLabels,testVec):
    firstStr=list(inputTree.keys())[0]
    secondDict=inputTree[firstStr]
    featIndex=featLabels.index(firstStr)        #使用indxe方法查找與列表中第一個匹配firstStr變量元素
    for key in secondDict.keys():
        if testVec[featIndex]==key:             #比較testVec變量中的值與樹節點的值,如果達到葉子節點,則返回當前的節點分類標籤
            if type(secondDict[key]).__name__=='dict':
                classLabel=classify(secondDict[key],featLabels,testVec)
            else:
                classLabel=secondDict[key]

    return classLabel

"""
使用pickle模塊存儲決策樹
"""
#將決策樹寫入txt文檔中
def storeTree(inputTree,filename):
    fw=open(filename,'wb')
    pickle.dump(inputTree,fw)
    fw.close()

#將決策樹從txt文檔讀出
def grabTree(filename):
    fr=open(filename,'rb')
    return pickle.load(fr)


"""
讀取隱形眼鏡數據集
"""
def ReadLenses(filename):
    fr=open(filename)
    lenses=[inst.strip().split('\t') for inst in fr.readlines()]
    lensesLabel=['age','prescript','astigmatic','tearRate']
    return lenses,lensesLabel


def main():

# #1.----------------利用分類函數進行分類測試--------------------------
#     dataSet,labels=createDataSet()              #創建一個數據集
#     myTree = retrieveTree(0)                    #創建一個樹用於測試畫樹的效果
#     classifyLabel=classify(myTree,labels,[1,0]) #利用決策樹進行分類函數
#     print(classifyLabel)
#     storeTree(myTree,'classifierStorage.txt')   #測試利用pickle模塊存儲決策樹
#     myTree1=grabTree('classifierStorage.txt')   #測試利用pickle模塊讀取決策
#------------------------------------------------------------------


#2.----------使用決策樹進行預測隱形眼鏡類型----------------------------
    lenses,lensesLable=ReadLenses('lenses.txt')   #加載隱形眼睛的數據集
    lensesTree=createTree(lenses,lensesLable)     #創建隱形眼鏡的決策樹
    print(lensesTree)                             #輸出隱形眼鏡決策樹
    createPlot(lensesTree)                        #畫出決策樹的樹圖
#-------------------------------------------------------------------

# #3.----------局部函數測試---------------------------------------------
#     myTree=createTree(dataSet,labels)          #利用創建的數據集得到決策樹
#     splitDataSet(dataSet,0,1)                  #
#     shannonEnt=calcShannonEnt(dataSet)         #測試計算香農熵的函數
#     print(myTree)
#     createPlot()
#
#     getNumLeafs(myTree)                        #測試得到葉子節點的函數
#     getTreeDepth(myTree)                       #測試得到樹的深度的函數
#     createPlot(myTree)                          #測試畫決策樹的函數
# #-------------------------------------------------------------------

if __name__=="__main__":
    main()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章