機器學習筆記(2)-決策樹

決策樹

一.問題概述

決策樹(decision tree)希望從給定的數據集學得一個模型用以對新示例來進行分類,把這個樣本分類的任務看作對“當前樣本屬於正類嗎?”這個問題的“決策”或者“判定”的過程。決策樹是基於樹的結構進行決策的,如下圖:
這裏寫圖片描述

二.決策樹學習的基本算法
這裏寫圖片描述
三.實現算法
決策樹最核心的問題就是如何選擇出最優的劃分屬性,即上述算法中的第8行,一般而言,我們希望決策樹的分支節點所包含的樣本儘可能的屬於同一個類別,即樣本的“純度”越來越高。

  1. ID3決策樹算法
    1.1.以信息增益爲準則來選擇劃分屬性
    1.2 基本定義
    信息熵(information entropy),是度量樣本集合純度最常用的一種指標,假定樣本集合D中第k類樣本所佔的比例爲Pk(k=1,2,…,|y|),則D的信息熵定義爲:
    這裏寫圖片描述
    假定離散屬性a有V個可能的取值{a1 , a2 , … , aV},若使用a來對樣本進行劃分,則會產生V個節點,其中第v個分支節點包含了D中所有在屬性a上取值爲aV的樣本,記爲Dv。於是可以計算用屬性a劃分的“信息增益(information gain)”爲:
    這裏寫圖片描述
    一般而言,信息增益越大,則意味着用屬性a進行劃分所得的”純度提升“越大,因此我們可以使用信息增益爲準則來劃分屬性,即選擇屬性
    這裏寫圖片描述
    1.3 缺點:信息增益爲準則對可取值數目較多的屬性有所偏好
  2. C4.5決策樹算法
    2.1增益率(gain ratio)的定義:
    這裏寫圖片描述
    其中IV(a)稱爲屬性a的固有值,屬性a的可能取值的數目越多,則IV(a)的值通常會越大。
    2.2 由於增益率對可取值數目較多的屬性有所偏好,C4.5採用啓發式方法:先從候選的劃分屬性中找出信息增益高於平均水平的屬性,再從中選擇增益率最高的。
  3. CART決策樹算法
    3.1使用“基尼指數(Gini index)”來選擇劃分屬性。一個數據集的純度可以使用基尼值來度量:
    這裏寫圖片描述
    基尼指數Gini(D)反應了從數據集中隨機抽取兩個樣本不一致的概率。因此,Gini(D)越小,則數據集的純度越高。
    相應的,屬性a的基尼指數定義爲:
    這裏寫圖片描述
    4.實現的python代碼如下,我這裏實現了上述三個算法,根據方法chooseBestFeatureToSplit(dataSet, modelType =’ID3’)的modelType參數來選擇相應的算法。
# -*- coding: utf-8 -*-
"""
Decision Tree Source Code for Machine Learning
algorithm:  ID3,C4.5,CART 以信息增益、增益率爲準則來選擇最優的劃分屬性
@author leyuan
"""
from math import log
import operator
import treePlotter

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


def calcShannonEnt(dataSet):
    """
    計算給定數據集的信息熵(information entropy),
    :param dataSet:
    :return:
    """
    numEntries = len(dataSet)
    labelCounts = {}
    # 統計每個類別出現的次數,保存在字典labelCounts中
    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)  # 取2爲底的對數
    return shannonEnt


def calcGini(dataSet):
    """
    計算給定數據集的基尼指數
    :param dataSet:
    :return:
    """
    numExample = len(dataSet)
    lableCounts = {}
    # 統計每個類別出現的次數,保存在字典lableCounts中
    for featVect in dataSet:
        currentLable = featVect[-1]
        # 如果當前鍵值不存在,則擴展字典將當前鍵值加入到字典中
        if currentLable not in lableCounts.keys():
            lableCounts[currentLable] = 0
        lableCounts[currentLable] += 1
    gini = 1.0
    for key in lableCounts:
        # 使用所有類標籤的頻率來計算概率
        prob = float(lableCounts[key])/numExample
        # 計算基尼指數
        gini -= prob**2
    return gini

