吴恩达机器学习(六)—— 神经网络的学习

  神经网络是当下最强大的学习算法之一。接下来讨论一个能在给定训练集时为神经网络拟合参数的学习算法。

1. 代价函数

  神经网络在分类问题中的应用:
在这里插入图片描述

图1 神经网络模型

  首先引入一些标记方法:假设神经网络的训练样本有 m m m个,每个包含一组输入 x x x和一组输出 y y y L L L表示神经网络层数, S l S_{l} Sl 表示第 l l l层的单元数即神经元的数量(不包含第 l l l层的偏差单元)。
  在图1所示的神经网络模型中, L = 4 L=4 L=4 S 1 = 3 S_{1}=3 S1=3 S 2 = 5 S_{2}=5 S2=5 S 3 = 5 S_{3}=5 S3=5 S 4 = 4 S_{4}=4 S4=4
  神经网络的分类有两种情况:二元分类(Binary classification)和多类别分类(Multi-class classification)。

  • 二元分类

  对于二元分类,神经网络只有一个输出单元,其输出值 y y y不是0就是1,即 S L = 1 S_{L}=1 SL=1
在这里插入图片描述

  • 多类别分类

  对于多类别分类,神经网络有 K K K个输出单元,即 S L = K S_{L}=K SL=K
  回想一下之前的Logistic回归中的代价函数为
J ( θ ) = − 1 m [ ∑ i = 1 m y ( i ) × l o g ( h θ ( x ( i ) ) ) + ( 1 − y ( i ) ) × l o g ( 1 − h θ ( x ( i ) ) ) ] + λ 2 m ∑ j = 1 n θ j 2 J(\theta )=-\frac{1}{m}\left [\sum_{i=1}^{m}y^{(i)}\times log(h_{\theta}(x^{(i)}))+(1-y^{(i)})\times log(1-h_{\theta}(x^{(i)}))\right ]+\frac{\lambda }{2m} \sum_{j=1}^{n} \theta _{j}^{2} J(θ)=m1[i=1my(i)×log(hθ(x(i)))+(1y(i))×log(1hθ(x(i)))]+2mλj=1nθj2

  在Logistic回归中,我们只有一个输出变量 y y y,又称标量(scalar);但是在神经网络中,我们可以有很多个输出变量, h θ ( x ) h_{\theta}(x) hθ(x)是一个维度为 K K K的向量,因此神经网络的代价函数会比Logistic回归更加复杂一些。

  神经网络中的代价函数为:

J ( Θ ) = − 1 m [ ∑ i = 1 m ∑ k = 1 k y k ( i ) log ⁡ ( h Θ ( x ( i ) ) ) k + ( 1 − y k ( i ) ) log ⁡ ( 1 − ( h Θ ( x ( i ) ) ) k ) ] + λ 2 m ∑ l = 1 L − 1 ∑ i = 1 s l ∑ j = 1 s l + 1 ( Θ j i ( l ) ) 2 J(\Theta)=-\frac{1}{m}\left[\sum_{i=1}^{m} \sum_{k=1}^{k} y_{k}^{(i)} \log \left(h_{\Theta}\left(x^{(i)}\right)\right)_{k}+\left(1-y_{k}^{(i)}\right) \log \left(1-\left(h_{\Theta}\left(x^{(i)}\right)\right)_{k}\right)\right] +\frac{\lambda}{2 m} \sum_{l=1}^{L-1} \sum_{i=1}^{s_{l}} \sum_{j=1}^{s_{l}+1}\left(\Theta_{j i}^{(l)}\right)^{2} J(Θ)=m1[i=1mk=1kyk(i)log(hΘ(x(i)))k+(1yk(i))log(1(hΘ(x(i)))k)]+2mλl=1L1i=1slj=1sl+1(Θji(l))2

  其中, h θ ( x ) ∈ R K h_{\theta}(x)\in R^{K} hθ(x)RK ( h θ ( x ) ) k (h_{\theta}(x))_{k} (hθ(x))k表示第 k k k个输出。正则化的那一项需要先排除每一层的 θ 0 \theta_{0} θ0 后再对每一层的 θ \theta θ 矩阵求和。最里层的循环 j j j循环所有的行(由 S l + 1 S_{l}+1 Sl+1层的激活单元数决定),循环 i i i则循环所有的列(由 S l S_{l} Sl层的激活单元数所决定)。
  虽然神经网络的代价函数看起来复杂很多,但其背后的思想还是一样的,我们希望通过代价函数来观察算法预测的结果与真实情况的误差有多大。唯一不同的是,对于每一个样本的特征,基本上我们都可以利用循环对其预测出 K K K个不同的结果,然后再在所有预测结果中选择可能性最高的一个,将其与 y y y中的实际数据进行比较。

