机器学习系列 06:决策树 01

  本内容将介绍用于分类的决策树(decision tree),以及 ID3、C4.5 和 CART 算法。

  决策树(decision tree)是一种基本的分类与回归方法。决策树模型呈树形结构,分为分类树、回归树和模型树,前者用于分类,后者用于预测实数值。其主要优点是模型具有可读性,分类速度快。决策树学习通常包括 3 个步骤:特征选择、决策树的生成和决策树的剪枝。这些决策树学习的思想主要来源于由 Quinlan 在 1986 年提出的 ID3 算法和 1993 年提出的 C4.5 算法,以及由 Breiman 等人在 1984 年提出的 CART 算法。

  本内容主要介绍分类决策树,不涉及回归树和模型树。可以参阅 机器学习系列:决策树 02 - CART 算法 了解 CART 算法和回归树、模型树。

一、决策树模型和学习

1.1 决策树模型

  一棵决策树包含一个根节点、若干个内部节点和若干个叶节点。图-1 为一棵决策树,椭圆形为判断模块,进行特征判断;矩形为终止模块,进行结果决策。从根节点到每个叶节点的路径对应了一个判断测试序列。

  用决策树进行预测,从根节点开始,对实例的某一特征进行判断,根据判断结果,将实例分配到其子节点。如此递归地对实例进行判断并分配,直至到达叶节点。最后根据叶节点对实例进行决策,分类树输出类别,回归树输出预测值。

在这里插入图片描述
                        图-1 决策树模型

1.2 决策树学习

  决策树的学习,也称为决策树的生成或决策树的构造。决策树学习过程:得到原始数据集,选择最优划分特征,并根据此特征划分数据集,数据将被向下传递到子节点。如果子节点上的数据满足特定条件,将构建叶节点;否则,再次选择最优特征划分数据集。如此递归地划分数据集,最终将形成一棵决策树。决策树的生成就是递归地构建树的过程。

  以分类树进行说明,上面提到的特定条件为:

  • 当前节点包含的样本全属于同一类别,无需划分。
  • 当前可划分属性集为空,或是所有样本在所有属性上取值相同,无法划分。
  • 当前节点包含的样本集合为空,不能划分。

  对上面提到的情况,构造叶节点的类别设定方法:

  • 设定为该节点所含样本的类别(所有样本为同一类别)。
  • 设定为该节点所含样本最多的类别(即采用多数表决法决定叶节点的类别)。
  • 设定为其父节点所含样本最多的类别。

  决策树学习的目的是为了产生一棵泛化能力强,即处理未见实例能力强的决策树。其基本流程如下:

  

函数 create_tree() 的伪代码大致如下:
create_tree():
    if 数据集满足特定条件:
        将该节点存为叶节点
        return
    end if
    
    根据最优特征划分数据集,产生多个子集
    for 每个划分的子集:
        if 子集为空:
            构造叶结点,其类别为父节点所含样本最多的类别
        else:
            调用函数 create_tree() 递归划分子集
        end if
    end for

  决策树是一种贪心算法,它要在给定时间内做出最佳选择,但并不关心能否达到全局最优。

  接下来我们需要了解如何选择最优划分特征,以及如何划分数据集。

二、特征选择

  特征选择在于选取对训练数据具有分类能力的特征。这样可以提高决策树学习的效率。通常特征选择的准则有信息增益、信息增益比和基尼指数等。在具体介绍它们之前,我们先来了解一下熵和条件熵。

2.1 熵与条件熵

2.1.1 熵

  在信息论与概率统计中,(entropy,又被称为信息熵)是表示随机变量不确定性的度量。假设 XX 是一个取有限个值的离散随机变量,其概率分布为

P(X=xi)=pi,i=1,2, ,n P(X=x_i) = p_i,\quad i=1,2,\cdots,n

则随机变量 XX 的熵定义为

(1)H(X)=i=1npilogpi H(X) = -\sum_{i=1}^{n} p_i logp_i \tag{1}

约定若 pi=0p_i = 0 时,pilogpi=0p_ilogp_i = 0H(X)H(X) 的最小值为 0,最大值为 lognlogn。通常,式(1)中的对数以 2 、自然常数或 10 为底,熵的单位分别称为比特(bit)、纳特(nat)或 Hart。熵越大,随机变量的不确定性就越大。

