《机器学习》第四章:决策树

简述

决策树是机器学习的一类常见算法,其核心思想是通过构建一个树状模型来对新样本进行预测。树的叶结点是预测结果,而所有非叶结点皆是一个个决策过程。决策树很多任务都 是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,机器学习算法最终将使用这些机器从数据集中创造的规则。

image

ps:决策树实际上就是一个将数据按属性分堆的过程。虽然名字叫的挺高大上的,但是背后的逻辑却十分简单。(其实大多数算法的思想都很简单,大家不要被复杂的公式吓到),那么它较难的地方就不在于这个背后逻辑了,而在于,比如我们是根据什么策略来决定属性测试的顺序的,因为我么对属性的优先级有特殊偏好;选定属性的取值的标准是什么;构建的决策树是否足够泛化等问题。

决策树代码


from math import log

import operator

def calcShannonEnt(dataSet):  # 计算数据的熵(entropy)

    numEntries=len(dataSet)  # 数据条数

    labelCounts={}

    for featVec in dataSet:

        currentLabel=featVec[-1] # 每行数据的最后一个字(类别)

        if currentLabel not in labelCounts.keys():

            labelCounts[currentLabel]=0

        labelCounts[currentLabel]+=1  # 统计有多少个类以及每个类的数量

    shannonEnt=0

    for key in labelCounts:

        prob=float(labelCounts[key])/numEntries # 计算单个类的熵值

        shannonEnt-=prob*log(prob,2) # 累加每个类的熵值

    return shannonEnt

def createDataSet1():    # 创造示例数据

    dataSet = [['青绿' , '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '好瓜'],

              ['乌黑' , '蜷缩' , '沉闷' , '清晰' , '凹陷' , '硬滑' , '好瓜'] ,

              ['乌黑' , '蜷缩' , '浊响' , '清晰' , '凹陷' , '硬滑' , '好瓜'] ,

              ['青绿' , '蜷缩' , '沉闷' , '清晰' , '凹陷' , '硬滑' , '好瓜'] ,

              ['浅白' , '蜷缩' , '浊响' , '清晰' , '凹陷' , '硬滑' , '好瓜'] ,

              ['青绿' , '稍缩' , '浊响' , '清晰' , '稍凹' , '软粘' , '好瓜'] ,

              ['乌黑' , '稍缩' , '浊响' , '稍糊' , '稍凹' , '软粘' , '好瓜'] ,

              ['乌黑' , '稍缩' , '浊响' , '清晰' , '稍凹' , '硬滑' , '好瓜'] ,

              ['乌黑' , '稍缩' , '沉闷' , '稍糊' , '稍凹' , '硬滑' , '好瓜'] ,

              ['青绿' , '硬挺' , '清脆' , '清晰' , '平坦' , '硬滑' , '坏瓜'] ,

              ['浅白' , '硬挺' , '清脆' , '模糊' , '平坦' , '软粘' , '坏瓜'] ,

              ['浅白' , '蜷缩' , '浊响' , '模糊' , '平坦' , '硬滑' , '坏瓜'] ,

              ['青绿' , '稍缩' , '浊响' , '稍糊' , '凹陷' , '软粘' , '坏瓜'] ,

              ['浅白' , '稍缩' , '沉闷' , '稍糊' , '凹陷' , '硬滑' , '坏瓜'] ,

              ['乌黑' , '稍缩' , '浊响' , '清晰' , '稍凹' , '软粘' , '坏瓜'] ,

              ['浅白' , '蜷缩' , '浊响' , '模糊' , '平坦' , '硬滑' , '坏瓜'] ,

              ['青绿' , '蜷缩' , '沉闷' , '稍糊' , '稍凹' , '硬滑' , '坏瓜'] ]

    labels = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']  #6个特征

    return dataSet,labels

def splitDataSet(dataSet,axis,value): # 按某个特征分类后的数据

    retDataSet=[]

    for featVec in dataSet:

        if featVec[axis]==value:

            reducedFeatVec =featVec[:axis]

            reducedFeatVec.extend(featVec[axis+1:])

            retDataSet.append(reducedFeatVec)

    return retDataSet

def chooseBestFeatureToSplit(dataSet):  # 选择最优的分类特征

    numFeatures = len(dataSet[0])-1

    baseEntropy = calcShannonEnt(dataSet)  # 原始的熵

    bestInfoGain = 0

    bestFeature = -1

    for i in range(numFeatures):

        featList = [example[i] for example in dataSet]

        uniqueVals = set(featList)

        newEntropy = 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):  # 若按某特征划分后,熵值减少的最大,则次特征为最优分类特征

            bestInfoGain=infoGain

            bestFeature = i

    return bestFeature

def majorityCnt(classList):    #按分类后类别数量排序,比如:最后分类为2好瓜1坏瓜,则判定为好瓜;

    classCount={}

    for vote in classList:

        if vote not in classCount.keys():

            classCount[vote]=0

        classCount[vote]+=1

    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)

    return sortedClassCount[0][0]

