機(jī)器學(xué)習(xí)實(shí)戰(zhàn)之python實(shí)現(xiàn)決策樹ID3

圖片.png

決策樹的主要算法有ID3,C4.5,CART
ID3使用信息增益來選擇特征
C4.5使用信息增益率來選擇特征
CART使用基尼系數(shù)來選擇特征
本文主要介紹ID3抑片,相關(guān)的理論知識請參考下面的博文
http://blog.csdn.net/acdreamers/article/details/44661149

'''構(gòu)建算法'''

def calcShannonEnt(dataSet):#定義求熵函數(shù)
    numEntries = len(dataSet)#總共有多少條數(shù)據(jù)
    labelCounts = {}#建立一個字典阅懦,收集結(jié)果中的分類
    for i in dataSet:
        currentLabel = i[-1] #注意數(shù)據(jù)集的分類要在最后一列
        if currentLabel not in labelCounts.keys():#統(tǒng)計(jì)各分類數(shù)量
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    shannonEnt = 0
    for key in labelCounts:#計(jì)算系統(tǒng)的信息熵
        prob = float(labelCounts[key])/numEntries #計(jì)算每種類別的概率
        shannonEnt -= prob*log(prob,2) #計(jì)算所有信息期望值的和即為信息熵
        #這一句可理解成這樣shannonEnt += -prob*log(prob,2)
    return shannonEnt

def createDataSet():#定義創(chuàng)建數(shù)據(jù)集函數(shù),測試用
    dataSet = [[1,1,'yes'],
               [1,1,'yes'],
               [1,0,'no'],
               [0,1,'no'],
               [0,1,'no']]
    labels = ['no surfacing','flippers']
    return dataSet,labels

#myDat,labels = createDataSet()
#print(calcShannonEnt(myDat)) #目前結(jié)果有兩類踢俄,可得出信息熵是0.97咪橙,我們增加一個分類
#myDat[0][-1] = 'maybe'
#print(calcShannonEnt(myDat)) #可以看到信息熵增加了,也就是說果善,數(shù)據(jù)越無序(即越不可預(yù)測)诊笤,熵越大

'''以上定義求信息熵函數(shù),下面我們依據(jù)信息熵來選擇最優(yōu)特征'''

def splitDataSet(dataSet,axis,value):#按照給定特征劃分?jǐn)?shù)據(jù)集
    #axis為dataSet列方向的索引(某個特征)巾陕,value為該列(特征)所包含的值(類別)
    #這個函數(shù)的目的是將dataSet按照某列的某值進(jìn)行分類
    retDataSet = []
    for i in dataSet:
        if i[axis] == value:
            reducedFeatvec = i[:axis]
            reducedFeatvec.extend(i[axis+1:])
            retDataSet.append(reducedFeatvec)
    return retDataSet

def chooseBestFeatureToSplit(dataSet):
    #該函數(shù)的目的是選擇最好的數(shù)據(jù)集劃分特征
    numFeatures = len(dataSet[0])-1 #獲得數(shù)據(jù)集中的特征個數(shù)
    #選出一個元素讨跟,-1是因?yàn)樽詈笠涣惺菢?biāo)簽,不是特征
    baseEntropy = calcShannonEnt(dataSet) #計(jì)算最初的熵
    bestInfoGain = 0
    bestFeature = -1
    for i in range(numFeatures):#按特征的個數(shù)進(jìn)行循環(huán)
        featList = [example[i] for example in dataSet] #獲取索引為i的特征列表
        uniqueVals = set(featList) #獲取特征所包含的唯一值
        newEntropy = 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

'''以上晾匠,先通過chooseBestFeatureToSplit函數(shù)選擇信息增益最大的特征,
而后使用splitDataSet函數(shù)通過該特征對數(shù)據(jù)集進(jìn)行分類'''

def majorityCnt(classList):
    #該函數(shù)的作用是梯刚,當(dāng)所有的特征都處理完了以后凉馆,標(biāo)簽仍然不存在唯一值,這個時候我們
    #人為選擇出現(xiàn)標(biāo)簽出現(xiàn)最多的那個
    classCount = {} #定義標(biāo)簽字典
    for vote in 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]

def createTree(dataSet,labels):#創(chuàng)建樹函數(shù)
    classList = [example[-1] for example in dataSet] #將所有的標(biāo)簽提取出來亡资,組成一個列表
    if classList.count(classList[0]) == len(classList):#判斷標(biāo)簽列表時不是唯一值
        return classList[0]
