cs231n_激活函数

首先讲述的是激活函数的内容。

在上一个lecture中,我们看到了任意特定层是如何生成输出的,输入数据在全连接层或者卷积层,将输入乘以权重值,之后将结果放入一个激活函数(非线性单元)。


下面是一些激活函数的例子,有在之前使用过的sigmoid函数,之前提及到的ReLU函数。这里会提及更多的激活函数,并在它们之间进行权衡。


先是之前见过的sigmoid函数,这个函数的表达式是 \sigma(x)=1/(1+e^{-x}) 。每一个元素输入到sigmoid非线性函数中,并被压缩到0到1的范围内。如果有一个非常大的输入值,那么输出结果将会非常接近于1;如果有一个非常小的负值,那么输出结果将会非常接近于0。在横坐标接近于0的区域中,可以将这部分看成是线性区域,因为这有点像线性函数。sigmoid函数曾经非常流行,因为它在某种意义上可以被看成是一种神经元的饱和“firing rate(放电率)”。当更为深入地研究这个非线性函数,实际它存在着几个问题。首先是饱和的神经元将会使得梯度消失。


下面是一个sigmoid gate,是计算图中的一个节点,将数据x作为输入,从sigmoid gate传出输出,这时让L对 \sigma 求偏导数,上游梯度往回传播,然后乘以 \sigma 对x的偏导数,这是一个局部的sigmoid函数的梯度,把它们chain起来作为传递回来的下游梯度。当x=-10时会看到梯度的取值非常接近于0,因为这个值太过接近于sigmoid函数的负饱和区域,而这个区域几乎是平的所以梯度也会接近于0。所以返回的上游梯度乘以一个约等于0的数值,会得到一个非常小的梯度,因此在某种意义上说经过链式法则的多次相乘后会让梯度消失,这时零梯度便会传递到下游的节点,同理当x=10时也是相同的情况。当x=0时会得到一个合理的梯度,并可以很好的进行反向传播。


第二个问题是sigmoid是一个非零中心的函数。


当输入神经元的数值始终为正时,再乘以权值W,这时结果会要么全部是正数,要么全部是负数。这时把上游梯度传回来,就是损失L对f进行求导, \partial{L}/\partial{f} 为正数或者负数,当任意传回来一些梯度乘以本地梯度,求关于权值w的梯度 \partial{f}/\partial{w}=x ,如果x的值总是正数,对w的梯度就等于 \partial{L}/\partial{f} 乘以 \partial{f}/\partial{w} ,此时就相当于把上游梯度的符号传回来,这意味着所有关于w的梯度全为正值或者全为负值,那么它们就会总是朝着同一个方向移动。当在做参数更新的时候,可以选择用同一个正数或者用不同的正数去增加所有w的值,或是用类似的方法减小w的值,这时会出现的问题是,这样更新梯度是非常低效的。看到下面右图的例子,假设w是二维的,所以可以用平面坐标轴来表示w,如果用全为正或者全为负的数去进行更新迭代。我们得到两个象限,即在坐标轴上有两个区域,一个全为正(一象限)和一个全为负(三象限),这是根据梯度更新得到的两个方向,假设最好的w是图中蓝色向量,而实际的迭代过程是如红色箭头所示,可以看到不能沿着w这个方向直接求梯度,因为这不是允许的两个梯度方向中的一个。所以在一般情况下要使用均值为0的数据,我们希望输入x的均值为0,就能得到正和负的数值,这就不会陷入上述梯度更新会出现的问题,因为这将会沿着同一个方向移动。


* 这里举一个简单例子,说明sigmoid函数的输出非零中心导致反向传播回w的梯度为全正或者全负是有问题的。假设我们有权值 w=[1,−1,1] ,我们需要将权值更新为 w=[−1,1,−1] ,如果梯度是同时有正和有负的,我们可以只更新一次就可得到结果: w=[1,−1,1]+[−2,2,−2]=[−1,1,−1] ;但是如果梯度只能是正或者只能是负,则需要两次更新才能得到结果: w=[1,−1,1]+[−3,−3,−3]+[1,5,1]=[−1,1,−1]


第三个问题是这里使用了指数函数,这会消耗更多的计算能力,在网络的整体框架中,这个问题一般并不是主要的问题。因为进行卷积和点积的计算能力消耗会更大。


