Python 无框架实现决策树(DecisionTree)

决策树是一类常见的机器学习方法,我们可以通过对样本的属性进行一系列判断,最终决策其所属的标签类别,也就是说决策树是一类分类算法。
以西瓜书第4章决策树所给数据为例,构建决策树的过程大致为,我们每次通过选出“信息熵增益(Gain Information Entropy)”最大的属性,直到最后能够对样本标签进行预测。

请添加图片描述

一、基本概念

信息熵

信息熵 是度量样本集合纯度常用的一种指标。假定当前样本集合D中第k类标签所占的比例为 p k ( k = 1 , 2 , . . . . ∣ Y ∣ ) ( 1.1 ) p_{k}(k=1,2,....|Y|) \qquad(1.1) pk(k=1,2,....Y)1.1,则D的信息熵定义为
E n t ( D ) = − ∑ k = 1 ∣ y ∣ p k l o g 2 p k Ent(D)=-\sum^{|y|}_{k=1}{p_{k}log_{2}p_{k}} Ent(D)=k=1ypklog2pk
其中, E n t ( D ) Ent(D) Ent(D)的值越小,那么D的纯度就越高

信息增益

假定离散属性a有V个可能的取值 a 1 , a 2 , . . . , a V {a^{1},a^{2},...,a^{V}} a1,a2,...,aV,若使用a来对样本集D进行划分,则会产生V个分支节点,其中第v个分支节点包含了D中所有在属性a上取值为 a v {a^{v}} av的样本,记为 D V D^{V} DV。我们可以根据式1.1计算出 D V D^{V} DV的信息熵,再考虑到不同的分支节点所包含的样本数量不同,给分支节点进行加权 ∣ D v / ∣ D ∣ |D^{v}/|D| Dv/D,那么,分支节点样本数越多,则影响越大。给出信息增益公式
G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) ( 1.2 ) Gain(D,a)=Ent(D)-\sum^{V}_{v=1}{\dfrac{|D^{v}|}{|D|}Ent(D^{v})} \qquad(1.2) Gain(D,a)=Ent(D)v=1VDDvEnt(Dv)(1.2)

增益率

信息增益准则对可取值数目较多的属性有所偏好,为减少这种偏好所带来的不利影响,著名的C4.5决策树算法不直接使用信息增益,而是使用“增益率(Gain Ratio)”来选择划分最优属性。增益率采用和(1.2)式相同的符号,其公式表达如下 G a i n R a t i o ( D , a ) = G a i n ( D , a ) I V ( a ) ( 1.3 ) GainRatio(D,a)=\dfrac{Gain(D,a)}{IV(a)}\qquad(1.3) GainRatio(D,a)=IV(a)Gain(D,a)(1.3)
其中, I V ( a ) = − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ l o g 2 ∣ D v ∣ ∣ D ∣ ( 1.4 ) IV(a)=-\sum^{V}_{v=1}{\dfrac{|D_{v}|}{|D|}log_{2}\dfrac{|D^{v}|}{|D|}}\qquad(1.4) IV(a)=v=1VDDvlog2DDv(1.4)
需要注意的是,增益率准则对可取值数目较少的属性有所偏好,因此,C4.5算法并不是直接选择增益率最大的候选划分属性,而是使用了一个启发式:先从候选划分属性中找出信息增益高出平均水平的属性

连续值

