【机器学习】决策树原理以及代码实现

什么是决策树

决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。由于这种决策分支画成图形很像一棵树的枝干,故称决策树。在机器学习中,决策树是一个预测模型,他代表的是对象属性与对象值之间的一种映射关系。Entropy = 信息熵(系统的凌乱程度),使用算法ID3, C4.5和C5.0生成树算法使用熵。
决策树是一种树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。
分类树(决策树)是一种十分常用的分类方法。他是一种监管学习,所谓监管学习就是给定一堆样本,每个样本都有一组属性和一个类别,这些类别是事先确定的,那么通过学习得到一个分类器,这个分类器能够对新出现的对象给出正确的分类。这样的机器学习就被称之为监督学习
给出如下的一组数据,一共有十个样本(学生数量),每个样本有分数,出勤率,回答问题次数,作业提交率四个属性,最后判断这些学生是否是好学生。最后一列给出了人工分类结果。
在这里插入图片描述
我们假设决策树为二叉树,且类似于下图:
在这里插入图片描述
上面图来源
因此机器学习决策树就是类似于这样的二叉树结构,来分类数据。

信息论基础知识
  • 信息量:信息多少的量度,可以说是事件发生的不确定度。根据信息的出现概率来计算信息量。计算公式 I = − l o g 2 P ( i ) I = -log_2 P(i) I=log2P(i)
    举个简单的例子:①太阳从东边升起西边落下②抽奖中了1E
    对应①来说,出现该事件的概率几乎是100%; 对应②来说,出现该事件的概率是十分小的,设为 1 256 \frac{1}{256} 2561
    如果有一天,别人告诉你太阳今天从东边升起西边落下,你会觉得他莫名其妙,因为该事件的发生概率太大,所以对于你来说信息量不大。但是如果有人对你说,你抽奖中了1E,对于你来说信息量就巨大了。
    所以 I ① = − l o g 2 1 = 0 ( b i t ) , I ② = − l o g 2 1 256 = 8 ( b i t ) I_①=-log_21 = 0(bit), I_②=-log_2\frac{1}{256} = 8(bit) I=log21=0(bit),I=log22561=8(bit)
    因此事件出现的概率越低,信息量越大,不确定度越大。

  • 信息熵:直到1948年,香农提出了“信息熵”的概念,信息熵这个词是从热力学中借用过来的。热力学中的热熵是表示分子状态混乱程度的物理量。香农用信息熵的概念来描述信源的不确定度。
    通常,一个信源发送出什么符号是不确定的,衡量它可以根据其出现的概率来度量。概率大,出现机会多,不确定性小;反之不确定性就大。由于信息量乘上相应的概率,就是整个信源的信息熵,所以信息熵也可以理解为事件不确定度的期望
    计算公式为 H = ∑ M > i > N − P ( i ) l o g 2 [ P ( i ) ] = ∑ M > i > N − P ( i ) I i H=\sum_{M>i>N}-P(i)log_2[P(i)]=\sum_{M>i>N}-P(i)I_i H=M>i>NP(i)log2[P(i)]=M>i>NP(i)Ii
    例如一个信源的发送概率如下 [ P ( ′ 发 送 A 符 号 ′ ) = 0.4 , P ( ′ 发 送 B 符 号 ′ ) = 0.6 ] [P('发送A符号')=0.4, P('发送B符号')=0.6] [P(A)=0.4,P(B)=0.6]
    I A = − l o g 2 0.4 = 1.32 b i t I_A = -log_20.4=1.32bit IA=log20.4=1.32bit, I B = − l o g 2 0.6 = 0.73 b i t I_B = -log_20.6=0.73bit IB=log20.6=0.73bit,所以信息熵为 H = 0.4 I A + 0.6 I B = 0.966 b i t H=0.4I_A+0.6I_B=0.966bit H=0.4IA+0.6IB=0.966bit, 所以信源的不确定度的期望为0.966bit。即如果信源发送无穷个符号的情况下,发送A的概率会无限接近0.4,发送B的概率会无限接近0.6,所以总的平均不确定度就是该信源的信息熵。

