机器学习算法——利用AdaBoost元算法提高分类性能(基于单层决策树构建的弱分类器)

当做出重要决定时,我们往往会听取多个专家而不只是一个人的意见。元算法正是采用这种思路,元算法是对其他算法进行组合的一种方式,本篇博文主要介绍AdaBoost元算法,该算法是机器学习工具箱中最强有力的工具之一。
博文用到的数据集和代码:
链接:https://pan.baidu.com/s/1cJ6B1Z8tFUDopnXsEQBnrw
提取码:01gm

集成学习

1、基于数据集多重抽样的分类器

之前的博文已经介绍了几种常用的分类器,包括决策树,朴素贝叶斯,支持向量机等。它们有着不同的特性,我们可以将不同的分类器组合起来,这种组合结果就被称为集成方法或者元算法。
使用集成方法时有多种形式:可以是不同算法的集成,也可以是同一种算法在不同设置下的集成,还可以是数据集不同部分分配给不同分类器之后的集成。

AdaBoost
优点:泛化错误率低,易编码,可以应用在大部分分类器上,无参数调整。
缺点:对离群点敏感
适用数据类型:数值型和标称型数据

1.1 bagging:基于数据随机重抽样的分类器构建方法

自举汇聚法(boostrap aggregating) ,也称为bagging方法,是在从原始数据集选择S次后得到S个新数据集的一种技术。新数据集和原数据集的大小相等。每个数据集都是通过在原始数据集中随机选择一个样本来进行替换而得到的。
也就是说,从原始集合中随机选择一个样本,然后随机选择一个样本来代替这个样本。bagging中的数据集通常被认为是放回取样得到的,比如要得到一个大小为n的新数据集,该数据集中的每个样本都是在原始数据集中随机抽样(即抽样之后又放回)得到的。
因此,替换就意味着可以多次地选择同一个样本,这一性质也使得新数据集中可以出现重复的值,而原始数据集的某些值在新集合中则不再出现。
在S个数据集构建好之后,将某个学习算法分别作用于每个数据集就得到了S个数据集。当我们要面对新数据进行分类时,就可以应用这S个分类器进行分类,与此同时,选择分类器投票结果中最多的类别作为最后的分类结果。
当然,除了上述的bagging方法,还有更先进的方法,例如随机森林,限于篇幅,按下不表,后续会增加一篇博文继续学习。

1.2 boosting:

boosting是一种与bagging很类似的技术。在boosting和bagging中,所使用的多个分类器的类型都是一致的。但是在前者中,不同的分类器是通过串行训练获得的,每个新分类器都根据已训练出的分类器的性能来进行训练。boosting是通过集中关注被已有分类器错分的那些数据来获取新的分类器。
由于boosting分类的结果是基于所有分类器的加权求和结果的,因此boosting与bagging不太一样。bagging中的分类器全中国是相等的,而boosting中的分类器权重并不相等,每个权重代表的是其对应分类器在上一轮迭代中的成功度。
本文重点介绍AdaBoost

Adaboost的一般流程
(1)收集数据:可以使用任何方法,诸如爬虫等
(2)准备数据:依赖于所使用的弱分类器类型,后续将使用单层决策树,该分类其可以处理处理任何数据类型,也可以使用其他任意类型的弱分类器,作为弱分类器,简单分类器的效果最好
(3)分析数据:可以绘制出相关的散点图等
(4)训练算法:AdaBoost的大部分时间都在训练上,分类器将多次在同一数据集上训练弱分类器
(5)测试算法:计算分类的正确率
(6)使用算法:和SVM一样,原始的AdaBoost用于二分类,如果需要应用多分类,就需要像多类SVM中一样对算法做出修改。

boost 算法系列的起源来自于PAC Learnability(直译过来称为:PAC 可学习性)。这套理论主要研究的是什么时候一个问题是可被学习的。PAC 模型的作用相当于提供了一套严格的形式化语言来陈述以及刻画这里所提及的可学习性 Learnability 以及(样本)复杂度 (Sample) Complexity 问题。

PAC 定义了学习算法的强弱:

  • 弱学习算法:识别错误率小于1/2(即准确率仅比随机猜测略高的学习算法)
  • 强学习算法:识别准确率很高并能在多项式时间内完成的学习算法。

