深度学习详细笔记 (通俗易懂,这一篇就够了 )——浅层神经网络详解

1. 神经网络概览 (Neural Networks Overview)

在上一次的课程中,我们学习了如何使用向量化和矩阵计算的方法提高Python编程效率,并采用了逻辑回归作为案例展示了如何利用向量化来加速算法运行速度。接下来,我们将进一步深入,探索更为复杂的神经网络模型。

首先,我们要明确什么是神经网络。简单来说,神经网络是由多个层次构成的,其中每个层都是由多个神经元组成。与我们之前学的逻辑回归相比,神经网络的主要区别是它有多层结构,其中的隐藏层就是这些额外增加的层。

这些层是如何工作的呢?

想象你正在建造一个多层楼房。逻辑回归就像是一个平房,而神经网络则是一栋多层大楼。每一层都要进行某种计算,从底层开始,然后将结果传递到上一层。这种从一层传递到下一层的过程,我们称之为“正向传播”。

那么,正向传播是如何进行的呢?

  • 首先,从输入层开始,我们进行第一层的计算:
    z [ 1 ] = W [ 1 ] x + b [ 1 ] z^{[1]} = W^{[1]}x + b^{[1]} z[1]=W[1]x+b[1]
    a [ 1 ] = σ ( z [ 1 ] ) a^{[1]} = \sigma(z^{[1]}) a[1]=σ(z[1])

  • 接下来,我们使用第一层的输出作为第二层的输入:
    z [ 2 ] = W [ 2 ] a [ 1 ] + b [ 2 ] z^{[2]} = W^{[2]}a^{[1]} + b^{[2]} z[2]=W[2]a[1]+b[2]
    a [ 2 ] = σ ( z [ 2 ] ) a^{[2]} = \sigma(z^{[2]}) a[2]=σ(z[2])

这里,我们使用了σ函数,也就是激活函数,它可以帮助我们得到非线性的输出。在这里的表示中,方括号上标 [ i ] [i] [i] 表示层的编号,而圆括号上标 ( i ) (i) (i) 则表示样本的编号。

然后,当我们需要调整模型以优化预测时,就会执行“反向传播”过程。简单来说,反向传播就是从输出开始,反过来查找每层中可能的错误,然后对其进行调整。

反向传播过程和正向传播类似,也是分层进行的,不过是从输出层开始,然后回到输入层。

正如下图所示,我们可以看到一个简单的两层神经网络的结构:

神经网络结构

在后续的课程中,我们将更加深入地探讨这些概念,以及如何使用它们建立高效的神经网络模型。

2. 神经网络表示 (Neural Network Representation)

为了进一步加深对神经网络的理解,我们来观察一个具有单隐藏层的神经网络结构。下图为你展示了这样的一个结构,它可以被称作是浅层神经网络(或者说是初级版的神经网络)。

神经网络结构

整体看,神经网络可以被分为三个主要部分:输入层 (Input layer)、隐藏层 (Hidden layer) 和输出层 (Output layer)。从名字可以简单地推断,输入层是我们将数据输入到网络中的地方,而输出层则是得到预测结果的地方。那隐藏层是什么呢?它是一个介于输入和输出之间的中间层,帮助我们实现复杂的非线性映射。

在表示神经网络的结构时,我们常常使用矩阵来描述。例如,我们将输入数据矩阵 X X X记作 a [ 0 ] a^{[0]} a[0],而隐藏层的输出则记作 a [ 1 ] a^{[1]} a[1]。这里的上标表示层数,注意是从0开始计数的。同样,下标用于表示神经元的编号,从1开始计数。比如, a [ 1 ] 1 a^{[1]_1} a[1]1代表隐藏层的第一个神经元。如果我们知道隐藏层有4个神经元,那么其输出矩阵 a [ 1 ] a^{[1]} a[1]可以写为:

a [ 1 ] = [ a [ 1 ] 1 a [ 1 ] 2 a [ 1 ] 3 a [ 1 ] 4 ] a^{[1]} = \left[ \begin{array}{c} a^{[1]_1} \\ a^{[1]_2} \\ a^{[1]_3} \\ a^{[1]_4} \\ \end{array} \right] a[1]= a[1]1a[1]2a[1]3a[1]4

