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

决策树是一种基本的分类与回归方法。以分类为例,可以认为是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_i\mid }{D}log\frac{\mid D_i\mid }{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 T\mid =\sum_{t}^{\mid T\mid}N_tH_t(T)+\alpha \mid T\mid$$上面这个式子应该很好理解,其中的经验熵是可以算的。上式对预测误差与模型复杂度进行了权衡,也就是结构风险最小化。剪枝具体过程如下:
    • 对于生成的树,计算每个节点的经验熵
    • 递归地从叶节点向上回缩。通俗的说,就是比较一组叶节点回缩到父节点前后的损失函数,如果损失函数减小,则进行剪枝,父节点变为新的叶节点。
    • 一直执行直到不能继续。
      理论上,如果给定$\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_1\mid }{D}Gini(D_1)+\frac{\mid D_2\mid }{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_t\mid $$当参数较小时,后者小于前者,存在一个数值,使参数等于他时,两个损失函数相等,但\(t\)节点少,比\(T_t\)更可取,因此剪去\(T_t\)
    • 为此,对\(T_0\)中每一内部节点计算$$g(t)=\frac{C(t)-C(T_t)}{\mid T\mid-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]

posted @ 2018-09-02 19:58  流影心  阅读(1088)  评论(0编辑  收藏  举报