决策树的构造
下图构造了一个假想的邮件分类系统:
前面介绍的k-近邻算法可以完成很多分类任务,但它最大的缺点是无法给出数据的内在含义,决策树的主要优势就在于数据形式非常容易理解。
决策树很多任务都是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,机器学习算法最终将使用这些机器从数据集中创造的规则。
专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。
1、决策树的构造
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配问题。
适用数据类型:数值型和标称型。
首先我们讨论数学上如何使用信息论划分数据集,然后编写代码将理论应用到具体的数据集上,最后编写代码构建决策树。
第一个问题:当前数据集上哪个特征在划分数据分类时起决定性作用。
因此,必须评估每个特征。
完成测试后,原始数据集就被划分为几个数据子集。划分数据子集和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。
创建分支的伪代码函数createBranch()如下:
#检测数据集中的每个子项是否属于同一分类
If so return 类标签;
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch并增加返回结果到分支节点中
return 分支节点
上面的伪代码createBranch是一个递归函数,在倒数第二行直接调用了它自己。
表3-1的数据包含5个海洋动物。现在我们要决定依据第一个特征还是第二个特征划分数据。
我们可以先分析一下表中的数据。如果我们按照第一个特征,可分为两组,其中一组将有2个属于鱼类,一个属于非鱼类;另一组则全部属于非鱼类。如果按照第二个特征分组,则其中一组将有两个属于鱼类,两个属于非鱼类;另一组只有一个非鱼类。
(1)信息增益
划分数据集的大原则:将无序的数据变得有序。
方法:使用信息论度量信息。划分数据集前后信息发生的变化称为信息增益,获得信息增益最高的特征就是最好的选择。
计算给定数据集的香农熵
from math import log
#计算给定数据集的香农熵
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 createDataSet():
dataSet = [[1,1,'yes'], [1,1,'yes'], [1,0,'no'], [0,1,'no'], [0,1,'no']]
# dataSet[0][-1] = 'maybe'
labels = ['no surfacing','flippers']
return dataSet, labels
def main():
data,labels = createDataSet()
print(data)
print(calcShannonEnt(data))
if __name__ == '__main__':
main()
则可见数据集和熵值如下:
>>>[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
0.9709505944546686
将 dataSet[0][-1] = 'maybe’去掉注释,即在数据集中添加更多的分类,则混合的数据越多,熵越高:
>>>[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
1.3709505944546687
(2)划分数据集
以下代码的功能是:按照给定特征划分数据集。
使用了三个输入参数:待划分的数据集、划分数据集的特征、特征的返回值。
可以这样理解这段代码:当我们按照某个特征划分数据集时,就需要将所有符合要求的元素抽取出来。
#按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):#待划分的数据集、划分数据集的特征、特征的返回值
retDataSet = [] #声明一个新列表对象
for featVec in dataSet:
if featVec[axis] == value: #将符合特征的数据抽取出来
reducedFeatVec = featVec[:axis] #从头开始取,到axis-1元素
reducedFeatVec.extend(featVec[axis+1:])#axis+1开始取直到结束
retDataSet.append(reducedFeatVec)
return retDataSet #符合特征的、去掉了axis的数据
执行结果:
print(data)
print(splitDataSet(data,1,1)) #第一个值为1的数据
>>>[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
[[1, 'yes'], [1, 'yes'], [0, 'no'], [0, 'no']]
(a) list.append(object)方法
是指在列表末尾增加一个数据项
students = ['Cleese','Palin','Jones']
students_new = ['Idle','Gavin']
students.append(students_new)
print(students)
>>>['Cleese', 'Palin', 'Jones', ['Idle', 'Gavin']]
使用append时,将students_new看作一个对象,作为一个整体添加到students中。
(b) list.extend(sequence)方法
是指在列表末尾增加一个数据集合
students = ['Cleese','Palin','Jones']
students_new = ['Idle','Gavin']
students.extend(students_new)
print(students)
>>>['Cleese', 'Palin', 'Jones', 'Idle', 'Gavin']
使用extend时,是把一个序列seq的内容添加到列表,将students_new看作一个序列,将此序列放在students序列之后。
© list.insert()方法
是指在某个特定位置前面增加一个数据项
students = ['Cleese','Palin','Jones']
students_new = ['Idle','Gavin']
students.insert(2,students_new)
print(students)
>>>['Cleese', 'Palin', ['Idle', 'Gavin'], 'Jones']
使用insert时,也是将对象作为整体插入的,插入在列表下标为2的对象之前。
以下这段代码遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。
#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #数据特征的个数,最后一列是类别标签
baseEntropy = calcShannonEnt(dataSet)#计算整个数据集的原始香农熵
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures): #遍历数据集中的所有特征
featList = [example[i] for example in dataSet] #获取dataSet的第i个所有特征
uniqueVals = set(featList)#创建set集合,元素不可重复
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value) #按给定特征划分数据集
prob = len(subDataSet)/float(len(dataSet)) #计算子集的概率
newEntropy += prob * calcShannonEnt(subDataSet) #计算信息熵
infoGain = baseEntropy - newEntropy #信息增益
if (infoGain > bestInfoGain): #寻找最大增益
bestInfoGain = infoGain
bestFeature = i
return bestFeature #返回信息增益最大时的特征索引值
运行结果:
>>>0
说明第1个特征是最好的用于划分数据集的特征。
(d)set()函数
描述:set()函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可计算交集、差集、并集等。
语法:class set([iterable])
参数说明:iterable-------可迭代对象
返回值:返回新的集合对象
实例:
以上是从数据集构造决策树算法所需要的子功能模块,其工作原理如下:
得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。
因此,我们可以采用递归的原则处理数据集。
(3)递归构建决策树
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。
如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。
如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类。代码如下:
#采用多数表决的方法决定该叶子节点的分类
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):#classList中第一个元素的个数与classList长度相同,表示类别相同,则停止划分
return classList[0]
if len(dataSet[0]) == 1:#一个数据集中只有一个类别时,停止划分
return majorityCnt(classList) #返回出现次数最多的分类名称
bestFeat = chooseBestFeatureToSplit(dataSet) #选择最好的数据集划分方式,返回特征索引值
bestFeatLabel = labels[bestFeat] #信息增益最大时的划分特征
myTree = {bestFeatLabel:{}} #字典变量myTree存储了树的划分特征
del(labels[bestFeat]) #从标签中删除已经划分好的特征
featValues = [example[bestFeat] for example in dataSet] #得到该属性的样例的所有数据值
uniqueVals = set(featValues)
for value in uniqueVals: #在每个数据集划分上递归调用函数createTree()
subLabels = labels[:] #复制了类标签存储到新变量列表subLabels中
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)#按照最佳分割点位置上的值进行分类,有几个值就又建几棵树。递归
return myTree
data,labels = createDataSet() print(createTree(data,labels))
>>>{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
变量myTree包含了很多代表树结构信息的嵌套字典。
从左边开始,第一个关键字no surfacing是第一个划分数据集的特征名称,该关键字的值是no surfacing节点的子节点。这些值可能是类标签,也可能是另一个数据字典。
如果值是类标签,则该子节点是叶子节点;如果值是另一个数据字典,则子节点是一个判断节点,这种格式结构不断重复就构成了整棵树。
可以看到,虽然我们已经从数据集中创建了树,但是字典的表示形式非常不易于理解,而且直接绘制图形也比较困难。下节学习使用Matplotlib库创建树形图!呼~