def splitDataSet(dataSet, axis, value):
    """
    按照給定特徵劃分數據集
    dataSet:待劃分的數據集
    axis:   劃分數據集的第axis個特徵
    value:  特徵的返回值(比較值)
    """
    retDataSet = []
    # 遍歷數據集中的每個元素,一旦發現符合要求的值,則將其添加到新創建的列表中
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)

            # extend()和append()方法功能相似,但在處理列表時,處理結果完全不同
            # a=[1,2,3]  b=[4,5,6]
            # a.append(b) = [1,2,3,[4,5,6]]
            # a.extend(b) = [1,2,3,4,5,6]
    return retDataSet


def chooseBestFeatureToSplit(dataSet, modelType ='ID3'):
    """
    選擇最好的數據集劃分方式,支持ID3,C4.5,CART
    :param dataSet: 數據集
    :param modelType: 決定選擇最優劃分屬性的方式
    :return: 最優分類的特徵的index
    """
    # 計算特徵數量
    numFeatures = len(dataSet[0]) - 1
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain = 0.0
    bestFeature = -1
    infoGainList = []
    gain_ratioList = []
    gini_index_list = []
    for i in range(numFeatures):
        # 創建唯一的分類標籤列表
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)
        # 計算用某種屬性劃分的信息熵和信息增益
        newEntropy = 0.0
        instrinsicValue = 0.0
        # 基尼指數
        gini_index = 0.0
        for value in uniqueVals:
            # 計算屬性的每個取值的信息熵x權重
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)
            # 計算固有值(instrinsic value)
            instrinsicValue -= prob * log(prob, 2)
            # 計算基尼指數
            gini_index += prob * calcGini(subDataSet)
        # 計算信息增益
        infoGain = baseEntropy - newEntropy
        infoGainList.append(infoGain)
        # 計算增益率
        if instrinsicValue == 0:
            gain_ratio = 0
        else:
            gain_ratio = infoGain/instrinsicValue
        gain_ratioList.append(gain_ratio)
        # 保存基尼指數
        gini_index_list.append(gini_index)
    # C4.5實現兩個步驟:1.找出信息增益高於平均水平的屬性組成集合A  2.從A中選擇增益率最高的
    # 求infoGain平均值
    avgInfoGain = sum(infoGainList)/len(infoGainList)
    infoGainSublist = [gain for gain in infoGainList if gain >= avgInfoGain]


    # ID3信息增益越大能得到最優化分
    if modelType == 'ID3':
        bestInfoGain = max(infoGainList)
        bestFeature = infoGainList.index(bestInfoGain)
    # C4.5得到最優化分屬性
    elif modelType == 'C4.5':
        # 選擇增益率最高的
        maxGainRatio = 0.0
        for i in [infoGainList.index(infor) for infor in infoGainSublist]:
            if gain_ratioList[i] > maxGainRatio:
                maxGainRatio = gain_ratioList[i]
                bestFeature = i
    elif modelType == 'CART':
        # 選擇劃分後基尼指數最小的
        minGini = 1
        for i in range(len(gini_index_list)):
            if gini_index_list[i] < minGini:
                minGini = gini_index_list[i]
                bestFeature = i
    return bestFeature


def majorityCnt(classList):
    """
    投票表決函數
    輸入classList:標籤集合,本例爲:['yes', 'yes', 'no', 'no', 'no']
    輸出:得票數最多的分類名稱
    :param classList:
    :return:
    """
    classCount={}
    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, featDict):
    """
    創建樹
    :param dataSet: 數據集
    :param labels: 標籤列表(屬性集合)
    :return:
    """
    # classList爲數據集的所有類標籤
    classList = [example[-1] for example in dataSet]
    # 停止條件1:所有類標籤完全相同,直接返回該類標籤
    if classList.count(classList[0]) == len(classList): 
        return classList[0]
    # 停止條件2:遍歷完所有特徵時仍不能將數據集劃分成僅包含唯一類別的分組,則返回出現次數最多的
    # 此處還存在一種情況數據集dataSet在屬性集上取值相同???
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)
    # 選擇最優分類特徵
    bestFeat = chooseBestFeatureToSplit(dataSet, modelType='ID3')
    bestFeatLabel = labels[bestFeat]

    # myTree存儲樹的所有信息
    myTree = {bestFeatLabel: {}}
    # 以下得到列表包含的所有屬性值
    del(labels[bestFeat])
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    # 遍歷當前選擇特徵包含的所有屬性值(怎麼保證該屬性能取到屬性的所有值?我這裏在外面寫了一個getFeatAllVals)
    for value in featDict[bestFeatLabel]:
        resDataSet = splitDataSet(dataSet, bestFeat, value)
        if len(resDataSet) == 0:
            myTree[bestFeatLabel][value] = majorityCnt(classList)
        else:
            subLabels = labels[:]
            myTree[bestFeatLabel][value] = createTree(resDataSet, subLabels, featDict)
    return myTree                         


