决策树之ID3算法及其Python实现

  • 主要内容
  • 决策树背景知识
  • 决策树一般构建过程
  • ID3算法分裂属性的选择
  • ID3算法流程及其优缺点分析
  • ID3算法Python代码实现

1. 决策树背景知识
  决策树是数据挖掘中最重要且最常用的方法之一,主要应用于数据挖掘中的分类和预测。决策树是知识的一种呈现方式,决策树中从顶点到每个结点的路径都是一条分类规则。决策树算法最先基于信息论发展起来,经过几十年发展,目前常用的算法有:ID3、C4.5、CART算法等。

2. 决策树一般构建过程
  构建决策树是一个自顶向下的过程。树的生长过程是一个不断把数据进行切分细分的过程,每一次切分都会产生一个数据子集对应的节点。从包含所有数据的根节点开始,根据选取分裂属性的属性值把训练集划分成不同的数据子集,生成由每个训练数据子集对应新的非叶子节点。对生成的非叶子节点再重复以上过程,直到满足特定的终止条件,停止对数据子集划分,生成数据子集对应的叶子节点,即所需类别。测试集在决策树构建完成后检验其性能。如果性能不达标,我们需要对决策树算法进行改善,直到达到预期的性能指标。
  注:分裂属性的选取是决策树生产过程中的关键,它决定了生成的决策树的性能、结构。分裂属性选择的评判标准是决策树算法之间的根本区别。

3. ID3算法分裂属性的选择——信息增益
  属性的选择是决策树算法中的核心。是对决策树的结构、性能起到决定性的作用。ID3算法基于信息增益的分裂属性选择。基于信息增益的属性选择是指以信息熵的下降速度作为选择属性的方法。它以的信息论为基础,选择具有最高信息增益的属性作为当前节点的分裂属性。选择该属性作为分裂属性后,使得分裂后的样本的信息量最大,不确定性最小,即熵最小。
  信息增益的定义为变化前后熵的差值,而熵的定义为信息的期望值,因此在了解熵和信息增益之前,我们需要了解信息的定义。
  信息:分类标签xi 在样本集 S 中出现的频率记为 p(xi),则 xi 的信息定义为:−log2p(xi) 。
  分裂之前样本集的熵:E(S)=−∑Ni=1p(xi)log2p(xi),其中 N 为分类标签的个数。
  通过属性A分裂之后样本集的熵:EA(S)=−∑mj=1|Sj||S|E(Sj),其中 m 代表原始样本集通过属性A的属性值划分为 m 个子样本集,|Sj| 表示第j个子样本集中样本数量,|S| 表示分裂之前数据集中样本总数量。
  通过属性A分裂之后样本集的信息增益:InfoGain(S,A)=E(S)−EA(S)
  注:分裂属性的选择标准为:分裂前后信息增益越大越好,即分裂后的熵越小越好。

4. ID3算法
  ID3算法是一种基于信息增益属性选择的决策树学习方法。核心思想是:通过计算属性的信息增益来选择决策树各级节点上的分裂属性,使得在每一个非叶子节点进行测试时,获得关于被测试样本最大的类别信息。基本方法是:计算所有的属性,选择信息增益最大的属性分裂产生决策树节点,基于该属性的不同属性值建立各分支,再对各分支的子集递归调用该方法建立子节点的分支,直到所有子集仅包括同一类别或没有可分裂的属性为止。由此得到一棵决策树,可用来对新样本数据进行分类。

  • ID3算法流程:
    (1) 创建一个初始节点。如果该节点中的样本都在同一类别,则算法终止,把该节点标记为叶节点,并用该类别标记。
    (2) 否则,依据算法选取信息增益最大的属性,该属性作为该节点的分裂属性。
    (3) 对该分裂属性中的每一个值,延伸相应的一个分支,并依据属性值划分样本。
    (4) 使用同样的过程,自顶向下的递归,直到满足下面三个条件中的一个时就停止递归。
      A、待分裂节点的所有样本同属于一类。
      B、训练样本集中所有样本均完成分类。
      C、所有属性均被作为分裂属性执行一次。若此时,叶子结点中仍有属于不同类别的样本时,选取叶子结点中包含样本最多的类别,作为该叶子结点的分类。
  • ID3算法优缺点分析
    优点:构建决策树的速度比较快,算法实现简单,生成的规则容易理解。
    缺点:在属性选择时,倾向于选择那些拥有多个属性值的属性作为分裂属性,而这些属性不一定是最佳分裂属性;不能处理属性值连续的属性;无修剪过程,无法对决策树进行优化,生成的决策树可能存在过度拟合的情况。