#    if set(classList) == 1:
#        return classList[0]
    if len(dataSet[0]) == 1:#如果標(biāo)簽不唯一澜共,但dataSet的長度已經(jīng)為1了,即所有的特征已經(jīng)
        #處理完了锥腻,這個時候嗦董,認(rèn)為選擇標(biāo)簽出現(xiàn)最多的那一個
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)#獲取最大信息熵的特征索引
    bestFeatLabel = labels[bestFeat]#獲取對應(yīng)的標(biāo)簽
    myTree = {bestFeatLabel:{}} #建立樹的字典
    del labels[bestFeat] #從標(biāo)簽列表中刪除已經(jīng)處理過的特征標(biāo)簽
    featValues = [example[bestFeat] for example in dataSet] #獲取篩選出的特征里的所有值
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),
              subLabels) #遞歸使用cerateTree函數(shù),直到標(biāo)簽值唯一或者所有特征處理完為止
        #每一次遞歸都會將篩選出的擁有最大信息熵的特征傳遞給myTree字典旷太,以構(gòu)建樹
    return myTree 

'''createTree為遞歸創(chuàng)建樹函數(shù)展懈,每次調(diào)用都先判斷標(biāo)簽是否一致,
如果一致說明此特征已分類完成供璧,
如果標(biāo)簽不一致存崖,那就判斷還有沒有特征集,
有睡毒,就繼續(xù)尋找最優(yōu)特征来惧,進(jìn)行分類,
沒有演顾,就調(diào)用majorityCnt選擇當(dāng)前結(jié)果中標(biāo)簽出現(xiàn)最多的那個'''
    
myDat,labels = createDataSet()
myTree = createTree(myDat,labels)
#輸出結(jié)果為{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
#解釋如下供搀,首先通過最大信息熵選擇第一個特征,即‘no surfacing’钠至,它下面有兩個值葛虐,
#分別是0,1棉钧。0分支下面所有的標(biāo)簽都是no屿脐,即類標(biāo)簽,那么該節(jié)點(diǎn)判斷完畢,為葉子結(jié)點(diǎn)
#1分支下面還含有其他特征的诵,繼續(xù)通過最大信息熵進(jìn)行選擇万栅,這次選擇的是特征是'flippers',
#它下面有兩個值西疤,0和1烦粒,每個值下面都是類標(biāo)簽,所以該節(jié)點(diǎn)判斷完畢代赁,為葉子節(jié)點(diǎn)

def classify(inputtree,featLabels,testvec):#定義測試決策樹函數(shù)
    #后兩個參數(shù)代表的是指定標(biāo)簽以及對應(yīng)標(biāo)簽的值
    firstStr = list(inputtree.keys())[0]
    secondDict = inputtree[firstStr]
    featIndex = featLabels.index(firstStr)
    for key in secondDict.keys():
        if testvec[featIndex] == key:
            if type(secondDict[key]) == dict:
                classLabel = classify(secondDict[key],featLabels,testvec)
            else:
                classLabel = secondDict[key]
    return classLabel
print(classify(myTree,['no surfacing','flippers'],[1,0])
#可以看到扰她,當(dāng)特征no surfacing為1,特征flippers為0時管跺,輸出結(jié)果是no义黎,可數(shù)據(jù)集中的一致

def storeTree(inputtree,filename):#定義一個函數(shù),保存決策樹結(jié)果
    import pickle
    f = open(filename,'wb')
    pickle.dump(inputtree,f)
    f.close()
    
def grabTree(filename):#定義一個函數(shù)豁跑,讀取決策樹結(jié)果
    import pickle
    with open(filename,'rb') as f:
        return pickle.load(f)
    
'''
下面定義幾個函數(shù)將決策樹畫出來
'''
#畫圖的時候要確定圖所需大概空間廉涕,通過以下函數(shù)來定義相關(guān)屬性    
def getNumLeafs(myTree):#定義一個獲取樹葉節(jié)點(diǎn)數(shù)目的函數(shù)
    numLeafs = 0
    firstStr = list(myTree.keys())[0] #第一層肯定只有一個特征
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]) == dict:
            numLeafs += getNumLeafs(secondDict[key])
        else:
            numLeafs += 1
    return numLeafs

def getTreeDepth(myTree): #定義一個獲取樹層數(shù)的函數(shù)
    maxDepth = 0
    firstStr = list(myTree.keys())[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]) == dict:
            maxDepth = 1 + getTreeDepth(secondDict[key])
        else:
            maxDepth = 1 #這里不是+=1,是因?yàn)檫@里是在節(jié)點(diǎn)的循環(huán)里艇拍,層數(shù)與節(jié)點(diǎn)數(shù)無關(guān)
    return maxDepth
       
