第十一课.决策树

决策树与特征选择

决策树是一种基本的分类与回归方法,下面主要介绍分类决策树的原理与应用。分类决策树既可以看作是一个规则集合,又可以看作是给定特征条件下类的条件概率分布。

决策树的形式

决策树常用三种形式表达:树形结构,规则集合,条件概率;

树形结构
某银行使用决策树模型决定是否同意申请人贷款,每来一个贷款申请人,需要先看他是否有房,然后看他是否有工作,根据这两个特征决定要不要给他贷款:
fig1
规则集合
由上面的决策树可知,从根结点到叶子结点一共有三条路径,分别对应下面的三条决策规则:

  • 1.有房 ==》同意贷款
  • 2.没房、有工作 ==》同意贷款
  • 3.没房、没工作 ==》不同意贷款

这三条决策规则都是给定条件,然后得出结论,其中条件对应着样本特征,结论对应着样本的类别标记,可以如下描述:

# 决策树 ==》决策规则集合
def decision_tree(x):
    # 有房
    if x[0]=='yes':
        return '同意贷款'
    # 没房有工作
    elif x[1]=='yes':
        return '同意贷款'
    # 没房没工作
    else:
        return '不同意贷款'

条件概率
上述决策树的三条决策路径对应了 3 个条件概率分布,即:给定样本特征的条件下,样本类别的概率分布。将最大概率值对应的类别作为决策树的输出结果,实例如下:

# 决策树 ==》条件概率分布
def decision_tree(x):
    # 有房
    if x[0]=='yes':
        # 假设有房的条件下类别的概率分布如下
        p = {'同意贷款': 0.8,'不同意贷款':0.2}
        # 返回概率最大值对应的类别
        # sorted默认升序排列
        return sorted(p.items(),key=lambda x:x[1])[-1][0]
    # 没房有工作
    elif x[1]=='yes':
        # 假设没房有工作的条件下类别的概率分布如下
        p = {'同意贷款': 0.7,'不同意贷款':0.3}
        # 返回概率最大值对应的类别
        return sorted(p.items(),key=lambda x:x[1])[-1][0]
    # 没房没工作
    else:
        # 假设没房没工作的条件下类别的概率分布如下
        p = {'同意贷款': 0.1,'不同意贷款':0.9}
        # 返回概率最大值对应的类别
        return sorted(p.items(),key=lambda x:x[1])[-1][0]

决策树的学习

决策树的学习主要有三个步骤:特征选择、决策树的生成、决策树的剪枝。假设决策树是通过如下训练数据学习得到的:
fig2
图中共有9个样本,每个样本有三个特征,分别是 x0 = '有房?'x1 = '有工作?'x2 = '年龄';样本类别为:是否同意贷款,用 y 表示;

事先并不知道使用哪些特征去判断是否同意申请人的贷款,但是观察训练数据可知,若根据某些特征筛选得到的样本集合是属于同一类别的,则可以形成一条特征到类别的决策规则,如:有房同意贷款,没房没工作不同意贷款,没房有工作同意贷款。若某些特征筛选得到的样本集合不是同一类别的,则按照多数表决的方法进行对训练样本进行分类。

不同的特征筛选得到的训练样本集合,其类别的组成也不同,统计每一类的数量占比即样本在给定特征条件下的类别概率分布。我们希望这个概率分布是某一类占多数,或者都是同一类别的,这样的特征具有较强的分类能力。

下面介绍如何选择具有较强分类能力的特征。

特征选择

信息量
离散型随机变量 Y 的概率分布如下:
fig3
其中, P ( Y = y i ) = p i P(Y=y_{i})=p_{i} P(Y=yi)=pi i ∈ [ 1 , 2 , . . . , n ] i\in [1,2,...,n] i[1,2,...,n]

随机变量 Y 取某个值的信息量与其对应的概率成反比,用公式表示如下:
I ( Y = y i ) = − l o g 2 [ P ( Y = y i ) ] = − l o g 2 ( p i ) I(Y=y_{i})=-log_{2}[P(Y=y_{i})]=-log_{2}(p_{i}) I(Y=yi)=log2[P(Y=yi)]=log2(pi)
一件事发生的概率越高,其信息量越小,当概率值为 1 时成为确定性事件,信息量为零。例如,太阳早上从东边升起属于确定性事件,这个消息的信息量为零。公式中的对数通常以 2 为底或以 e 为底,这时信息量的单位分别叫做比特或纳特。