def getFeatAllVals(dataSet, lables):
    """
    獲得給定數據集的指定標籤的所有屬性取值
    :param dataSet:
    :param lables:
    :return:
    """
    featDict = {}
    for i in range(len(lables)):
        featValues = [example[i] for example in dataSet]
        uniqueVals = set(featValues)
        featDict[lables[i]] = uniqueVals
    return featDict


def classify(inputTree, featLabels, testVec):
    """
    決策樹的分類函數
    :param inputTree: 訓練好的樹信息
    :param featLabels: 標籤列表
    :param testVec: 測試向量
    :return:
    """
    # 在2.7中,找到key所對應的第一個元素爲:firstStr = myTree.keys()[0],
    # 這在3.4中運行會報錯:‘dict_keys‘ object does not support indexing,這是因爲python3改變了dict.keys,
    # 返回的是dict_keys對象,支持iterable 但不支持indexable,
    # 我們可以將其明確的轉化成list,則此項功能在3中應這樣實現:
    firstSides = list(inputTree.keys())
    firstStr = firstSides[0]
    secondDict = inputTree[firstStr]

    # 將標籤字符串轉換成索引
    featIndex = featLabels.index(firstStr)

    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    # 遞歸遍歷整棵樹,比較testVec變量中的值與樹節點的值,如果到達葉子節點,則返回當前節點的分類標籤
    if isinstance(valueOfFeat, dict): 
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else:
        classLabel = valueOfFeat
    return classLabel


def storeTree(inputTree, filename):
    """
    使用pickle模塊存儲決策樹
    :param inputTree: 訓練好的樹信息
    :param filename:
    :return:
    """
    import pickle
    fw = open(filename, 'wb+')
    pickle.dump(inputTree, fw)
    fw.close()


def grabTree(filename):
    """
     導入決策樹模型
    :param filename:
    :return:
    """
    import pickle
    fr = open(filename, 'rb')
    return pickle.load(fr)

if __name__ == "__main__":
    fr = open('watermellon2')
    lenses = [inst.strip().split('-') for inst in fr.readlines()]
    lensesLabels = ['color', 'root', 'stroke', 'grain', 'navel', 'touch']
    featDict = getFeatAllVals(lenses, lensesLabels)
    lensesTree = createTree(lenses, lensesLabels, featDict)
    treePlotter.createPlot(lensesTree)



5.我使用的數據訓練集合爲:
這裏寫圖片描述
下面是採用ID3的運行結果:
這裏寫圖片描述

詳細代碼請參考我的gihub地址:https://github.com/yyHaker/MachineLearning/tree/master/MLaction-master/Ch03_DT
四.剪枝、連續值處理

  1. 預剪枝
    在決策樹生成過程中,對每個節點在劃分前進行估計,若當前節點的劃分不能帶來決策樹泛化性能(指處理未見實例的能力)的提升,則停止劃分,並將當前結點標記爲葉子節點。
  2. 後剪枝
    先從訓練集生成一顆完整的決策樹,然後自底向上地對非葉子結點進行考察,若將該節點對應的子樹替換爲葉節點能帶來決策樹泛化性能的提升,則將該葉子結點替換爲葉子結點。
    兩種剪枝方法的比較:
    預剪枝使得很多的決策樹分支沒有申展,這不僅降低了過擬合的風險,還顯著的減少了決策樹的訓練時間開銷和測試時間開銷;另一方面有些分支的當前劃分雖然不能提升泛化性能,甚至可能導致泛化性能暫時下降,但是在其基礎上進行的後續劃分卻有可能導致性能顯著的提高;預剪枝基於“貪心”本質禁止這些分支展開,給決策樹帶來了欠擬合的風險。
    後剪枝通常比預剪枝保留了更多的保留了分支。一般情況下,後剪枝決策樹的欠擬合的風險很小,泛化性能往往優於預剪枝的決策樹,但是後剪枝的決策樹是在生成的完全的決策樹之後的,並且要自底向上的對樹種的所有非葉子節點進行注意考察,因此其訓練時間開銷比未剪枝決策樹和預剪枝決策樹都要大的多。
  3. 連續值的處理
    採用二分法對連續屬性進行處理,給定屬性集D和連續屬性a,a在D上出現了n個不同的取值,將這些值從小到大排序,記爲{a1,a2,a3,…,an}.
    對連續屬性a我們考察n-1個元素的候選劃分點集合:
    這裏寫圖片描述
    找出使得信息增益最大的候選劃分點:
    這裏寫圖片描述
    區間[ai , a(i+1)]的中位點作爲候選劃分點t.

    需要注意與離散屬性不同,若當前屬性爲連續屬性,改屬性還可以作爲其後代節點的劃分屬性。

  4. 代碼實現,我這裏實現了以基尼指數爲最優的劃分策略、連續值的處理和離散值的處理,不剪枝策略、預剪枝策略以及後剪枝策略,代碼如下:

   # coding: utf-8
