1. 程式人生 > >機器學習筆記(5)——C4.5決策樹中的連續值處理和Python實現

機器學習筆記(5)——C4.5決策樹中的連續值處理和Python實現

在ID3決策樹演算法中,我們實現了基於離散屬性的決策樹構造。C4.5決策樹在劃分屬性選擇、連續值、缺失值、剪枝等幾方面做了改進,內容較多,今天我們專門討論連續值的處理和Python實現。

1. 連續屬性離散化

C4.5演算法中策略是採用二分法將連續屬性離散化處理:假定樣本集D的連續屬性a有n個不同的取值,對這些值從小到大排序,得到屬性值的集合\left \{ a^1,a^2,...,a^n \right \}。把區間\left [a^i,a^i+1 \right )的中位點\frac{a^i+a^{i+1}}{2}作為候選劃分點,於是得到包含n-1個元素的劃分點集合

T_a=\left \{ \frac {a_1+a_{i+1}}{2}|1\leqslant i\leqslant n-1\right \}

基於每個劃分點t,可將樣本集D分為子集D_{t}^{-}D_{t}^{+},其中D_{t}^{-}中包含屬性a上不大於t的樣本D_{t}^{+}包含屬性a上大於t的樣本。

對於每個劃分點t,按如下公式計算其資訊增益值,然後選擇使資訊增益值最大的劃分點進行樣本集合的劃分。Gain(D,a,t)=Ent(D)-\sum_{\lambda \in \left \{ -,+ \right \}}\frac{|D^\lambda _t|}{|D|}Ent(D^\lambda _t)

在ID3演算法中的西瓜資料集中增加兩個連續屬性“密度”和“含糖率”,下面我們計算屬性“密度”的資訊增益。

從資料可以看出,17個樣本的密度屬性值均不同,因此該屬性的候選劃分點集合由16個值組成:

T_{density}=\left \{ 0.244,0.294,0.351,0.381,0.420,0.459,0.518,0.574,0.600,0.621,0.636,0.648,0.661,0.681,0.708,0.746 \right \}

Ent(D)=\sum_{k=1}^{2}p_klog_2p_k=-(\frac{8}{17}log_2\frac{8}{17}+\frac{9}{17}log_2\frac{9}{17})=0.998

Gain(D,a,t=0.244)=Ent(D)-\sum_{\lambda \in \left \{ -,+ \right \}}\frac{|D^\lambda _{0.244}|}{|D|}Ent(D^\lambda _{0.244})

                                       =0.998-\frac{1}{17}\left ( -1log_21 \right )-\frac{16}{17}\left ( -\frac{8}{16}log_2\frac{8}{16} -\frac{8}{16}log_2\frac{8}{16} \right )

                                       =0.998-0-0.941=0.057

Gain(D,a,t=0.294)=Ent(D)-\sum_{\lambda \in \left \{ -,+ \right \}}\frac{|D^\lambda _{0.294}|}{|D|}Ent(D^\lambda _{0.294})

                                       =0.998-\frac{2}{17}\left ( -\frac{2}{2}log_2\frac{2}{2} \right )-\frac{15}{17}\left ( -\frac{8}{15}log_2\frac{8}{15} -\frac{7}{15}log_2\frac{7}{15} \right )

                                       =0.998-0-0.880=0.118

Gain(D,a,t=0.351)=0.998-\frac{3}{17}\left ( -\frac{3}{3}log_2\frac{3}{3} \right )-\frac{14}{17}\left ( -\frac{8}{14}log_2\frac{8}{14} -\frac{6}{14}log_2\frac{6}{14} \right )

                                       =0.998-0-0.811=0.187

Gain(D,a,t=0.381)=0.998-\frac{4}{17}\left ( -\frac{4}{4}log_2\frac{4}{4} \right )-\frac{13}{17}\left ( -\frac{8}{13}log_2\frac{8}{13} -\frac{5}{13}log_2\frac{5}{13} \right )

                                       =0.998-0-0.735=0.263

所有劃分點的資訊增益均可按上述方法計算得出,最優劃分點為0.381,對應的資訊增益為0.263。我們分別按照離散值和連續值的資訊增益計算方法,計算出每個屬性的資訊增益,從而選擇最優劃分屬性,構造決策樹。

需要注意的是:當前節點劃分屬性為連續屬性,該屬性還可作為其後代節點的劃分屬性。

2. Python實現