信息熵
信息熵定义为信息量的数学期望,用公式表示如下:
H ( Y ) = ∑ i = 1 n P ( Y = y i ) I ( Y = y i ) = − ∑ i = 1 n p i l o g 2 ( p i ) H(Y)=\sum_{i=1}^{n}P(Y=y_{i})I(Y=y_{i})=-\sum_{i=1}^{n}p_{i}log_{2}(p_{i}) H(Y)=i=1nP(Y=yi)I(Y=yi)=i=1npilog2(pi)
其中, n n n代表随机变量 Y 的取值数量;

信息熵表示随机变量不确定性的大小,信息熵越大,不确定性越高。上面公式中的 Y 代表样本的类别,在解决分类问题时希望 Y 的不确定性越小越好,即信息熵越小越好;

问题是如何减少信息熵,实际上,可以增加已知条件,减少不确定性。例如,在不知道申请人任何特征的情况下,对于是否同意其贷款是很不确定的;但如果知道了这个人有房有工作,那么就很容易做出决定了。

条件熵
条件熵 H ( Y ∣ X ) H(Y|X) H(YX) 表示已知随机变量 X 的条件下随机变量 Y 的不确定性,定义为 X 给定条件下 Y 的条件概率分布的熵对于 X 的数学期望,用公式表示如下:
H ( Y ∣ X ) = ∑ i = 1 n x p x , i H ( Y ∣ X = x i ) H(Y|X)=\sum_{i=1}^{n_{x}}p_{x,i}H(Y|X=x_{i}) H(YX)=i=1nxpx,iH(YX=xi)
其中, p x , i = P ( X = x i ) p_{x,i}=P(X=x_{i}) px,i=P(X=xi) i ∈ [ 1 , 2 , . . . , n x ] i \in [1,2,...,n_{x}] i[1,2,...,nx] n x n_{x} nx代表随机变量 X 的取值数量, H ( Y ∣ X = x i ) H(Y|X=x_{i}) H(YX=xi)代表随机变量 X = x i X=x_{i} X=xi条件下的随机变量 Y Y Y的子集的信息熵;

信息增益
信息增益(information gain)表示已知特征 X 的信息使得类别 Y 的不确定性减少的程度。从定义上容易得知:特征 X 的信息增益 = Y 的信息熵 - Y 的条件熵,当信息熵和条件熵中的概率由数据估计(如极大似然估计)得到时,所对应的信息熵与条件熵分别称为经验熵和经验条件熵;

对于训练数据 D D D,信息增益的计算步骤如下:

  • 1.计算样本类别的经验熵 H ( D ) H(D) H(D)
    H ( D ) = − ∑ k = 1 K ∣ C k ∣ ∣ D ∣ l o g 2 ( ∣ C k ∣ ∣ D ∣ ) H(D)=-\sum_{k=1}^{K}\frac{|C_{k}|}{|D|}log_{2}(\frac{|C_{k}|}{|D|}) H(D)=k=1KDCklog2(DCk)
    其中, ∣ D ∣ |D| D为训练集D的样本数量, ∣ C k ∣ |C_{k}| Ck代表类别为 C k C_{k} Ck的样本数量, K K K代表训练集 D D D的样本类别数量;
    实现为:
from math import log2
from collections import Counter

