机器学习实战 | 第3章 决策树

决策树的一个重要任务是:为了数据中蕴含的知识信息。
决策树可以使用不熟悉的数据集合,并从中提取除一系列规则,在这些机器根据数据集创建规则时,就是机器学习的过程。

3.1 决策树的构造

  1. 优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
    缺点:可能会产生过度匹配问题。
    适用数据类型:数值型和标称型。
  2. 在构造决策树时,我们需要解决的第一个问题是:当前数据集上哪个特征在划分数据分类时起决定性作用。
    为了找到决定性特征,划分出最好的结果,我们必须评估每个特征。
    完成测试之后,原始数据集就被划分为几个数据子集。
    这些数据子集会分布在第一个决策点的所有分支上。
    如果某分支上的数据属于同一类型,无需进一步对数据集进行分割。
    如果数据子集内的数据不属于同一类型,则需要重复划分数据子集。
    直到所有具有相同数据类型的数据均在一个数据子集内。
  3. 创建分支的伪代码函数createBranch():
    创建分支的伪代码函数
  4. 决策树的一般流程:
    (1) 收集数据:可以使用任何方法。
    (2) 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
    (3) 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
    (4) 训练算法:构造树的数据结构。
    (5) 测试算法:使用经验树计算错误率。
    (6) 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据
    的内在含义。

3.1.1 信息增益

  1. 划分数据集的大原则:将无序的数据变得更加有序。
  2. 信息增益:划分数据集之前之后信息发生的变化。获得信息增益最高的特征就是最好的选择。
  3. 香农熵(熵):集合信息的度量方式。
  4. 熵:定义为信息的期望值。
  5. 信息:符号xi的信息定义:
    信息
    其中p(xi)是选择该分类的概率。
  6. 信息熵:信息熵
    其中n是分类的数目。
  7. 基尼不纯度:度量集合无序程度的方法。简单的说就是从一个数据集中随机选取子项,度量其被错误分类到其他组里的概率。

程序清单3-1 计算给定数据集的香农熵

from math import log
from numpy import *
import operator
import matplotlib.pyplot as plt

# 为所有可能分类创建字典
def cal_entropy(data):
    entries_num = len(data)
    label_count = {}
    for vec in data:
        cur_label = vec[-1]
        label_count[cur_label] = label_count.get(cur_label,0)+1
    Entropy =0.0
    
    # 以2为底求对数
    for key in label_count:
        prob =float(label_count[key])/entries_num
        Entropy += prob*math.log(prob,2)
    return (0-Entropy)

# 定义自己的数据集
def createData():
    data = [[1,1,'yes'],[1,1,'yes'],[1,0,'no'],[0,1,'no'],[0,1,'no']]
    labels = ['no sufacing','flippers']
    return data,labels

执行:

>import trees
>myDat,labels = trees.createData()
>myDat
>trees.cal_entropy(myDat)
# 增加第三个名为maybe的分类,测试熵的变化:
>myDat[0][-1] = 'maybe'
>myDat
>trees.cal_entropy(myDat)

3.1.2 划分数据集

  1. 使用ID3算法划分数据集。
    三组参数:待划分的数据集、划分数据集的特征、需要返回的特征的值。

程序清单3-2 按照给定特征划分数据集

def Split_Data(dataset, axis, value):
    '''
    使用传入的axis以及value划分数据集
    axis代表在每个列表中的第X位,value为用来划分的特征值
    '''
    new_subset = []
    # 利用循环将不符合value的特征值划分入另一集合
    # 相当于将value单独提取出来(或作为叶节点)
    for vec in dataset:
        if vec[axis] == value:
            feature_split = vec[:axis]
            feature_split.extend(vec[axis + 1:])
            new_subset.append(feature_split)
    # extend将vec中的元素一一纳入feature_split
    # append则将feature_split作为列表结合进目标集合
            
    return new_subset

执行:

