机器学习之决策树算法

前言:决策树模型是一类算法的集合,在数据挖掘十大算法中,具体的决策树算法占有两席位置,即C4.5和CART算法

下表为是否适合打垒球的决策表,预测E= {天气=晴,温度=适中,湿度=正常,风速=弱} 的场合,是否合适中打垒球。


如何发现这些数据之中所掩藏的规律,从而较好的预测在给定条件下,所可能的结果。决策树是一种以示例为基础的归纳学习方法,能够较好的解决这类问题。

  • 一个简单的例子
    请给出布尔函数(A * -B)+ C(+:或,*:与,-非)的最小体积(或结点)决策树。
    当C为1时,AB不管取何值整个表达式都为真,此时这个表达式就可以确定真假,所以选择C作为头结点。若C为0,表达式无法确定真假,还需进一步看AB的取值,A与非B是与的关系,两者具有相同的地位,所以接下来无论取A还是B都可以,整个决策树构造结果如下图所示。



    类似于这个简单例子对于打垒球这些数据,我们可以将天气,温度,湿度,风速(可以成为属性或特征)类比成布尔函数的ABC,而它们的取值,如天气的取值可以是晴,雨,阴类比成ABC布尔取值真假,那么活动的取消或进行,就可以类比成整个布尔表达式的真或假。要构造一颗最小体积决策树,就要每次在各个属性中找到区分度最大的属性来作为当前决策树的节点。

  • 相关名词

通常熵表示事物的混乱程度,熵越大表示混乱程度越大,越小表示混乱程度越小。对于随机事件S,如果我们知道它有N种取值情况,每种情况发生的概论为,那么这件事的熵就定义为:

例如对于打垒球的例子,要求活动的熵H(活动)。在活动一栏属性中发现活动的取值有两种:取消(5个)和进行(9个),它们所占的比例分别为5/14,9/14。那么H(活动)的取值为:

,算出的结果约为0.94。

对于熵的理解

如果一件事发生的可能是1,不发生的肯为0那么这件事的熵为=0,这就表明这件事肯定发生,没有不发生的情况,那么它的混乱程度是最小的0。同理当不发生的可能是1,混乱程度也是0。当发生与不发生各占一半时,这件事就越不好确定,所以此时熵为最大,其图像如下图所示。


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

信息增益

随机事件未按照某个属划的不同取值划分时的熵减去按照某个属性的不同取值划分时的平均熵。即前后两次熵的差值。

还是对于打垒球的例子,未按照某个属划的不同取值划分时的熵即H(活动)已算出未0.94。现在按照天气属性的不同取值来划分,发现天气属性有3个不同取值分别为晴,阴,雨。划分好后如下图所示。
在天气为晴时有5种情况,发现活动取消有3种,进行有2种,计算现在的条件熵


=0.971

同理天气为阴时有4种情况,活动进行的有4种,则条件熵为:

=0

同理天气为雨时有5种情况,活动取消的有2种,进行的有3种,则条件熵为:

=0.971

由于按照天气属性不同取值划分时,天气为晴占整个情况的5/14,天气为阴占整个情况的4/14,天气为雨占整个情况的5/14,则按照天气属性不同取值划分时的带权平均值熵为:

算出的结果约为0.693.

则此时的信息增益Gain(活动,天气)=* H*(活动) - H(活动|天气) = 0.94- 0.693 = 0.246

同理我们可以计算出按照温度属性不同取值划分后的信息增益:

Gain(活动,温度)= H(活动) - H(活动|温度) = 0.94- 0.911 = 0.029

按照湿度属性不同取值划分后的信息增益:

Gain(活动,湿度)= H(活动) - H(活动|湿度) = 0.94- 0.789 = 0.151

按照风速属性不同取值划分后的信息增益:

Gain(活动,风速)= H(活动) - H(活动|风速) = 0.94- 0.892 = 0.048

对于信息增益的理解

信息增益就是两个熵的差,当差值越大说明按照此划分对于事件的混乱程度减少越有帮助。

计算各个属性的信息增益,并选择信息增益最大的属性的代码如下

