机器学习算法——决策树6(划分属性选择实例)

本节讲解根据信息增益如何选择划分属性与算法实现,以西瓜数据集2.0为例进行讲解。

 

1. 理论求信息增益

(理论参考:机器学习算法——决策树1(ID3决策树算法)_Vicky_xiduoduo的博客-CSDN博客)。

表1 西瓜数据集2.0
编号色泽根蒂 敲声纹理脐部触感好瓜
1青绿蜷缩浊响清晰凹陷硬滑
2乌黑蜷缩沉闷清晰凹陷硬滑
3乌黑蜷缩浊响清晰凹陷硬滑
4青绿蜷缩沉闷清晰凹陷硬滑
5浅白蜷缩浊响清晰凹陷硬滑
6青绿稍蜷浊响清晰稍凹软粘
7乌黑稍蜷浊响稍糊稍凹软粘
8乌黑稍蜷浊响清晰稍凹硬滑
9乌黑稍蜷沉闷稍糊稍凹硬滑
10青绿硬挺清脆清晰平坦软粘
11浅白硬挺清脆模糊平坦硬滑
12浅白蜷缩浊响模糊平坦软粘
13青绿稍蜷浊响稍糊凹陷硬滑
14浅白稍蜷沉闷稍糊凹陷硬滑
15乌黑稍蜷浊响清晰稍凹软粘
16浅白蜷缩浊响模糊平坦硬滑
17青绿蜷缩沉闷稍糊稍凹硬滑

计算出当前属性集合{色泽,根蒂,敲声,纹理,脐部,触感}中每个属性的信息增益。

信息熵公式:

信息增益公式:

