机器学习实战 -- Task01. 决策树

利用八周时间,完成以下任务:

  • 分类问题:K邻近算法
  • 分类问题:决策树
  • 分类问题:朴素贝叶斯
  • 分类问题:逻辑回归
  • 分类问题:支持向量机
  • 分类问题:AdaBoost
  • 回归问题:线性回归、岭回归、套索方法、逐步回归等
  • 回归问题:树回归
  • 聚类问题:K均值聚类
  • 相关问题:Apriori
  • 相关问题:FP-Growth
  • 简化数据:PCA主成分分析
  • 简化数据:SVD奇异值分解

一、算法概述

算法原理:

决策树模型是一种描述对实例进行分类的树形结构。其由结点(node)和有向(directed edge)组成。结点有两种类型:内部结点(internal node)和叶结点(leaf node)。内部结点表示一个特征或属性(features),叶结点表示一个类(labels)。

用决策树对需要测试的实例进行分类:从根节点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点;这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至达到叶结点。最后将实例分配到叶结点的类中。

决策树模型可以认为是 if-then 规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。

决策树学习通常包括 3 个步骤:特征选择、决策树的生成和决策树的修剪。

信息熵

  • 熵(entropy): 熵指的是体系的混乱的程度,在不同的学科中也有引申出的更为具体的定义,是各领域十分重要的参量。
  • 信息论(information theory)中的熵(香农熵): 是一种信息的度量方式,表示信息的混乱程度,也就是说:信息越有序,信息熵越低
  • 信息增益(information gain): 在划分数据集前后信息发生的变化称为信息增益。

划分数据集的原则是:将无序的数据变得更加有序。选择信息增益最大的特征来划分数据集。所用到的公式如下:

数据集的信息熵:

H ( D ) = − ∑ i = 0 N ∣ C k ∣ ∣ D ∣ l o g 2 ∣ C k ∣ ∣ D ∣ H(D)=-\sum_{i=0}^N{\frac {|C_k|}{|D|}log_2{{\frac {|C_k|} {|D|} } }} H(D)=i=0NDCklog2DCk

其中 C k C_k Ck表示集合 D D D中属于第 k k k类样本的样本子集。

针对某个特征 A A A ,对于数据集 D D D 的条件熵 H ( D ∣ A ) H(D|A) H(DA) 为:

H ( D ∣ A ) = ∑ i = 1 N ∣ D i ∣ ∣ D ∣ H ( D i ) H(D|A)=\sum_{i=1}^N{\frac{|D_i|}{|D|}}H(D_i) H(DA)=i=1NDDiH(Di)

其中 D i D_i Di 表示 D D D 中特征 A A A 取第 i i i 个值的样本子集。

信息增益 = 信息熵 - 条件熵:
G a i n ( D , A ) = H ( D ) − H ( D ∣ A ) Gain(D,A)=H(D)-H(D|A) Gain(D,A)=H(D)H(DA)

信息增益越大表示使用特征 A A A 来划分所获得的“纯度提升越大”。

  • 熵越高,则说明混合的数据也越多。
  • 得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集。
  • 信息增益是熵的减少或者是数据无序度的减少

如何构造一个决策树?

我们使用 createBranch() 方法,如下所示:

def createBranch():
'''
此处运用了迭代的思想。
'''
    检测数据集中的所有数据的分类标签是否相同:
        If so return 类标签
        Else:
            寻找划分数据集的最好特征(划分之后信息熵最小,也就是信息增益最大的特征)
            划分数据集
            创建分支节点
                for 每个划分的子集
                    调用函数 createBranch (创建分支的函数)并增加返回结果到分支节点中
            return 分支节点

二、实战案例

项目案例1:判定鱼类和非鱼类

项目概述

根据以下 2 个特征,将动物分成两类:鱼类和非鱼类。

  • 不浮出水面是否可以生存
  • 是否有脚蹼

开发流程

Step1:收集数据

机器学习实战 -- Task01. 决策树_第1张图片

Step2:准备数据

  • 不浮出水面是否可以生存: 1 代表 ”是“ ; 0 代表 ”否“

  • 是否有脚蹼:1 代表 ”是“ ; 0 代表 ”否“

  • 属于鱼类: yes 代表 ”是“ ; no 代表 ”否“

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

输出结果:
机器学习实战 -- Task01. 决策树_第2张图片

