《机器学习实战》-03 决策树

说明:

作业的所有代码都要基于Python3。
学习大纲:https://blog.csdn.net/qq_34243930/article/details/84669684
(所有计划均在学习大纲里)

03 决策树(第二周)

3.1 决策树的构造
3.2 在 Python 中使用 Matplotlib 注解绘制树形图
3.3 测试和存储分类器
3.4 示例:使用决策树预测隐形眼镜类型

第一节学习内容
学习时间: 12/09-12/10
任务1题目: 书籍阅读
任务详解: 阅读《机器学习实战》书籍第三章3.1、3.3、3.4节(3.2节选做)
参考文献: 李航《统计学习方法》第5章中的5.1-5.3节

作业1: 概括决策树分类算法的原理。
作业2: 在构建一个决策树模型时,我们对某个属性分割节点,下面四张图中,哪个属性对应的信息增益最大?
在这里插入图片描述
作业3: 将本章中“使用决策树预测隐形眼镜类型”完整代码键入,并添加详细注释。

第一节学习内容笔记

《第3章-决策树》

在这里插入图片描述
你是否玩过二十个问题的游戏,游戏的规则很简单:参与游戏的一方在脑海里想某个事物,其他参与者向他提问题,只允许提20个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围。
决策树的工作原理与20个问题类似,用户输入一系列数据,然后给出游戏的答案。
我们经常使用决策树处理分类问题,近来的调查表明决策树也是最经常使用的数据挖掘算法
图3-1所示的流程图就是一个决策树,长方形代表判断模块(decision block),椭圆形代表终止模块(terminating block),表示已经得出结论,可以终止运行。从判断模块引出的左右箭头称作分支(branch),它可以到达另一个判断模块或者终止模块。
图3-1构造了一个假想的邮件分类系统,它首先检测发送邮件域名地址。如果地址为myEmployer,com,则将其放在分类“无聊时需要阅读的邮件”中。如果邮件不是来自这个域名,则检查邮件内容里是否包含单词曲棍球,如果包含则将邮件归类到“需要及时处理的朋友邮件”,如果不包含则将邮件归类到“无需阅读的垃圾邮件”。
第2章介绍的k-近邻算法可以完成很多分类任务,但是它最大的缺点就是无法给出数据的内在含义,决策树的主要优势就在于数据形式非常容易理解。
本章构造的决策树算法能够读取数据集合,构建类似于图3-1的决策树。决策树的一个重要任务是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,在这些机器根据数据集创建规则时,就是机器学习的过程。专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。
在这里插入图片描述
现在我们已经大致了解了决策树可以完成哪些任务,接下来我们将学习如何从一堆原始数据中构造决策树。首先我们讨论构造决策树的方法,以及如何编写构造树的Python代码;接着提出一些度量算法成功率的方法;最后使用递归建立分类器,并且使用Matplotlib绘制决策树图。构造完成决策树分类器之后,我们将输入一些隐形眼镜的处方数据,并由决策树分类器预测需要的镜片类型。

3.1 决策树的构造