首先求(p_k集合D中第k类样本所占的比例,好瓜占\frac{8}{17},坏瓜占\frac{9}{17}

Ent(D)=-(\frac{8}{17}log_2\frac{8}{17}+\frac{9}{17}log_2\frac{9}{17})=0.998

以“色泽”(代表属性a)为例,a可能的取值为:{青绿,乌黑,浅白}(属性a上的取值为a^v,记为D^v)。若使用该属性对样本集合D进行划分,则得到D^1(色泽=青绿),D^2(色泽=乌黑),D^3(色泽=浅白)。接下来,求Ent(D^1),Ent(D^2),Ent(D^3)。

根据信息熵公式,先求p_k(集合D中第k类样本所占的比例)。

D^1包含样本{1,4,6,10,13,17},正例(代表集合D^1中好瓜所占的比例)占p_1=\frac{3}{6} ,反例(代表D^1中坏瓜所占的比例)占p_2=\frac{3}{6}

D^2包含样本{2,3,7,8,9,15},正例占p_1=\frac{4}{6},反例占p_2=\frac{2}{6}

D^3包含样本{5,11,12,14,16},正例占p1=\frac{1}{5},反例占p_2=\frac{4}{5}

 所以三者的信息熵为:

Ent(D^1)=-(\frac{3}{6}log_2\frac{3}{6}+\frac{3}{6}log_2\frac{3}{6})=1.00

Ent(D^2)=-(\frac{4}{6}log_2\frac{4}{6}+\frac{2}{6}log_2\frac{2}{6})=0.918

Ent(D^3)=-(\frac{1}{5}log_2\frac{1}{5}+\frac{4}{5}log_2\frac{4}{5})=0.722

 然后求“色泽”的信息增益:

Gain(D, 色泽)=Ent(D)-\sum_{v=1}^{3}\frac{|D^v|}{|D|}Ent(D^v) =0.998-(\frac{6}{17}\times 1.000+\frac{6}{17}\times0.918+\frac{5}{17}\times0.722)=0.109

 同理,可以得到其他属性的信息增益:

Gain(D,根蒂)=0.143

Gain(D,敲声)=0.141

 Gain(D,纹理)=0.381

Gain(D,脐部)=0.289

Gain(D,触感)=0.006

显然,属性“纹理”的信息增益最大。于是它被选为划分属性,作为根节点。

纹理的属性集合为{清晰,稍糊,模糊},以“纹理=清晰”为例,D^1中有编号{1,2,3,4,6,8,10,15}的九个样例,可用属性{色泽,根蒂,敲声,脐部,触感}。基于D^1计算出各属性的信息增益:

Gain(D^1,色泽)=0.043

Gain(D^1,根蒂)=0.458

Gain(D^1,敲声)=0.331

Gain(D^1,脐部)=0.458

Gain(D^1,触感)=0.458

根蒂、脐部、触感三个属性均取得了最大的信息增益,可任选其中之一作为划分属性。类似的,对每个分支节点进行上述操作,最终得到一棵决策树。

  2. 算法实现(ID3算法)

(1)导入所需要的工具包

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

(2)导入西瓜数据集2.0

def creat_dataset_watermelon2(): #导入西瓜数据集2.0并进行初始化
    data_watermelon = pd.read_csv('D:/Machine_Learning/西瓜数据集2.0.csv', encoding='GBK')
    dataset_watermelon = data_watermelon.values.tolist() #将数组转化为列表
    featureNames = ['色泽', '根蒂', '敲击', '纹理', '脐部', '触感']
    #获取特征的值
    featureNamesSet = []
    for i in range(len(dataset_watermelon[0])-1):
        col = [x[i] for x in dataset_watermelon]
        colSet = set(col)
        featureNamesSet.append(list(colSet))
    return dataset_watermelon, featureNames, featureNamesSet

(3)计算信息熵

def calEntropy(dataSet): #求信息熵
    mD = len(dataSet)
    dataLabelList=[x[-1] for x in dataSet]
    dataLabelSet=set(dataLabelList)
    ent = 0
    for label in dataLabelSet:
       mDv=dataLabelList.count(label)
       prop=float(mDv)/mD
       ent=ent-prop * np.emath.log2(prop)
    return ent

(4)进行数据集分割,以便进行求每个属性的信息增益

def splitDataSet(dataSet, index, feature):
    #按特征的值,划分数据集
    #index要拆分的特征的下标,feature要拆分的特征
    split_DataSet = []
    for data in dataSet:
        if (data[index] == feature):
            sliceTmp = data[:index]
            sliceTmp.extend(data[index+1:]) #用在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)。
            split_DataSet.append(sliceTmp)
    return split_DataSet

(5)选择最优属性

def chooseBestFeature(dataSet): #求最大的信息增益 return返回最合适特征的索引值i
    entD = calEntropy(dataSet)
    mD = len(dataSet)
    featureNumber = len(dataSet[0])-1
    maxGain = 0
    maxIndex = -1
    for i in range(1, featureNumber):
        entDCopy = entD
        featureI = [x[i] for x in dataSet]
        featureSet = set(featureI)
        for feature in featureSet:
            splitedDataSet = splitDataSet(dataSet, i, feature)  # 拆分数据集
            mDv = len(splitedDataSet)
            entDCopy = entDCopy - float(mDv) / mD * calEntropy(splitedDataSet)
        if(maxIndex == -1):
            maxGain = entDCopy
            maxIndex = i
        elif(maxGain < entDCopy):
            maxGain = entDCopy
            maxIndex = i
    return maxIndex

(6)求解特殊情况下最优属性

def mainLabel(labelList): #返回值是标签
    labelRec = labelList[0]
    maxLabelCount = -1
    labelSet = set(labelList)
    for label in labelSet:
        if (labelList.count(label) > maxLabelCount):
            maxLabelCount = labelList.count(label)
            labelRec = label
    return labelRec

(7)构建决策树

def CreatDecisionTree(dataSet, featureNames, featureNamesSet, LabellistParent): #构造决策树 LabellistParent=featureNames
    LabelList = [x[-1] for x in dataSet]
    if(len(dataSet) == 0):#dataset为空集
        return mainLabel(LabellistParent)
    elif(len(dataSet) == 1): #没有可划分属性了
        return mainLabel(LabelList)
    elif (LabelList.count(LabelList[0]) == len(LabelList)):#全部属于同一个label
        return LabelList[0]

    bestFeatureIndex = chooseBestFeature(dataSet)#选择最优属性的索引值
    bestFeatureName = featureNames.pop(bestFeatureIndex-1)#将最优属性在列表中删除,将列表中指定位置的元素移除,同时移除的元素赋值给某个变量
    myTree = {bestFeatureName: {}}#构造树的字典
    featureList = featureNamesSet.pop(bestFeatureIndex) #将最优属性在集合中删除掉。根据键将字典中指定的键值对删除,同时将删除的值赋值给变量
    featureSet = set(featureList)
    for feature in featureSet:
        featureNamesNext = featureNames[:]
        featureNamesSetNext = featureNamesSet[:][:]
        splited_dataset=splitDataSet(dataSet, bestFeatureIndex, feature)
        myTree[bestFeatureName][feature] = CreatDecisionTree(splited_dataset, featureNamesNext, featureNamesSetNext, LabelList)
    return myTree

(8)画出决策树

# 能够显示中文
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['font.serif'] = ['SimHei']

# 分叉节点,也就是决策节点
decisionNode = dict(boxstyle="round", fc="0.5")

# 叶子节点
leafNode = dict(boxstyle="round4", fc="0.8")

# 箭头样式
arrow_args = dict(arrowstyle="<-")


def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    #param nodeTxt: 描述该节点的文本信息
    #param centerPt: 文本的坐标
    #param parentPt: 点的坐标,这里也是指父节点的坐标
    #param nodeType: 节点类型,分为叶子节点和决策节点
    #return:
    createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
                            xytext=centerPt, textcoords='axes fraction',
                            va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)