# 经验熵的实现
def H(y):
    '''
    y: 随机变量 y 的一组观测值,例如:[1,1,0,0,0]
    '''
    # 随机变量 y 取值的概率估计值
    probs = [n/len(y) for n in Counter(y).values()]
    # 经验熵:根据概率值计算信息量的数学期望
    return sum([-p*log2(p) for p in probs])
  • 2.计算给定特征 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 ) = − ∑ i = 1 n ∣ D i ∣ ∣ D ∣ ∑ k = 1 K ∣ D i k ∣ ∣ D i ∣ l o g 2 ( ∣ D i k ∣ ∣ D i ∣ ) H(D|A)=\sum_{i=1}^{n}\frac{|D_{i}|}{|D|}H(D_{i})=-\sum_{i=1}^{n}\frac{|D_{i}|}{|D|}\sum_{k=1}^{K}\frac{|D_{ik}|}{|D_{i}|}log_{2}(\frac{|D_{ik}|}{|D_{i}|}) H(DA)=i=1nDDiH(Di)=i=1nDDik=1KDiDiklog2(DiDik)
    其中, n n n代表特征 A A A的取值数量,
    D i D_{i} Di代表特征 A = a i A=a_{i} A=ai对应的样本子集,
    ∣ D i ∣ |D_{i}| Di是样本子集 D i D_{i} Di的样本数量,
    ∣ D ∣ |D| D是训练集 D D D的样本数量,
    H ( D i ) H(D_{i}) H(Di)是样本子集 D i D_{i} Di的经验熵,
    K K K为样本子集 D i D_{i} Di的样本类别数量,
    D i k D_{ik} Dik是样本子集 D i D_{i} Di中属于类别 C k C_{k} Ck的样本集合,
    ∣ D i k ∣ |D_{ik}| Dik是样本子集 D i k D_{ik} Dik的样本数量;
    实现为:
# 经验条件熵的实现
def cond_H(a):
    '''
    a: 根据某个特征的取值分组后的 y 的观测值,例如:
       [[1,1,1,0],
        [0,0,1,1]]
       每一行表示特征 A=a_i 对应的样本子集
    '''
    # 计算样本总数
    sample_num = sum([len(y) for y in a])
    # 返回条件概率分布的熵对特征的数学期望
    return sum([(len(y)/sample_num)*H(y) for y in a])
  • 3.计算特征 A A A 对于训练数据 D D D 的信息增益 g ( D , A ) g(D,A) g(D,A)
    g ( D , A ) = H ( D ) − H ( D ∣ A ) g(D,A)=H(D)-H(D|A) g(D,A)=H(D)H(DA)
    根据信息增益选择特征的方法是:对于训练数据集 D D D,计算其每个特征的信息增益,并比较它们的大小,选择信息增益最大的特征。

比如对于数据集 D D D
fig4
判断特征 x 0 x_{0} x0 x 1 x_{1} x1 对于数据集 D D D 的信息增益哪个更大:
fig5
所以,应该选择特征 x 0 x_{0} x0

信息增益比
以信息增益划分训练数据集的特征,会出现偏向于取值数较多的特征,例如根据某个特征的取值可以将 N N N 个训练样本划分为 N N N 个子集,则每个子集的经验熵均为零。为了解决这个问题,可以使用信息增益比来进行特征选择。

特征 A A A 对训练数据集 D D D 的信息增益比用公式表示如下:
g R ( D , A ) = g ( D , A ) H A ( D ) g_{R}(D,A)=\frac{g(D,A)}{H_{A}(D)} gR(D,A)=HA(D)g(D,A)
H A ( D ) = − ∑ i = 1 n ∣ D i ∣ ∣ D ∣ l o g 2 ( ∣ D i ∣ ∣ D ∣ ) H_{A}(D)=-\sum_{i=1}^{n}\frac{|D_{i}|}{|D|}log_{2}(\frac{|D_{i}|}{|D|}) HA(D)=i=1nDDilog2(DDi)
其中, n n n为特征 A A A的取值个数, D i D_{i} Di代表特征 A = a i A=a_{i} A=ai对应的样本子集,特征 A A A的取值数越多, H A ( D ) H_{A}(D) HA(D)就越大,从而将信息增益的值向下调整,即:特征 A 取值的经验熵越大,则它对于样本类别的信息增益比越小

决策树的生成与剪枝

决策树常用两种算法:ID3算法和C4.5算法,两者的区别在于特征选择方法的不同,ID3算法使用信息增益选择特征,C4.5算法使用信息增益比选择特征。由于决策树的生成只考虑对训练数据进行拟合,没有考虑模型的泛化能力。故还需要使用损失函数极小化来对生成的决策树进行剪枝。

决策树的生成