#定义按照某个特征进行划分的函数splitDataSet
#输入三个变量(待划分的数据集,特征,分类值)
#axis特征值中0代表no surfacing,1代表flippers
#value分类值中0代表否,1代表是
def splitDataSet(dataSet,axis,value):
    retDataSet = []
    for featVec in dataSet:#取大列表中的每个小列表
        if featVec[axis]==value:
            reduceFeatVec=featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)
    
    return retDataSet #返回不含划分特征的子集
    
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0]) - 1
    baseEntropy = calcShannonEnt(dataSet)
    bestInforGain = 0
    bestFeature = -1
    
    for i in range(numFeature):
        featList = [number[i] for number in dataSet]#得到某个特征下所有值(某列)
        uniquelVals = set(featList) #set无重复的属性特征值,得到所有无重复的属性取值
        
        #计算每个属性i的概论熵
        newEntropy = 0
        for value in uniquelVals:
            subDataSet = splitDataSet(dataSet,i,value)#得到i属性下取i属性为value时的集合
            prob = len(subDataSet)/float(len(dataSet))#每个属性取值为value时所占比重
            newEntropy+= prob*calcShannonEnt(subDataSet)
        inforGain = baseEntropy - newEntropy #当前属性i的信息增益
        
        if inforGain>bestInforGain:
            bestInforGain = inforGain
            bestFeature = i
   
return bestFeature#返回最大信息增益属性下标
构造决策树

决策树的构造就是要选择当前信息增益最大的属性来作为当前决策树的节点。因此我们选择天气属性来做为决策树根节点,这时天气属性有3取值可能:晴,阴,雨,我们发现当天气为阴时,活动全为进行因此这件事情就可以确定了,而天气为晴或雨时,活动中有进行的也有取消的,事件还无法确定,这时就需要在当前按照天气属性划分下的剩下的属性中递归再次计算活动熵和信息增益,选择信息增益最大的属性来作为下一个节点,直到整个事件能够确定下来。

例如当天气为晴时,得到如下表所示的事件



我们需要递归处理,继续在温度,湿度,风速这三个属性中找到信息增益最大的属性来做为下一个节点。

首先继续计算活动熵,此时有5个样例,活动取消有3个,进行有2个,则活动熵为:

=0.971

接着计算信息增益,在天气为晴的前提下,按照温度属性的不同取值分类后结果如下所示



发现湿度为高时有3种情况,活动取消有3种,进行有0种,则条件熵为:

=0

湿度正常有2种情况,活动取消0种,进行2中,则条件熵为:

=0

由于按照湿度属性不同取值划分时,湿度为高占总情况的3/5,湿度正常占总情况的2/5,则按照湿度属性不同取值划分时的带权平均值熵为:

,算出的结果约为0。

所以此时在天气为晴的前提下,按照湿度属性的不同取值划分的信息增益为:

Gain=* H*(活动|天气=晴) - H(活动|天气,湿度) = 0.971- 0=0.971

同理还需继续计算在天气为晴的前提下,按照温度,风速属性的不同取值划分的信息增益,找到信息增益最大的作为决策树的下一个节点。

递归构造决策树的代码如下

#递归创建树,用于找出出现次数最多的分类名称
def majorityCnt(classList):
    classCount={}
    for vote in classList:#统计当前划分下每中情况的个数
        if vote not in classCount.keys():
            classCount[vote]=0
        classCount[vote]+=1
    sortedClassCount=sorted(classCount.items,key=operator.itemgetter(1),reversed=True)#reversed=True表示由大到小排序
    #对字典里的元素按照value值由大到小排序
    print("****************")
    print(sortedClassCount[0][0])
    return sortedClassCount[0][0]


