机器学习----监督学习算法之决策树(Decision Tree)

感谢Jack-Cui大佬的知识分享

机器学习专栏点击这里

0. 概述

决策树(Decision Tree)是一种基本的分类与回归方法,其模型呈树状结构,在分类问题中,表示基于特征对实例进行分类的过程。本质上,决策树模型就是一个定义在特征空间与类空间上的 条件概率分布。决策树由 节点(node)和有向边(directed edge) 组成。节点有两种类型:内部节点(internal node)和叶节点(leaf node),内部节点表示一个特征或属性,叶节点表示一个

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

可以把决策树看成一个if-then规则的集合,将 决策树转换成if-then规则的过程是 这样的:

  • 由决策树的根结点(root node)到叶结点(leaf node)的每一条路径构建一条规则;
  • 路径上内部结点的特征对应着规则的条件,而叶结点的类对应着规则的结论。
  • 决策树的路径或其对应的if-then规则集合具有一个重要的性质:互斥并且完备。这就是说,每一个实例都被一条路径或一条规则所覆盖,而且只被一条路径或一条规则所覆盖。这里所覆盖是指实例的特征与路径上的特征一致或实例满足规则的条件。

1. 使用决策树做预测需要以下过程:

  • 收集数据:可以使用任何方法。比如想构建一个相亲系统,我们可以从媒婆那里,或者通过参访相亲对象获取数据。根据他们考虑的因素和最终的选择结果,就可以得到一些供我们利用的数据了。
  • 准备数据:收集完的数据,我们要进行整理,将这些所有收集的信息按照一定规则整理出来,并排版,方便我们进行后续处理。
  • 分析数据:可以使用任何方法,决策树构造完成之后,我们可以检查决策树图形是否符合预期
  • 训练算法:这个过程也就是构造决策树,同样也可以说是决策树学习,就是构造一个决策树的数据结构。
  • 测试算法:使用经验树计算错误率。当错误率达到了可接收范围,这个决策树就可以投放使用了。
  • 使用算法:此步骤可以使用适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义

2. 决策树构建步骤

决策树学习(构建)通常包括三个步骤:特征选择、决策树的生成和决策树的修剪
构造决策树的数据必须要充足特征较少的数据集可能会导致决策树的正确率偏低。若数据特征充足但不会选择特征也会影响决策树的正确率。

2.1 特征选择

特征选择即决定用数据集中哪一个特征划分特征空间,主要在于选取对训练数据具有 分类能力的特征。不论一个数据集有多少特征,每次划分数据集时只能选一个特征,那么 第一次选择 哪个特征作为划分的参考属性才能将数据更快的分类呢?通常特征选择的标准是 信息增益(information gain)或信息增益比,信息增益越大,对应的特征其分类效果越好

2.1.1 熵(香农熵)

在计算信息增益之前,需要先知道"熵"这个概念。熵定义为信息的期望值。在信息论与概率统计中,熵是表示随机变量不确定性的度量,一句通俗的话讲就是这个体系的混乱程度是如何的。
假设有一个样本为N的数据集,第 i 类样本为 xi,如果待分类的事务可能划分在 多个分类 之中,则符号 xi的信息 定义为
在这里插入图片描述
其中 p(xi)是选择该分类的概率。通过改变 i 的值即可获得数据集中所有类别的信息。
在这里插入图片描述

为了 计算熵,我们需要计算所有类别所有可能值包含的信息期望值(数学期望),通过下面的公式得到:
在这里插入图片描述
熵越高,变量的不确定性越大,也就是数据的混合程度越高

当熵中的概率由数据估计(特别是最大似然估计)得到时,所对应的熵称为 经验熵(empirical entropy)

2.1.2 代码:计算经验熵

import numpy as np 
import pandas as pd
"""
函数说明: 创建数据集
Parameters:
    无
Returns:
    DataSet -- 表格型数据集

    年龄:0代表青年,1代表中年,2代表老年;
    有工作:0代表否,1代表是;
    有自己的房子:0代表否,1代表是;
    信贷情况:0代表一般,1代表好,2代表非常好;
    类别(是否给贷款):no代表否,yes代表是。
"""
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 = ['年龄', '有工作', '有自己的房子', '信贷情况','是否给贷款']		#分类属性
    #矩阵转化Dataframe
    DataSet = pd.DataFrame(dataSet,columns=labels)
    return DataSet
