【机器学习】决策树 总结

具体的细节概念就不提了,这篇blog主要是用来总结一下决策树的要点和注意事项,以及应用一些决策树代码的。

一、决策树的优点:

易于理解和解释。数可以可视化。也就是说决策树属于白盒模型,如果一个情况被观察到,使用逻辑判断容易表示这种规则。相反,如果是黑盒模型(例如人工神经网络),结果会非常难解释。
几乎不需要数据预处理。其他方法经常需要数据标准化,创建虚拟变量和删除缺失值。在sklearn中的决策树还不支持缺失值处理。
• 利用训练好的决策树进行分类或回归的时间复杂度仅是训练生成决策树的时间的对数,时间复杂度大大缩小。
• 既可以处理数值变量,也可以处理分类数据,也就是说,如果数据类型不同,也可以在一颗决策树中进行训练。其他方法通常只适用于分析一种类型的变量。
• 可以处理多值输出变量问题。
• 可以使用统计检验验证模型的可靠性。
• 如果真实的数据不符合决策树构建时的假设条件,也可以较好的适用

二、决策树的缺点:

• 决策树学习可能创建一个过于复杂的树,降低泛化性能,这就是过拟合。修剪机制(现在不支持),构建决策树时设置一个叶子节点需要的最小样本数量,或者树的最大深度,都可以避免过拟合。
• 决策树容易受到噪声影响,同样的数据如果略微改变可能生成一个完全不同的树。这个问题通过集成学习来缓解。
• 学习一颗最优的决策树是一个**NP-完全问题**under several aspects of optimality and even for simple concepts。所以,考虑计算量,在实际工程当中,决策树算法采用启发式算法,例如贪婪算法,可以保证局部最优解,但无法确保是全局最优解。这个问题可以采用集成学习来解决。
• 决策树对于一些问题有局限性,主要是这些问题难以用条件判断来解决。例如,异或问题, parity,multiplexer问题等等.
• 如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,建议在训练之前,先抽样使样本均衡

三、决策树的复杂度:

构建一个决策树的时间复杂度是 o(n^2*m*log(m)),其中,m是训练样本的数量,m是训练样本的特征数量。而利用决策树分类或回归一个测试样本的时间复杂度是 o(log(n)),也就是说,训练样本数量越多,越容易造成决策树深度爆炸。
深度爆炸的主要原因就是重复计算,
A naive implementation (as above) would recompute the class label histograms (for classification) or the means (for regression) at for each new split point along a given feature.
克服这种问题的普适性方法就是预先划分属性Presorting the feature over all relevant samples, and retaining a running label count, will reduce the complexity at each node to , which results in a total cost of . This is an option for all tree based algorithms. By default it is turned on for gradient boosting, where in general it makes training faster, but turned off for all other algorithms as it tends to slow down training when training deep trees.

以上一段是我没有理解的,英文原文粘上来了,如果看官有懂的帮我解释一下。

四、决策树在实际工程使用过程中应当注意以下:

  • 决策树在特征非常多的时候很容易导致过拟合,如果训练样本还比较少,那就更容易过拟合,毕竟决策树的本质是轴平行的分类回归算法。所以要控制好训练样本数量和特征数量之间的比值
  • 由于特征多了容易产生很多麻烦,所以预先采用降维方法能够更好地找到有代表性的,能够起分类作用的特征。这些方法主要有PCA、ICA、特征选择。
  • 先从最大树深为3开始,训练构建决策树,观察生成的树的结构,看看是否符合预期,然后再逐渐地加深树的深度。
  • 确定设置一个叶子节点的最小样本支撑值太小,则容易造成过拟合,而太大会造成学习能力不足. 训练开始时,首先设置成5,然后慢慢修改。
  • 如果某些特征的值太大,而其他特征的值太小,这时就需要在特征上加权值。权值设置的方法就是将训练数据的特征的值全部相加,归一化,使每一个特征都具有相同的特征的值,这个归一化的参数就是权值。特征均衡之后,预剪枝的效果也会更好,偏差bias更小。