from numpy import *
import pandas as pd
import codecs
import operator
import copy
import json
import treePlotter


def calcGini(dataSet):
    """
    計算給定數據集的基尼指數
    :param dataSet: 數據集 list
    :return:
    """
    numEntries = len(dataSet)
    labelCounts = {}
    # 給所有可能的分類創建字典
    for featVec in dataSet:
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    Gini = 1.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        Gini -= prob * prob
    return Gini


def splitDataSet(dataSet,axis,value):
    """
        對離散變量劃分數據集,取出該特徵值爲value的所有樣本
        :param dataSet: 數據集list
        :param axis: 屬性下標
        :param value: 屬性取值
        :return:retDataSet
        """
    returnMat = []
    for data in dataSet:
        if data[axis] == value:
            returnMat.append(data[:axis]+data[axis+1:])
    return returnMat

"""
注意到連續屬性和離散屬性不同,對離散屬性劃分數據集時會刪除對應屬性的數據,若當前節點劃分屬性爲連續屬性,
該屬性還可作爲其後代節點的劃分屬性,因此對連續變量劃分數據集時並沒有刪除對應屬性的數據
"""
def splitContinuousDataSet(dataSet, axis, value, direction):
    """
     對連續變量劃分數據集
     :param dataSet: 數據集
     :param axis: 屬性下標
     :param value: 屬性值
     :param direction: 劃分的方向,決定劃分是小於value的數據樣本還是大於value 的數據樣本
                             direction=0得到大於value的數據集
     :return: retDataSet
     """
    retDataSet = []
    for featVec in dataSet:
        if direction == 0:
            if featVec[axis] > value:
                retDataSet.append(featVec)
        else:
            if featVec[axis] <= value:
                retDataSet.append(featVec)
    return retDataSet

'''
決策樹算法中比較核心的地方,究竟是用何種方式來決定最佳劃分?
使用信息增益作爲劃分標準的決策樹稱爲ID3
使用信息增益比作爲劃分標準的決策樹稱爲C4.5,甚至綜合信息增益和信息增益比
本題爲CART基於基尼指數
從輸入的訓練樣本集中,計算劃分之前的熵,找到當前有多少個特徵,遍歷每一個特徵計算信息增益,找到這些特徵中能帶來信息增益最大的那一個特徵。
這裏用分了兩種情況,離散屬性和連續屬性
1、離散屬性,在遍歷特徵時,遍歷訓練樣本中該特徵所出現過的所有離散值,假設有n種取值,那麼對這n種我們分別計算每一種的熵,最後將這些熵加起來
就是劃分之後的信息熵
2、連續屬性,對於連續值就稍微麻煩一點,首先需要確定劃分點,用二分的方法確定(連續值取值數-1)個切分點。遍歷每種切分情況,對於每種切分,
計算新的信息熵,從而計算增益,找到最大的增益。
假設從所有離散和連續屬性中已經找到了能帶來最大增益的屬性劃分,這個時候是離散屬性很好辦,直接用原有訓練集中的屬性值作爲劃分的值就行,但是連續
屬性我們只是得到了一個切分點,這是不夠的,我們還需要對數據進行二值處理。
'''