在这里插入图片描述
本节将通过算法一步步地构造决策树,并会涉及许多有趣的细节。首先我们讨论数学上如何使用信息论划分数据集,然后编写代码将理论应用到具体的数据集上,最后编写代码构建决策树。
在构造决策树时,我们需要解决的第一个问题就是,当前数据集上哪个特征在划分数据分类时起决定性作用。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。
完成测试之后,原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则当前无需阅读的垃圾邮件已经正确地划分数据分类,无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。
创建分支的伪代码函数 createBranch() 如下所示:
在这里插入图片描述
上面的伪代码 createBranch 是一个递归函数,在倒数第二行直接调用了它自己。后面我们将把上面的伪代码转换为Python代码,这里我们需要进一步了解算法是如何划分数据集的。
在这里插入图片描述
一些决策树算法采用二分法划分数据,本书并不采用这种方法。如果依据某个属性划分数据将会产生4个可能的值,我们将把数据划分成四块,并创建四个不同的分支。本书将使用ID3算法划分数据集,该算法处理如何划分数据集,何时停止划分数据集(进一步的信息可以参见 http://en.wikipedia.org/wiki/ID3_algorithm )。每次划分数据集时我们只选取一个特征属性,如果训练集中存在20个特征,第一次我们选择哪个特征作为划分的参考属性呢?
表3-1的数据包含5个海洋动物,特征包括:不浮出水面是否可以生存,以及是否有脚蹼。我们可以将这些动物分成两类:鱼类和非鱼类。现在我们想要决定依据第一个特征还是第二个特征划分数据。 在回答这个问题之前,我们必须采用量化的方法判断如何划分数据。下一小节将详细讨论这个问题。
在这里插入图片描述

3.1.1 信息增益

划分数据集的大原则是:将无序的数据变得更加有序。我们可以使用多种方法划分数据集,但是每种方法都有各自的优缺点。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。我们可以在划分数据之前或之后使用信息论量化度量信息的内容。
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
在可以评测哪种数据划分方式是最好的数据划分之前,我们必须学习如何计算信息增益。集合信息的度量方式称为香农熵或者简称为熵,这个名字来源于信息论之父克劳德·香农。
信息增益(information gain)和熵(entropy)
信息的定义
如果待分类的事务可能划分在多个分类之中,则符号 x i x_{i} xi的信息定义为:
l ( x i ) = − log ⁡ 2 p ( x i ) l(x_{i})= -\log_{2}p(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}^{n} p(x_{i})\log_{2}p(x_{i}) H=i1np(xi)log2p(xi),其中n是分类的数目。

下面我们将学习如何使用Python计算信息熵,创建名trees.py的文件,将程序清单3-1的代码内容录入到trees.py文件中,此代码的功能是计算给定数据集的熵
在这里插入图片描述

def calcShannonEnt(dataSet):
    """
    计算给定数据集的香农熵
    :param dataSet:
    :return:
    """
    numEntries = len(dataSet)       # 数据集中实例的总数
    labelCounts = {}                # 创建一个数据字典,它的键值是最后一列类别出现的次数
    for featVec in dataSet:
        currentLabel = featVec[-1]  # currentLabel记录当前实例类别
        if currentLabel not in labelCounts.keys():  # 如果当前键值不存在
            labelCounts[currentLabel] = 0           # 则扩展字典并将当前键值加入字典
        labelCounts[currentLabel] += 1              # 次数+1

    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries   # 计算类别出现的概率
        shannonEnt -= prob * log(prob, 2)           # 以2为底求对数(累加值)
    return shannonEnt

【1】首先,计算数据集中实例的总数。声明一个变量保存
实例总数。
【2】然后,创建一个数据字典,它的键值是最后一列的数值 。如果当前键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。
【3】最后,使用所有类标签的发生频率计算类别出现的概率。我们将用这个概率计算香农熵 ,统计所有类标签发生的次数。
在这里插入图片描述

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
"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import trees


myDat, labels = trees.createDataSet()
print(myDat)
print(trees.calcShannonEnt(myDat))

在这里插入图片描述
熵越高,则混合的数据也越多,我们可以在数据集中添加更多的分类,观察熵是如何变化的。这里我们增加第三个名为 maybe 的分类,测试熵的变化:
在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import trees


myDat, labels = trees.createDataSet()
myDat[0][-1] = 'maybe'
print(myDat)
print(trees.calcShannonEnt(myDat))

在这里插入图片描述
得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集,下一节我们将具体学习如何划分数据集以及如何度量信息增益。

3.1.2 划分数据集

上节我们学习了如何度量数据集的无序程度。
分类算法除了需要测量信息熵,还需要划分数据集度量划分数据集的熵,以便判断当前是否正确地划分了数据集。
我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式
想象一个分布在二维空间的数据散点图,需要在数据之间划条线,将它们分成两部分,我们应该按照x轴还是y轴划线呢?答案就是本节讲述的内容。
在这里插入图片描述

def splitDataSet(dataSet, axis, value):
    """
    按照给定特征划分数据集
    :param dataSet:待划分的数据集
    :param axis:划分数据集的特征
    :param value:需要返回的特征的值
    """
    retDataSet = []                 # 创建一个新的列表对象
    for featVec in dataSet:
        if featVec[axis] == value:  # 将符合特征的数据抽取出来
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

程序清单3-2的代码使用了三个输入参数:待划分的数据集、划分数据集的特征、需要返回的特征的值。
【1】需要注意的是,Python语言不用考虑内存分配问题。Python语言在函数中传递的是列表的引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生存周期。为了消除这个不良影响,我们需要在函数的开始声明一个新列表对象。因为该函数代码在同一数据集上被调用多次,为了不修改原始数据集,创建一个新的列表对象 。
【2】数据集这个列表中的各个元素也是列表,我们要遍历数据集中的每个元素,一旦发现符合要求的值,则将其添加到新创建的列表中。
【3】在 if 语句中,程序将符合特征的数据抽取出来 。后面讲述得更简单,这里我们可以这样理解这代码:当我们按照某个特征划分数据集时,就需要将所有符合要求的元素抽取出来。

在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import trees


myDat, labels = trees.createDataSet()
print(myDat)
print(trees.splitDataSet(myDat, 0, 1))  # 抽取,特征[0]值为1
print(trees.splitDataSet(myDat, 0, 0))  # 抽取,特征[0]值为0

在这里插入图片描述
接下来我们将遍历整个数据集,循环计算香农熵和splitDataSet() 函数,找到最好的特征划分方式。熵计算将会告诉我们如何划分数据集是最好的数据组织方式。
在这里插入图片描述
程序清单3-3给出了函数 chooseBestFeatureToSplit() 的完整代码,该函数实现选取特征,划分数据集,计算得出最好的划分数据集的特征。函数 chooseBestFeatureToSplit() 使用了程序清单3-1和程序清单3-2中的函数。在函数中调用的数据需要满足一定的要求:第一个要求是,数据必须是一种由列表元素组成的列表,而且所有的列表元素都要具有相同的数据长度
第二个要求是,数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。数据集一旦满足上述要求,我们就可以在函数的第一行判定当前数据集包含多少特征属性。我们无需限定list中的数据类型,它们既可以是数字也可以是字符串,并不影响实际计算。

def chooseBestFeatureToSplit(dataSet):
    """
    选择最好的数据集划分方式
    :param dataSet: 数据集
    :return: 返回最好特征划分的索引值
    """
    numFeatures = len(dataSet[0]) - 1      # len(dataSet[0]) 计算列数(即特征数),-1是因为最后一列是类别标签
    baseEntropy = calcShannonEnt(dataSet)  # 整个数据集的原始香农熵
    bestInfoGain = 0.0   # 初始化最好信息增益
    bestFeature = -1     # 初始化最好特征划分的索引值
    for i in range(numFeatures):        # 循环遍历数据集中的所有特征
        featList = [example[i] for example in dataSet]   # 将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中
        uniqueVals = set(featList)      # 集合类型中的每个值互不相同,得到列表中唯一元素值
        newEntropy = 0.0
        for value in uniqueVals:        # 遍历当前特征中的所有唯一属性值
            subDataSet = splitDataSet(dataSet, i, value)     # 对当前特征numFeatures的每个值value划分一次数据集
            prob = len(subDataSet)/float(len(dataSet))       # 选择当前特征的当前值的概率
            newEntropy += prob * calcShannonEnt(subDataSet)  # 计算数据集的新熵值,并对所有唯一特征值得到的熵求和
        infoGain = baseEntropy - newEntropy     # 信息增益是熵的减少
        if infoGain > bestInfoGain:
            bestInfoGain = infoGain
            bestFeature = i                 # 记录最好特征划分的索引值
    return bestFeature                      # 返回最好特征划分的索引值

【1】在开始划分数据集之前,程序清单3-3的第3行代码计算了整个数据集的原始香农熵,我们保存最初的无序度量值,用于与划分完之后的数据集计算的熵值进行比较。
【2】第1个 for 循环遍历数据集中的所有特征。使用列表推导(List Comprehension)来创建新的列表,将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中 。
【3】然后使用Python语言原生的集合(set)数据类型。集合数据类型与列表类型相似,不同之处仅在于集合类型中的每个值互不相同。从列表中创建集合是Python语言得到列表中唯一元素值的最快方法。
【4】遍历当前特征中的所有唯一属性值,对每个特征划分一次数据集 ,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
【5】信息增益是熵的减少或者是数据无序度的减少,大家肯定对于将熵用于度量数据无序度的减少更容易理解。最后,比较所有特征中的信息增益,返回最好特征划分的索引值 。
在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import trees


myDat, labels = trees.createDataSet()
print(myDat)
print(trees.chooseBestFeatureToSplit(myDat))

在这里插入图片描述
代码运行结果告诉我们,第0个特征是最好的用于划分数据集的特征。结果是否正确呢?这个结果又有什么实际意义呢?
数据集中的数据来源于表3-1,让我们回头再看一下表1-1。如果我们按照第一个特征属性划分数据,也就是说第一个特征是1的放在一个组,第一个特征是0的放在另一个组,数据一致性如何?按照上述的方法划分数据集,第一个特征为1的海洋生物分组将有两个属于鱼类,一个属于非鱼类;另一个分组则全部属于非鱼类。
如果按照第二个特征分组,结果又是怎么样呢?第一个海洋动物分组将有两个属于鱼类,两个属于非鱼类;另一个分组则只有一个非鱼类。【这段话看得我头晕眼花,建议多看两遍,祝你好运】
第一种划分很好地处理了相关数据。
如果不相信目测结果,读者可以使用程序清单3-1的calcShannonEntropy() 函数测试不同特征分组的输出结果。

本节我们学习了如何度量数据集的信息熵,如何有效地划分数据集,下一节我们将介绍如何将这些函数功能放在一起,构建决策树。

3.1.3 递归构建决策树

目前我们已经学习了从数据集构造决策树算法所需要的子功能模块,其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分
第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集
递归结束的条件是: 程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节点的数据必然属于叶子节点的分类,参见图3-2所示。
在这里插入图片描述
第一个结束条件使得算法可以终止,我们甚至可以设置算法可以划分的最大分组数目。
后续章节还会介绍其他决策树算法,如C4.5和CART,这些算法在运行时并不总是在每次划分分组时都会消耗特征。由于特征数目并不是在每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。
目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,查看算法是否使用了所有属性即可。
如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类。
在这里插入图片描述

def majorityCnt(classList):
    """
    多数表决
    :param classList: 分类名称的列表
    :return:返回出现次数最多的分类名称
    """
    classCount = {}         # 创建键值为 classList 中唯一值的数据字典
    for vote in classList:  # 字典对象存储了 classList 中每个类标签出现的频率
        if vote not in classCount.keys():
            classCount[vote] = 0    # 没有就添加
        classCount[vote] += 1       # 次数+1
    # 利用operator操作键值降序排序字典
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]   # 返回出现次数最多的分类名称

