机器学习实战之决策树

决策树概述:
分支判断。选择可以使类型明确(即有序,熵小)的特征,优先判断。
计算每个特征按每个特征值分类后的熵,然后求和,优先判断最小的熵和所在的特征。

决策树的一个重要任务是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,在这些机器根据数据集创建规则时,就是机器学习的过程。专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。

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

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

划分数据集的大原则是:将无序的数据变得更加有序。我们可以使用多种方法划分数据集,
但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论
是量化处理信息的分支科学。我们可以在划分数据之前或之后使用信息论量化度量信息的内容。
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以
计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
在可以评测哪种数据划分方式是最好的数据划分之前,我们必须学习如何计算信息增益。集
合信息的度量方式称为香农熵或者简称为熵,这个名字来源于信息论之父克劳德·香农。

熵定义为信息的期望值。熵越高,则混合的数据也越多。

另一个度量集合无序程度的方法是基尼不纯度(Gini impurity),简单地说就是从一个数据
集中随机选取子项,度量其被错误分类到其他分组里的概率。

决策树分类器就像带有终止块的流程图,终止块表示分类结果。开始处理数据集时,我们首
先需要测量集合中数据的不一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的
所有数据属于同一分类。ID3算法可以用于划分标称型数据集。构建决策树时,我们通常采用递
归的方法将数据集转化为决策树。一般我们并不构造新的数据结构,而是使用Python语言内嵌的
数据结构字典存储树节点信息。
使用Matplotlib的注解功能,我们可以将存储的树结构转化为容易理解的图形。Python语言的
pickle模块可用于存储决策树的结构。隐形眼镜的例子表明决策树可能会产生过多的数据集划分,
从而产生过度匹配数据集的问题。我们可以通过裁剪决策树,合并相邻的无法产生大量信息增益
的叶节点,消除过度匹配问题。

k近邻和决策树都是通过样本的特征和对应分类进行训练,然后预测已知特征的测试样本的分类。k近邻是取训练样本中特征相近的前k个中频率最高的分类。决策树是在树中分支判断,叶子即分类,深度越小的节点越能使分类结果有序。k近邻适合特征范围无限,决策树适合特征范围有限。

样本数据、源码、注解、运行步骤、树形图打包下载地址:

https://download.youkuaiyun.com/download/haoranhaoshi/11064705

# coding:GBK
from math import log
import operator

'''
计算给定数据集的信息熵,用来判断数据集中的类型是否多数相同

@param dataSet:数据集(包括类别)
'''
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {} # key:类别,value:类别出现的次数
    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  # 选择该分类的概率 p(xi) = 类别次数/总次数
        shannonEnt -= prob * log(prob,2) # 所有类别的信息熵 H = -p(xi) * log2(p(xi))
    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

'''
>>> dataSet, labels = trees.createDataSet()
>>> dataSet
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.calcShannonEnt(dataSet)
0.9709505944546686

把第一行的最后一个分类类别换为'maybe',则熵会变大。熵越大,混合的数据也越多,数据越无序
类型增多,熵增
>>> dataSet[0][-1] = 'maybe'
>>> dataSet
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.calcShannonEnt(dataSet)
1.3709505944546687
'''

'''
按照给定特征划分数据集
将第axis个特征的特征值为value的划分到一起,返回这个数据集

@param dataSet:待划分的数据集(包含分类标签)
@param axis:划分数据集的特征:第axis个特征
@param value:需要返回的特征的值
'''
def splitDataSet(dataSet, axis, value):
    retDataSet = []   # 因为python传递的是列表的引用,故需要新建一个数据集,不然在函数内操作会修改dataSet
    for featVec in dataSet:
        if featVec[axis] == value:  # 如果该特征值和期望的值相同
        	# 将符合该特征的数据抽取出来,即从featerVec中去掉该特征axis,即[0,axis) [axis+1:
            reducedFeatVec = featVec[:axis]     
            reducedFeatVec.extend(featVec[axis+1:]) # extend是把列表所有元素与之前合并,而append是把列表当做一个整体与之前合并,故不能用append
            retDataSet.append(reducedFeatVec) # 把抽取掉axis特征的数据添加到结果集中
    return retDataSet

'''
>>> dataSet, labels = trees.createDataSet()
>>> dataSet
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

根据第1个特征的值为1,进行划分
>>> trees.splitDataSet(dataSet, 0, 1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]

根据第2个特征的值为0,进行划分
>>> trees.splitDataSet(dataSet, 0, 0)
[[1, 'no'], [1, 'no']]
'''