现在来看第二个要介绍的激活函数tanh,它和sigmoid函数看起来非常相似,区别在于它现在被压缩在-1到1之间,同时它是以0位中心的,这就不会存在sigmoid函数的第二个问题;但从图像中看到它也存在几乎是平的区域,所以仍会存在梯度饱和的问题。


接下来看看ReLU这个激活函数,这个函数的形式为 f(x)=max(0,x) ,它在输入中按照元素进行操作,如果输入是负数,会得到结果为0,如果输入的是正数,结果还是输入的数。这是经常使用的一个激活函数,它不会在正的区域产生梯度饱和的现象,因此在一半的输入空间不会出现tanh和sigmoid中出现的梯度饱和问题,这是一个很大的优点。因为ReLU只是进行简单的max操作,所以在计算速度和收敛速度上都会比sigmoid和tanh快,同时也有研究证明ReLU比sigmoid更具备生物学上的合理性。然而对于ReLU,仍然存在一个问题,它不是以0为中心的函数,除此之外还有一个不好的地方是在负半轴中会出现梯度不能更新的问题。


当x=-10时,对于ReLU函数此时梯度为0;而当x=10时,对于ReLU函数此时梯度不会为0;在线性区域内,当x=0时结果是不确定的,但在实践中,可以取x=0时的梯度为0,所以基本上在一半的区域内都会出现梯度消失的问题。


我们可以这个问题成为dead ReLU,当在负半轴的区域中的时候,这里主要有几个可能的原因。如果观察数据云,假设这个就是全部的训练数据,可以看到这些ReLU可能处于的位置。这些ReLU基本上在平面中的一半区域内能够产生激活,在这个平面区域内能够激活的又相应地定义了这些ReLU,可以看到这些dead ReLU基本不再数据云中,因此它从来不会被激活和更新,相比激活的ReLU,一些数据是正数,那么就能进行传递,一些则不会。主要有以下几个原因:第一是当有一个不好的初始化的时候,即权重设置非常的不好,它们恰巧不在数据云中,这就会出现dead ReLU的情况,这就导致不能得到一个能激活神经元的数据输入,同时也不会有一个合适的梯度传回来,它不会被更新和激活。当学习率很大的时候,在这种情况下从一个ReLU函数开始,但因为在进行大量的更新,权值不断波动,然后ReLU单元会被数据的多样性所淘汰。这些会在训练时发生,它在开始阶段很正常,但在某个时间点之后开始变差最后挂掉,所以如果实际上你冻结了一个已经训练好的网络,然后将数据放进去,可以看到实际上网络中有10%到20%的部分是这些挂了的ReLU单元。大多数使用ReLU的网络都有这类问题,在实际运用中大家会深入检查ReLU单元。


在实际运用时,大家也喜欢使用较小的正的偏置值来初始化ReLU,以增加它在初始化时被激活的可能性,并获得一些更新。这基本上只是让更多的ReLUs在一开始就处于能够被激活的状态,然而在实践中有些人认为这一点是有用的,也有的人认为没有,一般大家并不总是使用这个方法,大多数时候只是将偏置项初始化为0。


下面来看看基于ReLU的一些改进的激活函数,例如下面的Leaky ReLU,这和原始的ReLU看上起很相似,唯一的区别是在负区间Leaky ReLU给出了一个微小的负斜率,这解决了之前ReLU中的dead ReLU问题,同时在负区间中的计算仍然是非常高效的。



这里还有另外一个例子,简称为PReLU,它与Leaky ReLU非常相似,在负区间也有这样的一个倾斜的区域,但是现在在负区间的斜率是通过 \alpha 参数确定的。在这里不需要指定这个 \alpha ,而是把 \alpha 当做一个可以反向传播和学习的超参数,这就有了更多的灵活性。


还有叫做Exponential Linear Unit,简称ELU的激活函数,它具有ReLU的所有的优点,同时它的输出均值还接近为0,但与Leaky ReLU相比较,ELU没有在负区间倾斜,在这里实际上在建立一个负饱和机制。这里有一些具有争议的观点认为,这样使得模型对噪声具有更强的robust,然后得到的这些更robust的反激活状态,具体可以看看这篇论文,里面有很多关于为什么这么做的理由。从某种意义上说,这是一种介于ReLUs和Leaky ReLUs之间,同时具有Leaky ReLU所具有的曲线形状,使得输出均值更为接近于零。但是ELU也有一些比ReLU更饱和的行为。