为了方便描述,对数据集 D D D,每个样本加一个ID值,如图所示:
fig6
从根结点开始生成决策树,先将上述训练样本(0-8)全部放在根节点中。然后选择信息增益或信息增益比最大的特征向下分裂,按照所选特征取值的不同将训练样本分发到不同的子结点中。例如所选特征有3个取值,则分裂出3个子结点,然后在子结点中重复上述过程,直到所有特征的信息增益(比)很小或者没有特征可选为止,完成决策树的构建,如下图所示:
fig7
图中的决策树共有2个内部结点和3个叶子结点,每个结点旁边的编号代表训练样本的 ID 值。内部结点代表样本的特征,叶子结点代表样本的预测类别,通常将叶子节点中训练样本占比最大的类作为决策树的预测标记。

在使用构建好的决策树在测试数据上分类时,只需要从根节点开始依次测试内部结点代表的特征即可得到测试样本的预测分类。

下面是 ID3分类决策树的生成算法:


假设输入:训练数据集 D D D 、特征集合 A A A 的信息增益阈值 ;输出:决策树 T T T


  1. D D D 中的训练样本属于同一类,则 T T T 为单结点树,返回 D D D 中任意样本的类别。
  2. A A A 中的特征为空,则 T T T 为单结点树,返回 D D D 中数量最多的类别。
  3. 使用信息增益在 A A A 中进行特征选择,若所选出的特征 A i A_{i} Ai 的信息增益小于设定的阈值,则 T T T 为单结点树,返回 D D D 中数量最多的类别。
  4. 否则根据 A i A_{i} Ai 的每一个取值,将 D D D 分成若干子集 D i D_{i} Di,将 D i D_{i} Di 中数量最多的类作为标记值,构建子结点,返回 T T T
  5. D i D_{i} Di 为训练集,{ A − A i A - A_{i} AAi} 为特征集,递归地调用上述步骤,得到子树 T i T_{i} Ti,返回 T T T

使用 C4.5 算法进行决策树的生成只需要将信息增益改成信息增益比即可。

决策树剪枝

决策树的损失函数

决策树的叶子节点越多,模型越复杂。决策树的损失函数考虑了模型复杂度;可以通过优化其损失函数来对决策树进行剪枝。决策树的损失函数计算过程如下:

  1. 计算叶子结点 t t t 的样本类别经验熵:
    H t ( T ) = − ∑ k = 1 K N t k N t l o g N t k N t H_{t}(T)=-\sum_{k=1}^{K}\frac{N_{tk}}{N_{t}}log\frac{N_{tk}}{N_{t}} Ht(T)=k=1KNtNtklogNtNtk
    其中, N t N_{t} Nt为叶子节点 t t t 包含的训练样本个数, N t k N_{tk} Ntk为叶子节点 t t t 中样本类别为 C k C_{k} Ck 的样本个数;对于叶子结点 t t t 来说,其样本类别的经验熵越小, t t t 中训练样本的分类误差就越小。当叶子结点 t t t 中的训练样本为同一类别时,经验熵为零,分类误差为零。
  2. 计算决策树 T T T 在所有训练样本上的损失之和 C ( T ) C(T) C(T)
    C ( T ) = ∑ t = 1 ∣ T ∣ N t H t ( T ) C(T)=\sum_{t=1}^{|T|}N_{t}H_{t}(T) C(T)=t=1TNtHt(T)
    N t N_{t} Nt为叶子节点 t t t 包含的训练样本个数, ∣ T ∣ |T| T是决策树 T T T 包含的叶子节点个数;对于叶子结点 t t t 中的每一个训练样本,其类别标记都是随机变量 Y Y Y 的一个取值,这个取值的不确定性用信息熵来衡量,且可以用经验熵来估计。由上文可知,经验熵在一定程度上可以反映决策树在该样本上的预测损失,累加所有叶子结点上的训练样本损失即上面的计算公式。
  3. 计算考虑模型复杂度的的决策树损失函数:
    C α ( T ) = C ( T ) + α ∣ T ∣ , α ⩾ 0 C_{\alpha}(T)=C(T)+\alpha |T|,\alpha \geqslant 0 Cα(T)=C(T)+αT,α0
    α \alpha α为正则化系数,控制模型复杂度;决策树的叶子结点个数表示模型的复杂度,通过最小化上面的损失函数,一方面可以减少模型在训练样本上的预测误差,另一方面可以控制模型的复杂度,保证模型的泛化能力。

