Machine Learning In Action -- ID3决策树学习算法的python实现

decision tree Learning 决策树学习笔记

决策树学习是一种相对比较简单的分类学习方法,但是分类效果较好并且表示直观,主要针对离散型目标,它也等价于用if-then规则表示。


决策树学习在Mitchell的《机器学习》第三章中有详细讲解,也有许多人做了详细的笔记,比如http://www.cnblogs.com/pangxiaodong/archive/2011/05/11/2042882.html 。这里主要简单介绍最经典的决策树学习算法ID3的python实现。


1. 熵(Entropy)

      决策树中用到了信息论中熵的概念,熵主要表示的是体系的混乱程度,也称不确定程度。在信息论中,熵可以理解为对于信息S,用统一的概率表示,至少需要多少位来编码才能清楚的表示该信息。(这里概率的意思可以理解为:设S为全样本空间,可以根据某一组值(也就是我们的分类值和预测值)来标记S为S1...Sn,每一份在全样本空间中所占的比率。) 熵的公式为:


    举个例子:设S为全体实数空间,以正、负属性来标记S,则属性正、负的概率各为1/2,根据公式可以得到Entropy(S)=1,即我们需要1位来表示属性是正还是负;而如果S为正实数时,属性正、负的概率分别为1和0,Entropy(S)=0,即我们不需要任何位来表示该属性,因为它一定是正的。这就是熵的含义,取值范围为[0,1],值越大表示样本空间不确定性越高。


2.  信息增益(Information Gain)

      理解清楚熵的含义后,我们可以很容易理解信息增益的概念。S依然为样本空间,设A是S中的某一属性,当我们用属性A划分S时,会根据在A上的不同值将S划分为不同的子样本空间S1,S2…,然后我们计算所有子样本空间的熵Entropy(Si),以及Si占S的比例,并用类似全概率公式的方法求划分后的结果的熵Entropy(S’)。从信息论的角度看,这个熵就是用Entropy(S’)位编码才能表示划分后的结果。

     由此,我们可以得到信息增益的值Gain(S,A),就是用划分前的熵Entropy(S)减去划分后的熵Entropy(S’)。即公式


      那么这个值是越大越好,还是越小越好呢?根据上述讨论,当然是划分后的熵越小越好,越小表示划分后的结果越有序,也就越有利于我们进行决策;那么反过来,Gain(S,A)的结果越大说明属性A的划分效果越好,我们就越应该优先用A来划分样本空间S。

     

      OK,这就是最经典的决策树算法ID3的核心问题,即每次划分时哪个属性的划分效果最好?答案就是依次用没有用过的属性去划分父空间(因为涉及到递归),然后用信息增益最大的属性作为当前划分属性。


3.   求解决策树的思想和流程

     1)首先,我们知道决策树的建立是一个递归过程,那么递归的结束条件是什么呢?a)如果所有的属性都已用来划分,那么说明决策树建立已经完成,则结束;b)如果待划分空间内所有样本都属于同一分类,那么说明在该空间内已不需要再继续划分,则结束。

     2)然后,如果递归没有结束,则计算各属性划分的信息增益,并选择信息增益值最大的属性作为当前划分属性,并写入存储划分属性的数据结构中。

     3)最后,我们要对当前划分属性的每个值,再继续执行递归建立决策树的过程。


     这就是ID3决策树学习算法的主要流程,下面将《Machine Learning In Action》中的python实现代码学习着实现了一遍,如下所示:

<span style="font-family:SimSun;font-size:12px;">#coding=utf-8
'''
Created on Sep 13, 2014

Aim: To test decision tree algorithm 

@author: lemon
'''

# 要用到log运算
from math import log
import operator
import types