下面这个叫做Maxout Neuron,它看起来有点不一样,因为它和其他的具有不相同的形式,并没有先做点积运算,而是把这个元素级别的非线性特征放在它的前面。相反,它是取两组w和x的点积加上b的最大值。所以它的作用是泛化ReLU和Leaky ReLU,因为只是提取这两个线性函数的最大值。所以ELU给的是另一种线性机制的操作,这种方式不会饱和也不会挂掉,但问题在于会把每个神经元的参数数量翻倍,所以说如果每个神经元原有的权值集W,但是现在是原来的两倍。


所以在实际操作中,当考察所有这些激活函数的时候,一般最好的经验法则是使用ReLU,这是可用的激活函数中最为标准的一种,并且在通常情况下,需要非常小心地调整学习率。但是也可以去尝试其他较为有特点的激活函数,但是除了ReLU外,这些激活函数还是更为实验性一点(实用性较弱)。也可以稍微尝试一下tanh,但一般都不会实用sigmoid。


下面是讲述关于数据预处理的内容。


一些数据预处理的标准方法有零均值化数据和归一化数据。


零中心化的意义在上面已经详细的讲述了,这里就不再细讲。


归一化数据的意义在于让所有的特征都在相同的值域之内,并且让这些特征有相同的贡献。对于图像处理,在大部分情况下会进行零中心化处理,但实际上却不会真的去过多地进行归一化处理像素值。因为一般对图像来说,在每个位置已经得到了相对可比较的范围和分布,所以也没必要去进行归一化处理。


在机器学习中可能会看到更为复杂的东西,类似PCA或者是whitening,但在图像应用领域就是一直坚持使用零均值化,而一般不做其他复杂的预处理,因为我们不会想要所有的输入投影到一个更为低维度的空间中。


一旦在训练阶段对训练集进行了数据预处理,那么在测试阶段也需要对测试数据做同样的事情。在训练阶段决定的均值也会应用到测试数据中,所以会从训练数据中得到相同的经验均值来进行零均值化预处理。一般对于图像,从训练数据中可以计算均值图像,每张图像的尺寸都是相同的,然后会得到一组数据并对要传到网络中的每张图减去这组均值,最后对训练数据进行同样的处理。实际上对一些网络也通过减去单通道的均值,来代替用一整张均值图像来将图像数据集零中心化。这么做的原因是对于整张图片来看,减去均值图像和只是减去单通道均值相比并没有太大的不同,同时更容易传输和处理,例如你会在VGG网络中看到这样的操作。还有如果网络中使用的是sigmoid函数,数据预处理的零均值化仅仅会在网络的第一层解决了sigmoid函数式非零中心化的问题,因为一般使用的网络都是很深的,所以在后面的层中这个问题会以更恶化和严重的形式出现。


下面是讲述关于初始化网络权值的内容。


先来个问题,例如有一个标准的双层神经网络,我们看到所有这些用来学习的权值是需要初始化的,然后再使用梯度更新来更新这些权值那么这时候使用零的W的初始值会发生什么?这时候全部神经元做的事情都会是一样的,由于权值都是零,给定一个输入,每个神经元将在输入数据上有相同的操作,然后它们将会输出相同的数值并得到相同的梯度,因为这会用相同的方式更新并得到完全相同的神经元。但这不是我们想要的,我们希望不同的神经元学习到不同的知识。


所以首要做的事情是尝试将其改变为将所有的权值初始化为一个很小的随机数,我们可以从一个概率分布中抽样,例如从标准高斯分布中抽样并把抽样结果乘以0.01。这样的初始化在小型的神经网络中表现很好,因为这解决了参数对称的问题。但在结构较深的神经网络中会存在问题。


下面通过一个试验分析这个问题。这里使用一个结构较深的神经网络,在下面的例子中,初始化了一个有十层的神经网络,每层都会500个神经元,并使用tanh非线性激活函数,用很小的随机数来初始化权值。用随机数据作为输入,在每一层中观察产生的激活函数的数据的统计结果。


