全栈工程师开发手册 (作者:栾鹏)

python数据挖掘系列教程

决策树简介

决策树算是最好理解的分类器了。决策树就是一个多层if-else函数,就是对对象属性进行多层if-else判断,获取目标属性(类标签)的类别。由于只使用if-else对特征属性进行判断,所以一般特征属性为离散值,即使为连续值也会先进行区间离散化。

这里写图片描述

在机器学习中,决策树是一个预测模型,他代表的是对象属性与类别属性之间的一种映射关系。

分类决策树概念:是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部结点和叶结点。内部结点表示一个特征或属性,叶结点表示一个分类。

思考:选哪些特征属性参与决策树建模、哪些属性排在决策树的顶部,哪些排在底部,对属性的值该进行什么样的判断、样本属性的值缺失怎么办、如果输出不是分类而是数值能用么、对决策没有用或没有多大帮助的属性怎么办、什么时候使用决策树?

使用决策树做预测需要以下过程:

1、收集数据:可以使用任何方法。比如想构建一个相亲系统,我们可以从媒婆那里,或者通过参访相亲对象获取数据。根据他们考虑的因素和最终的选择结果,就可以得到一些供我们利用的数据了。

2、准备数据:收集完的数据,我们要进行整理,将这些所有收集的信息按照一定规则整理出来,并排版,方便我们进行后续处理。

3、分析数据:可以使用任何方法,决策树构造完成之后,我们可以检查决策树图形是否符合预期。

4、训练算法:这个过程也就是构造决策树,同样也可以说是决策树学习,就是构造一个决策树的数据结构。

5、测试算法:使用经验树计算错误率。当错误率达到了可接收范围,这个决策树就可以投放使用了。

信息增益

信息增益表示得知特征 X j X^j Xj的信息而使所属分类的不确定性减少的程度。

特征 A A A对训练数据集 D D D的信息增益 g ( D , A ) g(D,A) g(D,A),定义为集合 D D D的经验熵 H ( D ) H(D) H(D)与特征A给定的情况下D的经验条件熵 H ( D ∣ A ) H(D|A) H(DA)之差。
g ( D , A ) = H ( D ) − H ( D ∣ A ) g(D,A) =H(D) - H(D|A) g(D,A)=H(D)H(DA)
假设数据集D有K种分类,特征A有n种取值可能。

其中数据集D的经验熵 H ( D ) H(D) H(D)

H ( D ) = − ∑ k = 1 K P k l o g 2 P k H(D)=-\sum_{k=1}^K P_klog_2^{P_k} H(D)=k=1KPklog2Pk 其中 P k P_k Pk为集合D中的任一样本数据分类k的概率,或者说属于分类k的样本所占的比例。

经验条件熵 H ( D ∣ A ) H(D|A) H(DA)

H ( D ∣ A ) = ∑ i = 1 n P i H ( D i ) H(D|A)=\sum_{i=1}^n P_i H(D_i) H(DA)=i=1nPiH(Di) 其中 P i P_i Pi为特征取值为第i个可取值的概率。 D i D_i Di为特征A为第i个可取值的样本集合。

信息增益比

特征 A A A对训练数据集 D D D的信息增益 g R ( D , A ) g_R(D,A) gR(D,A),定义为其信息增益 g ( D , A ) g(D,A) g(D,A)与训练集D的经验熵 H ( D ) H(D) H(D)之比。

g R ( D , A ) = g ( D , A ) / H ( D ) g_R(D,A) = g(D,A) / H(D) gR(D,A)=g(D,A)/H(D)

是为了矫正在训练数据集的经验熵大时,信息增益值会偏大,反之,信息增益值会偏小的问题。

决策树ID3算法的思路

ID3算法就是用信息增益大小来判断当前节点应该用什么特征来构建决策树,用计算出的信息增益最大的特征来建立决策树的当前节点。

不足:

ID3算法虽然提出了新思路,但是还是有很多值得改进的地方。

