决策树(1)ID3原理以及代码实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/macanv/article/details/60958585

1. 简介

决策树又称判别树,它是基于树形结构来进行决策的,就比如咱以前学过的二叉树,一个节点有两个孩子,左孩子为true,右孩子为false,在决策树中,一个节点代表一个属性(特征,feature。。。)每一个属性可能有不同的属性值,有几个不同的属性值就有几个分支,决策树的树形结构就是数据集中的每一个属性作为其节点构造起来的。

2. 划分选择

在进行决策树构建的时候,我们的目标是使的划分到每一个节点的样本属于同一个类,极端一点样本就一个类,在第一个节点就已经划分好了。所有的决策树方法都基于这一需求,arg max( purity(node) )。
根据这一点,就提出了以下几种不同的度量纯度的方法:
+ 1. 信息增益 (information gain) ID3 决策树
+ 2. 增益率(gain ration) C4.5 决策树
+ 3. 基尼指数(Gini index) CART(Classification and Regression Tree) 决策树

本文只讲ID3决策树的原理以及实现,其他剩下的决策树方法在其他博文中补上。
信息增益建立在信息熵的基础之上,信息熵(information entropy) 是一种度量样本集合纯度的一种常用指标,假定当前样本集合有N个类,那么第i个类的信息为:
这里写图片描述
其熵为:
这里写图片描述
熵越小,其纯度也就越高
现在假定我们的数据集总体是D,有k个特征:F={f1, f2, f3,…,fk},其中f1的可能有V个不同的特征值(例如周志华老师书中讲到的西瓜的色泽有:青绿、乌黑、浅白,色泽是一个特征,不同的颜色是该特征不同取值)现在如果在f1这个特征下对样本集进行划分,那么会有V个不同的分支。会有部分数据划分到第v个分支中,第v个分支节点包含了在特征f1上取值为f1中第v个特征值的样本,这部分样本记为Dv。那么根据上式可以计算出Dv的信息熵:,Ent(Dv),每一个分支的Dv不一样,所以在计算f1特征上的信息增益的时候,取其权重|Dv|/|D|,这样的权重表明样本数越多的节点对整个树上节点的纯度影响越大。那么,在特征f1节点上的信息增益为:
这里写图片描述
周老师书上说,信息增益越大,意味着使用特征f1来划分所获得的纯度提升越大,因此可以使用信息增益来进行决策树的划分特征选择。

3. ID3决策树的坑

在使用ID3决策树的时候,有几点个人觉得很有必要注意一下:
+ 1. ID3决策树的特征值是离散的,而不是连续的(比如瓜的色泽是白色,青绿色,而不是1连续表示的数值)
+ 2. ID3决策树使用的信息增益准则对可取值数目较多的特征有偏好,例如数据样本中有一个特征是连续的特征值,每一个值都不一样,这样会使得信息增益变得很大,会使得决策树不具泛化能力(泛化能力降低)
+ 3. 即使将连续性数据通过量化化后转化为了标称数据类型,ID3可能任然不能更好的胜任。

4. 代码实现(ID3)

这里的代码主要使用了Machine learning in action 里面的代码,以后有时间使用java实现后再上传一次:

# -*- coding:utf-8 -*-

import numpy as np
from math import log
import operator