'''
   函数功能:计算给定集合的熵
   输入:给定数据集
   输出:熵的值
'''
def calculateEntropy(dataSet):
    # 计算数据集共有多少元组
    numOfObj = len(dataSet)
    
    # 用labelCounts存储数据集的分类标签
    labelCounts = {}
    
    # 遍历数据集
    for obj in dataSet:
        # 取分类标签
        currentLabel = obj[-1]
        # 如果标签不在labelCounts中,则在labelCounts中插入
        # 该标签键,并设值为0
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        # 该标签值 +1
        labelCounts[currentLabel] += 1
    
    entropy = 0.0
    # 遍历labelCounts,根据熵的公式计算值
    for label in labelCounts:
        # 该标签在总数据集中占的概率
        prob = float(labelCounts[label])/numOfObj
        entropy -= prob * log(prob,2)
    return entropy

'''
    函数功能:根据划分属性轴和某一属性值,得到划分后的数据子集
    输入:给定数据集dataSet,划分属性轴axis,给定属性值value
    输出:划分后的数据子集
'''
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    # 遍历dataSet中的所有对象,如果其在axis轴上值为value
    # 则将该元组存入retDataSet[],除了该轴数据
    for obj in dataSet:
        if obj[axis] == value:
            reducedObj = obj[:axis]
            reducedObj.extend(obj[axis+1:])
            retDataSet.append(reducedObj)
    return retDataSet

'''
    函数功能:选择划分效果最好的属性
    输入:给定数据集
    输出:划分属性轴的下标
'''
def bestFeatureToSplit(dataSet):
    # 初始化信息增益和划分属性的轴下标
    maxInfoGain = 0.0
    splitAxis = -1
    
    # 计算给定数据集的熵
    wholeEntropy = calculateEntropy(dataSet)
    
    # 计算共有多少属性,-1的含义是去掉分类标签那一列
    featureNum = len(dataSet[0]) - 1
    
    # 计算共有多少元组
    trainingNum = len(dataSet)
    
    # 对每一个属性,对其进行划分,并计算信息增益,如果该信息增益比
    # 当前记录的信息增益值大,则更新;否则,继续下一个属性
    for i in range(featureNum):
        # 获得属性轴为i的所有属性值,并存储在featureList中
        '''
            注意这里书上用两行代码实现,更简单
            featureList = [example[j] for example in dataSet]
            uniqueVals = set(featureList)   # set is unique
        '''
        featureList = []
        for j in range(trainingNum):
            if dataSet[j][i] not in featureList:
                featureList.append(dataSet[j][i])
            else:
                continue
            
        # 现在已获得了第i+1列属性的所有值,计算共有多少不同值
        iFeatureNum = len(featureList)
        
        # 计算划分后所有数据子集的熵和
        splitedEntropy = 0.0
        for k in range(iFeatureNum):
            # 获得划分子集
            retDataSet = splitDataSet(dataSet, i, featureList[k])
            # 得到子集元组数量
            retDataSetNum = len(retDataSet)
            # 得到子集熵
            retDataSetEntropy = calculateEntropy(retDataSet)
            # 累加
            splitedEntropy += (float(retDataSetNum)/float(trainingNum))*retDataSetEntropy
        
        # 判断当前划分的信息增益是否大于maxInfoGain
        if (wholeEntropy - splitedEntropy) > maxInfoGain:
            maxInfoGain = wholeEntropy - splitedEntropy
            splitAxis = i
        else:
            continue
    
    return splitAxis
            
'''
    函数功能:当划分结束时,如果某一子集中的所有分类标签还不相同,那么
            用这个函数,来选择该空间中某一标签值最多的作为标签,与kNN
            算法中的相同
    输入:标签list
    输出:标签值最多的标签
'''
def majorityCount(classList):
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    # sort
    sortedClassCount = sorted(classCount.iteritems(),
                              key=operator.itemgetter(1),reverse=True)
    
    return sortedClassCount[0][0]