a)ID3没有考虑连续特征,比如长度,密度都是连续值,无法在ID3运用。这大大限制了ID3的用途。

b)ID3采用信息增益大的特征优先建立决策树的节点。很快就被人发现,在相同条件下,取值比较多的特征比取值少的特征信息增益大。比如一个变量有2个值,各为1/2,另一个变量为3个值,各为1/3,其实他们都是完全不确定的变量,但是取3个值的比取2个值的信息增益大。如果校正这个问题呢?

c) ID3算法对于缺失值的情况没有做考虑

d) 没有考虑过拟合的问题

ID3 算法的作者昆兰基于上述不足,对ID3算法做了改进,这就是C4.5算法

决策树C4.5算法

上一节我们讲到ID3算法有四个主要的不足,一是不能处理连续特征,第二个就是用信息增益作为标准容易偏向于取值较多的特征,最后两个是缺失值处理的问和过拟合问题。昆兰在C4.5算法中改进了上述4个问题。

对于第一个问题,不能处理连续特征, C4.5的思路是将连续的特征离散化。
对于第二个问题,信息增益作为标准容易偏向于取值较多的特征的问题。我们引入一个信息增益比的变量 I R ( X , Y ) I R ( X , Y ) I_R(X,Y)IR(X,Y) IR(X,Y)IR(X,Y),它是信息增益和特征熵的比值。表达式如下:

I R ( D , A ) = I ( A , D ) H A ( D ) I_R(D,A)=\frac{I(A,D)}{H_A(D)} IR(D,A)=HA(D)I(A,D)

其中D为样本特征输出的集合,A为样本特征,对于特征熵 H A ( D ) H_A(D) HA(D), 表达式如下:

H A ( D ) = − ∑ i n ∣ D i ∣ ∣ D ∣ l o g 2 ∣ D i ∣ ∣ D ∣ H_A(D)=−∑_i^n\frac{|D_i|}{|D|}log_2\frac{|D_i|}{|D|} HA(D)=inDDilog2DDi

其中n为特征A的类别数, ∣ D i ∣ |D_i| Di为特征A取第i个值时对应的样本个数。|D|为总样本个数。

对于第三个缺失值处理的问题,主要需要解决的是两个问题,一是在样本某些特征缺失的情况下选择划分的属性,二是选定了划分属性,对于在该属性上缺失特征的样本的处理。

对于第一个子问题,对于某一个有缺失特征值的特征A。C4.5的思路是将数据分成两部分,对每个样本设置一个权重(初始可以都为1),然后划分数据,一部分是有特征值A的数据D1,另一部分是没有特征A的数据D2. 然后对于没有缺失特征A的数据集D1来和对应的A特征的各个特征值一起计算加权重后的信息增益比,最后乘上一个系数,这个系数是无特征A缺失的样本加权后所占加权总样本的比例。

对于第二个子问题,可以将缺失特征的样本同时划分入所有的子节点,不过将该样本的权重按各个子节点样本的数量比例来分配。比如缺失特征A的样本a之前权重为1,特征A有3个特征值A1,A2,A3。 3个特征值对应的无缺失A特征的样本个数为2,3,4.则a同时划分入A1,A2,A3。对应权重调节为2/9,3/9, 4/9。

对于第4个问题,C4.5引入了正则化系数进行初步的剪枝。具体方法这里不讨论。下篇讲CART的时候会详细讨论剪枝的思路。

不足

1)由于决策树算法非常容易过拟合,因此对于生成的决策树必须要进行剪枝。剪枝的算法有非常多,C4.5的剪枝方法有优化的空间。思路主要是两种,一种是预剪枝,即在生成决策树的时候就决定是否剪枝。另一个是后剪枝,即先生成决策树,再通过交叉验证来剪枝。后面在下篇讲CART树的时候我们会专门讲决策树的减枝思路,主要采用的是后剪枝加上交叉验证选择最合适的决策树。

