机器学习实战 第三章

本文详细介绍了决策树算法,包括算法概述、信息增益、数据集划分、递归构建决策树的过程,以及如何通过matplotlib进行可视化。文章还提供了代码示例,展示了如何计算熵、选择最佳特征、创建和可视化决策树。此外,讨论了分类器的测试和存储,强调了决策树在处理分类问题中的应用和优缺点。
摘要由CSDN通过智能技术生成

1.算法概述

决策树,就像它的名字一样,利用决策的方法形成的一棵树用来处理数据,它的工作原理与“二十个问题”的小游戏类似,都是通过层层分级缩小范围最后给出答案。

用判断邮件类别举个例子:判断一封邮件类别时,首先看它的发送邮件域名地址,若是’‘myEmployer.com’'则判断为无聊时阅读的邮件;否则进行下一层判断,根据是否包含"曲棍球"这个词来判断是需要及时处理的朋友邮件还是垃圾邮件
在这里插入图片描述
以上图像中,长方形的代表判断模块,椭圆形代表终止模块,左右箭头称作分支,这样就是一个决策树的基本框架

2.算法原理

信息增益

再深入研究该算法前,我们首先要对数据进行划分,这里采用ID3的算法来划分数据。同时,用信息增益来判断划分数据前后的变化好坏,使用信息增益最好的特征来对数据进行第一级的分层, 而信息增益的度量方式称作

有多个待分类的信息 x i x_i xi定义为 l ( x i ) = − log ⁡ 2 p ( x i ) l(x_i)=-\log_2p(x_i) l(xi)=log2p(xi)其中 p ( x i ) p(x_i) p(xi)是选择某分类的概率,而所有类别可能包含的信息期望值定义为 H = − ∑ i = 1 n p ( x i ) log ⁡ 2 p ( x i ) H=-\sum_{i=1}^np(x_i)\log_2p(x_i) H=i=1np(xi)log2p(xi)下面是计算给定数据集的熵的代码:

# 导入数学计算的库
from math import log

def calcShannonEnt(dataSet):
    # 获取数据集的元素数量
    numEntries = len(dataSet)
    # 创建一个空字典,用于统计每个类别的出现次数
    labelCounts = {}
    # 遍历数据集,统计每个类别的出现次数
    for featVec in dataSet:
        # 获取当前特征的标签
        currentLabel = featVec[-1]
        # 如果当前标签没有在字典中,初始化为0
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        # 将当前标签的出现次数加1
        labelCounts[currentLabel] += 1
    # 计算香农熵
    shannonEnt = 0.0
    # 遍历字典中的每个标签
    for key in labelCounts:
        # 计算当前标签的概率(出现次数除以总元素数)
        prob = float(labelCounts[key]) / numEntries
        # 计算香农熵(取对数并转换为2为底的对数)
        shannonEnt -= prob * log(prob, 2)
    # 返回香农熵
    return shannonEnt

我们先创建一个数据集函数createDataSet

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

而后用calcShannonEnt函数来测试以上数据集的熵

请添加图片描述
上述就是先对原始数据集计算熵,而后将第一个数据的标签’yes’改成’maybe’,可以看到再次计算的熵明显增大了,这也说明了数据变得更加“不稳定”了

划分数据集

决策树是种分类算法,而分类算法除了要测量信息熵,还要划分数据集和划分数据集的熵,以此判断是否正确划分了数据集,首先利用def splitDataSet函数按照给定特征划分数据集

def splitDataSet(dataSet, axis, value):
    # 创建一个空列表,用于存储分割后的数据
    retDataSet = []
    # 遍历每一个数据点
    for featVec in dataSet:
        # 如果当前数据点的某一轴值等于给定的值
        if featVec[axis] == value:
            # 获取当前数据点在当前轴上的起始和结束位置,然后将该轴上的值删去
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            # 将分割后的数据添加到结果列表中
            retDataSet.append(reducedFeatVec)
    # 返回分割后的数据集
    return retDataSet

这个函数会遍历输入的数据集,然后对于每一个数据点,如果其某一轴的值等于指定的值,就将这个轴上的值删去,并将剩余的部分加入到结果列表中。最终返回结果列表

