当前位置:   article > 正文

ML之决策树_ml树

ml树

最近由于文档需求,开始接触了一些机器学习的东西,这里简要记录下学习的过程,目的是简单了解下决策树、随机森林、朴素贝叶斯、聚类这几个分类器的使用。参考书籍为《机器学习实战》,学习之前首先了解下机器学习的分类,主要分为以下几类:

  • 监督学习:必须确定目标变量的值,以便机器学习算法可以发现特征和目标变量之间的关系,包括两种任务类型:分类-将实例数据划分到合适的类别中;回归-主要用于预测数值型数据,样本集由训练数据和测试数据两部分构成,前者用于训练,后者用于验证测试
    • 训练样本 = 特征(feature) + 目标变量(label: 分类-离散值/回归-连续值),特征通常是训练样本集的列,它们是独立测量得到的
    • 目标变量: 目标变量是机器学习预测算法的测试结果
    • 在分类算法中目标变量的类型通常是标称型(如:真与假),而在回归算法中通常是连续型(如:1~100)
    • 决策树、随机森林、朴素贝叶斯、KNN等分类器都是用于监督学习的
  • 无监督学习:数据没有类别信息,也不会给定目标值。在无监督学习中会主要涉及到两个概念:
    • 聚类:在无监督学习中,将数据集分成由类似的对象组成多个类的过程称为聚类
    • 密度估计:将寻找描述数据统计值的过程称之为密度估计,即根据训练样本确定x的概率分布
  • 半监督学习:介于监督学习与无监督学习之间的学习。
    这里给出一张比较直观的图( yealxxy-sklearn库的学习):
    在这里插入图片描述
    这次主要来了解下决策树的一些基础概念与用法。

1. 基础概念

决策树是一种机器学习的方法,它的生成算法有ID3, C4.5和CART等。决策树是一种树形结构(这里只考虑分类不考虑回归),其中每个内部节点表示一个属性上的判断,每个分支代表一个判断结果的输出,最后每个叶节点代表一种分类结果,之后要了解的随机森林也是基于决策树构建的。在分类问题中,可以简单认为是 if-then 规则的集合,表示基于特征对实例进行分类的过程。《机器学习实战》中给的示例图如下图所示:
在这里插入图片描述
不同的生成算法有不同的属性划分选择:

  • ID3算法: 该算法处理如何划分数据集,何时停止划分数据集,原理上由信息增益来决定属性划分即哪个特征做父节点,哪个节点需要分裂。在说明信息增益之前,先要了解熵的概念:
    • 熵(香农熵):是一种信息的度量方式,表示信息的混乱程度,也就是说:信息越有序(即纯度越高),信息熵越低。当Entropy最大为1的时候,是分类效果最差的状态,当它最小为0的时候,是完全分类的状态。由下图公式可以看出分类数目n越大,熵越大
      在这里插入图片描述
    • 经验熵(empirical entropy):有时候也会看到经验熵的概念,和香农熵相同,如果熵中的概率p是由数据估计得到时。所谓的数据估计就比如一个箱子里面共10个球,其中3个黑球7个白球,那么p(取出白球)=7/10,p(取出黑球)=3/10,这种概率就是由数据估计得出来的,用这种概率算出的熵就是经验熵。例如训练数据集D的经验熵为H(D),|D|表示其样本容量,设有K个类Ck(k = 1,2,3,···,K),|Ck|为属于类Ck的样本个数,这经验熵公式可以写为(参考机器学习实战(三)——决策树):
      k=1...K
    • 条件熵:表示为H(Y|X),意为在已知随机变量X的条件下随机变量Y的不确定性,定义给定X条件下Y的条件概率分布的熵对X的数学期望:
      在这里插入图片描述
      在这里插入图片描述
      当熵和条件熵中的概率由数据估计得到时,所对应的分别为经验熵和经验条件熵
    • 信息增益: 在划分数据集前后信息发生的变化称为信息增益。信息增益是相对于特征而言的。因此,特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差,即:
      在这里插入图片描述
  • C4.5算法:ID3存在一个问题,那就是越细小的分割分类错误率越小,所以ID3会越分越细,但是这种分割显然只对训练数据有用,对于新的数据没有意义,这就是所说的过度学习/拟合。C4.5对ID3进行了改进,在C4.5中,优化项要除以分割太细的代价,这个比值叫做信息增益率,显然分割太细分母增加,信息增益率会降低。
    • 信息增益率:特征A对训练数据集D的信息增益比gR(D,A)定义为其信息增益g(D,A)与训练数据集D的经验熵之比:
      在这里插入图片描述
  • CART算法:CART是一个二叉树,可以用来分类,但同时也是回归树。CART使用基尼(GINI)指数作为属性划分指标。基尼值反映了从数据集中随机抽取两个样本,其类别标记不一致的概率,因此基尼值越小,数据集纯度越高(类似熵的概念,但gini值的计算更快一些)。CART和ID3一样,都会存在过度学习(拟合)的问题,为了解决这一问题,可以采用对特别长的树进行剪枝处理的方法等等,其中剪枝处理方法又分为预剪枝或者后剪枝,我们这里就不讨论过度拟合的问题,详细剪枝算法请阅读《机器学习实战》。
    • 预剪枝是指在决策树生成过程中,对每个结点在划分前先进行估计,若当前结点的划分不能带来决策树泛化能力(在训练时加入验证集随时进行泛化验证)的提升,则停止划分并将当前结点标记为叶节点。
    • 后剪枝则是先从训练集中生成一颗完整的树,然后自底向上对非叶节点进行考察,若该节点对应的子树替换为叶节点能够提升泛化能力,则进行剪枝将该子树替换为叶节点,否则不剪枝。