Step3:分析数据

可以使用任何方法,构造树完成之后,我们可以将树画出来。

计算给定数据集的香农熵的函数如下:

import math

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

    # 对于 label 标签的占比,求出 label 标签的香农熵
    shannoEnt=0.0
    for key in labelCounts:
        # 使用所有类标签的发生频率计算类别出现的概率。
        prob=float(labelCounts[key])/numEntries
        # 计算香农熵,以 2 为底求对数
        shannoEnt-=prob*math.log(prob,2)
    return shannoEnt

dataSet,label=creatDataSet()
print(calcShannonEnt(dataSet))

输出结果:
机器学习实战 -- Task01. 决策树_第3张图片

划分数据集:

按照给定特征划分数据集

将指定特征的特征值等于 value 的行剩下列作为子数据集。

#划分数据集
def spilitDataSet(dataSet,index,value):
    """
    splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行)
        就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
    Args:
        dataSet 数据集                 待划分的数据集
        index 表示每一行的index列        划分数据集的特征
        value 表示index列对应的value值   需要返回的特征的值。
    Returns:
        index列为value的数据集【该数据集需要排除index列】
    """
    # 创建返回的数据集列表
    retDataSet=[]
    # 遍历数据集
    for featVec in dataSet:
        # 判断index列的值是否为value
        if featVec[index]==value:
            # 去掉axis特征
            reduceFeatVec=featVec[:,index]
            # 将符合条件的添加到返回的数据集
            reduceFeatVec.extend(featVec[index+1,:])

接下来我们将遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。熵计算将会告诉我们如何划分数据集是最好的数据组织方式。

# 选择最好的数据集划分方式
def chooseBeastFeatureToSpilit(dataSet):
    """
    chooseBestFeatureToSplit(选择最好的特征)
    Args:
        dataSet 数据集
    Returns:
        bestFeature 最优的特征列
    """
    # 求第一行有多少列的 Feature, 最后一列是label列
    numFeatures=len(dataSet[0])-1
    # 数据集的香农熵
    baseEntropy=calcShannonEnt(dataSet)
    # 最优的信息增益值, 和最优的Featurn编号
    bestInfoGain, bestFeature = 0.0, -1
    # 记录信息增益和最优特征的索引值
    for i in range(numFeatures):
        # 获取对应的feature下的所有数据
        feaList=[example[i] for example in dataSet]
        # 获取剔重后的集合,使用set对list数据进行去重
        uniqleVals=set(feaList)
        # 创建一个临时的信息熵
        newEntropy=0.0
        # 遍历某一列的value集合,计算该列的信息熵
        # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
        for value in uniqleVals:
            subDataSet=spilitDataSet(dataSet,i,value)
            #计算概率
            prob=len(subDataSet)/float(len(dataSet))
            # 计算条件熵
            newEntropy+=prob*calcShannonEnt(subDataSet)
        # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
        # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
        infoGain=baseEntropy-newEntropy
        print('infoGain=', infoGain, '    bestFeature=', i,"   ", baseEntropy, '   ',newEntropy)
        if infoGain>bestFeature:
            bestInfoGain=infoGain
            bestFeature=i
    return bestFeature

dataSet,label=creatDataSet()
print(chooseBeastFeatureToSpilit(dataSet))

在上述chooseBestFeatureToSplit()函数中调用的dataSet需要满足一定的要求:

  • 数据必须是一种由列表元素组成的列表,且所有的列表元素都要具有相同的数据长度;
  • 数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。

输出结果:
机器学习实战 -- Task01. 决策树_第4张图片

Step4:训练算法

采用多数表决的方法决定该叶子节点的分类

#多数表决法
def majorityCnt(classList):
    """
   majorityCnt(选择出现次数最多的一个结果)
   Args:
       classList label列的集合
   Returns:
       bestFeature 最优的特征列
   """
    classCount={}
    # 统计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)
    # 返回classList中出现次数最多的元素
    return sortedClassCount[0][0]

该方法与第2章classify0部分的投票表决代码非常类似,该函数使用分类名称的列表,然后创建键值为classList中唯一值的数据字典,字典对象存储了classList中每个类标签出现的频率,最后利用operator操作键值排序字典,并返回出现次数最多的分类名称。

递归构建决策树

由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。