def chooseBestFeatureToSplit(dataSet, labels):
    """
    選擇最優的劃分屬性
    :param dataSet: 數據集list
    :param labels: 屬性集合
    :return: 最優劃分屬性的下標
    """
    numFeatures = len(dataSet[0]) - 1
    bestGini = 10000.0
    bestFeature = -1
    bestSplitDict = {}
    for i in range(numFeatures):
        # 對連續型特徵進行處理 ,i代表第i個特徵,featList是每次選取一個特徵之後這個特徵的所有樣本對應的數據
        featList = [example[i] for example in dataSet]
        # 對連續型值處理
        if type(featList[0]).__name__ == 'float' or type(featList[0]).__name__ == 'int':
            # 產生n-1個候選劃分點
            sortfeatList = sorted(featList)
            splitList = []
            for j in range(len(sortfeatList) - 1):
                splitList.append((sortfeatList[j] + sortfeatList[j + 1]) / 2.0)
            bestSplitGini = 10000
            # 求用第j個候選劃分點劃分時,得到的信息熵,並記錄最佳劃分點
            for value in splitList:
                newGini = 0.0
                subDataSet0 = splitContinuousDataSet(dataSet, i, value, 0)
                subDataSet1 = splitContinuousDataSet(dataSet, i, value, 1)
                prob0 = len(subDataSet0) / float(len(dataSet))
                newGini += prob0 * calcGini(subDataSet0)
                prob1 = len(subDataSet1) / float(len(dataSet))
                newGini += prob1 * calcGini(subDataSet1)
                if newGini < bestSplitGini:
                    bestSplitGini = newGini
                    bestSplit = value
            # 用字典記錄當前特徵的最佳劃分點,記錄對應的基尼指數
            bestSplitDict[labels[i]] = bestSplit
            newGini = bestSplitGini

        # 對離散型特徵進行處理
        else:
            uniqueVals = set(featList)
            newGini = 0.0
            # 計算該特徵下劃分的信息熵,選取第i個特徵的值爲value的子集
            for value in uniqueVals:
                subDataSet = splitDataSet(dataSet, i, value)
                prob = len(subDataSet) / float(len(dataSet))
                newGini += prob * calcGini(subDataSet)

        # 得到最優的劃分屬性
        if newGini < bestGini:
            bestGini = newGini
            bestFeature = i

    # 若當前節點的最佳劃分特徵爲連續特徵,則將其以之前記錄的劃分點爲界進行二值化處理即是否小於等於bestSplitValue
    # 問題:爲什麼要進行二值化處理,怎麼保證如果選擇的當前劃分屬性爲連續屬性,該屬性還可以作爲後代的劃分屬性
    # 思路:能不能在選擇的劃分屬性爲連續屬性時除了返回屬性下標外,還返回劃分數值,後面再遞歸求解構造樹
    if type(dataSet[0][bestFeature]).__name__ == 'float' or type(dataSet[0][bestFeature]).__name__ == 'int':
        bestSplitValue = round(bestSplitDict[labels[bestFeature]], 3)
        newlable = lables[bestFeature]
        if '<=' in newlable:
            newlable = newlable[:newlable.index('<=')]
            lables[bestFeature] = newlable
        labels[bestFeature] = labels[bestFeature] + '<=' + str(bestSplitValue)
    return bestFeature


def majorityCnt(classList):
    """
    特徵已經劃分完成,節點下的樣本還沒有統一取值,則需要進行投票
    :param classList:
    :return:
    """
    classCount={}
    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]


# 由於在Tree中,連續值特徵的名稱以及改爲了feature <= value的形式
# 因此對於這類特徵,需要利用正則表達式進行分割,獲得特徵名以及分割閾值
def classify(inputTree, featLabels, testVec):
    """
     對給定的數據集合進行分類
    :param inputTree:訓練好i的決策樹
    :param featLabels:屬性集合
    :param testVec: 測試樣本
    :return:
    """
    firstStr = list(inputTree.keys())[0]
    if u'<=' in firstStr:
        featvalue = float(firstStr.split(u"<=")[1])
        featkey = firstStr.split(u"<=")[0]
        secondDict = inputTree[firstStr]
        # 對於連續屬性,我們遍歷列表得到屬性下標
        featIndex = 0
        for i in range(len(featLabels)):
            if featkey in featLabels[i]:
                featIndex = i
        if testVec[featIndex] <= featvalue:
            judge = 1
        else:
            judge = 0
        for key in secondDict.keys():
            if judge == int(key):
                if type(secondDict[key]).__name__ == 'dict':
                    classLabel = classify(secondDict[key], featLabels, testVec)
                else:
                    classLabel = secondDict[key]
    else:    # 離散屬性的情況
        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


