【机器学习入门Machine Learning For Beginners】笔记&代码实践——决策树

系列文章目录

第1章 专家系统



前言

你好哇,昆兰(Quinlan)在1986年发表的论文中详细描述了决策树算法,是一种用于分类的树状结构,简洁直观、有效。在1979年昆兰提出了ID3算法,该算法最初被用来判断国际象棋残局的输赢,后来通用于分类问题。并且在此基础上,提出了一系列的改进算法,如C4.5等。
在昆兰发表决策树算法的时代,机器学习的概念已经提出。人们意识到,学习是智能行为的重要特征,昆兰将当时的机器学习方法分为两类:一类是能够自我改进的自适应系统,它们通过监测自己的性能,调整系统内部参数,向着目标方向做出改进;另一类基于结构化知识的学习方法,把学习视为获取知识,比如专家系统中的产生式规则。昆兰将决策树纳入后一类学习方法,这种方法一定程度上解决了专家系统获取知识的瓶颈问题,但是非常耗时和低效(通过对领域专家进行访谈)。并且整个过程费时费力,而且 不能很好地解决一些边界情况或者极端情况,比如不常见地个例、重复或是冲突的规则
而决策树不再依赖于人类专家的经验,而是用统计的方法直接从数据中获得“第一手”经验。

在这里插入图片描述


一、构造决策树

类似于数据结构中的二叉树的定义、树的定义,构造决策树也是一个递归的过程:
从整个数据集开始,根据条件将数据集分割为两个互补的子集。这个条件构成了树的分支节点,称为节点的 分支条件。对于两个子数据集,我们递归地进行分割操作,分别形成节点的左右子树。当数据集被分割为仅包含单一类别的元素集合时,递归分割就终止了,这些不能继续分割的集合是 叶节点
在构建过程中,选取的分支条件决定了树的形态。如果随意选取可能造成:
1.得到一棵非常深而且非常宽的决策树,占据更多存储空间,耗费更多计算资源。
2.一棵复杂的树可能过拟合样本,决策树"记住了“训练样本中一些次要细节,不利于识别没有出现过的新样本。

一般来说,决策树的构建遵循 奥卡姆剃刀(Occam’s Razor)原理:
如无必要,勿增实体:一颗小的树预测能力更好。
采用 分而治之 的思想,利用贪心策略从局部出发来构造一棵大小紧凑的决策树。

决策树的 构建过程 主要步骤:
训练数据集D,类别集合C={C1, C2, … c}

1.创建一个结点t,初始情况下训练数据集中的所有样本与根结点关联,记为 D t D_t Dt将t设为当前结点。

2.如果当前结点所关联的数据集 D t D_t Dt.中所有样本的类别相同(假设为 C i C_i Ci) , 则将该结点标记为叶子节点,记录类别为 C i C_i Ci,停止对该结点所关联的数据集的进一步分裂。接着处理其他非叶子节点。否则,进入下一步。