更为严格的定义:

  • 弱学习算法:一个概念如果存在一个多项式的学习算法能够学习它,并且学习的正确率仅比随机猜测略好(高于50%),那么,这个概念是弱可学习的
  • 强学习算法:一个概念如果存在一个多项式的学习算法能够学习它,并且正确率很高,那么,这个概念是强可学习的。

2、 基于错误提升分类器的性能

任意给定仅比随机猜测略好的弱学习算法 ,是否可以将其提升为强学习算法?
或者说能否使用弱分类器和多个实例来构建一个强分类器?

AdaBoost算法正是脱胎于此。AdaBoost是adaptive boosting的缩写。
Adaboost是一种迭代算法,其核心思想是针对同一个训练集训练不同的分类器,即弱分类器,然后把这些弱分类器集合起来,构造一个更强的最终分类器,比起弱分类器,这个“强”分类器的错误率会低很多。

Adaboost算法本身是改变数据分布实现的,它根据每次训练集之中的每个样本的分类是否正确,以及上次的总体分类的准确率,来确定每个样本的权值。将修改权值的新数据送给下层分类器进行训练,然后将每次训练得到的分类器融合起来,作为最后的决策分类器。以下给出 Adaboost算法的运行过程:

  1. 训练数据中的每个样本,并赋予其一个权重,这些权重构成向量D,一开始时权重D初始化为相等的值;
  2. 先在训练样本上训练得到第一个弱分类器并计算分类器的错误率 ;
  3. 在同一数据集上再次训练弱分类器,在分类器的二次训练中,会重新调整每个样本的权重,其中第一次分类正确的样本的权重将会降低,而分类错误的样本权重将会提高
  4. 为了从所有弱分类器中得到最终的分类结果,Adaboost为每个分类器都分配了一个权重值alpha,这一组值是基于每个弱分类器的错误率进行计算的。

其中错误率ε的定义为:
在这里插入图片描述
而α的计算公式如下:
在这里插入图片描述
AdaBoost算法的流程如下:
在这里插入图片描述
图中的左边表示数据集,其中直方图的不同宽度表示每个样例上的不同权重。在经过一个分类器之后,加权的预测结果会通过三角形中的alpha值进行加权,每个三角形中输出的加权结果在圆形中求和,从而得到最终的输出结果。
计算出alpha值后,可以对权重向量D进行更新,使得那些正确分类的样本的权重降低而错分样本的权重升高。计算方法如下:

对于正确分类的样本,其权重更改为:
在这里插入图片描述
对于错误分类的样本,其权重更改为:
在这里插入图片描述
在计算出权重向量D后,Adaboost方法开始进入下一轮的迭代。Adaboost方法会不断地重复训练和调整权重的过程,知道训练错误率为0(或达到用户指定的条件)为止。

Adaboost的训练过程就像一个学生学习的过程:我们把每个训练样例看做一道练习题,所有的训练样本看做一本习题集。第一次做题的时候,由于每道题都没有做过,不知道哪些难哪些简单,所以一视同仁,做完了对照答案,可能会有很多题目做的不对,那么对于做错的题目,我们就重点标记,给予更高的重视程度,这个用权重w来标示,到第二轮做题的时候就重视这些没做对的“难题”,对于第一次就做对的题目,可以认为是比较简单的,那么第二次的时候稍微看下就可以了,可以降低他的权重。并且,对于第一轮做完以后的效果给一个整体的评分,评价这轮做题的能力,这个就是alpha。在第二轮做题的时候,就按照上一轮调整过的权重对不同的题目给予不同的重视程度和时间分配。如此不断练习,几轮下来,难题就逐渐被攻克了。每轮做题都是有收获的,只不过每次收获的知识权重不同(alpha),这样,我们最后就得到m个分类器,综合每个分类器的权重,我们就能得到一个“学习成绩很好”的分类器了。

当然,这种训练思路不是没有弊端的:如果有个别极端数据导致分类器的分类失败,那么算法会着重考虑这个极端数据的情况,这也就使得AdaBoost对离群点非常敏感。

3、 基于单层决策树构建弱分类器