决策树Python代码

这里还是借助《机器学习实战》里的代码。
计算信息熵

def calcShannonEnt(dataSet):
    # 计算dataSet最后一列 即标签的信息熵
    numEntries = len(dataSet)
    labelCounts = {}
    for feaVec in dataSet:
        # 取出数据集中每一行的最后一个元素 因为通常那个位置是用于摆放标签
        currentLabel = feaVec[-1]
        # 统计标签出现次数
        labelCounts[currentLabel] = labelCounts.get(currentLabel, 0) + 1
    shannonEnt = 0.0
    # 计算全部标签情况下的信息熵
    for key in labelCounts:
        # 计算每个标签的出现概率
        prob = float(labelCounts[key]) / numEntries
        # 计算信息熵
        shannonEnt -= prob * log(prob, 2)
    return shannonEnt

[解析]: 在此处的dataSet数据集每行的最后一列数据存放的是数据的标签。通过取出所有的不同标签,计算标签在数据集的出现次数,计算出现概率即标签出现的行数÷总行数=出现概率。再通过出现概率算出信息熵,也即是不确定度期望。
所以该算法是用于计算数据集中的标签的信息熵, 即不确定度期望


分割数据集

def splitDataSet(dataSet, axis, value):
    retDataSet = []
    # 遍历数据集每一行
    for featVec in dataSet:
        # 如果axis列中 含有value特征的 则返回去掉该特征后的向量; 若没有该特征 则不理会
        # 例如 数据集a = [[1, 2, 3], [1, 5, 7],]
        # 如果数据集中的第0列数据是1, 则去掉1后输出
        # splitDataSet(a, 0, 1)
        # >> [[2, 3], [5, 7]]
        # 如果数据集中的第1列数据是2, 则去掉2后输出
        # splitDataSet(a, 1, 2)
        # >> [[1, 3]]
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

[解析]: 算法的功能是将每行的第axis列中(每列的元素可看作是一个特征),若该值是value,则抽取出这行,并将这行中第axis列的value值去掉,然后append到要返回的数据集中。即找出符合条件的特征行并返回其他维度的特征,用于分割数据集。


计算最佳分割维度

def chooseBestFeatureToSplit(dataSet):
    # 获取数据集中的数据维度, 默认最后一列是标签
    numFeature = len(dataSet[0]) - 1
    # 初始数据集信息熵
    baseEntropy = calcShannonEnt(dataSet)
    # 初始最好数据熵增益
    bestInfoGain = 0.0
    # 初始最好特征索引
    bestFeature = -1
    # 遍历所有维度的特征
    for i in range(numFeature):
        # 取出所有数据集行中的第i维特征
        featList = [example[i] for example in dataSet]
        # 特征去重
        uniqueVals = set(featList)
        # 新的数据熵, 用于保存划分完数据集以后的信息熵
        newEntropy = 0.0
        # 遍历所有的特征, 求和算出在被特征分割后的数据集的信息熵
        for value in uniqueVals:
            # 获得分割后的数据集
            subDataSet = splitDataSet(dataSet, i, value)
            # 算出分割后的数据集在总数据集的出现概率
            probability = len(subDataSet) / float(len(dataSet))
            # 计算分割后数据集的信息熵
            newEntropy += probability * calcShannonEnt(subDataSet)
        # 信息增益 = 初始信息熵 - 特征分割后的总信息熵
        infoGain = baseEntropy - newEntropy
        # 找出最小的newEntropy 使得infoGain = baseEntropy - newEntropy最大 以证明该维度的分割方案是最佳的
        if infoGain > bestInfoGain:
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