>import trees
>myDat,labels = trees.createData()
>myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>trees.split_data(myDat,0,1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>trees.split_data(myDat,0,0)
[[1, 'no'], [1, 'no']]

Python语言不用考虑内存分配问题。Python语言在函数中传递的是列表的引用,在函		数内部对列表对象的修改,将会影响该列表对象的整个生存周期。为了消除这个不良影响,我们需要在函数的开始声明一个新列表对象。

程序清单3-3 选择最好的数据集划分方式

def Split_by_entropy(dataset):
	'''
    使用熵原则进行数据集划分
    @信息增益:info_gain = old -new
    @最优特征:best_feature
    @类别集合:uniVal
    '''
    feature_num = len(dataset[0]) - 1
    ent_old = cal_entropy(dataset)
    best_gain = 0.0
    best_feature = -1
    for i in range(feature_num):
        feature_list = [x[i] for x in dataset]
        # 将dataSet中的数据先按行依次放入x中,然后取得x中的x[i]元素,放入列表feature_list中
        uniVal = set(feature_list)
        ent_new = 0.0
        # 使用set剔除重复项,保留该特征对应的不同取值
        for value in uniVal:
            sub_set = split_data(dataset, i, value)
            prob = len(sub_set) / float(len(dataset))
            # 使用熵计算函数求出划分后的熵值
            ent_new += prob * (0 - cal_entropy(sub_set))

        # 由ent_old - ent_new选出划分对应的最优特征
        Info_gain = ent_old - ent_new
        if (Info_gain > best_gain):
            best_gain = Info_gain
            best_feature = i

    return best_feature

执行:

>import trees
>myDat,labels=trees.createData()
>trees.Split_by_entropy(myDat)
1
>myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]

3.1.3 递归构建决策树

  1. 工作原理:得到原始数据集,然后基于最好的属性值划分数据集(由于特征值可能多余两个,因此可能存在大于两个分支的数据集划分)。第一次划分之后,数据被向下传递到树分支的下一个节点,在这个节点上,再次划分数据。递归进行。
    递归结束的条件:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。
    如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类。

多数表决法:

def Majority_vote(classList):
    '''
    使用多数表决法:若集合中属于第K类的节点最多,则此分支集合划分为第K'''
    classcount = {}
    for vote in classList:
        classcount[vote] = classcount.get(vote,0) + 1
    sorted_count = sorted(classcount.items(), key = operator.itemgetter(1),\
                          reverse = True)
    # 获取每一类出现的节点数(没出现默认为0)并进行排序
    # 返回最大项的KEY所对应的类别
    return sorted_count[0][0]

程序清单3-4 创建树的函数代码

def Create_Tree(dataset, labels):
    # 类别完全相同则停止继续划分
    classList = [x[-1] for x in dataset]
    if classList.count(classList[0]) == len(classList):
        return classList[0]

    # 遍历完所有特征值时返回出现次数最多的
    if len(dataset[0]) == 1:
        return Majority_vote(classList)
    best_feature = Split_by_entropy(dataset)
    best_labels = labels[best_feature]

    # 得到列表包含的所有属性值
    myTree = {best_labels: {}}
    # 此位置书上写的有误,书上为del(labels[bestFeat])
    # 相当于操作原始列表内容,导致原始列表内容发生改变
    # 按此运行程序,报错'no surfacing'is not in list
    # 以下代码已改正

    # 复制当前特征标签列表,防止改变原始列表的内容
    subLabels = labels[:]
    # 删除属性列表中当前分类数据集特征
    del (subLabels[best_feature])

    # 使用列表推导式生成该特征对应的列
    f_val = [x[best_feature] for x in dataset]
    uni_val = set(f_val)
    for value in uni_val:
        # 递归创建子树并返回
        myTree[best_labels][value] = Create_Tree(split_data(dataset \
                                                            , best_feature, value), subLabels)

    return myTree

执行:

>import trees
>myDat,labels = trees.createData()
>myTree = trees.Create_Tree(myDat,labels)
>myTree
{'flippers': {0: 'no', 1: {'no sufacing': {0: 'no', 1: 'yes'}}}}

3.2 在 Python 中使用 Matplotlib 注解绘制树形图

Matplotlib提供了一个注解工具annotations,非常有用,它可以在数据图形上添加文本注释。

程序清单3-5 使用文本注解绘制树节点

我们要知道:
1)有多少个叶节点,以便确定x轴的长度;
2)树有多少层,以便确定y轴的高度。

#定义文本框和箭头格式
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
# 创建字典。 boxstyle=”sawtooth” 表示注解框的边缘是波浪线,fc=0.8” 是颜色深度
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-") # 箭头样式