2)C4.5生成的是多叉树,即一个父节点可以有多个节点。很多时候,在计算机中二叉树模型会比多叉树运算效率高。如果采用二叉树,可以提高效率。

3)C4.5只能用于分类,如果能将决策树用于回归的话可以扩大它的使用范围。

4)C4.5由于使用了熵模型,里面有大量的耗时的对数运算,如果是连续值还有大量的排序运算。如果能够加以模型简化可以减少运算强度但又不牺牲太多准确性的话,那就更好了。

CART分类树算法

CART生成二叉决策树

我们知道,在ID3算法中我们使用了信息增益来选择特征,信息增益大的优先选择。在C4.5算法中,采用了信息增益比来选择特征,以减少信息增益容易选择特征值多的特征的问题。但是无论是ID3还是C4.5,都是基于信息论的熵模型的,这里面会涉及大量的对数运算。能不能简化模型同时也不至于完全丢失熵模型的优点呢?

CART分类树算法使用基尼系数来代替信息增益比,基尼系数代表了模型的不纯度,基尼系数越小,则不纯度越低,特征越好。这和信息增益(比)是相反的。

子集计算基尼不纯度,即随机放置的数据项出现于错误分类中的概率。以此来评判属性对分类的重要程度。

G i n i ( D ) = ∑ k = 1 K p k ( 1 − p k ) = 1 − ∑ p k 2 Gini(D)=\sum_{k=1}^Kp_k (1−p_k )=1−∑ p^2_k Gini(D)=k=1Kpk(1pk)=1pk2 其中 p k p_k pk为任一样本点属于第k类的概率,也可以说成样本数据集中属于k类的样本的比例。

集合D的基尼指数为 G i n i ( D ) Gini(D) Gini(D),在特征A条件下的集合D的基尼指数为
G i n i ( D ∣ A ) = ∑ ∣ D i ∣ ∣ D ∣ G i n i ( D i ) Gini(D|A) = \sum \frac{|D_i|}{|D|}Gini(D_i) Gini(DA)=DDiGini(Di)

其中 ∣ D i ∣ |D_i| Di为特征A取第i个值时对应的样本个数。|D|为总样本个数

CART算法中对于分类树采用的是上述的基尼指数最小化准则。对于回归树,CART采用的是平方误差最小化准则。

CART树算法的剪枝

由于决策时算法很容易对训练集过拟合,而导致泛化能力差,为了解决这个问题,我们需要对CART树进行剪枝,即类似于线性回归的正则化,来增加决策树的返回能力。但是,有很多的剪枝方法,我们应该这么选择呢?

CART采用的办法是后剪枝法,即先生成决策树,然后产生所有(是所有的,也就是形成了子树序列)可能的剪枝后的CART树,然后使用交叉验证来检验各种剪枝的效果,选择泛化能力最好的剪枝策略。

那么CART算法是如何产生一批剪枝树的呢?

剪枝操作中的损失函数为:

损失函数=拟合度+a*模型复杂度

当a=0时,整体树为最优树,当a为无穷大时,只有根节点的树为最优树。a逐渐增大,树逐渐剪值。若 a 1 < a 2 a1<a2 a1<a2,则a1对应的最优树一定整体包含a2对应的最优树。也就是说最优树是嵌套的。

CART算法中对于子树T的损失函数为

C a ( T ) = C ( T ) + a ∣ T ∣ C_a(T) = C(T)+a|T| Ca(T)=C(T)+aT 其中 C ( T ) C(T) C(T)为对训练数据的预测误差(如基尼指数), ∣ T ∣ |T| T为树的叶子节点个数,用来代表模型复杂度。

树中每个节点 t t t下的树如果被减去,则整体损失函数减少的程度为