因此我们可以采用递归的原则处理数据集。

递归结束的条件是:

  • 程序遍历完所有划分数据集的属性
    同理,可以设置算法可以划分的最大分组数目
  • 或者每个分支下的所有实例都具有相同的分类
    如果所有实例具有相同的分类,则得到一个叶子节点或者终止块
    任何到达叶子节点的数据必然属于叶子节点的分类
#创建决策树
def creatTree(dataSet,labels):
    """
    创建决策树
    - - - -
    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)

    # 选择最优特征
    bestFeat=chooseBeastFeatureToSpilit(dataSet)
    # 最优特征的标签
    bestFeatLabel=labels[bestFeat]
    # 初始化myTree
    myTree={bestFeatLabel:{}}
    # 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
    # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
    # 删除已经使用特征标签
    del(labels[bestFeat])
    # 得到训练集中所有最优特征的属性值
    featValues = [example[bestFeat] for example in dataSet]
    # 去掉重复的属性值
    uniqueVals=set(featValues)
    # 遍历特征,创建决策树
    for value in uniqueVals:
        #复制类标签(保证每次调用函数createTree()时不改变原始列表的内容)
        subLabels = labels[:]
        # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
        myTree[bestFeatLabel][value] = creatTree(spilitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

myDat,labels=creatDataSet()
print(creatTree(myDat,labels))
#{'flippers': {0: 'no', 1: {'no surfacing': {0: 'no', 1: 'yes'}}}}

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

使用Matplotlib库创建树形图

# author:answer   time:2019/11/23
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from 决策树 import jc1

#获取决策树叶子结点的数目
def getNumLeafs(myTree):
    """
    获取叶节点的数目
    - - - -
    myTree - 决策树
    """
    # 初始化叶子
    numleafs=0
    # 获取结点属性
    firstStr=list(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):
    """
    获取叶节点的深度
    - - - -
    myTree - 决策树
    """
    # 初始化决策树深度
    maxDepth=0
    firstStr=list(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 plotNode(nodeTxt,centerPt,parentPt,nodeType):
    """
    绘制结点
    - - - -
    nodeTxt - 结点名
    centerPt - 文本位置
    parentPt - 标注的箭头位置
    nodeType - 结点格式
    """
    # 定义箭头格式
    arrow_args=dict(arrowstyle="<-")
    # 设置中文字体
    font = FontProperties(fname=r'C:/Windows/Fonts/simsun.ttc', size=16)
    # 绘制节点
    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 plotMidText(cntrPt, parentPt, txtString):
    """
    计算在父子节点间填充文本信息的位置
    - - - -
    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):
    """
    递归绘制决策树
    - - - -
    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]
    # y偏移
    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
    fig = plt.figure(1, facecolor='white')
    # 清空fig
    fig.clf()
    axprops = dict(xticks=[], yticks=[])
    # 去掉x、y轴
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
    # 获取决策树叶结点数目
    plotTree.totalW = float(getNumLeafs(inTree))
    # 获取决策树层数
    plotTree.totalD = float(getTreeDepth(inTree))
    # x偏移
    plotTree.xOff = -0.5/plotTree.totalW;
    # y偏移
    plotTree.yOff = 1.0;
    # 绘制决策树
    plotTree(inTree, (0.5,1.0), '')
    plt.show()

if __name__ == "__main__":
    dataSet,labels=jc1.creatDataSet()
    myTree=jc1.creatTree(dataSet,labels)
    print(myTree)
    createPlot(myTree)

输出结果:
机器学习实战 -- Task01. 决策树_第5张图片

Step5:测试算法

存储决策树

#使用pickle模块存储决策树
def storeTree(inputTree,filaname):
    import pickle
    fw=open(filaname,'wb')
    pickle.dump(inputTree,fw)
    fw.close()

def grabTree(filename):
    import  pickle
    fr=open(filename,'rb')
    return pickle.load(fr)

if __name__=='__main__':
    dataSet,labels=creatDataSet()
    labelsCopy=labels[:]
    myTree=creatTree(dataSet,labels)
    print(myTree)
    storeTree(myTree,'classifierStorage.pkl')
    tree=grabTree('classifierStorage.pkl')
    print(tree)

输出结果:

机器学习实战 -- Task01. 决策树_第6张图片

使用决策树执行分类

#使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
    # 获取tree的根节点对于的key值
    firstStr=list(inputTree.keys())[0]
    # 通过key得到根节点对应的value
    secondDict=inputTree[firstStr]
    # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
    featIndx=featLabels.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndx]==key:
            if type(secondDict[key]).__name__=='dict':
                classLabel=classify(secondDict[key],featLabels,testVec)
            else:classLabel=secondDict[key]
    return classLabel

Step6:使用算法

if __name__=='__main__':
    dataSet,labels=creatDataSet()
    labelsCopy=labels[:]
    myTree=creatTree(dataSet,labels)
    print("[0,0]  ",classify(myTree,labelsCopy,[0,0]))
    print("[0,1]  ",classify(myTree,labelsCopy,[0,1]))
    print("[1,0]  ",classify(myTree,labelsCopy,[1,0]))
    print("[1,1]  ",classify(myTree,labelsCopy,[1,1]))

输出结果:

机器学习实战 -- Task01. 决策树_第7张图片

项目案例2:使用决策树预测隐形眼镜类型

项目概述

隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。我们需要使用决策树预测患者需要佩戴的隐形眼镜类型。

开发流程

Step1:收集数据

提供的文本文件:

文本文件数据格式如下:

机器学习实战 -- Task01. 决策树_第8张图片

Step2:准备数据

解析tab键分隔的数据行

if __name__=='__main__':
    # 加载隐形眼镜相关的 文本文件 数据
    fr=open('lenses.txt')
    # 解析数据,获得 features 数据
    lenses = [inst.strip().split('\t') for inst in fr.readlines()]
    # 得到数据的对应的 Labels
    lensesLabels=['age', 'prescript', 'astigmatic', 'tearRate']

Step3:分析数据

快速检查数据,确保正确地解析数据内容,使用createPlot()函数绘制
最终的树形图

同案例1,计算香农熵、按照给定的特征划分数据集、选择最好的数据集划分方式。

Step4:训练算法

使用createTree()函数

 # 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树
    lensesTree = creatTree(lenses, lensesLabels)
    storeTree(lensesTree, 'classifierLenes.pkl')
    lensesTree = grabTree('classifierLenes.pkl')
    print(lensesTree)

输出结果:

机器学习实战 -- Task01. 决策树_第9张图片

Step5:测试算法

编写测试函数验证决策树可以正确分类给定的数据实例

Step6:使用算法

存储树的数据结构,以便下次使用时无需重新构造树

if __name__=='__main__':
    # 加载隐形眼镜相关的 文本文件 数据
    fr=open('lenses.txt')
    # 解析数据,获得 features 数据
    lenses = [inst.strip().split('\t') for inst in fr.readlines()]
    # 得到数据的对应的 Labels
    lensesLabels=['age', 'prescript', 'astigmatic', 'tearRate']
    # 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树
    lensesTree = creatTree(lenses, lensesLabels)
    #储存决策树
    storeTree(lensesTree, 'classifierLenes.pkl')
    print(lensesTree)
    jc2.createPlot(lensesTree)

输出结果:

机器学习实战 -- Task01. 决策树_第10张图片

三、总结

决策树的一些优点:

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

决策树的一些缺点:

  • 决策树学习可能创建一个过于复杂的树,并不能很好的预测数据。也就是过拟合。修剪机制(现在不支持),设置一个叶子节点需要的最小样本数量,或者数的最大深度,可以避免过拟合。
  • 决策树可能是不稳定的,因为即使非常小的变异,可能会产生一颗完全不同的树。这个问题通过decision trees with an ensemble来缓解。
  • 学习一颗最优的决策树是一个NP-完全问题under several aspects of optimality and even for simple concepts。因此,传统决策树算法基于启发式算法,例如贪婪算法,即每个节点创建最优决策。这些算法不能产生一个全家最优的决策树。对样本和特征随机抽样可以降低整体效果偏差。
  • 概念难以学习,因为决策树没有很好的解释他们,例如,XOR, parity or multiplexer problems.
  • 如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,建议在训练之前,先抽样使样本均衡。

参考文章
https://blog.csdn.net/lsgo_myp/article/details/103079044
https://blog.csdn.net/c406495762/article/details/76262487
https://github.com/apachecn/AiLearning/tree/master/docs/ml

你可能感兴趣的:(机器学习,决策树)