上面的代码与第2章 classify0 部分的投票表决代码非常类似,
【1】该函数使用分类名称的列表,
【2】然后创建键值为 classList 中唯一值的数据字典,字典对象存储了 classList 中每个类标签出现的频率,
【3】最后利用operator操作键值排序字典,并返回出现次数最多的分类名称。
在这里插入图片描述

def chooseBestFeatureToSplit(dataSet):
    """
    选择最好的数据集划分方式
    :param dataSet: 数据集
    :return: 返回最好特征划分的索引值
    """
    numFeatures = len(dataSet[0]) - 1      # len(dataSet[0]) 计算列数(即特征数),-1是因为最后一列是类别标签
    baseEntropy = calcShannonEnt(dataSet)  # 整个数据集的原始香农熵
    bestInfoGain = 0.0   # 初始化最好信息增益
    bestFeature = -1     # 初始化最好特征划分的索引值
    for i in range(numFeatures):        # 循环遍历数据集中的所有特征
        featList = [example[i] for example in dataSet]   # 将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中
        uniqueVals = set(featList)      # 集合类型中的每个值互不相同,得到列表中唯一元素值
        newEntropy = 0.0
        for value in uniqueVals:        # 遍历当前特征中的所有唯一属性值
            subDataSet = splitDataSet(dataSet, i, value)     # 对当前特征numFeatures的每个值value划分一次数据集
            prob = len(subDataSet)/float(len(dataSet))       # 选择当前特征的当前值的概率
            newEntropy += prob * calcShannonEnt(subDataSet)  # 计算数据集的新熵值,并对所有唯一特征值得到的熵求和
        infoGain = baseEntropy - newEntropy     # 信息增益是熵的减少
        if infoGain > bestInfoGain:
            bestInfoGain = infoGain
            bestFeature = i                 # 记录最好特征划分的索引值
    return bestFeature                      # 返回最好特征划分的索引值

