1. 程式人生 > >機器學習實戰教程(三):決策樹實戰篇之為自己配個隱形眼鏡

機器學習實戰教程(三):決策樹實戰篇之為自己配個隱形眼鏡

原文連結:cuijiahua.com/blog/2017/1…

一、前言

上篇文章機器學習實戰教程(二):決策樹基礎篇之讓我們從相親說起講述了機器學習決策樹的原理,以及如何選擇最優特徵作為分類特徵。本篇文章將在此基礎上進行介紹。主要包括:

  • 決策樹構建
  • 決策樹視覺化
  • 使用決策樹進行分類預測
  • 決策樹的儲存和讀取
  • sklearn實戰之預測隱形眼睛型別

二、決策樹構建

上篇文章也粗略提到過,構建決策樹的演算法有很多。篇幅原因,本篇文章只使用ID3演算法構建決策樹。

1、ID3演算法

ID3演算法的核心是在決策樹各個結點上對應資訊增益準則選擇特徵,遞迴地構建決策樹。具體方法是:從根結點(root node)開始,對結點計算所有可能的特徵的資訊增益,選擇資訊增益最大的特徵作為結點的特徵,由該特徵的不同取值建立子節點;再對子結點遞迴地呼叫以上方法,構建決策樹;直到所有特徵的資訊增益均很小或沒有特徵可以選擇為止。最後得到一個決策樹。ID3相當於用極大似然法進行概率模型的選擇。

在使用ID3構造決策樹之前,我們再分析下資料。

利用上篇文章求得的結果,由於特徵A3(有自己的房子)的資訊增益值最大,所以選擇特徵A3作為根結點的特徵。它將訓練集D劃分為兩個子集D1(A3取值為"是")和D2(A3取值為"否")。由於D1只有同一類的樣本點,所以它成為一個葉結點,結點的類標記為“是”。

對D2則需要從特徵A1(年齡),A2(有工作)和A4(信貸情況)中選擇新的特徵,計算各個特徵的資訊增益:

根據計算,選擇資訊增益最大的特徵A2(有工作)作為結點的特徵。由於A2有兩個可能取值,從這一結點引出兩個子結點:一個對應"是"(有工作)的子結點,包含3個樣本,它們屬於同一類,所以這是一個葉結點,類標記為"是";另一個是對應"否"(無工作)的子結點,包含6個樣本,它們也屬於同一類,所以這也是一個葉結點,類標記為"否"。

這樣就生成了一個決策樹,該決策樹只用了兩個特徵(有兩個內部結點),生成的決策樹如下圖所示。

這樣我們就使用ID3演算法構建出來了決策樹,接下來,讓我們看看如何進行代實現。

2、編寫程式碼構建決策樹

我們使用字典儲存決策樹的結構,比如上小節我們分析出來的決策樹,用字典可以表示為:

{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
複製程式碼

建立函式majorityCnt統計classList中出現此處最多的元素(類標籤),建立函式createTree用來遞迴構建決策樹。編寫程式碼如下:

# -*- coding: UTF-8 -*-
from math import log import operator """ 函式說明:計算給定資料集的經驗熵(夏農熵) Parameters: dataSet - 資料集 Returns: shannonEnt - 經驗熵(夏農熵) Author: Jack Cui Blog: http://blog.csdn.net/c406495762 Modify: 2017-07-24 """ def calcShannonEnt(dataSet): numEntires = len(dataSet) #返回資料集的行數 labelCounts = {} #儲存每個標籤(Label)出現次數的字典 for featVec in dataSet: #對每組特徵向量進行統計 currentLabel = featVec[-1] #提取標籤(Label)資訊 if currentLabel not in labelCounts.keys(): #如果標籤(Label)沒有放入統計次數的字典,新增進去 labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 #Label計數 shannonEnt = 0.0 #經驗熵(夏農熵) for key in labelCounts: #計算夏農熵 prob = float(labelCounts[key]) / numEntires #選擇該標籤(Label)的概率 shannonEnt -= prob * log(prob, 2) #利用公式計算 return shannonEnt #返回經驗熵(夏農熵) """ 函式說明:建立測試資料集 Parameters: 無 Returns: dataSet - 資料集 labels - 特徵標籤 Author: Jack Cui Blog: http://blog.csdn.net/c406495762 Modify: 2017-07-20 """ def createDataSet(): dataSet = [[0, 0, 0, 0, 'no'], #資料集 [0, 0, 0, 1, 'no'], [0, 1, 0, 1, 'yes'], [0, 1, 1, 0, 'yes'], [0, 0, 0, 0, 'no'], [1, 0, 0, 0, 'no'], [1, 0, 0, 1, 'no'], [1, 1, 1, 1, 'yes'], [1, 0, 1, 2, 'yes'], [1, 0, 1, 2, 'yes'], [2, 0, 1, 2, 'yes'], [2, 0, 1, 1, 'yes'], [2, 1, 0, 1, 'yes'], [2, 1, 0, 2, 'yes'], [2, 0, 0, 0, 'no']] labels = ['年齡', '有工作', '有自己的房子', '信貸情況'] #特徵標籤 return dataSet, labels #返回資料集和分類屬性 """ 函式說明:按照給定特徵劃分資料集 Parameters: dataSet - 待劃分的資料集 axis - 劃分資料集的特徵 value - 需要返回的特徵的值 Returns: 無 Author: Jack Cui Blog: http://blog.csdn.net/c406495762 Modify: 2017-07-24 """ def splitDataSet(dataSet, 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 #返回劃分後的資料集 """ 函式說明:選擇最優特徵 Parameters: dataSet - 資料集 Returns: bestFeature - 資訊增益最大的(最優)特徵的索引值 Author: Jack Cui Blog: http://blog.csdn.net/c406495762 Modify: 2017-07-20 """ def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 #特徵數量 baseEntropy = calcShannonEnt(dataSet) #計算資料集的夏農熵 bestInfoGain = 0.0 #資訊增益 bestFeature = -1 #最優特徵的索引值 for i in range(numFeatures): #遍歷所有特徵 #獲取dataSet的第i個所有特徵 featList = [example[i] for example in dataSet] uniqueVals = set(featList) #建立set集合{},元素不可重複 newEntropy = 0.0 #經驗條件熵 for value in uniqueVals: #計算資訊增益 subDataSet = splitDataSet(dataSet, i, value) #subDataSet劃分後的子集 prob = len(subDataSet) / float(len(dataSet)) #計運算元集的概率 newEntropy += prob * calcShannonEnt(subDataSet) #根據公式計算經驗條件熵 infoGain = baseEntropy - newEntropy #資訊增益 # print("第%d個特徵的增益為%.3f" % (i, infoGain)) #列印每個特徵的資訊增益 if (infoGain > bestInfoGain): #計算資訊增益 bestInfoGain = infoGain #更新資訊增益,找到最大的資訊增益 bestFeature = i #記錄資訊增益最大的特徵的索引值 return bestFeature #返回資訊增益最大的特徵的索引值 """ 函式說明:統計classList中出現此處最多的元素(類標籤) Parameters: classList - 類標籤列表 Returns: sortedClassCount[0][0] - 出現此處最多的元素(類標籤) Author: Jack Cui Blog: http://blog.csdn.net/c406495762 Modify: 2017-07-24 """ def majorityCnt(classList): classCount = {} for vote in classList: #統計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] #返回classList中出現次數最多的元素 """ 函式說明:建立決策樹 Parameters: dataSet - 訓練資料集 labels - 分類屬性標籤 featLabels - 儲存選擇的最優特徵標籤 Returns: myTree - 決策樹 Author: Jack Cui Blog: http://blog.csdn.net/c406495762 Modify: 2017-07-25 """ def createTree(dataSet, labels, featLabels): classList = [example[-1] for example in dataSet] #取分類標籤(是否放貸:yes or no) if classList.count(classList[0]) == len(classList): #如果類別完全相同則停止繼續劃分 return classList[0] if len(dataSet[0]) == 1 or len(labels) == 0: #遍歷完所有特徵時返回出現次數最多的類標籤 return majorityCnt(classList) bestFeat = chooseBestFeatureToSplit(dataSet) #選擇最優特徵 bestFeatLabel = labels[bestFeat] #最優特徵的標籤 featLabels.append(bestFeatLabel) myTree = {bestFeatLabel:{}} #根據最優特徵的標籤生成樹 del(labels[bestFeat]) #刪除已經使用特徵標籤 featValues = [example[bestFeat] for example in dataSet] #得到訓練集中所有最優特徵的屬性值 uniqueVals = set(featValues) #去掉重複的屬性值 for value in uniqueVals: #遍歷特徵,建立決策樹。 myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels) return myTree if __name__ == '__main__': dataSet, labels = createDataSet() featLabels = [] myTree = createTree(dataSet, labels, featLabels) print(myTree) 複製程式碼

遞迴建立決策樹時,遞迴有兩個終止條件:第一個停止條件是所有的類標籤完全相同,則直接返回該類標籤;第二個停止條件是使用完了所有特徵,仍然不能將資料劃分僅包含唯一類別的分組,即決策樹構建失敗,特徵不夠用。此時說明資料緯度不夠,由於第二個停止條件無法簡單地返回唯一的類標籤,這裡挑選出現數量最多的類別作為返回值。

執行上述程式碼,我們可以看到如下結果:

可見,我們的決策樹已經構建完成了。這時候,有的朋友可能會說,這個決策樹看著好彆扭,雖然這個能看懂,但是如果多點的結點,就不好看了。能直觀點嗎?完全沒有問題,我們可以使用強大的Matplotlib繪製決策樹。

三、決策樹視覺化

這裡程式碼都是關於Matplotlib的,如果對於Matplotlib不瞭解的,可以先學習下,Matplotlib的內容這裡就不再累述。視覺化需要用到的函式:

  • getNumLeafs:獲取決策樹葉子結點的數目
  • getTreeDepth:獲取決策樹的層數
  • plotNode:繪製結點
  • plotMidText:標註有向邊屬性值
  • plotTree:繪製決策樹
  • createPlot:建立繪製面板

我對視覺化決策樹的程式進行了詳細的註釋,直接看程式碼,除錯檢視即可。為了顯示中文,需要設定FontProperties,程式碼編寫如下:

# -*- coding: UTF-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
from math import log
import operator
 
"""
函式說明:計算給定資料集的經驗熵(夏農熵)
 
Parameters:
    dataSet - 資料集
Returns:
    shannonEnt - 經驗熵(夏農熵)
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def calcShannonEnt(dataSet):
    numEntires = len(dataSet)                        #返回資料集的行數
    labelCounts = {}                                #儲存每個標籤(Label)出現次數的字典
    for featVec in dataSet:                            #對每組特徵向量進行統計
        currentLabel = featVec[-1]                    #提取標籤(Label)資訊
        if currentLabel not in labelCounts.keys():    #如果標籤(Label)沒有放入統計次數的字典,新增進去
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1                #Label計數
    shannonEnt = 0.0                                #經驗熵(夏農熵)
    for key in labelCounts:                            #計算夏農熵
        prob = float(labelCounts[key]) / numEntires    #選擇該標籤(Label)的概率
        shannonEnt -= prob * log(prob, 2)            #利用公式計算
    return shannonEnt                                #返回經驗熵(夏農熵)
 
"""
函式說明:建立測試資料集
 
Parameters:
    無
Returns:
    dataSet - 資料集
    labels - 特徵標籤
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-20
"""
def createDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],                        #資料集
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['年齡', '有工作', '有自己的房子', '信貸情況']        #特徵標籤
    return dataSet, labels                             #返回資料集和分類屬性
 
"""
函式說明:按照給定特徵劃分資料集
 
Parameters:
    dataSet - 待劃分的資料集
    axis - 劃分資料集的特徵
    value - 需要返回的特徵的值
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def splitDataSet(dataSet, 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                                      #返回劃分後的資料集
 
"""
函式說明:選擇最優特徵
 
Parameters:
    dataSet - 資料集
Returns:
    bestFeature - 資訊增益最大的(最優)特徵的索引值
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-20
"""
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1                    #特徵數量
    baseEntropy = calcShannonEnt(dataSet)                 #計算資料集的夏農熵
    bestInfoGain = 0.0                                  #資訊增益
    bestFeature = -1                                    #最優特徵的索引值
    for i in range(numFeatures):                         #遍歷所有特徵
        #獲取dataSet的第i個所有特徵
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)                         #建立set集合{},元素不可重複
        newEntropy = 0.0                                  #經驗條件熵
        for value in uniqueVals:                         #計算資訊增益
            subDataSet = splitDataSet(dataSet, i, value)         #subDataSet劃分後的子集
            prob = len(subDataSet) / float(len(dataSet))           #計運算元集的概率
            newEntropy += prob * calcShannonEnt(subDataSet)     #根據公式計算經驗條件熵
        infoGain = baseEntropy - newEntropy                     #資訊增益
        # print("第%d個特徵的增益為%.3f" % (i, infoGain))            #列印每個特徵的資訊增益
        if (infoGain > bestInfoGain):                             #計算資訊增益
            bestInfoGain = infoGain                             #更新資訊增益,找到最大的資訊增益
            bestFeature = i                                     #記錄資訊增益最大的特徵的索引值
    return bestFeature                                             #返回資訊增益最大的特徵的索引值
 
 
"""
函式說明:統計classList中出現此處最多的元素(類標籤)
 
Parameters:
    classList - 類標籤列表
Returns:
    sortedClassCount[0][0] - 出現此處最多的元素(類標籤)
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def majorityCnt(classList):
    classCount = {}
    for vote in classList:                                        #統計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]                                #返回classList中出現次數最多的元素
 
"""
函式說明:建立決策樹
 
Parameters:
    dataSet - 訓練資料集
    labels - 分類屬性標籤
    featLabels - 儲存選擇的最優特徵標籤
Returns:
    myTree - 決策樹
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-25
"""
def createTree(dataSet, labels, featLabels):
    classList = [example[-1] for example in dataSet]            #取分類標籤(是否放貸:yes or no)
    if classList.count(classList[0]) == len(classList):            #如果類別完全相同則停止繼續劃分
        return classList[0]
    if len(dataSet[0]) == 1 or len(labels) == 0:                                    #遍歷完所有特徵時返回出現次數最多的類標籤
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)                #選擇最優特徵
    bestFeatLabel = labels[bestFeat]                            #最優特徵的標籤
    featLabels.append(bestFeatLabel)
    myTree = {bestFeatLabel:{}}                                    #根據最優特徵的標籤生成樹
    del(labels[bestFeat])                                        #刪除已經使用特徵標籤
    featValues = [example[bestFeat] for example in dataSet]        #得到訓練集中所有最優特徵的屬性值
    uniqueVals = set(featValues)                                #去掉重複的屬性值
    for value in uniqueVals:                                    #遍歷特徵,建立決策樹。                       
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
    return myTree
 
"""
函式說明:獲取決策樹葉子結點的數目
 
Parameters:
    myTree - 決策樹
Returns:
    numLeafs - 決策樹的葉子結點的數目
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def getNumLeafs(myTree):
    numLeafs = 0                                                #初始化葉子
    firstStr = next(iter(myTree))                                #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結點屬性,可以使用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
 
"""
函式說明:獲取決策樹的層數
 
Parameters:
    myTree - 決策樹
Returns:
    maxDepth - 決策樹的層數
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def getTreeDepth(myTree):
    maxDepth = 0                                                #初始化決策樹深度
    firstStr = next(iter(myTree))                                #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法獲取結點屬性,可以使用list(myTree.keys())[0]
    secondDict = myTree[firstStr]                                #獲取下一個字典
    for key in secondDict.keys():
        if type(secondDict[key]).__name__=='dict':                #測試該結點是否為字典,如果不是字典,代表此結點為葉子結點
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else:   thisDepth = 1
        if thisDepth > maxDepth: maxDepth = thisDepth            #更新層數
    return maxDepth
 
"""
函式說明:繪製結點
 
Parameters:
    nodeTxt - 結點名
    centerPt - 文字位置
    parentPt - 標註的箭頭位置
    nodeType - 結點格式
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    arrow_args = dict(arrowstyle="<-")                                            #定義箭頭格式
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)        #設定中文字型
    createPlot.ax1.annotate(nodeTxt, xy=parentPt,  xycoords='axes fraction',    #繪製結點
        xytext=centerPt, textcoords='axes fraction',
        va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font)
 