可以观察到如果计算每一层的均值和标准差,就可以看到第一层的数据的均值总是在0的附近(因为tanh是以0为中心的),但是标准差会缩小并快速逼近到0,如第二个图所示。如果把结果画出来,下面的折线图显示的是每层的均值和标准差,底部一系列的柱状图是展示了每一层的激活值的概率分布。

可以看到在第一层有一个合理的高斯分布,问题是当每一层乘以一些小的随机数的权值W后,激活数值这会随着一次次的相乘而迅速地缩小,最后得到的结果全部为零。在反向传播时,每一层都是很小的输入值(都小到近似于0),为了得到对权值的梯度,要用上游的梯度乘以本地的梯度,对 W\times x 节点,本地梯度就是x(已经是很小的输入值),这时的权值梯度结果就会近乎为零,这和之前用零来初始化权值出现的问题类似,这意味着它们基本上是没有更新。当把所有的梯度连接在一起,由于这个是点积运算,实际上在每一层的反向传播中是在做上游梯度和权值的乘法来得到下游的梯度。所以不断地乘以W会得到类似于正向传播中所有的数据变得越来越小的现象(因为正向传播中后面的激活值都是几乎为零),现在梯度和上游梯度都会趋向于零。


现在我们知道当权值很小的时候会出现问题,如果尝试使用增大权值来解决这个问题会出现什么?从标准高斯分布中抽样并把抽样结果乘以1,现在的权值都很大,W和X通过这样的网络得到输出,考虑到tanh的特点,如果输入的值是非常大或者非常小,这时网络就总是处于饱和状态。观察下面图中的激活值分布都是趋向于-1或者+1,这会到所有梯度都趋向于0,权值将不会得到更新。


可以看到初始化权值是一个挺困难的事情,初始化的权值不能太大或者太小,不过一个很好的初始化权值的方法是Xavier initialization。关于这个方法的公式的解释是,如果观察这里的W,可以看到想到把它们初始化为从高斯分布中取样,然后根据输入的数量来进行缩放。这个方法的解决问题的原理可以查阅课程对应的notes,但基本上的做法是指定输入的方差等于输出的方差,直观地说这种方法是如果有少量的输入,那么就除以较小的数从而得到较大的权值,我们需要较大大的权值,假设有少量的输入,用每一个输入值乘以权值,那就需要一个更大的权值来得到一个相同的输出方差,反之亦然。基本上想要一个unit gaussian权值作为每一层的输入,为了可以初始化unit gaussian权值,可以在训练时使用Xavier initialization,所以在每一层大概会有一个unit gaussian。


但问题是当使用类似ReLU等激活函数时,因为类似ReLU的激活函数会让一半的神经元挂掉,也就是每次都将有大约一半的神经元被设置为零,实际上这是把得到的方差减半。如果现在假设得到的推导和之前是相同的,那就不会得到正确的方差,而是一个特别小的值。最后将会再次看到unit gaussian分布开始收缩,这时越来越多的峰值会趋向于0。


在一些论文中已经指出可以试着通过除以2来解决这个问题,这些做法基本上是在调整,因为有一半的神经元被设置为0,所以有效的输入只是真实输入的一半,所以只要除以2这个因子,这就很好地解决了这个问题。同时也可以发现unit gaussian分布在深度神经网络中每层都表现得很好。在实践中,重视权值是非常重要的,这会对训练结果有很大的影响。


合理地初始化权值仍然是一个活跃的研究领域,下面是一些这个方面的论文和资源。一个很好的经验法则是从使用Xavier initialization开始,然后再考虑一些其他的方法。


接下来要讲述的是Batch Normalization的内容。


Batch Normalization的想法是在想要的gaussian范围内保持激活。我们想要unit gaussian激活,那么对它们进行批量归一化。考虑一下在某一层上的批量激活,取目前批量处理的均值,然后可以用均值和方差进行归一化。基本上是在训练开始的时候才设置这个值,而不是在初始化权值的时候设置,这就可以能在每一层都有很好的unit gaussian分布和在训练的时候能够一直保持。现在要明确地让所有通过深度神经网络的前向传播在每一层都有很好的unit gaussian分布。通过归一化每一个神经元的输入的这批数据的均值和方差来实现这个目的,同时这是一个可微函数。