'''
选择最好的特征作为根(或非叶子结点)进行划分
@param dataSet:数据集(包含分类标签)
'''
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个特征所有取值
        # print(featList)   # [1, 1, 1, 0, 0]  [1, 1, 0, 1, 1]
        uniqueVals = set(featList)       # 得到第i个特征里,独一无二的取值集合
        newEntropy = 0.0
        for value in uniqueVals:  # 对于第i个特征的每个唯一取值,进行数据集划分
            subDataSet = splitDataSet(dataSet, i, value)  # 从dataSet中抽取出该特征
            # 求数据集划分后的新信息熵
            prob = len(subDataSet) / float(len(dataSet))
            newEntropy += prob * calcShannonEnt(subDataSet)   # 对所有唯一特征值得到的熵求和 
        infoGain = baseEntropy - newEntropy     # 得到信息增益,即熵的减小量
        if (infoGain > bestInfoGain):       # 选择信息增益最大的划分
            bestInfoGain = infoGain         
            bestFeature = i  # 第i个特征就是能使信息增益达到最大的一个划分
    return bestFeature    # 返回最好的特征

'''
第0个特征就是能使信息增益达到最大的一个划分的特征
>>> trees.chooseBestFeatureToSplit(dataSet)
0
'''

'''
统计每个类标签出现的频率

@param classList:类别列表,如 ['yes', 'no', 'yes']
'''
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]  # 出现频数最多的类的名称,如 'yes'

'''
递归创建树的代码

@param dataSet:数据集,每递归一次,数据集中的特征数少1个(含有分类标签)
@param labels:数据集中特征对应的名称(如 [x1, x2] = ['no surfacing', 'flippers']),每递归一次,特征少1个(和dataSet保持同步)
注意:dataSet删除特征是根据划分splitDataSet()函数减少1个特征个数,labels是直接用del(labels)减少1个特征个数
'''
def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]
    if classList.count(classList[0]) == len(classList): # 当所有类都相同时,停止分裂,返回该类别标签
        return classList[0]   
    if len(dataSet[0]) == 1:  # 使用完了所有特征,仍然不能将数据集划分为仅包含唯一类别的分组,此时采用投票方式返回频率最高的类别
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]  # 挑选出的最好特征对应的标签,如 0 -> No Surfacing
    myTree = {bestFeatLabel:{}}  # 嵌套字典
    del(labels[bestFeat])  # 重要,将lables中该特征删除
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]   # 拷贝标签列表到新列表,因为python为传引用,不这样做可能会修改原标签列表lables(存在del删除分类标签的操作)
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels) # 按照最好特征划分数据集,然后递归调用
    return myTree

'''
测试创建树的代码
>>> dataSet, labels = trees.createDataSet()
>>> myTree = trees.createTree(dataSet, labels)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

导入使用Matplotlib画的这棵决策树
>>> import treePlotter
>>> treePlotter.createPlot(myTree)
'''

'''
递归求得分类类别

@param inputTree:创建的一棵训练树,递归一次,是一棵以某一个特征为根结点的子树
@param featLabels:特征标签,如 ['no surfacing', 'flippers'],传递它便于帮我们确定特征在数据集中的位置
@param testVec:测试向量,如 [1, 0],代表 'no surfacing' = 'yes' 和 'flippers' = 'no'
'''
def classify(inputTree, featLabels, testVec):
    firstStr = list(inputTree.keys())[0]  # inputTree.keys() 返回一个可迭代的对象dict_keys,故要转化为list,然后取出第1个key
    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

'''
测试分类类别
>>> dataSet, labels = trees.createDataSet()
>>> labels
['no surfacing', 'flippers']
>>> import treePlotter
>>> myTree = treePlotter.retrieveTree(0)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> trees.classify(myTree, labels, [1, 0])
'no'
>>> trees.classify(myTree, labels, [1, 1])
'yes'
'''

'''
使用python的pickle模块存储决策树,写入到指定文件

@param inputTree:决策树
@param filename:写入的文件
注意:pickle存储方式默认是二进制方式
'''
def storeTree(inputTree,filename):
    import pickle   # 要先导入pickle模块
    fw = open(filename,'wb') # 以二进制写的方式创建文件
    pickle.dump(inputTree,fw)
    fw.close()

'''
使用python的pickle模块加载文件,取出决策树

@param filename:读取存储了决策树的文件
''' 
def grabTree(filename):
    import pickle
    fr = open(filename, 'rb')
    return pickle.load(fr)

'''
测试pickle读取决策树 pickle无需pip下载安装,当前已经存在
>>> trees.storeTree(myTree, 'classifierStorage.txt')
>>> trees.grabTree('classifierStorage.txt')
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
'''

'''
# 使用决策树预测隐形眼镜类型
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]  # 读一行,去掉首尾空格,再用'\t'分隔各个字段
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
lensesLabelsTem = lensesLabels[:]
lensesTree = trees.createTree(lenses, lensesLabelsTem)  # 这个lensesLabelsTem会在代码中改变,故不能传递lensesLabels,因为之后还要用
print(lensesTree) # 打印这棵树

# 导入使用Matplotlib画的这棵决策树
import treePlotter  # 先导入treePlotter.py文件
treePlotter.createPlot(lensesTree)  # 画图

# 做预测
classLabel = classify(lensesTree, lensesLabels, ['young', 'myope', 'no', 'reduced'])  
print(classLabel)  # 分类类别:no lenses
'''

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风铃峰顶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值