"""
函式說明:標註有向邊屬性值
 
Parameters:
    cntrPt、parentPt - 用於計算標註位置
    txtString - 標註的內容
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
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)
 
"""
函式說明:繪製決策樹
 
Parameters:
    myTree - 決策樹(字典)
    parentPt - 標註的內容
    nodeTxt - 結點名
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def plotTree(myTree, parentPt, nodeTxt):
    decisionNode = dict(boxstyle="sawtooth", fc="0.8")                                        #設定結點格式
    leafNode = dict(boxstyle="round4", fc="0.8")                                            #設定葉結點格式
    numLeafs = getNumLeafs(myTree)                                                          #獲取決策樹葉結點數目,決定了樹的寬度
    depth = getTreeDepth(myTree)                                                            #獲取決策樹層數
    firstStr = next(iter(myTree))                                                            #下個字典                                                 
    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                                        #y偏移
    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)
            plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
    plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
 
"""
函式說明:建立繪製面板
 
Parameters:
    inTree - 決策樹(字典)
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def createPlot(inTree):
    fig = plt.figure(1, facecolor='white')                                                    #建立fig
    fig.clf()                                                                                #清空fig
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)                                #去掉x、y軸
    plotTree.totalW = float(getNumLeafs(inTree))                                            #獲取決策樹葉結點數目
    plotTree.totalD = float(getTreeDepth(inTree))                                            #獲取決策樹層數
    plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;                                #x偏移
    plotTree(inTree, (0.5,1.0), '')                                                            #繪製決策樹
    plt.show()                                                                                 #顯示繪製結果     
 
if __name__ == '__main__':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    print(myTree)  
    createPlot(myTree)
複製程式碼

不出意外的話,我們就可以得到如下結果,可以看到決策樹繪製完成。plotNode函式的工作就是繪製各個結點,比如有自己的房子有工作yesno,包括內結點和葉子結點。plotMidText函式的工作就是繪製各個有向邊的屬性,例如各個有向邊的01。這部分內容呢,個人感覺可以選擇性掌握,能掌握最好,不能掌握可以放一放,因為後面會介紹一個更簡單的決策樹視覺化方法。看到這句話,是不是想偷懶不仔細看這部分的程式碼了?(눈_눈)

四、使用決策樹執行分類

依靠訓練資料構造了決策樹之後,我們可以將它用於實際資料的分類。在執行資料分類時,需要決策樹以及用於構造樹的標籤向量。然後,程式比較測試資料與決策樹上的數值,遞迴執行該過程直到進入葉子結點;最後將測試資料定義為葉子結點所屬的型別。在構建決策樹的程式碼,可以看到,有個featLabels引數。它是用來幹什麼的?它就是用來記錄各個分類結點的,在用決策樹做預測的時候,我們按順序輸入需要的分類結點的屬性值即可。舉個例子,比如我用上述已經訓練好的決策樹做分類,那麼我只需要提供這個人是否有房子,是否有工作這兩個資訊即可,無需提供冗餘的資訊。

用決策樹做分類的程式碼很簡單,編寫程式碼如下:

# -*- coding: UTF-8 -*-
from math import log
import operator
 
"""
函式說明:計算給定資料集的經驗熵(夏農熵)
 