g ( t ) = C ( t ) − C ( T t ) ∣ T t ∣ − 1 g(t) =\frac{C(t)-C(T_t)}{|T_t|-1} g(t)=Tt1C(t)C(Tt) 其中 T t T_t Tt为以t为根节点的子树, C ( t ) C(t) C(t)为t为单节点树时的预测误差, C ( T t ) C(T_t) C(Tt)为树 T t T_t Tt的预测误差, ∣ T t ∣ |T_t| Tt为树 T t T_t Tt的叶子节点个数。

在整体树 T 0 T_0 T0中剪去$g(t) 最 小 的 最小的 T_t 得 到 子 树 得到子树 T_1 , 在 从 ,在从 T_1 中 剪 去 中剪去 g(t) 最 小 的 最小的 T_t 得 到 子 树 得到子树 T_2$,如此递归,直到只有根节点,这样就按找到了一个按顺序的子树序列。

最后通过交叉验证得到最优的一个子树,作为真正剪枝后的树。

案例

应用场景:

本文使用决策树对web站点的用户在线浏览行为及最终购买行为(选择的服务类型或者用户类型)进行预测。

每个用于的在线浏览行为信息包括:每个用户的来源网站、用户的ip位置、是否阅读FAQ、浏览网页数目。

目标分类为用户类型:游客、基本用户、高级用户

在建立决策树时,我们先要懂得一个概念,叫属性的分类重要性,就是某个属性的出现,对目标结果能带来多大的信息。属性的重要程度是根据样本数据集使用该属性进行划分子集后,集合的纯度增加了多少来决定。

我们以用户在线浏览信息为例,如果阅读过FAQ的用户全部都是高级用户,没有阅读过FAQ的都是基本用户,则是否阅读过FAQ这个属性就非常重要。因为通过FAQ属性划分子集后,产生的两个子集,非常“纯”。

决策树的建立过程是先找出最重要的分类属性,再找出第二重要的分类属性。以此建立树的层次。

算法支持模型树结构特征选择连续值处理缺失值处理剪枝
ID3分类多叉树信息增益不支持不支持不支持
C4.5分类多叉树信息增益比支持支持支持
CART分类,回归二叉树基尼系数,均方差支持支持支持

构建数据集

读者可以自己去获取自家公司的用户行为记录,这里给出一个简单的数据集

下面的数据为每个用户的来源网站、位置、是否阅读FAQ、浏览网页数目、以及用户类型(None为游客,Basic为基本用户,Premium为高级用户)。最后一列属性为用户类型,就是我们想要预测的分类结果

my_data=[['slashdot','USA','yes',18,'None'],
         ['google','France','yes',23,'Premium'],
         ['digg','USA','yes',24,'Basic'],
         ['kiwitobes','France','yes',23,'Basic'],
         ['google','UK','no',21,'Premium'],
         ['(direct)','New Zealand','no',12,'None'],
         ['(direct)','UK','no',21,'Basic'],
         ['google','USA','no',24,'Premium'],
         ['slashdot','France','yes',19,'None'],
         ['digg','USA','no',18,'None'],
         ['google','UK','no',18,'None'],
         ['kiwitobes','UK','no',19,'None'],
         ['digg','New Zealand','yes',12,'Basic'],
         ['slashdot','UK','no',21,'None'],
         ['google','UK','yes',18,'Basic'],
         ['kiwitobes','France','yes',19,'Basic']]

决策树前的准备工作

有了样本数据集,我们就可以用来构建决策树了。在构建决策树的过程中首先我们要对决策树上的点创建类,然后要能够根据属性划分成多个子数据集,还要能计算划分子数据集后的信息增益,用信息增益来判断这个属性的重要性,然后选择最重要的属性建第一层树。

1、为决策树上的点创建类

# 建立决策树上的节点类
class decisionnode:
    def __init__(self,col=-1,value=None,results=None,tb=None,fb=None):
        self.col=col                #待检测条件所属的列索引。即当前是对第几列数据进行分类
        self.value=value            #为使结果为true,当前列必须匹配的值
        self.results=results        #如果当前节点时叶节点,表示该节点的结果值,如果不是叶节点,为None
        self.tb=tb                  #判断条件为true后的子节点
        self.fb=fb                  #判断调节为false后的子节点

