机器学习(二)——决策树(DecisionTree)

机器学习(二)——决策树(DecisionTree)

  • 一、算法简介
    • 1.1 概念
    • 1.2 决策树的构造
      • 1.2.1 特征选择
      • 1.2.2 决策树的生成
  • 二、 Sklearn之使用决策树预测隐形眼镜类型
    • 2.1 背景
    • 2.2 使用Sklearn构建决策树
    • 2.3 使用Graphviz可视化决策树
  • 三、 总结
    • 3.1 优点
    • 3.2 缺点

一、算法简介

1.1 概念

决策树(Dicision Tree)是一种基本的分类与回归方法。决策树模型是一种描述对实例进行分类的树形结构。决策树由结点(node)和有向边(directed edge)组成。结点由两种类型:内部结点(internal node)和叶节点(leaf node)。内部结点表示一个特征或属性,叶节点表示一个类。

1.2 决策树的构造

决策树的构建可以分三步:

  1. 特征选择
  2. 决策树的生成
  3. 决策树的修减

1.2.1 特征选择

特征选择指选取对训练数据具有分类能力的特征。这样可以提高决策树的学习效率,如果利用一个特征进行分类的结果与随机分类的结果差别不大,那么这个特征是没有分类能力的。通常特征选择的标准是信息增益(information gain)或信息增益比。
什么是信息增益?在划分数据集之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。

(1)经验熵(香农熵)
熵定义为信息的期望值。在信息论与概率统计中,熵是表示随机变量不确定性的度量。如果待分类的事物可能划分在多个分类之中,则分类xi信息定义为 :

groot
其中,P(xi)是选择该分类的概率。
通过上式可以得到类别的信息。为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值(数学期望),通过下面的公式:
机器学习(二)——决策树(DecisionTree)_第1张图片
其中,n为分类的数目。熵越大,随机变量的不确定性就越大。
当熵中的概率由数据估计(特别是最大似然估计)得到时,所对应的熵为经验熵(empirical entropy)。意思就是有数据算出来的。我们定义贷款申请样本数据表中的数据为训练数据集D,则训练数据集D的经验熵为H(D),|D|表示其样本容量,及样本个数。设有k个类,Ck = 1,2…k,|Ck|为属于类Ck的样本个数,因此经验熵公式就可以写为:
机器学习(二)——决策树(DecisionTree)_第2张图片

根据此公式计算经验熵H(|D|),分析贷款申请样本数据表中的数据。
机器学习(二)——决策树(DecisionTree)_第3张图片
最终分裂结果只有两类,即放贷和不放贷。根据表中的数据统计可知,在15个数据中,9个数据的结果为放贷,6个数据的结果为不放贷。所以数据集D的经验熵H(|D|)为:
机器学习(二)——决策树(DecisionTree)_第4张图片

经过计算可知,数据集D的经验熵H(|D|)的值为0.971。

(2)编写计算经验熵代码
在编写代码之前,我们先对数据集进行属性标注。
年龄:0代表青年,1代表中年,2代表老年;
有工作:0代表否,1代表是;
有自己的房子:0代表否,1代表是;
信贷情况:0代表一般,1代表好,2代表非常好;
类别(是否给贷款):no代表否,yes代表是。

然后创建数据集并计算经验熵,代码如下:

from math import log

'''
函数说明:创建数据集

Rarameters:
    无
Returns:
    dataSet  - 数据集
    labels - 分类属性
Modify:
    2021-01-28
'''
def creatDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['放贷','不放贷']
    return dataSet,labels

'''
函数说明:计算给定数据集的经验熵

Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵
Modify:
    2021-01-28
'''
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                         #返回经验熵

if __name__  == '__main__':
    dataSet,labels = creatDataSet()
    print(dataSet)
    print(calcShannonEnt(dataSet))
     

(3)信息增益
信息增益时相对于特征而言的,信息增益越大,特征对最终的分类结果影响也就越大,我们就应该选择对最终分类结果影响最大的哪个特征作为我们的分类特征。
在了解信息增益定义之前,我们还需要明确一个概念,条件熵。条件熵(H(Y|X))表示在已知随机变量X的条件下随机变量Y的不确定性,在随机变量X给定的条件下随机变量Y 的条件熵(conditional gain),定义为X给定条件下Y的条件概率分布的熵对X的数学期望:
机器学习(二)——决策树(DecisionTree)_第5张图片
这里:
机器学习(二)——决策树(DecisionTree)_第6张图片
同理,当条件熵中的概率由数据估计(特别是极大似然估计)得到时,所对应的条件熵称为条件经验熵(empirical conditional entropy)。
明确了条件熵和经验条件熵的概念。然后就是信息增益,信息增益是相对于于特征而言的。特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(|D|)与特征A给定条件下D的经验条件熵H(D|A)之差,即:
机器学习(二)——决策树(DecisionTree)_第7张图片
熵H(|D|)与条件熵H(D|A)之差称为互信息(mutual information)。决策树学习中的信息增益等价于训练数据集中类与特征的互信息。
设特征A有n个不同的取值{a1,a2,a3,…,an},根据特征A的取值将D划分为n个子集{D1,D2,…,Dn},|Di|为Di的样本个数。记子集Di中属于Ck的样本的集合为Dik,即Dik = Di ∩ Ck,|Dik|为Dik的样本个数,于是经验条件熵的公式为:
机器学习(二)——决策树(DecisionTree)_第8张图片
概念没懂,举例就明白了。看下年龄的数据,也就是特征A1,一共有三个大类,分别是:青年,中年和老年。我们只看年龄是青年的数据,年龄是请奶奶的数据有5个,所以年龄是青年的数据在训练数据集出现的概率是十五分之五就是三分之一。年龄是中年和老年的也是三分之一。然后只看年龄是青年的数据最终得到贷款的概率是五分之二,年龄是中年和老年最终得到贷款的概率是五分之三和五分之四。所以计算年龄的信息增益,过程如下:
机器学习(二)——决策树(DecisionTree)_第9张图片