def testing(myTree, data_test, labels):
    """
    後剪枝
    :param myTree: 已經訓練成的樹
    :param data_test: 測試泛化能力的數據
    :param labels: 屬性集
    :return:
    """
    error = 0.0
    for i in range(len(data_test)):
        if classify(myTree, labels, data_test[i]) != data_test[i][-1]:
            error += 1
    return float(error)


def caclAccuracyRate(mtTree, data_test, lables):
    """
    計算決策樹模型預測的準確率
    :param mtTree:
    :param data_test:
    :param lables:
    :return:
    """
    return 1 - testing(myTree, data_test, lables)/float(len(data_test))

def testing_feat(feat, train_data, test_data, labels):
    """
    評測若選擇當前最優的劃分屬性進行劃分所產生決策樹的泛化能力
    :param feat: 當前最優的劃分屬性
    :param train_data: 數據集
    :param test_data: 測試泛化能力的數據集
    :param labels: 屬性集
    :return:
    """
    # 訓練數據的類別集合
    class_list = [example[-1] for example in train_data]
    bestFeatIndex = lables.index(feat)
    # 當前最優化分屬性下標在測試數據中對應的turple(屬性取值,所屬類別)
    test_data = [(example[bestFeatIndex], example[-1]) for example in test_data]
    error = 0.0

    # 判斷是離散屬性還是連續屬性
    if "<=" in feat:  # 連續屬性
        featvalue = float(feat.split("<=")[1])  # 連續屬性的劃分取值
        featkey = feat.split("<=")[0]  # 連續屬性的名字,下標爲 bestFeatIndex
        # value > featvalue  majority(classList0)
        subDataSet0 = splitContinuousDataSet(train_data, bestFeatIndex, featvalue, 0)
        classList0 =[example[-1] for example in subDataSet0]
        # value <= featvalue majority(classList1)
        subDataSet1 = splitContinuousDataSet(train_data, bestFeatIndex, featvalue, 1)
        classList1 = [example[-1] for example in subDataSet1]
        twoLables = [majorityCnt(classList0), majorityCnt(classList1)]
        # 計算error
        for data in test_data:
            if data[0] <= featvalue and data[1] != twoLables[1]:
                error += 1.0
            elif data[0] > featvalue and data[1] != twoLables[0]:
                error +=1.0
    else:  # 離散屬性
        # 當前最優劃分屬性的取值集合
        train_data = [example[bestFeatIndex] for example in train_data]
        all_feat = set(train_data)
        for value in all_feat:
            class_feat = [class_list[i] for i in range(len(class_list)) if train_data[i] == value]
            major = majorityCnt(class_feat)
            for data in test_data:
                if data[0] == value and data[1] != major:
                    error += 1.0
    # print 'myTree %d' % error
    return error


def testingMajor(major, data_test):
    """
    評測若不選擇當前最優的劃分屬性進行劃分所產生決策樹的泛化能力
    :param major: 當前訓練集合最多的類別
    :param data_test: 測試泛化能力的數據集
    :return:
    """
    error = 0.0
    for i in range(len(data_test)):
        if major != data_test[i][-1]:
            error += 1
    # print 'major %d' % error
    return float(error)