2、数据集划分功能

这里我们完成根据属性划分数据集的功能。一般对于字符串型的数据,我们将数据集分成等于和不等于两个子集。对数值型属性,我们分成大于和小于两个子集。

需要注意的是,在样本数据集中,每一行为一个对象,每一列为一种属性。样本数据集以矩阵的形式存在。

# 根据某一属性对数据集合进行拆分,能够处理数值型数据或名词性数据。其实决策树只能处理离散型数据,对于连续性数据也是划分为范围区间块
# rows样本数据集,column要匹配的属性列索引,value指定列上的数据要匹配的值
def divideset(rows,column_index,column_value):
    # 定义一个函数,令其告诉我们数据行属于第一组(返回值为true)还是第二组(返回值false)
    split_function=None
    if isinstance(column_value,int) or isinstance(column_value,float):
        split_function=lambda row:row[column_index]>=column_value   #按大于、小于区分
    else:
        split_function=lambda row:row[column_index]==column_value   #按等于、不等于区分

    # 将数据集拆分成两个子集,并返回
    set1=[row for row in rows if split_function(row)]
    set2=[row for row in rows if not split_function(row)]
    return (set1,set2)

信息增益的计算

完成了拆分子集,就可以计算拆分后的信息增益了。在计算信息增益时,我们不适用纯度,而使用凌乱度,意义相同。凌乱度越大越不好。

原样本集划分成多个子集后,每个子集的凌乱度我们可以由基尼不纯度的大小或熵的大小来代替。

那一个原数据集和多个子集之间如何比较凌乱度呢。这除了和每个子集的凌乱度有关外,还与子集的大小有关。因为如果其中一个子集样本数非常少,即使凌乱度非常低也不能代表什么。

划分后的多个子集的总体凌乱度我们设定为m=p1m1+p2m2+p3*m3。其中p1、p2、p3为每个子集所占的比例,m1、m2、m3为每个子集的凌乱度。

用没有划分子集前的原样本数据集的凌乱度减去多个子集的总体凌乱度,就是信息增益。信息增益越大,证明该属性对分类的重要程度越大。

这里每个子集使用基尼不纯度或熵来代表凌乱度。

1、基尼不纯度

子集计算基尼不纯度,即随机放置的数据项出现于错误分类中的概率。以此来评判属性对分类的重要程度。

G i n i ( p ) = ∑ p k ( 1 − p k ) = 1 − ∑ p k 2 Gini(p)=∑p_k (1−p_k )=1−∑ p^2_k Gini(p)=pk(1pk)=1pk2

换句话说,就是统计集合中所有分类的概率两两积的和。

比如下面的rows是通过一个属性划分的一个子集。我们肯定希望这个子集中尽可能都是同一种分类。这样这个子集的纯度才能够高。如何看纯度够不够高呢。

比如这个集合有包含了3个分类,分类1的比例是0.1,分类2的比例是0.1,分类3的比例是0.8。这3个比例的两两积的和为0.10.1+0.10.8+0.1*0.8 = 0.17

而如果这三个分类中,分类1的比例为0.3,分类2的比例为0.3,分类3的比例为0.4,则这3个比例的两两积的和为0.30.3+0.30.4+0.3*0.4=0.33,是不是比上面的大,表明纯度小。

为什么会存在这种大小关系呢?自己百度吧。

#rows样本数据集
def giniimpurity(rows):
    total=len(rows)
    counts=uniquecounts(rows)
    imp=0
    for k1 in counts:
        p1=float(counts[k1])/total
        for k2 in counts:
            if k1==k2: continue
            p2=float(counts[k2])/total
            imp+=p1*p2
    return imp

