统计学习之五-------决策树之分类树(ID3,C4.5)

1. 决策树模型

1.1 概括

  1. ID3,C4.5决策树主要用于分类,注意,这两个树不是二叉树,只是下面图这样举例子而已
  2. 我们先看它长什么样子,如下图:
    数据(x,y),其中数据x特征维度为2(上进心,有房子),y有三类(goodbye,备胎,考虑)

在这里插入图片描述

  1. 上图中的就是决策树的模型(我们最终也是要根据数据,构造出一个树结构,这就是它的模型),其中方框(非叶子节点)表示特征,圆框(叶子节点)表示类别,从父节点到孩子节点的选择过程属于特征选择部分.
  2. 这类决策树很像是在学习一种分类规则(先按什么分,再按照什么分…),那我们怎么知道自己选的规则集合是最优的?
  3. 对于常见的统计模型,一般把模型求解问题转化成损失函数的最优化问题,但是决策树是一个NP问题,显示求解最优化情况太复杂(类似于规则全排列),因此退而求其次,用一种启发算法来近似求解.因此结果不一定是最优的,是次最优的
  4. (个人水平有限,没太清楚这个启发求解过程中有没有模型损失函数的参与(虽然有特征选择的对应比较过程),个人感觉没有,虽然书上说"以损失函数最小化为目标")
  5. 我们的模型怎么用,很简单输入一个数据,数据根据对应节点(某一维度特征)的分类标准,进到对应子树里,直到叶子节点上,而这个叶子节点所代表的类别就是它的类别
  6. 所以重点是,我们怎么建立这样一个树结构

1.2 模型

  • 其实如果抛开细节和算法实现,其核心:就是特征选择,即一堆数据,希望选出某一维度特征,所有数据按照这一维度特征的类别划分成对应的几部分,然后记录信息后,继续划分,如此递归下去,直到递归结束条件.

  • ID3和C4.5的不同就是这个特征选择的公式选取不同.

  • 这三个公式我很久前看也很头疼,不过其实没什么,就是一个计算公式而已,里面加绝对值的那些全是基于数据统计出来的频率,用频率表示概率,高大上点,这叫最大似然估计…

  • 计算熵的公式,含义:当前数据的随机性(不确定性)(注意,概率为0.5是随机性最高,不偏向于0或1的任何一边,因此最不确定)
    在这里插入图片描述

  • 条件熵,就是在给定A的情况下的熵
    在这里插入图片描述

  • 信息增益的公式(ID3使用),两者相减,表示给定A情况下,可以减少多少不确定性.

在这里插入图片描述

  • 信息增益比(C4.5)

g R ( D , A ) = g ( D , A ) H ( D ) g_{R}(D,A) = \frac{g(D,A)}{H(D)} gR(D,A)=H(D)g(D,A)

对比参考
在这里插入图片描述
在这里插入图片描述

2. 代码篇

  1. ID3和C4.5决策树是2叉的吗??
    不是
  2. 构建树时,决策树数据集在用于节点划分时,其规模在不断减少(特征数量以及个数都在少)
    离散:不断划分,split(data,i,value)),其中i表示第几列,value表示等于哪一个离散值(不一定二分类,多分类也可以啊)
    连续:二分类划分,将数据集分为不大于和大于value的两部分
  3. 求单列连续特征信息增益
    需要离散化,即判断以哪一个点分割连续的特征列:这里是先去重,再排序(针对对应特征列),计算相邻两个数的中间值,分别用其分割,求解谁最佳分解点,
  4. 求单列离散特征信息增益
    不用分割,天然就是离散的,直接计算信息增益就好了
  5. 建立树的关键代码
    树的结构存储在字典中,关键逻辑代码
    myTree[bestFeatLabel][value] = subTree
    
  6. 预减枝,后减枝
    参考
    预剪枝标准(建树过程中): 用验证集测试,划分后分错的error和不划分的error,如果大于阈值,就不继续划分(代码中用的就是这种)
    后剪枝(树建立后):根据损失函数(这个损失函数是 经验损失 + 结构风险),从下往上对比叶子节点合并前后(向上回缩),损失函数是否减少,以此来判别是否合并叶子节点

代码参考

  • 代码来源于网上,很久以前clone下来的,忘了出处了,简单参考了解具体细节,这些都不是能写出来的,需要码出来
from math import log
import operator # operator模块是用c实现的,所以执行速度比python代码快
from collections import Counter
import numpy as np
import copy