同理,我们可以计算其余几个特征的信息增益g(D,A2),g(D,A3)和g(D,A4)。分别为:
机器学习(二)——决策树(DecisionTree)_第10张图片
机器学习(二)——决策树(DecisionTree)_第11张图片
机器学习(二)——决策树(DecisionTree)_第12张图片

最终对比三个特征的信息增益,由于特征A3(有无自己的房子)的信息增益最大,所以选择A3作为最优特征。

(4)编写计算信息增益代码
我们已经通过公式学会了计算信息增益,下面来编写对应的代码。

from math import log

'''
函数说明:创建数据集

Rarameters:
    无
Returns:
    dataSet  - 数据集
    labels - 分类属性
Modify:
    2021-01-29
'''
def creatDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['放贷','不放贷']
    return dataSet,labels

'''
函数说明:计算给定数据集的经验熵

Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵
Modify:
    2021-01-29
'''
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                         #返回经验熵


'''
函数说明:按照给定特征划分数据集

Parameters:
    dataSet - 待划分的数据集
    axis - 划分数据集的特征
    value - 需要返回的特征的值
Returns:
    无
Modify:
    2021-01-29
'''
def splitDataSet(dataSet,axis,value):
    retDataset = []                          #创建返回的数据集列表          
    for featVec in dataSet:                  #遍历列表
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]       #去掉axis特征
            reducedFeatVec.extend(featVec[axis+1:])  #将符合条件的添加到返回的数据集
            retDataset.append(reducedFeatVec)
    return retDataset                               #返回划分后的数据集

'''
函数说明:选择最优特征

Parameters:
    dataSet - 待划分的数据集
Returns:
    bestFeature - 信息增益最大的(最优)特征的索引值
Modify:
    2021-01-31
'''
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0]) - 1              #特征数量         
    baseENtropy = calcShannonEnt(dataSet)           #计算数据集的香农熵
    bestInfoGain = 0.0                          #最大信息增益
    bestFeature = -1                            #最有特征的索引
    for i in range(numFeature):                          #遍历所有特征
        featList = [example[i] for example in dataSet]  #获取dataSet的第i个特征的所有值
        uniqueVals = set(featList)            #创建set集合{},元素不可重复
        newEntorpy = 0.0                    #经验条件熵
        for value in uniqueVals:                #计算信息增益
            subDataSet = splitDataSet(dataSet,i,value)   #subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
            newEntorpy += prob * calcShannonEnt(subDataSet)  #计算各子集条件经验熵并累加
        infoGain = baseENtropy - newEntorpy     #计算信息增益
        print('第%d个特征信息增益为%.3f'%(i,infoGain))
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

if __name__ == '__main__':
    dataSet,Labels = creatDataSet()
    print('最优特征的索引值:'+str(chooseBestFeatureToSplit(dataSet)))

其中比较重要的点是splitDataSet函数用来选择各个特征的子集,比如选择年龄(第0个特征)的青年(用0代表)的子集,我们可以调用splitDataSet(dataSet,0,0)这样返回的子集就是年龄为青年的5个数据集。

1.2.2 决策树的生成

上面主要是将数据集到构造决策树算法所需要的子功能模块,包括经验熵的计算和最有特征的选择,其工作原理如下:1、得到原始数据集;2、按最优特征将数据集进行分类;3、划分后数据集被向下传递到树的分支的下一个节点继续进行最优特征的划分,采用递归的原则处理数据集。
构造决策树的算法有很多,常见有C4.5,ID3和CART,这些算法并不总会在每次划分数据分组时都会消耗特征。由于特征数目数目并不会在每次划分数据时减少就会出现一些问题。但是目前我们只需要算法运行前计算列的数目查看算法是否运用了所有的特征即可。
决策树算法递归地产生决策树,直到不能继续下去为止。这样产生的树对于训练数据分类很准确但是对于未知的测试数据的分类却并没有那么准确,即出现过拟合的现象。解决这个问题的办法就是考虑决策树的复杂度,对已生产的决策树进行简化。
构建决策树的算法有很多,本文仅介绍ID3算法的构建。