def plotMidText(cntrpt,parentpt,txtString):#該函數(shù)的作用是在兩個節(jié)點(diǎn)之間添加文本
    xMid = (parentpt[0]-cntrpt[0])/2 + cntrpt[0] 
    yMid = (parentpt[1]-cntrpt[1])/2 + cntrpt[1]
    axes.text(xMid,yMid,txtString)#text是在指定的位置添加文本

decisionNode = dict(boxstyle='sawtooth',fc='0.8')#設(shè)置文本外框樣式
leafNode = {'boxstyle':'round4','fc':'0.8'}#設(shè)置文本外框樣式
arrow_args={'arrowstyle':'<-'} #注意這里是反箭頭
def plotNode(nodeTxt,centerpt,parentpt,nodeType): #畫節(jié)點(diǎn)及連接線
    axes.annotate(nodeTxt,xy=parentpt,xycoords='axes fraction',
                            xytext=centerpt,textcoords='axes fraction',va='center',
                            ha='center',bbox=nodeType,arrowprops=arrow_args)
    #str=nodeTxt設(shè)置要插入的文本狐蜕,xytext為文本及箭頭起始,xy為箭頭終止位置卸夕,因?yàn)橥ㄟ^
    #arrowprops設(shè)置箭頭類型為反方向的层释,因此箭頭實(shí)際指向起始點(diǎn)即文本的位置
    #xycoords及textcoords是設(shè)置坐標(biāo)系標(biāo)準(zhǔn)的,'axes fraction'代表
    # 0,0 是軸域左下角快集,1,1 是右上角贡羔,bbox中的boxstyle屬性為設(shè)置文本外框樣式
    #annotate的詳解請參考matplotlib文檔或者以下博文
    #http://blog.csdn.net/wizardforcel/article/details/54782628
    
def plotTree(myTree,parentpt,nodeTxt): #畫圖主程序
    global xoff,yoff #引入全局變量
    numLeafs = getNumLeafs(myTree) #因?yàn)檫@個函數(shù)要迭代,
    #所以這里重新定義一個獲取葉子節(jié)點(diǎn)個數(shù)的變量
    firstStr = list(myTree.keys())[0]
    cntrpt = (xoff+(1+float(numLeafs))/(2*totalW),yoff)
    #這里首先要理解一個前提个初,就是為了確定整體圖形比較對稱乖寒,當(dāng)前節(jié)點(diǎn)所在的位置,
    #是由他擁有的葉子節(jié)點(diǎn)數(shù)來決定的院溺。比如該節(jié)點(diǎn)下面共有葉子節(jié)點(diǎn)m個楣嘁,那么這m個葉子節(jié)點(diǎn)
    #所占x軸的長度就是m*(1/n),當(dāng)前節(jié)點(diǎn)要居中珍逸,那長度就是(1/2)*(m*(1/n))逐虚,
    #(葉子結(jié)點(diǎn)的位置需要進(jìn)行偏移,但節(jié)點(diǎn)不需要)所以最初有個偏移所以要加回來谆膳,
    #即(1/2)*(m*(1/n))+(1/2)*(1/n)叭爱,合并后即為(1+m)/(2*n),此時的位置為相對位置
    #最后再加上根據(jù)已畫葉子節(jié)點(diǎn)產(chǎn)生的偏移量xoff,就是當(dāng)前節(jié)點(diǎn)最終在x軸上的位置
    plotMidText(cntrpt,parentpt,nodeTxt)
    plotNode(firstStr,cntrpt,parentpt,decisionNode)#畫當(dāng)前節(jié)點(diǎn)和上層節(jié)點(diǎn)的連接線
    secondDict = myTree[firstStr]
    yoff = yoff - 1/totalD #y軸的位置漱病,依據(jù)深度進(jìn)行遞減
    for key in secondDict.keys():
        if type(secondDict[key]) == dict:
            plotTree(secondDict[key],cntrpt,str(key))
        else:
            xoff = xoff + 1/totalW #每畫一個葉子節(jié)點(diǎn)涤伐,偏移量就要加上對應(yīng)的長度
            plotNode(secondDict[key],(xoff,yoff),cntrpt,leafNode)
            plotMidText((xoff,yoff),cntrpt,str(key))
    yoff = yoff + 1/totalD #注意這里馒胆,因?yàn)樯厦娴膄or循環(huán)中是隸屬于某個節(jié)點(diǎn)的,比如同層
    #有節(jié)點(diǎn)A和節(jié)點(diǎn)B凝果,首先在節(jié)點(diǎn)A里面進(jìn)行迭代畫圖,每次迭代yoff都會減1/totalD,因此睦尽,當(dāng)
    #A節(jié)點(diǎn)結(jié)束要處理B節(jié)點(diǎn)的時候器净,yoff的值要相應(yīng)加回來