例如,对于原始数据集,按照第0位是1的划分办法,能得到第八行的输出,第九行同理
在这里插入图片描述
之后我们能够用以下代码选择出最好的数据集划分方式

def chooseBestFeatureToSplit(dataSet):
    # 特征的数量就是数据集的长度减1,因为最后一个特征总是类别标签
    numFeatures = len(dataSet[0]) - 1     
    # 计算数据集的香农熵
    baseEntropy = calcShannonEnt(dataSet)     
    # 初始化最优特征和对应的信息增益
    bestInfoGain = 0.0; bestFeature = -1
    # 遍历每个特征,对于每个特征i:
    for i in range(numFeatures):       
        # 根据特征i将数据集划分为若干子集
        # example[i]是第i个特征对应的每个值,例如['A', 'A', 'B', 'B', 'B']
        featList = [example[i] for example in dataSet]
        # 生成一个只包含每个特征i的不同取值的集合
        uniqueVals = set(featList)       
        # 计算每个子集的香农熵
        newEntropy = 0.0
        # 对于每个特征i的不同取值value:
        for value in uniqueVals:
            # 根据特征i和value将数据集划分为若干子集
            subDataSet = splitDataSet(dataSet, i, value)
            # 计算子集的概率
            prob = len(subDataSet) / float(len(dataSet))
            # 将子集的香农熵累加到新的香农熵中
            newEntropy += prob * calcShannonEnt(subDataSet)
            # 计算信息增益,信息增益越大,特征i的信息增益比越高
        infoGain = baseEntropy - newEntropy        
        # 如果特征i的信息增益比当前最优的特征信息增益大
        if (infoGain > bestInfoGain):      
            # 更新最优特征和对应的信息增益
            bestInfoGain = infoGain            
            bestFeature = i
	# 返回最优特征                        
    return bestFeature     

递归构建决策树

根据决策树的算法原理,采用递归的方法最合适,首先用以下代码计算给定列表中最多数的的类别

def majorityCnt(classList):
    # 使用字典存储每个类别出现的次数
    classCount = {}
    # 遍历给定列表中的每个元素
    for vote in classList:
        # 如果当前元素未在字典中出现,则将其添加到字典中并设置为0
        if vote not in classCount.keys(): classCount[vote] = 0
        # 更新字典中当前元素对应的值
        classCount[vote] += 1
    # 对字典进行排序,并返回出现次数最多的类别
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

然后是创建决策树的代码

def createTree(dataSet, labels):
    # 获取所有特征和对应的标签
    classList = [example[-1] for example in dataSet]
    # 检查数据集是否只有一类标签
    if classList.count(classList[0]) == len(classList):
        # 如果数据集只有一类标签,直接返回该类标签
        return classList[0]
    # 获取数据集的最小特征值
    featLabels = labels[:]
    # 选择具有最小特征值的数据集
    bestFeat = chooseBestFeatureToSplit(dataSet)
    # 获取最小特征值的标签
    bestFeatLabel = labels[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)
    return myTree

createTree 函数用于创建决策树,该函数根据给定的特征值和标签列表(labels)来划分数据集。它会递归地调用自身,直到数据集被完全划分。下面就是利用createTree 函数进行数据集的划分,首先是对no surfacing特征划分,若为0则是no,否则再对值为1的数据进行flippers特征的划分,若为0则是no,否则是yes
请添加图片描述

可视化决策树

首先确定一个树形图的框和箭头形式和一些注释,再基本形成一个树的框架

import matplotlib.pyplot as plt

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)

def createPlot():
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
    plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)
    plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
    plt.show()

通过以上代码能够得到下面这样的图,这样两个不同形态的树节点就绘制出来了
请添加图片描述

但是这样远远不够,我们需要知道有多少个叶节点以及树的深度,以便确定图像的x、y轴长度,因此创建了getNumLeafsgetTreeDepth两个函数来计算两者的值

