python實現機器學習之決策樹

這幾天在看決策樹算法,發現這算法在實際的應用中使用挺多的。所以想總結一下:
這裏給出一些我覺得比較好的博客鏈接:
http://blog.jobbole.com/86443/ 通俗易懂,同時也講了一些決策樹算法:如ID3、C4.5之類的。以及建立完完整的決策樹之後,爲了防止過擬合而進行的剪枝的操作。
決策樹算法介紹及應用:http://blog.jobbole.com/89072/ 這篇博文寫的非常的好,主要是將數據挖掘導論這本數的第四章給總結了一番。主要講解了決策樹的建立,決策樹的評估以及最後使用R語言和SPASS來進行決策樹的建模。
數據挖掘(6):決策樹分類算法:http://blog.jobbole.com/90165/ 這篇博客將分類寫的很詳細。並且列舉了一個不同人羣,不同信貸購買電腦的例子。可惜沒有實現代碼。


在這裏我在讀完這3篇博文之後,給出了用python語言和sklearn包的完整實現的例子。


    決策樹(decision tree)是一個樹結構(可以是二叉樹或非二叉樹)。其每個非葉節點表示一個特徵屬性上的測試,每個分支代表這個特徵屬性在某個值域上的輸出,而每個葉節點存放一個類別。使用決策樹進行決策的過程就是從根節點開始,測試待分類項中相應的特徵屬性,並按照其值選擇輸出分支,直到到達葉子節點,將葉子節點存放的類別作爲決策結果。
決策樹的一般流程:
1、收集數據:可以使用任何的方法
2、準備數據:樹構造算法只適用標稱型數據,因此數值型的數據必須離散化。
3、分析數據:
4、訓練算法:構造樹的數據結構
5、測試算法:使用經驗樹計算錯誤率
6、使用算法




這裏給的例子如下:(注意:這裏使用的是ID3)

記錄ID 年齡 輸入層次 學生 信用等級 是否購買電腦
1 青少年  否   一般    否
2 青少年  否   良好    否
3 中年  否 一般  是
4 老年  否   一般    是
5 老年  是 一般    是
6 老年  是 良好  否
7 中年  是 良好    是
8 青少年  否 一般  否
9 青少年  是 一般  是
10 老年  是 一般    是
11 青少年  是 良好    是
12 中年  否 良好    是
13 中年  是 一般    是
14 老年  否 良好    否

這裏我將其轉換爲特徵向量的形式如下:


dataSet = [[1, 3, 0, 1, 'no'],
               [1, 3, 0, 2, 'no'],
               [2, 3, 0, 1, 'yes'],
               [3, 2, 0, 1, 'yes'],
               [3, 1, 1, 1, 'yes'],
               [3, 1, 1, 2, 'no'],
               [2, 1, 1, 2, 'yes'],
               [1, 2, 0, 1, 'no'],
               [1, 1, 1, 1, 'yes'],
               [3, 2, 1, 1, 'yes'],
               [1, 2, 1, 2, 'yes'],
               [2, 2, 0, 2, 'yes'],
               [2, 3, 0, 1, 'yes'],
               [3, 2, 0, 2, 'no'],
               ]
labels = ['age','salary','isStudent','credit']

完整的實現決策樹的代碼:

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


from math import log
import operator


def createDataSet():
    dataSet = [[1, 3, 0, 1, 'no'],
               [1, 3, 0, 2, 'no'],
               [2, 3, 0, 1, 'yes'],
               [3, 2, 0, 1, 'yes'],
               [3, 1, 1, 1, 'yes'],
               [3, 1, 1, 2, 'no'],
               [2, 1, 1, 2, 'yes'],
               [1, 2, 0, 1, 'no'],
               [1, 1, 1, 1, 'yes'],
               [3, 2, 1, 1, 'yes'],
               [1, 2, 1, 2, 'yes'],
               [2, 2, 0, 2, 'yes'],
               [2, 3, 0, 1, 'yes'],
               [3, 2, 0, 2, 'no'],
               ]
    labels = ['age','salary','isStudent','credit']

    #change to discrete values
    return dataSet, labels
############計算香農熵###############
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)#計算實例的總數
    labelCounts = {}#創建一個數據字典,它的key是最後把一列的數值(即標籤),value記錄當前類型(即標籤)出現的次數
    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

  