(1)ID3算法
ID3算法的核心是在决策树各个结点上对应信息增益准则选择特征,递归地构造决策树。具体方法时:从根节点(root node),对节点计算所有可能的特征的信息增益,选择信息增益最大的特征作为节点的特征,由该特征的不同取值建立子节点;再对子节点递归调用以上方法,构造决策树;直到所有特征的信息增益均很小或者没有特征可以选择为止。最后得到一个决策树。ID3相当于用极大似然法进行概率模型的选择。

(2)编写构建决策树代码
使用字典存储决策树的结构,比如上一节分析的决策树,用字典可表示为:

{'有自己的房子':{0:{'有工作':{0:'no',1:'yes'}},1:'yes'}}

创建函数majorityCnt统计classList中出现最多的元素(类标签),创建函数createTree用来递归构建决策树。编写代码如下:

from math import log
import operator
'''
函数说明:创建数据集

Rarameters:
    无
Returns:
    dataSet  - 数据集
    labels - 分类属性
Modify:
    2021-01-29
'''
def createDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['年龄', '有工作', '有自己的房子', '信贷情况'] 
    return dataSet,labels

'''
函数说明:计算给定数据集的经验熵

Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵
Modify:
    2021-01-29
'''
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                         #返回经验熵


'''
函数说明:按照给定特征划分数据集

Parameters:
    dataSet - 待划分的数据集
    axis - 划分数据集的特征
    value - 需要返回的特征的值
Returns:
    无
Modify:
    2021-01-29
'''
def splitDataSet(dataSet,axis,value):
    retDataset = []                          #创建返回的数据集列表          
    for featVec in dataSet:                  #遍历列表
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]       #去掉axis特征
            reducedFeatVec.extend(featVec[axis+1:])  #将符合条件的添加到返回的数据集
            retDataset.append(reducedFeatVec)
    return retDataset                               #返回划分后的数据集


'''
函数说明:选择最优特征

Parameters:
    dataSet - 待划分的数据集
Returns:
    bestFeature - 信息增益最大的(最优)特征的索引值
Modify:
    2021-01-31
'''
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0]) - 1              #特征数量         
    baseENtropy = calcShannonEnt(dataSet)           #计算数据集的香农熵
    bestInfoGain = 0.0                          #最大信息增益
    bestFeature = -1                            #最有特征的索引
    for i in range(numFeature):                          #遍历所有特征
        featList = [example[i] for example in dataSet]  #获取dataSet的第i个特征的所有值
        uniqueVals = set(featList)            #创建set集合{},元素不可重复
        newEntorpy = 0.0                    #经验条件熵
        for value in uniqueVals:                #计算信息增益
            subDataSet = splitDataSet(dataSet,i,value)   #subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
            newEntorpy += prob * calcShannonEnt(subDataSet)  #计算各子集条件经验熵并累加
        infoGain = baseENtropy - newEntorpy     #计算信息增益
#         print('第%d个特征信息增益为%.3f'%(i,infoGain))
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature


'''
函数说明:统计classList中出现此处最多的元素(类标签)
 
Parameters:
    classList - 类标签列表
Returns:
    sortedClassCount[0][0] - 出现此处最多的元素(类标签)
Modify:
    2021-01-31
'''
def mojorityCnt(classList):
    classList = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
            classCount += 1
    sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse = True)
    return sortedClassCount[0][0]    

'''
函数说明:创建决策树
 
Parameters:
    dataSet - 训练数据集
    labels - 分类属性标签
    featLabels - 存储选择的最优特征标签
Returns:
    myTree - 决策树
Modify:
    2021-01-31
'''
def createTree(dataSet,labels,featLabels):
    classList = [example[-1] for example in dataSet]     #取分类标签
    if classList.count(classList[0]) == len(classList):    #如果标签类别完全相同则不划分
        return classList[0]
    if len(dataSet[0]) == 1 or len(labels) == 0:       #样本数量仅有1个或者没有标签
        return mojorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)   #选择最优特征的索引
    bestFeatLabel = labels[bestFeat]           #选择最优特征的标签 
    featLabels.append(bestFeatLabel)
    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,featLabels) 
    return myTree


if __name__ == '__main__':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    print(myTree)


递归创建决策树时,递归有两个终止条件:一是所有的类标签完全相同,则直接返回该类标签;二是使用完了所有的特征,仍然不能将数据划分为仅包含唯一类别的分组,即决策树构建失败,特征不够用。此时说明数据维度不够,由于第二个停止条件无法简单地返回唯一的类标签,这里挑选出数量最多的类别作为返回值。

(3)可视化
为了将决策树过程更形象地展示出来,使用matplotlib包中的以下函数:
getNumLeafs:获取决策树叶子结点的数目
getTreeDepth:获取决策树的层数
plotNode:绘制结点
plotMidText:标注有向边属性值
plotTree:绘制决策树
createPlot:创建绘制面板
代码如下:

from math import log
import operator
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

'''
函数说明:创建数据集

Rarameters:
    无
Returns:
    dataSet  - 数据集
    labels - 分类属性
Modify:
    2021-01-29
'''
def createDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['年龄', '有工作', '有自己的房子', '信贷情况'] 
    return dataSet,labels