# 统计集合rows中每种分类的样本数目。(样本数据每一行数据的最后一列记录了分类结果)。rows样本数据
def uniquecounts(rows):
    results={}
    for row in rows:
        # 目标结果在样本数据最后一列
        r=row[len(row)-1]
        if r not in results: results[r]=0
        results[r]+=1
    return results

2、熵

熵,即遍历所有可能的结果之后得到的p(x)log(p(x))之和。也可以以此评判属性对分类的重要程度。

#rows样本数据集
def entropy(rows):
    from math import log
    log2=lambda x:log(x)/log(2)
    results=uniquecounts(rows)
    # 此处开始计算熵的值
    ent=0.0
    for r in results.keys():
        p=float(results[r])/len(rows)
        ent=ent-p*log2(p)
    return ent

# 对各种可能的目标结果(选择的服务类型)进行计数(样本数据每一行数据的最后一列记录了目标结果)。rows样本数据
def uniquecounts(rows):
    results={}
    for row in rows:
        # 目标结果在样本数据最后一列
        r=row[len(row)-1]
        if r not in results: results[r]=0
        results[r]+=1
    return results

构建决策树

当能判断哪个属性对分类更重要我们就可以构建决策树了,先选出最重要的属性,作为根节点,再将数据集划分成两个子集,并分别在子集中选出其次重要的属性,做为左右子树的根节点,并以此递推下去。

通过每一次的属性查询,我们就知道了该找那个属性作为节点,以及该如果进行左右子树的划分。

buildtree函数输入为样本数据集,输出为决策树。

所谓的输出为决策树,就是输出的为根节点。根节点就表示决策树。

# 构建决策树.scoref为信息增益的计算函数
def buildtree(rows,scoref=entropy):
    if len(rows)==0: return decisionnode()
    current_score=scoref(rows)

    # 定义一些变量以记录最佳拆分条件
    best_gain=0.0
    best_criteria=None
    best_sets=None

    column_count=len(rows[0])-1
    for col in range(0,column_count):    #遍历每一列(除最后一列,因为最后一列是目标结果)
        # 在当前列中生成一个由不同值构成的序列
        column_values={}
        for row in rows:
            column_values[row[col]]=1
        # 接下来根据这一列中的每个值,尝试对数据集进行拆分
        for value in column_values.keys():
            (set1,set2)=divideset(rows,col,value)

            # 计算信息增益
            p=float(len(set1))/len(rows)
            gain=current_score-p*scoref(set1)-(1-p)*scoref(set2)
            if gain>best_gain and len(set1)>0 and len(set2)>0:   #找到信息增益最大的分类属性
                best_gain=gain
                best_criteria=(col,value)
                best_sets=(set1,set2)
    # 创建子分支
    if best_gain>0:
        trueBranch=buildtree(best_sets[0])   #创建分支
        falseBranch=buildtree(best_sets[1])  #创建分支
        return decisionnode(col=best_criteria[0],value=best_criteria[1],tb=trueBranch,fb=falseBranch)  #返回决策树节点
    else:
        return decisionnode(results=uniquecounts(rows))

绘制决策树

from PIL import Image, ImageDraw

# 获取树的显示宽度
def getwidth(tree):
    if tree.tb==None and tree.fb==None: return 1
    return getwidth(tree.tb)+getwidth(tree.fb)

# 获取树的显示深度(高度)
def getdepth(tree):
    if tree.tb==None and tree.fb==None: return 0
    return max(getdepth(tree.tb),getdepth(tree.fb))+1

# 绘制树形图
def drawtree(tree,jpeg='tree.jpg'):
    w=getwidth(tree)*100
    h=getdepth(tree)*100+120

    img=Image.new('RGB',(w,h),(255,255,255))
    draw=ImageDraw.Draw(img)

    drawnode(draw,tree,w/2,20)  #根节点坐标
    img.save(jpeg,'JPEG')

