从头到尾理解树模型(一):决策树

image.png


XGBoost和LightGBM已经成为Kaggle,天池等比赛和数据研究,必用的算法。这篇文章,从决策树出发,一步步引出GBDT、XGBoost和LightGBM,讲解原理,比较不同,介绍应用。

一、决策树(Decision Tree)

1.基本概念

image.png
决策树就像是我们初中学过的流程图,一步步去判断条件,是或否依次进行不同的选择,只不过决策树不只是“是”和“否”这两个判断条件。
决策树不只可以用与分类问题,回归问题同样适用。

比如说图中的过滤邮件的流程图:
if:
  是这个域名发送的邮件:无聊时阅读
else:
  转下一步
if:
  包含曲棍球:需要及时处理
else:
  垃圾邮件

2.构造决策树

决策树的构建是我们的核心内容。决策树构建一般分为为3个步骤:特征选择、决策树的生成和决策树的修剪。
数据收集是否充足,特征选择是否恰当,决策树的修剪,都将影响到我们的准确率和训练速度。接下来主要便是介绍特征选择。

所用到的数据:

image.png
通过所给的训练数据学习一个贷款申请的决策树,用于对未来的贷款申请进行分类,即当新的客户提出贷款申请时,根据申请人的特征利用决策树决定是否批准贷款申请。

3.特征选择

3.1 特征选择的重要性:

回想流程图,一组数据含有多个特征,而我们到底选择哪个特征,首先选择哪个特征,然后选择哪个,会影响到我们训练的速度和准确率。也就是如何选择最优划分的属性的问题。
主要有三个判断指标:信息增益(ID3算法)、增益率(C4.5算法)、基尼指数(CART算法)。括号中分别是不同的三种决策树的构建方法,与这三个指标一一对应,。本文主要介绍信息增益这个指标。

3.2 信息增益

在划分数据集前后,信息发生的变化称为信息增益,获得信息增益最高的特征是最好的选择。

3.2.1 香农熵

集合信息的度量方式成为香农熵或者简称为熵(entropy)。熵是表示随机变量不确定性的度量。是度量样本信息和纯度最常用的一种指标。

  • 多个分类中,某一分类样本信息
    l ( x i ) = log ⁡ 2 p ( x i ) l\left(x_{i}\right)=\log _{2} p\left(x_{i}\right) l(xi)=log2p(xi)
    p(xi):是该分类的概率。

  • 香农熵:算所有类别所有可能值包含的信息期望值(数学期望)
    H = − ∑ i = 1 n p ( x i ) log ⁡ 2 p ( x i ) H=-\sum_{i=1}^{n} p\left(x_{i}\right) \log _{2} p\left(x_{i}\right) H=i=1np(xi)log2p(xi)
    n是分类的数目

编写代码计算经验熵
from math import log

"""
函数说明:创建测试数据集

Parameters:
    无
Returns:
    dataSet - 数据集
    labels - 分类属性
"""
def createDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],         #数据集
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['不放贷', '放贷']             #分类属性
    return dataSet, labels                #返回数据集和分类属性

"""
函数说明:计算给定数据集的经验熵(香农熵)

Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵(香农熵)
"""
def calcShannonEnt(dataSet):
    numEntires = len(dataSet)                        #返回数据集的行数
    labelCounts = {}                                #保存每个标签(Label)出现次数的字典
    for featVec in dataSet:                            #对每组特征向量进行统计
        currentLabel = featVec[-1]                    #提取标签(Label)信息
        if currentLabel not in labelCounts.keys():    #如果标签(Label)没有放入统计次数的字典,添加进去
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1                #Label计数
    shannonEnt = 0.0                                #经验熵(香农熵)
    for key in labelCounts:                            #计算香农熵
        prob = float(labelCounts[key]) / numEntires    #选择该标签(Label)的概率
        shannonEnt -= prob * log(prob, 2)            #利用公式计算
    return shannonEnt                                #返回经验熵(香农熵)

if __name__ == '__main__':
    dataSet, features = createDataSet()
    print(dataSet)
    print(calcShannonEnt(dataSet))
3.2.2 信息增益

信息增益无非就是,我们对原始数据根据不同的特征划分后,出现的新数据的香农熵,与原数据的香农熵的差。不要考虑什么条件概率,条件熵,只要抓住这一点,前后数据熵的变化即可,由于被划分后,数据的个数不同,所以不同的部分占有不同的权重,所以考虑进去权重即可,即使高中概率知识也可理解。
image.png

编写代码计算信息增益
def splitDataSet(dataSet,axis, value):
    """
    函数说明:
        对数据集根据某个特征进行划分
    :param dataSet: 被划分的数据集
    :param axis: 划分根据的特征
    :param value: 需要返回的特征的值
    :return: 无
    """
    retDataSet = []     #建立空列表,存储返回的数据集
    for featVec in dataSet:     #遍历数据,一个对象一个对象的进行操作
        if featVec[axis] == value:      #找到axis特征,等于value值得数据
            reducedFeatVec = featVec[:axis]    #选出去除axis特征,只保留其他特征的数据
            reducedFeatVec.extend(featVec[axis+1:])     #保留后面的
            retDataSet.append(reducedFeatVec)       #对数据进行连接,按照列表形式
    return retDataSet   #返回划分后被取出的数据集,即满足条件的数据