'''
函数说明:计算给定数据集的经验熵

Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵
Modify:
    2021-01-29
'''
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                         #返回经验熵


'''
函数说明:按照给定特征划分数据集

Parameters:
    dataSet - 待划分的数据集
    axis - 划分数据集的特征
    value - 需要返回的特征的值
Returns:
    无
Modify:
    2021-01-29
'''
def splitDataSet(dataSet,axis,value):
    retDataset = []                          #创建返回的数据集列表          
    for featVec in dataSet:                  #遍历列表
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]       #去掉axis特征
            reducedFeatVec.extend(featVec[axis+1:])  #将符合条件的添加到返回的数据集
            retDataset.append(reducedFeatVec)
    return retDataset                               #返回划分后的数据集


'''
函数说明:选择最优特征

Parameters:
    dataSet - 待划分的数据集
Returns:
    bestFeature - 信息增益最大的(最优)特征的索引值
Modify:
    2021-01-31
'''
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0]) - 1              #特征数量         
    baseENtropy = calcShannonEnt(dataSet)           #计算数据集的香农熵
    bestInfoGain = 0.0                          #最大信息增益
    bestFeature = -1                            #最有特征的索引
    for i in range(numFeature):                          #遍历所有特征
        featList = [example[i] for example in dataSet]  #获取dataSet的第i个特征的所有值
        uniqueVals = set(featList)            #创建set集合{},元素不可重复
        newEntorpy = 0.0                    #经验条件熵
        for value in uniqueVals:                #计算信息增益
            subDataSet = splitDataSet(dataSet,i,value)   #subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
            newEntorpy += prob * calcShannonEnt(subDataSet)  #计算各子集条件经验熵并累加
        infoGain = baseENtropy - newEntorpy     #计算信息增益
#         print('第%d个特征信息增益为%.3f'%(i,infoGain))
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature


'''
函数说明:统计classList中出现此处最多的元素(类标签)
 
Parameters:
    classList - 类标签列表
Returns:
    sortedClassCount[0][0] - 出现此处最多的元素(类标签)
Modify:
    2021-01-31
'''
def mojorityCnt(classList):
    classList = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
            classCount += 1
    sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse = True)
    return sortedClassCount[0][0]    

'''
函数说明:创建决策树
 
Parameters:
    dataSet - 训练数据集
    labels - 分类属性标签
    featLabels - 存储选择的最优特征标签
Returns:
    myTree - 决策树
Modify:
    2021-01-31
'''
def createTree(dataSet,labels,featLabels):
    classList = [example[-1] for example in dataSet]     #取分类标签
    if classList.count(classList[0]) == len(classList):    #如果标签类别完全相同则不划分
        return classList[0]
    if len(dataSet[0]) == 1 or len(labels) == 0:       #样本数量仅有1个或者没有标签
        return mojorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)   #选择最优特征的索引
    bestFeatLabel = labels[bestFeat]           #选择最优特征的标签 
    featLabels.append(bestFeatLabel)
    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,featLabels) 
    return myTree


'''
函数说明:获取决策树叶子结点的数目
 
Parameters:
    myTree - 决策树
Returns:
    numLeafs - 决策树的叶子结点的数目
Modify:
    2021-01-31

'''
'''上个例子中myTree:{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}'''
def getNumLeafs(myTree):
    numLeafs = 0                         #初始化叶子
    firstStr = next(iter(myTree))             #返回迭代器的下一个项目
    secondDict = myTree[firstStr]             #获取下一组字典
    for key in secondDict.keys():              
        if type(secondDict[key]).__name__=='dict':     #测试该节点是否为字典,如果不是字典代表此节点为叶子节点
            numLeafs += getNumLeafs(secondDict[key])
        else:
            numLeafs += 1
    return numLeafs

'''
函数说明:获取决策树的层数
 
Parameters:
    myTree - 决策树
Returns:
    maxDepth - 决策树的层数
Modify:
    2021-01-31

'''
def getTreeDepth(myTree):
    maxDepth = 0                       #初始化决策树深度
    firstStr = next(iter(myTree))        #返回迭代器inter(myTree)的下一个项目
    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                   #更新层数

'''
函数说明:绘制结点
 
Parameters:
    nodeTxt - 结点名
    centerPt - 文本位置
    parentPt - 标注的箭头位置
    nodeType - 结点格式
Returns:
    无
Modify:
    2021-02-01
'''
def plotNode(nodeTxt,centerPt,parentPt,nodeType):
    arrow_args = dict(arrowstyle = '<-')              #定义箭头格式
    font = FontProperties(fname = r"c:\Windows\Fonts\SimHei.ttf",size = 14)
    createPlot.ax1.annotate(nodeTxt,xy=parentPt,xycoords='axes fraction',    #绘制结点 
                           xytext=centerPt,textcoords='axes fraction',va='center',
                          ha='center',bbox=nodeType,arrowprops=arrow_args,
                          FontProperties=font)

'''
函数说明:标注有向边属性值
 
Parameters:
    cntrPt、parentPt - 用于计算标注位置
    txtString - 标注的内容
Returns:
    无
Modify:
    2021-02-01
 '''   
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,va='center',ha='center',rotation=30)