2. 反向传播算法

  之前在计算神经网络预测结果的时候我们采用了一种正向传播方法:从神经网络的第一层开始正向一层一层进行计算,直到最后一层。
  现在,为了计算代价函数的偏导数 ∂ ∂ Θ i j ( l ) J ( Θ ) \frac{\partial }{\partial \Theta _{ij}^{(l)}}J(\Theta ) Θij(l)J(Θ),我们需要采用一种反向传播算法:首先计算最后一层的误差,然后再一层一层反向求出各层的误差,直到倒数第二层。

  通过图1 中的神经网络模型来说明反向传播算法。
在这里插入图片描述
  假设我们的训练集只有一个样本 ( x ( 1 ) , y ( 1 ) ) (x^{(1)},y^{(1)}) (x(1)y(1)),我们的神经网络如上图所示是一个四层的神经网络,其中, K = 4 K=4 K=4 S L = 4 S_{L}=4 SL=4 L = 4 L=4 L=4

前向传播算法:
a ( 1 ) = x z ( 2 ) = Θ ( 1 ) a ( 1 ) a ( 2 ) = g ( z ( 2 ) ) (  add  a 0 ( 2 ) ) z ( 3 ) = Θ ( 2 ) a ( 2 ) a ( 3 ) = g ( z ( 3 ) ) (  add  a 0 ( 3 ) ) z ( 4 ) = Θ ( 3 ) a ( 3 ) a ( 4 ) = g ( z ( 4 ) ) = h Θ ( x ) \begin{aligned} a^{(1)} &=x \\ z^{(2)} &=\Theta^{(1)} a^{(1)} & \\ a^{(2)} &=g\left(z^{(2)}\right) &\left(\text { add } a_{0}^{(2)}\right) \\ z^{(3)} &=\Theta^{(2)} a^{(2)} & \\ a^{(3)} &=g\left(z^{(3)}\right) &\left(\text { add } a_{0}^{(3)}\right) \\ z^{(4)} &=\Theta^{(3)} a^{(3)} & \\ a^{(4)} &=g\left(z^{(4)}\right)=h_{\Theta}(x) \end{aligned} a(1)z(2)a(2)z(3)a(3)z(4)a(4)=x=Θ(1)a(1)=g(z(2))=Θ(2)a(2)=g(z(3))=Θ(3)a(3)=g(z(4))=hΘ(x)( add a0(2))( add a0(3))

反向传播算法:
  对于反向传播算法,首先我们定义第 l l l层第 j j j个结点激活单元的预测值与实际值之间的误差为 δ j ( l ) = a j ( l ) − y j \delta_{j}^{(l)}=a_{j}^{(l)}-y_{j} δj(l)=aj(l)yj,其中 a j ( l ) a_{j}^{(l)} aj(l)代表第 l l l层第 j j j个结点的激活值。
  我们从最后一层的误差开始计算,可以得到
δ j ( 4 ) = a j ( 4 ) − y j \delta_{j}^{(4)}=a_{j}^{(4)}-y_{j} δj(4)=aj(4)yj
  当 δ ( 4 ) \delta^{(4)} δ(4) a ( 4 ) a^{(4)} a(4) y j y_{j} yj均为维数相同的向量时,有 δ ( 4 ) = a ( 4 ) − y \delta^{(4)}=a^{(4)}-y δ(4)=a(4)y。 我们利用这个误差值来计算前一层的误差:
δ ( 3 ) = ( Θ ( 3 ) ) T δ ( 4 ) ⋅ g ′ ( z ( 3 ) ) \delta ^{(3)}=(\Theta ^{(3)})^{T}\delta ^{(4)}\cdot g^{'}(z^{(3)}) δ(3)=(Θ(3))Tδ(4)g(z(3))
  其中, g ′ ( z ( 3 ) ) g^{'}(z^{(3)}) g(z(3)) S S S形函数的导数, g ′ ( z ( 3 ) ) = a ( 3 ) ⋅ ( 1 − a ( 3 ) ) g^{'}(z^{(3)})=a^{(3)}\cdot (1-a^{(3)}) g(z(3))=a(3)(1a(3)),而 ( Θ ( 3 ) ) T δ ( 4 ) (\Theta ^{(3)})^{T}\delta ^{(4)} (Θ(3))Tδ(4)则是权重导致的误差的和。接下来继续计算第二层的误差:

δ ( 2 ) = ( Θ ( 2 ) ) T δ ( 3 ) ⋅ g ′ ( z ( 2 ) ) \delta ^{(2)}=(\Theta ^{(2)})^{T}\delta ^{(3)}\cdot g^{'}(z^{(2)}) δ(2)=(Θ(2))Tδ(3)g(z(2))
  因为第一层是输入变量,不存在误差,所以在我们有了所有误差的表达式后,便可以计算代价函数的偏导数了。

  假设 λ = 0 \lambda =0 λ=0,即我们不做任何正则化处理时有:
∂ ∂ Θ i j ( l ) J ( Θ ) = a j ( l ) δ i ( l + 1 ) \frac{\partial }{\partial \Theta _{ij}^{(l)}}J(\Theta )=a_{j}^{(l)}\delta_{i}^{(l+1)} Θij(l)J(Θ)=aj(l)δi(l+1)
  其中, l l l代表目前所计算的是第几层; j j j代表目前计算层中的激活单元的下标,也将是下一层的第 j j j个输入变量的下标; i i i代表下一层中误差单元的下标,是受到权重矩阵中第 i i i行影响的下一层中的误差单元的下标。

  如果我们考虑正则化处理,并且我们的训练集是一个特征矩阵而非向量,则我们需要为整个训练集计算误差单元,此时的误差单元是一个矩阵,我们用 Δ i j ( l ) \Delta _{ij}^{(l)} Δij(l)来表示这个误差矩阵, Δ i j ( l ) \Delta _{ij}^{(l)} Δij(l)表示第 l l l层的第 i i i个激活单元受到第 j j j个参数影响而导致的误差。

反向传播算法可以表示如下:
在这里插入图片描述
  即首先运用正向传播方法计算出每一层的激活单元,利用训练集的实际值与神经网络预测的结果求出最后一层的误差,然后利用该误差运用反向传播法计算出直至第二层的所有误差。在求出 Δ i j ( l ) \Delta _{ij}^{(l)} Δij(l)之后,我们便可以计算代价函数的偏导数 ∂ ∂ Θ i j ( l ) J ( Θ ) = D i j ( l ) \frac{\partial }{\partial \Theta _{ij}^{(l)}}J(\Theta )=D _{ij}^{(l)} Θij(l)J(Θ)=Dij(l) 了,计算方法如下:
在这里插入图片描述

3. 反向传播算法的直观理解

在这里插入图片描述

图2 神经网络模型

  如图2所示的神经网络模型中,对于某一个输入样本,我们首先根据前向传播算法计算出所有的 z j ( l ) z_{j}^{(l)} zj(l) a j ( l ) a_{j}^{(l)} aj(l),例如 z 1 ( 3 ) = Θ 10 ( 2 ) × 1 + Θ 11 ( 2 ) a 1 ( 2 ) + Θ 12 ( 2 ) a 2 ( 2 ) z_{1}^{(3)}=\Theta_{10}^{(2)}\times1+\Theta_{11}^{(2)}a_{1}^{(2)}+\Theta_{12}^{(2)}a_{2}^{(2)} z1(3)=Θ10(2)×1+Θ11(2)a1(2)+Θ12(2)a2(2)。然后再根据反向传播算法计算出所有的误差项 δ j ( l ) \delta _{j}^{(l)} δj(l),例如 δ 2 ( 2 ) = Θ 12 ( 2 ) δ 1 ( 3 ) + Θ 22 ( 2 ) δ 2 ( 3 ) \delta _{2}^{(2)}=\Theta_{12}^{(2)}\delta _{1}^{(3)}+\Theta_{22}^{(2)}\delta _{2}^{(3)} δ2(2)=Θ12(2)δ1(3)+Θ22(2)δ2(3)
  反向传播算法这个名字源于我们从输出层开始计算 δ \delta δ项,根据 δ ( 4 ) \delta^{(4)} δ(4)的值返回到上一层计算 δ ( 3 ) \delta^{(3)} δ(3) ,接着我们再往前一步计算 δ ( 2 ) \delta^{(2)} δ(2) ,所以说我们是类似于把输出层的误差反向传播给了第3层,然后再传到第2层,这就是反向传播的意思。

4. 实现注意:展开参数

  一个实现过程中的细节:怎样把我们的参数从矩阵展开成向量,以便我们在高级优化步骤中的使用。

优化算法:

function  [jVal, gradient]=costFunction(theta)
... ...
optTheta=fminunc(@costFunction, initialTheta, options)

  假设有一个4层的神经网络如图3所示, S 1 = 10 S_{1}=10 S1=10 S 2 = 10 S_{2}=10 S2=10 S 3 = 10 S_{3}=10 S3=10 S 4 = 1 S_{4}=1 S4=1,则有参数矩阵 Θ ( 1 ) ∈ R 10 × 11 \Theta^{(1)}\in R^{10\times 11} Θ(1)R10×11 Θ ( 2 ) ∈ R 10 × 11 \Theta^{(2)}\in R^{10\times 11} Θ(2)R10×11 Θ ( 3 ) ∈ R 1 × 11 \Theta^{(3)}\in R^{1\times 11} Θ(3)R1×11;梯度矩阵 D ( 1 ) ∈ R 10 × 11 D^{(1)}\in R^{10\times 11} D(1)R10×11 D ( 2 ) ∈ R 10 × 11 D^{(2)}\in R^{10\times 11} D(2)R10×11 D ( 3 ) ∈ R 1 × 11 D^{(3)}\in R^{1\times 11} D(3)R1×11

在这里插入图片描述

图3 一个4层的神经网络模型

在MATLAB中用于矩阵和向量之间来回转化的代码如下:

  • 矩阵转化为向量
thetaVec = [Theta1( : ); Theta2( : ); Theta3( : )];
DVec = [D1( : ); D2( : ); D3( : )];
  • 向量转化为矩阵
Theta1 = reshape(thetaVec(1:110), 10, 11);
Theta2 = reshape(thetaVec(111:220), 10, 11);
Theta1 = reshape(thetaVec(221:231), 1, 11);

学习算法:
  首先初始化参数 Θ ( 1 ) \Theta^{(1)} Θ(1) Θ ( 2 ) \Theta^{(2)} Θ(2) Θ ( 3 ) \Theta^{(3)} Θ(3),展开后可以得到初始化向量initialTheta并传入 f m i n u n c ( @ c o s t F u n c t i o n ,   i n i t i a l T h e t a ,   o p t i o n s ) fminunc (@costFunction, \ _{}initialTheta, \ _{}options) fminunc(@costFunction, initialTheta, options)中。其次在函数 f u n c t i o n   [ j V a l ,   g r a d i e n t V e c ] = c o s t F u n c t i o n ( t h e t a V e c ) function\ _{} [jVal,\ _{} gradientVec] = costFunction(thetaVec) function [jVal, gradientVec]=costFunction(thetaVec)中,根据 t h e t a V e c thetaVec thetaVec我们可以得到 Θ ( 1 ) \Theta^{(1)} Θ(1) Θ ( 2 ) \Theta^{(2)} Θ(2) Θ ( 3 ) \Theta^{(3)} Θ(3);然后使用反向传播算法计算 D ( 1 ) D^{(1)} D(1) D ( 2 ) D^{(2)} D(2) D ( 3 ) D^{(3)} D(3) J ( Θ ) J(\Theta) J(Θ);展开 D ( 1 ) D^{(1)} D(1) D ( 2 ) D^{(2)} D(2) D ( 3 ) D^{(3)} D(3)可以得到gradientVec。

5. 梯度下降

  当我们对一个较为复杂的模型(例如神经网络)使用梯度下降算法时,可能会存在一些不容易察觉的错误。这意味着虽然代价看上去在不断减小,但最终的结果可能并不是最优解。
  为了避免这样的问题,我们采取一种叫做梯度的数值检验(Numerical Gradient Checking)方法。这种方法的思想是通过估计梯度值来检验我们计算的导数值是否真的是我们所要求的。

在这里插入图片描述

图4 梯度的数值检验

  对梯度的估计采用的方法是在代价函数上沿着切线的方向选择两个离 θ \theta θ非常近的点,然后计算两个点的平均值用以估计梯度。如图4所示,对于某个特定的 θ \theta θ,我们计算出在 θ − ε \theta-\varepsilon θε处和 θ + ε \theta+\varepsilon θ+ε处的代价值 J ( θ − ε ) J(\theta-\varepsilon) J(θε) J ( θ + ε ) J(\theta+\varepsilon) J(θ+ε) ε \varepsilon ε是一个非常小的值,通常选取 0.001),然后求出两个代价的平均值用以估计在 θ \theta θ处的代价值,有 d d θ J ( θ ) ≈ J ( θ + ε ) − J ( θ − ε ) 2 ε \frac{d}{d\theta }J(\theta )\approx \frac{J(\theta +\varepsilon )-J(\theta -\varepsilon )}{2\varepsilon } dθdJ(θ)2εJ(θ+ε)J(θε),我们需要对所有的偏导数项进行检验。
在这里插入图片描述
在MATLAB中,用如下方法实现导数的估算:
在这里插入图片描述
  其中, g r a d A p p r o x gradApprox gradApprox为用上述方法得到的导数, D V e DVe DVe为在反向传播中得到的导数。如果两种方法得到的导数是一样的或者非常接近(只有几位小数的差距),则可以确信反向传播的实现是正确的。

总结一下如何实现数值上的梯度检验:

  • 通过反向传播计算 D V e c DVec DVec
  • 实现数值上的梯度检验,计算出 g r a d A p p r o x gradApprox gradApprox
  • 确保 D V e c DVec DVec g r a d A p p r o x gradApprox gradApprox能得出相似的值;
  • 在使用代码进行学习或者训练网络之前,重要的是关掉梯度检验,不要计算 g r a d A p p r o x gradApprox gradApprox。(因为梯度检验的代码是一个计算量非常大、也非常慢的计算导数的程序,相对来说反向传播算法则是一个高性能的计算导数的方法,所以一旦通过梯度检验确定反向传播的实现是正确的,就应该关掉梯度检验,不再去使用它。)

6. 随机初始化

  任何优化算法都需要一些初始化的参数。到目前为止我们都是初始化所有参数为0,这样的初始化方法对于Logistic回归来说是可行的,但是对于神经网络来说是不可行的。如果我们令所有的初始化参数都为0,这将意味着我们第二层的所有激活单元都会有相同的值。同理,如果我们将所有的参数都初始化为同一个非0的数,结果也是一样的。
在这里插入图片描述

图5 神经网络模型

  在图5所示的神经网络模型中,如果对于所有的 i i i, j j j, l l l都有 Θ i j ( l ) = 0 \Theta_{ij}^{(l)}=0 Θij(l)=0,则 a 1 ( 2 ) = a 2 ( 2 ) a_{1}^{(2)}=a_{2}^{(2)} a1(2)=a2(2) δ 1 ( 2 ) = δ 2 ( 2 ) \delta_{1}^{(2)}=\delta_{2}^{(2)} δ1(2)=δ2(2) ⇒ \Rightarrow ∂ ∂ Θ 01 ( 1 ) J ( Θ ) = ∂ ∂ Θ 02 ( 1 ) J ( Θ ) \frac{\partial }{\partial \Theta _{01}^{(1)}}J( \Theta)=\frac{\partial }{\partial \Theta _{02}^{(1)}}J( \Theta) Θ01(1)J(Θ)=Θ02(1)J(Θ)。所以每次更新之后,这两个隐藏单元的每个参数输入都是相等的,意味着即使梯度下降进行了一次迭代,但这两个隐藏单元依然以相同的函数作为输入来计算。这是一种高度冗余的现象,最后的Logistic单元只能得到一个特征,因为所有单元都一样,这阻止了神经网络去学习任何有趣的东西。
  所以为了解决上述问题,在神经网络中我们要将所有参数进行随机初始化,而不是全部置为0。随机初始化参数的目的是使对称失效(Symmetry breaking)。
  我们通常初始化参数为正负 ε \varepsilon ε之间的随机值。假设我们要随机初始化一个尺寸为10×11的参数矩阵,MATLAB代码为Theta1 = rand(10, 11) * (2*INIT_EPSILON) – INIT_EPSILON。
  所以为了训练神经网络,我们应该首先将权重随机初始化为一个接近0、范围在 − ε -\varepsilon ε ε \varepsilon ε之间的数,然后进行反向传播,再进行梯度检验,最后使用梯度下降或者其他高级优化算法来最小化代价函数 J ( Θ ) J(\Theta) J(Θ)

7. 综合起来

训练神经网络前我们需要做的:

  • 选择一种网络架构:架构是指神经元之间的连接模式;
  • 选择输入单元和输出单元的数量:输入单元的数量为特征的维度,输出单元的数量为类别个数;
  • 选择隐藏单元的数量:合理的默认选项为单个隐藏层或者不止一个隐藏层,但通常都应有相同的单元数。如果有大量隐藏单元,计算量一般会比较大,但一般来说隐藏单元还是越多越好。并且一般来说每个隐藏层所包含的单元数量还应和输入x的维度相匹配,即和特征数量相匹配,隐藏单元的数量可以和输入特征数量相同,或者是它的二倍、三倍或四倍。因此,隐藏单元的数量和输入特征数相区配或者比特征数大几倍都是有效的。

训练神经网络的步骤:

  • 随机初始化权重
  • 执行前向传播算法,对于该神经网络任意一个输入 x ( i ) x^{(i)} x(i),计算出对应的 h Θ ( x ( i ) ) h_{\Theta}(x^{(i)}) hΘ(x(i))
  • 通过代码计算出代价函数 J ( Θ ) J(\Theta) J(Θ)
  • 执行反向传播算法来计算出这些偏导数项 ∂ ∂ Θ i j ( l ) J ( Θ ) \frac{\partial }{\partial \Theta _{ij}^{(l)}}J( \Theta) Θij(l)J(Θ)
    使用梯度检验来比较这些已经计算得到的偏导数项,把用反向传播得到的偏导数项值与用数值方法得到的估计值进行比较,然后停用梯度检验代码;
  • 使用一个最优化算法,例如梯度下降算法或者更加高级的优化算法,将这些优化算法和反向传播算法相结合来最小化代价函数 J ( Θ ) J(\Theta) J(Θ)

  对于神经网络来说,代价函数 J ( Θ ) J(\Theta) J(Θ)是一个非凸函数,因此理论上可能停留在局部最小值的位置。代价函数 J ( Θ ) J(\Theta) J(Θ)度量的就是这个神经网络对训练数据的拟合情况。反向传播算法能够让更复杂、强大、非线性的函数模型跟我们的数据很好的拟合。

8. 自动驾驶

  一个有趣并具有历史意义的神经网络学习例子:使用神经网络来实现自动驾驶。
  ALVINN (Autonomous Land Vehicle In a Neural Network)是一个基于神经网络的智能系统,通过观察人类的驾驶来学习驾驶。ALVINN能够控制NavLab,装在一辆改装版军用悍马上,这辆悍马装载了传感器、计算机和驱动器用来进行自动驾驶的导航试验。
原文链接:https://blog.csdn.net/HUAI_BI_TONG/article/details/108810069

  实现ALVINN功能的第一步是对它进行训练,让一个人驾驶汽车,然后让ALVINN观看,ALVINN每两秒将前方的路况图生成一张数字化图片,并且记录驾驶者的驾驶方向,得到的训练集图片被压缩为 30 × 32 30\times32 30×32像素,并且作为输入提供给ALVINN的三层神经网络。通过使用反向传播学习算法,ALVINN会训练得到一个与人类驾驶员操纵方向基本相近的结果。
  一开始,我们的网络选择出的方向是随机的,大约经过两分钟的训练后,我们的神经网络便能够准确地模拟人类驾驶者的驾驶方向,对其他道路类型也能重复进行这个训练过程。当网络被训练完成后,操作者就可按下运行按钮,车辆便开始行驶了。
  每秒钟ALVINN生成12次数字化图片,并且将图像传送给神经网络进行训练,多个神经网络同时工作,每一个网络都生成一个行驶方向以及一个预测置信度的参数,预测置信度最高的那个神经网络得到的行驶方向将最终用于控制车辆方向。这就是基于神经网络的自动驾驶技术。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值