机器学习|决策树代码实现解读

前言

如果你能找到这里,真是我的幸运~这里是蓝白绛的学习笔记,本集合主要针对《统计学习方法》这本书,主要是基本机器学习算法代码实现的解读。
本篇的代码整理来自github:wepe-Basic Machine Learning and Deep Learning
本篇整理决策树实现,github地址为:wepe-DecisionTree

第五章 决策树

1、模型

  1. 分类决策树模型是一种描述对实例进行分类的树形结构,由结点(node)和有向边(directed edge)组成。结点有两种类型:内部结点(internal node)和叶结点(leaf node)。内部结点表示一个特征或属性,叶结点表示一个类。
  2. 特征选择
    特征选择在于选取对训练数据具有分类能力的特征。特征选择的准则通常是信息增益或信息增益比。
    (entropy):表示随机变量不确定性的度量。设X是一个取有限个值的离散随机变量,其概率分布为P(X=x_i)=p_i,i=1,2...,n.则随机变量X的熵定义为H(X)=-\sum_{i=1}^np_i\log p_i定义0\log 0=0\log2为底单位为比特(bit);以e为底单位为纳特(nat)。
    条件熵(conditional entropy):表示在已知随机变量X的条件下随机变量Y的不确定性。随机变量X给定条件下随机变量Y的条件熵H(Y|X)定义为X给定条件下Y的条件概率分布的熵对X的数学期望H(Y|X)=\sum_{i=1}^np_iH(Y|X=x_i)当熵和条件熵中的概率由数据统计(特别是极大似然估计)得到时,所对应的熵与条件熵分别称为经验熵(empirical entropy)和经验条件熵(empirical conditional entropy)。
  • 信息增益(Information gain):特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差g(D,A)=H(D)-H(D|A). H(D)=-\sum_{k=1}^K\frac{|C_k|}{|D|}\log_2\frac{|C_k|}{|D|} H(D|A)=\sum_{i=1}^n\frac{|D_i|}{|D|}H(D_i)=\sum_{i=1}^n\frac{|D_i|}{|D|}\sum_{k=1}^K\frac{|D_{ik}|}{|D|}\log_2\frac{|D_{ik}|}{|D|}其中k表示样本类别数,n为特征A的类别数,|D_{ik}|表示样本中特征A为第i个取值且样本类别为第k个取值的样本个数。信息增益表示由于特征A而使得数据集D的分类的不确定性减少的程度。
    一般地,熵H(Y)与条件熵H(Y|X)之差称为互信息。决策树学习中的信息增益等价于训练数据集中类与特征的互信息。
  • 信息增益比(Information gain ratio):信息增益g(D,A)与训练数据集D关于特征A的值的熵H_A(D)的比g_R(D,A)=\frac{g(D,A)}{H_A(D)} H_A(D)=-\sum_{i=1}^n\frac{|D_i|}{|D|}\log_2\frac{|D_i|}{|D|}\
  • Gini指数(Gini index):假设K个类,样本点属于第k类的概率为p_i,则概率分布的基尼指数定义为{\rm Gini}(p)=\sum_{k=1}^Kp_k(1-p_k)=1-\sum_{k=1}^Kp_k^2二分类问题中,若样本点属于第一个类的概率为p,则概率分布的基尼指数为{\rm Gini}(p)=2p(1-p)对于给定样本集合D,基尼指数为{\rm Gini}(D)=1-\sum_{k=1}^K(\frac{|C_k|}{|D|})^2其中C_k是D中属于第k类的样本子集。
    在特征A的条件下,将A是否取可能值a分为两个子集D_1,D_2。在特征A的条件下,集合D的基尼指数定义为{\rm Gini}(D,A=a)=\frac{|D_1|}{|D|}{\rm Gini}(D_1)+\frac{|D_2|}{|D|}{\rm Gini}(D_2)
  1. ID3、C4.5、CART树:
    ID3:特征选择策略为信息增益。
    C4.5:特征选择策略为信息增益比。信息增益偏向于选择取值较多的特征,用信息增益比进行校正。
    CART树:特征选择策略为Gini index。既可做分类也可以做回归。ID3、C4.5可能是多叉树,CART树是二叉树