'''
函数说明:绘制决策树
 
Parameters:
    myTree - 决策树(字典)
    parentPt - 标注的内容
    nodeTxt - 结点名
Returns:
    无
Modify:
    2021-02-01   
'''
def plotTree(myTree,parentPt,nodeTxt):
    decisionNode = dict(boxstyle='sawtooth',fc='0.8')     #设置结点格式
    leafNode = dict(boxstyle='round4',fc='0.8')        #设置叶结点格式
    numLeafs = getNumLeafs(myTree)            #获取决策树叶结点数目
    depth = getTreeDepth(myTree)            #获取决策树层数
    firstStr = next(iter(myTree))           #下一个字典
    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偏移
    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

    
'''
函数说明:创建绘制面板
 
Parameters:
    inTree - 决策树(字典)
Returns:
    无
Modify:
    2021-02-01
'''  
def createPlot(inTree):
    fig = plt.figure(1,facecolor='white')        #创建fig
    fig.clf()                                #清空fig
    axprops = dict(xticks=[],yticks=[])    
    createPlot.ax1 = plt.subplot(111,frameon = False, **axprops)   #去掉x、y轴
    plotTree.totalW = float(getNumLeafs(inTree))        #获取决策树叶结点层数
    plotTree.totalD = float(getTreeDepth(inTree))         #获取决策树层数
    plotTree.xOff = -0.5/plotTree.totalW;plotTree.yOff = 1.0;    #x偏移
    plotTree(inTree,(0.5,1.0),'')              #绘制决策树
    plt.show()

if __name__ == '__main__':
    dataSet,labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet,labels,featLabels)
    print(myTree)
    createPlot(myTree)

(4)使用决策树执行分类
在经过训练集训练后的决策树可以将其用于实际数据的分类。在执行数据分类时,需要将决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程进入叶子结点;最后将测试数据定义为叶子结点所属类型。这这里面有个featLabels参数是用来记录各个分类结点,在用决策树做预测的时候,我们按顺序输入需要的分类结点的属性值即可。
用决策树做分类代码如下:

from math import log
import operator
'''
函数说明:创建数据集

Rarameters:
    无
Returns:
    dataSet  - 数据集
    labels - 分类属性
Modify:
    2021-01-29
'''
def createDataSet():
    dataSet = [[0, 0, 0, 0, 'no'],
            [0, 0, 0, 1, 'no'],
            [0, 1, 0, 1, 'yes'],
            [0, 1, 1, 0, 'yes'],
            [0, 0, 0, 0, 'no'],
            [1, 0, 0, 0, 'no'],
            [1, 0, 0, 1, 'no'],
            [1, 1, 1, 1, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [1, 0, 1, 2, 'yes'],
            [2, 0, 1, 2, 'yes'],
            [2, 0, 1, 1, 'yes'],
            [2, 1, 0, 1, 'yes'],
            [2, 1, 0, 2, 'yes'],
            [2, 0, 0, 0, 'no']]
    labels = ['年龄', '有工作', '有自己的房子', '信贷情况'] 
    return dataSet,labels

'''
函数说明:计算给定数据集的经验熵

Parameters:
    dataSet - 数据集
Returns:
    shannonEnt - 经验熵
Modify:
    2021-01-29
'''
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                         #返回经验熵


'''
函数说明:按照给定特征划分数据集

Parameters:
    dataSet - 待划分的数据集
    axis - 划分数据集的特征
    value - 需要返回的特征的值
Returns:
    无
Modify:
    2021-01-29
'''
def splitDataSet(dataSet,axis,value):
    retDataset = []                          #创建返回的数据集列表          
    for featVec in dataSet:                  #遍历列表
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]       #去掉axis特征
            reducedFeatVec.extend(featVec[axis+1:])  #将符合条件的添加到返回的数据集
            retDataset.append(reducedFeatVec)
    return retDataset                               #返回划分后的数据集


'''
函数说明:选择最优特征

Parameters:
    dataSet - 待划分的数据集
Returns:
    bestFeature - 信息增益最大的(最优)特征的索引值
Modify:
    2021-01-31
'''
def chooseBestFeatureToSplit(dataSet):
    numFeature = len(dataSet[0]) - 1              #特征数量         
    baseENtropy = calcShannonEnt(dataSet)           #计算数据集的香农熵
    bestInfoGain = 0.0                          #最大信息增益
    bestFeature = -1                            #最有特征的索引
    for i in range(numFeature):                          #遍历所有特征
        featList = [example[i] for example in dataSet]  #获取dataSet的第i个特征的所有值
        uniqueVals = set(featList)            #创建set集合{},元素不可重复
        newEntorpy = 0.0                    #经验条件熵
        for value in uniqueVals:                #计算信息增益
            subDataSet = splitDataSet(dataSet,i,value)   #subDataSet划分后的子集
            prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率
            newEntorpy += prob * calcShannonEnt(subDataSet)  #计算各子集条件经验熵并累加
        infoGain = baseENtropy - newEntorpy     #计算信息增益
#         print('第%d个特征信息增益为%.3f'%(i,infoGain))
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature


'''
函数说明:统计classList中出现此处最多的元素(类标签)
 
Parameters:
    classList - 类标签列表
Returns:
    sortedClassCount[0][0] - 出现此处最多的元素(类标签)
Modify:
    2021-01-31
'''
def mojorityCnt(classList):
    classList = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
            classCount += 1
    sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse = True)
    return sortedClassCount[0][0]    

'''
函数说明:创建决策树
 
Parameters:
    dataSet - 训练数据集
    labels - 分类属性标签
    featLabels - 存储选择的最优特征标签
Returns:
    myTree - 决策树
Modify:
    2021-01-31
'''
def createTree(dataSet,labels,featLabels):
    classList = [example[-1] for example in dataSet]     #取分类标签
    if classList.count(classList[0]) == len(classList):    #如果标签类别完全相同则不划分
        return classList[0]
    if len(dataSet[0]) == 1 or len(labels) == 0:       #样本数量仅有1个或者没有标签
        return mojorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)   #选择最优特征的索引
    bestFeatLabel = labels[bestFeat]           #选择最优特征的标签 
    featLabels.append(bestFeatLabel)
    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,featLabels) 
    return myTree

'''
函数说明:使用决策树分类
 
Parameters:
    inputTree - 已经生成的决策树
    featLabels - 存储选择的最优特征标签
    testVec - 测试数据列表,顺序对应最优特征标签
Returns:
    classLabel - 分类结果
Modify:
    2021-02-01
'''