Parameters:
    dataSet - 資料集
Returns:
    shannonEnt - 經驗熵(夏農熵)
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def calcShannonEnt(dataSet):
    numEntires = len(dataSet)                        #返回資料集的行數
    labelCounts = {}                                #儲存每個標籤(Label)出現次數的字典
    for featVec in dataSet:                            #對每組特徵向量進行統計
        currentLabel = featVec[-1]                    #提取標籤(Label)資訊
        if currentLabel not in labelCounts.keys():    #如果標籤(Label)沒有放入統計次數的字典,新增進去
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1                #Label計數
    shannonEnt = 0.0                                #經驗熵(夏農熵)
    for key in labelCounts:                            #計算夏農熵
        prob = float(labelCounts[key]) / numEntires    #選擇該標籤(Label)的概率
        shannonEnt -= prob * log(prob, 2)            #利用公式計算
    return shannonEnt                                #返回經驗熵(夏農熵)
 
"""
函式說明:建立測試資料集
 
Parameters:
    無
Returns:
    dataSet - 資料集
    labels - 特徵標籤
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-20
"""
def createDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],                        #資料集
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['年齡', '有工作', '有自己的房子', '信貸情況']        #特徵標籤
    return dataSet, labels                             #返回資料集和分類屬性
 
"""
函式說明:按照給定特徵劃分資料集
 
Parameters:
    dataSet - 待劃分的資料集
    axis - 劃分資料集的特徵
    value - 需要返回的特徵的值
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def splitDataSet(dataSet, 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                                      #返回劃分後的資料集
 
"""
函式說明:選擇最優特徵
 