# 迭代画树的节点
def drawnode(draw,tree,x,y):
    if tree.results==None:
        # 得到每个分支的宽度
        w1=getwidth(tree.fb)*100
        w2=getwidth(tree.tb)*100

        # 确定此节点所要占据的总空间
        left=x-(w1+w2)/2
        right=x+(w1+w2)/2

        # 绘制判断条件字符串
        draw.text((x-20,y-10),str(tree.col)+':'+str(tree.value),(0,0,0))

        # 绘制到分支的连线
        draw.line((x,y,left+w1/2,y+100),fill=(255,0,0))
        draw.line((x,y,right-w2/2,y+100),fill=(255,0,0))

        # 绘制分支的节点
        drawnode(draw,tree.fb,left+w1/2,y+100)
        drawnode(draw,tree.tb,right-w2/2,y+100)
    else:
        txt=' \n'.join(['%s:%d'%v for v in tree.results.items()])
        draw.text((x-20,y),txt,(0,0,0))

这里写图片描述

使用决策树对新的待测数据进行分类

# 对新的观测数据进行分类。observation为观测数据。tree为建立好的决策树
def classify(observation,tree):
    if tree.results!=None:
        return tree.results
    else:
        v=observation[tree.col]
        branch=None
        if isinstance(v,int) or isinstance(v,float):
            if v>=tree.value: branch=tree.tb
            else: branch=tree.fb
        else:
            if v==tree.value: branch=tree.tb
            else: branch=tree.fb
        return classify(observation,branch)

剪枝操作

在决策树创建时,由于数据中的噪声和离群点,许多分支反应的是训练数据中的异常,或者构建决策树时选取的阈值较小,造成构造的决策树特别复杂。这些都导致决策树对训练数据的分类效果很好,但是对未知数据的分类效果不理想,也就是过拟合现象。我们通过剪支方法处理这种过拟合数据问题。通常,这种方法使用统计量剪掉最不可靠的分支,一颗未剪枝的树和剪支的树对比如图。剪支后树更小,更简单,判断更快更好。

如图中,比如属性A1的值为no时正常情况都应该属于分类B,但是由于噪声原因,存在了几个属性A1的值为no的,但是属于分类A的对象。所以在决策树时就会多出一条分支。

这里写图片描述

剪支分为先剪支和后剪支。后剪支更常用,就是先完整的构建一个树,再通过删除节点的分支并用树叶替换它而剪掉给定节点上的子树。

剪支的过程就是对具有相同父节点的一组节点进行检查,判断如果将其合并,熵的增加量是否会小于某个指定的阈值。如果确实如此,则这些叶节点会被合并成一个单一的节点,合并后的新节点包含了所有可能的结果值。这种做法有助于避免过度拟合的情况,也使得根据决策树做出的预测结果,不至于比从数据集中得到的实际结论还要特殊。

CART树的剪枝算法中使用的是用交叉验证来检验剪枝后的预测能力,选择泛化预测能力最好的剪枝后的数作为最终的CART树。

# 决策树剪枝。(因为有些属性的分类产生的熵值的差太小,没有区分的必要),mingain为门限。
# 为了避免遇到大小台阶的问题(子树分支的属性比较重要),所以采取先建树,再剪支的方式
def prune(tree,mingain):
    # 如果分支不是叶节点,则对其进行剪枝操作
    if tree.tb.results==None:
        prune(tree.tb,mingain)
    if tree.fb.results==None:
        prune(tree.fb,mingain)

    # 如果两个自分支都是叶节点,则判断他们是否需要合并
    if tree.tb.results!=None and tree.fb.results!=None:
        # 构建合并后的数据集
        tb,fb=[],[]
        for v,c in tree.tb.results.items():
            tb+=[[v]]*c
        for v,c in tree.fb.results.items():
            fb+=[[v]]*c

        # 检查熵的减少情况
        delta=entropy(tb+fb)-(entropy(tb)+entropy(fb)/2)

        if delta<mingain:
            # 合并分支
            tree.tb,tree.fb=None,None
            tree.results=uniquecounts(tb+fb)