5. ID3算法Python代码实现

# -*- coding: utf-8 -*-
__author__ = 'zhihua_oba'

import operator
from numpy import *
from math import log

#文件读取
def file2matrix(filename, attribute_num):   #传入参数:文件名,属性个数
    fr = open(filename)
    arrayOLines = fr.readlines()
    numberOfLines = len(arrayOLines)    #统计数据集行数(样本个数)
    dataMat = zeros((numberOfLines, attribute_num))
    classLabelVector = []   #分类标签
    index = 0
    for line in arrayOLines:
        line = line.strip() #strip() 删除字符串中的'\n'
        listFromLine = line.split() #将一个字符串分裂成多个字符串组成的列表,不带参数时以空格进行分割,当代参数时,以该参数进行分割
        dataMat[index, : ] = listFromLine[0:attribute_num]    #读取数据对象属性值
        classLabelVector.append(listFromLine[-1])   #读取分类信息
        index += 1
    dataSet = []    #数组转化成列表
    index = 0
    for index in range(0, numberOfLines):
        temp = list(dataMat[index, :])
        temp.append(classLabelVector[index])
        dataSet.append(temp)
    return dataSet

#划分数据集
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featvec in dataSet: #每行
        if featvec[axis] == value:  #每行中第axis个元素和value相等   #删除对应的元素,并将此行,加入到rerDataSet
            reducedFeatVec = featvec[:axis]
            reducedFeatVec.extend(featvec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

#计算香农熵  #计算数据集的香农熵 == 计算数据集类标签的香农熵
def calcShannonEnt(dataSet):
    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.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        shannonEnt -= prob * log(prob, 2)
    return shannonEnt

#根据香农熵,选择最优的划分方式    #根据某一属性划分后,类标签香农熵越低,效果越好
def chooseBestFeatureToSplit(dataSet):
    baseEntropy = calcShannonEnt(dataSet)   #计算数据集的香农熵
    numFeatures = len(dataSet[0])-1
    bestInfoGain = 0.0  #最大信息增益
    bestFeature = 0    #最优特征
    for i in range(0, numFeatures):
        featList = [example[i] for example in dataSet]  #所有子列表(每行)的第i个元素,组成一个新的列表
        uniqueVals = set(featList)
        newEntorpy = 0.0
        for value in uniqueVals:    #数据集根据第i个属性进行划分,计算划分后数据集的香农熵
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet)/float(len(dataSet))
            newEntorpy += prob*calcShannonEnt(subDataSet)
        infoGain = baseEntropy-newEntorpy   #划分后的数据集,香农熵越小越好,即信息增益越大越好
        if(infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

#如果数据集已经处理了所有属性,但叶子结点中类标签依然不是唯一的,此时需要决定如何定义该叶子结点。这种情况下,采用多数表决方法,对该叶子结点进行分类
def majorityCnt(classList): #传入参数:叶子结点中的类标签
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
            classCount[vote] += 1
    sortedClassCount = sorted(classCount.iteritems(), 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[:]
        subDataSet = splitDataSet(dataSet, bestFeat, value)
        myTree[bestFeatLabel][value] = createTree(subDataSet, subLabels)
    return myTree

#测试算法:使用决策树,对待分类样本进行分类
def classify(inputTree, featLabels, testVec):   #传入参数:决策树,属性标签,待分类样本
    firstStr = inputTree.keys()[0]  #树根代表的属性
    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

def main():
    dataSet = file2matrix('test_sample.txt', 4)
    labels = ['attr01', 'attr02', 'attr03', 'attr04']
    labelsForCreateTree = labels[:]
    Tree = createTree(dataSet, labelsForCreateTree )
    testvec = [2, 3, 2, 3]
    print classify(Tree, labels, testvec)
if __name__ == '__main__':
     main()