2.1.2 条件熵

  假设随机变量 (X,Y)(X, Y) 的联合概率分布为

P(X=xi,Y=yj)=pij,i=1,2, ,n;j=1,2, ,m P(X=x_i, Y=y_j)=p_{ij}, \quad i=1,2,\cdots,n;j=1,2,\cdots,m

  条件熵 H(YX)H(Y|X) 表示在已知随机变量 XX 的条件下随机变量 YY 的不确定性。随机变量 XX 给定条件下 YY 的条件熵(conditional entropy)H(YX)H(Y|X),定义为 XX 给定条件下 YY 的条件概率分布的熵对 XX 的数学期望

(2)H(YX)=i=1npiH(YX=xi),pi=P(X=xi);i=1,2, ,n H(Y|X) = \sum_{i=1}^n p_i H(Y|X=x_i), \quad p_i = P(X=x_i); i=1,2,\cdots,n \tag{2}

  当熵和条件熵中的概率由数据估计(特别是极大似然估计)得到时,所对应的熵和条件熵分别称为经验熵(empirical entropy)和经验条件熵(empirical conditional entropy)。

2.2 信息增益

  信息增益(information gain)表示得知特征 XX 的信息而使得类 YY 的信息的不确定性减少的程度。

  特征 AA 对训练数据集 DD 的信息增益 Gain(D,A)Gain(D,A),定义为集合 DD 的经验熵 H(D)H(D) 与特征 AA 给定条件下 DD 的经验条件熵 H(DA)H(D|A) 之差,即

(3)Gain(D,A)=H(D)H(DA) Gain(D,A) = H(D) - H(D|A) \tag{3}

  假设训练数据集为 DD,存在以下设定:

  • D|D| 为其样本容量,即样本个数。
  • 训练数据集 DDKK 个类 CkC_kk=1,2, ,Kk=1,2,\cdots,KCk|C_k| 为属于类 CkC_k 的样本个数。
  • 特征 AAnn 个不同的取值 {a1,a2, ,an}\{a_1,a_2,\cdots,a_n\},根据特征 AA 的取值将 DD 划分为 nn 个子集 D1,D2, ,DnD_1,D_2,\cdots,D_nDi|D_i|DiD_i 的样本个数。
  • 子集 DiD_i 中属于类 CkC_k 的样本的集合为 DikD_{ik}Dik|D_{ik}|DikD_{ik} 的样本个数。

  则使用特征 AA 划分训练数据集 DD 获得的信息增益为

(4)Gain(D,A)=(k=1KCkDlogCkD)(i=1nDiDH(Di)) Gain(D,A) = \Big(-\sum_{k=1}^{K} \frac{|C_k|}{|D|}log\frac{|C_k|}{|D|}\Big) - \Big(-\sum_{i=1}^{n} \frac{|D_i|}{|D|}H(D_i) \Big) \tag{4}

其中,H(Di)H(D_i)

(5)H(Di)=k=1KDikDilogDikDi H(D_i) = -\sum_{k=1}^{K} \frac{|D_{ik}|}{|D_i|} log\frac{|D_{ik}|}{|D_i|} \tag{5}

  ID3 算法就是使用信息增益来选择划分特征。

2.3 信息增益率

  信息增益准则对可取值数目较多的特征有所偏好,为减少这种偏好可能带来的不利影响,可使用信息增益率(information gain ratio)来选择最优划分特征。

(6)Gain_ratio(D,A)=Gain(D,A)IV(A) Gain\_ratio(D,A) = \frac{Gain(D,A)}{IV(A)} \tag{6}

其中,IV(A)IV(A)

(7)IV(A)=i=1nDiDlogDiD IV(A) = -\sum_{i=1}^{n} \frac{|D_i|}{|D|} log\frac{|D_i|}{|D|} \tag{7}

称为特征 AA 的“固有值”(intrinsic value)。特征 AA 的可能取值数目越多(即 nn 越大),则 IV(A)IV(A) 的值通常会越大。

   C4.5算法就是使用信息增益率来选择划分特征。 需注意的是,增益率准则对可取值数目较少的特征有所偏好。因此,C4.5 算法并不是直接选择增益率最大的候选划分特征,而是使用了一个启发式:先从候选划分特征中找出信息增益高于平均水平的特征,再从中选择增益率最高的。

2.4 基尼指数

  基尼指数(Gini index)定义为