如果观察输入的数据,就会想到假设在当前的批处理中有N个训练样本,并假设每个批量处理都是D维度的,然后将对每个维度单独计算经验均值和方差,并且对其进行归一化。

批量归一化通常在全连接或者卷积层之后插入,我们不断地在这些层上乘以权值W,虽然会对每一层造成不好的scaling effect,但是这基本上可以消除这种影响。因为基本上是通过每个与神经元激活函数相连的输入来进行缩放,所以可以用相同的方法在全连接层和卷积层,唯一的区别是在卷积层的情况下,我们不仅想要归一化所有的训练样例的每个独立的特征维度,而且要联合归一化在激活映射中所有的特征维度、空间位置和所有特性。这么做的原因是想要服从卷积的性质,我们希望附近的位置能够以同样的方式进行归一化。所以在卷积层的情况下,在每个激活图中会有一个均值和方差,我们将在批量处理的所有实例中进行归一化。


要注意的是我们并不清楚要在每个全连接层之后进行批量归一化的操作,也并不清楚是否确实需要给这些tanh非线性函数输入unit gaussian数据。因为这里所做的是把输入限制在非线性函数的线性区域内,虽然这并不绝对,但基本而言我们是希望避免出现饱和状态,但是有一点饱和状态也是好的,同时你希望能够控制饱和的程度。


所以要强调的是,批量归一化是在完成归一化操作之后需要进行额外的缩放操作。所以我们先做了归一化,然后使用常量 \gamma 进行缩放,再用另外一个因子 \beta 进行平移,并且这里实际在做的是允许你恢复恒等函数。网络可以按照需要学习缩放因子 \gamma 使之等于方差,也可以学习 \beta 使之等于均值,在这种情况下就可以恢复恒等映射,就像没有进行批量归一化一样。现在就拥有让网络为了达到比较好的训练效果而去学习控制让tanh具有更高或者更低饱和状态的能力。我们希望学习 \gamma\beta 从而恢复恒等函数的原因是想要它具有灵活性,批量归一化做的是将数据转换为unit gaussian数据,虽然这是一个很好的想法,但这不能保证总是最好的选择,在tanh这样的特殊例子中,我们希望可以控制饱和状态的程度,所以这样做是为了让类似进行unit gaussian归一化一样更具备灵活性。


下面是批量归一化的一个小结。


在测试阶段,对批量归一化层,现在减去了训练数据中的均值和方差,所以在测试阶段不用重新计算,只需把这当成训练阶段,例如在训练阶段用到了平均偏移,在测试阶段也同样会有所用到。


接下来要讲述的是Babysitting the Learning Process的内容。


我们现在已经定义了网络的结构,现在要讨论如何监控训练,并在训练过程中调整这些超参数以获得最好的学习结果。首先要做的第一步还是进行数据预处理,要把数据进行零均值处理。


然后选择网络结构,例如这里是一个含有50个神经元的隐藏层的简单的神经网络。


接下来是初始化网络并进行前向传播,当想确定最终的损失函数是否合理,按照在前面lecture中讲过的方法。这里有一个softmax函数分类器,当权重很小且分布很分散的时候,softmax函数分类器的损失函数将是负对数似然的计算结果,如果这里有10个类别,这个结果会是 -log\frac{1}{10}\approx 2.3 。我们要确定损失函数的数值是所期望的,所以这是一个非常好的完备性检查。


一旦看到初始的损失值还不错,接下来要做的就是加入0正则项,如果不加入这个正则项,那么损失值就是数据的损失值(2.3左右)。当开启这个正则项时,我们希望看到损失值增加,因为添加了额外的正则项。


现在开始训练,最好是先从小数据集开始,因为对于小数据集,神经网络能够把其拟合得非常好。这里先不开启正则项,因为要观察是否能把训练集的损失降为0。


从下面可以看到训练损失值在降低,最后看到损失值降为0。同时也看到训练集的准确率上升到1,所以当时小数据集的时候,神经网络能够完美地拟合这些数据。


一旦完成了所有的完备性检查操作,就可以加上一个小的正则项对所有训练数据集进行训练。现在要确定最重要的参数之一:学习率。它是想要首先调整的参数,这里使用的学习率是1e-6,但是可以看到损失值基本上没有变化,这是因为设置的学习率太低了,导致梯度更新就会很小,从而损失值也基本上不会变化。这里需要注意的一点是即使损失基本不变,但是训练集和验证集的准确率都很快地提升到了20%,因为这里的分数依然很分散,所以损失项很接近,但是把这些所有的分布都朝着正确的方向轻微地移动,权值参数在朝着正确的方向改变;现在准确率可能发生突变,这是由于正在选取最大的准确率,所以准确率会得到一个很大的提升。


