机器学习之决策树算法
下表为是否适合打垒球的决策表,预测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。当发生与不发生各占一半时,这件事就越不好确定,所以此时熵为最大,其图像如下图所示。
计算熵的代码如下
1 def calcShannonEnt(dataSet):#计算香农熵 2 numEntries = len(dataSet) 3 4 labelCounts = {} 5 for featVec in dataSet: 6 currentLabel = featVec[-1] #取得最后一列数据,计算该属性取值情况有多少个 7 if currentLabel not in labelCounts.keys(): 8 labelCounts[currentLabel] = 0 9 labelCounts[currentLabel]+=1 10 11 #计算熵 12 shannonEnt = 0.0 13 for key in labelCounts: 14 prob = float(labelCounts[key])/numEntries 15 shannonEnt -= prob*log(prob,2) 16 17 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
对于信息增益的理解
信息增益就是两个熵的差,当差值越大说明按照此划分对于事件的混乱程度减少越有帮助。
计算各个属性的信息增益,并选择信息增益最大的属性的代码如下
1 #定义按照某个特征进行划分的函数splitDataSet 2 #输入三个变量(待划分的数据集,特征,分类值) 3 #axis特征值中0代表no surfacing,1代表flippers 4 #value分类值中0代表否,1代表是 5 def splitDataSet(dataSet,axis,value): 6 retDataSet = [] 7 for featVec in dataSet:#取大列表中的每个小列表 8 if featVec[axis]==value: 9 reduceFeatVec=featVec[:axis] 10 reduceFeatVec.extend(featVec[axis+1:]) 11 retDataSet.append(reduceFeatVec) 12 13 return retDataSet #返回不含划分特征的子集 14 15 def chooseBestFeatureToSplit(dataSet): 16 numFeature = len(dataSet[0]) - 1 17 baseEntropy = calcShannonEnt(dataSet) 18 bestInforGain = 0 19 bestFeature = -1 20 21 for i in range(numFeature): 22 featList = [number[i] for number in dataSet]#得到某个特征下所有值(某列) 23 uniquelVals = set(featList) #set无重复的属性特征值,得到所有无重复的属性取值 24 25 #计算每个属性i的概论熵 26 newEntropy = 0 27 for value in uniquelVals: 28 subDataSet = splitDataSet(dataSet,i,value)#得到i属性下取i属性为value时的集合 29 prob = len(subDataSet)/float(len(dataSet))#每个属性取值为value时所占比重 30 newEntropy+= prob*calcShannonEnt(subDataSet) 31 inforGain = baseEntropy - newEntropy #当前属性i的信息增益 32 33 if inforGain>bestInforGain: 34 bestInforGain = inforGain 35 bestFeature = i 36 37 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
同理还需继续计算在天气为晴的前提下,按照温度,风速属性的不同取值划分的信息增益,找到信息增益最大的作为决策树的下一个节点。
递归构造决策树的代码如下
1 #递归创建树,用于找出出现次数最多的分类名称 2 def majorityCnt(classList): 3 classCount={} 4 for vote in classList:#统计当前划分下每中情况的个数 5 if vote not in classCount.keys(): 6 classCount[vote]=0 7 classCount[vote]+=1 8 sortedClassCount=sorted(classCount.items,key=operator.itemgetter(1),reversed=True)#reversed=True表示由大到小排序 9 #对字典里的元素按照value值由大到小排序 10 print("****************") 11 print(sortedClassCount[0][0]) 12 return sortedClassCount[0][0] 13 14 15 def createTree(dataSet,labels): 16 classList=[example[-1] for example in dataSet]#创建数组存放所有标签值,取dataSet里最后一列(结果) 17 #类别相同,停止划分 18 if classList.count(classList[-1])==len(classList):#判断classList里是否全是一类,count() 方法用于统计某个元素在列表中出现的次数 19 return classList[-1] #当全是一类时停止分割 20 #长度为1,返回出现次数最多的类别 21 if len(classList[0])==1: #当没有更多特征时停止分割,即分到最后一个特征也没有把数据完全分开,就返回多数的那个结果 22 return majorityCnt(classList) 23 #按照信息增益最高选取分类特征属性 24 bestFeat=chooseBestFeatureToSplit(dataSet)#返回分类的特征序号,按照最大熵原则进行分类 25 bestFeatLable=labels[bestFeat] #该特征的label, #存储分类特征的标签 26 27 myTree={bestFeatLable:{}} #构建树的字典 28 del(labels[bestFeat]) #从labels的list中删除该label 29 30 featValues=[example[bestFeat] for example in dataSet] 31 uniqueVals=set(featValues) 32 for value in uniqueVals: 33 subLables=labels[:] #子集合 ,将labels赋给sublabels,此时的labels已经删掉了用于分类的特征的标签 34 #构建数据的子集合,并进行递归 35 myTree[bestFeatLable][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLables) 36 return myTree
最后得到的决策树如下图所示
整个程序如下
1 from math import log 2 from operator import * 3 4 def storeTree(inputTree,filename): 5 import pickle 6 fw=open(filename,'wb') #pickle默认方式是二进制,需要制定'wb' 7 pickle.dump(inputTree,fw) 8 fw.close() 9 10 def grabTree(filename): 11 import pickle 12 fr=open(filename,'rb')#需要制定'rb',以byte形式读取 13 return pickle.load(fr) 14 15 16 def createDataSet(): 17 ''' 18 dataSet=[[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']] 19 labels = ['no surfacing','flippers'] 20 ''' 21 dataSet = [['sunny','hot','high','weak','no'], 22 ['sunny','hot','high','strong','no'], 23 ['overcast','hot','high','weak','yes'], 24 ['rain','mild','high','weak','yes'], 25 ['rain','cool','normal','weak','yes'], 26 ['rain','cool','normal','strong','no'], 27 ['overcast','cool','normal','strong','yes'], 28 ['sunny','mild','high','weak','no'], 29 ['sunny','cool','normal','weak','yes'], 30 ['rain','mild','normal','weak','yes'], 31 ['sunny','mild','normal','strong','yes'], 32 ['overcast','mild','high','strong','yes'], 33 ['overcast','hot','normal','weak','yes'], 34 ['rain','mild','high','strong','no']] 35 labels = ['outlook','temperature','humidity','wind'] 36 return dataSet,labels 37 38 def calcShannonEnt(dataSet):#计算香农熵 39 numEntries = len(dataSet) 40 41 labelCounts = {} 42 for featVec in dataSet: 43 currentLabel = featVec[-1] #取得最后一列数据,该属性取值情况有多少个 44 if currentLabel not in labelCounts.keys(): 45 labelCounts[currentLabel] = 0 46 labelCounts[currentLabel]+=1 47 48 #计算熵 49 shannonEnt = 0.0 50 for key in labelCounts: 51 prob = float(labelCounts[key])/numEntries 52 shannonEnt -= prob*log(prob,2) 53 54 return shannonEnt 55 56 #定义按照某个特征进行划分的函数splitDataSet 57 #输入三个变量(待划分的数据集,特征,分类值) 58 #axis特征值中0代表no surfacing,1代表flippers 59 #value分类值中0代表否,1代表是 60 def splitDataSet(dataSet,axis,value): 61 retDataSet = [] 62 for featVec in dataSet:#取大列表中的每个小列表 63 if featVec[axis]==value: 64 reduceFeatVec=featVec[:axis] 65 reduceFeatVec.extend(featVec[axis+1:]) 66 retDataSet.append(reduceFeatVec) 67 68 return retDataSet #返回不含划分特征的子集 69 70 def chooseBestFeatureToSplit(dataSet): 71 numFeature = len(dataSet[0]) - 1 72 baseEntropy = calcShannonEnt(dataSet) 73 bestInforGain = 0 74 bestFeature = -1 75 76 for i in range(numFeature): 77 featList = [number[i] for number in dataSet]#得到某个特征下所有值(某列) 78 uniquelVals = set(featList) #set无重复的属性特征值,得到所有无重复的属性取值 79 80 #计算每个属性i的概论熵 81 newEntropy = 0 82 for value in uniquelVals: 83 subDataSet = splitDataSet(dataSet,i,value)#得到i属性下取i属性为value时的集合 84 prob = len(subDataSet)/float(len(dataSet))#每个属性取值为value时所占比重 85 newEntropy+= prob*calcShannonEnt(subDataSet) 86 inforGain = baseEntropy - newEntropy #当前属性i的信息增益 87 88 if inforGain>bestInforGain: 89 bestInforGain = inforGain 90 bestFeature = i 91 92 return bestFeature#返回最大信息增益属性下标 93 94 #递归创建树,用于找出出现次数最多的分类名称 95 def majorityCnt(classList): 96 classCount={} 97 for vote in classList:#统计当前划分下每中情况的个数 98 if vote not in classCount.keys(): 99 classCount[vote]=0 100 classCount[vote]+=1 101 sortedClassCount=sorted(classCount.items,key=operator.itemgetter(1),reversed=True)#reversed=True表示由大到小排序 102 #对字典里的元素按照value值由大到小排序 103 104 return sortedClassCount[0][0] 105 106 107 def createTree(dataSet,labels): 108 classList=[example[-1] for example in dataSet]#创建数组存放所有标签值,取dataSet里最后一列(结果) 109 #类别相同,停止划分 110 if classList.count(classList[-1])==len(classList):#判断classList里是否全是一类,count() 方法用于统计某个元素在列表中出现的次数 111 return classList[-1] #当全是一类时停止分割 112 #长度为1,返回出现次数最多的类别 113 if len(classList[0])==1: #当没有更多特征时停止分割,即分到最后一个特征也没有把数据完全分开,就返回多数的那个结果 114 return majorityCnt(classList) 115 #按照信息增益最高选取分类特征属性 116 bestFeat=chooseBestFeatureToSplit(dataSet)#返回分类的特征序号,按照最大熵原则进行分类 117 bestFeatLable=labels[bestFeat] #该特征的label, #存储分类特征的标签 118 119 myTree={bestFeatLable:{}} #构建树的字典 120 del(labels[bestFeat]) #从labels的list中删除该label 121 122 featValues=[example[bestFeat] for example in dataSet] 123 uniqueVals=set(featValues) 124 for value in uniqueVals: 125 subLables=labels[:] #子集合 ,将labels赋给sublabels,此时的labels已经删掉了用于分类的特征的标签 126 #构建数据的子集合,并进行递归 127 myTree[bestFeatLable][value]=createTree(splitDataSet(dataSet,bestFeat,value),subLables) 128 return myTree 129 130 131 if __name__=="__main__": 132 my_Data,labels = createDataSet() 133 134 #print(calcShannonEnt(my_Data)) 135 Mytree = createTree(my_Data,labels) 136 print(Mytree)