2. 决策树构建

这里主要学习基于ID3算法划分数据的决策树,ID3算法核心是在决策树各个结点上对应信息增益准则选择特征,递归地构建决策树。简单描述构建方法如下:

  • 1)构建根节点,将所有训练数据都放在根节点,选择一个最优特征(通过信息增益准则来选择最优特征),按着这一最优特征将训练数据集分割成子集,使得各个子集有一个在当前条件下最好的分类。
  • 2)如果这些子集已经能够被基本正确分类,那么构建叶节点并将这些子集分到所对应的叶节点去。
  • 3)如果还有子集不能够被正确的分类,那么就对这些子集选择新的最优特征,继续对其进行分割,构建相应的节点。递归进行直至所有训练数据子集被基本正确的分类,或者没有合适的特征为止。
  • 4)每个子集都被分到叶节点上,即都有了明确的类,这样就生成了一颗决策树。另外还需要进行剪枝处理,否则很可能会产生过度匹配的问题。
    上述过程的伪代码如下:
If so return 类标签:
Else
     寻找划分数据集的最优特征
     划分数据集
     创建分支节点
         for 每个划分的子集
             调用函数createBranch()并增加返回结果到分支节点中
         return 分支节点
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

通过上述步骤其实也可以看到,通过信息增益准则来选择最优特征从而划分数据是比较重要的一步,数据划分的过程就是其实构建决策树的过程,之后我们用例子来看一下前面提到的一些概念。这里参考机器学习实战(三)——决策树中给的例子。
在这里插入图片描述
上图是一个贷款申请的数据表,每一行包含一个人的对应列即特征的值,最后一列给出类别即是否可以贷款。在编写代码之前,先对数据集进行属性标注。

  • 年龄:0代表青年,1代表中年,2代表老年;
  • 有工作:0代表否,1代表是;
  • 有自己的房子:0代表否,1代表是;
  • 信贷情况:0代表一般,1代表好,2代表非常好;
  • 类别(是否给贷款):no代表否,yes代表是。

首先引入数据集:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
'
运行

计算给定数据集的香农熵的函数,我们可以计算下原始数据集的香农熵,由于是没有分类的,因此熵值应该是比较大的:

def calcShannonEnt(dataSet):
    # 求list的长度,表示计算参与训练的数据量
    numEntries = len(dataSet)
    # 计算分类标签label出现的次数
    labelCounts = {}
    # the the number of unique elements and their occurrence
    for featVec in dataSet:
        # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签
        currentLabel = featVec[-1]
        # 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    # 对于 label 标签的占比,求出 label 标签的香农熵
    shannonEnt = 0.0
    for key in labelCounts:
        # 使用所有类标签的发生频率计算类别出现的概率。
        prob = float(labelCounts[key])/numEntries
        # 计算香农熵,以 2 为底求对数
        shannonEnt -= prob * log(prob, 2)
    return shannonEnt
    