2、决策树的剪枝

《统计学习方法》这本书上的剪枝均是后剪枝,如果要详细了解剪枝可以查阅其他资料,其中Hulu的《百面机器学习》这本书中的剪枝讲的非常容易理解。本站中我整理的百面机器学习|第三章经典算法知识点记录了这本书中讲解的前剪枝、后剪枝算法介绍,感兴趣的朋友可以查阅。

3、代码实现

下面的代码块中的代码原作者依然是wepe:wepe-DecisionTree,我仅添加一些注释,方便理解。
下面的代码中的树是用字典实现的。

class DecisionTree:
    """决策树使用方法:
        - 生成实例: clf = DecisionTrees(). 参数mode可选,ID3或C4.5,默认C4.5
        - 训练,调用fit方法: clf.fit(X,y).  X,y均为np.ndarray类型
        - 预测,调用predict方法: clf.predict(X). X为np.ndarray类型
        - 可视化决策树,调用showTree方法
    """

    def __init__(self, mode='C4.5'):
        self._tree = None# 树的根节点初始值设为None

        if mode == 'C4.5' or mode == 'ID3':# ID3的特征选择方法为信息增益,C4.5的特征选择方法为信息增益比。
            self._mode = mode
        else:
            raise Exception('mode should be C4.5 or ID3')

    def _calcEntropy(self, y):
        """
        函数功能:计算熵
        参数y:数据集的标签
        """
        num = y.shape[0]
        # 统计y中不同label值的个数,并用字典labelCounts存储
        labelCounts = {}
        for label in y:
            if label not in labelCounts.keys():
                labelCounts[label] = 0
            labelCounts[label] += 1
        # 计算熵
        entropy = 0.0
        for key in labelCounts:
            prob = float(labelCounts[key]) / num# 由label对应的个数计算出label的占比
            entropy -= prob * np.log2(prob)# 计算每个label的信息熵,将他们相加,取负
        return entropy

    def _splitDataSet(self, X, y, index, value):
        """
        函数功能:返回数据集中特征下标为index,特征值等于value的子数据集
        """
        ret = []# ret保存当前特征列中特征值等于value的行数下标,后面用于筛选行。
        featVec = X[:, index]# 筛选当前列数据
        X = X[:, [i for i in range(X.shape[1]) if i != index]]# 去除当前列数据,留下剩下的数据
        for i in range(len(featVec)):
            if featVec[i] == value:# 如果当前特征列的值等于value时,保留下标到ret
                ret.append(i)
        return X[ret, :], y[ret]# 根据ret筛选行

    def _chooseBestFeatureToSplit_ID3(self, X, y):
        """ID3
        函数功能:对输入的数据集,选择最佳分割特征
        参数dataSet:数据集,最后一列为label
        主要变量说明:
                numFeatures:特征个数
                oldEntropy:原始数据集的熵
                newEntropy:按某个特征分割数据集后的熵
                infoGain:信息增益
                bestInfoGain:记录最大的信息增益
                bestFeatureIndex:信息增益最大时,所选择的分割特征的下标
        """
        numFeatures = X.shape[1]# numFeatures是特征的数量
        oldEntropy = self._calcEntropy(y)# 计算没有分裂前y的经验熵
        bestInfoGain = 0.0# bestInfoGain为最大信息增益,初始设为0,后面逐步用更大的信息增益替代
        bestFeatureIndex = -1# 每次替换最大信息增益时记录是哪一个特征

        # 对每个特征都计算一下infoGain,并用bestInfoGain记录最大的那个
        for i in range(numFeatures):# 对每个特征
            featList = X[:, i]# 筛选出该特征对应列的数据
            uniqueVals = set(featList)# uniqueVals为这列特征的可能取值

            newEntropy = 0.0# newEntropy记录根据当前特征进行分裂后的经验条件熵
            # 对第i个特征的各个value,得到各个子数据集,计算各个子数据集的熵,
            # 进一步地可以计算得到根据第i个特征分割原始数据集后的熵newEntropy
            for value in uniqueVals:
                sub_X, sub_y = self._splitDataSet(X, y, i, value)
                prob = len(sub_y) / float(len(y))
                newEntropy += prob * self._calcEntropy(sub_y)
                # 计算按特征i分裂的经验条件熵,将特征i对应的几个取值对应的经验条件熵加起来
            infoGain = oldEntropy - newEntropy# 计算信息增益,根据信息增益选择最佳分割特征
            if (infoGain > bestInfoGain):# 如果计算得到的信息增益大于当前信息增益,则更新信息增益及对应特征下标
                bestInfoGain = infoGain
                bestFeatureIndex = i
        return bestFeatureIndex

    def _chooseBestFeatureToSplit_C45(self, X, y):
        """C4.5
            ID3算法计算的是信息增益,C4.5算法计算的是信息增益比,对上面ID3版本的函数稍作修改即可
        """
        numFeatures = X.shape[1]
        oldEntropy = self._calcEntropy(y)
        bestGainRatio = 0.0
        bestFeatureIndex = -1
        # 对每个特征都计算一下gainRatio=infoGain/splitInformation
        for i in range(numFeatures):
            featList = X[:, i]
            uniqueVals = set(featList)
            newEntropy = 0.0
            splitInformation = 0.0
            # 对第i个特征的各个value,得到各个子数据集,计算各个子数据集的熵,
            # 进一步地可以计算得到根据第i个特征分割原始数据集后的熵newEntropy
            for value in uniqueVals:
                sub_X, sub_y = self._splitDataSet(X, y, i, value)
                prob = len(sub_y) / float(len(y))
                newEntropy += prob * self._calcEntropy(sub_y)
                splitInformation -= prob * np.log2(prob)
            # 计算信息增益比,根据信息增益比选择最佳分割特征
            # splitInformation若为0,说明该特征的所有值都是相同的,显然不能作为分割特征,则计算下一个特征
            if splitInformation == 0.0:
                pass
            else:
                infoGain = oldEntropy - newEntropy
                gainRatio = infoGain / splitInformation
                if (gainRatio > bestGainRatio):
                    bestGainRatio = gainRatio
                    bestFeatureIndex = i
        return bestFeatureIndex

    def _majorityCnt(self, labelList):
        """
        函数功能:返回labelList中出现次数最多的label
        """
        labelCount = {}
        for vote in labelList:
            if vote not in labelCount.keys():
                labelCount[vote] = 0
            labelCount[vote] += 1
        sortedClassCount = sorted(labelCount.iteritems(), key=lambda x: x[1], reverse=True)
        # iteritems()形成的是tuple,x[0]是label,x[1]是count。
        return sortedClassCount[0][0]# 返回排序后的第一个tuple的label

    def _createTree(self, X, y, featureIndex):
        """建立决策树
        featureIndex,类型是元组,它记录了X中的特征在原始数据中对应的下标。featureIndex为('x0','x1',...,'xn')
        """
        labelList = list(y)
        # 所有label都相同的话,则停止分割,返回该label
        if labelList.count(labelList[0]) == len(labelList):
            return labelList[0]

        # 没有特征可分割时,停止分割,返回出现次数最多的label
        if len(featureIndex) == 0:
            return self._majorityCnt(labelList)

        # 可以继续分割的话,确定最佳分割特征
        if self._mode == 'C4.5':
            bestFeatIndex = self._chooseBestFeatureToSplit_C45(X, y)
        elif self._mode == 'ID3':
            bestFeatIndex = self._chooseBestFeatureToSplit_ID3(X, y)

        bestFeatStr = featureIndex[bestFeatIndex]
        featureIndex = list(featureIndex)# featureIndex是一个tuple,先将其转化为list
        featureIndex.remove(bestFeatStr)# 移除被选择的最优分裂特征
        featureIndex = tuple(featureIndex)# 将list变回tuple

        # 用字典存储决策树。最佳分割特征作为key,而对应的键值仍然是一棵树(仍然用字典存储)
        myTree = {bestFeatStr: {}}
        featValues = X[:, bestFeatIndex]
        uniqueVals = set(featValues)
        for value in uniqueVals:
            # 对每个value递归地创建树
            sub_X, sub_y = self._splitDataSet(X, y, bestFeatIndex, value)
            myTree[bestFeatStr][value] = self._createTree(sub_X, sub_y, featureIndex)
            # 最终的树类似{'x2':{1:{x0:{...}}, 2:{}, 3:{}}}
        return myTree

    def fit(self, X, y):
        # 类型检查
        if isinstance(X, np.ndarray) and isinstance(y, np.ndarray):# ndarray对象是用于存放同类型元素的多维数组
            pass
        else:
            try:
                X = np.array(X)
                y = np.array(y)
            except:
                raise TypeError("numpy.ndarray required for X,y")

        featureIndex = tuple(['x' + str(i) for i in range(X.shape[1])])# featureIndex为('x0','x1',...,'xn')的tuple
        self._tree = self._createTree(X, y, featureIndex)# 建立决策树,这里是用字典存储的
        return self  # allow chaining: clf.fit().predict()

    def predict(self, X):
        if self._tree == None:
            raise NotFittedError("Estimator not fitted, call `fit` first")

        # 类型检查
        if isinstance(X, np.ndarray):
            pass
        else:
            try:
                X = np.array(X)
            except:
                raise TypeError("numpy.ndarray required for X")

        def _classify(tree, sample):
            """
            用训练好的决策树对输入数据分类 
            决策树的构建是一个递归的过程,用决策树分类也是一个递归的过程
            _classify()一次只能对一个样本(sample)分类
            To Do: 多个sample的预测怎样并行化?
            """
            featIndex = tree.keys()[0]# featIndex是特征名,一个str
            secondDict = tree[featIndex]# 在当前树中找到当前特征对应的可能值
            key = sample[int(featIndex[1:])]# 得到要预测的sample对应特征的值,为key
            valueOfkey = secondDict[key]# 找到对应key的value
            if isinstance(valueOfkey, dict):# 如果这个value是一个字典,则可继续递归
                label = _classify(valueOfkey, sample)
            else:# 如果这个value是一个值,则到了叶子节点,对应的valueOfkey是要预测的标签
                label = valueOfkey
            return label

        if len(X.shape) == 1:# 预测单条数据
            return _classify(self._tree, X)
        else:# 预测多条数据,循环调用_classify()
            results = []
            for i in range(X.shape[0]):
                results.append(_classify(self._tree, X[i]))
            return np.array(results)