"""
author :macan
date   :2017.3.8
mail   :[email protected]
"""
class DecisionTree:
    """
    决策树  ID3 algorithm
    输入为一个二维的矩阵,每一行是一个样本,最后一维是样本的标签
    """

    def __init__(self, dataSet):
        self.dataSet = dataSet

    def calcShannonEnt(self):
        """
        计算数据集的熵
        :param dataSet: 给定带计算的数据集
        :return: 返回数据集的熵
        """
        numEntries = len(self.dataSet)
        labelCount = {}
        for featVec in self.dataSet:
            # 最后一列
            currentLabel = featVec[-1]
            # 如果当前这个类别不在类别集合中,设置其value为0,否则value + 1
            if currentLabel not in labelCount.keys():
                labelCount[currentLabel] = 0
            else:
                labelCount[currentLabel] += 1
        # 计算整个语料库的总熵
        shannonEnt = 0.0
        for key in labelCount:
            # 计算每一个类的概率
            prob = float(labelCount[key]) / numEntries
            # 累加每一个类的熵
            shannonEnt -= prob * log(prob, 2)

        return shannonEnt

    def splitDataSet(self, dataSet, axis, value):
        """
        划分规则 划分数据集
        :param dataSet:  总数据集
        :param axis: 特征
        :param value: 规则
        :return: 符合规则的特征集合  相当于去掉了axis 这列特征
        """
        retDataSet = []
        for featVec in dataSet:
            if featVec[axis] == value:
                reduceFeactVec = featVec[:axis] #a[:x] 取 list 中的0:x个数据
                reduceFeactVec.extend(featVec[axis + 1:])  # [x:]取x:end数据  这行代码执行完,就获得了0:x x+1:end 这两个list中的并集
                retDataSet.append(reduceFeactVec)
        return retDataSet

    def chooseBestFeatureToSplit(self, dataSet):
        """
        选择信息熵最大的特征划分,  最好的属性划分数据集
        :param self:
        :param dataSet:  数据集
        :return: 返回最好的属性
        """
        # 获取特征总数, -1 去掉了类别哪一列
        numFeatures = len(dataSet[0]) - 1
        # 计算样本的总熵
        bestEntroy = self.calcShannonEnt(dataSet)
        bestInfoGain = 0.0
        bestFeature = -1
        # 对每一个特征进行挑选
        for i in xrange(numFeatures):
            # featList 中保存着每一个样本的第i 个特征的数值
            featList = [example[i] for example in dataSet]
            # 唯一分类特征
            uniqueVals = set(featList)
            newEntory = 0.0
            # 计算在特征维度为i,特征数值为value 的规则下划分数据集的信息增益后部分
            for value in uniqueVals:
                # 去掉当前特征后的划分数据集
                subDataSet = self.splitDataSet(dataSet, i, value)
                prob = len(subDataSet) / float(len(dataSet))
                # 计算当前
                newEntory == prob * self.calcShannonEnt(subDataSet)

            # 计算信息增益
            infoGain = bestEntroy - newEntory
            # 如果信息增益大于先前的信息熵。update best entropy
            if (infoGain > bestEntroy):
                bestEntroy = infoGain
                bestFeature = i
        return bestFeature

    def majorityCnt(self, classList):
        """
        多数表决的方法,决定叶子节点的分类
        :param classList: 每一个样本的类别 list
        :return: 类别出现次数最多的类
        """
        classCount = {}
        for vote in classList:
            if vote not in classCount.keys():
                classCount[vote] = 0
            else:
                classCount[vote] += 1
        # 将类别按照出现的次数排序
        sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
        return sortedClassCount[0][0]

    def createTree(self, dataSet, labes):
        """
        决策树的构建方法  递归构建决策树
        :param dataSet: 带分类数据集
        :param labes: 标签
        :return: 返回决策树map
        """
        # 获取每一个样本的类别
        classList = [example[-1] for  example in dataSet]
        # 类别完全相同的时候,停止条件  相当于训练集合中只有一个类
        if classList.count(classList[0]) == len(classList):
            return classList[0]
        # 如果数据样本只有一列
        if (len(dataSet[0]) == 1):
            # 遍历所有的特征,返回出现次数做多的
            return self.majorityCnt(classList)
        # 根据信息增益,挑选最好的特征
        bestFeat = self.chooseBestFeatureToSplit(dataSet)
        # 获取特征label
        bestFeatLabels = labes[bestFeat]
        # init decision tree
        myTree = {bestFeatLabels : {}}
        # 删除当前已经处理的特征 label
        del (labes[bestFeat])
        # 获取当前特征列的特征值
        featValues = [example[bestFeat] for example in dataSet]
        # 构建唯一特征值
        uniqueValues = set(featValues)
        #
        for value in uniqueValues:
            subLabels = labes[:]
            # 特征:特征value  色泽:白、黑、青  递归构造决策树
            myTree[bestFeatLabels][value] = self.createTree(self.splitDataSet(dataSet, bestFeat, value), subLabels)
        return myTree

    def retrieveTree(self, i):
        """
        输出预先存储的树信息
        :param i:
        :return:
        """
        pass

    def storeTree(self, inputTree, filename):
        """
        存储决策树
        :param inputTree:  输入tree
        :param filename:  存储路径
        :return:
        """
        import pickle
        with open(filename, 'w') as fw:
            pickle.dump(inputTree, fw)

    def grabTree(self, filename):
        import pickle
        with open(filename, 'r') as fw:
            return pickle.load(fw)

    def fit(self, train_x, train_y, test_x):
        """
        分类
        :param train_x: 训练数据
        :param train_y:  训练数据的label
        :param test_x: 待分类的测试数据集
        :return: 返回测试数据的预测分类
        """
        # 1.生成决策树
        tree = self.createTree(train_x, train_y)
        firstStr = tree.keys()[0]
        secondDict = tree[firstStr]
        featIndex = train_y.index[firstStr]
        # classLabel = 0
        for key in secondDict.keys():
            if test_x[featIndex] == key:
                if type(secondDict[key]).__name == 'dict':
                    classLabel = self.fit(secondDict[key], train_y, test_x)
                else:
                    classLabel = secondDict[key]

        return classLabel

5. 参考文献

  1. 《机器学习》 周志华
  2. 《Machine Learning in Action》 Peter Harrington
  3. 百度百科: 泛化能力

猜你喜欢

转载自blog.csdn.net/macanv/article/details/60958585