#---------------------------------------------------------------------------
def chooseBestFeaturesToSplit(dataSet):
    """
    函数说明:
        选择最佳的分类属性
    :param :dataSet-数据集
    :return: bestFeature-最佳划分属性
    """
    numFeatures = len(dataSet[0]) - 1       #特征数量,去除最后一个标签值
    baseEntropy = calShannoEnt(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:        #计算信息增益
            #根据第i特征,value值对数据进行划分
            subDataSet = splitDataSet(dataSet, i, value)
            #求划分后数据的经验条件熵
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calShannoEnt(subDataSet)
        infoGain = baseEntropy - newEntropy     #求划分前后的信息增益
        print("第%d个特征的增益是%.3f" %(i,infoGain))
        if(infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

二、决策树的构建

上篇博客已经讲到,构建决策树,我们有三种算法:ID3,C4.5和CART。这一篇,我们只介绍,ID3算法,其他两种后面介绍。

ID3算法

也就是使用使用信息增益作为判断指标,用于选择最优的划分特征。递归调用上述算法,不断产生分支,直到所有特征的信息增益均很小或没有特征可以选择为止。

1. 示例分析:

首先看我们要分析的数据:
image.png
通过上篇写到的信息增益的计算方法,计算各个特征的信息增益,我们发现是否有自己的房子,有最大的信息增益,所以我们把有自己的房子,作为根节点的划分标准。

将训练集D划分为两个子集D1(有自己的房子为"是")和D2(有自己的房子为"否")。由于D1只有同一类的样本点,所以它成为一个叶结点,结点的类标记为“是”。

然后,我们对D2的A1(年龄),A2(有工作)和A4(信贷情况)特征中选择最优划分特征,计算各个特征的信息增益:
image.png
可以看出A2信息增益最大,所以选择A2(有工作)作为划分特征。然后根据是否有工作,再次进行划分。一个对应"是"(有工作)的子结点,包含3个样本,它们属于同一类,所以这是一个叶结点,类标记为"是";另一个是对应"否"(无工作)的子结点,包含6个样本,它们也属于同一类,所以这也是一个叶结点,类标记为"否"。
生成决策树:
image.png

2. 编写代码构建决策树

使用字典存储决策树的结构,上小节的决策树,用字典可以表示为:

{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}

创建函数majorityCnt统计classList中出现此处最多的元素(类标签),创建函数createTree用来递归构建决策树。

递归创建决策树时,递归有两个终止条件:
一:所有的类标签完全相同,则直接返回该类标签;
二:使用完了所有特征,仍然不能将数据划分仅包含唯一类别的分组,即决策树构建失败,特征不够用。此时说明数据纬度不够,由于第二个停止条件无法简单地返回唯一的类标签,这里挑选出现数量最多的类别作为返回值。

创建决策树所需要用到的函数:
  • 1、createDataSet():创建数据集
  • 2、calShannoEnt(dataSet):计算香农熵
  • 3、splitDataSet(dataSet,axis, value):根据特征划分数据集
  • 4、chooseBestFeaturesToSplit(dataSet):选择最佳特征
  • 5、majorityCnt(classList):确定类别
  • 6、createTree(dataSet, labels, featLabels):创建决策树
创建数据:
from math import log
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import operator
#--------------------------------------------------------------
def createDataSet():
    """
    函数说明:创建测试数据集
    参数:无
    return: dataSet-数据集
        labels-分类属性
    """
    dataSet = [
        [0,0,0,0,'no'],
        [0,0,0,1,'no'],
        [0,1,0,1,'yes'],
        [0,1,1,0,'yes'],
        [0,0,0,0,'no'],
        [1,0,0,0,'no'],
        [1,0,0,1,'no'],
        [1,1,1,1,'yes'],
        [1,0,1,1,'yes'],
        [1,0,1,2,'yes'],
        [2,0,1,2,'yes'],
        [2,0,1,1,'yes'],
        [2,1,0,1,'yes'],
        [2,1,0,2,'yes'],
        [2,0,0,0,'no']
    ]
    labels = ['年龄', '有工作', '有自己的房子', '信贷情况']
    return dataSet,labels
计算香农熵:
def calShannoEnt(dataSet):
    """
    函数说明:
        计算数据集的香农熵
    param :dataSet-数据集
    return:  shannonEnt-香农熵
    """
    numEntires = len(dataSet)   #数据集行数
    labelCounts = {}    #该字典用于保存每个标签出现的次数
    for featVec in dataSet:
        currentLabel = featVec[-1]  #for循环,来处理每一个数据的标签
        if currentLabel not in labelCounts.keys():  #对字典进行初始化
            labelCounts[currentLabel] = 0   #令两类初始值分别为0
        labelCounts[currentLabel] += 1  #统计数据

    shannonEnt = 0.0
    for key in labelCounts:     #根据公式,利用循环求香农熵
        prob = float(labelCounts[key])/numEntires
        shannonEnt -= prob*log(prob,2)
    return shannonEnt
#-------------------------------------------------------------
划分数据:
def splitDataSet(dataSet,axis, value):
    """
    函数说明:
        对数据集根据某个特征进行划分
    :param dataSet: 被划分的数据集
    :param axis: 划分根据的特征
    :param value: 需要返回的特征的值
    :return: 无
    """
    retDataSet = []     #建立空列表,存储返回的数据集
    for featVec in dataSet:     #遍历数据,一个对象一个对象的进行操作
        if featVec[axis] == value:      #找到axis特征,等于value值得数据
            reducedFeatVec = featVec[:axis]    #选出去除axis特征,只保留其他特征的数据
            reducedFeatVec.extend(featVec[axis+1:])     #保留后面的
            retDataSet.append(reducedFeatVec)       #对数据进行连接,按照列表形式
    return retDataSet   #返回划分后被取出的数据集,即满足条件的数据
#---------------------------------------------------------------------------
选择最佳分类属性:
def chooseBestFeaturesToSplit(dataSet):
    """
    函数说明:
        选择最佳的分类属性
    :param :dataSet-数据集
    :return: bestFeature-最佳划分属性
    """
    numFeatures = len(dataSet[0]) - 1       #特征数量,去除最后一个标签值
    baseEntropy = calShannoEnt(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:        #计算信息增益
            #根据第i特征,value值对数据进行划分
            subDataSet = splitDataSet(dataSet, i, value)
            #求划分后数据的经验条件熵
            prob = len(subDataSet)/float(len(dataSet))
            newEntropy += prob * calShannoEnt(subDataSet)
        infoGain = baseEntropy - newEntropy     #求划分前后的信息增益
        print("第%d个特征的增益是%.3f" %(i,infoGain))
        if(infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature
#--------------------------------------------------------------
选择类别:

在决策树构建过程中,我们通过不断地划分,可能一直到遍历所有特征,依然不能彻底划分数据,也就是说,划分到最后,包含的数据类别存在不一样。此时我们选择,这多个类别中,出现次数最多的作为该部分数据的类别。

def majorityCnt(classList):
    """
    函数说明:
        当划分结束,遍历所有特征,但是依然不能彻底划分数据时
        将这多个类别中,出现次数最多的作为该类别
    :param classList-类别列表
    :return: sortedClassCount[0][0]-类别
    """
    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, featLabels):
    """
    函数说明:
        创建决策树
    :param dataSet-训练集
    :param labels-训练集数据标签
    :param featLabels:
    :return:
    """
    classList = [example[-1] for example in dataSet]        #取分类标签(是否放贷:yes or no)
    #注意条件,对列表的某一类计数,若为列表长度,则类别完全相同
    if classList.count(classList[0]) == len(classList):      #如果类别完全相同则停止继续划分
        return classList[0]
    if len(dataSet[0]) == 1:         #遍历完所有特征时返回出现次数最多的类标签
        return majorityCnt(classList)
    bestFeat = chooseBestFeaturesToSplit(dataSet)       #最优特征
    bestFeatLabel = labels[bestFeat]        #最优特征的标签
    featLabels.append(bestFeatLabel)        #
    myTree = {bestFeatLabel:{}}     #根据最优特征的标签建立决策树
    del(labels[bestFeat])       #删除已经使用的特征标签
    #训练集中,最优特征所对应的所有数据的值
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)        #去掉重复特征
    for value in uniqueVals:        #遍历特征创建决策树
        #通过此句实现字典的叠加
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),labels,featLabels)
    return myTree
#-------------------------------------------------------------
主函数创建决策树:
if __name__ == '__main__':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    print(myTree)

运行后我们可以得到,刚开始,字典类型的决策树。

二、测试决策树:

测试函数:
def classify(inputTree, featLabels, testVec):
    firstStr = next(iter(inputTree))
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex] == key:
            if type(secondDict[key]).__name__ == 'dict':
                classLabel = classify(secondDict[key], featLabels, testVec)
            else:
                classLabel = secondDict[key]
    return classLabel
主函数:
if __name__ == '__main__':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    testVec = [0,1]                                        #测试数据
    result = classify(myTree, featLabels, testVec)
    if result == 'yes':
        print('放贷')
    if result == 'no':
        print('不放贷')

image.png


以上简短的介绍了,决策树的生成和实现。有句俗话,三个臭皮匠顶个诸葛亮,机器学习上也存在同样类似的理论。我们对决策树,进行集成,得到多个基学习器的组合,效果会比使用单个学习器要好。
下一篇我们介绍集成学习。

ID3C4.5CART
缺失值对缺失值敏感一定处理一定的处理
分之数多分支多分支二叉树
特征复用不会复用不会复用可以被重复利用
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

linxid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值