'''以下代碼定義了幾個其他函數(shù)會用到的全局變量,因此沒有寫入到函數(shù)里面当凡,
你也可以寫到函數(shù)里山害,然后再通過調(diào)用其他函數(shù)時傳遞進(jìn)去'''     
fig=plt.figure(1,facecolor='white')
fig.clf()
axprops = dict(xticks=[],yticks=[])#這兩個參數(shù)設(shè)置xy軸不顯示
axes = plt.subplot(111,frameon=False,**axprops)
totalW = float(getNumLeafs(inTree)) #獲取總的葉子結(jié)點(diǎn)個數(shù)
totalD = float(getTreeDepth(inTree)) #獲取總的深度
xoff = -1/(2*totalW) #設(shè)定x軸上的偏移量,假定樹一共有n個葉子節(jié)點(diǎn)沿量,將x軸等分為n個段浪慌,
#每段的長度就是1/n,為了對稱畫圖朴则,每個葉子節(jié)點(diǎn)在每段的中間位置权纤,所以要整體往左偏移
#即減去(1/2)*(1/n),最初偏移量就是-1/(2*n)乌妒,那么第i個節(jié)點(diǎn)的x的位置就是i*(1/n)-1/(2*n)
#即xoff + i*(1/n)汹想,xoff這個變量后面會根據(jù)已畫的葉子節(jié)點(diǎn)數(shù)變化
yoff = 1

'''用些測試數(shù)據(jù)查看畫圖效果'''
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: 'no'}}}}]
    return listoftrees[i]
inTree = retrieveTree(0)
plotTree(inTree,(0.5,1),'')#這里設(shè)定0.5,1是因?yàn)閜lotTree函數(shù)里面有兩個功能撤蚊,畫當(dāng)前節(jié)點(diǎn)
#以及和上層節(jié)點(diǎn)的連接線古掏,第一層節(jié)點(diǎn)沒有上一層,所以設(shè)定這個值侦啸,使得當(dāng)前節(jié)點(diǎn)和上層節(jié)點(diǎn)畫在一起
plt.show() 

下面將以上構(gòu)建樹及畫圖的函數(shù)應(yīng)用到預(yù)測隱形眼鏡類型上

data = pd.read_table('lense.txt',sep='\t',header=None)
datalist = data.as_matrix().tolist() #將數(shù)據(jù)加載進(jìn)來后槽唾,先轉(zhuǎn)化為矩陣格式,而后轉(zhuǎn)為列表
labelslist = ['age','prescript','astigmatic','tearRate'] #數(shù)據(jù)集里共有四列光涂,設(shè)置每列名稱
lenseTree = createTree(datalist,labelslist)
plotTree(inTree,(0.5,1),'')
plt.show()    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末庞萍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子顶捷,更是在濱河造成了極大的恐慌挂绰,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件服赎,死亡現(xiàn)場離奇詭異葵蒂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)重虑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門践付,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缺厉,你說我怎么就攤上這事永高∷硗粒” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵命爬,是天一觀的道長曹傀。 經(jīng)常有香客問我,道長饲宛,這世上最難降的妖魔是什么皆愉? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮艇抠,結(jié)果婚禮上幕庐,老公的妹妹穿的比我還像新娘。我一直安慰自己家淤,他們只是感情好异剥,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著絮重,像睡著了一般冤寿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绿鸣,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天疚沐,我揣著相機(jī)與錄音,去河邊找鬼潮模。 笑死亮蛔,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的擎厢。 我是一名探鬼主播究流,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼动遭!你這毒婦竟也來了芬探?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤厘惦,失蹤者是張志新(化名)和其女友劉穎偷仿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宵蕉,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡酝静,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了羡玛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片别智。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖稼稿,靈堂內(nèi)的尸體忽然破棺而出薄榛,到底是詐尸還是另有隱情讳窟,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布敞恋,位于F島的核電站丽啡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏耳舅。R本人自食惡果不足惜碌上,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望浦徊。 院中可真熱鬧,春花似錦天梧、人聲如沸盔性。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冕香。三九已至,卻和暖如春后豫,著一層夾襖步出監(jiān)牢的瞬間悉尾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工挫酿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留构眯,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓早龟,卻偏偏與公主長得像惫霸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子葱弟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容