if __name__ == "__main__":
    myData,labels = createDataSet()
    print("ShannonEnt :" + str(calcShannonEnt(myData)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

运行结果,得到原始数据集的熵,因为还没有分类的原因也就是无序状态,可以看到熵值还是比较大的:
在这里插入图片描述
接着按照给定特征划分数据集,这里特征是之后传入的:

def splitDataSet(dataSet, index, value):
    """
    作用:通过遍历dataSet数据集,求出index对应的colnum列的值为value的行
    Args:
        dataSet 数据集                 待划分的数据集
        index 表示每一行的index列        划分数据集的特征
        value 表示index列对应的value值   需要返回的特征的值
    Returns:
        index列为value的数据集(行)【该数据集需要排除index列】
    """
    retDataSet = []
    for featVec in dataSet: 
        # index列为value的数据集【该数据集需要排除index列】
        # 判断index列的值是否为value
        if featVec[index] == value:
            # chop out index used for splitting
            # [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行
            reducedFeatVec = featVec[:index]
            reducedFeatVec.extend(featVec[index+1:])
            # [index+1:]表示从跳过 index 的 index+1行,取接下来的数据
            # 收集结果值 index列为value的行【该行需要排除index列】
            retDataSet.append(reducedFeatVec)
    return retDataSet
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
'
运行

然后就是我们上面说的选择最优特征:

def chooseBestFeatureToSplit(dataSet):
    """
    作用:选择最优特征
    Args:
        dataSet 数据集
    Returns:
        bestFeature 最优的特征列
    """
    # 求第一行有多少列的 Feature, 最后一列是label列嘛
    numFeatures = len(dataSet[0]) - 1
    # 数据集的原始信息熵
    baseEntropy = calcShannonEnt(dataSet)
    # 最优的信息增益值, 和最优的Featurn编号
    bestInfoGain, bestFeature = 0.0, -1
    # iterate over all the features
    for i in range(numFeatures):
        # create a list of all the examples of this feature
        # 获取对应的feature下的所有数据
        featList = [example[i] for example in dataSet]
        # get a set of unique values
        # 获取剔重后的集合,使用set对list数据进行去重
        uniqueVals = set(featList)
        # 创建一个临时的信息熵
        newEntropy = 0.0
        # 遍历某一列的value集合,计算该列的信息熵 
        # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet, i, value)
            # 计算概率
            prob = len(subDataSet)/float(len(dataSet))
            # 计算信息熵
            newEntropy += prob * calcShannonEnt(subDataSet)
        # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
        # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
        infoGain = baseEntropy - newEntropy
         print('选择第',i,'列作为最优特征得到的信息增益infoGain=', infoGain,'原始数据集熵=',baseEntropy,'按照这个特征值划分后得到的熵=',newEntropy)
        if (infoGain > bestInfoGain):
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

if __name__ == "__main__":
    myData,labels = createDataSet()
    #print("ShannonEnt :" + str(calcShannonEnt(myData)))
    bestFeature = chooseBestFeatureToSplit(myData)
    print('最优特征列',bestFeature)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

运行结果:
在这里插入图片描述
可以看到,信息增益就等于原始数据集熵-数据集划分后得到的熵,并且选择第2列(有自己的房子)作为最优特征列得到的信息增益最大。
由于特征数目并不是在每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,查看算法是否使用了所有属性即可。如果数据集已经处理了所有属性,但是类标签依然不是唯一
的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类:

def majorityCnt(classList):
    """
    作用:选择出现次数最多的一个结果
    Args:
        classList label列的集合
    Returns:
        sortedClassCount[0][0]:出现次数最多的元素(类标签)
    """
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    # 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
'
运行

接着就开始创建树:

def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]
    # 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
    # 第一个停止条件: 所有的类标签完全相同,则直接返回该类标签。
    # count() 函数是统计括号中的值在list中出现的次数
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
    # 第二个停止条件: 使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)
    # 选择最优的列,得到最优列对应的label含义
    bestFeat = chooseBestFeatureToSplit(dataSet)
    # 获取label的名称
    bestFeatLabel = labels[bestFeat]
    # 初始化myTree
    myTree = {bestFeatLabel: {}}
    # 注: labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
    # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
    del(labels[bestFeat])
    # 取出最优列,然后它的branch做分类
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        # 求出剩余的标签label
        subLabels = labels[:]
        # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

if __name__ == "__main__":
    myData,labels = createDataSet()
    #print("ShannonEnt :" + str(calcShannonEnt(myData)))
    #bestFeature = chooseBestFeatureToSplit(myData)
    #print('最优特征列',bestFeature)
    myTree = createTree(myData,labels)
    print(myTree)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

运行结果如下:
在这里插入图片描述
可以看到,对原始数据的第一次划分时选择的是第2列(实际上是表中的第三列-有自己的房子)作为最优特征列,第一次划分后,第2列(实际上是表中的第三列-有自己的房子)这个特征就分出去了(所以第二次划分的第2列实际上是表中的第4列),接着再次递归建树,第二次时,选择的是第一列(实际上是表中的第二列-有工作)作为最优特征列(infoGain=0.918最大,且划分后熵为0代表数据集为完全有序状态),最后得到的决策树如上图中最后一行所示,二次划分后就已经得到了一个有序的数据集。

3. 决策树可视化

使用python提供的Matplotlib进行绘制决策树,使用到的函数如下:

  • getNumLeafs:获取决策树叶子结点的数目
  • getTreeDepth:获取决策树的层数
  • plotNode:绘制结点
  • plotMidText:标注有向边属性值
  • plotTree:绘制决策树
  • createPlot:创建绘制面板

首先初始化一些格式例如节点、箭头,然后依次实现上述函数:

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

decisionNode = dict(boxstyle="sawtooth", fc="0.8")#设置结点格式
leafNode = dict(boxstyle="round4", fc="0.8")#设置叶结点格式
arrow_args = dict(arrowstyle="<-")#设置箭头格式

def plotNode(nodeTxt, centerPt, parentPt, nodeType):
    font = FontProperties(fname=r"/Users/iris/Downloads/simsun.ttc", size=10) 
    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)

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