现在尝试另一个非常大的学习率1e6,现在损失值是NaN,这往往意味着损失值已经非常大了,原因就是设置的学习率太大了。


这就需要调低学习率,当尝试调低到3e-3时,损失值仍然会非常的大。所以一般情况下学习率设置在1e-3到1e-5之间,这是一个想要交叉验证的粗略范围,然后根据实际的损失值大小来在这个范围中确定学习率的值。


下面讲述的内容是Hyperparameter Optimization。


调整超参数的策略是对超参数进行交叉验证,观察这些超参数的实验结果。首先要做的是选择相当分散的数值,然后用几个epoch去学习,这会了解到哪些超参数有效,例如观察到损失值为NaN,则要调低学习率。一般这么做的话就可以找到一个比较好的参数区间,然后根据这个参数区间搜索更精确的值。在训练迭代期间,有一个技巧可以快速找到像NaN的问题,开始训练一些参数,在每个迭代观察损失值,如果出现一个远远大于初始代价的值,例如超过了3倍,这就可以知道这里的参数设置出现了问题。

接下来看一看下面的例子,使用5个epoch来进行参数搜索,现在就是去观察得到的验证集的准确度。这里用红色方框标注了比较好的结果,这些区间就是需要进一步细化的区间,一般采用对数来优化效果会更好,这是因为学习率乘以梯度更新,具有乘法效应,因此考虑学习率的时候用一些值的乘以或者除以的值会更为合理,而不是使用均匀采样。


一旦找到了这些区间,就可以根据其来调整变化范围。在上面的例子中,已经有一个范围1e-4到0,这就是可以进一步精确搜索的区间,接着重复前面的步骤,就可以得到一个相对较好的准确度,例如53%,这意味着是调整正确了方向。需要注意的是,这里其实有一个问题,红色箭头标注的地方是准确度最高的地方,所有好的学习率都在这个精确搜索的区域间中,这会让它们都在采样边缘。这是不好的,因为不能在全部搜索空间上充分寻找,所以我们想要确定的是这些好的值是整个区间的一部分,还是在整个区间当中。


另外一点就是使用grid search对不同的超参数采样,对每个超参数的一组固定值采样,实际上对这些值进行网格式采样,但实际上中一种随机排列的方式对每一个超参数在一定范围内进行随机值采样。在这里有两个超参数需要采样,可以得到像右边这样的采样点,随机采样是考虑到对一个不只一个变量的函数而言,随机搜索更加真实,这对真正的维度进行稍微有效地降维,然后就可以得到更多已有的重要变量的样本。看看图上方画出的绿色的函数,表示较好的值的位置,如果使用grid layout就只能采样到3个值,同时会错过了很多不错的区域。有了重要的变量,不同值的更多样本才能得到更多有用的信息。


在观察每条损失曲线的时候,学习率是一个重要的因素,通过下面的图可以直观地感受到哪些学习率是好的,哪些是不好的,好的学习率曲线如红色的损失函数曲线所示。


当观察学习率损失曲线的时候,如果它在一定时间内很平滑,然后突然开始训练,这可能是初始值没有设置好,从下图可以看出开始的时候梯度变化并不太好,什么也没学到,到达某一点后突然开始正确地调节,就像才刚开始训练一样。


下面是准确度曲线,如果在训练集和验证集的准确度之间有一个很大的差值,这意味着可能产生了过拟合,这时候可以尝试增加正则项的权值;如果没有差值就没有过拟合,可以尝试去增加模型的复杂度提高准确度。


整体来说,可以利用已有参数的norms来跟踪更新的值,权值的更新和权值幅值的比率来得到它们有多大,什么时候更新size,也可以利用范数来描述它的大小。让比率在0.001附近,因为区间内有很多变化,所以不需要得到具体的某个值,但需要知道哪些值和需要的值相比过大或者过小。


最后是今天讲的内容的一个小结。

「真诚赞赏,手留余香
  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值