输出层的表示我们则记作 a [ 2 ] a^{[2]} a[2]。这种单隐藏层的神经网络,虽然被称为三层结构,但其实只有两层需要我们进行计算,因此也叫做两层神经网络(2-layer NN)。为什么如此命名呢?因为我们通常不计算输入层的输出,它只是提供数据而已。

当我们构建这样的网络时,我们需要为每一层定义权重和偏置项。例如,对于隐藏层,其权重矩阵 W [ 1 ] W^{[1]} W[1]的维度是(4,3),其中4代表隐藏层的神经元数量,3代表输入数据的特征数量。隐藏层的偏置矩阵 b [ 1 ] b^{[1]} b[1]维度为(4,1)。输出层的权重矩阵 W [ 2 ] W^{[2]} W[2]维度是(1,4),这里1代表输出层的神经元数量,4代表隐藏层神经元数量。输出层的偏置 b [ 2 ] b^{[2]} b[2]维度是(1,1)。

总结一下,第 i i i层的权重矩阵 W [ i ] W^{[i]} W[i]的维度的行数等于第 i i i层的神经元数量,列数等于第 i − 1 i-1 i1层的神经元数量;第 i i i层的偏置矩阵 b [ i ] b^{[i]} b[i]的维度的行数等于第 i i i层的神经元数量,列数始终为1。

希望以上内容能帮助你更好地理解神经网络的基本结构和表示方法!

3. 计算神经网络的输出 (Computing a Neural Network’s Output)

神经网络的魅力之处在于它如何进行计算。我们可以把两层神经网络看作是逻辑回归的两次运算。为了理解这个概念,首先我们回忆一下逻辑回归的基础。在逻辑回归中,我们进行两个主要的计算步骤,如下所示:

z = w T x + b z = w^T x + b z=wTx+b

接着,

a = σ ( z ) a = \sigma(z) a=σ(z)

逻辑回归图示

当我们看向两层神经网络时,从输入层到隐藏层的运算与逻辑回归相似,然后从隐藏层到输出层再次进行相似的运算。在标记这些运算时,我们需要注意上下标的使用。上标通常用来表示层的编号,而下标则表示在该层的哪一个神经元。例如, a i [ l ] a^{[l]}_i ai[l] 表示第 l l l层的第 i i i个神经元的激活值。

下面是神经网络从输入层到隐藏层的计算公式:

z 1 [ 1 ] = w 1 [ 1 ] T x + b 1 [ 1 ] , a 1 [ 1 ] = σ ( z 1 [ 1 ] ) z^{[1]}_1 = w^{[1]T}_1 x + b^{[1]}_1, \quad a^{[1]}_1 = \sigma(z^{[1]}_1) z1[1]=w1[1]Tx+b1[1],a1[1]=σ(z1[1])

z 2 [ 1 ] = w 2 [ 1 ] T x + b 2 [ 1 ] , a 2 [ 1 ] = σ ( z 2 [ 1 ] ) z^{[1]}_2 = w^{[1]T}_2 x + b^{[1]}_2, \quad a^{[1]}_2 = \sigma(z^{[1]}_2) z2[1]=w2[1]Tx+b2[1],a2[1]=σ(z2[1])

z 3 [ 1 ] = w 3 [ 1 ] T x + b 3 [ 1 ] , a 3 [ 1 ] = σ ( z 3 [ 1 ] ) z^{[1]}_3 = w^{[1]T}_3 x + b^{[1]}_3, \quad a^{[1]}_3 = \sigma(z^{[1]}_3) z3[1]=w3[1]Tx+b3[1],a3[1]=σ(z3[1])

z 4 [ 1 ] = w 4 [ 1 ] T x + b 4 [ 1 ] , a 4 [ 1 ] = σ ( z 4 [ 1 ] ) z^{[1]}_4 = w^{[1]T}_4 x + b^{[1]}_4, \quad a^{[1]}_4 = \sigma(z^{[1]}_4) z4[1]=w4[1]Tx+b4[1],a4[1]=σ(z4[1])

接着,从隐藏层到输出层的计算公式如下:

z 1 [ 2 ] = w 1 [ 2 ] T a [ 1 ] + b 1 [ 2 ] , a 1 [ 2 ] = σ ( z 1 [ 2 ] ) z^{[2]}_1 = w^{[2]T}_1 a^{[1]} + b^{[2]}_1, \quad a^{[2]}_1 = \sigma(z^{[2]}_1) z1[2]=w1[2]Ta[1]+b1[2],a1[2]=σ(z1[2])

在这里,我们将隐藏层的输出汇总为一个向量:

a [ 1 ] = [ a 1 [ 1 ] a 2 [ 1 ] a 3 [ 1 ] a 4 [ 1 ] ] a^{[1]} = \left[ \begin{array}{c} a^{[1]}_1 \\ a^{[1]}_2 \\ a^{[1]}_3 \\ a^{[1]}_4 \\ \end{array} \right] a[1]= a1[1]a2[1]a3[1]a4[1]

每次计算都对应着逻辑回归的步骤,涉及到 z z z a a a的计算。

为了使计算更高效,我们采用矩阵运算来向量化这些操作:

z [ 1 ] = W [ 1 ] x + b [ 1 ] z^{[1]} = W^{[1]} x + b^{[1]} z[1]=W[1]x+b[1]

a [ 1 ] = σ ( z [ 1 ] ) a^{[1]} = \sigma(z^{[1]}) a[1]=σ(z[1])

z [ 2 ] = W [ 2 ] a [ 1 ] + b [ 2 ] z^{[2]} = W^{[2]} a^{[1]} + b^{[2]} z[2]=W[2]a[1]+b[2]

a [ 2 ] = σ ( z [ 2 ] ) a^{[2]} = \sigma(z^{[2]}) a[2]=σ(z[2])

神经网络矩阵计算图示

结尾,我们再次提及这些矩阵的维度: W [ 1 ] W^{[1]} W[1]的维度是(4,3), b [ 1 ] b^{[1]} b[1]的维度是(4,1), W [ 2 ] W^{[2]} W[2]的维度是(1,4),而 b [ 2 ] b^{[2]} b[2]的维度是(1,1)。正确地理解这些维度是非常重要的。

希望上述内容能够帮助你清晰地理解神经网络的计算过程!

4. 向量化多个样本(Vectorizing across multiple examples)

假设你有一个长队的人等着买票。如果售票员每次只为一个人售票,这将是一个缓慢的过程。相同地,在神经网络中,如果我们一个接一个地处理数据样本,会很低效。向量化是一种技巧,允许我们同时处理所有样本,就像有多个售票员同时工作一样。

每个样本都有自己的标签,如 X ( i ) X^{(i)} X(i) Z ( i ) Z^{(i)} Z(i) A ( i ) A^{(i)} A(i)中的 ( i ) (i) (i)。如果有 m m m个样本,你可能会想用循环来处理它们:

for i = 1 to m:

但这不够快。向量化使我们能够一次性处理所有样本:

Z [ 1 ] = W [ 1 ] X + b [ 1 ] Z^{[1]} = W^{[1]}X + b^{[1]} Z[1]=W[1]X+b[1]
A [ 1 ] = σ ( Z [ 1 ] ) A^{[1]} = \sigma(Z^{[1]}) A[1]=σ(Z[1])
Z [ 2 ] = W [ 2 ] A [ 1 ] + b [ 2 ] Z^{[2]} = W^{[2]}A^{[1]} + b^{[2]} Z[2]=W[2]A[1]+b[2]
A [ 2 ] = σ ( Z [ 2 ] ) A^{[2]} = \sigma(Z^{[2]}) A[2]=σ(Z[2])

这里,你可以想象矩阵就像是一个大屏幕,显示了所有样本的结果。

5. 向量化实现的解释(Explanation for Vectorized Implementation)

简单地说,上述方法就是利用更大的显示屏一次性查看所有电影,而不是一个接一个地看。

6. 激活函数(Activation functions)

你可以将激活函数想象成音乐的音调调节器。就像调节音乐的高低,激活函数帮助我们调整输出的“音调”。

  • sigmoid函数:
    sigmoid

  • tanh函数:
    tanh

  • ReLU函数:
    ReLU

  • Leaky ReLU函数:
    Leaky ReLU

每种函数都像是不同的音调设置。选择哪个最好并没有固定答案,就像选择最喜欢的歌曲一样,这取决于情境和个人喜好。但通常,ReLU和Leaky ReLU在隐藏层中的性能比其他函数好,而sigmoid则适用于输出层。