'''
主程序,遞歸產生決策樹。
params:
dataSet:用於構建樹的數據集,最開始就是data_full,然後隨着劃分的進行越來越小,第一次劃分之前是17個瓜的數據在根節點,然後選擇第一個bestFeat是紋理
紋理的取值有清晰、模糊、稍糊三種,將瓜分成了清晰(9個),稍糊(5個),模糊(3個),這個時候應該將劃分的類別減少1以便於下次劃分
labels:還剩下的用於劃分的類別
data_full:全部的數據
label_full:全部的類別
既然是遞歸的構造樹,當然就需要終止條件,終止條件有三個:
1、當前節點包含的樣本全部屬於同一類別;-----------------註釋1就是這種情形
2、當前屬性集爲空,即所有可以用來劃分的屬性全部用完了,這個時候當前節點還存在不同的類別沒有分開,這個時候我們需要將當前節點作爲葉子節點,
同時根據此時剩下的樣本中的多數類(無論幾類取數量最多的類)-------------------------註釋2就是這種情形
3、當前節點所包含的樣本集合爲空。比如在某個節點,我們還有10個西瓜,用大小作爲特徵來劃分,分爲大中小三類,10個西瓜8大2小,因爲訓練集生成
樹的時候不包含大小爲中的樣本,那麼劃分出來的決策樹在碰到大小爲中的西瓜(視爲未登錄的樣本)就會將父節點的8大2小作爲先驗同時將該中西瓜的
大小屬性視作大來處理。
'''
def createTree(dataSet, labels, data_full, labels_full, test_data, mode="unpro"):
    """
    遞歸的產生決策樹
    :param dataSet: 數據集
    :param labels: 屬性集
    :param data_full: 全部的數據
    :param labels_full: 全部的屬性
    :param test_data: 測試數據,用來評測泛化能力
    :param mode:剪枝策略,不剪枝,預剪枝,後剪枝
    :return:
    """
    classList=[example[-1] for example in dataSet]
    # 數據集中的樣本全部屬於同一類別,將該節點標記爲葉節點,並標記爲該類別(註釋1)
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 屬性集爲空或者樣本數據在屬性集上完全相同,將該節點標記爲葉子結點,類別標記爲樣本中類別最多的一個類(註釋2)
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)
    # 平凡情況,每次找到最佳劃分的特徵
    labels_copy = copy.deepcopy(labels)    # 淺拷貝只得到引用,深拷貝得到具體的值
    bestFeat=chooseBestFeatureToSplit(dataSet, labels)
    bestFeatLabel = labels[bestFeat]

    # 相應的剪枝操作
    if mode == "unpro" or mode == "post":
        myTree = {bestFeatLabel: {}}
    elif mode == "prev":
        if testing_feat(bestFeatLabel, dataSet, test_data, labels_copy) < testingMajor(majorityCnt(classList), test_data):
            myTree = {bestFeatLabel: {}}
        else:
            return majorityCnt(classList)

    # 判斷選擇的最優的劃分屬性是連續屬性還是離散屬性
    if '<=' in bestFeatLabel:   # 連續屬性
        featvalue = float(bestFeatLabel.split("<=")[1])  # 連續屬性的劃分取值
        featkey = bestFeatLabel.split("<=")[0]             # 連續屬性的名字,下標爲 bestFeat

        for i in range(2):
            subDataSet = splitContinuousDataSet(dataSet, bestFeat, featvalue, i)
            subClassList = [example[-1] for example in subDataSet]
            if len(subDataSet) == 0 or len(set(subClassList)) == 1:
                myTree[bestFeatLabel][i] = majorityCnt(subClassList)
            else:
                myTree[bestFeatLabel][i] = createTree(subDataSet, lables, data_full, lables_full, test_data, mode=mode)

    else:  # 離散屬性
        featValues = [example[bestFeat] for example in dataSet]
        uniqueVals = set(featValues)

        '''
        剛開始很奇怪爲什麼要加一個uniqueValFull,後來思考下覺得應該是在某次劃分,比如在根節點劃分紋理的時候,將數據分成了清晰、模糊、稍糊三塊
        ,假設之後在模糊這一子數據集中,下一劃分屬性是觸感,而這個數據集中只有軟粘屬性的西瓜,這樣建立的決策樹在當前節點劃分時就只有軟粘這一屬性了,
        事實上訓練樣本中還有硬滑這一屬性,這樣就造成了樹的缺失,因此用到uniqueValFull之後就能將訓練樣本中有的屬性值都囊括。
        如果在某個分支每找到一個屬性,就在其中去掉一個,最後如果還有剩餘的根據父節點投票決定。
        但是即便這樣,如果訓練集中沒有出現觸感屬性值爲“一般”的西瓜,但是分類時候遇到這樣的測試樣本,那麼應該用父節點的多數類作爲預測結果輸出。
        '''
        if type(dataSet[0][bestFeat]).__name__ == 'unicode' or type(dataSet[0][bestFeat]).__name__ == 'str':
            currentlabel = labels_full.index(labels[bestFeat])
            featValuesFull = [example[currentlabel] for example in data_full]
            uniqueValsFull = set(featValuesFull)

        del(labels[bestFeat])

        '''
        針對bestFeat的每個取值,劃分出一個子樹。對於紋理,樹應該是{"紋理":{?}},顯然?處是紋理的不同取值,有清晰模糊和稍糊三種,對於每一種情況,
        都去建立一個自己的樹,大概長這樣{"紋理":{"模糊":{0},"稍糊":{1},"清晰":{2}}},對於0\1\2這三棵樹,每次建樹的訓練樣本都是值爲value特徵數減少1
        的子集。
        '''
        for value in uniqueVals:
            subLabels = labels[:]
            # print(type(dataSet[0][bestFeat]+" "+dataSet[0][bestFeat]).__name__)
            if type(dataSet[0][bestFeat]).__name__ == 'unicode' or type(dataSet[0][bestFeat]).__name__ == 'str':
                uniqueValsFull.remove(value)
            myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels, data_full, labels_full, splitDataSet(test_data, bestFeat, value), mode=mode)
        if type(dataSet[0][bestFeat]).__name__ == 'unicode' or type(dataSet[0][bestFeat]).__name__ == 'str':
            for value in uniqueValsFull:
                myTree[bestFeatLabel][value] = majorityCnt(classList)
    # 後剪枝
    if mode == "post":
        if testing(myTree, test_data, labels_copy) > testingMajor(majorityCnt(classList), test_data):
            return majorityCnt(classList)
    return myTree