在ID3決策樹演算法的基礎上,我們需要新增或修改一些方法,以便可以處理連續值。

  • 新增一個劃分資料集的方法
# 劃分資料集, axis:按第幾個特徵劃分, value:劃分特徵的值, LorR: value值左側(小於)或右側(大於)的資料集
def splitDataSet_c(dataSet, axis, value, LorR='L'):
    retDataSet = []
    featVec = []
    if LorR == 'L':
        for featVec in dataSet:
            if float(featVec[axis]) < value:
                retDataSet.append(featVec)
    else:
        for featVec in dataSet:
            if float(featVec[axis]) > value:
                retDataSet.append(featVec)
    return retDataSet
  • 修改原來的最優劃分屬性選擇方法,在其中增加連續屬性的分支
# 選擇最好的資料集劃分方式
def chooseBestFeatureToSplit_c(dataSet, labelProperty):
    numFeatures = len(labelProperty)  # 特徵數
    baseEntropy = calcShannonEnt(dataSet)  # 計算根節點的資訊熵
    bestInfoGain = 0.0
    bestFeature = -1
    bestPartValue = None  # 連續的特徵值,最佳劃分值
    for i in range(numFeatures):  # 對每個特徵迴圈
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList)  # 該特徵包含的所有值
        newEntropy = 0.0
        bestPartValuei = None
        if labelProperty[i] == 0:  # 對離散的特徵
            for value in uniqueVals:  # 對每個特徵值,劃分資料集, 計算各子集的資訊熵
                subDataSet = splitDataSet(dataSet, i, value)
                prob = len(subDataSet) / float(len(dataSet))
                newEntropy += prob * calcShannonEnt(subDataSet)
        else:  # 對連續的特徵
            sortedUniqueVals = list(uniqueVals)  # 對特徵值排序
            sortedUniqueVals.sort()
            listPartition = []
            minEntropy = inf
            for j in range(len(sortedUniqueVals) - 1):  # 計算劃分點
                partValue = (float(sortedUniqueVals[j]) + float(
                    sortedUniqueVals[j + 1])) / 2
                # 對每個劃分點,計算資訊熵
                dataSetLeft = splitDataSet_c(dataSet, i, partValue, 'L')
                dataSetRight = splitDataSet_c(dataSet, i, partValue, 'R')
                probLeft = len(dataSetLeft) / float(len(dataSet))
                probRight = len(dataSetRight) / float(len(dataSet))
                Entropy = probLeft * calcShannonEnt(
                    dataSetLeft) + probRight * calcShannonEnt(dataSetRight)
                if Entropy < minEntropy:  # 取最小的資訊熵
                    minEntropy = Entropy
                    bestPartValuei = partValue
            newEntropy = minEntropy
        infoGain = baseEntropy - newEntropy  # 計算資訊增益
        if infoGain > bestInfoGain:  # 取最大的資訊增益對應的特徵
            bestInfoGain = infoGain
            bestFeature = i
            bestPartValue = bestPartValuei
    return bestFeature, bestPartValue
  • 修改原來的決策樹構建方法 
# 建立樹, 樣本集 特徵 特徵屬性(0 離散, 1 連續)
def createTree_c(dataSet, labels, labelProperty):
    # print dataSet, labels, labelProperty
    classList = [example[-1] for example in dataSet]  # 類別向量
    if classList.count(classList[0]) == len(classList):  # 如果只有一個類別,返回
        return classList[0]
    if len(dataSet[0]) == 1:  # 如果所有特徵都被遍歷完了,返回出現次數最多的類別
        return majorityCnt(classList)
    bestFeat, bestPartValue = chooseBestFeatureToSplit_c(dataSet,
                                                        labelProperty)  # 最優分類特徵的索引
    if bestFeat == -1:  # 如果無法選出最優分類特徵,返回出現次數最多的類別
        return majorityCnt(classList)
    if labelProperty[bestFeat] == 0:  # 對離散的特徵
        bestFeatLabel = labels[bestFeat]
        myTree = {bestFeatLabel: {}}
        labelsNew = copy.copy(labels)
        labelPropertyNew = copy.copy(labelProperty)
        del (labelsNew[bestFeat])  # 已經選擇的特徵不再參與分類
        del (labelPropertyNew[bestFeat])
        featValues = [example[bestFeat] for example in dataSet]
        uniqueValue = set(featValues)  # 該特徵包含的所有值
        for value in uniqueValue:  # 對每個特徵值,遞迴構建樹
            subLabels = labelsNew[:]
            subLabelProperty = labelPropertyNew[:]
            myTree[bestFeatLabel][value] = createTree_c(
                splitDataSet(dataSet, bestFeat, value), subLabels,
                subLabelProperty)
    else:  # 對連續的特徵,不刪除該特徵,分別構建左子樹和右子樹
        bestFeatLabel = labels[bestFeat] + '<' + str(bestPartValue)
        myTree = {bestFeatLabel: {}}
        subLabels = labels[:]
        subLabelProperty = labelProperty[:]
        # 構建左子樹
        valueLeft = '是'
        myTree[bestFeatLabel][valueLeft] = createTree_c(
            splitDataSet_c(dataSet, bestFeat, bestPartValue, 'L'), subLabels,
            subLabelProperty)
        # 構建右子樹
        valueRight = '否'
        myTree[bestFeatLabel][valueRight] = createTree_c(
            splitDataSet_c(dataSet, bestFeat, bestPartValue, 'R'), subLabels,
            subLabelProperty)
    return myTree
  • 修改原來的測試方法