【1】上述代码首先创建了名为classList 的列表变量,其中包含了数据集的所有类别标签:example[-1]
【2】递归函数的第一个停止条件是所有的类标签完全相同,则直接返回该类标签 。
【3】递归函数的第二个停止条件是使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组 。由于第二个条件无法简单地返回唯一的类标签,这里使用程序清单3-3的函数挑选出现次数最多的类别作为返回值。
【4】下一步程序开始创建树,这里使用Python语言的字典类型存储树的信息,当然也可以声明特殊的数据类型存储树,但是这里完全没有必要。字典变量 myTree 存储了树的所有信息,这对于其后绘制树形图非常重要。当前数据集选取的最好特征存储在变量 bestFeat 中,得到列表包含
的所有属性值 。这部分代码与程序清单3-3中的部分代码类似,这里就不再进一步解释了。
【5】最后代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree() ,得到的返回值将被插入到字典变量 myTree 中,因此函数终止执行时,字典中将会嵌套很多代表叶子节点信息的字典数据。
在解释这个嵌套数据之前,我们先看一下循环的第一行subLabels = labels[:] ,这行代码复制了类标签,并将其存储在新列表变量 subLabels 中。之所以这样做,是因为在Python语言中函数参数是列表类型时,参数是按照引用方式传递的。为了保证每次调用函数 createTree() 时不改变原始列表的内容,使用新变量 subLabels 代替原始列表。
在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import trees


myDat, labels = trees.createDataSet()

myTree = trees.createTree(myDat, labels)
print(myTree)

在这里插入图片描述
变量 myTree 包含了很多代表树结构信息的嵌套字典,
从左边开始,第一个关键字 no surfacing 是第一个划分数据集的特征名称,该关键字的值也是另一个数据字典。
第二个关键字是 no surfacing 特征划分的数据集,这些关键字的值是 no surfacing 节点的子节点。
这些值可能是类标签(例如’flippers’),也可能是另一个数据字典。如果值是类标签,则该子节点是叶子节点;如果值是另一个数据字典,则子节点是一个判断节点,这种格式结构不断重复就构成了整棵树。
本节的例子中,这棵树包含了3个叶子节点以及2个判断节点。

单步调试结果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
本节讲述了如何正确地构造树,下一节将介绍如何绘制图形,方便我们正确理解数据信息的内在含义。

3.1 Python 语法解析

1、extend() 和 append() 方法

这两个方法功能类似,但是在处理多个列表时,这两个方法的处理结果是完全不同的。
在这里插入图片描述

2、python的各种推导式(列表推导式、字典推导式、集合推导式)

https://www.cnblogs.com/tkqasn/p/5977653.html
推导式comprehensions(又称解析式),是Python的一种独有特性。推导式是可以从一个数据序列构建另一个新的数据序列的结构体。
共有三种推导:
列表(list)推导式
1、使用[]生成list

variable = [out_exp_res  for  out_exp  in  input_list  if out_exp == 2]

out_exp_res:列表生成元素表达式,可以是有返回值的函数。
for out_exp in input_list:迭代input_list将out_exp传入out_exp_res表达式中。
if out_exp == 2:根据条件过滤哪些值可以。
在这里插入图片描述
字典(dict)推导式
集合(set)推导式

3、集合(set)

集合(set)是一个无序的不重复元素序列。
可以使用大括号 { } 或者 set() 函数创建集合。
注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。
创建格式:
parame = {value01,value02,…}
或者
set(value)
例子见:https://blog.csdn.net/qq_34243930/article/details/83748085#t32 中集合部分

4、operator.itemgetter函数

operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为一些序号。
在这里插入图片描述

5、字典(Dictionary) items()方法

Python 字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组。
dict.items()

6、sorted() 函数

sorted() 函数对所有可迭代的对象进行排序操作。
sorted(iterable, key=None, reverse=False)
iterable – 可迭代对象。
key – 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
reverse – 排序规则,reverse = True 降序 , reverse = False 升序(默认)。

