《机器学习实战》第三章 决策树 代码与注释

trees.py

# -*- coding: utf-8 -*-
"""
Created on 2019.6.21 Fri

@author: guiyuyang
"""
'''
决策树
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据
缺点:可能会产生过度匹配问题
适用数据类型:数值型和标称型
'''
'''
决策树的一般流程:
    (1) 收集数据:可以使用任何方法。
    (2) 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
    (3) 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
    (4) 训练算法:构造树的数据结构。
    (5) 测试算法:使用经验树计算错误率。
    (6) 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据
    的内在含义。
'''
'''
    (1) 本代码实现了香农熵的计算
    (2) 本代码实现了基于最佳特征值(属性值)划分数据集
    (3) 本代码实现了多数表决方法
    (4) 本代码实现了一个简单的决策树
    (5) 本代码使用了决策树进行分类
    (6) 本代码使用了pickle模块序列化对象来储存决策树
    (7) 本代码可以用于示例:使用决策树预测隐形眼镜类型
    (8) 代码思路及来源参见Perer Harrington《机器学习实战》
'''

from math import log
import operator

#3.1.1 信息增益
'''
计算香农熵
'''
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)#获得数据集的行数
    labelCounts = {}#保存每个标签出现次数的字典
    for featVec in dataSet:
        currentLabel = featVec[-1]#提取当前Label信息,Label是最后一列的数值
        if currentLabel not in labelCounts.keys():labelCounts[currentLabel] = 0#如果当前Label不存在,则扩展字典,并将当前Label加入字典
        labelCounts[currentLabel] += 1#Label计数累加
    shannonEnt = 0.0#香农熵
    for key in labelCounts:#计算香农熵
        prob = float(labelCounts[key])/numEntries#求取 选择某标签(Label)对应的概率P(x)
        shannonEnt -= prob * log(prob,2) #求取香农熵 求以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

#3.1.2 划分数据集
'''
按照给定特征划分数据集

输入:dataSet:待划分的数据集
     axis:取数据集特征点的索引值
     value:给定的特征值
返回: 符合特征要求划分后的数据集列表,即去掉了dataSet中axis索引处值为value的项 

example:if: myDat = [[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]
        input: trees.splitDataSet(myDat,0,1)
        output: [[1, 'yes'], [1, 'yes'], [0, 'no']]
'''
def splitDataSet(dataSet,axis,value):
    retDataSet = []#创建新的列表对象  原因:避免对原有列表修改的不良影响
    for featVec in dataSet:
        if featVec[axis] == value:#判断特征值
            reducedFeatVec = featVec[:axis]#划分后的 前面一部分
            reducedFeatVec.extend(featVec[axis+1:])#去掉featVec中序号为axis的特征 将结果储存在retDataSet中
            #上两步作用为 排除特征值
            retDataSet.append(reducedFeatVec)#并入返回的数据集
    return retDataSet


'''
 选择最好的数据集划分方式
 即选择最优特征
 通过循环计算香农熵来判断
 
 输入:待划分的数据集
 返回:最佳划分数据集特征的索引值
'''
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0])-1#统计特征项数量 yes no项不算
    baseEntropy = calcShannonEnt(dataSet)#计算数据集的原始香农熵
    bestInfoGain = 0.0#最佳香农熵(对应最好的数据集划分方式)
    bestFeature = -1#最佳划分方式对应的特征索引值
    for i in range(numFeature):#历遍所有特征项  range(6)   1,2,3,4,5       range(2)   1
        featList = [example[i] for example in dataSet]#创建唯一的分类标签列表  分别取dataSet所有列表的第一项、第二项、第三项...
        uniqueVals = set(featList)#集合 得到featList中唯一标签
        newEntropy = 0.0#经验条件熵
        for value in uniqueVals:#计算每种方式的香农熵
            subDataSet = splitDataSet(dataSet,i,value)#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


#3.1.3 递归构建决策树
'''
多数表决算法
如果数据集已经处理了所有属性,但类标签依然不是唯一的,此时用 多数表决法 来决定该叶子节点的分类
作用:统计classList中出现次数最多的元素

输入:待分类的列表
输出:classList中出现次数最多的元素
'''
def majorityCnt(classList):
    classCount = {}
    for vote in classList:#统计classList中每个元素出现的次数
        if vote not in classCount.keys():classCount[vote] = 0 
        classCount[vote] +=1#分类计数
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reversed=True)
    #排序,取出次数最多的分类项
    return sortedClassCount[0][0]


'''
创建决策树

输入:dataSet:数据集
     lables:标签列表
输出:myTree:树
'''
def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]#取出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]#获得最佳特征bestFeat的标签
    myTree = {bestFeatLabel:{}}#建立决策树
    del(labels[bestFeat])#删除掉已经使用的特征标签
    featValues = [example[bestFeat] for example in dataSet]#得到列表包含的所有属性值
    uniqueVals = set(featValues)#去掉重复的属性值
    for value in uniqueVals:#历遍特征,创建决策树
        subLabels = labels[:]#每次递归时 复制类标签
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
        #递归调用createTree(),将得到的返回值插入myTree中
    return myTree