Parameters:
    dataSet - 資料集
Returns:
    bestFeature - 資訊增益最大的(最優)特徵的索引值
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-20
"""
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1                    #特徵數量
    baseEntropy = calcShannonEnt(dataSet)                 #計算資料集的夏農熵
    bestInfoGain = 0.0                                  #資訊增益
    bestFeature = -1                                    #最優特徵的索引值
    for i in range(numFeatures):                         #遍歷所有特徵
        #獲取dataSet的第i個所有特徵
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)                         #建立set集合{},元素不可重複
        newEntropy = 0.0                                  #經驗條件熵
        for value in uniqueVals:                         #計算資訊增益
            subDataSet = splitDataSet(dataSet, i, value)         #subDataSet劃分後的子集
            prob = len(subDataSet) / float(len(dataSet))           #計運算元集的概率
            newEntropy += prob * calcShannonEnt(subDataSet)     #根據公式計算經驗條件熵
        infoGain = baseEntropy - newEntropy                     #資訊增益
        # print("第%d個特徵的增益為%.3f" % (i, infoGain))            #列印每個特徵的資訊增益
        if (infoGain > bestInfoGain):                             #計算資訊增益
            bestInfoGain = infoGain                             #更新資訊增益,找到最大的資訊增益
            bestFeature = i                                     #記錄資訊增益最大的特徵的索引值
    return bestFeature                                             #返回資訊增益最大的特徵的索引值
 
 
"""
函式說明:統計classList中出現此處最多的元素(類標籤)
 