7、count()方法

count() 方法用于统计字符串里某个字符出现的次数。可选参数为在字符串搜索的开始与结束位置。

str.count(sub, start= 0,end=len(string))

sub – 搜索的子字符串
start – 字符串开始搜索的位置。默认为第一个字符,第一个字符索引值为0。
end – 字符串中结束搜索的位置。字符中第一个字符的索引为 0。默认为字符串的最后一个位置。

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

上节我们已经学习了如何从数据集中创建树,然而字典的表示形式非常不易于理解,而且直接绘制图形也比较困难。
本节我们将使用Matplotlib库创建树形图。决策树的主要优点就是直观易于理解,如果不能将其直观地显示出来,就无法发挥其优势。
本节我们将学习如何编写代码绘制如图3-3所示的决策树。
在这里插入图片描述

3.2.1 Matplotlib 注解

Matplotlib提供了一个注解工具 annotations ,非常有用,它可以在数据图形上添加文本注释。注解通常用于解释数据的内容。由于数据上面直接存在文本描述非常丑陋,因此工具内嵌支持带箭头的划线工具,使得我们可以在其他恰当的地方指向数据位置,并在此处添加描述信息,解释数据内容。
如图3-4所示,在坐标(0.2, 0.1)的位置有一个点,我们将对该点的描述信息放在(0.35,0.3)的位置,并用箭头指向数据点(0.2, 0.1)。
在这里插入图片描述
本书将使用Matplotlib的注解功能绘制树形图,它可以对文字着色并提供多种形状以供选择,而且我们还可以反转箭头,将它指向文本框而不是数据点。打开文本编辑器,创建名为treePlotter.py的新文件,然后输入下面的程序代码。
在这里插入图片描述

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)
    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()

【1】代码定义了树节点格式的常量 。
【2】然后定义plotNode() 函数执行了实际的绘图功能,该函数需要一个绘图区,该区域由全局变量createPlot.ax1 定义。
Python语言中所有的变量默认都是全局有效的,只要我们清楚知道当前
代码的主要功能,并不会引入太大的麻烦。
【3】最后定义 createPlot() 函数,它是这段代码的核心。
createPlot() 函数首先创建了一个新图形并清空绘图区,然后在绘图区上绘制两个代表不同类型的树节点,后面我们将用这两个节点绘制树形图。
在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import treePlotter

treePlotter.createPlot()

在这里插入图片描述

3.2.2 构造注解树

绘制一棵完整的树需要一些技巧。我们虽然有x、y坐标,但是如何放置所有的树节点却是个问题。
我们必须知道有多少个叶节点,以便可以正确确定x轴的长度;
我们还需要知道树有多少层,以便可以正确确定y轴的高度。
这里我们定义两个新函数 getNumLeafs() 和 getTreeDepth() ,来获取叶节点的数目和树的层数,参见程序清单3-6,并将这两个函数添加到文件treePlotter.py中。
在这里插入图片描述

def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = list(myTree)[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):
    maxDepth = 0
    firstStr = list(myTree)[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

上述程序中的两个函数具有相同的结构,后面我们也将使用到这两个函数。这里使用的数据结构说明了如何在Python字典类型中存储树信息。
【1】第一个关键字是第一次划分数据集的类别标签,附带的数值表示子节点的取值。从第一个关键字出发,我们可以遍历整棵树的所有子节点。
【2】使用Python提供的 type() 函数可以判断子节点是否为字典类型 。如果子节点是字典类型,则该节点也是一个判断节点,需要递归调用 getNumLeafs() 函数。
【3】getNumLeafs() 函数遍历整棵树,累计叶子节点的个数,并返回该数值。
第2个函数 getTreeDepth() 计算遍历过程中遇到判断节点的个数。该函数的终止条件是叶子节点,一旦到达叶子节点,则从递归调用中返回,并将计算树深度的变量加一。

为了节省大家的时间,函数 retrieveTree 输出预先存储的树信息,避免了每次测试代码时都要从数据中创建树的麻烦。
在这里插入图片描述

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]

在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import treePlotter

print(treePlotter.retrieveTree(1))
myTree = treePlotter.retrieveTree(0)
print(treePlotter.getNumLeafs(myTree))
print(treePlotter.getTreeDepth(myTree))

在这里插入图片描述
函数 retrieveTree() 主要用于测试,返回预定义的树结构。上述命令中调用 getNumLeafs()函数返回值为3,等于树0的叶子节点数;调用 getTreeDepths() 函数也能够正确返回树的层数。

现在我们可以将前面学到的方法组合在一起,绘制一棵完整的树。
在这里插入图片描述