# 測試演算法
def classify_c(inputTree, featLabels, featLabelProperties, testVec):
    firstStr = inputTree.keys()[0]  # 根節點
    firstLabel = firstStr
    lessIndex = str(firstStr).find('<')
    if lessIndex > -1:  # 如果是連續型的特徵
        firstLabel = str(firstStr)[:lessIndex]
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstLabel)  # 跟節點對應的特徵
    classLabel = None
    for key in secondDict.keys():  # 對每個分支迴圈
        if featLabelProperties[featIndex] == 0:  # 離散的特徵
            if testVec[featIndex] == key:  # 測試樣本進入某個分支
                if type(secondDict[key]).__name__ == 'dict':  # 該分支不是葉子節點,遞迴
                    classLabel = classify_c(secondDict[key], featLabels,
                                           featLabelProperties, testVec)
                else:  # 如果是葉子, 返回結果
                    classLabel = secondDict[key]
        else:
            partValue = float(str(firstStr)[lessIndex + 1:])
            if testVec[featIndex] < partValue:  # 進入左子樹
                if type(secondDict['是']).__name__ == 'dict':  # 該分支不是葉子節點,遞迴
                    classLabel = classify_c(secondDict['是'], featLabels,
                                           featLabelProperties, testVec)
                else:  # 如果是葉子, 返回結果
                    classLabel = secondDict['是']
            else:
                if type(secondDict['否']).__name__ == 'dict':  # 該分支不是葉子節點,遞迴
                    classLabel = classify_c(secondDict['否'], featLabels,
                                           featLabelProperties, testVec)
                else:  # 如果是葉子, 返回結果
                    classLabel = secondDict['否']

    return classLabel

3. 繪製決策樹並測試資料

 我們利用上面的西瓜資料,繪製一個決策樹。

fr = open(r'D:\Projects\PyProject\DecisionTree\watermalon3.0.txt')

listWm = [inst.strip().split('\t') for inst in fr.readlines()]
labels = ['色澤', '根蒂', '敲聲', '紋理', '臍部', '觸感', '密度', '含糖率']
labelProperties = [0, 0, 0, 0, 0, 0, 1, 1]  # 屬性的型別,0表示離散,1表示連續
Trees = trees.createTree_c(listWm, labels, labelProperties)

print(json.dumps(Trees, encoding="cp936", ensure_ascii=False))

treePlotter.createPlot(Trees)

返回的決策樹資料:{"紋理": {"模糊": "否", "清晰": {"密度<0.3815": {"是": "否", "否": "是"}}, "稍糊": {"觸感": {"軟粘": "是", "硬滑": "否"}}}}

 再用一條測試資料測試一下演算法,看是否能得到正確的分類。

labels = ['色澤', '根蒂', '敲聲', '紋理', '臍部', '觸感', '密度', '含糖率']
labelProperties = [0, 0, 0, 0, 0, 0, 1, 1]
testData = ['淺白', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', 0.585, 0.002]
testClass = trees.classify_c(Trees, labels, labelProperties, testData)
print(json.dumps(testClass, encoding="cp936", ensure_ascii=False))

測試返回的結果是好瓜,從構造的樹可以看出該資料進入紋理清晰,密度大於0.3815的分支,是正確的。

參考:

周志華《機器學習》

Peter Harrington 《機器學習實戰》