单层决策树(decision stump, 也称决策树桩)是一种简单的决策树。前面我们已经介绍了决策树的工作原理,接下来将构建一个单层决策树,而它仅基于单个特征来做决策。由于这棵树只有一次分裂过程,因此它实际上就是一个树桩。
数据集加载代码如下:

def loadSimpData():
    datMat = matrix([[ 1. ,  2.1],
        [ 2. ,  1.1],
        [ 1.3,  1. ],
        [ 1. ,  1. ],
        [ 2. ,  1. ]])
    classLabels = [1.0, 1.0, -1.0, -1.0, 1.0]
    return datMat,classLabels

绘制数据集代码如下:

def draw(dataSet, labelSet):
    x1 = []
    x2 = []
    y1 = []
    y2 = []
    for i in range(len(labelSet)):
        if labelSet[i] == 1.0:
            x1.append(dataSet[i][0])
            y1.append(dataSet[i][1])
        else:
            x2.append(dataSet[i][0])
            y2.append(dataSet[i][1])
    axes = plt.figure().add_subplot(111)
    # 正类数据集
    first = axes.scatter(x1, y1, c='red')
    # 负类数据集
    second = axes.scatter(x2, y2, c='green')
    # 防止出现中文乱码
    chinese_font = matplotlib.font_manager.FontProperties(fname='C:\Windows\Fonts\simsun.ttc')
    plt.title('单层决策树测试数据', fontproperties=chinese_font)
    plt.show()

datMat, classLabels = loadSimpData()
draw(datMat, classLabels)

在这里插入图片描述
r如果想要试着从某个坐标轴上选择一个值(即选择一条与坐标轴平行的直线)来将所有的红色点和绿色点分开是不可能的。这就是单层决策树难以处理的著名问题,通过使用多棵单层决策树,我们就可以构建一个能够对该数据集完全正确分类的分类器。

有了数据,接下来就岢以通过构建多个函数来建立单层决策树。
第一个函数将用于测试是否有某个值小于或者大于我们正在测试的阈值。第二个函数则更加复杂一些,它会在一个加权数据集中循环,并找到具有最低错误率的单层决策树。
这个程序的伪代码看起来大致如下:
将最小错误率minError设为+00
对数据集中的每一个特征(第一层循环):
  对每个步长(第二层循环):
    对每个不等号(第三层循环):
      建立一棵单层决策树并利用加权数据集对它进行测试
      如果错误率低于m in Err0r,则将当前单层决策树设为最佳单层决策树返回最佳单雇决策树

单层决策树生成函数,代码如下:

def stumpClassify(dataMatrix, dimen, threshVal, threshIneq):  # just classify the data
    # 构建(m,1)单位数组
    retArray = np.ones((np.shape(dataMatrix)[0], 1))
    # 通过数组过滤实现分类
    if threshIneq == 'lt':
        retArray[dataMatrix[:, dimen] <= threshVal] = -1.0
    else:
        retArray[dataMatrix[:, dimen] > threshVal] = -1.0
    return retArray


def buildStump(dataArr, classLabels, D):
    dataMatrix = np.mat(dataArr)
    # 求出数据集维度
    labelMat = np.mat(classLabels).T
    m, n = np.shape(dataMatrix)
    # 特征由最小值增加到最大值的步数
    numSteps = 10.0
    bestStump = {}
    bestClasEst = np.mat(np.zeros((m, 1)))
    minError = np.inf  # 初始化错误率为正无穷
    for i in range(n):  # 对所有维度进行循环
        # 求第i个特征的最小值,最大值
        rangeMin = dataMatrix[:, i].min()
        # 特征由最小值增加到最大值的步长
        rangeMax = dataMatrix[:, i].max()
        stepSize = (rangeMax - rangeMin) / numSteps
        for j in range(-1, int(numSteps) + 1):  # 在当前维度的取值中循环所有值
            for unequal in ['lt', 'gt']:  # 循环小于和大于
                # 分割数据集的特征值
                threshVal = (rangeMin + float(j) * stepSize)
                # 得出分类数组
                predictedVals = stumpClassify(dataMatrix, i, threshVal,
                                              unequal)  # 调用单层决策树
                # 存放分类错误的数组,初始化为(m,1)单位数组
                errArr = np.mat(np.ones((m, 1)))
                # 通过数组过滤,将分类正确的设置为0
                errArr[predictedVals == labelMat] = 0
                # 计算加权错误值
                weightedError = D.T * errArr  # 利用D计算总错误率
                print("split: dim %d, thresh %.2f, thresh unequal: %s, the weighted error is %.3f" % (
                    i, threshVal, unequal, weightedError))
                # 如果加权错误值小于最小加权错误值,那么重置加权错误值,并且保存分类数组,存放最佳决策树桩
                if weightedError < minError:
                    minError = weightedError
                    bestClasEst = predictedVals.copy()
                    bestStump['dim'] = i
                    bestStump['thresh'] = threshVal
                    bestStump['unequal'] = unequal
    # 返回最佳决策树桩,最小加权错误值,以及最佳分类数组
    return bestStump, minError, bestClasEst