函数 createPlot() 是我们使用的主函数,它调用了 plotTree() ,函数 plotTree 又依次调
用了前面介绍的函数和 plotMidText() 。绘制树形图的很多工作都是在函数 plotTree() 中完成
的,函数 plotTree() 首先计算树的宽和高 。全局变量 plotTree.totalW 存储树的宽度,全
局变量 plotTree.totalD 存储树的深度,我们使用这两个变量计算树节点的摆放位置,这样可
以将树绘制在水平方向和垂直方向的中心位置。与程序清单3-6中的函数 getNumLeafs() 和
getTreeDepth() 类似,函数 plotTree() 也是个递归函数。树的宽度用于计算放置判断节点的
位置,主要的计算原则是将它放在所有叶子节点的中间,而不仅仅是它子节点的中间。同时我们
使用两个全局变量 plotTree.xOff 和 plotTree.yOff 追踪已经绘制的节点位置,以及放置下一
个节点的恰当位置。另一个需要说明的问题是,绘制图形的x轴有效范围是0.0到1.0,y轴有效范
围也是0.0~1.0。为了方便起见,图3-6给出具体坐标值,实际输出的图形中并没有x、y坐标。通
过计算树包含的所有叶子节点数,划分图形的宽度,从而计算得到当前节点的中心位置,也就是
说,我们按照叶子节点的数目将x轴划分为若干部分。按照图形比例绘制树形图的最大好处是无
需关心实际输出图形的大小,一旦图形大小发生了变化,函数会自动按照图形大小重新绘制。如
果以像素为单位绘制图形,则缩放图形就不是一件简单的工作。
接着,绘出子节点具有的特征值,或者沿此分支向下的数据实例必须具有的特征值 。使用
函数 plotMidText() 计算父节点和子节点的中间位置,并在此处添加简单的文本标签信息 。
然后,按比例减少全局变量 plotTree.yOff ,并标注此处将要绘制子节点 ,这些节点既可以
是叶子节点也可以是判断节点,此处需要只保存绘制图形的轨迹。因为我们是自顶向下绘制图形,
因此需要依次递减y坐标值,而不是递增y坐标值。然后程序采用函数 getNumLeafs() 和
getTreeDepth() 以相同的方式递归遍历整棵树,如果节点是叶子节点则在图形上画出叶子节点,如果不是叶子节点则递归调用 plotTree() 函数。在绘制了所有子节点之后,增加全局变量Y的偏移。
程序清单3-7的最后一个函数是 createPlot() ,它创建绘图区,计算树形图的全局尺寸,
并调用递归函数 plotTree() 。
在这里插入图片描述
输出效果如图3-6所示,但是没有坐标轴标签。
在这里插入图片描述
接着按照如下命令变更字典,重新绘制树形图:
在这里插入图片描述

3.2 Python 语法解析

3.3 测试和存储分类器

下面我们将把重点转移到如何利用决策树执行数据分类上。
本节我们将使用决策树构建分类器,以及实际应用中如何存储分类器。

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

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

def classify(inputTree, featLabels, testVec):
    """
    使用决策树的分类函数
    :param inputTree:决策树
    :param featLabels:标签
    :param testVec:测试数据
    :return:
    """
    firstStr = list(inputTree)[0]                 # inputTree数据类型为字典,list(dict)把dict的键转化成list
    # firstStr = inputTree.keys()[0]          # python3改变了dict.keys,返回的是dict_keys对象,支持iterable 但不支持indexable
    secondDict = inputTree[firstStr]        # 访问 键firstStr 的值
    featIndex = featLabels.index(firstStr)  # 返回第一次出现元素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

程序清单3-8定义的函数也是一个递归函数,在存储带有特征的数据会面临一个问题:程序无法确定特征在数据集中的位置,例如前面例子的第一个用于划分数据集的特征是 no surfacing 属性,但是在实际数据集中该属性存储在哪个位置?是第一个属性还是第二个属性?
特征标签列表将帮助程序处理这个问题。**使用 index 方法查找当前列表中第一个匹配 firstStr变量的元素 。**然后代码递归遍历整棵树,比较 testVec 变量中的值与树节点的值,如果到达叶子节点,则返回当前节点的分类标签。
在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import trees
import treePlotter


myDat, labels = trees.createDataSet()
print(labels)

myTree = treePlotter.retrieveTree(0)
print(myTree)

print(trees.classify(myTree, labels, [1, 0]))
print(trees.classify(myTree, labels, [1, 1]))

在这里插入图片描述
与图3-6比较上述输出结果。第一节点名为no surfacing,它有两个子节点:一个是名字为0的叶子节点,类标签为no;另一个是名为flippers的判断节点,此处进入递归调用,flippers节点有两个子节点。以前绘制的树形图和此处代表树的数据结构完全相同。

现在我们已经创建了使用决策树的分类器,但是每次使用分类器时,必须重新构造决策树,下一节我们将介绍如何在硬盘上存储决策树分类器

3.3.2 使用算法:决策树的存储

构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用已经构造好的决策树。
为了解决这个问题,需要使用Python模块pickle序列化对象,参见程序清单3-9。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。任何对象都可以执行序列化操作,字典对象也不例外。
在这里插入图片描述

def storeTree(inputTree, filename):
    import pickle
    fw = open(filename, 'wb')
    pickle.dump(inputTree, fw)  # 将inputTree对象序列化存入已经打开的文件:fw中
    fw.close()


def grabTree(filename):
    import pickle
    fr = open(filename, 'rb')  
    return pickle.load(fr)     # 将文件中的对象序列化读出

在这里插入图片描述
通过上面的代码,我们可以将分类器存储在硬盘上,而不用每次对数据分类时重新学习一遍,这也是决策树的优点之一,
第2章介绍了k-近邻算法就无法持久化分类器
我们可以预先提炼并存储数据集中包含的知识信息,在需要对事物进行分类时再使用这些知识。下节我们将使用这些工具处理隐形眼镜数据集。