def getNumLeafs(myTree):
    # 定义函数 getNumLeafs,输入参数为二叉树
    # 返回结果为二叉树的叶子节点数量
    numLeafs = 0
    # 初始化叶子节点数量
    firstStr = list(myTree)[0]
    # 获取二叉树的第一个节点
    secondDict = myTree[firstStr]
    # 获取二叉树的第一个节点的子节点
    for key in secondDict.keys():
        # 遍历第二个节点的所有键
        if type(secondDict[key]).__name__ == 'dict':
            # 如果第二个节点的子节点类型为字典,则递归调用 getNumLeafs 函数来计算该子节点的叶子节点数量,并将结果添加到 numLeafs 中
            numLeafs += getNumLeafs(secondDict[key])
        else:
            # 如果第二个节点的子节点类型不是字典,则直接将该子节点添加到 numLeafs 中
            numLeafs += 1
    # 返回二叉树的叶子节点数量
    return numLeafs

def getTreeDepth(myTree):
    # 定义函数 getTreeDepth,输入参数为二叉树
    # 返回结果为二叉树的深度,即树中最长路径的长度
    maxDepth = 0
    # 初始化树的深度
    firstStr = list(myTree)[0]
    # 获取二叉树的第一个节点
    secondDict = myTree[firstStr]
    # 获取二叉树的第一个节点的子节点
    for key in secondDict.keys():
        # 遍历第二个节点的所有键
        if type(secondDict[key]).__name__ == 'dict':
            # 如果第二个节点的子节点类型为字典,则递归调用 getTreeDepth 函数来计算该子节点的深度,并将结果添加到 maxDepth 中
            maxDepth = max(maxDepth, getTreeDepth(secondDict[key]))
        else:
            # 如果第二个节点的子节点类型不是字典,则将该子节点的深度加1,并将其添加到 maxDepth 中
            maxDepth = max(maxDepth, 1)
    # 返回二叉树的深度
    return maxDept

getNumLeafs函数定义了一个递归方法来计算二叉树的叶子节点数量。它首先创建一个空的列表 numLeafs,并将其初始值设置为0。然后,它遍历二叉树的第一个节点,即列表的第一个元素。对于每个子节点,如果其类型为字典,则调用函数 getNumLeafs 来计算该字典的叶子节点数量,并将结果添加到 numLeafs 中。如果子节点的类型不是字典,则直接将其添加到 numLeafs 中。最后,返回 numLeafs 的值作为二叉树的叶子节点数量

getTreeDepth定义了一个递归方法来计算二叉树的深度。它首先创建一个空的列表 maxDepth,并将其初始值设置为0。然后,它遍历二叉树的所有节点,并计算从根节点到每个节点的最长路径的长度。对于每个节点,它计算该节点的子节点的最长路径长度,并将其加1,如果子节点的数量为1,则将其添加到 maxDepth 中。最后,返回 maxDepth 的值作为二叉树的深度

创造一个retrieveTree(i)函数输出预先存储的树信息

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]

调用getNumLeafsgetTreeDepth函数能够得到该数据形成的树的叶数和深度
请添加图片描述

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)

import matplotlib.pyplot as plt

def plotTree(myTree, parentPt, nodeTxt):
	# 得到树的叶子数量和深度
    numLeafs = getNumLeafs(myTree)  
    depth = getTreeDepth(myTree)
    # 计算父节点的坐标
    cntrPt = (plotTree.xOff + (1.0 + float(getNumLeafs(myTree)))/2.0/plotTree.totalW, plotTree.yOff)
    # 在父节点的中心位置添加文本
    plotMidText(cntrPt, parentPt, nodeTxt)
    # 在父节点的位置绘制子节点
    plotNode(myTree[nodeTxt], cntrPt, parentPt, decisionNode)
    # 递归地绘制子树
    secondDict = myTree[nodeTxt]
    # 计算下一个节点的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 = plt.figure(1, facecolor='white')
    fig.clf()
    # 创建一个子图,并设置其边框可见性为False
    axprops = dict(xticks=[], yticks=[])
    # 创建一个新的子图,并设置其边框可见性为False
    createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)   
    #createPlot.ax1 = plt.subplot(111, frameon=False) 
    # 得到树的叶子数量和深度
    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.show()

最后得到如下所示的树形图
请添加图片描述

测试和存储分类器

使用决策树分类