上述所有内容全是建立在数据样本特征为离散值的基础之上的,当数据的信息为连续值时,其决策树的构建与离散值还有一定的不同。其具体区别为,如果针对于某一特征a,其取值可能为{a1,a2,a3},由于是离散值,我们可以很自然的由特征a引申出3个分支。但是针对于连续值,我们只能在已有的连续值范围(min,max)间寻找几个端点,用于分支划分。对于给定样本D和连续属性a,假定a在D上出现了n个不同的连续取值,将这些取值从小到大进行排序,记为 a 1 , a 2 , . . . , a 3 {a_{1},a_{2},...,a_{3}} a1,a2,...,a3。基于划分点t,可以将D分为子集 D v − 和 D v + D_{v}^{-}和D_{v}^{+} DvDv+,分别包含在属性a上取值小于t和大于t的样本点。那么对于属性取值 a i 和 a i + 1 a^{i}和a^{i+1} aiai+1来说,t在区间 [ a i , a i + 1 ) [a^{i},a^{i+1}) [ai,ai+1)中取任意值所产生的划分结果相同。因此,对于连续属性a,我们可考察包含n-1个元素的候选划分点集合
T a = a i + a i + 1 2 ∣ 1 ≤ i ≤ n − 1 ( 1.7 ) T_{a}={\dfrac{a^{i}+a^{i+1}}{2}|1\le i\le n-1}\qquad(1.7) Ta=2ai+ai+11in1(1.7)
最终,基于连续值属性的信息增益为 G a i n ( D , a ) = m a x t ∈ T a G a i n ( D , a , t ) Gain(D,a)=max_{t\in T_{a}}Gain(D,a,t) Gain(D,a)=maxtTaGain(D,a,t)
= m a x t ∈ T a E n t ( D ) − ∑ λ ∈ { − , + } ∣ D t λ ∣ ∣ D ∣ E n t ( D t λ ) =max_{t \in T_{a}} Ent(D)-\sum_{\lambda\in \{-,+\}}{\dfrac{|D_{t}^{\lambda}|}{|D|}}Ent(D_{t}^{\lambda}) =maxtTaEnt(D)λ{,+}DDtλEnt(Dtλ)

其他功能

目前囿于时间限制,只完成了基础的离散值和连续值决策树构建,缺失值处理、剪枝等功能待后续有时间和精力继续完善…

二、决策树构建算法

决策树构建算法的伪代码如下:
输入:训练集 D = ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x m , y m ) D={(x_{1},y_{1}),(x_{2},y_{2}),...,(x_{m},y_{m})} D=(x1,y1),(x2,y2),...,(xm,ym);
属性集 A = a 1 , a 2 , . . . , a d A={a_{1},a_{2},...,a_{d}} A=a1,a2,...,ad
过程:
函数TreeGenerate(D,A)
1:生成节点node;
2:if D中的样本全部属于某一个标签类别C
3: then 将node标记为C类叶节点;return
4:end if
5:if A = ∅ A=\emptyset A= OR D中样本在A上的取值全部相同 then
6:将node标记为叶节点,其类别标记为D中样本数量最多的类
7:end if
8:从A中选择最优划分属性a*;
9:for a的每一个值 a v ∗ a_{v}^{*} av do:
10:为node生成一个分支;令 D v D_{v} Dv表示D中在a
上取值为 a v ∗ a_{v}^{*} av 的样本子集;
11:if D v D_{v} Dv为空 then
12:将分支节点标记为叶节点,其类别标记为D中样本组多的类;return
13:else:
14:以TreeGenerate(Dv,A-a*)为分支结点(即用样本子集Dv,和去掉属性a*的属性集再衍生出新的树)
15:end if
16:end for
决策树的生成过程是一个递归函数,导致递归返回的,有三种情况:
一是样本D中的样本标签,也就是y值全都一样,那么就会生成一个叶子节点,然后返回;二是样本D中的样本各属性(或者叫特征)的值完全一样,这样的话,将该节点标记为叶节点,然后将其类别标记为D中数据量最多的标记;三是当前节点所含的样本为空,那么利用其父节点的数据中数量最多的标记来标记当前节点的标签。
那么,只要不是叶节点,都会继续向下开拓出子树。

三、代码实现

节点Node结构定义,在这里我是用一个类然后实例化对象来保存,看过大佬写的文章是用字典来保存。这里可以凭借个人喜好。我选择类的原因是调用各种参数相对比较方便,而且还可以加内置函数。

定义节点类class Node:
class Node:
  def __init__(self,attribute,ifleaf,subnode,label,contivalue=None):
      self.attribute=attribute
      self.ifleaf=ifleaf
      self.subnode=subnode
      self.label=label
      self.contivalue=contivalue

  def UpdateAttr(self,attribute):
      self.attribute=attribute

  def UpdataIfleaf(self,ifleaf):
      self.ifleaf=ifleaf

  def UpdateSubnode(self,subnode):
      self.subnode=subnode

  def UpdateLabel(self,label):
      self.label=label

  def UpdateSubnode(self,attribute,node):
      self.subnode.update({attribute:node})