def classify(inputTree,featLabels,testVec):
    firstStr = next(iter(inputTree))
    secondDict = inputTree[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
                
                
                
if __name__ == '__main__':
    dataSet, labels = createDataSet()
    featLabels = []
    myTree = createTree(dataSet, labels, featLabels)
    testVec = [0,1]
    result = classify(myTree,featLabels,testVec)
    if result == 'yes':
        print('放贷')
    if result == 'np':
        print('不放贷')
 

**(五)决策树的存储 **
构造决策树是很耗时的任务,即使很小的数据集也需要几秒的时间,故创建好的决策树解决分类问题可以很快完成。为了节省计算时间,最好能够每次执行分类时调用已经构造好的决策树。为了解决这个问题,需要使用python模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。
假设我们已经得到决策树{‘有自己的房子’: {0: {‘有工作’: {0: ‘no’, 1: ‘yes’}}, 1: ‘yes’}},使用pickle.dump存储决策树。

import pickle

'''
函数说明:存储决策树

Parameters:
    inputTree - 已经生成的决策树
    filename - 决策树的存储文件名
Returns:
    无
Modify:
    2021-02-01
'''
def storeTree(inputTree,filename):
    with open(filename,'wb') as fw:
        pickle.dump(inputTree,fw)

if __name__ == '__main__':
    myTree =  {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}
    storeTree(myTree,'classifierStorage.txt')

存储后会出现一个classifierStorage.txt文件,使用pickle进行载入进行使用:

import pickle

'''
函数说明:读取决策树

Parameters:
    filename - 决策树的存储文件名
Returns:
    pickle.load(fr) - 决策树字典
Modify:
    2021-02-01
'''
def grabTree(filename):
    fr = open(filename,'rb') 
    return pickle.load(fr)

if __name__ == '__main__':
    myTree =  grabTree('classifierStorage.txt')
    print(myTree)

二、 Sklearn之使用决策树预测隐形眼镜类型

2.1 背景

隐形眼镜数据集是非常著名的数据集,它包含很多换着眼部状态的观察条件以及医生推荐的隐形眼镜类型。隐形眼镜类型包括硬材质(hard)、软材质(soft)以及不适合佩戴隐形眼镜(no lenses)。数据来源与UCI数据库,数据集下载地址:https://github.com/Jack-Cherish/Machine-Learning/blob/master/Decision%20Tree/classifierStorage.txt

一共有24组数据,数据的Labels依次是age、prescript、astigmatic、tearRate、class,也就是第一列是年龄,第二列是症状,第三列是是否散光,第四列是眼泪数量,第五列是最终的分类标签。
机器学习(二)——决策树(DecisionTree)_第13张图片

2.2 使用Sklearn构建决策树

本次实战内容使用的是DecisionTreeClassifier和export_graphviz,前者用于决策树构建,后者用于决策树可视化。
机器学习(二)——决策树(DecisionTree)_第14张图片
参数说明如下:

  • criterion:特征选择标准,可选参数,默认是gini,可以设置为entropy。gini是基尼不纯度,是将来自集合的某种结果随机应用于某一数据项的预期误差率,是一种基于统计的思想。entropy是香农熵,也就是上篇文章讲过的内容,是一种基于信息论的思想。Sklearn把gini设为默认参数,应该也是做了相应的斟酌的,精度也许更高些?ID3算法使用的是entropy,CART算法使用的则是gini。

  • splitter:特征划分点选择标准,可选参数,默认是best,可以设置为random。每个结点的选择策略。best参数是根据算法选择最佳的切分特征,例如gini、entropy。random随机的在部分划分点中找局部最优的划分点。默认的"best"适合样本量不大的时候,而如果样本数据量非常大,此时决策树构建推荐"random"。

  • max_features:划分时考虑的最大特征数,可选参数,默认是None。寻找最佳切分时考虑的最大特征数(n_features为总共的特征数),有如下6种情况:

    • 如果max_features是整型的数,则考虑max_features个特征;
    • 如果max_features是浮点型的数,则考虑int(max_features * n_features)个特征;
    • 如果max_features设为auto,那么max_features = sqrt(n_features);
    • 如果max_features设为sqrt,那么max_featrues = sqrt(n_features),跟auto一样;
    • 如果max_features设为log2,那么max_features = log2(n_features);
    • 如果max_features设为None,那么max_features = n_features,也就是所有特征都用。
    • 一般来说,如果样本特征数不多,比如小于50,我们用默认的"None"就可以了,如果特征数非常多,我们可以灵活使用刚才描述的其他取值来控制划分时考虑的最大特征数,以控制决策树的生成时间。
  • max_depth:决策树最大深,可选参数,默认是None。这个参数是这是树的层数的。层数的概念就是,比如在贷款的例子中,决策树的层数是2层。如果这个参数设置为None,那么决策树在建立子树的时候不会限制子树的深度。一般来说,数据少或者特征少的时候可以不管这个值。或者如果设置了min_samples_slipt参数,那么直到少于min_smaples_split个样本为止。如果模型样本量多,特征也多的情况下,推荐限制这个最大深度,具体的取值取决于数据的分布。常用的可以取值10-100之间。

  • min_samples_split:内部节点再划分所需最小样本数,可选参数,默认是2。这个值限制了子树继续划分的条件。如果min_samples_split为整数,那么在切分内部结点的时候,min_samples_split作为最小的样本数,也就是说,如果样本已经少于min_samples_split个样本,则停止继续切分。如果min_samples_split为浮点数,那么min_samples_split就是一个百分比,ceil(min_samples_split * n_samples),数是向上取整的。如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。

  • min_samples_leaf:叶子节点最少样本数,可选参数,默认是1。这个值限制了叶子节点最少的样本数,如果某叶子节点数目小于样本数,则会和兄弟节点一起被剪枝。叶结点需要最少的样本数,也就是最后到叶结点,需要多少个样本才能算一个叶结点。如果设置为1,哪怕这个类别只有1个样本,决策树也会构建出来。如果min_samples_leaf是整数,那么min_samples_leaf作为最小的样本数。如果是浮点数,那么min_samples_leaf就是一个百分比,同上,celi(min_samples_leaf * n_samples),数是向上取整的。如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。

  • min_weight_fraction_leaf:叶子节点最小的样本权重和,可选参数,默认是0。这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会和兄弟节点一起被剪枝。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,这时我们就要注意这个值了。

  • max_leaf_nodes:最大叶子节点数,可选参数,默认是None。通过限制最大叶子节点数,可以防止过拟合。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。如果特征不多,可以不考虑这个值,但是如果特征分成多的话,可以加以限制,具体的值可以通过交叉验证得到。

  • class_weight:类别权重,可选参数,默认是None,也可以字典、字典列表、balanced。指定样本各类别的的权重,主要是为了防止训练集某些类别的样本过多,导致训练的决策树过于偏向这些类别。类别的权重可以通过{class_label:weight}这样的格式给出,这里可以自己指定各个样本的权重,或者用balanced,如果使用balanced,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。当然,如果你的样本类别分布没有明显的偏倚,则可以不管这个参数,选择默认的None。

  • random_state:可选参数,默认是None。随机数种子。如果是证书,那么random_state会作为随机数生成器的随机数种子。随机数种子,如果没有设置随机数,随机出来的数与当前系统时间有关,每个时刻都是不同的。如果设置了随机数种子,那么相同随机数种子,不同时刻产生的随机数也是相同的。如果是RandomState instance,那么random_state是随机数生成器。如果为None,则随机数生成器使用np.random。

  • min_impurity_split:节点划分最小不纯度,可选参数,默认是1e-7。这是个阈值,这个值限制了决策树的增长,如果某节点的不纯度(基尼系数,信息增益,均方差,绝对差)小于这个阈值,则该节点不再生成子节点。即为叶子节点 。

  • presort:数据是否预排序,可选参数,默认为False,这个值是布尔值,默认是False不排序。一般来说,如果样本量少或者限制了一个深度很小的决策树,设置为true可以让划分点选择更加快,决策树建立的更加快。如果样本量太大的话,反而没有什么好处。问题是样本量少的时候,我速度本来就不慢。所以这个值一般懒得理它就可以了。

除了这些参数要注意以外,其他在调参时的注意点有:

  • 当样本数量少但是样本特征非常多的时候,决策树很容易过拟合,一般来说,样本数比特征数多一些会比较容易建立健壮的模型。
  • 如果样本数量少但是样本特征非常多,在拟合决策树模型前,推荐先做维度规约,比如主成分分析(PCA),特征选择(Losso)或者独立成分分析(ICA)。这样特征的维度会大大减小。再来拟合决策树模型效果会好。
  • 推荐多用决策树的可视化,同时先限制决策树的深度,这样可以先观察下生成的决策树里数据的初步拟合情况,然后再决定是否要增加深度。
  • 在训练模型时,注意观察样本的类别情况(主要指分类树),如果类别分布非常不均匀,就要考虑用class_weight来限制模型过于偏向样本多的类别。
  • 决策树的数组使用的是numpy的float32类型,如果训练数据不是这样的格式,算法会先做copy再运行。
  • 如果输入的样本矩阵是稀疏的,推荐在拟合前调用csc_matrix稀疏化,在预测前调用csr_matrix稀疏化。

了解以上后进行代码编写:
由于训练数据集时不能使用string类型的数据,故需要先将数据集进行编码,通常有两种方式:

  • LabelEncoder :将字符串转换为增量值
  • OneHotEncoder:使用One-of-K算法将字符串转换为整数
    这里先将数据集处理成Dataframe,这样能够方便我们序列化工作。这里我使用的方法是,原始数据->字典->pandas数据->LabelEncoder(),编写代码如下:
import pandas as pd
from sklearn import tree
from sklearn.preprocessing import LabelEncoder
import pydotplus 
# from sklearn.externals.six import StringIO 

if __name__ == '__main__':
    with open('lenses.txt','r') as fr:
        lenses = [inst.strip().split('\t') for inst in fr.readlines()]
    lenses_target =[]
    for each in lenses:
        lenses_target.append(each[-1])
    lensesLabels = ['age','prescript','astigmatic','tearRate']
    lenses_list = []
    lenses_dict = {}
    for each_label in lensesLabels:
        for each in lenses:
            lenses_list.append(each[lensesLabels.index(each_label)])
        lenses_dict[each_label] = lenses_list
        lenses_list = []
#     print(lenses_dict)
    lenses_pd = pd.DataFrame(lenses_dict)
    print(lenses_pd)
    le = LabelEncoder()
    for col in lenses_pd.columns:
        lenses_pd[col] = le.fit_transform(lenses_pd[col])
    print(lenses_pd)

2.3 使用Graphviz可视化决策树

在编写代码之前,我们需要安装两样东西,即pydotplus和Grphviz。命令行指令直接安装:pip install pydotplus和pip install grphviz。

import pandas as pd
import numpy as np
from sklearn import tree
from sklearn.preprocessing import LabelEncoder
import pydotplus 
from io import StringIO

if __name__ == '__main__':
    with open('lenses.txt','r') as fr:
        lenses = [inst.strip().split('\t') for inst in fr.readlines()]
    lenses_target =[]
    for each in lenses:
        lenses_target.append(each[-1])
    lensesLabels = ['age','prescript','astigmatic','tearRate']
    lenses_list = []
    lenses_dict = {}
    for each_label in lensesLabels:
        for each in lenses:
            lenses_list.append(each[lensesLabels.index(each_label)])
        lenses_dict[each_label] = lenses_list
        lenses_list = []
#     print(lenses_dict)
    lenses_pd = pd.DataFrame(lenses_dict)
#     print(lenses_pd)
    le = LabelEncoder()
    for col in lenses_pd.columns:
        lenses_pd[col] = le.fit_transform(lenses_pd[col])
    print(lenses_pd)
    
    clf = tree.DecisionTreeClassifier(max_depth=4)
    clf = clf.fit(lenses_pd.values.tolist(),lenses_target)
    dot_data = StringIO()
    tree.export_graphviz(clf,out_file = dot_data,feature_names = lenses_pd.keys(),
                         class_names = clf.classes_,filled = True, rounded = True,
                         special_characters = True) 
    graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
    graph.write_pdf('tree.pdf')

三、 总结

3.1 优点

  • 易于理解和解释。决策树可以可视化。
  • 几乎不需要数据预处理。其他方法经常需要数据标准化,创建虚拟变量和删除缺失值。决策树还不支持缺失值。
  • 使用树的花费(例如预测数据)是训练数据点(data points)数量的对数。
  • 可以同时处理数值变量和分类变量。其他方法大都适用于分析一种变量的集合。
  • 可以处理多值输出变量问题。
  • 使用白盒模型。如果一个情况被观察到,使用逻辑判断容易表示这种规则。相反,如果是黑盒模型(例如人工神经网络),结果会非常难解释。
  • 即使对真实模型来说,假设无效的情况下,也可以较好的适用。

3.2 缺点

  • 决策树学习可能创建一个过于复杂的树,并不能很好的预测数据。也就是过拟合。修剪机制(现在不支持),设置一个叶子节点需要的最小样本数量,或者数的最大深度,可以避免过拟合。
  • 决策树可能是不稳定的,因为即使非常小的变异,可能会产生一颗完全不同的树。这个问题通过decision trees with an ensemble来缓解。
  • 概念难以学习,因为决策树没有很好的解释他们,例如,XOR, parity or multiplexer problems。
  • 如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,建议在训练之前,先抽样使样本均衡。

参考:https://cuijiahua.com/blog/2017/11/ml_3_decision_tree_2.html

你可能感兴趣的:(机器学习,机器学习,决策树,python,数据挖掘,数据分析)