def createTree(dataSet,labels):
    classList=[example[-1] for example in dataSet]#创建数组存放所有标签值,取dataSet里最后一列(结果)
    #类别相同,停止划分
    if classList.count(classList[-1])==len(classList):#判断classList里是否全是一类,count() 方法用于统计某个元素在列表中出现的次数
        return classList[-1] #当全是一类时停止分割
    #长度为1,返回出现次数最多的类别
    if len(classList[0])==1: #当没有更多特征时停止分割,即分到最后一个特征也没有把数据完全分开,就返回多数的那个结果
        return majorityCnt(classList)
    #按照信息增益最高选取分类特征属性
    bestFeat=chooseBestFeatureToSplit(dataSet)#返回分类的特征序号,按照最大熵原则进行分类
    bestFeatLable=labels[bestFeat] #该特征的label, #存储分类特征的标签
    
    myTree={bestFeatLable:{}} #构建树的字典
    del(labels[bestFeat]) #从labels的list中删除该label
    
    featValues=[example[bestFeat] for example in dataSet]
    uniqueVals=set(featValues)
    for value in uniqueVals:
        subLables=labels[:] #子集合 ,将labels赋给sublabels,此时的labels已经删掉了用于分类的特征的标签
        #构建数据的子集合,并进行递归
        myTree[bestFeatLable][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLables)
return myTree

最后得到的决策树如下图所示


from math import log
from operator import *

def storeTree(inputTree,filename):
    import pickle
    fw=open(filename,'wb') #pickle默认方式是二进制,需要制定'wb'
    pickle.dump(inputTree,fw)
    fw.close()
    
def grabTree(filename):
    import pickle
    fr=open(filename,'rb')#需要制定'rb',以byte形式读取
    return pickle.load(fr)


def createDataSet():
    '''
    dataSet=[[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]
    labels = ['no surfacing','flippers']
    '''
    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

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

#定义按照某个特征进行划分的函数splitDataSet
#输入三个变量(待划分的数据集,特征,分类值)
#axis特征值中0代表no surfacing,1代表flippers
#value分类值中0代表否,1代表是
def splitDataSet(dataSet,axis,value):
    retDataSet = []
    for featVec in dataSet:#取大列表中的每个小列表
        if featVec[axis]==value:
            reduceFeatVec=featVec[:axis]
            reduceFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reduceFeatVec)
    
    return retDataSet #返回不含划分特征的子集
    
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0]) - 1
    baseEntropy = calcShannonEnt(dataSet)
    bestInforGain = 0
    bestFeature = -1
    
    for i in range(numFeature):
        featList = [number[i] for number in dataSet]#得到某个特征下所有值(某列)
        uniquelVals = set(featList) #set无重复的属性特征值,得到所有无重复的属性取值
        
        #计算每个属性i的概论熵
        newEntropy = 0
        for value in uniquelVals:
            subDataSet = splitDataSet(dataSet,i,value)#得到i属性下取i属性为value时的集合
            prob = len(subDataSet)/float(len(dataSet))#每个属性取值为value时所占比重
            newEntropy+= prob*calcShannonEnt(subDataSet)
        inforGain = baseEntropy - newEntropy #当前属性i的信息增益
        
        if inforGain>bestInforGain:
            bestInforGain = inforGain
            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.items,key=operator.itemgetter(1),reversed=True)#reversed=True表示由大到小排序
    #对字典里的元素按照value值由大到小排序
   
    return sortedClassCount[0][0]


def createTree(dataSet,labels):
    classList=[example[-1] for example in dataSet]#创建数组存放所有标签值,取dataSet里最后一列(结果)
    #类别相同,停止划分
    if classList.count(classList[-1])==len(classList):#判断classList里是否全是一类,count() 方法用于统计某个元素在列表中出现的次数
        return classList[-1] #当全是一类时停止分割
    #长度为1,返回出现次数最多的类别
    if len(classList[0])==1: #当没有更多特征时停止分割,即分到最后一个特征也没有把数据完全分开,就返回多数的那个结果
        return majorityCnt(classList)
    #按照信息增益最高选取分类特征属性
    bestFeat=chooseBestFeatureToSplit(dataSet)#返回分类的特征序号,按照最大熵原则进行分类
    bestFeatLable=labels[bestFeat] #该特征的label, #存储分类特征的标签
    
    myTree={bestFeatLable:{}} #构建树的字典
    del(labels[bestFeat]) #从labels的list中删除该label
    
    featValues=[example[bestFeat] for example in dataSet]
    uniqueVals=set(featValues)
    for value in uniqueVals:
        subLables=labels[:] #子集合 ,将labels赋给sublabels,此时的labels已经删掉了用于分类的特征的标签
        #构建数据的子集合,并进行递归
        myTree[bestFeatLable][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLables)
    return myTree


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