"""
函数说明:计算给定数据集的经验熵(香农熵)

Parameters:
    DataSet - 数据集
Returns:
    Ent - 经验熵(香农熵)
"""
def calcShannonEnt(DataSet):
    
    # Series 线性的数据结构, series是一个一维数组
    Loan_type = DataSet['是否给贷款'].value_counts()  # type:<class 'pandas.core.series.Series'>
    # print(Loan_type[0]) # yes    9 ;no     6  # 实质就是[9,6]

    #获取样本个数
    Nums = DataSet.shape[0] #  (15, 5)
    #每个分类的概率,即 P(xi)
    P = Loan_type/Nums
    #计算熵值
    Ent = (-P*np.log2(P)).sum()
    return Ent

def main():
    DataSet=CreatDataset()
    print(DataSet)
    Ent=calcShannonEnt(DataSet)
    print('经验熵H(D):%f' % Ent)

if __name__ == "__main__":
    main()

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

2.1.3 信息增益

由介绍已知,根据信息增益选择特征。也就是说,信息增益是相对于特征而言的,信息增益越大,特征对最终的分类结果影响也就越大,我们就应该选择对最终分类结果影响最大的那个特征作为我们的初始分类特征。

信息增益的计算 就是将父节点的熵减去其下所有子节点的熵之和,并且在求和时,由于类别比重不同,需要对其实现加权平均。

在讲解信息增益定义之前,我们还需要明确一个概念:条件熵
条件熵 H(Y|X) 表示在已知随机变量X的条件下随机变量Y的不确定性,随机变量X给定的条件下随机变量Y的条件熵(conditional entropy) H(Y|X),定义X给定条件下 Y的条件概率分布的熵 对X 的数学期望:
在这里插入图片描述
当熵和条件熵中的概率由极大似然估计得到时,所对应的熵和条件熵分别称为检验熵(empirical entropy)和经验条件熵(empirical conditional entropy).
在这里插入图片描述
在这里插入图片描述

熵可以用来表示数据集的不确定性,熵越大,则数据集的不确定性越大。因此使用划分前后数据集熵的差值度量使用当前特征对于数据集进行划分的效果(类似于深度学习的代价函数)。对于待划分的数据集D,其划分前的数据集的熵是一定的 H(D),但是划分之后的熵 H(D|A) 是不定的,H(D|A) 越小说明使用此特征划分得到的子集的不确定性越小(也就是纯度越高)。因此 H(D)-H(D|A) 越大,说明使用当前特征划分数据集时,纯度上升的更快

2.1.4 代码:计算信息增益

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

Parameters:
    DataSet -- 数据集
    col -- 选择需要分割的特征
    axis -- 分割特征值
Returns:
    ReData -- 分割后的数据集
'''
def Split(DataSet,col,axis):
    ReData=DataSet[DataSet.iloc[:,col]==axis]
    return ReData
'''
函数说明:根据各特征的信息增益,选择最优初始特征

Parameters:
    DataSet:数据集
Returns:
    Bestfeature:增益最大的(最优)特征的索引值