五、几种决策树算法的比较:

  • ID3 (Iterative Dichotomiser 3):采用贪婪策略,按照信息增益来计算分类目标,划分叶子节点。剪枝策略采用后减枝。
  • C4.5:继承自ID3,但是数据类型不一定非得是离散类型了。C4.5具有很清晰易懂的if else语句能够描述决策树的结构。也是后减枝,具体策略是预删除树结点看泛化性能是否下降。
    C5.0是一个更新版本,它消耗的内存更低,决策树构建的更小,但是精度更高。
  • CART (Classification and Regression Trees) 与C4.5非常相似,区别在于它支持目标Y值是连续型,也不计算规则集合。
    Sklearn包里采用的是优化的 CART 算法,当然计算因子可以从信息增益和基尼系数中选择。优化主要是时间复杂度上的优化,sklearn包里的算法预先将训练样本里的特征排序,这样就将时间复杂度缩减到了o(n*m*log(m))。,即采用基尼系数来计算。但是它不支持缺失值处理,也不支持剪枝策略。数据类型都是np.float32的。如果输入的训练样本构成的矩阵是稀疏矩阵,则sklearn包提供了优化算法,训练时间大大缩短。

尽管不同的方法采用的计算因子不同,有的是信息增益,有的是基尼系数,但是这些对泛化性能的影响都不大。

六、一种特殊情况:

预测值Y是一组n个的数组,这样需要考虑这个样本的n个值是否是一致的。
如果是相互独立的,那就可以把问题化为一维的情况进行处理,也就是基础的分类和回归。但是如果n个数值之间有相互关系,那就需要将这n维数据同时进行预测,sklearn包里提供了这样的功能,比如,给定一个实数,预测其正弦和余弦,还有面部预测,但是看效果不如神经网络的好啊。

七、树的代码:

1、sklearn包中的代码

DecisionTreeClassifier(class_weight=None,  # 指某个类别所占的权重。字典类型,或以字典为元素的列表类型,或 balanced,或默认 None,一般单值分类问题都默认,即所有的权重都是1,对多输出问题,由于需要各个维度配合,所以经常需要处理数据以方便处理。如果一堆训练数据,其中一类的数据量特别小,而其他类的数据量巨大则可以采用 balanced,根据类别标签的频率,使频率低的权重提高,达到重视少数样本的目的。
                       criterion='entropy',  # 构造决策树时选择属性的准则,entropy指信息增益,gini指基尼系数,还有一个信息增益率,这里不支持
                       max_depth=160,  # 最大树深,一般不设置,它和每个节点最小需要的支持样本数相辅相成,共同决定数的结构
                       max_features=None,  # 默认所有的属性特征总数,指寻找最优分支的时候要计算多少个属性,默认就是所有属性全部计算,如果是整数,那么就从整数个属性里选,如果是小数,按照百分比来计算,如果是 auto 或 sqrt,就是按照总属性数目开根号计算,如果是 log2,就按照取对数来计算。其目的主要是在属性数量太多的时候,加快计算速度。
                       max_leaf_nodes=None,  # 默认为 None,没有限制,指的是在分割数据集时,选用的属性对分割后的结果有限制,其中的一个结点拥有的样本数不允许超过此参数值。目的是最大化降低不纯度。
                       min_samples_leaf=1,  # 默认是1,指在构建树结点的时候,如果仅有一个子节点划分在它下面,说明随机性很大,就不允许这个样本单独来构造一个树节点。
                       min_samples_split=3,  # 默认是2,指如果某个结点中仅有2个样本划分在它这里,则不构造子结点来对它分割。如果是整数,那么这个整数就是所需的样本数目如果是小数,必须得小于1,指占总训练数据的比例必须大于该数才有资格划分结点
                       min_weight_fraction_leaf=0.0,  # 默认是0.0,The minimum weighted fraction of the sum total of weights (of allthe input samples) required to be at a leaf node. Samples haveequal weight when sample_weight is not provided.
                       min_impurity_decrease=0.0,  # 构造决策树的过程就是在降低不纯度,构造结点的时候,如果不纯度降低的值比这个要求的最小值还小,则说明该结点选择不够最优,重新选择。
                       min_impurity_split=0.0,  # 指如果一个结点的不纯度低于这个参数,说明已经够纯了,不用再划分了,反之则需要继续切分。
                       presort=False,  # 布尔类型,默认为False,指预处理,预先分类。是否加速训练速度,设置成True可能会降低训练速度。
                       random_state=None,  # 在选择属性等过程中需要随机处理,也就需要随机数,若指定整数,则该数就是随机实例的seed值,如果是一个指定的随机实例,则使用它,如果采用默认None,会默认采用 np.random
                       splitter='best'  # best指从满足准则的里面选取信息增益最大的,gini系数最大的。random指并非从符合准则的里面选最优的,而是给出几个相对较优的,随机从里面选一个。这样做主要是为了克服criterion对某些取值数目较少的属性有偏好
                       )

# 其预测结果也可以选择,一种就是直接给出到底是哪个类别,第二种是给出属于每一类的概率值,再就是给出概率值的对数。