class DecisionTree:
    #def __init__(self):

    def calcShannonEnt(self,dataSet):
        """
        Desc:
            calculate Shannon entropy -- 计算给定数据集的香农熵
        Args:
            dataSet -- 数据集
        Returns:
            shannonEnt -- 返回 每一组 feature 下的某个分类下,香农熵的信息期望
        """
        numEntries = len(dataSet)

        labelCounts = {}
        # the the number of unique elements and their occurance
        for featVec in dataSet:
            # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签
            currentLabel = featVec[-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
            # log base 2
            # 计算香农熵,以 2 为底求对数
            shannonEnt -= prob * log(prob, 2)

        return shannonEnt

    # 离散值:该函数才是那个最秒的...解决了递归三个要求之一:规模越来越小,其行、列同时选择的方法很巧妙
    def splitDataSet(self,dataSet, index, value):
        """
        Desc:
            划分数据集
            splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行)
            就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
        Args:
            dataSet  -- 数据集                 待划分的数据集
            index -- 表示每一行的index列        划分数据集的特征
            value -- 表示index列对应的value值   需要返回的特征的值。
        Returns:
            index 列为 value 的数据集【该数据集需要排除index列】
        """
        retDataSet = [data[:index] + data[index + 1:] for data in dataSet for i, v in enumerate(data) if
                      i == index and v == value]

        return retDataSet

    # 连续值的分割
    def splitDataSetForSeries(self,dataSet, axis, value):

        """
        按照给定的数值,将数据集分为不大于和大于两部分
        :param dataSet: 要划分的数据集
        :param i: 特征值所在的下标
        :param value: 划分值
        :return:
        """
        # 用来保存不大于划分值的集合
        eltDataSet = []
        # 用来保存大于划分值的集合
        gtDataSet = []
        # 进行划分,保留该特征值
        for feat in dataSet:
            if feat[axis] <= value:
                eltDataSet.append(feat)
            else:
                gtDataSet.append(feat)

        return eltDataSet, gtDataSet

    # 求单列连续特征信息增益的函数
    def calcInfoGainForSeries(self,dataSet, i, baseEntropy):
        """
        计算连续值的信息增益
        :param dataSet:整个数据集
        :param i: 对应的特征值下标
        :param baseEntropy: 基础信息熵
        :return: 返回一个信息增益值,和当前的划分点
        """

        # 记录最大的信息增益
        maxInfoGain = 0.0

        # 最好的划分点
        bestMid = -1

        # 得到数据集中所有的当前特征值列表
        featList = [example[i] for example in dataSet]

        # 得到分类列表
        classList = [example[-1] for example in dataSet]

        dictList = dict(zip(featList, classList))

        # 将其从小到大排序,按照连续值的大小排列
        sortedFeatList = sorted(dictList.items(), key=operator.itemgetter(0))

        # 计算连续值有多少个
        numberForFeatList = len(sortedFeatList)

        # 二分隔,所以用相邻两个数的中间值分割
        midFeatList = [round((sortedFeatList[k][0] + sortedFeatList[k + 1][0]) / 2.0, 3) for k in
                       range(numberForFeatList - 1)]

        # 计算出各个划分点信息增益
        for mid in midFeatList:

            # 将连续值划分为不大于当前划分点和大于当前划分点两部分
            eltDataSet, gtDataSet = self.splitDataSetForSeries(dataSet, i, mid)

            # 计算两部分的特征值熵和权重的乘积之和
            newEntropy = float(len(eltDataSet)) / float(len(sortedFeatList)) * float(
                self.calcShannonEnt(eltDataSet)) + float(len(gtDataSet)) / float(len(sortedFeatList)) * float(
                self.calcShannonEnt(gtDataSet))

            # 计算出信息增益
            infoGain = baseEntropy - newEntropy
            # print('当前划分值为:' + str(mid) + ',此时的信息增益为:' + str(infoGain))
            if infoGain > maxInfoGain:
                bestMid = mid
                maxInfoGain = infoGain

        return maxInfoGain, bestMid

    # 求单列离散特征信息增益的函数
    def calcInfoGain(self,dataSet, featList, i, baseEntropy):
        """
        计算信息增益
        :param dataSet: 数据集
        :param featList: 当前特征列表
        :param i: 当前特征值下标
        :param baseEntropy: 基础信息熵
        :return:
        """
        # 将当前特征唯一化,也就是说当前特征值中共有多少种
        uniqueVals = set(featList)

        # 新的熵,代表当前特征值的熵
        newEntropy = 0.0

        # 遍历现在有的特征的可能性
        for value in uniqueVals:
            # 在全部数据集的当前特征位置上,找到该特征值等于当前值的集合
            subDataSet = self.splitDataSet(dataSet, i, value)
            # 计算出权重
            prob = float(len(subDataSet)) / float(len(dataSet))
            # 计算出当前特征值的熵
            newEntropy += prob * self.calcShannonEnt(subDataSet)

        # 计算出“信息增益”
        infoGain = baseEntropy - newEntropy

        return infoGain

    def chooseBestFeatureToSplit(self,dataSet):
        """
        Desc:
            选择切分数据集的最佳特征
        Args:
            dataSet -- 需要切分的数据集
        Returns:
            bestFeature -- 切分数据集的最优的特征列
        """
        # 得到数据的特征值总数
        numFeatures = len(dataSet[0]) - 1
        print(numFeatures)

        # 计算初始香农熵,单纯基于标签
        baseEntropy = self.calcShannonEnt(dataSet)

        # 基础信息增益为0.0
        bestInfoGain = 0

        # 最好特征值的index
        bestFeature = -1

        # 标记当前最好的特征值是不是连续值
        flagSeries = 0

        # 如果是连续值的话,用来记录连续值的划分点
        bestSeriesMid = 0.0

        # 遍历每一个特征
        for i in range(numFeatures):

            # 对当前第i列的特征进行统计
            # feature_count = Counter([data[i] for data in dataSet])
            featList = [example[i] for example in dataSet]

            # 判断是否连续离散
            series_flag = len(set(featList))

            # 可以自己调,当然这些直接写死很麻瓜。
            # 参考的代码其函数的写法个人不赞同,实际情况我们也会去用独热编码的,单纯字符串判断不了
            if series_flag < 10:
                # 离散求解信息增益
                infoGain = self.calcInfoGain(dataSet, featList, i, baseEntropy)
                print(infoGain)
            else:
                # 连续数值求解信息增益
                infoGain,bestMid = self.calcInfoGainForSeries(dataSet, i, baseEntropy)
                flagSeries = 1

            # 如果当前的信息增益比原来的大
            if infoGain > bestInfoGain:
                # 最好的信息增益
                bestInfoGain = infoGain

                # 新的最好的用来划分的特征值
                bestFeature = i


            if flagSeries:
                bestSeriesMid = bestMid

            # print('信息增益最大的特征为:' + labels[bestFeature])
            if flagSeries:
                return bestFeature, bestSeriesMid
            else:
                return bestFeature

    # 这个函数用于预剪枝和后剪枝
    def testingMajor(self,major,data_test):
        error = 0.0
        for i in range(len(data_test)):
            if major != data_test[i][-1]:
                error += 1
        return float(error)

    def majorityCnt(self,classList):
        """
        Desc:
            选择出现次数最多的一个结果
        Args:
            classList label列的集合
        Returns:
            bestFeature 最优的特征列
        """
        classCount = {}
        for vote in classList:
            if vote not in classCount.keys():
                classCount[vote] = 0
            classCount[vote] += 1
        # 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果
        '''sorted用法比较'''
        # sorted(students, key=lambda student : student[2])
        # sorted(students, key=operator.itemgetter(2))
        sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
        return sortedClassCount[0][0]


    # 利用训练好的树分类,同样采用递归,这里我真的是无语了,思维误区害死人
    def classify(self,inputTree, featLabels, testVec):
        """
        Desc:
            对新数据进行分类
        Args:
            inputTree  -- 已经训练好的决策树模型
            featLabels -- Feature标签对应的名称,不是目标变量
            testVec    -- 测试输入的数据,注意注意!!!!! 一维的列表......
        Returns:
            classLabel -- 分类的结果值,需要映射label才能知道名称
        """
        # 获取tree的根节点对于的key值
        firstStr = list(inputTree.keys())[0]

        # 通过key得到根节点对应的value
        secondDict = inputTree[firstStr]

        # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
        featIndex = featLabels.index(firstStr)

        # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
        key = testVec[featIndex]

        valueOfFeat = secondDict[key]

        # 判断分枝是否结束: 判断valueOfFeat是否是dict类型
        if isinstance(valueOfFeat, dict):
            classLabel = self.classify(valueOfFeat, featLabels, testVec)
        else:
            classLabel = valueOfFeat
        return classLabel

    # 这个函数专门用于"后剪枝",错误率计算
    def testing(self,myTree, data_test, lists):
        # 这里输入的labels不是全部的特征名称
        # 这里输入的data_test不带有全部的特征名称

        error = 0.0
        for i in range(len(data_test)):
            if self.classify(myTree, lists, data_test[i]) != data_test[i][-1]:  # 如果预测结果与验证数据的类别标签不一致
                error += 1  # 那么错误数就+1
        return float(error)

    '''
    程序思路:
        1. 以递归为主体,循环当然也可以,但是没有递归简洁,可如果是针对大规模数据呢??
        2. 程序出口( 出口位置在程序前后中会有影响 ):  (1)特征为空,返回此时的类别
                     (2) 所有实例均属于一类,返回此时的类别
           解释:不能递归的原因有两个,我这个节点达到目的,不需要继续递归了,我这个节点即使可以递归,但是没有多的特征允许了。
        3. 树结构存储在字典内。个人以前还一直以为是指针来着....,但是在字典里面,越想越觉得秒啊    
    '''

    def my_createTree(self,dataSets,feature_name,test_feature_name,test_data,theta,is_post):

        """
        Desc:
            创建决策树
        Args:
            dataSet -- 要创建决策树的训练数据集
            feature_name -- 训练数据集中特征对应的含义的labels,不是目标变量
        Returns:
            myTree -- 创建完成的决策树
        """
        # 第一个出口,节点内全部为一个样本
        labels = [i[-1] for i in dataSets]

        if labels.count(labels[0]) == len(labels):
            return labels[0]

        # 第二个出口,没有特征可供继续分支
        if len(dataSets[0]) == 1:
            return self.majorityCnt(labels)

        # 得到最好特征的名称
        bestFeatLabel = ''

        # 正常开始: 1. 选取特征,单纯基于  2. 特征分支递归
        # 1 best_feature: index

        # 就这一行,开始没错,后来错了??? bestFeat = -1
        bestFeat = self.chooseBestFeatureToSplit(dataSets)

        # 得到分叉点信息
        bestFeatLabel = feature_name[bestFeat]
        # 离散值标志
        flagSeries = 0

        # 2分支开始
        # 构造存储树的字典
        myTree = {bestFeatLabel: {}}

        # 得到当前特征标签的所有可能值
        featValues = [example[bestFeat] for example in dataSets]


        # 离散值处理
        print("离散")
        # 将本次划分的特征值从列表中删除掉
        del (feature_name[bestFeat])

        # 唯一化,去掉重复的特征值
        uniqueVals = set(featValues)

        # 遍历所有的特征值
        for value in uniqueVals:
            # 得到剩下的特征标签
            subLabels = feature_name[:]
            # 递归调用,将数据集中该特征等于当前特征值的所有数据划分到当前节点下,递归调用时需要先将当前的特征去除掉
            subTree = self.my_createTree(self.splitDataSet(dataSets,bestFeat,value), subLabels,test_feature_name,test_data,theta,is_post)
            # 将子树归到分叉处下
            myTree[bestFeatLabel][value] = subTree

        if is_post:
            # 继续分错误的损失 - 部分错误的损失 > 阈值
            if self.testing(myTree, test_data, test_feature_name) - self.testingMajor(self.majorityCnt(labels),
                                                                                      test_data) > theta:
                return self.majorityCnt(labels)
                # 实现后剪枝操作
                # 无视当前的myThree,直接返回一个叶子节点,等效于实现了REP后剪枝

        #后减枝操作
        return myTree

    # 为了同步,这里简单包装
    def train(self,dataSets,feature_name,test_labels,test_data,theta=1,is_post=True):
        result = self.my_createTree(dataSets,feature_name,test_labels,test_data,theta,is_post)
        return result

    # 适应一、二维列表输入
    def predict(self,inputTree, featLabels, testVec):
        """
        Desc:
            对新数据进行分类
        Args:
            inputTree  -- 已经训练好的决策树模型
            featLabels -- Feature标签对应的名称,不是目标变量
            testVec    -- 测试输入的数据,注意注意!!!!! 一维的列表......
        Returns:
            classLabel -- 分类的结果值,需要映射label才能知道名称
        """

        labels = []

        #
        if isinstance(testVec[0],list):
            for data in testVec:
                label = self.classify(inputTree, featLabels, data)
                labels.append(label)

        else:
            label = self.classify(inputTree, featLabels,testVec)
            labels.append(label)

        return labels
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值