3.3 Python 语法解析

1、list(dict)

list(dict)把dict的键转化成list
在这里插入图片描述

2、dict.keys()

遍历字典dict的所有
字典举例:https://blog.csdn.net/qq_34243930/article/details/83748085#t28

3、dict[key]

访问字典某项,返回key的值value

4、list.index(x)

返回第一次出现元素x的索引值
列表举例:https://blog.csdn.net/qq_34243930/article/details/83748085#t18

5、type()

type()函数查看数据类型
在这里插入图片描述

6、Python模块pickle序列化对象

pickle模块实现Python对象结构的二进制协议序列化和反序列化。
pickling将Python对象层次结构转换为字节流的过程。
unpickling是相反操作,将字节流(二进制文件或类似字节的对象)转换回对象层次结构。
模块接口
(1) pickle.dump(obj,file,protocol = None,*,fix_imports = True )
函数的功能:将obj对象序列化存入已经打开的file中。
obj:想要序列化的obj对象。
file:文件名称。
protocol:序列化使用的协议。如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
(2) pickle.dumps(obj,protocol = None,* ,fix_imports = True )
函数的功能:将obj对象序列作为bytes对象返回,而不是存入文件中。
(3) pickle.load(file,* ,fix_imports = True,encoding =“ASCII”,errors =“strict” )
函数的功能:将file中的对象序列化读出。
(4) pickle.loads(bytes_object,*,fix_imports = True,encoding =“ASCII”,errors =“strict” )
函数的功能:从bytes对象中读出序列化前的obj对象。

3.4 示例:使用决策树预测隐形眼镜类型

本节我们将通过一个例子讲解决策树如何预测患者需要佩戴的隐形眼镜类型。
使用小数据集,我们就可以利用决策树学到很多知识:眼科医生是如何判断患者需要佩戴的镜片类型;一旦理解了决策树的工作原理,我们甚至也可以帮助人们判断需要佩戴的镜片类型。
在这里插入图片描述
这里使用的隐形眼镜数据集是非常著名的数据集,它包含很多患者眼部状况的观察条件以及医生推荐的隐形眼镜类型。隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。数据来源于UCI数据库,为了更容易显示数据,本书对数据做了简单的更改,数据存储在源代码下载路径的文本文件中。
在这里插入图片描述
myope 患近视的
在这里插入图片描述
在这里插入图片描述

"""
Created on Dec 9, 2018
Decision Tree Source Code for Machine Learning in Action
'trees_test.py'
@author: xpt
"""
import trees
import treePlotter

fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLables = ['age', 'prescript', 'astigmatic', 'tearRate']
lensesTree = trees.createTree(lenses, lensesLables)
print(lensesTree)
treePlotter.createPlot(lensesTree)

在这里插入图片描述
在这里插入图片描述
采用文本方式很难分辨出决策树的模样,最后一行命令调用 createPlot() 函数绘制了如图3-8所示的树形图。沿着决策树的不同分支,我们可以得到不同患者需要佩戴的隐形眼镜类型。从图3-8上我们也可以发现,医生最多需要问四个问题就能确定患者需要佩戴哪种类型的隐形眼镜。
在这里插入图片描述
图3-8所示的决策树非常好地匹配了实验数据,然而这些匹配选项可能太多了。我们将这种问题称之为过度匹配(overfitting)
为了减少过度匹配问题,我们可以裁剪决策树,去掉一些不必要的叶子节点。如果叶子节点只能增加少许信息,则可以删除该节点,将它并入到其他叶子节点中。第9章将进一步讨论这个问题。
第9章将学习另一个决策树构造算法CART,本章使用的算法称为ID3,它是一个好的算法但并不完美。ID3算法无法直接处理数值型数据,尽管我们可以通过量化的方法将数值型数据转化为标称型数值,但是如果存在太多的特征划分,ID3算法仍然会面临其他问题。

3.4 Python 语法解析

1、File readlines() 方法

readlines() 方法用于读取所有行(直到结束符 EOF)并返回列表,该列表可以由 Python 的 for… in … 结构进行处理

2、strip()方法

Python strip() 方法用于移除字符串头尾指定的字符(默认为空格)或字符序列。
注意:该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。

str.strip([chars])

chars – 移除字符串头尾指定的字符序列。

3、字符串分割str.split()

str.split()函数
()里放的是要以什么进行分割
输出的是一个列表list
例子见:https://blog.csdn.net/qq_34243930/article/details/83748085#t32 中字符串分割部分

4、

在这里插入图片描述

5、数值型数据与标称型数值

标称型:一般在有限的数据中取,而且只存在‘是’和‘否’两种不同的结果(一般用于分类)
数值型:可以在无限的数据中取,而且数值比较具体化,例如4.02,6.23这种值(一般用于回归分析)

3.5 本章小结