datMat, classLabels = loadSimpData()
D = np.mat(np.ones((5, 1)) / 5)
print(buildStump(datMat, classLabels, D))
datMat, classLabels = loadSimpData()

运行结果如下:

split: dim 0, thresh 0.90, thresh unequal: lt, the weighted error is 0.400
split: dim 0, thresh 0.90, thresh unequal: gt, the weighted error is 0.600
split: dim 0, thresh 1.00, thresh unequal: lt, the weighted error is 0.400
...........................
split: dim 1, thresh 1.99, thresh unequal: gt, the weighted error is 0.600
split: dim 1, thresh 2.10, thresh unequal: lt, the weighted error is 0.600
split: dim 1, thresh 2.10, thresh unequal: gt, the weighted error is 0.400
({'dim': 0, 'thresh': 1.3, 'unequal': 'lt'}, matrix([[0.2]]), array([[-1.],
       [ 1.],
       [-1.],
       [-1.],
       [ 1.]]))

上述单层决策树的生成函数是决策树的一个简化版本。它就是所谓的弱学习器,即弱分类算法。到现在为止,我们已经构建了单层决策树,并生成了程序,做好了过渡到完整AdaBoost算法的准备。

4、 完整AdaBoost算法的实现

整个实现的伪代码如下:
对每次迭代:
  利用buildStump函数找到最佳的单层决策树
  将最佳单层决策树加入到单层决策树数组
  计算alpha
  计算新的权重向量D
  更新累计类别估计值
  如果错误率等于0 ,则退出循环

基于单层决策树的AdaBoost训练过程,代码如下:

def adaBoostTrainDS(dataArr, classLabels, numIt=40):
    # 最佳决策树桩字典数组
    weakClassArr = []
    # 获取数据集行数
    m = np.shape(dataArr)[0]
    D = np.mat(np.ones((m, 1)) / m)  # 初始化D为分类器数目的平均值
    # 存放预测分类数组,初始化为(m,1)维矩阵
    aggClassEst = np.mat(np.zeros((m, 1)))
    for i in range(numIt):
        # 获取最佳决策树桩,加权错误值,以及预测分类数组
        bestStump, error, classEst = buildStump(dataArr, classLabels, D)  # 构建单层树桩
        print("D:", D.T)
        alpha = float(
            0.5 * np.log(
                (1.0 - error) / max(error, 1e-16)))  # 计算alpha, 利用max函数,以防error=0报错
        bestStump['alpha'] = alpha
        weakClassArr.append(bestStump)  # 存储单层决策树的参数
        print("classEst: ", classEst.T)
        expon = np.multiply(-1 * alpha * np.mat(classLabels).T, classEst)
        D = np.multiply(D, np.exp(expon))  # 为下一轮迭代计算D的值
        D = D / D.sum()
        # 计算所有分类器的错误率, 如果为0,则提前退出
        aggClassEst += alpha * classEst
        print("aggClassEst: ", aggClassEst.T)
        # sign(x)函数表示x>0,返回0;x=0,返回0;x<0;返回-1
        # sign(aggClassEst) != mat(classLabels) 返回bool矩阵,对应元素相等则为True,不等则为False
        # multiply(),False相当于0,True相当于1,表示累加分类错误个数
        aggErrors = np.multiply(np.sign(aggClassEst) != np.mat(classLabels).T, np.ones((m, 1)))
        # 总分类错误率
        errorRate = aggErrors.sum() / m
        print("total error: ", errorRate)
        if errorRate == 0.0:
            break
    return weakClassArr