决策树的剪枝算法

  • 1.计算决策树中每个结点的样本类别经验熵,便于对不同形式的树组合起来计算该树的损失函数,如图所示的决策树,需要计算 5 个结点的样本类别经验熵:
    fig8
  • 2.遍历非叶子结点,剪枝相当于去除其子结点,自身变为叶子结点,在各种形式的树中选择损失函数小的树作为结果:
    fig9

实际上还有一种决策树算法:分类与回归树(classification and regression tree,简称 CART),它既可以用于分类也可以用于回归,同样包含了特征选择、决策树的生成与剪枝算法。关于 CART 的内容更多会在 XGBoost中


Python实现决策树

首先创建数据集:

import pandas as pd

# 样本数据
data = [['yes', 'no', '青年', '同意贷款'],
        ['yes', 'no', '青年', '同意贷款'],
        ['yes', 'yes', '中年', '同意贷款'],
        ['no', 'no', '中年', '不同意贷款'],
        ['no', 'no', '青年', '不同意贷款'],
        ['no', 'no', '青年', '不同意贷款'],
        ['no', 'no', '中年', '不同意贷款'],
        ['no', 'no', '中年', '不同意贷款'],
        ['no', 'yes', '中年', '同意贷款']]

# 转为 dataframe 格式
df = pd.DataFrame(data)
# 设置列名
df.columns = ['有房?', '有工作?', '年龄', '类别']

df

fig10
定义经验熵的计算函数:

from math import log2
from collections import Counter

# 经验熵的实现
def H(y):
    '''
    y: 随机变量 y 的一组观测值,例如:[1,1,0,0,0]
    '''
    # 随机变量 y 取值的概率估计值
    probs = [n/len(y) for n in Counter(y).values()]
    # 经验熵:根据概率值计算信息量的数学期望
    return sum([-p*log2(p) for p in probs])

定义经验条件熵的计算函数:

# 经验条件熵的实现
def cond_H(a):
    '''
    a: 根据某个特征的取值分组后的 y 的观测值,例如:
       [[1,1,1,0],
        [0,0,1,1]]
       每一行表示特征 A=a_i 对应的样本子集
    '''
    # 计算样本总数
    sample_num = sum([len(y) for y in a])
    # 返回条件概率分布的熵对特征的数学期望
    return sum([(len(y)/sample_num)*H(y) for y in a])

定义函数计算信息增益:

def feature_select(df,feats,label):
    '''
    df:训练集数据,dataframe 类型
    feats:候选特征集
    label:df 中的样本标记名,字符串类型
    '''
    # 最佳的特征与对应的信息增益
    best_feat,gain_max = None,-1
    # 遍历每个特征
    for feat in feats:
        # 按照特征的取值对样本进行分组
        group = df.groupby(feat)[label].apply(lambda x:x.tolist()).tolist()
        
        """
        比如:
        df.groupby('有房?')['类别'].apply(lambda x:x.tolist()).tolist()
        [['不同意贷款', '不同意贷款', '不同意贷款', '不同意贷款', '不同意贷款', '同意贷款']-----no,
         ['同意贷款', '同意贷款', '同意贷款']------yes]
        """
        
        # 计算该特征的信息增益:经验熵-经验条件熵
        gain = H(df[label].values) - cond_H(group)
        # 更新最大信息增益和对应的特征
        if gain > gain_max:
            best_feat,gain_max = feat,gain
        
    return best_feat,gain_max 

对于上面的信息增益计算,在访问每个特征时,加入当前特征的经验熵作为分母,即转换成信息增益比:

def feature_select(df,feats,label):
    '''
    df:训练集数据,dataframe 类型
    feats:候选特征集
    label:df 中的样本标记名,字符串类型
    '''

    # 最佳的特征与对应的信息增益比
    best_feat,gainR_max = None,-1
    # 遍历每个特征
    for feat in feats:
        # 按照特征的取值对样本进行分组
        group = df.groupby(feat)[label].apply(lambda x:x.tolist()).tolist()
        # 计算该特征的信息增益:经验熵-经验条件熵
        gain = H(df[label].values) - cond_H(group)
        # 计算该特征的信息增益比
        gainR = gain / H(df[feat].values)
       
        # 更新最大信息增益比和对应的特征
        if gainR > gainR_max:
            best_feat,gainR_max = feat,gainR
        
    return best_feat,gainR_max 

