• 机器学习:决策树(一)——原理与代码实现


    决策树是一种基本的分类与回归方法。以分类为例,可以认为是if-then规则的集合,也可以认为是定义在特征空间与类别空间上的条件概率分布。一般分为三个步骤:特征选择,决策树生成,决策树剪枝。

    熵与条件熵

    • 熵是度量随机变量不确定性(集合不纯度)的一种指标。(X)是一个取有限个值得离散随机变量,其概率分布(P(X=mathbf{x}_i)=p_i,i=1,2,3,...n),则随机变量(X)的熵定义为(H(X)=-sum_{i=1}^{n}p_ilogp_i),熵越大,代表不确定越大。
    • 设有随机变量联合概率分布,条件熵(H(Y|X))表示在已知随机变量(X)的条件下(Y)的不确定性。定义为$$H(Y|X)=sum_{i=1}^{n}p_iH(Y|X=x_i),p_i=P(X=x_i),i=1,2,3,...,n$$
    ID3
    • 信息增益。特征A对训练数据D的信心增益(g(D,A)),定义为集合(D)的经验熵(H(D))与特征(A)给定条件下(D)的经验条件熵(H(D|A))之差,即$$g(D,A)=H(D)-H(D|A)$$
    • 由于分类的目的是是为了是属于同一类的样本归于一类,因此应该对于各个特征来说,我们应该选取经验条件熵较小的那个,从而 得到的信息增益较大。ID3算法的核心就是以信息增益作为特征选择准则,选择信息增益大的特征进行划分。
    • 输入:训练数据集(D),特征集A,阈值。输出:决策树T。
      1.若D中所有实例属于同一类别(c_k),则(T)为单节点树,将类(c_k)作为该节点类别,返回(T)
      2.若A中特征为空,则将实例数最大的类作为节点标记,返回单节点树(T)
      3.否则,计算A中各个特征的信息增益,选择最大的特征(A_g)
      4.如果(A_g)小于阈值,则将实例数最大的类别作为节点标志,返回单节点树。
      5.否则,对(A_g)的每一可能取值(A_gi)将D划分成若干(D_i),将子集中实例数最大的类作为标记,构建子节点,由节点子节点构成树T。
      6.对i个子节点,以(D_i)为训练集,以(A-{A_g}为特征集,递归进行1-5,得到子树)T_i$,返回。
    • 缺点:由于需要考虑每个特征的可能取值,因此不能用于连续特征;只有树的生成,容易过拟合;选择信息增益作为划分准则,容易偏向取值数目较多的特征;没有考虑缺失值情况。

    C4.5

    • 信息增益比。n为特征A取值的个数$$g_R(D,A)=frac{g(D,A)}{H_A(D)},H_A(D,A)=-sum_{i=1}^{n}frac{mid D_imid }{D}logfrac{mid D_imid }{D}$$
    • C4.5改进之处。
      1.可以用于连续特征。对于某连续特征a,在训练集中出现了从小到大排列为a1,a2,...,an的n个值。,则C4.5取相邻两样本值的平均数,一共取得n-1个划分点,对于这n-1个点,分别计算以该点作为二元分类点时的信息增益。选择信息增益最大的点作为该连续特征的二元离散分类点。要注意的是,与离散属性不同的是,如果当前节点为连续属性,则该属性后面还可以参与子节点的产生选择过程。
      2.用信息增益比作为特征选择准则。注意信息增益比准则偏向于特征数目取值较少的属性,因此C4.5算法使用时进行了进行了权衡,并不是直接选择增益率最大的候选特征,而是先找出信息增益高于平均水平的特征,然后从中选择信息增益比最大的特征。
      3.能够处理缺失值问题。主要需要解决的是两个问题,一是在某些特征缺失的情况下如何选择划分的属性,二是选定了划分属性,对于在该属性上缺失特征的样本如何进行划分。对于前者,在某个特征没有缺失的数据子集上进行操作。对于后者,让同一个样本以不同的概率划入到不同的子节点中去。
      4.进行了剪枝。关于此处需要重点总结一下,因为CART的剪枝算法与之一脉相承。决策树的剪枝往往通过极小化整体的损失函数或者代价函数来实现。设树(T)的叶节点数目为(|T|)(t)是树的某个叶节点,该节点有(N_t)个样本数,其中(k)类的样本数目为(N_{tk}),(H_t(T))为叶节点上的经验熵。**在(alpha)确定的情况下,可以得到整个决策树的损失函数$$C_alpha (T)=C(T)+alpha mid Tmid =sum_{t}^{mid Tmid}N_tH_t(T)+alpha mid Tmid$$上面这个式子应该很好理解,其中的经验熵是可以算的。上式对预测误差与模型复杂度进行了权衡,也就是结构风险最小化。剪枝具体过程如下:
      • 对于生成的树,计算每个节点的经验熵
      • 递归地从叶节点向上回缩。通俗的说,就是比较一组叶节点回缩到父节点前后的损失函数,如果损失函数减小,则进行剪枝,父节点变为新的叶节点。
      • 一直执行直到不能继续。
        理论上,如果给定$alpha $,那么可以得到一个较好的剪枝树,上述剪枝算法确实可以防止过拟合。但是一个明显的问题就是,不同参数下会得到许多树,那么如何确定哪个参数下的树是最优的呢?并且参数是一个连续值,更是难以得到所有的参数值。CART的剪枝算法解决了这一问题

    CART算法

    • CART为二叉树,对回归树采用平方误差最小准则,对分类树采用基尼指数最小化原则进行特征选择,选择基尼指数小的特征进行特征划分。
    • 树的生成。回归树的生成采用启发式方法,不断切分空间,最终将输入空间划分为许多单元,每个单元有一个固定的输出值。原理很清楚易懂,不再赘述;分类树为二叉树,用基尼指数选择最优特征。(Gini(D)=sum_{k=1}^{K}p_k(1-p_k)),对于二分类,则有(Gini(D)=2p(1-p))如果集合根据特征A是否取值a分为(D_1,D_2),则在特征A条件下,集合的基尼指数为$$Gini(D,A=a)=frac{mid D_1mid }{D}Gini(D_1)+frac{mid D_2mid }{D}Gini(D_2)$$因此若是某个特征(A_1)取值数目大于2,比如有n个,则需计算(Gini(D,A_1=a_1),Gini(D,A_1=a_2),...,Gini(D,A_1=a_n)),对于特征数目为2的特征直接计算(Gini(D,A_2=a))即可。
    • 树的剪枝。前面提过,对于固定(alpha),一定存在使损失函数最小的子树。当参数大的时候,子树偏小,参数小的时候,子树偏大。极端情况,参数为零,整体树最优。参数无穷大,根节点组成的单节点树最优。可以证明,用递归的方法可以对树进行剪枝。将(alpha)从小增大,(0<alpha _0<alpha _1<...<alpha _n<propto)产生一系列区间([alpha _i,alpha _{i+1}));剪枝得到的子树对应各个区间。
      • 具体地,从整体树(T_0)开始。对内部节点(t),以之为单节点树的损失函数为$$C_alpha (t)=C(t)+alpha $$以之为根节点树的子树(T_t)损失函数为$$C_alpha (T_t)=C(T_t)+alpha mid T_tmid $$当参数较小时,后者小于前者,存在一个数值,使参数等于他时,两个损失函数相等,但(t)节点少,比(T_t)更可取,因此剪去(T_t)
      • 为此,对(T_0)中每一内部节点计算$$g(t)=frac{C(t)-C(T_t)}{mid Tmid-1 }$$取其中最小的,然后在(T_0)中剪去对应的(T_t),将得到的树作为(T_1),同时将最小的(g(t))设为(alpha_1)(T_1)为区间([alpha _1,alpha _2))最优子树。
      • 如此剪枝下去,直至得到根节点,过程中不断增加参数值,产生新的区间。
      • 在剪枝得到的子树序列中通过交叉验证选取最优。

    代码实现

    代码为ID3算法(未加阈值),重在理解特征选择准则,准则计算,根据准则划分数据集等决策树构建的过程,因此没有可视化部分。

    from math import log
    import operator
    import matplotlib.pyplot as plt
    
    '''计算香农熵'''
    def calcShannonEnt(dataSet):
        numEntries = len(dataSet)
        labelCounts = {}
        for featVec in dataSet:
            currentLabel = featVec[-1]
            # labelCounts[currentLabel] = labelCounts.get(currentLabel, 0) + 1     #这句话和下面三句效果相同
            if currentLabel not in labelCounts.keys():
                labelCounts[currentLabel] = 0
            labelCounts[currentLabel] += 1
        shannonEnt = 0.0
        for key in labelCounts:
            prob = float(labelCounts[key]) / numEntries
            shannonEnt -= prob * log(prob, 2)
        return shannonEnt
    
    '''按照某个特征的某个取值划分数据集'''
    def splitDataSet(dataSet,axis,value):
        retDataSet = []
        for featVec in dataSet:
            if featVec[axis] == value:
                reduceFeatVec = featVec[:axis]
                reduceFeatVec.extend(featVec[axis+1:])
                retDataSet.append(reduceFeatVec)
        return retDataSet
    
    '''遍历数据集,选择最好的特征划分方式'''
    def chooseBestFeatureToSplit(dataSet):
        numFeatures = len(dataSet) - 1
        baseEntropy = calcShannonEnt(dataSet)
        bestInfoGain = 0.0
        bestFeature = -1
        for i in range(numFeatures):
            featList = [example[i] for example in dataSet]
            uniqueVals = set([featList])
            newEntropy = 0.0
            for value in uniqueVals:
                subDataSet = splitDataSet(dataSet, i, value)
                prob = len(subDataSet) / float(len(dataSet))
                newEntropy += prob * calcShannonEnt(subDataSet)
            infoGain = baseEntropy - newEntropy
            if (infoGain > bestInfoGain):
                bestFeature = infoGain
                bestFeature = i
        return bestFeature
    
    
    '''多数表决'''
    def majorityCnt(classList):
        classCount = {}
        for vote in classList:
            classCount[vote] = classList.count(vote)
        sortedclassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
        return sortedclassCount[0][0]
    
    
    '''创建树'''
    def createTree(dataSet,labels):   # 注意这里labels存储的为特征的标签,例如:身高,体重。
        classList = [example[-1] for example in dataSet]
        if classList.count(classList[0]) == len(classList):
            return classList[0]
        if len(dataSet[0]) == 1:
            return majorityCnt(classList)
        bestFeat = chooseBestFeatureToSplit(dataSet)
        bestFeatLabel = labels[bestFeat]
        myTree = {bestFeatLabel: {}}
        del(labels[bestFeat])
        featValues = [example[bestFeat] for example in dataSet]
        uniqueVals = set(featValues)
        for value in uniqueVals:
            sublables = labels[:]
            myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), sublables)
        return myTree
    
    '''获取叶结点数目和树的深度'''
    
    def getNumLeafs(myTree):
        numLeafs = 0
        firstStr = myTree.keys()[0]
        secondDict = firstStr
        for key in secondDict.keys():
            if type(secondDict[key]).__name__ =='dict':
                numLeafs += getNumLeafs(secondDict[key])
            else:
                numLeafs += 1
        return numLeafs
    
    def getTreeDepth(myTree):
        maxDepth = 0
        firstStr = myTree.keys()[0]
        secondDict = firstStr
        for key in secondDict.keys():
            if type(secondDict[key]).__name__ == 'dict':
                thisdepth = 1 + getTreeDepth(secondDict[key])
            else:
                thisdepth = 1
            if thisdepth > maxDepth:
                maxDepth = thisdepth
        return maxDepth
    
    '''构建分类器'''
    def classify(inputTree, featLables, testVec):
        firstStr = inputTree.keys()[0]
        secondDict = inputTree[firstStr]
        featIndex = featLables.index(firstStr)
        for key in secondDict.keys():
            if testVec[featIndex] == key:
                if type(secondDict[key]).__name__ == 'dict':
                    classLabel = classify(secondDict[key], featLables, testVec)
                else:
                    classLabel = secondDict[key]
        return classLabel
    

    [https://zhuanlan.zhihu.com/p/32164933]
    [https://zhuanlan.zhihu.com/p/32180057]

    如有错误,欢迎批评指正。转载请注明出处。沟通交流liuyingxinwy@163.com
  • 相关阅读:
    10、代码块、构造代码块、静态代码块及main方法之间的关系
    2.0、Hibernate框架的简单搭建
    1.0、Struts2的简单搭建方法
    5、Servlet的使用
    angular组件之间的通信
    angular项目中遇到的问题
    ng-zorro-mobile中遇到的问题
    angular管道操作符的使用
    angular路由配置以及使用
    搭建Angular环境
  • 原文地址:https://www.cnblogs.com/lyxML/p/9542696.html
Copyright © 2020-2023  润新知