Parameters:
    classList - 類標籤列表
Returns:
    sortedClassCount[0][0] - 出現此處最多的元素(類標籤)
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-24
"""
def majorityCnt(classList):
    classCount = {}
    for vote in classList:                                        #統計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]                                #返回classList中出現次數最多的元素
 
"""
函式說明:建立決策樹
 
Parameters:
    dataSet - 訓練資料集
    labels - 分類屬性標籤
    featLabels - 儲存選擇的最優特徵標籤
Returns:
    myTree - 決策樹
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-25
"""
def createTree(dataSet, labels, featLabels):
    classList = [example[-1] for example in dataSet]            #取分類標籤(是否放貸:yes or no)
    if classList.count(classList[0]) == len(classList):            #如果類別完全相同則停止繼續劃分
        return classList[0]
    if len(dataSet[0]) == 1 or len(labels) == 0:                                    #遍歷完所有特徵時返回出現次數最多的類標籤
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)                #選擇最優特徵
    bestFeatLabel = labels[bestFeat]                            #最優特徵的標籤
    featLabels.append(bestFeatLabel)
    myTree = {bestFeatLabel:{}}                                    #根據最優特徵的標籤生成樹
    del(labels[bestFeat])                                        #刪除已經使用特徵標籤
    featValues = [example[bestFeat] for example in dataSet]        #得到訓練集中所有最優特徵的屬性值
    uniqueVals = set(featValues)                                #去掉重複的屬性值
    for value in uniqueVals:                                    #遍歷特徵,建立決策樹。                       
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
    return myTree
 
"""
函式說明:使用決策樹分類
 
Parameters:
    inputTree - 已經生成的決策樹
    featLabels - 儲存選擇的最優特徵標籤
    testVec - 測試資料列表,順序對應最優特徵標籤
Returns:
    classLabel - 分類結果
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-25
"""
def classify(inputTree, featLabels, testVec):
    firstStr = next(iter(inputTree))                                                        #獲取決策樹結點
    secondDict = inputTree[firstStr]                                                        #下一個字典
    featIndex = featLabels.index(firstStr)                                               
    for key in secondDict.keys():
        if testVec[featIndex] == key:
            if type(secondDict[key]).__name__ == 'dict':
                classLabel = classify(secondDict[key], featLabels, testVec)
            else: classLabel = secondDict[key]
    return classLabel
 
if __name__ == '__main__':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    testVec = [0,1]                                        #測試資料
    result = classify(myTree, featLabels, testVec)
    if result == 'yes':
        print('放貸')
    if result == 'no':
        print('不放貸')
複製程式碼

這裡只增加了classify函式,用於決策樹分類。輸入測試資料[0,1],它代表沒有房子,但是有工作,分類結果如下所示:

看到這裡,細心的朋友可能就會問了,每次做預測都要訓練一次決策樹?這也太麻煩了吧?有什麼好的解決嗎?

五、決策樹的儲存

構造決策樹是很耗時的任務,即使處理很小的資料集,如前面的樣本資料,也要花費幾秒的時間,如果資料集很大,將會耗費很多計算時間。然而用建立好的決策樹解決分類問題,則可以很快完成。因此,為了節省計算時間,最好能夠在每次執行分類時呼叫已經構造好的決策樹。為了解決這個問題,需要使用Python模組pickle序列化物件。序列化物件可以在磁碟上儲存物件,並在需要的時候讀取出來。

假設我們已經得到決策樹{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}},使用pickle.dump儲存決策樹。

# -*- coding: UTF-8 -*-
import pickle
 
"""
函式說明:儲存決策樹
 