'''
def ChooseBestFeature(DataSet):

    Nums = DataSet.shape[1] #  (15, 5)
    # 用于存放各特征的信息熵
    H=np.zeros(Nums-1)
    # 1.计算经验熵(香农熵)
    AllEnt=calcShannonEnt(DataSet,-1)
                   
    for col in range(Nums-1): 
        ent=0
        for axis in range(3):
            # 2.按特征分割数据:创建函数 Split() 
            redata=Split(DataSet,col,axis)
            if len(redata.values) == 0: # 判断返回的数据是否为空
                break
            else:
                # 统计特征的各类别的个数
                Loan_type = DataSet.iloc[:,col].value_counts()
                # print(Loan_type[1])
                ent += Loan_type[axis]/sum(Loan_type)*calcShannonEnt(redata,-1)
        # 3.计算各特征的信息熵
        H[col]=AllEnt-ent  
    Bestfeature = np.argmax(H)
    print('各特征信息增益: ',H)
    print('最优特征索引值: %d' % np.argmax(H))
    print('对应特征为: ',DataSet.columns[Bestfeature])
    return Bestfeature

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

Series标签索引演示

在这里插入图片描述
在这里插入图片描述

2.2 构建决策树—ID3算法

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

根据上部分求得的结果,由于特征A3(有自己的房子)的信息增益值最大,所以选择特征A3作为根结点的特征。它将训练集D划分为两个 子集D1(A3取值为”是”)和D2(A3取值为”否”) 。由于D1只有同一类的样本点,所以它成为一个叶结点,结点的类标记为“是”,信息如下:可以发现有房子就可以贷款,即不用继续向下分
在这里插入图片描述
对D2则需要从特征A1(年龄),A2(有工作)和A4(信贷情况)中选择新的特征,计算各个特征的信息增益:

  • g(D2,A1) = H(D2) - H(D2 | A1) = 0.251
  • g(D2,A2) = H(D2) - H(D2 | A2) = 0.918
  • g(D2,A3) = H(D2) - H(D2 | A3) = 0.474

根据计算,选择信息增益最大的特征A2(有工作)作为结点的特征。由于A2有两个可能取值,从这一结点引出两个子结点:一个对应”是”(有工作)的子结点,包含3个样本,它们属于同一类,所以这是一个叶结点,类标记为”是”;另一个是对应”否”(无工作)的子结点,包含6个样本,它们也属于同一类,所以这也是一个叶结点,类标记为”否”。 这样就生成了一个决策树,该决策树只用了两个特征(有两个内部结点),生成的决策树如下图所示。
在这里插入图片描述

2.2.1 代码构建决策树

我们 使用字典存储决策树的结构,比如上小节我们分析出来的决策树,用字典可以表示为:

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

在构造决策树之前,先回顾一下前几个子模块的工作原理:先获取原始数据集,然后基于最优特征划分数据集,当数据集特征大于两个时,第一次划分之后,数据将被向下传递至树的下一个节点,在这个节点上,在此划分数据, 此过程是利用递归原理处理数据集。什么时候划分结束呢? 当程序遍历完所有划分数据集的属性,或者 每个分支下所有实例分类一致 时代表划分数据集结束。而构造决策树的过程就是将每一次划分出的数据填入一个字典中,当数据集划分结束时,向字典中填充数据也结束,此过程也是一个递归过程,至此决策树的构造完成。

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

'''
函数说明: 创建决策树

Parameters:
    DataSet -- 数据集
Returns:
    Tree -- 决策树(字典形式)
'''
def CreateTree(DataSet):
    # featurelabels=[] # 用于存放最优特征

    # 获取所有特征标签
    index_list = list(DataSet.columns) # ['年龄', '有工作', '有自己的房子', '信贷情况', '是否给贷款']
    # 获取最后一列(分类标签)的类别
    # value_counts () 返回的序列默认是降序的
    label_series = DataSet.iloc[:,-1].value_counts()  # yes:9 , no: 6

    # 判断是否继续可分:
    # 1、判断类别标签(最多一个)是否等于数据样本数 
    # 2、数据集是否只有一列 
    if label_series[0]==DataSet.shape[0] or DataSet.shape[1] == 1:
        return label_series.index[0] #返回类标签 
    # 选择最优特征和最优特征标签
    bestfeatureindex,bestfeaturelabel= ChooseBestFeature(DataSet)
    # featurelabels.append(bestfeaturelabel)

    Tree={bestfeaturelabel:{}}
    # 删除已经使用特征标签
    del(index_list[bestfeatureindex]) 
    # 得到训练集中所有最优特征的属性值
    value_list = set(DataSet.iloc[:,bestfeatureindex])
    # 遍历特征,创建决策树
    for value in value_list:
        newdataset=Split(DataSet,bestfeatureindex,value)
        Tree[bestfeaturelabel][value] = CreateTree(newdataset)
    return Tree

运行结果:

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

2.3 决策树存储和读取

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

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

Parameters:
    inputTree - 已经生成的决策树
    filepath - 决策树的存储文件路径
Returns:
    无
"""
def StoreTree(inputTree, filepath):
    with open(filepath, 'wb') as fw:
        pickle.dump(inputTree, fw)
"""
函数说明:读取决策树

Parameters:
    filepath- 决策树的存储文件名
Returns:
    pickle.load(fr) - 决策树字典
"""
def GrabTree(filepath):
    fr = open(filepath, 'rb')
    return pickle.load(fr)

2.4 分类测试

依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子结点;最后将测试数据定义为叶子结点所属的类型。
在构建决策树的代码,可以看到,有个 featLabels参数。它是用来干什么的?它就是用来记录各个分类结点的,在用决策树做预测的时候,我们按顺序输入需要的分类结点的属性值即可
举个例子,比如我用上述已经训练好的决策树做分类,那么我只需要提供这个人是否有房子,是否有工作这两个信息即可,无需提供冗余的信息。

'''
函数说明: 决策树分类