决策树分类器就像带有终止块的流程图,终止块表示分类结果。开始处理数据集时,我们首先需要测量集合中数据的不一致性,也就是熵,然后寻找最优方案划分数据集,直到数据集中的所有数据属于同一分类。ID3算法可以用于划分标称型数据集。构建决策树时,我们通常采用递归的方法将数据集转化为决策树。一般我们并不构造新的数据结构,而是使用Python语言内嵌的数据结构字典存储树节点信息。
使用Matplotlib的注解功能,我们可以将存储的树结构转化为容易理解的图形。
Python语言的pickle模块可用于存储决策树的结构。
隐形眼镜的例子表明决策树可能会产生过多的数据集划分,从而产生过度匹配数据集的问题。我们可以通过裁剪决策树,合并相邻的无法产生大量信息增益的叶节点,消除过度匹配问题。
还有其他的决策树的构造算法,最流行的是C4.5和CART,第9章讨论回归问题时将介绍CART算法。
本书第2章、第3章讨论的是结果确定的分类算法,数据实例最终会被明确划分到某个分类中
下一章我们讨论的分类算法将不能完全确定数据实例应该划分到某个分类,或者只能给出数据实例属于给定分类的概率。

作业2

在构建一个决策树模型时,我们对某个属性分割节点,下面四张图中,哪个属性对应的信息增益最大?
在这里插入图片描述
李航的《统计学方法》中,对信息增益有如下定义:
特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差,即
g(D,A)=H(D)-H(D|A)

H = − ∑ i − 1 n p ( x i ) log ⁡ 2 p ( x i ) H=-\sum _{i-1}^{n} p(x_{i})\log_{2}p(x_{i}) H=i1np(xi)log2p(xi),其中n是分类的数目。

此题解答:
共有实例14个,有2类
其中分类结果为‘yes’9个,为‘no’5个
所以经验熵H(D)= − 9 14 ∗ log ⁡ 9 14 − 5 14 ∗ log ⁡ 5 14 -\frac{9}{14}*\log\frac{9}{14}-\frac{5}{14}*\log\frac{5}{14} 149log149145log145=0.4807

图1:
在这里插入图片描述
H(D,A)=
5 14 ( − 2 5 ∗ log ⁡ 2 5 − 3 5 ∗ log ⁡ 3 5 ) \frac{5}{14}(-\frac{2}{5}*\log\frac{2}{5}-\frac{3}{5}*\log\frac{3}{5}) 145(52log5253log53)
+ 4 14 ( − 4 4 ∗ log ⁡ 4 4 − 0 ) \frac{4}{14}(-\frac{4}{4}*\log\frac{4}{4}-0) 144(44log440)
+ 5 14 ( − 3 5 ∗ log ⁡ 3 5 − 2 5 ∗ log ⁡ 2 5 ) \frac{5}{14}(-\frac{3}{5}*\log\frac{3}{5}-\frac{2}{5}*\log\frac{2}{5}) 145(53log5352log52)=0.4807
g(D,A)=H(D)-H(D|A)=0.1711

图2:
在这里插入图片描述
H(D,A)= 7 14 ( − 3 7 ∗ log ⁡ 3 7 − 4 7 ∗ log ⁡ 4 7 ) \frac{7}{14}(-\frac{3}{7}*\log\frac{3}{7}-\frac{4}{7}*\log\frac{4}{7}) 147(73log7374log74)
+ 7 14 ( − 6 7 ∗ log ⁡ 6 7 − 1 7 ∗ log ⁡ 1 7 ) \frac{7}{14}(-\frac{6}{7}*\log\frac{6}{7}-\frac{1}{7}*\log\frac{1}{7}) 147(76log7671log71)=0.5465
g(D,A)=H(D)-H(D|A)=0.1053

图3:
在这里插入图片描述
H(D,A)= 8 14 ( − 6 8 ∗ log ⁡ 6 8 − 2 8 ∗ log ⁡ 2 8 ) \frac{8}{14}(-\frac{6}{8}*\log\frac{6}{8}-\frac{2}{8}*\log\frac{2}{8}) 148(86log8682log82)
+ 6 14 ( − 3 6 ∗ log ⁡ 3 6 − 3 6 ∗ log ⁡ 3 6 ) \frac{6}{14}(-\frac{3}{6}*\log\frac{3}{6}-\frac{3}{6}*\log\frac{3}{6}) 146(63log6363log63)=0.6184
g(D,A)=H(D)-H(D|A)=0.0334

图4:
在这里插入图片描述
H(D,A)= 4 14 ( − 2 4 ∗ log ⁡ 2 4 − 2 4 ∗ log ⁡ 2 4 ) \frac{4}{14}(-\frac{2}{4}*\log\frac{2}{4}-\frac{2}{4}*\log\frac{2}{4}) 144(42log4242log42)
+ 6 14 ( − 4 6 ∗ log ⁡ 4 6 − 2 6 ∗ log ⁡ 2 6 ) \frac{6}{14}(-\frac{4}{6}*\log\frac{4}{6}-\frac{2}{6}*\log\frac{2}{6}) 146(64log6462log62)
+ 4 14 ( − 3 4 ∗ log ⁡ 3 4 − 1 4 ∗ log ⁡ 1 4 ) \frac{4}{14}(-\frac{3}{4}*\log\frac{3}{4}-\frac{1}{4}*\log\frac{1}{4}) 144(43log4341log41)=0.6315
g(D,A)=H(D)-H(D|A)=0.0203

综上:图1,outlook对应的信息增益最大

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏普通

谢谢打赏~普通在此谢过

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值