#########按給定的特徵劃分數據#########
def splitDataSet(dataSet, axis, value): #axis表示特徵的索引  value是返回的特徵值
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]     #抽取除axis特徵外的所有的記錄的內容
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

#######遍歷整個數據集,選擇最好的數據集劃分方式########    
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      #獲取當前實例的特徵個數,一般最後一列是標籤。the last column is used for the labels
    baseEntropy = calcShannonEnt(dataSet)#計算當前實例的香農熵
    bestInfoGain = 0.0; bestFeature = -1#這裏初始化最佳的信息增益和最佳的特徵
    for i in range(numFeatures):        #遍歷每一個特徵 iterate over all the features
        featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
        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         #if better than current best, set to best
            bestFeature = i
    return bestFeature,bestInfoGain                      #返回最佳劃分的特徵索引和信息增益

'''該函數使用分類名稱的列表,然後創建鍵值爲classList中唯一值的數據字典。字典
對象的存儲了classList中每個類標籤出現的頻率。最後利用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]#stop splitting when all of the classes are equal
    if len(dataSet[0]) == 1: #遍歷完所有的特徵時,仍然不能將數據集劃分成僅包含唯一類別的分組
        return majorityCnt(classList)#由於無法簡單的返回唯一的類標籤,這裏就返回出現次數最多的類別作爲返回值
    bestFeat,bestInfogain= chooseBestFeatureToSplit(dataSet)#獲取最好的分類特徵索引
    bestFeatLabel = labels[bestFeat]#獲取該特徵的名稱
    
    #這裏直接使用字典變量來存儲樹信息,這對於繪製樹形圖很重要。
    myTree = {bestFeatLabel:{}}#當前數據集選取最好的特徵存儲在bestFeat中
    del(labels[bestFeat])#刪除已經在選取的特徵
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]       #copy all of labels, so trees don't mess up existing labels
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree                            
    
def classify(inputTree,featLabels,testVec):
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    if isinstance(valueOfFeat, dict): 
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    return classLabel

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)



import matplotlib.pyplot as plt

decisionNode = dict(boxstyle="sawtooth", fc="0.8") #定義文本框與箭頭的格式
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")

def getNumLeafs(myTree):#獲取樹節點的數目
    numLeafs = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():#測試節點的數據類型是不是字典,如果是則就需要遞歸的調用getNumLeafs()函數
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
            numLeafs += getNumLeafs(secondDict[key])
        else:   numLeafs +=1
    return numLeafs

def getTreeDepth(myTree):#獲取樹節點的樹的層數
    maxDepth = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth
    return maxDepth

def plotNode(nodeTxt, centerPt, parentPt, nodeType): #繪製帶箭頭的註釋
    createPlot.ax1.annotate(nodeTxt, xy=parentPt,  xycoords='axes fraction',#createPlot.ax1會提供一個繪圖區
             xytext=centerPt, textcoords='axes fraction',
             va="center", ha="center", bbox=nodeType, arrowprops=arrow_args )
    
def plotMidText(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, va="center", ha="center", rotation=30)

def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
    numLeafs = getNumLeafs(myTree)  #首先計算樹的寬和高
    depth = getTreeDepth(myTree)
    firstStr = myTree.keys()[0]     #the text label for this node should be this
    cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
    plotMidText(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':#test to see if the nodes are dictonaires, if not they are leaf nodes   
            plotTree(secondDict[key],cntrPt,str(key))        #recursion
        else:   #it's a leaf node print the leaf node
            plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
            plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
            plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
    plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
#if you do get a dictonary you know it's a tree, and the first element will be another dict
# 
def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)    #no ticks
    #createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses 
    plotTree.totalW = float(getNumLeafs(inTree))#c存儲樹的寬度
    plotTree.totalD = float(getTreeDepth(inTree))#存儲樹的深度。我們使用這兩個變量計算樹節點的擺放位置
    plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
    plotTree(inTree, (0.5,1.0), '')
    plt.show()

# def createPlot():
#     fig = plt.figure(1, facecolor='white')
#     fig.clf()
#     createPlot.ax1 = plt.subplot(111, frameon=False) #創建一個新圖形,並清空繪圖區
#     plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)#然後在繪圖區上繪製兩個代表不同類型的樹節點
#     plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
#     plt.show()

測試代碼:

#########測試代碼#############
myDat,labels=createDataSet()
print calcShannonEnt(myDat)  
print myDat
 
bestfeature,bestInfogain=chooseBestFeatureToSplit(myDat)   
print bestfeature,bestInfogain
myTree=createTree(myDat, labels)
print myTree
print getNumLeafs(myTree)
print getTreeDepth(myTree)
createPlot(myTree)
##########測試結束############# 
結果顯示:
0.940285958671
[[1, 3, 0, 1, 'no'], [1, 3, 0, 2, 'no'], [2, 3, 0, 1, 'yes'], [3, 2, 0, 1, 'yes'], [3, 1, 1, 1, 'yes'], [3, 1, 1, 2, 'no'], [2, 1, 1, 2, 'yes'], [1, 2, 0, 1, 'no'], [1, 1, 1, 1, 'yes'], [3, 2, 1, 1, 'yes'], [1, 2, 1, 2, 'yes'], [2, 2, 0, 2, 'yes'], [2, 3, 0, 1, 'yes'], [3, 2, 0, 2, 'no']]
0 0.246749819774
{'age': {1: {'isStudent': {0: 'no', 1: 'yes'}}, 2: 'yes', 3: {'credit': {1: 'yes', 2: 'no'}}}}
5
2

畫出決策樹圖:
決策樹1.png


接下來我打算使用python的scikit-learn機器學習算法包來實現上面的決策樹:

#-*- coding:utf-8 -*-
from sklearn.datasets import load_iris
from sklearn import tree
dataSet = [[1, 3, 0, 1, 'no'],
               [1, 3, 0, 2, 'no'],
               [2, 3, 0, 1, 'yes'],
               [3, 2, 0, 1, 'yes'],
               [3, 1, 1, 1, 'yes'],
               [3, 1, 1, 2, 'no'],
               [2, 1, 1, 2, 'yes'],
               [1, 2, 0, 1, 'no'],
               [1, 1, 1, 1, 'yes'],
               [3, 2, 1, 1, 'yes'],
               [1, 2, 1, 2, 'yes'],
               [2, 2, 0, 2, 'yes'],
               [2, 3, 0, 1, 'yes'],
               [3, 2, 0, 2, 'no'],
               ]
labels = ['age','salary','isStudent','credit']
from sklearn.cross_validation import train_test_split  #這裏是引用了交叉驗證 

FeatureSet=[]
Label=[]
for i in dataSet:
    FeatureSet.append(i[:-1])
    Label.append(i[-1])
X_train,X_test, y_train, y_test = train_test_split(FeatureSet, Label, random_state=1)#將數據隨機分成訓練集和測試集
print X_train
print X_test
print y_train
print y_test
#print iris
clf = tree.DecisionTreeClassifier()
clf = clf.fit(X_train, y_train)
from sklearn.externals.six import StringIO
with open("isBuy.dot", 'w') as f:
    f = tree.export_graphviz(clf, out_file=f)
import os
os.unlink('isBuy.dot')
#  
from sklearn.externals.six import StringIO  
import pydot #注意要安裝pydot2這個python插件。否則會報錯。
dot_data = StringIO() 
tree.export_graphviz(clf, out_file=dot_data) 
graph = pydot.graph_from_dot_data(dot_data.getvalue()) 
graph.write_pdf("isBuy.pdf") #將決策樹以pdf格式輸出
  
pre_labels=clf.predict(X_test)
print pre_labels


結果顯示如下:
[[1, 2, 1, 2], [3, 1, 1, 1], [1, 3, 0, 2], [2, 3, 0, 1], [1, 3, 0, 1], [3, 2, 0, 2], [3, 2, 1, 1], [1, 1, 1, 1], [2, 2, 0, 2], [3, 1, 1, 2]]
[[3, 2, 0, 1], [1, 2, 0, 1], [2, 1, 1, 2], [2, 3, 0, 1]]
['yes', 'yes', 'no', 'yes', 'no', 'no', 'yes', 'yes', 'yes', 'no']
['yes', 'no', 'yes', 'yes']
['yes' 'yes' 'yes' 'yes']

由最後兩行我們可以發現第二個結果預測錯誤了。但是這並不能說明我們的決策樹不夠好。相反我覺得這樣反而不會出現過擬合的現象。當然也不是絕對的,如果過擬合的化,就需要考慮剪枝了。這是後話了。
決策樹的圖片如下:
圖片中的X[3]表示的是labels = ['age','salary','isStudent','credit']中的索引爲3的特徵。
該圖將會保存在isBuy.pd 








發佈了45 篇原創文章 · 獲贊 31 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章