def getTreeDepth(myTree):
    maxDepth = 0
    firstStr = next(iter(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

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)

def plotTree(myTree, parentPt, nodeTxt):
    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
    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):
    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.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
    plotTree(inTree, (0.5,1.0), '')
    plt.rcParams['font.sans-serif']=['SimHei'] 
    plt.rcParams['axes.unicode_minus']=False
    plt.show()
    
if __name__ == "__main__":
    myData,labels = createDataSet()
    #print("ShannonEnt :" + str(calcShannonEnt(myData)))
    #bestFeature = chooseBestFeatureToSplit(myData)
    myTree = createTree(myData,labels)
    #print(myTree)
    createPlot(myTree)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

效果如下:
在这里插入图片描述
对应的树和前面运行的结果相同:
在这里插入图片描述

4. 使用决策树进行分类

依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子结点;最后将测试数据定义为叶子结点所属的类型。

......
def classify(inputTree, featLabels, testVec):
    """classify(给输入的节点,进行分类)
    Args:
        inputTree  决策树模型
        featLabels Feature标签对应的名称
        testVec    测试输入的数据
    Returns:
        classLabel 分类的结果值,需要映射label才能知道名称
    """
    # 获取tree的根节点对于的key值
    firstStr=next(iter(inputTree))
    # 通过key得到根节点对应的value
    secondDict = inputTree[firstStr]
    # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
    featIndex = featLabels.index(firstStr)
    # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    #print(firstStr, secondDict, '---', key, '>>>', valueOfFeat)
    # 判断分枝是否结束: 判断valueOfFeat是否是dict类型
    if isinstance(valueOfFeat, dict):
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else:
        classLabel = valueOfFeat
    return classLabel
......
if __name__ == "__main__":
    myData,labels = createDataSet()
    #print("ShannonEnt :" + str(calcShannonEnt(myData)))
    #bestFeature = chooseBestFeatureToSplit(myData)
    myTree = createTree(myData,copy.deepcopy(labels))
    #print(myTree)
    #createPlot(myTree)
    testVec = [2,0,0,1]
    result=classify(myTree,labels,testVec)
    if result=='yes':
        print('放贷')
    if result=='no':
        print('不放贷')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