(8)Gini(D)=k=1Kkkpkpk Gini(D) = \sum_{k=1}^{K} \sum_{k^{'} \neq k}p_kp_{k^{'}} \tag{8}

(9)=k=1Kpk(1pk) = \sum_{k=1}^{K} p_k(1-p_k) \tag{9}

(10)=1k=1Kpk2 = 1 - \sum_{k=1}^{K}p_k^2 \tag{10}

  从上面的公式可知,基尼指数有两种不同理解形式:

  • 从式(8)来看,Gini(D)Gini(D) 表示从数据集 DD 中随机抽取两个样本,其类别标记不一致的概率。
  • 从式(9)来看,Gini(D)Gini(D) 表示从数据集 DD 中随机选中的一个样本被分错的概率。

  因此,Gini(D)Gini(D)​ 越小,则数据集 DD​ 的纯度越高。

  则使用特征 AA 划分训练数据集后的基尼指数为

(11)Gini(D,A)=i=1nDiDGini(Di) Gini(D,A) = \sum_{i=1}^{n} \frac{|D_i|}{|D|} Gini(D_i) \tag{11}

其中 Gini(Di)Gini(D_i)

(12)Gini(Di)=1k=1K(DikDi)2 Gini(D_i) = 1 - \sum_{k=1}^{K} \Big(\frac{|D_{ik}|}{|D_i|}\Big)^2 \tag{12}

  CART 算法进行分类时,就是使用基尼指数来选择划分特征。

三、划分数据集方法

3.1 离散属性

  针对离散属性,可以采用多分法或者二分法划分数据集。假设有数据集 DD,其中属性 AAnn 个不同的取值,根据属性 AA 的取值将数据集划分为 nn 个子集,就是多分法;选择其中的某个取值 aia_i,根据该值将数据集划分为 2 个子集 D1D_1D2D_2D1D_1 包含属性 AA 不大于 aia_i 的样本,D2D_2 包含属性 AA 大于 aia_i 的样本,就是二分法。

  注意:如果采用多分法后,该属性不能作为其后代节点的划分属性;如果采用二分法,该属性可以继续作为其后代节点的划分属性。

3.2 连续属性

  由于连续性属性的可取值不再有限,因此,不能直接根据连续型属性的可取值来对节点进行划分。此时,连续型属性离散化技术可派上用场。最简单的策略是采用二分法(bi-partition)对连续属性进行处理,C4.5 和 CART 决策树算法都使用该方法。

  注意:因为连续属性只能采用二分法,所以该属性还可作为其后代节点的划分属性。

  ID3 算法不能处理连续属性,C4.5 和 CART 算法可以处理连续型属性。

四、决策树的剪枝

  在决策树学习中,为了尽可能对数据集进行正确分类,从而构建出过于复杂的决策树。这样产生的树往往对训练数据集的分类很正确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。可以通过降低对决策树的复杂度(即去掉一些分支)来解决这个问题,这个过程称之为剪枝(pruning)。

  决策树剪枝有预剪枝(prepruning)和后剪枝(postpruning)两种。预剪枝是指在决策树生成过程中,对每个节点在划分前后进行估计,若当前节点的划分不能带来决策树泛化性能提升,则停止划分并将当前节点标记为叶节点。后剪枝则是先从训练集生成一棵完整的决策树;然后从上到下找到叶节点,用测试集来判断将这些叶节点合并是否能降低测试误差,如果能就进行合并。

  决策树的生成对应于模型的局部选择,决策树的剪枝对应于模型的全局选择。决策树的生成只考虑局部最优,相对地,决策树的剪枝则考虑全局最优。

  在 机器学习系列:决策树 02 - CART 算法 里面会讲到剪枝的具体实现。

五、代码实现

  在生成分类决策树时,ID3、C4.5 和 CART 三种算法的流程基本相同,基本上只需要在特征选择和特征划分时进行不同处理即可。下面给出 ID3 算法的 Python 代码实现(Python 3.x):

from math import log