定义TreeGenerate函数:
def TreeGenerate(data,string,attributes,fatherNode,layer,attrValue):   #attribute为输入属性
    dataNumpy=data.values
    node=Node(None,False,{},None)
    tempValue = attrValue
    if ">" in attrValue:
        fatherNode.contivalue = tempValue.replace(">", "")
    elif "<" in attrValue:
        fatherNode.contivalue = tempValue.replace("<", "")
    labels=data[string]
    labels=np.unique(np.array(labels))
    # theFirstele=data[0][string]
    singleClassMark=True     #用来标记数据集中是否都为同一类型
    singleFeatureMark=True   #用来标记数据集的特征取值是否都一样
   
    if len(labels)>1:    #判断数据集中的标记是否相同
        singleClassMark=False

    if singleClassMark==True:       #如果相同则将其标记为该类叶节点
        node.ifleaf=True
        node.label=data[string][0]

        # return node
        fatherNode.UpdateSubnode(attrValue,node)
        layer+=1
        G.add_node(node.label,subset=layer)
        dot.node(node.label,fontname="Microsoft YaHei")
        if fatherNode.attribute!=None:
            G.add_edge(fatherNode.attribute,node.label,name=attrValue)
            dot.edge(fatherNode.attribute,node.label,label=attrValue,fontname="Microsoft YaHei")
        elif fatherNode.label!=Node:
            G.add_edge(fatherNode.label,node.label,name=attrValue)
            dot.edge(fatherNode.label,node.label,label=attrValue,fontname="Microsoft YaHei")

        return None

    for i in dataNumpy:         #用来判断,数据的特征取值是否都一样
        if (i!=dataNumpy[0]).any():    #只要两组数据中有一个数据不一样,那么特征取值就会不同
            singleFeatureMark=False
            break

    if attributes==None or singleFeatureMark==True:
        node.ifleaf=True
        #获取数据中元素数量最多的标签
        labelDivide,labelKind=DataDivided(data,string)
        templabellen=0
        index=0
        for i in range(len(labelDivide)):
            if len(labelDivide[labelKind[i]])>templabellen:
                templabellen=len(labelDivide[labelKind[i]])
                index=i
        node.label=labelKind[index]

        fatherNode.UpdateSubnode(attrValue,node)
        layer+=1
        G.add_node(node.label,subset=layer)
        dot.node(node.label,fontname="Microsoft YaHei")
        if fatherNode.attribute != None:
            G.add_edge(fatherNode.attribute, node.label,name=attrValue)
            dot.edge(fatherNode.attribute,node.label,label=attrValue,fontname="Microsoft YaHei")

        elif fatherNode.label != Node:
            G.add_edge(fatherNode.label, node.label,name=attrValue)
            dot.edge(fatherNode.label,node.label,label=attrValue,fontname="Microsoft YaHei")

        return None
        # return node
    #从A中选择最优的划分属性
    attrTuple=SelBestAttr(data,attributes,string,attrValue,layer)
    if len(attrTuple)==2:
        bestAttr,bestEntGain=attrTuple[0],attrTuple[1]
        # bestAttrValue=np.unique(data[bestAttr])
        bestAttrDiv,bestAttrValue=DataDivided(data,bestAttr)
        node.attribute=bestAttr
        node.label=None
        fatherNode.UpdateSubnode(attrValue,node)
        layer += 1
        G.add_node(node.attribute, subset=layer)
        dot.node(node.attribute,fontname="Microsoft YaHei")
        if fatherNode.attribute != None:
            G.add_edge(fatherNode.attribute, node.attribute,name=attrValue)
            dot.edge(fatherNode.attribute,node.attribute,label=attrValue,fontname="Microsoft YaHei")
        elif fatherNode.label != Node:
            G.add_edge(fatherNode.label, node.attribute,name=attrValue)
            dot.edge(fatherNode.label,node.attribute,label=attrValue,fontname="Microsoft YaHei")
        attributes.remove(bestAttr)   #将最优特征从特征表中去除
        #为最优属性展开子树
        for i in range(len(bestAttrDiv)):
            # if isinstance(data[bestAttr][1],str):
                if len(bestAttrDiv[bestAttrValue[i]])==0:
                    labelDivide, labelKind = DataDivided(data, string)
                    templabellen = 0
                    index = 0
                    for j in range(len(labelDivide)):

                        if len(labelDivide[labelKind[j]]) > templabellen:
                            templabellen = len(labelDivide[labelKind[j]])
                            index = j
                    newNode1=Node(None,True,None,labelKind[index])
                    node.UpdateSubnode(bestAttrValue[i],newNode1)

                    layer+=1
                    G.add_node(newNode1.label,subset=layer)
                    dot.node(newNode1.label,fontname="Microsoft YaHei")
                    G.add_edge(node.attribute,newNode1.label,name=bestAttrValue[i])
                    dot.edge(node.attribute,newNode1.label,label=bestAttrValue[i],fontname="Microsoft YaHei")
                else:
                    bestAttrDiv[bestAttrValue[i]] = pd.DataFrame(bestAttrDiv[bestAttrValue[i]], columns=data.columns)
                    # newNode2=Node(bestAttrValue[i],False,Node,string)
                    TreeGenerate(bestAttrDiv[bestAttrValue[i]],string,attributes,node,layer,bestAttrValue[i])
    else:
        bestAttr,bestValue,divideNum=attrTuple[0],attrTuple[1],attrTuple[2]
        node.attribute = bestAttr
        node.label = None

        fatherNode.UpdateSubnode(attrValue, node)
        layer += 1
        G.add_node(node.attribute, subset=layer)
        dot.node(node.attribute,fontname="Microsoft YaHei")
        if fatherNode.attribute != None:
            G.add_edge(fatherNode.attribute, node.attribute, name=attrValue)
            dot.edge(fatherNode.attribute,node.attribute,label=attrValue,fontname="Microsoft YaHei")
        elif fatherNode.label != Node:
            G.add_edge(fatherNode.label, node.attribute, name=attrValue)
            dot.edge(fatherNode.label,node.attribute,label=attrValue,fontname="Microsoft YaHei")
        for i in range(len(divideNum)):
            if len(divideNum[i])==0:
                labelDivide, labelKind = DataDivided(data, string)
                templabellen = 0
                index = 0
                for j in range(len(labelDivide)):
                    if len(labelDivide[labelKind[j]]) > templabellen:
                        templabellen = len(labelDivide[labelKind[j]])
                        index = j
                newNode1 = Node(None, True, None, labelKind[index])
                node.contivalue=bestValue
                edgeName="<%f"%bestValue if i==0  else ">%f"%bestValue

                node.UpdateSubnode(edgeName, newNode1)

                layer += 1
                G.add_node(newNode1.label, subset=layer)
                dot.node(newNode1.label,fontname="Microsoft YaHei")
                G.add_edge(node.attribute, newNode1.label, name=edgeName)
                dot.edge(node.attribute,newNode1.label,label=edgeName,fontname="Microsoft YaHei")
            else:
                edgeName = "<%f" % bestValue if i == 0 else ">%f" % bestValue
                TreeGenerate(divideNum[i],string,attributes,node,layer,edgeName)