运行结果:
在这里插入图片描述

5.决策树的存储

《机器学习实战》中提到:构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用已经构造好的决策树。为了解决这个问题,需要使用Python模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。拿上面我们构建的程序来说,每给出一个新的测试用例,运行的时候找最优特征、划分数据建树等过程都会再进行一遍,很耗时。如果我们能存储这个树,那么只需要调用classify这个函数就好,且第一个参数inputTree不用我们再重新构建了,就节省了很多的时间。

def storeTree(inputTree,filename):
    fw = open(filename,'wb')
    pickle.dump(inputTree,fw)#使用pickle.dump存储决策树
    fw.close()
    
def grabTree(filename):
    fr = open(filename,'rb')
    return pickle.load(fr)#pickle.load进行载入决策树

if __name__ == "__main__":
    myData,labels = createDataSet()
    #print("ShannonEnt :" + str(calcShannonEnt(myData)))
    #bestFeature = chooseBestFeatureToSplit(myData)
    #print(myTree)
    #createPlot(myTree)
    storeTree(createTree(myData,copy.deepcopy(labels)),"myTree.txt")
    myTree = grabTree("myTree.txt")
    testVec = [2,0,0,1]
    result=classify(myTree,labels,testVec)
    if result=='yes':
        print('放贷')
    if result=='no':
        print('不放贷')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

效果如下:
在这里插入图片描述

6. 实例-使用决策树预测隐形眼镜类型

隐形眼镜数据集是非常著名的数据集,它包含很多患者眼部状况的观察条件以及医生推荐的隐形眼镜类型。特征包括’age’, ‘prescript’, ‘astigmatic’, ‘tearRate’,隐形眼镜类型包括hard、soft以及no lenses(不适合佩戴隐形眼镜)。

young	myope	no	reduced	no lenses
young	myope	no	normal	soft
young	myope	yes	reduced	no lenses
young	myope	yes	normal	hard
young	hyper	no	reduced	no lenses
young	hyper	no	normal	soft
young	hyper	yes	reduced	no lenses
young	hyper	yes	normal	hard
pre	myope	no	reduced	no lenses
pre	myope	no	normal	soft
pre	myope	yes	reduced	no lenses
pre	myope	yes	normal	hard
pre	hyper	no	reduced	no lenses
pre	hyper	no	normal	soft
pre	hyper	yes	reduced	no lenses
pre	hyper	yes	normal	no lenses
presbyopic	myope	no	reduced	no lenses
presbyopic	myope	no	normal	no lenses
presbyopic	myope	yes	reduced	no lenses
presbyopic	myope	yes	normal	hard
presbyopic	hyper	no	reduced	no lenses
presbyopic	hyper	no	normal	soft
presbyopic	hyper	yes	reduced	no lenses
presbyopic	hyper	yes	normal	no lenses
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

那么按照我们之前的步骤。给出如下程序:

def LensesTest():
    """
    作用:预测隐形眼镜的测试代码
    Args:none
    Returns:none
    """
    # 加载隐形眼镜相关的 文本文件 数据
    fr = open('lenses.txt')
    # 解析数据,获得 features 数据
    lenses = [inst.strip().split('\t') for inst in fr.readlines()]
    # 得到数据的对应的 Labels
    lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
    # 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树
    lensesTree = createTree(lenses, lensesLabels)
    print(lensesTree)
    # 画图可视化展现
    createPlot(lensesTree)

if __name__ == "__main__":
    LensesTest()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

效果如下:
在这里插入图片描述

7. sklearn

这里简单了解一下其概念,之后有时间再具体学习。sklearn(Scikit-learn)是机器学习中常用的第三方模块,对常用的机器学习方法进行了封装,包括回归(Regression)、降维(Dimensionality Reduction)、分类(Classfication)、聚类(Clustering)等方法。sklearn库的结构如下图所示:可以看到和树很像
在这里插入图片描述
由图中可以看到sklearn库的算法主要有四类:分类,回归,聚类,降维。其中:

  • 回归:线性、决策树、SVM、KNN;集成回归:随机森林、Adaboost、GradientBoosting、Bagging、ExtraTrees
  • 分类:线性、决策树、SVM、KNN,朴素贝叶斯;集成分类:随机森林、Adaboost、GradientBoosting、Bagging、ExtraTrees
  • 聚类:k均值(K-means)、层次聚类(Hierarchical clustering)、DBSCAN
  • 降维:LinearDiscriminantAnalysis、PCA