class DecisionTreeID3:
    def __init__(self):
        pass

    def creat_tree(self, data_set, feature_labels):
        """
        创建决策树

        data_set:创建决策树的数据集
        feature_labels:特征名列表
        """
        labels_list = [sample[-1] for sample in data_set]
        # 数据集中的样本属于同一个类别,返回这个类别
        if labels_list.count(labels_list[0]) == len(labels_list):
            return labels_list[0]
        # 当数据集中已经没有可划分的属性,采用多数表决法返回类别
        if len(data_set[0]) == 1:
            return self._majority_cnt(labels_list)

        # 选择最优划分属性
        best_feature = self._choose_best_feature_to_split(data_set)
        best_feature_label = feature_labels[best_feature]
        # 初始化树
        tree = {best_feature_label: {}}
        # 移除已经选择的划分属性
        del feature_labels[best_feature]

        # 获取当前划分属性的所有取值
        feature_values = [sample[best_feature] for sample in data_set]
        # set() 可以去除 feature_values 中存在的相同值
        unique_values = set(feature_values)
        for value in unique_values:
            sub_feature_labels = feature_labels[:]
            tree[best_feature_label][value] = self.creat_tree(
                self._split_data_set(data_set, best_feature, value),
                sub_feature_labels)

        return tree

    def _calc_shannon_ent(self, data_set):
        """
        计算信息熵
        """
        # 计算数据集中样本数量
        samples_num = len(data_set)
        labels_count = {}
        # 遍历数据集中所有样本,计算每种类别的样本数量
        for sample in data_set:
            # 获取样本的标签值,即类别
            cur_label = sample[-1]
            if cur_label not in labels_count.keys():
                labels_count[cur_label] = 0
            labels_count[cur_label] += 1
        shannon_ent = 0.0
        # 求信息熵
        for count in labels_count.values():
            prob = float(count)/samples_num
            shannon_ent -= prob*log(prob, 2)
        return shannon_ent

    def _split_data_set(self, data_set, axis, value):
        """
        按照给定特征划分数据集,返回数据集中满足特征值为 value 的子集

        data_set:待划分的数据集
        axis:划分数据集的特征
        value:划分数据集的特征值
        """
        sub_data_set = []
        for feat_vec in data_set:
            if feat_vec[axis] == value:
                reduced_feat_vec = feat_vec[:axis]
                reduced_feat_vec.extend(feat_vec[axis+1:])
                sub_data_set.append(reduced_feat_vec)
        return sub_data_set

    def _choose_best_feature_to_split(self, data_set):
        """
        选择最好的划分属性

        data_set:待划分的数据集
        """
        # 获取数据集的样本数量
        samples_num = len(data_set)
        # 获取样本特征值数量
        feature_num = len(data_set[0]) - 1
        # 计算数据集未划分前的信息熵
        base_entropy = self._calc_shannon_ent(data_set)
        best_info_gain = 0.0
        best_feature = -1
        # 遍历每一个特征
        for i in range(feature_num):
            feature_values = [sample[i] for sample in data_set]
            unique_vals = set(feature_values)
            new_entropy = 0.0
            # 遍历当前特征的每一个值,并计算出划分数据集后信息熵
            for value in unique_vals:
                sub_data_set = self._split_data_set(data_set, i, value)
                prob = float(len(sub_data_set))/samples_num
                new_entropy -= prob*self._calc_shannon_ent(sub_data_set)
            info_gain = base_entropy - new_entropy
            # 获取信息增益最高的划分特征和特征值
            if info_gain > best_info_gain:
                best_info_gain = info_gain
                best_feature = i
        return best_feature

    def _majority_cnt(self, labels_list):
        """
        返回数量最多的类型

        labels_list:类型列表
        """
        labels_count = {}
        for label in labels_list:
            if label not in labels_count.keys():
                labels_count[label] = 0
            labels_count[label] += 1
        sorted_labels_count = \
            sorted(labels_count.items(), key=lambda x: x[1], reverse=True)
        return sorted_labels_count[0][0]


def get_training_datas():
    data_set = [[1, 1, 'yes'],
                [1, 1, 'yes'],
                [1, 0, 'no'],
                [0, 1, 'no'],
                [0, 1, 'no']]
    feature_labels = ['no surfacing', 'flippers']
    return data_set, feature_labels


if __name__ == "__main__":
    data_set, feature_labels = get_training_datas()
    decision_tree_mode = DecisionTreeID3()
    decision_tree = decision_tree_mode.creat_tree(data_set, feature_labels)
    print(decision_tree)

疑问:

  1. C4.5 对离散型属性是采用二分还是多分?

参考:
[1] 周志华《机器学习》
[2] 李航《统计学习方法》
[3] 《机器学习实战》

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