原作者在github中说明了代码还存在几个问题:
(1) 如果测试集中某个样本的某个特征的值在训练集中没出现,则会造成训练出来的树的某个分支对该样本不能分类,出现KeyError。
(2)目前还不能对多个样本并行预测。

结尾

如果您发现我的文章有任何错误,或对我的文章有什么好的建议,请联系我!如果您喜欢我的文章,请点喜欢~*我是蓝白绛,感谢你的阅读!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容

  • 1、模型原理 (一)原理 1、原理:引入信息熵(不确定程度)的概念,通过计算各属性下的信息增益程度(信息增益越大,...
    Python_Franklin阅读 12,350评论 0 17
  • 决策树理论在决策树理论中,有这样一句话,“用较少的东西,照样可以做很好的事情。越是小的决策树,越优于大的决策树”。...
    制杖灶灶阅读 5,851评论 0 25
  • 一. 决策树(decision tree):是一种基本的分类与回归方法,此处主要讨论分类的决策树。在分类问题中,表...
    YCzhao阅读 2,136评论 0 2
  • Decision Trees (DTs) 是一种用来classification和regression的无参监督学...
    婉妃阅读 6,116评论 0 8
  • 决策树 决策树模型与学习 特征选择 决策树的生成 决策树的剪枝 CART 算法 决策树模型实现 决策树模型呈树形结...
    千与千与阅读 699评论 1 1