《机器学习实战》第三章 决策树

  • k-近邻算法可以完成很多分类任务,但是它最大的缺点就是无法给出数据的内在含义,决策树的主要优势就在于数据形式非常容易理解。
  • 决策树很多任务都是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,机器学习算法最终将使用这些机器从数据集中创造的规则。

  • 优点: 计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
  • 缺点: 可能会产生过度匹配问题。
  • 适用数据类型: 数值型和标称型

一般流程:
(1)收集数据:可以使用任何方法。
(2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3)分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
(4)训练算法:构造树的数据结构。
(5)测试算法:使用经验树计算错误率。
(6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。


构造决策树的方法

首先,评估每个特征,根据结果确定划分数据依据的数据特征。原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。

信息增益

划分数据集的大原则是:将无序的数据变得更加有序。
信息增益
符号 的信息量:

熵:

其中n是分类的数目.
在划分数据集之前之后信息发生的变化。知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益。
:定义为信息的期望值。得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集。
例子
trees.py

'''
功能:计算给定数据集的香农熵
'''

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']]
    labels = ['no surfacing','flippers']
    return dataSet, labels

main.py

import trees
myDat, labels = trees.createDataSet()
print(trees.calcShannonEnt(myDat))
'''
熵越高,则混合的数据也越多
在数据集中添加更多的分类,观察熵是如何变化的。
'''
myDat[0][-1] = 'maybe'
print(trees.calcShannonEnt(myDat))

output:

0.9709505944546686
1.3709505944546687

划分数据集

在trees.py中添加:

#按照给定特征划分数据集
def splitDataSet(dataSet, axis, value): #参数分别为:待划分的数据集,划分数据集的特征,特征返回值
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

'''
遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。
熵计算将会告诉我们如何划分数据集是最好的数据组织方式。
'''
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      #the last column is used for the labels
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain = 0.0; bestFeature = -1
    for i in range(numFeatures):        #iterate over all the features
        featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
        uniqueVals = set(featList)       #get a set of unique values
        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     #calculate the info gain; ie reduction in entropy
        if (infoGain > bestInfoGain):       #compare this to the best gain so far
            bestInfoGain = infoGain         #if better than current best, set to best
            bestFeature = i
    return bestFeature                      #returns an integer

main.py中添加

#测试splitDataSet()函数
print(trees.splitDataSet(myDat,0,1))
print(trees.splitDataSet(myDat,0,0))

#测试chooseBestFeatureToSplit()函数
print(trees.chooseBestFeatureToSplit(myDat))
print(myDat)

output

[[1, 'maybe'], [1, 'yes'], [0, 'no']]
[[1, 'no'], [1, 'no']]
0   #第0个特征是最好的用于划分数据集的特征。
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

工作原理总结:
得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节点的数据必然属于叶子节点的分类。

递归构建决策树

在trees.py中增加如下代码:

import operator
'''
该函数使用分类名称的列表,然后创建键值为classList中唯一值的数据字典。
字典对象存储了classList中每个类标签出现的频率。
最后利用operator操作键值排序字典,并返回出现次数最多的分类名称。
'''
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), 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]#stop splitting when all of the classes are equal
    if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet
        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)
    #遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
    for value in uniqueVals:
        subLabels = labels[:]       #copy all of labels, so trees don't mess up existing labels
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree

在main.py中添加:

#测试createTree()函数
myTree = trees.createTree(myDat,labels)
print(myTree)

output:

{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'maybe'}}}}

第一个关键字 no surfacing 是第一个划分数据集的特征名称,该关键字的值也是另一个数据字典。
第二个关键字是 no surfacing 特征划分的数据集,这些关键字的值是no surfacing节点的子节点。
这些值可能是类标签,也可能是另一个数据字典。
如果值是类标签,则该子节点是叶子节点;
如果值是另一个数据字典,则子节点是一个判断节点。
这种格式结构不断重复就构成了整棵树。

测试和存储分类器

测试算法:使用决策树执行分类

依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进人叶子节点;最后将测试数据定义为叶子节点所属的类型。
在trees.py中添加:

#使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
    firstStr = list(inputTree)[0]
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr) #将标签换为索引
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    if isinstance(valueOfFeat, dict): 
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    return classLabel

在main.py中添加:

#测试classify()函数
trees.classify(myTree,labels,[1,0])
trees.classify(myTree,labels,[1,1])

output:

'no'
'yes'

比较上述输出结果。第一节点名为 no surfacing ,它有两个子节点:一个是名字为0的叶子节点,类标签为no; 另一个是名为flippers的判断节点,此处进人递归调用,flippers节点有两个子节点。

决策树的存储

构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则何以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用巳经构造好的决策树。
在trees.py中添加:

def storeTree(inputTree,filename):
    import pickle
    fw = open(filename,'wb')
    pickle.dump(inputTree,fw)
    fw.close()
    
def grabTree(filename):
    import pickle
    fr = open(filename, 'rb')
    return pickle.load(fr)

在main.py中添加:

trees.storeTree(myTree,'classifierStorage.txt')
trees.grabTree('classifierStorage.txt')

通过上面的代码,我们可以将分类器存储在硬盘上,而不用每次对数据分类时重新学习一遍,这也是决策树的优点之一。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值