计算信息熵
def InforEntCreate(data,label):
    labelValue=np.unique(data[label])
    len=data.shape[0]
    countList=[]
    for i in labelValue:
        tempcount=0
        for j in range(len):
            if data[label][j]==i:
                tempcount+=1
        countList.append(tempcount)
    countList=np.array(countList)
    countList=countList/len
    countList=-np.multiply(countList,np.log2(countList))
    return np.sum(countList)

对数据进行分割
def DataDivided(data,attribute):   #用于针对某一特性的数据分割
    singleClassData = dataInit[attribute]
    singleClassData=list(singleClassData)    #将singleClassData转化为列表,从而可以使用数标引用
    attrKinds = np.unique(singleClassData)

    singleClassData=data[attribute]      #将输入数据的某一列特征值抽取
    singleClassData=list(singleClassData)
    dataDivided = {}  # 用于将数据分割保存
    for i in attrKinds:
        dataDivided.update({i:[]})
    data=data.values     #将dataFrame格式变成Narray形式
    for i in range(len(dataDivided)):
        for j in range(len(singleClassData)):
            if singleClassData[j] == attrKinds[i]:
                dataDivided[attrKinds[i]].append(data[j])
    # for i in range(len(dataDivided)):
    #     dataDivided[i]=pd.DataFrame(dataDivided[i],data.columns)
    return dataDivided,attrKinds