#3.3.1 测试算法:使用决策树执行分类
'''
使用决策树的分类函数
输入:inputTree:输入的用于分类的决策树
     featLabels:
     testVec:测试向量
输出:classLabel:分类结果标签
'''
def classify(inputTree,featLabels,testVec):
    firstStr = list(inputTree.keys())[0]#获取第一个键值
    secondDict = inputTree[firstStr]#获取字典的第二层
    featIndex = featLabels.index(firstStr)#将标签字符串转换为索引 使用index方法查找当前列表中第一个匹配firstStr变量的元素
    for key in secondDict.keys():#历遍整棵树
        if testVec[featIndex] == key:#testVec中的值等于树节点的值
            if type(secondDict[key]).__name__=='dict':#判断子节点是否为字典类型,如果不是则是叶子节点
                classLabel = classify(secondDict[key], featLabels, testVec)#不是叶子节点,递归调用classify
            else:   classLabel = secondDict[key]#达到叶子节点,返回当前节点的标签
    return classLabel

#3.3.2 使用算法:决策树的存储
'''
使用pickle模块 存储 决策树

序列化对象:
使用python模块pickle序列化对象,序列化对象可以在磁盘上保存对象,并在需要的时候读取出来
任何对象都可以执行序列化操作,包括字典

输入:inputTree:想要存储的决策树
      filename:保存的文件名
输出:无
'''
def storeTree(inputTree,filename):
    import pickle#导入pickle库
    fw = open(filename,'w')#允许写方式打开文件
    pickle.dump(inputTree,fw)#将字典存入文件
    fw.close()#关闭文件句柄
'''
使用pickle模块 读取 决策树
输入:filename:保存的文件名
输出:pickle文件句柄
'''
def grabTree(filename):
    import pickle#导入pickle库
    fr = open(filename)
    return pickle.load(fr)

#3.4 示例:使用决策树预测隐形眼镜类型
'''
示例:使用决策树预测隐形眼镜类型
    (1) 收集数据:提供的文本文件。
    (2) 准备数据:解析文本中Tab键分隔的数据行。
    (3) 分析数据:快速检查数据,确保正确的解析数据内容,使用createPlot()函数绘制最终的树形图。
    (4) 训练算法:使用3.1节的createTree()函数。
    (5) 测试算法:编写测试函数验证决策树分类数据实例的正确性。
    (6) 使用算法:储存树的数据结构,以便下次使用时无需再次重新构造树
'''
'''
命令行输入:
>>> fr=open('lenses.txt')    #打开文本文件
>>> lenses=[inst.strip().split('\t') for inst in fr.readlines()]    #去掉文本每行中的tab字符
>>> lensesLabels=['age','prescript','astigmatic','tearRate']    #文本每列的标签 不包含最后一列 最后一列为分类结果
>>> lensesTree=trees.createTree(lenses,lensesLabels)    #创建决策树
>>> lensesTree
>>> treePlotter.createPlot(lensesTree)    #绘制决策树
'''

 

treePlotter.py

# -*- coding: utf-8 -*-
"""
Created on Tue Jun 25 10:50:16 2019

@author: Administrator
"""

'''
    (1) 本代码实现了使用文本注解绘制树节点
    (2) Matplotlib提供了一个注解工具annotations,它可以在数据图形上添加文本注解
    *(3)* 关于使用matplotlib绘图中文显示不出来的解决方法:
          在作图前添加下面两行代码:
        plt.rcParams['font.sans-serif']=['SimHei'] #解决绘图中的中文显示
        plt.rcParams['axes.unicode_minus']=False #解决绘图中的符号无法显示 
    (4) 本代码实现了对决策树 叶子节点数目 和 层数 的获取
    (5) 本代码实现了整颗决策树的绘制
    (4) 代码思路及来源参见Perer Harrington《机器学习实战》
'''

import matplotlib.pyplot as plt

#3.2.1 Matplotlib注解
#定义文本框和箭头格式
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.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',\
                            xytext=centerPt,textcoords='axes fraction',\
                            va="center",ha="center",bbox=nodeType,arrowprops=arrow_args)#绘制带箭头的注解
    #python所有变量都默认为全局有效   绘图区由全局变量createPlot.ax1定义