[解析]: 该算法是计算出哪一维度的特征分割后的数据集分类效果最佳。
首先初始化特征的总维度、数据集的信息熵。遍历每一维度的特征,即从第0维开始遍历到第n维,数据集中最后一列默认存放最后一个特征是标签。假设从第0维开始,将数据集中每一行的第0维特征取出,然后做去重工作,得到的特征集为{A, B, C}。通过拿到的不重复的特征,通过splitDataSet函数分割数据集算法,拿到符合该特征下的数据集(假设用A去分割,即数据即中若每行中的第0维特征是A,则取出此行,并取出行是已经去掉了第0维特征的),通过遍历所有的特征ABC,最终得到的3个子数据集且子数据集之间是不重复的,三个数据集合并一起就是总的数据集。
[讨论] 该3个数据集为什么是不重复且能合成总数据集?
由于是满足特征条件的行才会提取出来。例如数据集长这样:
[1 1 yes]
[1 1 yes]
[1 0 no ]
[0 1 no ]
[0 1 no ]
如果用第0维的各个特征作分割,因为第0维的特征只有01,就会分割成2个子数据集
A
[1 yes]
[1 yes]
[0 no ]

B
[1 no ]
[1 no ]
所以遍历每个特征,并用该特征分割数据集,是能保证数据完整性和不重复性的。
接着上文所说。我们已经通过提取第0维的所有特征,并分割出不同的数据集,但是怎么评估用第0维分割数据集是否是最好的呢?从较前处的上文可知道,信息熵是信息不确定度的量纲。我们就可以通过计算分割后的数据集信息熵进行评估。引用上面[讨论]的例子,假如用了第0维特征作为分割数据集,这时候我们得到了2个子数据集,下一步通过算出子数据集的行数与数据总行数的商,先算出子数据集的出现概率。即子数据集出现的概率为 P ( A ) = 3 5 , P ( B ) = 2 5 P(A)=\frac{3}{5}, P(B)=\frac{2}{5} P(A)=53,P(B)=52, 然后通过calcShannonEnt函数算法,根据A, B两个子数据集里面的标签算出不确定度期望——信息熵。最后通过AB子数据集的信息熵乘上出现概率,就是总的分割数据后的信息熵。
A中 P ( y e s ) = 2 3 , P ( n o ) = 1 3 P(yes)=\frac{2}{3},P(no)=\frac{1}{3} P(yes)=32,P(no)=31, H A = − 2 3 × l o g 2 2 3 − 1 3 × l o g 2 1 3 = 0.91 H_A=-\frac{2}{3}×log_2\frac{2}{3} - \frac{1}{3}×log_2\frac{1}{3} = 0.91 HA=32×log23231×log231=0.91
B中 P ( n o ) = 1 P(no)=1 P(no)=1, H B = − 1 × l o g 2 1 = 0 H_B = -1×log_21=0 HB=1×log21=0
AB两个熵代表的意思是,A中有不同的标签所以不确定度不为0,因为会出现yes也会出现no的情况,yes和no的构成的信息混乱程度是0.91,而B中没有不同的标签所以不准确度为0即绝对是出现no。
而分割后数据集的总信息熵为 H = P ( A ) H A + P ( B ) H B = 3 5 × 0.91 + 0 = 0.546 H=P(A)H_A+P(B)H_B=\frac{3}{5}×0.91 + 0 = 0.546 H=P(A)HA+P(B)HB=53×0.91+0=0.546
分割后数据集的总信息熵代表的意思是, 用该维度的特征分割数据后,求各个子数据集的不确定度的均值。
又如果用第1维分割数据集,即有01两个特征, 01分割成两个子数据集
A
[1 yes]
[1 yes]
[0 no ]
[0 no ]
B
[1 no ]
P ( A ) = 4 5 , P ( B ) = 1 5 P(A)=\frac{4}{5}, P(B)=\frac{1}{5} P(A)=54,P(B)=51, H A = − P ( y e s ) l o g 2 P ( y e s ) − P ( n o ) l o g 2 P ( n o ) = − 1 2 l o g 2 1 2 − 1 2 l o g 2 1 2 = 1 H_A=-P(yes)log_2P(yes)-P(no)log_2P(no) = -\frac{1}{2}log_2\frac{1}{2}-\frac{1}{2}log_2\frac{1}{2}=1 HA=P(yes)log2P(yes)P(no)log2P(no)=21log22121log221=1, H B = − P ( n o ) l o g 2 P ( n o ) = − 1 × l o g 2 1 = 0 H_B=-P(no)log_2P(no)=-1×log_21=0 HB=P(no)log2P(no)=1×log21=0
H = P ( A ) H A + P ( B ) H B = 4 5 × 1 + 1 5 × 0 = 0.8 H=P(A)H_A+P(B)H_B=\frac{4}{5}×1+\frac{1}{5}×0=0.8 H=P(A)HA+P(B)HB=54×1+51×0=0.8
这样就可以使用该维度的信息熵,来判断用该维度分割数据后,数据分类结果的好坏了。即例如第n维的信息熵是0,里面有6个特征,则表示的是,用第n维度特征做分割,分割出来的6个子数据集中,他们的信息熵都为0,即6个数据集拿到的标签都是相同的,这样我们就可以通过这维度的特征作为标准,用以分类数据。又如上文的计算可得,用第0维分割数据后,信息熵为0.546,而用第1维分割数据后,信息熵为0.8,在数学角度上看,第0维分割比第一维好,从肉眼观察数据可得,也是如此。第0维分割的两个类别,A的yes占了0.67,B的no是1,因此就能完成了一个粗糙的分类器。而相对于第1维分割的两个类别,A的yes和no都占了0.5,B的no占了1,但是当数据在A数据集的时候,就有比较大的可能会分类错误。所以用信息熵来衡量分割效果是比较科学的。
回到代码,代码已经到了通过遍历所有特征,拿到用该特征分割后的数据集,通过这些数据集算出信息熵,再算出该维度分割后产生的信息熵的步骤。通过遍历所有的维度,找出信息熵最小的维度,然后返回此维度的i值,作为最佳分割方式。