计算信息增益
def InforGainEnt(data,attribute,label):
    dataDivided,dataKinds=DataDivided(data,attribute)
    multiInforEnt=0
    for i in range(len(dataDivided)):
        dataDivided[dataKinds[i]]=pd.DataFrame(dataDivided[dataKinds[i]],columns=data.columns)

    length=data.shape[0]
    for i in range(len(dataDivided)):
        multiInforEnt+=InforEntCreate(dataDivided[dataKinds[i]],label)*dataDivided[dataKinds[i]].shape[0]/length
    #将dataFrame重组,以便保证其index从0开始
    data.index=np.arange(data.shape[0])

    totalEnt=InforEntCreate(data,label)
    inforGain=totalEnt-multiInforEnt
    return inforGain

增益率计算
def GainRatio(data,attribute,label):
    length=data.shape[0]
    gain=InforGainEnt(data,attribute,label)
    dataDivided,attrKinds=DataDivided(data,attribute)
    attrIV=0
    for i in range(len(dataDivided)):
        valueLen=len(dataDivided[attrKinds[i]])
        if valueLen!=0:
            attrIV+=-valueLen/length*log2(valueLen/length)

    return gain/attrIV

选择最优划分属性
def SelBestAttr(data,attributes,label,attrValue,layer):
    tempAttr=0
    maxAttr=0
    maxIndex=""
    bestValue=0
    bestDivNum=[]
    tempValue=0
    tempDivNum=[]
    for i in attributes:
        if isinstance(data[i][1],str):


            tempAttr=InforGainEnt(data,i,label)
            # tempAttr=GainRatio(data,i,label)

        else:
            tempAttr,tempValue,tempDivNum=ContiAttrSelect(data,i,label)
        if tempAttr>maxAttr:
            maxAttr=tempAttr
            maxIndex=i
            bestValue=tempValue
            bestDivNum=tempDivNum
    print("maxIndex:",maxIndex,"attributes",attributes,"attrValue",attrValue,"layer",layer)
    if isinstance(data[maxIndex][1],str):      #这里多少得有问题
        return maxIndex,maxAttr
    else:
        return maxIndex,bestValue,bestDivNum

计算连续值信息熵
def ContiAttrSelect(data,attribute,label):
    length=data.shape[0]
    columns=data.columns
    contiAttrs=data[attribute]
    contiAttrs=np.unique(np.array(contiAttrs))
    contiAttrs=np.sort(contiAttrs)
    for i in range(len(contiAttrs)-1):
        contiAttrs[i]=(contiAttrs[i]+contiAttrs[i+1])/2
    contiAttrs=list(contiAttrs)
    contiAttrs.pop(-1)
    tempEnt=0
    tempBest=0
    bestValue=0
    bestDivNum=[]
    for j in contiAttrs:
        divideNum=[[],[]]
        for i in range(length):
            if data[attribute][i]<j:
                divideNum[0].append(data.loc[i])
            else:
                divideNum[1].append(data.loc[i])
        divideNum[0]=pd.DataFrame(divideNum[0],columns=columns,index=np.arange(len(divideNum[0])))
        divideNum[1]=pd.DataFrame(divideNum[1],columns=columns,index=np.arange(len(divideNum[1])))

        tempEnt+=InforEntCreate(divideNum[0],label)*divideNum[0].shape[0]/length
        tempEnt+=InforEntCreate(divideNum[1],label)*divideNum[1].shape[0]/length
        totalEnt=InforEntCreate(data,label)
        if totalEnt-tempEnt>tempBest:
            tempBest=totalEnt-tempEnt
            bestValue=j
            bestDivNum=divideNum
        tempEnt=0

    return tempBest,bestValue,bestDivNum

初始化以及画图

这里我用了两种框架,一个是专门用来画图结构的networkx和另一个Graphviz,两种框架上手都比较快。networkx好像是只能以图的形式表现,如果树的结构比较复杂的话看上去就会比较乱。