对于缺失数据的情况

对包含缺失数据的新的待测数据进行分类。会在逐层分类到缺失属性层时,不知道该往哪个方向继续判断。这时可以支同时查询每个分支,这样就有多个最终分类结果。最后计算多个分类的结果的加权结果。

def mdclassify(observation,tree):
    if tree.results!=None:
        return tree.results
    else:
        v=observation[tree.col]
        if v==None:
            tr,fr=mdclassify(observation,tree.tb),mdclassify(observation,tree.fb) #分别使用左右子树进行分类预测
            # 求统计结果
            tcount=sum(tr.values())
            fcount=sum(fr.values())
            tw=float(tcount)/(tcount+fcount)
            fw=float(fcount)/(tcount+fcount)
            result={}
            for k,v in tr.items(): result[k]=v*tw
            for k,v in fr.items(): result[k]=v*fw
            return result
        else:
            if isinstance(v,int) or isinstance(v,float):
                if v>=tree.value: branch=tree.tb
                else: branch=tree.fb
            else:
                if v==tree.value: branch=tree.tb
                else: branch=tree.fb
            return mdclassify(observation,branch)

对于数值型输出结果

如果输出结果不是分类而是数字,可以使用方差作为评价函数来取代熵或基尼不纯度

#计算数据集的统计方差
def variance(rows):
    if len(rows)==0: return 0
    data=[float(row[len(row)-1]) for row in rows]
    mean=sum(data)/len(data)
    variance=sum([(d-mean)**2 for d in data])/len(data)
    return variance

测试代码

if __name__=='__main__':  #只有在执行当前模块时才会运行此函数
    tree = buildtree(my_data)   #创建决策树
    printtree(tree)
    drawtree(tree,jpeg='treeview.jpg')  #画树形图
    prune(tree,0.1)   #剪支
    result = mdclassify(['google',None,'yes',None],tree)   #使用决策树进行预测,新数据包含缺失数据
    print(result)

决策树的内容到这里就讲完了。下面我们来总结一下。

决策树优点

1、决策树易于理解和实现,人们在在学习过程中不需要使用者了解很多的背景知识,这同时是它的能够直接体现数据的特点,只要通过解释后都有能力去理解决策树所表达的意义。

2、对于决策树,数据的准备往往是简单或者是不必要的,而且能够同时处理数据型和常规型属性,在相对短的时间内能够对大型数据源做出可行且效果良好的结果。

3、易于通过静态测试来对模型进行评测,可以测定模型可信度;如果给定一个观察的模型,那么根据所产生的决策树很容易推出相应的逻辑表达式。

决策树缺点

1)对连续性的字段比较难预测。

2)对有时间顺序的数据,需要很多预处理的工作。

3)当类别太多时,错误可能就会增加的比较快。

4)一般的算法分类的时候,只是根据一个字段来分类。

Regression Decision Tree:回归树

回归树总体流程类似于分类树,区别在于,回归树的每一个节点都会得一个预测值,以年龄为例,该预测值等于属于这个节点的所有人年龄的平均值。分枝时穷举每一个feature的每个阈值找最好的分割点,但衡量最好的标准不再是最大熵,而是最小化平方误差。也就是被预测出错的人数越多,错的越离谱,平方误差就越大,通过最小化平方误差能够找到最可靠的分枝依据。分枝直到每个叶子节点上人的年龄都唯一或者达到预设的终止条件(如叶子个数上限),若最终叶子节点上人的年龄不唯一,则以该节点上所有人的平均年龄做为该叶子节点的预测年龄。

这里写图片描述

这里写图片描述

思考:选哪些特征属性参与决策树建模、哪些属性排在决策树的顶部,哪些排在底部,对属性的值该进行什么样的判断、样本属性的值缺失怎么办、如果输出不是分类而是数值能用么、对决策没有用或没有多大帮助的属性怎么办、什么时候使用决策树?

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