def getNumLeafs(myTree):

    #获取叶节点的数目
    numLeafs = 0

    # 得到当前第一个key,也就是根节点
    firstStr = list(myTree.keys())[0]
    # 得到第一个key对应的内容
    secondDict = myTree[firstStr]

    # 递归遍历叶子节点
    for key in secondDict.keys():
        # 如果key对应的是一个字典,就递归调用
        if type(secondDict[key]).__name__ == 'dict':
            numLeafs += getNumLeafs(secondDict[key])
        # 不是的话,说明此时是一个叶子节点
        else:
            numLeafs += 1
    return numLeafs


def getTreeDepth(myTree):
   
    #得到数的深度层数
    
    # 用来保存最大层数
    maxDepth = 0

    # 得到根节点
    firstStr = list(myTree.keys())[0]

    # 得到key对应的内容
    secondDic = myTree[firstStr]

    # 遍历所有子节点
    for key in secondDic.keys():
        # 如果该节点是字典,就递归调用
        if type(secondDic[key]).__name__ == 'dict':
            # 子节点的深度加1
            thisDepth = 1 + getTreeDepth(secondDic[key])

        # 说明此时是叶子节点
        else:
            thisDepth = 1

        # 替换最大层数
        if thisDepth > maxDepth:
            maxDepth = thisDepth

    return maxDepth


def plotMidText(cntrPt, parentPt, txtString):
    """
    计算出父节点和子节点的中间位置,填充信息
    :param cntrPt: 子节点坐标
    :param parentPt: 父节点坐标
    :param txtString: 填充的文本信息
    :return:
    """
    # 计算x轴的中间位置
    xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
    # 计算y轴的中间位置
    yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
    # 进行绘制
    createPlot.ax1.text(xMid, yMid, txtString)


def plotTree(myTree, parentPt, nodeTxt):
    """
    绘制出树的所有节点,递归绘制
    :param myTree: 树
    :param parentPt: 父节点的坐标
    :param nodeTxt: 节点的文本信息
    :return:
    """
    # 计算叶子节点数
    numLeafs = getNumLeafs(myTree)

    # 计算树的深度
    depth = getTreeDepth(myTree)

    # 得到根节点的信息内容
    firstStr = list(myTree.keys())[0]

    # 计算出当前根节点在所有子节点的中间坐标,也就是当前x轴的偏移量加上计算出来的根节点的中心位置作为x轴(
    # 比如说第一次:初始的x偏移量为:-1/2W,计算出来的根节点中心位置为:(1+W)/2W,相加得到:1/2),当前y轴偏移量作为y轴
    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]

    # 计算出新的y轴偏移量,向下移动1/D,也就是下一层的绘制y轴
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD

    # 循环遍历所有的key
    for key in secondDict.keys():
        # 如果当前的key是字典的话,代表还有子树,则递归遍历
        if isinstance(secondDict[key], dict):
            plotTree(secondDict[key], cntrPt, str(key))
        else:
            # 计算新的x轴偏移量,也就是下个叶子绘制的x轴坐标向右移动了1/W
            plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
            # 打开注释可以观察叶子节点的坐标变化
            # print((plotTree.xOff, plotTree.yOff), secondDict[key])
            # 绘制叶子节点
            plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
            # 绘制叶子节点和父节点的中间连线内容
            plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))

    # 返回递归之前,需要将y轴的偏移量增加,向上移动1/D,也就是返回去绘制上一层的y轴
    plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD


def createPlot(inTree):
    """
    需要绘制的决策树
    :param inTree: 决策树字典
    :return:
    """
    # 创建一个图像
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
    # 计算出决策树的总宽度
    plotTree.totalW = float(getNumLeafs(inTree))
    # 计算出决策树的总深度
    plotTree.totalD = float(getTreeDepth(inTree))
    # 初始的x轴偏移量,也就是-1/2W,每次向右移动1/W,也就是第一个叶子节点绘制的x坐标为:1/2W,第二个:3/2W,第三个:5/2W,最后一个:(W-1)/2W
    plotTree.xOff = -0.5/plotTree.totalW
    # 初始的y轴偏移量,每次向下或者向上移动1/D
    plotTree.yOff = 1.0
    # 调用函数进行绘制节点图像
    plotTree(inTree, (0.5, 1.0), '')
    # 绘制
    plt.show()

(9)调用上述函数

if __name__ == '__main__':
    dataset_watermelon, featureNames, featureNamesSet = creat_dataset_watermelon2()
    myTree = CreatDecisionTree(dataset_watermelon, featureNames, featureNamesSet, featureNames)
    createPlot(myTree)

最后得到的结果如下

 欢迎私信探讨讲解不清楚的地方!

C4.5算法与CART算法与ID3算法的实现类似,区别在于最优选择属性选择上,其余代码一致,不再赘述。

下一章将进行剪枝处理实例讲解以及代码实现。

欢迎各位关注,一起探讨学习机器学习算法!

  • 9
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值