#绘制带箭头的注释
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
# centerPt:节点中心坐标  parentPt:起点坐标
     createPlot.ax1.annotate(nodeTxt, xy=parentPt,  xycoords='axes fraction',xytext=centerPt, textcoords='axes fraction',va="center",ha="center", bbox=nodeType, arrowprops=arrow_args )
# 参考annotate说明文档

程序清单3-6 获取叶节点的数目和树的层数

def Num_of_leaf(myTree): 
    '''计算此树的叶子节点数目,输入为我们前面得到的树(字典)'''
    num_leaf = 0  # 初始化
    first_node = myTree.keys() 
    first_node = list(first_node)[0]  # 获得第一个key值(根节点) 'no surfacing'
    # python 3X 中: mytree.keys() 返回 :dict_keys([’ ‘])是类似于列表但又不是列表的东东,它是个字典的key值的一个视图(view),所以改写为本句方法。
    
    second_dict = myTree[first_node]  # 获得value值 {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}
    # Python3中使用LIST转换firstnode,原书使用[0]直接索引只能用于Python2
    # 对于树,每次判断value是否为字典,若为字典则进行递归,否则累加器+1
    
    for key in second_dict.keys():  # 键值:01
        if type(second_dict[key]).__name__ =='dict':  # 判断如果里面的一个value是否还是dict
            num_leaf += Num_of_leaf(second_dict[key])  # 递归调用
        else: num_leaf += 1
    return num_leaf


def Depth_of_tree(myTree):
    '''计算此树的总深度'''
    depth = 0
    first_node = myTree.keys()
    first_node = list(first_node)[0]
    second_dict = myTree[first_node]

    for key in second_dict.keys():
        if type(second_dict[key]).__name__ == 'dict':
            pri_depth = 1 + Depth_of_tree(second_dict[key])
        else:
            pri_depth = 1
        # 对于树,每次判断value是否为字典,若为字典则进行递归,否则计数器+1
        if pri_depth > depth: depth = pri_depth
    return depth
    
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]

程序清单3-7 plotTree函数

def plotmidtext(cntrpt, parentpt, txtstring):
    '''作用是计算tree的中间位置
    cntrpt起始位置,parentpt终止位置,txtstring:文本标签信息
    (在两个节点之间的线上写上字)
    '''
    xmid = (parentpt[0] - cntrpt[0]) / 2.0 + cntrpt[0]
    # cntrPt 起点坐标 子节点坐标
    # parentPt 结束坐标 父节点坐标
    ymid = (parentpt[1] - cntrpt[1]) / 2.0 + cntrpt[1]  # 找到x和y的中间位置
    createPlot.ax1.text(xmid, ymid, txtstring)  # text() 的使用


def plottree(mytree, parentpt, nodetxt):  # 画树
    numleafs = Num_of_leaf(mytree)
    depth = Depth_of_tree(mytree)
    firststr = list(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 - 1.0 / plottree.totald
    # 每绘制一次图,将y的坐标减少1.0/plottree.totald,间接保证y坐标上深度的
    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))
    plottree.yoff = plottree.yoff + 1.0 / plottree.totald

def createPlot(intree):
    # 类似于Matlab的figure,定义一个画布(暂且这么称呼吧),背景为白色
    fig = plt.figure(1, facecolor='white')
    fig.clf()  # 把画布清空
    axprops = dict(xticks=[], yticks=[])
    # createPlot.ax1为全局变量,绘制图像的句柄,subplot为定义了一个绘图,
    # 111表示figure中的图有11列,即1个,最后的1代表第一个图
    # frameon表示是否绘制坐标轴矩形
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)

    plottree.totalw = float(Num_of_leaf(intree))
    plottree.totald = float(Depth_of_tree(intree))
    plottree.xoff = -0.6 / plottree.totalw;plottree.yoff = 1.2;
    plottree(intree, (0.5, 1.0), '')
    plt.show()

执行:

>import trees
Backend TkAgg is interactive backend. Turning interactive mode on. #???
>myTree=trees.retrieveTree(0)
>trees.createPlot(myTree)
>myTree['no surfacing'][3]='maybe'
>myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}, 3: 'maybe'}}
>trees.createPlot(myTree)

tree
为啥没有根节点呢??

3.3 测试和存储分类器

使用决策树构建分类器,以及实际应用中如何存储分类器。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值