# 讀入csv文件數據
def load_data(file_name):
    file = codecs.open(file_name, "r", 'utf-8')
    filedata = [line.strip('\n').split(',') for line in file]
    filedata = [[float(i) if '.' in i else i for i in row] for row in filedata]  # change decimal from string to float
    train_data = [row[1:] for row in filedata[1:12]]
    test_data = [row[1:] for row in filedata[11:]]
    labels = []
    for label in filedata[0][1:-1]:
        labels.append(unicode(label))
    return train_data,test_data,labels


if __name__ == "__main__":
    """
    train_data,test_data,labels = load_data("data/西瓜數據集2.0.csv")
    data_full = train_data[:]
    labels_full = labels[:]
    """
    # 數據測試
    df = pd.read_csv('watermellon4.2.1.csv')
    data = df.values[:11, 1:].tolist()
    test_data = df.values[11:, 1:].tolist()
    data_full = data[:]
    lables = df.columns.values[1:-1].tolist()
    lables_full = lables[:]
    """
    爲了代碼的簡潔,將預剪枝,後剪枝和未剪枝三種模式用一個參數mode傳入建樹的過程
    post代表後剪枝,prev代表預剪枝,unpro代表不剪枝
    """
    # mode = "unpro"
    # mode = "prev"
    # mode = "post"
    mode = "unpro"
    myTree = createTree(data, lables, data_full, lables_full, test_data, mode=mode)
    # myTree = postPruningTree(myTree,train_data,test_data,labels_full)
    print(myTree)
    print(json.dumps(myTree, ensure_ascii=False, indent=4))
    print("accuracyRate:", caclAccuracyRate(myTree, test_data, lables_full))
    treePlotter.createPlot(myTree)
  1. 測試
    5.1我使用的數據集如下:
    這裏寫圖片描述
    5.2使用前面11個數據訓練決策是,後面7個數據測試,結果如下

    5.2.1 不剪枝,預測準確率:0.5 得到決策樹如下:
    這裏寫圖片描述
    5.2.2 預剪枝,預測準確率:0.6667 得到決策樹如下:
    這裏寫圖片描述
    5.2.3 後剪枝,預測準確率:0.6667 得到決策樹如下:
    這裏寫圖片描述
    5.2.4 只選取數據集合中連續屬性:
    a.不剪枝 準確率0.8333 得到決策樹
    這裏寫圖片描述
    b.預剪枝 準確率 0.6667 得到決策樹
    這裏寫圖片描述

    c.後剪枝 準確率 0.6667 得到決策樹
    這裏寫圖片描述

詳細代碼請參考我的github:https://github.com/yyHaker/MachineLearning/tree/master/MLaction-master/Ch03_DT/treeCART
如有問題,請指正,一起學習,謝謝!


參考網址以及書籍:
1.周志華《機器學習》
2.使用CART實現預剪枝、後剪枝:http://blog.csdn.net/sysu_cis/article/details/51874229

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