Parameters:
    inputTree - 已經生成的決策樹
    filename - 決策樹的儲存檔名
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-25
"""
def storeTree(inputTree, filename):
    with open(filename, 'wb') as fw:
        pickle.dump(inputTree, fw)
 
if __name__ == '__main__':
    myTree = {'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
    storeTree(myTree, 'classifierStorage.txt')
複製程式碼

執行程式碼,在該Python檔案的相同目錄下,會生成一個名為classifierStorage.txt的txt檔案,這個檔案二進位制儲存著我們的決策樹。我們可以使用sublime txt開啟看下儲存結果。

看不懂?沒錯,因為這個是個二進位制儲存的檔案,我們也無需看懂裡面的內容,會儲存,會用即可。那麼問題來了。將決策樹儲存完這個二進位制檔案,然後下次使用的話,怎麼用呢?

很簡單使用pickle.load進行載入即可,編寫程式碼如下:

# -*- coding: UTF-8 -*-
import pickle
 
"""
函式說明:讀取決策樹
 
Parameters:
    filename - 決策樹的儲存檔名
Returns:
    pickle.load(fr) - 決策樹字典
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-07-25
"""
def grabTree(filename):
    fr = open(filename, 'rb')
    return pickle.load(fr)
 
if __name__ == '__main__':
    myTree = grabTree('classifierStorage.txt')
    print(myTree)
複製程式碼

如果在該Python檔案的相同目錄下,有一個名為classifierStorage.txt的檔案,那麼我們就可以執行上述程式碼,執行結果如下圖所示:

從上述結果中,我們可以看到,我們順利載入了儲存決策樹的二進位制檔案。

六、Sklearn之使用決策樹預測隱形眼睛型別

1、實戰背景

進入本文的正題:眼科醫生是如何判斷患者需要佩戴隱形眼鏡的型別的?一旦理解了決策樹的工作原理,我們甚至也可以幫助人們判斷需要佩戴的鏡片型別。

隱形眼鏡資料集是非常著名的資料集,它包含很多換著眼部狀態的觀察條件以及醫生推薦的隱形眼鏡型別。隱形眼鏡型別包括硬材質(hard)、軟材質(soft)以及不適合佩戴隱形眼鏡(no lenses)。資料來源與UCI資料庫,資料集下載地址:github.com/Jack-Cheris…

一共有24組資料,資料的Labels依次是age、prescript、astigmatic、tearRate、class,也就是第一列是年齡,第二列是症狀,第三列是是否散光,第四列是眼淚數量,第五列是最終的分類標籤。資料如下圖所示:

可以使用已經寫好的Python程式構建決策樹,不過出於繼續學習的目的,本文使用Sklearn實現。

2、使用Sklearn構建決策樹

官方英文文件地址:scikit-learn.org/stable/modu…

sklearn.tree模組提供了決策樹模型,用於解決分類問題和迴歸問題。方法如下圖所示:

本次實戰內容使用的是DecisionTreeClassifier和export_graphviz,前者用於決策樹構建,後者用於決策樹視覺化。

DecisionTreeClassifier構建決策樹:

讓我們先看下DecisionTreeClassifier這個函式,一共有12個引數:

引數說明如下:

  • criterion:特徵選擇標準,可選引數,預設是gini,可以設定為entropy。gini是基尼不純度,是將來自集合的某種結果隨機應用於某一資料項的預期誤差率,是一種基於統計的思想。entropy是夏農熵,也就是上篇文章講過的內容,是一種基於資訊理論的思想。Sklearn把gini設為預設引數,應該也是做了相應的斟酌的,精度也許更高些?ID3演算法使用的是entropy,CART演算法使用的則是gini。
  • splitter:特徵劃分點選擇標準,可選引數,預設是best,可以設定為random。每個結點的選擇策略。best引數是根據演算法選擇最佳的切分特徵,例如gini、entropy。random隨機的在部分劃分點中找區域性最優的劃分點。預設的"be