7. 为什么需要非线性激活函数 (Why do we need non-linear activation functions?)

在很多的书籍和资料中,我们经常看到大部分激活函数都是非线性的。那么,我们可能会有疑问,为什么不使用线性激活函数呢?下面就来带大家解开这个谜团。

想象一下,如果我们的神经网络中所有的激活函数都是线性的。其实这意味着无论我们有多少层,都只能得到输入数据的线性组合。这样的话,多层的神经网络和一个简单的线性模型没什么两样。换句话说,这样的神经网络的表达能力会很有限。

详细来说,如果所有激活函数都是线性的,我们可以看到:
a [ 2 ] = z [ 2 ] = W [ 2 ] a [ 1 ] + b [ 2 ] = ( W [ 2 ] W [ 1 ] ) x + ( W [ 2 ] b [ 1 ] + b [ 2 ] ) = W ′ x + b ′ a^{[2]} = z^{[2]} = W^{[2]}a^{[1]}+b^{[2]} = (W^{[2]}W^{[1]})x + (W^{[2]}b^{[1]}+b^{[2]}) = W'x + b' a[2]=z[2]=W[2]a[1]+b[2]=(W[2]W[1])x+(W[2]b[1]+b[2])=Wx+b

这里,我们发现无论如何都是输入x的线性组合。这样的网络与简单的线性模型几乎没有区别。

如果隐藏层全部使用线性激活函数,而仅在输出层使用非线性激活函数,整个神经网络的复杂度和深度其实都被大大简化了,它可能只是一个简单的逻辑回归模型。

但是,如果我们处理的是预测问题,输出层的激活函数是可以使用线性的,因为我们的目标是得到一个连续的值。此外,如果输出恒为正值,ReLU也是一个不错的选择。

8. 激活函数的导数 (Derivatives of activation functions)

当我们使用梯度下降算法来训练神经网络时,需要计算每个激活函数的导数。这些导数是关键,因为它们帮助我们知道如何调整每层的权重。

对于sigmoid函数:
g ( z ) = 1 1 + e − z g(z) = \frac{1}{1+e^{-z}} g(z)=1+ez1
它的导数是:
g ′ ( z ) = g ( z ) ( 1 − g ( z ) ) = a ( 1 − a ) g'(z) = g(z)(1-g(z)) = a(1-a) g(z)=g(z)(1g(z))=a(1a)

tanh函数的定义如下:
g ( z ) = e z − e − z e z + e − z g(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} g(z)=ez+ezezez
而它的导数是:
g ′ ( z ) = 1 − ( g ( z ) ) 2 = 1 − a 2 g'(z) = 1 - (g(z))^2 = 1 - a^2 g(z)=1(g(z))2=1a2