'''
    函数功能:递归建立决策树
    输入:给定数据集dataSet,划分属性名称labels
    输出:决策树(dictionary格式)
'''
def createTree(dataSet,labels):
    # 获得该dataSet的所有标签
    classList = [example[-1] for example in dataSet]
    
    # 结束条件1:所有标签相同,则返回该标签
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 结束条件2:所有属性已经划分,返回最多数量的标签
    if len(dataSet[0]) == 1:
        return majorityCount(classList)
    
    # 若没结束,则划分
    bestFeature = bestFeatureToSplit(dataSet)
    bestFeatureLabel = labels[bestFeature]
    
    # 用dictionary格式存储该划分
    myTree = {bestFeatureLabel:{}}  
    # 从labels中删除该属性
    del(labels[bestFeature])
            
    # 获得该划分属性的所有属性值
    featureValues = [example[bestFeature] for example in dataSet]
    uniqueVals = set(featureValues)  # 取唯一值
    
    # 对每一属性值递归求决策树
    for value in uniqueVals:
        # 用subLabels存储labels的值是因为,递归的时候会修改list的内容
        subLabels = labels[:]
        myTree[bestFeatureLabel][value] = createTree(splitDataSet\
                                                     (dataSet,bestFeature,value),subLabels)
    
    return myTree

'''
    函数功能:创建数据集和属性名称
    输出:返回数据集和属性名称list
'''
def createDataset():
    dataSet = [['Sunny','Hot','High','Weak','No'],
               ['Sunny','Hot','High','Strong','No'],
               ['Overcast','Hot','High','Weak','Yes'],
               ['Rain','Mild','High','Weak','Yes'],
               ['Rain','Cool','Normal','Weak','Yes'],
               ['Rain','Cool','Normal','Strong','No'],
               ['Overcast','Cool','Normal','Strong','Yes'],
               ['Sunny','Mild','High','Weak','No'],
               ['Sunny','Cool','Normal','Weak','Yes'],
               ['Rain','Mild','Normal','Weak','Yes'],
               ['Sunny','Mild','Normal','Strong','Yes'],
               ['Overcast','Mild','High','Strong','Yes'],
               ['Overcast','Hot','Normal','Weak','Yes'],
               ['Rain','Mild','High','Strong','No'],]
    labels = ['Outlook','Temperature','Humidity','Wind']
    return dataSet, labels

dataSet, labels = createDataset()

labelsCopy = labels[:]

decisionTree = createTree(dataSet,labels)
print (decisionTree)

'''
    函数功能:根据得到的决策树进行预测
    输入:决策树inputTree,属性名称列表featLabels,测试数据testVector
    输出:预测分类标签
'''
def classify(inputTree, featLabels, testVector):
    # 决策树是dictionary格式,所以取第一个键firstStr和值secondDict
    firstStr = tuple(inputTree.keys())[0]
    secondDict = inputTree[firstStr]
    
    # 根据键(属性)获得其轴下标
    featIndex = featLabels.index(firstStr)
    
    # 循环递归判断
    for key in secondDict.keys():
        if testVector[featIndex] == key:
            # 如果键的类型是dict,说明是dictionary,还需递归判断
            if type(secondDict[key]).__name__ == 'dict':
                classLabel = classify(secondDict[key],featLabels,testVector)
            # 否则,判断结束,直接返回
            else:
                classLabel = secondDict[key]
    
    return classLabel

classLabel = classify(decisionTree,labelsCopy,['Sunny','Mild','High','Weak'])
print(classLabel)


</span>

程序的结构很简单,创建决策树的主函数是createTree(),分类函数是classify(),注释也比较详细,较好理解,值得提的是,createTree()函数返回的结果是嵌套的dictionary格式,比如{'outLook':{‘a’:{''...}, 'b': 'yes'}...}。然后classify()就是按照这个结构进行分类。这里对其中遇到的新用法记录一下:

1)其中两次用到了featureValues = [example[bestFeature] for example in dataSet]这种用法,相对于我用的for然后判定要简洁的多,以后多用这种用法来填充list;

2)其中用到了type()这个用法,在classify()函数中,它主要是返回括号内变量的类型,然后用classes.__name__用法来获取变量的类型名称,从而进行比较;


其实在《机器学习》书中,还有一些关于ID3算法的优化和讨论,以及C4.5算法等,在上面提到的那个笔记中有简单介绍,后面有时间的话会整理一篇关于决策树的算法讨论出来。



评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值