def createTree(dataSet,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:

        subLabels=labels[:]

        myTree[bestFeatLabel][value]=createTree(splitDataSet\

                            (dataSet,bestFeat,value),subLabels)

    return myTree

if __name__=='__main__':

    dataSet, labels=createDataSet1()  # 创造示列数据

    print(createTree(dataSet, labels))  # 输出决策树模型结果

这里参考了andy老师的源码,参考资料:https://gitee.com/ZHBIT-MachineLearning/Machine-Learning-Base/blob/master/Fourth_DecisionTree/Fourth_DecisionTree.py#

知识点

1、信息熵

“信息熵”(information entropy)是度量样本集合纯度最常用的一种指标。其代表的是不同的类别在数据在总数据集中的熵的和,当样本集合越“纯”时,信息熵越小(信息熵的取值范围为0~log2(k))。

2、信息增益

原始信息熵与新的划分后的信息墒之差就是这次属性划分的“信息增益”(information gain),所以我们的属性划分选择就是在每个结点找到最高信息增益的属性。

3、增益率

增益率是让信息增益除以选择的划分属性的“固有值”(intrinsic value)。一般来说属性a的可能取值数目越多,则固有值越大。

4、基尼指数

原始基尼值与新划分后的不同样本集合的基尼值之和的差值就是“基尼指数”(Gini index),而基尼值的公式如下:

Gini(D) = 1 - pk²

基尼值反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率。因此,基尼值越小,则数据集D的纯度越高。

决策树的优缺点

优点:

1、计算量简单,可解释性强,比较适合处理有缺失属性值的样本,能够处理不相关的特征;

缺点:

1、单颗决策树分类能力弱,并且对连续值变量难以处理;

2、容易过拟合(后续出现了随机森林,减小了过拟合现象);

决策树构建步骤

我们在划分中,希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的“纯度”(purity)越来越高。要使得结点纯度越来越高,我们得要选择合适的指标来对纯度进行评估,并且我们也得要在这些指标上对属性的划分进行选择。目前决策树主流使用的属性划分指标为以下三个(如何量化纯度):

[1]信息增益

"信息熵" (information entropy)是度量样本集合纯度最常用的一种指标.假定当前样本集合 D 中第 k 类样本所占的比例为 Pk

(k = 1, 2,. . . , IYI) ,则 D的信息墒定义为:

image

属性划分逻辑是这样的:信息熵代表的是样本集合的纯度,当用一个属性对样本进行划分后,分开的不同样本集合的信息墒之和纯度最高,那么这个属性就是目前最好的划分选择。

如何度量数据集的无序程度的方式是计算信息熵

信息增益(information gain)=原始信息熵-新的划分后的信息墒,所以我们的属性划分选择就是在每个结点找到最高信息增益的属性。

[2]增益率

增益率是让信息增益除以选择的划分属性的“固有值”(intrinsic value)。

image

为何要引入增益率?因为增益率是一个增量△值,引入增益率可以解决信息增益对可取值数目较多的属性有所偏好的问题,更加正确的描述样本集合纯度。

然而信息增益除以固有值后获得的增益率反而是偏爱取值数目较少的属性的。

解决办法:先从候选划分属性中选出信息增益高于平均水平的属性,再从中选择增益率最高的。

[3]基尼指数

基尼值的公式如下:

image

从上式可以看出,基尼值反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率。因此,基尼值越小,则数据集D的纯度越高。

那么同信息增益的选择策略一样:当用一个属性对样本进行划分后,原始基尼值与新划分后的不同样本集合的基尼值之和的差值就是“基尼指数”(Gini index),所以我们的属性划分选择就是在每个结点找到划分后最高基尼指数的属性。

[4]剪枝处理

剪枝(pruning)是决策树学习算法对付“过拟合”的主要手段。为了尽可能正确分类训练样本,会不断的进行结点划分,有时会造成决策树分支过多,这时就可能因为训练过程学得太好了,将训练集自身的一些特点当作普遍化特征进行学习而会导致过拟合。因此,可通过主动去掉一些分支来降低过拟合的风险。目前决策树剪枝的基本策略有以下两种:

前置剪枝(预剪枝):在分裂节点的时候设计比较苛刻的条件,如不满足则直接停止分裂(这样干决策树无法到最优,也无法得到比较好的效果)

后置剪枝(后剪枝):在树建立完之后,用单个节点代替子树,节点的分类采用子树中主要的分类(这种方法比较浪费前面的建立过程)

减枝过程:使用第二章中提到的性能评估方法来比较泛化性能的改变:留出法、交叉验证法、自助法等。

从样本预留出一部分数据用作“验证集”以进行性能评估。

image
image

由图可看出,头结点脐部,精度由43.9%提高到71.4%,高于其他属性,因此选择脐部为头结点,其他分支同理。第二个枝叶,选取色泽,精度从71.4%降低到了57.1%,所以该枝叶别减。


补充:剪枝代码详细解释+周志华《机器学习》决策树图4.5、图4.6、图4.7绘制

部分代码如下:


#这个用于预剪枝

deftesting_feat(feat,train_data,test_data,labels):

print"train_data=",json.dumps(train_data,ensure_ascii=False)

class_list=[example[-1]forexampleintrain_data]

bestFeatIndex=labels.index(feat)

train_data=[example[bestFeatIndex]forexampleintrain_data]

test_data=[(example[bestFeatIndex],example[-1])forexampleintest_data]

all_feat=set(train_data)

error=0.0

forvalueinall_feat:

class_feat=[class_list[i]foriinrange(len(class_list))iftrain_data[i]==value]

major=majorityCnt(class_feat)

fordataintest_data:

ifdata[0]==valueanddata[1]!=major:

error+=1.0

# print 'myTree %d' % error

returnerror

#这个函数用于预剪枝和后剪枝

deftestingMajor(major,data_test):

error=0.0

foriinrange(len(data_test)):

ifmajor!=data_test[i][-1]:

error+=1

# print 'major %d' % error

returnfloat(error)

#这个函数专门用于"后剪枝"

deftesting(myTree,data_test,labels):

#这里输入的labels不是全部的特征名称

#这里输入的data_test不带有全部的特征名称

print"----------进入testing函数--------------"

print"data_test=",json.dumps(data_test,ensure_ascii=False)

print"labels=",json.dumps(labels,ensure_ascii=False)

error=0.0

foriinrange(len(data_test)):

ifclassify(myTree,labels,data_test[i])!=data_test[i][-1]:#如果预测结果与验证数据的类别标签不一致

error+=1#那么错误数就+1

print('myTree %d'%error)

returnfloat(error)

参考资料:https://blog.csdn.net/appleyuchi/article/details/83041047

运行结果:

image

ps:减少过拟合现象的另一种方法:构建随机森林RF

参考 https://www.jianshu.com/p/d792279a30bc

随机森林是有很多随机得决策树构成,它们之间没有关联。得到RF以后,在预测时分别对每一个决策树进行判断,最后使用Bagging的思想进行结果的输出(也就是投票的思想)

学习过程

1、现在有N个训练样本,每个样本的特征为M个,需要建K颗树

2、从N个训练样本中有放回的取N个样本作为一组训练集(其余未取到的样本作为预测分类,评估其误差)

3、从M个特征中取m个特征左右子集特征(m<

4、对采样的数据使用完全分裂的方式来建立决策树,这样的决策树每个节点要么无法分裂,要么所有的样本都指向同一个分类

5、重复2的过程K次,即可建立森林


[5]连续与缺失值

那么当遇到属性是连续或缺失时,我们应该知道该如何处理?

连续值处理

遇到连续值属性时处理逻辑是:将连续值转换为离散值。我们需要做的是将训练样本中该属性的所有取值进行排序,并对这排好序的取值队列进行分区划分,每一个分区即为该属性的一个离散点取值。

我们取二分法(即分为两个分区,可以根据需要分为N个)来进行描述。将区间的中位点作为候选划分点:

image

根据不同的划分,我们可以分别计算一下信息增益的大小,然后选出一个最好的划分来。

需注意的是,和离散值不同,连续值的划分是可以在子结点上继续划分的。即你将身高划分为“小于175”和“大于等于175”两部分,对于“小于175”的子结点,你仍然可以继续划分为“小于160”和“大于160”两部分。

[6]缺失值处理

属性值缺失的情况下进行属性划分选择

不将缺失值的样本代入选择判断的公式计算(信息增益、增益率、基尼指数)之中,只在计算完后乘以一个有值的样本比例即可。

比如训练集有10个样本,在属性 a 上,有两个样本缺失值,那么计算该属性划分的信息增益时,我们可以忽略这两个缺失值的样本来计算信息增益,然后在计算结果上乘以8/10即可。

本在划分属性上的值为时节点的分配

若样本 x 在划分属性 a 上取值未知,则将 x 划入所有子结点,但是对划入不同子结点中的 x 赋予不同的权值(不同子结点上的不同权值一般体现为该子结点所包含的数据占父结点数据集合的比例)。

[7]多变量决策树

在此类决策树中,非叶结点不再是仅对某个属性,而是对属性的线性组合进行测试;换言之,每个非叶结点时一个形如线性回归的线性分类器了。下图是一个在连续属性密度和含糖率上的选瓜多变量决策树样例:

image
image

python构建决策树这篇文章总结的很好:https://www.jianshu.com/p/e7ed734b8fcb

你可能感兴趣的:(《机器学习》第四章:决策树)