再来看ReLU函数:
g ( z ) = m a x ( 0 , z ) g(z) = max(0, z) g(z)=max(0,z)
它的导数是:
g ′ ( z ) = { 0 , if  z < 0 1 , if  z ≥ 0 g'(z) = \begin{cases} 0, & \text{if } z<0 \\ 1, & \text{if } z\geq0 \end{cases} g(z)={0,1,if z<0if z0

最后是Leaky ReLU函数:
g ( z ) = m a x ( 0.01 z , z ) g(z) = max(0.01z, z) g(z)=max(0.01z,z)
它的导数为:
g ′ ( z ) = { 0.01 , if  z < 0 1 , if  z ≥ 0 g'(z) = \begin{cases} 0.01, & \text{if } z<0 \\ 1, & \text{if } z\geq0 \end{cases} g(z)={0.01,1,if z<0if z0

这些激活函数及其导数在神经网络训练中起到了非常关键的作用,帮助网络学习到数据中的复杂模式。

9. 神经网络中的梯度下降 (Gradient descent for neural networks)

神经网络的训练核心是通过梯度下降来不断更新权重和偏置,以最小化代价函数。为了实现这一目的,我们需要知道每个参数对代价函数的影响,即它们的梯度。下面我们会进一步探讨这个过程。

考虑一个简单的浅层神经网络。其中我们有参数, W [ 1 ] , b [ 1 ] , W [ 2 ] , b [ 2 ] W^{[1]}, b^{[1]}, W^{[2]}, b^{[2]} W[1],b[1],W[2],b[2]。假设输入层有n个特征,隐藏层有m个神经元,输出层有k个神经元。于是我们可以得到:

  • W [ 1 ] W^{[1]} W[1] 的维度是 (m, n)
  • b [ 1 ] b^{[1]} b[1]的维度是 (m, 1)
  • W [ 2 ] W^{[2]} W[2] 的维度是 (k, m)
  • b [ 2 ] b^{[2]} b[2] 的维度是 (k, 1)

神经网络的正向传播定义如下:
Z [ 1 ] = W [ 1 ] X + b [ 1 ] Z^{[1]} = W^{[1]}X + b^{[1]} Z[1]=W[1]X+b[1]
A [ 1 ] = g ( Z [ 1 ] ) A^{[1]} = g(Z^{[1]}) A[1]=g(Z[1])
Z [ 2 ] = W [ 2 ] A [ 1 ] + b [ 2 ] Z^{[2]} = W^{[2]}A^{[1]} + b^{[2]} Z[2]=W[2]A[1]+b[2]
A [ 2 ] = g ( Z [ 2 ] ) A^{[2]} = g(Z^{[2]}) A[2]=g(Z[2])

这里的g代表了激活函数。

反向传播是神经网络训练中非常关键的一步,它帮助我们计算出代价函数关于每个参数的梯度。下面是这些梯度的计算公式:

d Z [ 2 ] = A [ 2 ] − Y dZ^{[2]} = A^{[2]} - Y dZ[2]=A[2]Y
d W [ 2 ] = 1 m d Z [ 2 ] A [ 1 ] T dW^{[2]} = \frac{1}{m} dZ^{[2]} A^{[1]T} dW[2]=m1dZ[2]A[1]T
d b [ 2 ] = 1 m n p . s u m ( d Z [ 2 ] , a x i s = 1 , k e e p d i m s = T r u e ) db^{[2]} = \frac{1}{m} np.sum(dZ^{[2]}, axis=1, keepdims=True) db[2]=m1np.sum(dZ[2],axis=1,keepdims=True)
d Z [ 1 ] = W [ 2 ] T d Z [ 2 ] ∗ g ′ ( Z [ 1 ] ) dZ^{[1]} = W^{[2]T} dZ^{[2]} * g'(Z^{[1]}) dZ[1]=W[2]TdZ[2]g(Z[1])
d W [ 1 ] = 1 m d Z [ 1 ] X T dW^{[1]} = \frac{1}{m} dZ^{[1]} X^T dW[1]=m1dZ[1]XT
d b [ 1 ] = 1 m n p . s u m ( d Z [ 1 ] , a x i s = 1 , k e e p d i m s = T r u e ) db^{[1]} = \frac{1}{m} np.sum(dZ^{[1]}, axis=1, keepdims=True) db[1]=m1np.sum(dZ[1],axis=1,keepdims=True)

其中,*代表元素对元素的乘法,g’是激活函数的导数。

这些公式可能看起来有些复杂,但它们都是基于链式法则来推导的。在接下来的部分,我们会详细探讨这些公式的推导过程,帮助你更好地理解反向传播是如何工作的。

10. 反向传播的直观理解 (Backpropagation intuition, optional)

我们继续用计算图的方式去理解神经网络的反向传播。当我们之前学习逻辑回归的时候,已经使用了计算图来推导正向传播和反向传播。

这里写图片描述

由于神经网络引入了隐藏层,这使得其计算图的复杂性比逻辑回归更高。如下所示的计算图,为单个训练样本的正向传播和反向传播流程:

这里写图片描述

对于反向传播,我们可以基于以下的梯度计算:

d z [ 2 ] = a [ 2 ] − y dz^{[2]} = a^{[2]} - y dz[2]=a[2]y
d W [ 2 ] = d z [ 2 ] a [ 1 ] T dW^{[2]} = dz^{[2]} a^{[1]T} dW[2]=dz[2]a[1]T
d b [ 2 ] = d z [ 2 ] db^{[2]} = dz^{[2]} db[2]=dz[2]
d z [ 1 ] = W [ 2 ] T d z [ 2 ] ∗ g ′ [ 1 ] ( z [ 1 ] ) dz^{[1]} = W^{[2]T} dz^{[2]} * g'^{[1]}(z^{[1]}) dz[1]=W[2]Tdz[2]g[1](z[1])
d W [ 1 ] = d z [ 1 ] x T dW^{[1]} = dz^{[1]} x^T dW[1]=dz[1]xT
d b [ 1 ] = d z [ 1 ] db^{[1]} = dz^{[1]} db[1]=dz[1]

上述梯度公式看起来可能很抽象,但它们在计算图中有明确的物理意义。每一个计算步骤都可以理解为在计算图中从输出开始,逐步往输入方向推导的结果。

这里写图片描述

简而言之,一个含有一个隐藏层的浅层神经网络,对于m个训练样本,其正向传播和反向传播可以总结为6个主要的数学表达式。这些表达式为神经网络的训练提供了明确的数学指导,帮助我们更新和优化网络的权重和偏置。

接下来,我们将进一步深入反向传播的细节,尝试更直观地理解这些数学公式背后的物理意义和逻辑。

11. 随机初始化 (Random Initialization)

当我们构建神经网络模型时,经常会遇到一个疑问:为什么不能将权重W全部初始化为零呢?让我们一起深入探讨下这个问题。

想象一下,你正在创建一个简单的浅层神经网络。这个网络有两个输入,隐藏层有两个神经元。如果你尝试将所有权重初始化为零,那么:

W [ 1 ] = [ 0 0 0 0 ] W^{[1]} = \begin{bmatrix} 0 & 0 \\ 0 & 0 \end{bmatrix} W[1]=[0000]

W [ 2 ] = [ 0 0 ] W^{[2]} = \begin{bmatrix} 0 & 0 \end{bmatrix} W[2]=[00]

发生了什么?隐藏层的两个神经元输出会是一样的,因为他们的权重是相同的。这意味着无论我们如何进行训练,这两个神经元始终会有完全相同的更新和输出。那么这样的话,为什么还需要两个神经元呢?一个就够了。而且更重要的是,所有的神经元都会变得相同,失去了多个神经元的意义。

要注意的是,虽然权重不能初始化为零,但偏置b是可以的。因为初始化为零的偏置不会带来这种对称性问题。

那么,权重为什么不能初始化为零?这个问题我们称之为“对称性打破问题”(symmetry breaking problem)。

为了解决这个问题,我们可以使用随机初始化权重的方法。这样每个神经元在开始时就有不同的权重,随着训练的进行,它们会有各自的学习路径。在Python中,我们可以使用以下代码来随机初始化权重和偏置:

W_1 = np.random.randn((2,2))*0.01
b_1 = np.zero((2,1))
W_2 = np.random.randn((1,2))*0.01
b_2 = 0
你可能会疑惑,为什么我们在初始化权重时要将其乘以0.01?

这其实是个技巧,确保权重不会太大也不会太小。当我们使用像sigmoid或tanh这样的激活函数时,这个小权重会让我们计算的 z z z值接近于0。而当 z z z值接近0时,这些激活函数的梯度是最大的,从而使得我们的梯度下降算法能够更迅速地找到解决方案。

但如果你的神经网络使用了ReLU或Leaky ReLU这类激活函数,这个初始化权重的问题就没那么敏感了。不过,如果输出层你选择了sigmoid函数,那最好还是让权重小一些。

12. 总结 (Summary)

神经网络听起来很复杂,但其实它只是一种模仿人脑工作方式的算法。想象一下,你有一个大型的计算机网络,这个网络由很多输入、中间处理单元(隐藏层)和输出组成。这次,我们就是要深入这个网络,了解它的内部运作。

我们首先看了神经网络的基本框架,也就是输入、隐藏层和输出。然后,我们用计算图深入了解了它是如何进行计算的。这之后,我们探讨了激活函数,这些函数能让网络做出非线性的决策,没有它们,神经网络就只是一堆无用的数学运算。

然后,我们进入了反向传播的世界,这是让神经网络"学习"的关键步骤。通过这个过程,网络可以知道自己在哪里犯了错误,并做出调整。

最后,我们探讨了为什么要随机初始化权重。简单地说,这是为了打破网络中的对称性,让每个神经元都能有自己的角色。

希望这次简单的解释可以帮你更好地理解神经网络,并应用在实际问题中!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快撑死的鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值