统计传入的标签集,返回出现次数最多的那个标签

def majorityCnt(classList):
    # 统计classList中,里面每一个分类的出现次数,并返回出现次数最多的那个分类
    classCount = dict()
    for vote in classList:
        classCount[vote] = classCount.get(vote, 0) + 1
    # 根据字典的值进行排序
    sortedClassCount = sorted(classCount.items(), key=itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

创建决策树,直到深度最大为止。另外在sklearn的决策树算法中,可以设置树的深度。
此算法是通过计算信息熵,拿到信息熵最小的分割数据的方案,依此作为节点,进行分割。产生后的叶子如果可以分割,继续计算信息熵,拿到信息熵最小的分割数据的方案,依此作为节点,再进行分割。这是一个递归的过程。

def createTree(dataSet, labels):
    # 获取数据集的标签索引
    classList = [example[-1] for example in dataSet]
    # 如果classList里的全部标签是一样的,则不用分类,直接返回标签
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 如果数据集没有任何维度的数据,只有标签,则返回标签出现次数最多的那个标签
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)
    # 获取最佳分割的维度 bestFeat 的索引值, 即数据集的所在列
    bestFeat = chooseBestFeatureToSplit(dataSet)
    # 通过拿到最佳维度 bestFeat,转化成该维度对应的标签
    bestFeatLabel = labels[bestFeat]
    # 把最佳的特征标签放进字典
    myTree = {bestFeatLabel: {}}
    # 删除最佳的那个维度
    del labels[bestFeat]
    # 遍历数据集 将第bestFeat列的数据取出, 即取出特征
    featValues = [example[bestFeat] for example in dataSet]
    # 特征去重
    uniqueVals = set(featValues)
    for value in uniqueVals:
        # 取得labels维度标签的副本, 使得不改变这个labels标签
        subLabels = labels[:]
        # 递归
        # 将该列的每个特征都先用splitDataSet进行分割,将分割后的数据再进行分类
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

所以决策树的分类过程如下
在这里插入图片描述
代码示例

data = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
label = ['no surfacing', 'flippers']
createTree(data, label)
>> {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值