'''
使用文本注解绘制树节点
从 parentPt坐标 指向 centerPt坐标,带箭头的线,箭头文本为 nodeTxt
输入:nodeTxt:节点文本
     centerPt:文本坐标
     parentPt:标注的箭头坐标
     nodeType:节点样式
输出:树节点图形
'''
def createPlot(inTree):
    plt.rcParams['font.sans-serif']=['SimHei']#解决绘图中的中文显示
    plt.rcParams['axes.unicode_minus']=False#解决绘图中的符号无法显示   
    
    fig = plt.figure(1,facecolor='white')#创建新图形
    fig.clf()#清空绘图区域
    axprops = dict(xticks=[], yticks=[])
    createPlot.ax1 = plt.subplot(111,frameon=False,**axprops)
     
    plotTree.totalW = float(getNumLeafs(inTree)) #获取树的叶节点数目
    plotTree.totalD = float(getTreeDepth(inTree)) #获取树的层数
    
    plotTree.x0ff = -0.5/plotTree.totalW
    plotTree.y0ff = 1.0#设置x,y的偏移
    plotTree(inTree, (0.5, 1.0), '')
    #plotNode('决策节点', (0.5, 0.1), (0.1, 0.5), decisionNode)#绘制指向决策节点的箭头 从(0.1, 0.5)指向(0.5, 0.1)
    #plotNode('叶节点', (0.8, 0.1), (0.3, 0.8), leafNode)#绘制指向叶节点的箭头 从(0.3, 0.8)指向(0.8, 0.1)
    plt.show()
 
#3.2.2 构造注解树
'''
获取叶节点数目
输入:myTree:决策树
输出:numLeafs:决策树的叶子节点数目
'''    
def getNumLeafs(myTree):
    numLeafs = 0#初始化叶子
    firstStr = list(myTree.keys())[0]#获取节点类别标签
    secondDict = myTree[firstStr]#获取子节点 myTree字典的第二层
    for key in secondDict.keys():#对于所有的子节点
        if type(secondDict[key]).__name__=='dict':#判断子节点是否为字典类型,如果不是则是叶子节点
            numLeafs += getNumLeafs(secondDict[key])#如果子节点为字典类型,则该节点也是一个判断节点,递归调用getNumLeafs,继续寻找叶子节点
        else:   numLeafs += 1#如果子节点不为字典类型,则为叶子节点
    return numLeafs

'''
获取决策树的层数
输入:myTree:决策树
输出:maxDepth:决策树的层数
''' 
def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = list(myTree.keys())[0]#获取节点类别标签
    secondDict = myTree[firstStr]#获取子节点 myTree字典的第二层
    for key in secondDict.keys():#对于所有的子节点
        if type(secondDict[key]).__name__=='dict':#判断子节点是否为字典类型,如果不是则是叶子节点
            thisDepth = 1 + getTreeDepth(secondDict[key])#如果子节点为字典类型,则该节点也是一个判断节点,递归调用getNumLeafs,继续寻找叶子节点
        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]
    

'''
在父子节点间填充文本信息
输入:cntrPt:子节点坐标
     parentPt:父节点坐标
     txtString:文本字符串
输出:无
'''
def plotMidText(cntrPt,parentPt,txtString):
    xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0] 
    yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1]#计算父子节点连线中间坐标 计算标注位置
    createPlot.ax1.text(xMid, yMid, txtString)#填充文本

'''
绘制整颗树
*绘制图形的有效范围X,Y轴均为0.0-1.0
输入:myTree:树
     parentPt:父节点坐标
     nodeTxt:文本信息
输出:无
'''  
def plotTree(myTree,parentPt,nodeTxt):
    numLeafs = getNumLeafs(myTree)#获取决策树叶子节点的数目,决定了树的宽度
    firstStr = list(myTree.keys())[0]#获取节点类别标签
    cntrPt = (plotTree.x0ff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.y0ff)#计算中心节点坐标

    plotMidText(cntrPt, parentPt, nodeTxt)
    plotNode(firstStr, cntrPt, parentPt, decisionNode)#绘制从parentPt指向cntrPt的箭头
    
    secondDict = myTree[firstStr]#获取子节点 myTree字典的第二层
    plotTree.y0ff = plotTree.y0ff - 1.0/plotTree.totalD#减少y偏移
    for key in secondDict.keys():#对于所有的子节点
        if type(secondDict[key]).__name__=='dict':#判断子节点是否为字典类型,如果不是则是叶子节点
            plotTree(secondDict[key], cntrPt, str(key))#如果子节点为字典类型,则该节点也是一个判断节点,递归调用plotTree,继续寻找叶子节点
        else:#如果子节点不为字典类型,则为叶子节点
            plotTree.x0ff = plotTree.x0ff + 1.0/plotTree.totalW#减少x偏移
            plotNode(secondDict[key], (plotTree.x0ff, plotTree.y0ff), cntrPt, leafNode)#绘制中心节点指向子节点箭头(叶子节点)
            #plotNode(nodeTxt,centerPt,parentPt,nodeType):#绘制叶子节点
            plotMidText((plotTree.x0ff, plotTree.y0ff), cntrPt, str(key))#填充中心节点指向子节点的信息(叶子节点)
    plotTree.y0ff = plotTree.y0ff + 1.0/plotTree.totalD#减少y偏移
    

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值