关闭

[完]机器学习实战 第三章 决策树(Decision Tree)

标签: 决策树机器学习Python
398人阅读 评论(0) 收藏 举报
分类:

【参考书籍】机器学习实战(Machine Learning in Action)

本章内容

  决策树分类器就像带有终止块(表示分类结果)的流程图。处理数据集的流程,1、测量集合中数据的不一致性,也就是熵;2、寻找最优化方案划分数据集,直到数据集中的所有数据属于同一分类。ID3算法可以用于划分标称型数据集。构建决策树时,通常采用递归方法将数据集转化为决策树。在Python中,采用内嵌的数据结构字典存储树的节点信息。

  使用Matplotlib可以将字典决策树转化为图形。使用Python的pickle模块可以序列化对象,用于存储决策树结构。其他决策树算法:C4.5,CART

  构造决策树时,要解决的第一个问题是:当前数据集上哪个特征在划分数据分类时起决定作用,这需要评估每个特征。

  一般决策树算法采用二分法划分数据,但文中采用ID3算法划分数据集。每次划分数据集只选取一个特征属性,在多个特征属性中,第一次选择哪个特征作为划分的参考属性?首先,需要采用量化的方法判断如何划分数据,信息论是量化处理信息的分支科学。

信息增益

  划分数据集的原则:将无需的数据变得更加有序。

  信息增益:在划分数据集之前信息发生的变化称为信息增益。计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征是最好的选择

  熵(香农熵):集合信息的度量方式称为熵。熵定义为信息的期望值。熵越高,则混合的数据也越多。待分类的事务可能划分在多个分类中,则符号x2i的信息定义为:

l(xi)=log2p(xi)
  其中p(xi)是选择该分类的概率。 就是所有类别所有可能值包含的信息期望值,公式:
H=i=1np(xi)log2p(xi)
  基尼不纯度:从一个数据集中随机选取子项,度量其被错误分类到其他分组里的概率。这是另一个度量集合无序程度的方法。

递归创建决策树

  由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。

  递归创建决策树的结束条件是:遍历完所划分的数据集的属性,或者每个分支下的所有实例都具有相同的分类。可以使用Python语言中的字典类型存储树的信息。

myTree = {bestFeatLabel : {}}       # Python定义

>>> myTree                          # 命令行中输出

遇到的问题:使用Matplotlib输出中文的问题。

使用的函数

函数 功能
arr[-1] 数组的最后一个元素
len(arr) 数组的长度
dict.keys() 获取字典的key列表
log(prob, n) n为底的prob的对数
featVec[:axis] 获取featVec从0到axis为索引的元素
arr1.extend(arr2) 将arr2中元素逐个添加到arr1中
arr1.append(arr2) 将arr2作为一个元素添加到arr1中
set(list) 对list求集合操作,集合中的所有元素不相同,且包含list中的所有元素
列表推导式 list = [item[i] for item in dataSet],将数据集合dataSet的第i列赋给集合list
sorted(,,) 排序函数
{label : {}} 嵌套字典类型,可用于存储树的信息
del(list[i]) 删除list中的第i个元素
dict(boxstyle="sawtooth", fc=0.8) 定义文本框,boxstyle="round4"另一种边框线条
dict(arrowstyle="<-" 定义箭头格式
plt.annotate() 绘制注解,可用于绘制点箭头的注释,详见代码
plt.text(x,y,txt) 在位置(x,y)处填充文本
type(var).\__name__=='vartype' 判断变量var的类型是否是vartype类型
d=dic(key1='value1', key2='value2') d={'key1':'value1', 'key2':'value2'}功能一样,在代码中不能把key1当成变量使用
plt.subplot() plt.subplot(111, frameon=False, **axprops),frameon控制是否显示边框,**axprops控制边框上显示的数字刻度
fw=open(filename, ‘w’) 以写的方式打开文件
fr=open(filename) 以读的方式打开文件
pickle.dump(obj, fw) 将对象obj序列化进fw文件中
pickle.load(fr) 将文件fr中的序列化对象读取出来

问题:有些字符串不能作为python文件名(如test.py)

程序代码

# coding=utf-8
# The File Name is 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意思:不浮出水面, 脚蹼
    labels = ['no surfacing', 'flippers']
    return dataSet, labels

# 按照给定特征划分数据集
# dataSet: 待划分的数据集
# axis: 划分数据集的特征
# value: 需要返回的特征的值
def splitDataSet(dataSet, axis, value) :
    retDataSet = []
    for featVec in dataSet : 
        # 如果数据条目的特征的值与value相等
        if featVec[axis] == value :
            # 后边两条语句将向量featVec的axis维去掉后,剩余部分存储在reducedFeatVec
            reducedFeatVec = featVec[ : axis]               # featVec[ : axis],返回数组featVec的第0-axis元素
            reducedFeatVec.extend(featVec[axis+1 : ])       # featVec[axis+1 : ],返回数组featVec的第axis-末尾元素
            retDataSet.append(reducedFeatVec)
    return retDataSet

# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet) :
    numFeatures = len(dataSet[0]) - 1
    baseEntropy = calcShannonEnt(dataSet)
    # info gain: 信息增益
    bestInfoGain = 0.0; bestFeature = -1
    for i in range(numFeatures) : 
        # 将dataSet中的第i列元素存储在featList中
        featList = [example[i] for example in dataSet]
        # 特征值featList列表中各不相同的元素组成一个集合
        uniqueVals = set(featList)
        newEntropy = 0.0
        # 对第i特征划分一次数据集,计算数据的新熵值,并对所有唯一特征值得到的熵求和
        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

# 创建树的函数代码
# dataSet: 数据集
# labels: 标签
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]
    # 字典数据存储树的信息
    myTree = {bestFeatLabel : {}}
    del(labels[bestFeat])
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals :
        # 复制类标签,并将其存储在新列表变量subLabels中
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree


# 使用文本注释绘制树节点
import matplotlib.pyplot as plt

# 设置中文字体,不设置无法显示中文
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)  

# 定义文本框和箭头样式
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")  

# 绘制带箭头注解,\后边不能有空格
def plotNode(nodeTxt, centerPt, parentPt, nodeType) :   
    createPlot.axl.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', xytext=centerPt, \
        textcoords='axes fraction', va='center', ha='center', bbox=nodeType, \
        arrowprops=arrow_args, fontproperties=font)     # fontproperties=font,设置显示中文字体

# 后边有同样名字的函数更新此函数       
def createPlot() :  
    fig = plt.figure(1, facecolor='white')
    fig.clf()   
    createPlot.axl = plt.subplot(111, frameon=False)
    plotNode(u'决策节点', (0.5, 0.1), (0.1, 0.5), decisionNode);
    plotNode(u'叶节点', (0.8, 0.1), (0.3, 0.8), decisionNode);
    plt.show();


# 获取叶节点的数目
def getNumLeafs(myTree) :   
    numLeafs = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys() :
        # 判断节点的数据类型是否为字典
        if type(secondDict[key]).__name__=='dict' :
            numLeafs += getNumLeafs(secondDict[key])
        else : 
            numLeafs += 1
    return numLeafs

# 获取树的层数            
def getTreeDepth(myTree) :
    maxDepth = 0
    firstStr = myTree.keys()[0]
    secondDict = myTree[firstStr]
    for key in secondDict.keys() :
        if type(secondDict[key]).__name__=='dict' :
            thisDepth = 1 + getTreeDepth(secondDict[key])
        else : 
            thisDepth = 1
        if thisDepth > maxDepth :
            maxDepth = thisDepth
    return maxDepth

# 输出预先存储树的信息,避免每次测试代码都要从数据创建树的麻烦
def retrieveTree(i) :
    listOfTrees = [{'no surfacing' : {0 : 'no', 1 : {'flippers' : \
        {0 : 'no' , 1 : 'yes'}}}},
        {'no surfacing' : {0 : 'no', 1 : {'flippers' : \
        {0 : {'head' : {0 : 'no', 1 : 'yes'}} , 1 : 'no'}}}}]
    return listOfTrees[i]

# 创建最终决策树的代码段
# 在父子节点间填充文本信息(子节点具有的特征值)
def plotMidText(cntrPt, parentPt, txtString) : 
    xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0]
    yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1]
    createPlot.axl.text(xMid, yMid, txtString)

def plotTree(myTree, parentPt, nodeTxt) :
    # 计算树的宽和高
    numLeafs = getNumLeafs(myTree)
    depth = getTreeDepth(myTree)
    firstStr = myTree.keys()[0]
    cntrPt = (plotTree.xOff + (1.0+float(numLeafs))/2.0/plotTree.totalW, \
        plotTree.yOff)
    # 绘制子节点具有的特征值,或者沿此分支向下的数据实例必须具有的特征值
    plotMidText(cntrPt, parentPt, nodeTxt)
    plotNode(firstStr, cntrPt, parentPt, decisionNode)
    secondDict = myTree[firstStr]
    # 按比例减少全局变量plotTree.yOff,并标注此处将要绘制子节点
    plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
    for key in secondDict.keys() :
        if type(secondDict[key]).__name__=='dict' :
            plotTree(secondDict[key], cntrPt, str(key))
        else :
            plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
            plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
            plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
    # 绘制所有子节点后,增加全局Y的偏移
    plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD

# 绘制图形是按照比例绘制树形图,好处是无需关心实际输出的图形大小
# x轴和y轴的有效范围是0.0-1.0
def createPlot(inTree) :    
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    createPlot.axl = plt.subplot(111, frameon=False, **axprops)
    # plotTree.totalW: 存储树的宽度;plotTree.totalD: 存储树的深度,
    # 使用这两个变量可以计算树的节点的摆放位置,这样可以将树绘制在水平方向和垂直方向的中心位置
    plotTree.totalW = float(getNumLeafs(inTree))
    plotTree.totalD = float(getTreeDepth(inTree))
    # plotTree.xOff和plotTree.yOff追踪已经绘制的节点位置,以及放置下一个节点的恰当位置
    plotTree.xOff = -0.5/plotTree.totalW
    plotTree.yOff = 1.0
    plotTree(inTree, (0.5,1.0), '')
    plt.show()

# 使用决策树的分类函数
def classify(inputTree, featLabels, testVec) :
    firstStr = inputTree.keys()[0]
    secondDict = inputTree[firstStr]
    # 将标签字符串转换为索引,使用index()方法查找当前列表中第一个匹配firstStr的索引
    featIndex = featLabels.index(firstStr)
    for key in secondDict.keys() :
        if testVec[featIndex] == key :
            if type(secondDict[key]).__name__=='dict' :
                classLabel = classify(secondDict[key], featLabels, testVec)
            else : 
                classLabel = secondDict[key]
    return classLabel

# 使用pickle模块存储决策树,使用pickle序列化对象,序列化对象可以在磁盘保存对象,并在需要时读取出来
def storeTree(inputTree, filename) :
    import pickle
    fw = open(filename, 'w')
    pickle.dump(inputTree, fw)
    fw.close()

# 读取序列化对象
def grabTree(filename) :
    import pickle
    fr = open(filename)
    return pickle.load(fr)

# 使用决策树预测隐形眼镜类型,这直接可以在命令行中找到这部分

在命令行中执行

>>> import trees
>>> myDat, labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>> trees.calcShannonEnt(myDat)
0.9709505944546686

>>> trees.splitDataSet(myDat, 0, 1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>>> trees.splitDataSet(myDat, 0, 0)
[[1, 'no'], [1, 'no']]

>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.chooseBestFeatureToSplit(myDat)
0

>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.pyc'>
>>> myDat, labels = trees.createDataSet()
>>> myTree = trees.createTree(myDat, labels)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> trees.createPlot()          # 生成的图,见末尾图1

>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> trees.retrieveTree(0)
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> trees.retrieveTree(1)
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1:
 'no'}}}}
>>> myTree = trees.retrieveTree(0)
>>> trees.getNumLeafs(myTree)
3
>>> trees.getTreeDepth(myTree)
2
>>> myTree = trees.retrieveTree(1)
>>> trees.getTreeDepth(myTree)
3
>>> trees.getNumLeafs(myTree)
4

>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> 
trees.createPlot(myTree)        # 生成的图见末尾图2

>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> myDat, labels=trees.createDataSet()
>>> trees.createPlot(myTree)
>>> myTree=trees.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'

>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> trees.storeTree(myTree, 'c:\python27\ml\classifierStorage.txt')
>>> trees.grabTree('c:\python27\ml\classifierStorage.txt')
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

>>> fr=open('c:\python27\ml\ch03\lenses.txt')       # 使用决策树预测隐形眼镜类型
>>> lenses=[inst.strip().split('\t') for inst in fr.readlines()]
>>> lenseslabels=['age','prescript','astigmatic','tearRate']
>>> lensesTree=trees.createTree(lenses, lenseslabels)
>>> lensesTree
{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'yes': {'prescri
pt': {'hyper': {'age': {'pre': 'no lenses', 'presbyopic': 'no lenses', 'young':
'hard'}}, 'myope': 'hard'}}, 'no': {'age': {'pre': 'soft', 'presbyopic': {'presc
ript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'young': 'soft'}}}}}}
>>> 
trees.createPlot(lensesTree)    # 生成的图见末尾图3


绘制的决策树决策节点和叶节点
图1 绘制的决策树决策节点和叶节点


超过两个分支的树形图
图2 超过两个分支的树形图


使用决策树预测隐形眼镜类型
图3 使用决策树预测隐形眼镜类型

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:37711次
    • 积分:837
    • 等级:
    • 排名:千里之外
    • 原创:47篇
    • 转载:3篇
    • 译文:0篇
    • 评论:7条
    文章分类
    最新评论