定义决策树生成函数:

import pickle
def creat_tree(df,feats,label):
    '''
    df:训练集数据,dataframe 类型
    feats:候选特征集,字符串列表
    label:df 中的样本标记名,字符串类型
    '''
    # 当前候选的特征列表
    feat_list = feats.copy() # 浅拷贝
    
    # 若当前训练数据的样本标记值只有一种
    if df[label].nunique()==1:
        # 将数据中的任意样本标记返回,这里取第一个样本的标记值
        return df[label].values[0]
    # 若候选的特征列表为空时
    if len(feat_list)==0:
        # 返回当前数据样本标记中的众数,各类样本标记持平时取第一个
        return df[label].mode()[0]
    # 在候选特征集中进行特征选择
    feat,gain = feature_select(df,feat_list,label)
    # 若选择的特征信息增益太小,小于阈值 0.1
    if gain<0.1:
        # 返回当前数据样本标记中的众数
        return df[label].mode()[0]
    
    # 根据所选特征构建决策树,使用字典存储
    tree = {feat:{}}
    # 根据特征取值对训练样本进行分组
    g = df.groupby(feat)
    
    """
    g = df.groupby('有房?')
    g.groups
    {'no': Int64Index([3, 4, 5, 6, 7, 8], dtype='int64'),
     'yes': Int64Index([0, 1, 2], dtype='int64')}
    """
    
    # 用过的特征要移除
    feat_list.remove(feat)
    # 遍历特征的每个取值 i
    for i in g.groups:
        # 获取分组数据,使用剩下的候选特征集创建子树
        """
        print(g.get_group('no'))
           有房? 有工作?  年龄     类别
        3  no     no        中年  不同意贷款
        4  no     no        青年  不同意贷款
        5  no     no        青年  不同意贷款
        6  no     no        中年  不同意贷款
        7  no     no        中年  不同意贷款
        8  no     yes       中年   同意贷款
        """
        tree[feat][i] = creat_tree(g.get_group(i),feat_list,label)
    
    # 存储决策树
    # wb:以二进制格式打开一个文件只用于写入,如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除
    pickle.dump(tree,open('tree.model','wb'))
        
    return tree

定义决策树分类函数:

def predict(tree,feats,x):
    '''
    tree:决策树,字典结构
    feats:特征集合,字符串列表
    x:测试样本特征向量,与 feats 对应
    '''
    # 获取决策树的根结点:对应样本特征
    root = next(iter(tree))
    # 获取该特征在测试样本 x 中的索引
    i = feats.index(root)
    # 遍历根结点分裂出的每条边:对应特征取值
    for edge in tree[root]:
        # 若测试样本的特征取值=当前边代表的特征取值
        if x[i]==edge:
            # 获取当前边指向的子结点
            child = tree[root][edge]
            # 若子结点是字典结构,说明是一颗子树
            if type(child)==dict:
                # 将测试样本划分到子树中,继续预测
                return predict(child,feats,x)
            # 否则子结点就是叶子节点
            else:
                # 返回叶子节点代表的样本预测值
                return child

算法测试与评估:

# 获取特征名列表 
feats = list(df.columns[:-1])
print(feats) # ['有房?', '有工作?', '年龄']

# 获取标记名
label = df.columns[-1]
print(label) # 类别

# 创建决策树(此处使用信息增益比进行特征选择)
T = creat_tree(df,feats,label)
print(T) # {'有房?': {'no': {'有工作?': {'no': '不同意贷款', 'yes': '同意贷款'}}, 'yes': '同意贷款'}}

# 若已生成过决策树,可以直接加载
T = pickle.load(open('tree.model','rb'))
print(T) # {'有房?': {'no': {'有工作?': {'no': '不同意贷款', 'yes': '同意贷款'}}, 'yes': '同意贷款'}}

# 计算训练集上的预测结果
preds = [predict(T,feats,x) for x in df[feats].values]
# 计算准确率
sum([int(i) for i in (df[label].values==preds)])/len(preds)
#1.0

从上面结果可以看出,算法只进行了决策树的生成,而没有进行决策树的剪枝。一般来说,未进行剪枝的决策树会倾向过拟合,具体表现:在训练集上的准确率很高,测试集上的准确率很低

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值