Parameters:
    mytree --决策树
    featLabels -- 最优特征标签
    testVec -- 测试数据
Returns:
    result -- 分类结果
'''
def Classify(mytree,featLabels,testVec):
    # 获取根节点
    # iter 将 dic 转换成 迭代器(iterator),迭代器有 next 方法
    root_node = next(iter(mytree)) 
    # 取下一个节点(子节点):字典形式 
    secondDict = mytree[root_node]  # {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}
    featIndex = featLabels.index(root_node) # 0
    # 遍历字典中的 key : 0 1                                              
    for key in secondDict.keys():
        # 判断特征,选择流向
        if testVec[featIndex] == key: 
            # 判断是否到达叶节点
            if type(secondDict[key]).__name__ == 'dict': 
                result = Classify(secondDict[key], featLabels, testVec)
            else: result = secondDict[key]
    return result

结果:放贷

完整代码

import numpy as np 
import pandas as pd
from math import log # 默认底数为e 
import pickle

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

Parameters:
    inputTree - 已经生成的决策树
    filepath - 决策树的存储文件路径
Returns:
    无
"""
def StoreTree(inputTree, filepath):
    with open(filepath, 'wb') as fw:
        pickle.dump(inputTree, fw)
"""
函数说明:读取决策树

Parameters:
    filepath - 决策树的存储文件名
Returns:
    pickle.load(fr) - 决策树字典
"""
def GrabTree(filepath):
    fr = open(filepath, 'rb')
    return pickle.load(fr)
"""
函数说明: 创建数据集
Parameters:
    无
Returns:
    DataSet -- 表格型数据集

    年龄:0代表青年,1代表中年,2代表老年;
    有工作:0代表否,1代表是;
    有自己的房子:0代表否,1代表是;
    信贷情况:0代表一般,1代表好,2代表非常好;
    类别(是否给贷款):no代表否,yes代表是。
"""
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 = ['年龄', '有工作', '有自己的房子', '信贷情况','是否给贷款']		#分类属性
    #矩阵转化Dataframe
    DataSet = pd.DataFrame(dataSet,columns=labels)
    return DataSet
"""
函数说明:计算给定数据集的经验熵(香农熵)

Parameters:
    DataSet - 数据集
    i -- 特征索引值 
Returns:
    Ent - 经验熵(香农熵)
"""
def calcShannonEnt(DataSet,i):
    
    # Series 线性的数据结构, series是一个一维数组
    Loan_type = DataSet.iloc[:,i].value_counts()  # type:<class 'pandas.core.series.Series'>
    # print(Loan_type[0]) # yes    9 ;no     6  # 实质就是[9,6]

    #获取样本个数
    Nums = DataSet.shape[0] #  (15, 5)
    #每个分类的概率,即 P(xi)
    P = Loan_type/Nums
    #计算熵值
    Ent = (-P*np.log2(P)).sum()
    return Ent
'''
函数说明:按照给定特征划分数据集

Parameters:
    DataSet -- 数据集
    col -- 选择需要分割的特征
    axis -- 分割特征值
Returns:
    ReData -- 分割后的数据集
'''
def Split(DataSet,col,axis):
    ReData=DataSet[DataSet.iloc[:,col]==axis]
    return ReData
'''
函数说明:根据各特征的信息增益,选择最优初始特征

Parameters:
    DataSet:数据集
Returns:
    BestfeatureIndex:增益最大的(最优)特征的索引值
    BestfeatureLabel:最优特征标签