G=nx.DiGraph()
dot=Digraph(format="jpg")
attributes=dataInit.columns
attributes=list(attributes)
dataNumpy=dataInit.values
global root
root=Node("root",False,{},None)
G.add_node("root",subset=0)
dot.node("root")
layer=0
TreeGenerate(dataInit,attributes[-1],attributes[:-1],root,layer,"root")
pos=nx.multipartite_layout(G)
edge_labels=nx.get_edge_attributes(G,"name")
nx.draw_networkx(G, node_size=1000, pos=pos,font_family="SimSun")
nx.draw_networkx_edge_labels(G,pos,edge_labels=edge_labels,font_family="SimSun")
pyplot.show()
dot.render("DecisionTree.jpg",view=True)
随机生成样本

原理大概就是,如果某一属性a是离散值的话,那就按照离散值{a1,a2,…,an}生成(0,n)之间的整数,然后将整数标记对应转换为相应的离散值。如果a是连续值,那就获取a属性中的最大最小值,生成(min,max)的随机浮点数。

import numpy as np
from DataProcess import dataInit,attributes
import pandas as pd
size=10000
attrlen={}   #  利用numpy随机数生成字典
for i in attributes:
    if isinstance(dataInit[i][1],str):
        attrcol=dataInit[i]
        attrcol=np.array(attrcol)
        length=len(np.unique(attrcol))
        attrlen.update({i:np.random.randint(length,size=size)})
    else:
        max=np.max(np.array(dataInit[i]))
        min=np.min(np.array(dataInit[i]))
        attrlen.update({i:np.random.rand(size)*(max-min)+min})

dataRan=pd.DataFrame(attrlen)    #利用字典生成伪随机数dataFrame
#将随机整数对应于相应的标签
for i in attributes:
    if isinstance(dataInit[i][1],str):
        attrcol=dataInit[i]
        attrcol=np.array(attrcol)
        attrValue=np.unique(attrcol)
        for j in range(dataRan.shape[0]):
            dataRan[i][j]=attrValue[dataRan[i][j]]
dataRan.to_csv("NewWatermelon2.csv")
测试函数
import pandas as pd
dataTest=pd.read_csv("NewWatermelon2.csv",index_col=0)
# dataTest=dataTest.iloc[:50,:]
from AllKindsDT import root
def WatermelonEstimate(data,node):
    if node.label==None and node.attribute==None:
        print("This node is not a estimate node.")
    if node.label==None and node.attribute!=None:
        if isinstance(data[node.attribute],str):#这里要分属性是离散值还是连续值
            attrValue=data[node.attribute]
            return WatermelonEstimate(data,node.subnode[attrValue])
        else:
            contiValue=float(node.contivalue)
            attrValue=">%f"%contiValue if data[node.attribute]>contiValue else "<%f"%contiValue
            return WatermelonEstimate(data,node.subnode[attrValue])
    if node.label!=None:
        currentLabel=node.label
        return currentLabel

def AccuracyCal(data,node,label):
    length=data.shape[0]
    accuracy=0
    for i in range(data.shape[0]):
        label_pred=WatermelonEstimate(data.loc[i],node)
        if label_pred==data[label][i]:
            accuracy+=1

    accRatio=accuracy/length
    return accRatio

acc=AccuracyCal(dataTest,root.subnode["root"],"好瓜")
print(acc)

西瓜书P84 表4.3数据集生成决策树结构如下请添加图片描述
基于西瓜书数据随机生成的数据量为100的数据生成决策树如下
请添加图片描述
由于连续值属性使用后不用从特征集中删除,可以重复使用,因此连续属性在表现上会产生超多的分支,数据越多分支越多。因此,需要对决策树进行剪枝。
基于西瓜书数据再生成数据量为10000的数据集,然后在上述决策树上进行测试。
决策树-100
准确率为0.4994

决策树-17
准确率为0.5031
在这里插入图片描述
我们可以看到准确率基本都逼近0.5.
因为生成随机数使用的是numpy的random模块。从Numpy的官网可以找到有关于numpy.random的大概说明
在这里插入图片描述
即随机数生成:这个对象将BitGenerator(二进制生成器)生成的一串随机二进制转化为一串遵循特定分(均匀分布,正态分布,二项分布)布具有特定间隔的数。
因此,无论是上述分布中的哪一种,基于生成决策树数据集产生的随机数据集都是关于50%的密度值对称的,也就是说最后只有50%的值分布满足于原数据集。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值