datMat, classLabels = loadSimpData()
classifierArray = adaBoostTrainDS(datMat, classLabels, 9)
print("classifierArray : ", classifierArray)

运行结果如下:

..........
split: dim 1, thresh 1.88, thresh unequal: lt, the weighted error is 0.571
split: dim 1, thresh 1.88, thresh unequal: gt, the weighted error is 0.429
split: dim 1, thresh 1.99, thresh unequal: lt, the weighted error is 0.571
split: dim 1, thresh 1.99, thresh unequal: gt, the weighted error is 0.429
split: dim 1, thresh 2.10, thresh unequal: lt, the weighted error is 0.857
split: dim 1, thresh 2.10, thresh unequal: gt, the weighted error is 0.143
D: [[0.28571429 0.07142857 0.07142857 0.07142857 0.5       ]]
classEst:  [[1. 1. 1. 1. 1.]]
aggClassEst:  [[ 1.17568763  2.56198199 -0.77022252 -0.77022252  0.61607184]]
total error:  0.0
classifierArray :  [{'dim': 0, 'thresh': 1.3, 'unequal': 'lt', 'alpha': 0.6931471805599453}, {'dim': 1, 'thresh': 1.0, 'unequal': 'lt', 'alpha': 0.9729550745276565}, {'dim': 0, 'thresh': 0.9, 'unequal': 'lt', 'alpha': 0.8958797346140273}]

5、 测试算法:基于AdaBoost的分类

有了多个弱分类器以及对应的alpha值,进行测试就变得相当容易了。我们将在马疝病数据集上应用AdaBoost分类器。

def adaBoostTrainDS(dataArr, classLabels, numIt=40):
    # 最佳决策树桩字典数组
    weakClassArr = []
    # 获取数据集行数
    m = np.shape(dataArr)[0]
    D = np.mat(np.ones((m, 1)) / m)  # 初始化D为分类器数目的平均值
    # 存放预测分类数组,初始化为(m,1)维矩阵
    aggClassEst = np.mat(np.zeros((m, 1)))
    for i in range(numIt):
        # 获取最佳决策树桩,加权错误值,以及预测分类数组
        bestStump, error, classEst = buildStump(dataArr, classLabels, D)  # 构建单层树桩
        print("D:", D.T)
        alpha = float(
            0.5 * np.log(
                (1.0 - error) / max(error, 1e-16)))  # 计算alpha, 利用max函数,以防error=0报错
        bestStump['alpha'] = alpha
        weakClassArr.append(bestStump)  # 存储单层决策树的参数
        print("classEst: ", classEst.T)
        expon = np.multiply(-1 * alpha * np.mat(classLabels).T, classEst)
        D = np.multiply(D, np.exp(expon))  # 为下一轮迭代计算D的值
        D = D / D.sum()
        # 计算所有分类器的错误率, 如果为0,则提前退出
        aggClassEst += alpha * classEst
        print("aggClassEst: ", aggClassEst.T)
        # sign(x)函数表示x>0,返回0;x=0,返回0;x<0;返回-1
        # sign(aggClassEst) != mat(classLabels) 返回bool矩阵,对应元素相等则为True,不等则为False
        # multiply(),False相当于0,True相当于1,表示累加分类错误个数
        aggErrors = np.multiply(np.sign(aggClassEst) != np.mat(classLabels).T, np.ones((m, 1)))
        # 总分类错误率
        errorRate = aggErrors.sum() / m
        print("total error: ", errorRate)
        if errorRate == 0.0:
            break
    return weakClassArr
    
def loadDataSet(fileName):  # 读取利用tab分隔的浮点数据
    numFeat = len(open(fileName).readline().split('\t'))  # 获取特征数
    dataMat = []
    labelMat = []
    fr = open(fileName)
    for line in fr.readlines():
        lineArr = []
        curLine = line.strip().split('\t')
        for i in range(numFeat - 1):
            lineArr.append(float(curLine[i]))
        dataMat.append(lineArr)
        labelMat.append(float(curLine[-1]))
    return dataMat, labelMat

datArr, labelArr = loadDataSet("horseColicTraining2.txt")
classifierArray = adaBoostTrainDS(datArr, labelArr, 55)
testArr, testLabelArr = loadDataSet("horseColicTest2.txt")
prediction55 = adaClassify(testArr, classifierArray)