# 定义函数,接受特征树、标签向量和测试向量作为输入
def classify(inputTree, featLabels, testVec):
    # 获取输入特征树的第一个元素(键)
    firstStr = list(inputTree)[0]
    # 获取输入特征树的第一个元素对应的字典(键-值对)
    secondDict = inputTree[firstStr]
    # 获取标签向量中输入特征树第一个元素对应的索引(键)
    featIndex = featLabels.index(firstStr)
    # 获取测试向量中输入特征树第一个元素对应的值(键)
    key = testVec[featIndex]
    # 如果第二个字典(键-值对)是一个字典,则对其进行分类
    if isinstance(secondDict, dict):
        # 获取第二个字典中输入特征树第一个元素对应的值(键)
        valueOfFeat = secondDict[key]
        # 如果第二个字典中输入特征树第一个元素对应的值(键)也是一个字典,则继续对其进行分类
        if isinstance(valueOfFeat, dict):
            # 获取第二个字典中输入特征树第一个元素对应的值(键)中的测试向量中输入特征树第一个元素对应的值(键)
            classLabel = classify(valueOfFeat, featLabels, testVec)
        else:
            classLabel = valueOfFeat
    # 否则,直接返回输入特征树第一个元素对应的值(键)
    else: classLabel = secondDict[key]
    # 返回分类结果(标签向量中输入特征树第一个元素对应的值)
    return classLabel

函数classify用于对输入的特征树和标签向量进行分类,从下图可以看到在flippers判断节点中进行递归调用,从而形成一棵树

请添加图片描述
每次使用分类器时需要重新构造决策树,我们可以通过存储决策树减少构造决策树的时间,以下是存储和加载分类器模型的两个函数

# 定义函数,用于存储分类器模型
def storeTree(inputTree, filename):
    # 导入pickle模块
    import pickle
    # 使用'wb'模式打开文件,以写入二进制数据
    fw = open(filename, 'wb')
    # 使用pickle.dump函数将输入特征树保存到文件中
    pickle.dump(inputTree, fw)
    # 关闭文件
    fw.close()

# 定义函数,用于加载分类器模型
def grabTree(filename):
    # 导入pickle模块
    import pickle
    # 使用'rb'模式打开文件,以读取二进制数据
    fr = open(filename, 'rb')
    # 使用pickle.load函数从文件中读取输入特征树
    return pickle.load(fr)

通过上述代码可以将分类器存储到硬盘上,不用每次重新学习并构造一遍

3.算法示例

本次示例使用决策树来对患者需要佩戴的隐形眼镜进行预测

基本流程

  1. 收集数据:提供含有数据的文件,如txt、xlsx、csv等
  2. 准备数据:使用Python解析数据文件
  3. 分析数据:快速检查数据,确保正确解析数据内容,使用createPlot函数绘制最终的树形图
  4. 训练算法:利用createTree函数
  5. 测试算法:编写测试函数验证决策树可以正确分类给定的数据实例
  6. 使用算法:存储树的结构,以便下次再使用

具体使用

请添加图片描述
能够看到以上先给出了不同的特征值,再使用createTree函数创建决策树,并用createPlot函数进行可视化

4.总结

本章学习了决策树的相关知识,决策树算法(Decision Tree Algorithm)是一种常用的分类和回归方法。它的基本原理是通过递归地划分数据集,从而构建一个类似于树状结构的模型。从算法的前期准备工作到实际操作都有了更详尽的了解。在对数据集进行划分时,主要是依靠熵来寻找最优方案划分数据集,直到数据集中的所有数据属于同一分类

同时由于文字并不能很好的直观体现出决策树的情况,因此使用matplotlib来对产生的结果进行可视化操作,在过程中也要注意叶子节点和树深度对于决策树影响,比如停止条件可以是达到预设的深度、节点中的样本数达到某个阈值等,调整好合适的参数才能使树的形状更好看

为了防止过拟合,对决策树进行剪枝是很有必要的,剪枝的主要目标是降低树的复杂度,提高泛化能力,决策树算法既可以用于分类问题,也可以用于回归问题。对于分类问题,树的叶子节点包含一个类标签;对于回归问题,叶子节点包含一个连续数值

决策树算法的优点是易于理解和解释,对于非线性和高维数据集具有较好的泛化能力。然而,它也存在一些缺点,如易受噪声影响、过拟合风险较高等

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值