'''
def ChooseBestFeature(DataSet):

    Nums = DataSet.shape[1] #  (15, 5)
    # 用于存放各特征的信息熵
    H=np.zeros(Nums-1)
    # 计算经验熵(香农熵)
    AllEnt=calcShannonEnt(DataSet,-1)
    #print(AllEnt)
                
    for col in range(Nums-1): 
        ent=0
        for axis in range(3):
            # 按特征分割数据:创建函数 Split() 
            redata=Split(DataSet,col,axis)
            if len(redata.values) == 0: # 判断返回的数据是否为空
                break
            else:
                # print(DataSet.columns[col],'\n',redata)
                # 统计特征的各类别的个数
                Loan_type = DataSet.iloc[:,col].value_counts()
                # print(Loan_type[1])
                ent += Loan_type[axis]/sum(Loan_type)*calcShannonEnt(redata,-1)
        # 3.计算各特征的信息熵
        H[col]=AllEnt-ent  

    BestfeatureIndex = np.argmax(H)
    BestfeatureLabel = DataSet.columns[BestfeatureIndex]
    # print('各特征信息增益: ',H)
    # print('最优特征索引值: %d' % np.argmax(H))
    # print('对应特征为: ',DataSet.columns[Bestfeature])
    return BestfeatureIndex,BestfeatureLabel
'''
函数说明: 创建决策树

Parameters:
    DataSet -- 数据集
Returns:
    Tree -- 决策树(字典形式)
'''
def CreateTree(DataSet,featurelabels):
    # 获取所有特征标签
    index_list = list(DataSet.columns) # ['年龄', '有工作', '有自己的房子', '信贷情况', '是否给贷款']
    # 获取最后一列(分类标签)的类别
    # value_counts () 返回的序列默认是降序的
    label_series = DataSet.iloc[:,-1].value_counts()  # yes:9 , no: 6

    # 判断是否继续可分:
    # 1、判断类别标签(最多一个)是否等于数据样本数 
    # 2、数据集是否只有一个特征
    if label_series[0]==DataSet.shape[0] or DataSet.shape[1] == 1:
        return label_series.index[0] #返回类标签 
    # 最优特征索引值
    bestfeatureindex,bestfeaturelabel= ChooseBestFeature(DataSet)
    # 最优特征标签
    featurelabels.append(bestfeaturelabel)
    # 决策树形式
    Tree={bestfeaturelabel:{}}
    # 删除已经使用特征标签
    del(index_list[bestfeatureindex]) 
    # 得到训练集中所有最优特征的属性值
    value_list = set(DataSet.iloc[:,bestfeatureindex])
    # 遍历特征,创建决策树
    for value in value_list:
        newdataset=Split(DataSet,bestfeatureindex,value)
        Tree[bestfeaturelabel][value] = CreateTree(newdataset,featurelabels)
    return Tree
'''
函数说明: 决策树分类

Parameters:
    mytree --决策树
    featLabels -- 最优特征标签
    testVec -- 测试数据
Returns:
    result -- 分类结果
'''
def Classify(mytree,featLabels,testVec):
    # 获取根节点
    # iter 将 dic 转换成 迭代器(iterator),迭代器有 next 方法
    root_node = next(iter(mytree)) 
    # 取下一个节点(子节点):字典形式 
    secondDict = mytree[root_node]  # {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}
    featIndex = featLabels.index(root_node) # 0
    # 遍历字典中的 key : 0 1                                              
    for key in secondDict.keys():
        # 判断特征,选择流向
        if testVec[featIndex] == key: 
            # 判断是否到达叶节点
            if type(secondDict[key]).__name__ == 'dict': 
                result = Classify(secondDict[key], featLabels, testVec)
            else: result = secondDict[key]
    return result

def main():
    save_path='classifierStorage.txt' # 根据个人情况设置路径
    # 用于存放最优特征,即分类所需特征
    featurelabels=[]   # 用于对应测试数据索引
    DataSet=CreatDataset()
    # Ent=calcShannonEnt(DataSet,-1) # -1 取最后一列,即分类结果
    # bestfeature=ChooseBestFeature(DataSet)
    Tree=CreateTree(DataSet,featurelabels)
    # 存储决策树
    # StoreTree(Tree,save_path)
    # mytree=GrabTree(save_path)
    result=Classify(Tree,featurelabels,[0,1])

    if result == 'yes':
        print('放贷')
    if result == 'no':
        print('不放贷')

if __name__ == "__main__":
    main()

传送门

决策树(Decision Tree)
机器学习笔记(三)——搞懂决策树必备的信息增益
机器学习笔记(四)——决策树的构建及可视化
决策树ID3的实现-python

代码相关

Pandas-DataFrame基础知识点总结
python数据分析之pandas里的Series
Series下标索引、标签索引、切片索引、布尔型索引
python中count()、values_counts()、size()函数
Pandas | 频数统计很简单,但这5 种技巧你使用过吗?-- .value_counts()
python获得list或numpy数组中最大元素对应的索引
Python中的next()\iter()函数详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值