3.为数据集 D t D_t Dt选择分裂属性和分裂条件。根据分裂条件将数据集 D t D_t Dt{分裂为m个子集,为结点创建m个子女结点,将这m个数据集分别与之关联。依次将每个结点设为当前结点,转至步骤2进行处理,直至所有结点都标记为叶子结点。

二、ID3算法

ID3算法的全称是Iterative Dichotomiser,是一个迭代二分算法。为了使分支尽快到达叶节点,在对落入某个节点的样本进行分割的时候,应该力求分割得到的两个集合具有较高的 纯度。这个算法利用了 __信息熵__来衡量分割的纯度,p表示正样本(分类为是)的比例,n表示负样本(分类为否)的比例,一组样本的熵H(p,n)定义如下。
H ( p , n ) = − p p + n log ⁡ 2 p p + n − n p + n log ⁡ 2 n p + n H(p, n)=-\frac{p}{p+n} \log _{2} \frac{p}{p+n}-\frac{n}{p+n} \log _{2} \frac{n}{p+n} H(p,n)=p+nplog2p+npp+nnlog2p+nn
假设某个分割A将样本分为两组,两组正负样本数量分别为(P1,n1)和(Pz,n2),分割后的熵就是两组熵的加权和。
H ( A ) = ( p 1 + n 1 ) H ( p 1 , n 1 ) + ( p 2 + n 2 ) H ( p 2 , n 2 ) p 1 + n 1 + p 2 + n 2 H(A)=\frac{\left(p_{1}+n_{1}\right) H\left(p_{1}, n_{1}\right)+\left(p_{2}+n_{2}\right) H\left(p_{2}, n_{2}\right)}{p_{1}+n_{1}+p_{2}+n_{2}} H(A)=p1+n1+p2+n2(p1+n1)H(p1,n1)+(p2+n2)H(p2,n2)
熵是不确定性的度量,如果一组样本大都属于同一类别,纯度高,那么它们的不确定性就低,熵就小。反之,样本纯度低,熵就大。
所以说,好的分割应该让熵减小的更多。选择节点分支条件时,我们要选取信息增益最大的分支条件。即:
g a i n ( A ) = H ( p , n ) − H ( A ) gain(A)=H(p,n)-H(A) gain(A)=H(p,n)H(A)
ID3是一个贪心算法。每次选取分支条件时,它只关注局部最优,也就是如何对落入当前节点的数据子集进行分割,因此并不能保证得到全局最优的决策树。

三、信息熵

在度量分支优劣的时候,我们用到了信息熵的概念。信息熵和热力学熵在本质上是相通的,它们度量了系统的不确定性或者说无序程度。在热力学中,熵和系统的微观状态数呈对数关系。在信息论中,熵有类似的定义,它与随机事件的概率呈对数关系。熵是一个抽象的概念,下面我们试图从具体的角度来理解熵是如何定义的。

为了理解熵的度量,我们通过一个例子来观察熵和信息的关系。考试单项选择题有A、B、C、D这4个选项,如果我们不知道题目怎么做,那么4个选项都有可能是正确答案,此时这个“系统”具有极大的不确定性,它的熵很大。现在,老师给我们提示说A和B都是错误的,因此系统的不确定性就大大降低了。因为老师带来了信息,我们得知可能的正确选项只剩下两个,信息使得熵减小了。所以说, 信息和熵是互补的,信息蕴含着负熵。如果老师直接告诉我们正确答案,那么系统的不确定性就完全消除了,熵就减小到了0。由此可见, 系统的熵等于完全消解它的不确定性所需要的信息量

如何描述信息和熵的数量呢?我们需要一一个单位, 在数字世界里,最为通用的信息单位是比特(bit),也就是一个二进制位。回到单项选择题的例子,最初4个选项都有可能是正确的,为了区别这4种情况,我们需要 log ⁡ 2 4 = 2 \log_{2}{4} =2 log24=2个比特(0、1、2、3这4种情况用二进制表示为00、01、10、11,需要2个二进制位,即2比特)。在老师给出提示后,只有两个选项之一是正确的,区分两种情况需要 log ⁡ 2 2 = 1 \log_{2}{2}=1 log22=1比特(0和1两种情况,只需要1位二进制数就可以表示,因此是1比特)。

由此可见,系统最初的熵是2比特,老师带来了1比特信息后,熵减小到了1比特。

考虑一个离散变化的系统,有 N N N种可能状态,那么需要 log ⁡ 2 N \log_{2}{N} log2N位二进制来区分0到 N − 1 N-1 N1的不同状态,它的熵就等于 log ⁡ 2 N \log_{2}{N} log2N.系统的熵(或信息量)与状态数呈对数关系。对数底数的选择并不影响熵的相对大小,只影响衡量单位数量的熵的尺度。因此,不同底数对应于熵的不同单位,以2为底数的时候,单位是比特。当然我们也可以用自然对数来进行计算,那样的话,单位叫作奈特。
下面我们来看随机事件和随机变量。假设某个事件发生的概率为p,系统在事件发生前具有N种可能的等概率状态,事件发生使得可能的状态坍缩到p×N种。在这个过程中,熵从 log ⁡ 2 N \log_{2}{N} log2N减小到 log ⁡ 2 p N \log_{2}{pN} log2pN,这个事件的熵是 log ⁡ 2 N − log ⁡ 2 p N = log ⁡ 2 p \log_{2}{N}-\log_{2}{pN}=\log_{2}{p} log2Nlog2pN=log2p
如果有一个随机变量x,它可以取集合X中的值,每个取值的概率为p(x),那么这个随机变量的嫡H就是取各种可能值的嫡的期望。
H = ∑ x ∈ X − p ( x ) log ⁡ 2 p ( x ) H=\sum_{x\in{X}}{}-p(x)\log_{2}{p(x)} H=xXp(x)log2p(x)

回到上面所说的决策树,假如一个二分类问题,正样本比例是p,那么这个节点标签的熵就是
H = − p log ⁡ x p − ( 1 − p ) log ⁡ 2 ( 1 − p ) H = -p\log_{x}{p}-(1-p)\log_{2}{(1-p)} H=plogxp(1p)log2(1p)

熵和正样本比例p是什么关系呢?
在这里插入图片描述
可以看到当样本比例接近于0或者1的时候,样本集的纯度比较高,熵很小。而正负样本比例相当时,熵达到了最大值。

四、基尼不纯度

在CART算法中,采用基尼不纯度(Gini impurity)来度量数据集的纯度。
基尼不纯度衡量了一个随机选中的样本在数据集中被分错的可能性。当基尼值大时,数据子集的纯度低,样本被分错的可能性大;当基尼值小时,数据子集的纯度高,样本被分错的可能性小。
假设样本落入了某个决策树节点代表的数据子集,在这个子集上,某个类别 i i i的样本所占比例为 p i p_{i} pi。那么,样本是类别i的概率就是 p i p_{i} pi;。真实类别为的样本落入该节点后,被错误分类为其他类别的概率是 ( 1 − p i ) (1-p_{i}) (1pi)。假设有 J J J个不同类别,那么,一个随机样本被错误分类的概率如下:
G i n i = ∑ i = 1 J p i ( 1 − p i ) = ∑ i = 1 J p i − ∑ i = 1 J p i 2 = 1 − ∑ i = 1 J p i 2 Gini = \sum_{i=1}^{J}{p_{i}(1-p_{i})} =\sum_{i=1}^{J}{p_{i}}-\sum_{i=1}^{J}{p_{i}^{2}} =1-\sum_{i=1}^{J}{p_{i}^{2}} Gini=i=1Jpi(1pi)=i=1Jpii=1Jpi2=1i=1Jpi2
考虑只有两个类别时,如果正负样本参半,那么,Gini值等于0.5,而如果全为正样本或负样本,那么Gini值为0。
可见,完全纯净的数据集的Gini值为0,而不纯净的数据集的Gini值会趋向于1。

五、代码实践

1.计算信息熵

函数以一组数组为输入,0、1分别代表负样本、正样本,如[1,1,1,1,1]表示全部为正样本

import math

# 计算概率为x 的随机事件的熵
def h(x):
    if x >= 1 or x <= 0:
        return 0
    return -x*math.log2(x)

# 计算包含0 和 1 两个类别的数据集的熵
def data_entropy(labels):
    if len(labels) < 1:
        return 0
    # 计算正样本比例
    p = sum(labels)/len(labels)
    #计算负样本比例
    n = 1 - p
    return h(p)+h(n)

2.构造决策树

书中给的数据如下,是否适合运动是需要预测的一列。
在这里插入图片描述

首先,准备数据集,将每一条数据描述为一个具有5个元素的数组,分别记录天气的观感、温度、湿度、风力和是否适合运动。
对于天气观感,用0表示晴,1表示阴,2表示雨;
对于温度,用0表示热,1表示中等,2表示凉;
对于湿度,用0表示潮湿,1表示正常;
对于风力,用0表示无风,1表示有风;
对于类标签,即是否适合运动,用0表示不适合,1表示适合。

import numpy

# 每一行的5个元素分别表示
# 天气观感 (晴、阴、雨), 温度 ,湿度, 风力和是否适合运动
weather_data = numpy.array(
    [0,0,0,0,0],
    [0,0,0,1,0],
    [0,1,0,0,0],
    [0,2,1,0,1],
    [0,1,1,1,1],
    [1,0,0,0,1],
    [1,2,1,1,1],
    [1,1,0,1,1],
    [1,0,1,0,1],
    [2,1,0,0,1],
    [2,2,1,0,1],
    [2,2,1,1,0],
    [2,1,1,0,1],
    [2,1,0,1,0]
)
# 计算某个划分的熵
# 输入是数据集、划分选取的属性和属性值
# 输出是熵 和 分割出的两个数据集
def split_entropy(data, property_id, property_value):
    # 选取property_id这一列 与 property_value进行比较
    left_index = data[:, property_id] == property_value
    right_index = data[:, property_id] != property_value

    # 根据比较结果选取分支两侧的数据子集
    left_data = data[left_index, :]
    right_data= data[right_index, :]

    #取数据子集的最后一列,即类标签列,计算子集的熵
    left_entropy = data_entropy(left_data[:,-1])
    right_entropy = data_entropy(right_data[:,-1])

    # 计算分割后两个子集的加权平均熵
    split_entropy = (left_data.shape[0] * left_entropy +
                     right_data.shape[0] * right_entropy) / data.shape[0]

    return split_entropy,left_data, right_data

print(split_entropy(weather_data,0,0)[0])
# 输出0.838
# 即用天气观感“晴”作为划分条件,分割得到的两个数据集的平均熵是 0.838

在此基础上,我们可以通过枚举所有属性和可能的划分,找到对于当前数据集最优的划分。

# 选择划分的函数
# 输入是数据集, 输出是划分后的熵、选取的属性和属性值,
# 划分后的数据子集
def find_split(data):
    min_entropy = None
    best_split_property_id = None
    best_split_property_value = None
    best_split_left = None
    best_split_right = None

    # 枚举所有属性
    for index in range(data.shape[1] - 1):
        # 获取该属性的可能取值
        unique_values = numpy.unique(data[:,index])
        if len(unique_values) < 2:
            continue
        # 枚举在这一属性上可能的划分, 划分比取值数量少1
        for value in unique_values[0:-1]:
            entropy, left, right = split_entropy(data, index, value)
            if min_entropy is None or min_entropy > entropy:
                min_entropy = entropy
                best_split_property_id = index
                best_split_property_value = value
                best_split_left = left
                best_split_right = right

    return min_entropy,best_split_property_id,best_split_property_value,\
        best_split_left,best_split_right


split = find_split(weather_data)
print('entropy = {0},\n entropy_id={1},\n entropy_value={2}'.format(split[0],split[1],split[2]))

输出结果为
entropy = 0.7142857142857143,
entropy_id=0,
entropy_value=1
可见对于上数据集,第一个划分应该选取天气观感是否为阴。这样划分后的数据集平均熵较小。

接下来构造决策树,只需要递归调用find_split函数即可。当无法产生有效划分的时候,就到达了决策树的叶节点;如果能够将数据集划分为两个子集,就可以把它们作为左右子树递归地划分下去。

# 构造决策树
# 输入参数是数据集和控制缩进用的空白
def build_desicion_tree(data, tabspace):
    class_count = len(numpy.unique(data[:,-1])) #分类数量
    # 如果数据集包含不同类别,就进行划分
    if class_count > 1:
        split = find_split(data)
    else:
        split = [None]
    # 如果无法划分,则到达决策树的叶节点
    if split[0] is None:
        print('{0}class={1}'.format(tabspace,data[0,-1]))
        return

    # 如果划分成功,左右递归划分左右子树
    print('{0}property{1} value={2}'.format(tabspace,split[1],split[2]))
    build_desicion_tree(split[3],tabspace + " ")
    build_desicion_tree(split[4],tabspace + " ")


build_desicion_tree(weather_data, '')

在这里插入图片描述
这样就得到一个类似于树的输出了。

3.使用scikit-learn

当需要深入理解某个算法或者模型的原理,或者需要对算法进行一些修改、裁剪或者改进,那么从头开始实现这个算法是很有必要的。
但有时我们也可以使用一些已经构筑好的软件包,避免重复劳动,同时它们对算法性能进行了充分的优化,排除了常见漏洞、边界情况或极端情况。
scikit-learn软件包实现了机器学习的大部分常用算法和模型。
使用

pip install scikit-learn

安装这个包。

from sklearn import tree
from matplotlib import pyplot as plt
X = weather_data[:,0:-1]
Y = weather_data[:,-1]
clf = tree.DecisionTreeClassifier()

clf.fit(X,Y)

result = clf.predict([[0,0,0,0]]
                     )
print(result)

tree.plot_tree(clf)
plt.show()  # 确保图形显示


1)scikit-learn 软件包采用的是CART算法,该算法可以更好地用于具有连续取值的属性,并且同时适用于分类和回归问题。
2)该算法将天气观感这样的离散枚举型样本属性当作连续值处理,因此,得到的划分与ID3算法不同。
值得注意的是,scikit-learn中的CART决策树算法并不支持枚举型的样本属性,只支持数值型的样本属性,也就是说,样本属性构成的矩阵X中的值必须全部为数值。严格来说,天气观感为晴、阴、雨这样的属性是枚举类型的,3个值之间并无数值上的大小关系,将它们直接映射到单个数值是稍有问题的。比如,如果设0表示晴,1表示阴,2表示雨,就隐含了这样的假设,即晴和阴之间的距离与阴和雨之间的距离是相同的,
而晴和雨之间的距离是晴和阴之间距离的2倍。这样的假设对于天气观感这个属性来说并没有太大的坏处。但对于某些枚举值更多的属性来说,将它们直接映射到整数上是有问题的。这个时候就应该采取 __独热编码__来解决
例如“晴 阴 雨” 就应该用三个维度来表示"是否晴“ ”是否阴“ ”是否雨“ 用向量分别表示为[1,0,0] 、[0,1,0]、[0,0,1]。
这样就保证了 各个枚举值之间的欧式距离一致 不会引入次序关系和距离差别。

小结一下

这一章介绍了决策树的构造方法,有ID3算法(根据最大信息增益来分裂)和基于基尼指数的算法。同时在这一部分引入了信息熵的概念。在构造决策树时,需要注意递归的逻辑处理,递归边界条件是什么。

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值