部分运行结果如下所示:

...........
split: dim 20, thresh 7.07, thresh unequal: lt, the weighted error is 0.491
split: dim 20, thresh 7.07, thresh unequal: gt, the weighted error is 0.509
split: dim 20, thresh 8.08, thresh unequal: lt, the weighted error is 0.487
split: dim 20, thresh 8.08, thresh unequal: gt, the weighted error is 0.513
split: dim 20, thresh 9.09, thresh unequal: lt, the weighted error is 0.487
split: dim 20, thresh 9.09, thresh unequal: gt, the weighted error is 0.513
split: dim 20, thresh 10.10, thresh unequal: lt, the weighted error is 0.482
split: dim 20, thresh 10.10, thresh unequal: gt, the weighted error is 0.518
D: [[0.00392362 0.00951665 0.00124911 0.00444236 0.00404967 0.00119172
  0.00141071 0.01122249 0.00304085 0.00232588 0.00643284 0.00157226
............................
  0.01708484 0.00306095 0.00291896 0.00130803 0.00278509 0.01035272
  0.00553144 0.00602523 0.00087698 0.00334503 0.00698136 0.01279119
  0.00115591 0.00066165 0.00427535 0.00080671 0.00222209 0.00251883
  0.004564   0.00051118 0.00169694 0.00155451 0.00465744 0.00434726
  0.00086599 0.00074284 0.00231914 0.0018587  0.003067   0.00031235
  0.00473017 0.00169354 0.00157171 0.00591104 0.0097876 ]]
classEst:  [[ 1. -1. -1.  1. -1. -1. -1. -1.  1. -1. -1. -1. -1. -1. -1. -1. -1. -1.
  -1. -1.  1. -1. -1. -1. -1. -1. -1. -1. -1. -1.  1. -1. -1. -1.  1. -1.
  -1. -1. -1. -1. -1. -1. -1.  1. -1.  1.  1. -1. -1. -1. -1. -1. -1. -1.
  -1.  1. -1. -1. -1.  1. -1. -1. -1.  1. -1. -1. -1. -1. -1. -1. -1. -1.
  -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1.  1. -1. -1. -1. -1. -1. -1.
.....................
  -1.  1. -1.  1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1.
  -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1.]]
aggClassEst:  [[-0.22774154  0.55241005  1.37232593 -0.10357246 -0.30199903  1.4193594
   1.25066177  0.71728667 -0.48261933  0.75065862 -0.26665819  1.1422417
   1.66754641  0.32512952  0.91828305 -0.49740425 -1.55378853  0.36275552
   0.09549862 -0.174707    0.26733119  2.07773413  1.48738836  1.41683972
   0.41009786  0.1753098   0.26824877  0.88659758  0.12521061  2.35562628
  -0.90445505 -0.00746376  1.02899488  0.67636683 -0.09532266 -2.08500387
...........
   0.87924179 -1.55508737  1.88509242  2.10880281 -0.33341373  0.93199434
   ...........................
   total error:  0.1939799331103679

不同弱分类器数目情况下的AdaBoost测试和分类错误率,该数据集是个难数据集。通常,AdaBoost会达到一个稳定的测试错误率,而并不会随分类器数目的增多而一直提高。
在这里插入图片描述
(注意,图中单位不是百分比,而是小数)
观察表中的测试错误率一栏,就会发现测试错误率在达到了一个最小值之后又开始上升了,即出现了过拟合现象。有文献声称,对于表现好的数据集,AdaBoost的测试错误率就会达到一个稳定值,并不会随着分类器的增多而上升。或许在本例子中的数据集也称不上“表现好”。该数据集一开始有30%的缺失值,对于Logistic回归而言,这些缺失值的假设就是有效的,而对于决策树却可能并不合适。

对比AdaBoost和SVM这两个强大的监督学习算法可以发现,二者其实有很多相似之处。我们可以把弱分类器想象成SVM中的一个核函数,也可以按照最大化某个最小间隔的方式重写AdaBoost算法。而它们的不同就在于其所定义的间隔计算方式有所不同,因此导致的结果也不同。特别是在高纬度空间下,这两者的差异就会更加显著。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值