例如sklearn提供了我们这次学习的决策树模型,用于解决分类和回归问题。

class sklearn.tree.DecisionTreeClassifier(criterion=’gini’, splitter=’best’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, class_weight=None, presort=False)[source]
  • 1

参数说明如下:

  • criterion:特征选择标准,可选参数,默认是gini,可以设置为entropy。gini是基尼不纯度,是将来自集合的某种结果随机应用于某一数据项的预期误差率,是一种基于统计的思想。entropy是香农熵,也就是上篇文章讲过的内容,是一种基于信息论的思想。Sklearn把gini设为默认参数,应该也是做了相应的斟酌的,精度也许更高些?ID3算法使用的是entropy,CART算法使用的则是gini。
  • splitter:特征划分点选择标准,可选参数,默认是best,可以设置为random。每个结点的选择策略。best参数是根据算法选择最佳的切分特征,例如gini、entropy。random随机的在部分划分点中找局部最优的划分点。默认的”best”适合样本量不大的时候,而如果样本数据量非常大,此时决策树构建推荐”random”。
  • max_features:划分时考虑的最大特征数,可选参数,默认是None。一般来说,如果样本特征数不多,比如小于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_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可以让划分点选择更加快,决策树建立的更加快。如果样本量太大的话,反而没有什么好处。问题是样本量少的时候,我速度本来就不慢。所以这个值一般懒得理它就可以了。

通常来说只需要调整其中较为重要的几个参数就行:

  • criterion:用以设置用信息熵还是基尼系数计算。
  • splitter:指定分支模式
  • max_depth:最大深度,防止过拟合
  • min_samples_leaf:限定每个节点分枝后子节点至少有多少个数据,否则就不分枝

sklearn.tree.DecisionTreeClassifier还提供了一些方法,其相应的作用如下图所示:
在这里插入图片描述
这里尝试使用sklearn构建上面隐形眼镜的决策树,根据上图中的函数,很容易写出下面的程序:

from sklearn import tree

if __name__ == "__main__":
    fr = open('lenses.txt')
    lenses = [inst.strip().split('/t') for inst in fr.readlines()]
    print(lenses)
    lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
    clf = tree.DecisionTreeClassifier()
    lenses = clf.fit(lenses, lensesLabels)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

但是运行报错:
在这里插入图片描述
这是因为在fit()函数不能接收string类型的数据,通过打印的信息可以看到,数据都是string类型的。在使用fit()函数之前,我们需要对数据集进行编码,这里可以使用两种方法:

  • 1)LabelEncoder:将字符串转换为增量值
  • 2)OneHotEncoder:使用One-of-K算法将字符串转换为整数
    为了对string类型的数据序列化,需要先生成pandas数据,这样方便我们的序列化工作。这里我使用的方法是,原始数据->字典->pandas数据,编写代码如下:
from sklearn import tree
import pandas as pd
from sklearn.preprocessing import LabelEncoder
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数据的临时列表
    lenses_list = []
    # 保存lenses数据的字典,用于生成pandas
    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)
    #生成pandas.DataFrame
    lenses_pd = pd.DataFrame(lenses_dict)
    print(lenses_pd)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

运行结果如下图所示:
在这里插入图片描述
然后通过LabelEncoder序列化数据:

 ......#省略程序代码和前面一样
    #print(lenses_dict)
    #生成pandas.DataFrame
    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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行结果如下:
在这里插入图片描述
然后接下来我们就要通过sklearn中的tree拟合数据,并通过tree.export_graphviz绘制决策树,完整代码如下:

import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn import tree
from six import StringIO
import pydotplus

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数据的临时列表
    lenses_list = []
    # 保存lenses数据的字典,用于生成pandas
    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)
    #生成pandas.DataFrame
    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") 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

运行后会生成pdf文件:
在这里插入图片描述
之后再有新数据需要预测的时候通过clf.predict()方法,例如print(clf.predict([[1,1,1,0]])),程序预测如下:
在这里插入图片描述
参考以下文章:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/1019174
推荐阅读
相关标签
  

闽ICP备14008679号