DecisionTreeRegressor(criterion='mse',  # 回归问题采用的划分准则,mse指最小均方误差,是L2损失,friedman_mse是采用福雷曼增益值的最小均方误差。mae 指平均绝对误差,是L1损失。
                     )


ExtraTreeRegressor(# 是从上面两个标准的继承过来的,含义是极度随机化的树分类和树回归。其含义是在选择属性构造子节点时,是从最大特征数中随机选择一些进行构造。选择哪些则是完全随机的。这么做的目的是为了提高训练速度,避免过拟合,陷入极小点。
)

2、《深入机器学习实战》中的例子代码

我给它添加了详细的注释,网上很多blog都抄 了它的代码。但是在这个代码中只是一个示范代码,仍存在很多的问题,主要就是可调节参数太少,支持的机制太少,仅仅能够用于处理简单离散分类问题。


class C45DecisionTree(object):
    '''
        该决策树仅仅是决策树的最简版本,其局限性:
        1、不能处理连续取值的属性值,
        2、不能处理某些属性没有取值的数据,
        3、没有剪枝策略,
        4、数据没有预处理机制,
        5、控制参数缺失,如最大树深等,
        6、不支持多变量决策树。
        该类的使用方法:
            c45 = C45DecisionTree()
            myTree = c45.createTree(myDat, labels)  # 输入训练数据和标签,构造决策树
            print myTree  # 查看决策树的形状
            predict = c45.classify(myTree, labels, testData)  # 输入构造的树,标签集合,测试数据(只能是一条)
    '''

    def __init__(self):
        import math
        import operator
        self.my_tree = {}  # 用来查看构造出来的决策树

    def calc_shannon_entropy(self, data_set):
        '''
            计算给定数据集的香浓熵。
            data_set的结构是一个列表,其中的每个元素都是一个数据项,数据项结构依然是一个列表,前面n-1项都是属性对应的值,最后一项是分类结果
        '''
        num_entries = len(data_set)
        label_counts = {}  # 类别字典(类别的名称为键,该类别的个数为值)
        for feat_vector in data_set:  # 读取数据集中的每一条数据,统计每一条数据的类别 label 出现的次数,得到一个字典
            current_label = feat_vector[-1]  # 最末尾的一个数值
            if current_label not in label_counts.keys():  # 还没添加到字典里的类型,获取字典里的所有键值
                label_counts[current_label] = 0
            label_counts[current_label] += 1
        shannon_entropy = 0.0
        for key in label_counts:  # 求出每种类型的熵,将所有的熵相加,得到信息熵
            prob = float(label_counts[key]) / num_entries  # 每种类型个数占所有的比值
            shannon_entropy -= prob * math.log(prob, 2)  # 计算熵
        return shannon_entropy  # 返回熵

    def split_data_set(self, data_set, axis, value):
        '''
            按照给定的特征划分数据集,特征就是第axis列对应的特征,该特征值等于value的划分为一个集合,作为该函数的返回
        :param dataSet:数据集不解释
        :param axis:数据集中每一项数据的第axis列的属性
        :param value:给定的第axis列的可能的取值中的一个
        :return:返回将第axis列的属性数据删除之后的数据集
        '''
        ret_data_set = []
        for feat_vector in data_set:  # 按dataSet矩阵中的第axis列的值等于value的分数据集
            if feat_vector[axis] == value:  # 值等于value的,每一行为新的列表(去除第axis个数据)
                reduced_feat_vector = feat_vector[:axis]
                reduced_feat_vector.extend(feat_vector[axis + 1:])  # 也就是说,去掉了第 axis 维的数据
                ret_data_set.append(reduced_feat_vector)
        return ret_data_set  # 返回分类后的新矩阵

    def choose_best_feature_to_split(self, data_set):
        '''
            选择最好的数据集划分方法,即找出来某个属性,作为决策树下一步要采用的划分属性。按照信息增益来找的
            但是针对离散数据比较好操作,针对连续数据这里需要增加代码
        :param data_set:数据集
        :return:找到到底按照哪个属性来划分决策树,返回该属性的索引
        '''
        num_features = len(data_set[0]) - 1  # 求属性的个数
        base_entropy = self.calc_shannon_entropy(data_set)
        best_info_gain = 0.0  # 要选择最高的信息增益
        best_feature = -1  # 最好的特征的索引,-1表示不存在,还没开始选择
        for i in range(num_features):  # 求所有属性的信息增益
            feat_list = [example[i] for example in data_set]  # 将数据集中所有的第i个特征值全部提取出来,组成一个列表
            unique_vals = set(feat_list)  # 第 i列属性的取值(不同值)数集合,对于离散的数据值还比较好用,如果是连续数据,数据量太大
            new_entropy = 0.0
            split_info = 0.0
            for value in unique_vals:  # 求第 i列属性每个不同值的熵 *他们的概率
                sub_data_set = self.split_data_set(data_set, i, value)  # 子数据集,是按照 i属性划分成的多个部分的其中一个
                prob = len(sub_data_set) / float(len(data_set))  # 求出该值在i列属性中的概率
                new_entropy += prob * self.calc_shannon_entropy(sub_data_set)  # 求i列属性各值对于的熵求和
                split_info -= prob * math.log(prob, 2)
            info_gain = (base_entropy - new_entropy) / split_info  # 求出第i列属性的信息增益率
            # print infoGain
            if info_gain > best_info_gain:  # 保存信息增益率最大的信息增益率值以及所在的下表(列值i)
                best_info_gain = info_gain
                best_feature = i
        return best_feature

    def majority_count(self, class_list):
        '''
            找到出现次数最多的分类的名称,即每个数据项的最后一列,分类的结果标签
        :param class_list: 类别列表
        :return: 返回的值是类别最多的那一类的类名
        '''
        class_count = {}  # 类别计数
        for vote in class_list:
            if vote not in class_count.keys():  # 增加分类,class_count就是标明了每一个类有多少个数据项
                class_count[vote] = 0
            class_count[vote] += 1
        sorted_class_count = sorted(class_count.iteritems(), key=operator.itemgetter(1), reverse=True)
        # 将字典可迭代化,按照迭代化后的每个数据项的1索引的取值排序,逆序排序,其实完全没有必要,时间复杂度比较高
        return sorted_class_count[0][0]

    def create_tree(self, data_set, labels):
        '''
            创建决策树。很明显,使用过的属性不可以再使用,所以对于连续属性取值不可用,且树的深度也不会很深。
        :param data_set: 数据集不多解释
        :param labels: 指属性的名字,字符串类型的列表
        :return:
        '''
        class_list = [example[-1] for example in data_set]  # 训练数据的结果列表(例如最外层的列表是[N, N, Y, Y, Y, N, Y])
        if class_list.count(class_list[0]) == len(class_list):  # 如果所有的训练数据都是属于一个类别,则返回该类别
            return class_list[0]
        if len(data_set[0]) == 1:  # 训练数据只给出类别数据(没给任何属性值数据),返回出现次数最多的分类名称
            return self.majority_count(class_list)

        best_feat = self.choose_best_feature_to_split(data_set)  # 选择信息增益最大的属性进行分(返回值是属性类型列表的下标)
        best_feat_label = labels[best_feat]  # 根据下表找属性名称,当作树的根节点,树结构用字典来表示
        my_tree = {best_feat_label: {}}  # 以best_feat_label为根节点建一个空树
        del (labels[best_feat])  # 从属性列表中删掉已经被选出来当根节点的属性,当数据属于连续类型的时候,这一条就是错误的
        feat_values = [example[best_feat] for example in data_set]  # 找出该属性所有训练数据的值(创建列表)
        unique_vals = set(feat_values)  # 求出该属性的所有值的集合(集合的元素不能重复)
        for value in unique_vals:  # 根据该属性的值求树的各个分支,如果该属性为真那该怎样,为假又该怎样……
            sub_labels = labels[:]  # 已经是剔除了上面已选的属性之后的子属性集合
            my_tree[best_feat_label][value] = self.create_tree(self.split_data_set(data_set, best_feat, value),
                                                               sub_labels)  # 根据各个分支递归创建树
        self.my_tree = my_tree
        return my_tree  # 生成的树

    def classify(self, input_tree, feat_labels, test_vec):
        '''
            使用决策树进行分类
        :param input_tree: 输入决策树,树结构按字典给出的
        :param feat_labels: 特征标签,列表,顺序和数据集中的属性排列顺序一致
        :param test_vec: 测试数据集
        :return: 返回分类的标签
        '''
        first_str = input_tree.keys()[0]  # 使用的决策树的所有的键的第一个
        second_dict = input_tree[first_str]
        feat_index = feat_labels.index(first_str)  # 找到类别标签的索引
        for key in second_dict.keys():  # 第二层判断
            if test_vec[feat_index] == key:  # 找到使用哪颗子树
                if type(second_dict[key]).__name__ == 'dict':
                    class_label = self.classify(second_dict[key], feat_labels, test_vec)  # 继续分类
                else:
                    class_label = second_dict[key]  # 已经到了树的叶子节点,它指示什么类别就是什么了。
        return class_label

猜你喜欢

转载自blog.csdn.net/dongrixinyu/article/details/78808753