神经网络与深度学习三:编写单隐层神经网络

三:编写单隐层神经网络

1 神经网络概述

这篇文章你会学到如何实现一个神经网络,在我们深入学习技术细节之前,现在先大概快速的了解一下如何实现神经网络,如果你对某些内容不甚理解(后面的文章中会深入其中的细节)

上周我们讨论了logistic回归,一起了解了这个模型

image-20220419100815880

和下面这个流程图的联系

image-20220419101232069

这里面你需要输入特征x,参数w和b,用那些计算z,然后用z计算出 a,我们用a表示 y ^ \hat{y} y^,接下来你就可以计算损失函数L,神经网络就是这个样子。之前提到过你可以把很多sigmoid单元堆叠起来构成一个神经网络

image-20220419101511959

其中的每一个结点都像logistic的计算(对应像z这样的计算,然后是类似a的计算)

image-20220419101915184

用x表示输入特征,还有参数W和b这样你就计算出了 z [ 1 ] z^{[1]} z[1]

image-20220419114559983

用上标[i]表示第i层,使用[]的目的是区分用来表示单个训练样本的圆括号, x ( i ) x^{(i)} x(i)表示第i个样本,上标[1],[2]用于表示不同的层。然后使用类似logistic回归去计算 z [ 1 ] z^{[1]} z[1]之后,需要用 s i g m o i d ( z [ 1 ] ) sigmoid(z^{[1]}) sigmoid(z[1])计算 a [ 1 ] a^{[1]} a[1],接下来用另一个线性方程计算 z [ 2 ] z^{[2]} z[2],接着计算 a [ 2 ] a^{[2]} a[2] a [ 2 ] a^{[2]} a[2]就是整个神经网络最终输出

image-20220419115628231

y ^ \hat{y} y^表示网络的输出,我知道其中有很多细节,但关键你要学到这种直觉,logistic回归中得到z后直接计算a,而这个神经网络中我们要做多次计算,反复计算z和a,最后计算损失函数。应该还记得在logistic回归中会有这种反向传播用来计算da,dz。

image-20220419120148933

同样在神经网络中也有类似的反向计算,最后会计算 d a [ 2 ] da^{[2]} da[2], d z [ 2 ] dz^{[2]} dz[2]等等,

image-20220419120515131

现在你已经大概了解了神经网络是什么样,基本上我们把logsitic回归重复了两次。如果你对以上内容没有理解,别担心,接下来会慢慢讲解这些细节。

2 神经网络的表示

我们先看一下只有一个隐藏层的神经网络如下

image-20220420164314332

在一个神经网络中,当你用监督学习训练它的时候,训练集包含了输入x,目标输出y,隐藏层的含义是在训练集中这些中间节点的真正数值我们是不知道的,在训练集中你看不到它们的数值。现在我们再引入几个符号

之前我们用向量x表示输入特征,输入特征的数值还有另外一种表示方式,我们用 a [ 0 ] a^{[0]} a[0]表示,这个a也表示激活的意思,它意味着网络中不同层的值会传递给后面的值,输入层将x的值传递给隐藏层,我们将输入层的激活值称为 a [ 0 ] a^{[0]} a[0],下一次即隐藏层也同样会产生一些激活值将其记作 a [ 1 ] a^{[1]} a[1]。隐藏层的第一个结点命名为 a 1 [ 1 ] a^{[1]}_1 a1[1],依次类推

image-20220420165655178

这里的 a [ 1 ] a^{[1]} a[1]是一个四维向量,写成python代码它是一个4×1的矩阵,或者大小为4的列向量

image-20220420170013180

它是四维的因为本例中我们有四个结点,四个单元 四个隐藏层单元,最后的输出层会产生某个数值 a [ 2 ] a^{[2]} a[2]是个实数, y ^ = a [ 2 ] \hat{y}=a^{[2]} y^=a[2],所以这就和logistic回归类似,在logistic回归中 y ^ = a \hat{y}=a y^=a我们只有一个输出层,所以没有用带方括号的上标。在约定俗成的符号里上面这个例子就是双层神经网络,当我们计算网络层数时不算入输入层的原因是因为我们不把输入层看作一个标准的层。

image-20220420171705437

我们要知道隐藏层和输出层是带有参数的,隐藏层的参数w1,b1,之后我们会看到w是一个4×3的矩阵,b是一个4×1的向量,第一个数字4是说有四个节点或者说四个隐藏单元,然后数字3来自这里有三个输入特征,之后会更加详细的讨论这些矩阵的维数,到那时你可能就明白了。类似的输出层也有一些和它有关的参数w2,b2从维数来看分别是1×4和1×1,1×4是因为隐藏层有四个单元而输出层只有一个单元,之后会对这些矩阵和向量的维度做更加深入的解释

所以现在你已经知道一个双层神经网络是怎样的,它是只有一个隐藏层的神经网络,接下来我们将更加深入的了解这个神经网络到底在计算什么,怎么输入x然后又是怎么一直算下去得到 y ^ \hat{y} y^

3 计算神经网络的输出

我们之前说的logistic回归,这里的圆圈代表了回归计算的两个步骤

image-20220420172355700

神经网络不过是重复计算这些步骤很多次,

image-20220420173208921

我们先看隐藏的第一个节点,暂时隐去其他节点,左边看上去和logistic回归很相似,隐层的这个节点进行两步计算,第一步可以看作是节点的左边计算 z = w T x + b z=w^Tx+b z=wTx+b,然后第二步计算a=sigmoid(z)

image-20220420173452399

上标方括号表示层数,下标i则表示层中的第几个节点

现在看看神经网络的第二个节点

image-20220420173723621

所以隐藏层的计算就是

image-20220420173952795但是用for循环来做很低效,所以接下来要做的就是把这四个等式向量化

image-20220420174346921我们将单独的z堆叠起来构成一个列向量 Z [ 1 ] Z^{[1]} Z[1]

image-20220420174604999

最后你需要计算的就是a

image-20220420174909491

用sigmoid函数作用于Z的四个元素

用同样的方法推导下一层

image-20220420175624657

最后就得到了一个实数

盖住左侧,你会发现和logistic回归类似,不过是参数名变化了, w T − > W [ 2 ] , b − > b [ 2 ] w^T->W^{[2]},b->b^{[2]} wT>W[2],b>b[2]

image-20220420175816940

现在你知道如何输入单个特征向量x,你可以用四行代码计算出这个神经网络的输出,和我们处理logistic回归时的做法类似,我们也想把整个训练样本都向量化,我们会发现通过把不同的训练样本堆叠起来构成矩阵,只需要稍微修改这些公式,你就可以得到之前logistic回归的结果,能不只是一次计算出一个样本的神经网络输出,而是能一次性计算你的整个训练集。接下来就看看怎么实现

4 多个样本的向量化

你现在有m个样本

image-20220420181420132

这里的 a [ 2 ] ( i ) a^{[2](i)} a[2](i)表示:圆括号里的i表示训练样本i,方括号指第二层

如果你不通过向量化,使用for循环,就是下面这种形式

image-20220420181959223

下面看看如何向量化m个训练样本

还记得我们之前定义过矩阵X,就是我们的训练样本堆到各列,做成一个 n x × m n_x×m nx×m维的矩阵

image-20220420182541372

像X一样,把z堆叠起来

image-20220420202513409

同样a,把它们以列向量堆叠起来

image-20220420203419407

横向的话,我们对所有的训练样本用指标排序,所以横向指标就对应了不同的训练样本,当你从左到右扫的时候就扫过了整个训练集,而在竖向,竖向指标就对应了神经网络里的不同节点,比如下面紫色(A里的第一行第一列)对应第一个训练样本第一个隐藏单元的激活函数

image-20220420204134762

同样的形式也适用于矩阵Z和矩阵X

这样就得到了向量化的表示

image-20220420203542632

5 向量化实现的解释

在之前的文章中我们看到了如何将训练样本横向堆叠起来构成矩阵X,你就可以导出一个网络中正向传播算法的向量化实现,这里我们讲一下更多的理由说明为什么我们写下的方程向量化在多样本时的正确实现,我们对几个样本手动算算正向传播。为了形式上简便,先让b=0

image-20220420210018261

到目前为止我们一直用的是sigmoid函数,事实证明这不是最好的选择,在下面我们进一步深入研究如何使用不同种类的激活函数,其实sigmoid函数只是其中的一个可能选择

6 激活函数

要搭建一个神经网络你可以选择的是选择隐层里用哪一个激活函数,还有神经网络的输出单元用什么激活函数,到目前为止我们一直用的是sigmoid函数,但有时其他函数效果要好得多。我们看看一些可供选择的函数。

在神经网络的正向传播步骤中我们用的是

image-20220420211754385

在更一般的情况下我们可以只用不同的函数 g ( Z [ 1 ] ) g(Z^{[1]}) g(Z[1]),其中g可以是非线性函数不一定是sigmoid

image-20220420212025298

比如sigmoid介于0和1之间,有个激活函数几乎总比sigmoid表现更好,就是tanh函数或者叫双曲正切函数

image-20220420213210208

介于-1和1之间,数学上这其实就是sigmoid函数平移后的版本。事实证明对于隐藏单元如果你让函数g(z)等于tanh(z),这几乎总比sigmoid函数效果更好,因为现在函数输出介于-1和1之间,激活函数的平均值就更接近0,有时你可能要平移所有的数据让数据平均值为0,使用tanh函数而不是sigmoid函数也有类似数据中心化的效果使得数据平均值接近0,而不是0.5,这实际上让下一层的学习更方便一点,在后面会详细讨论这一点,那时也会介绍算法优化。但这里要记住一点我们几乎不用sigmoid激活函数了,tanh函数几乎在所有场合都更优越。一个例外是输出层,因为如果y是0或1那么你希望 y ^ \hat{y} y^介于0到1之间更合理。我会使用sigmoid函数的一个例外场合是使用二元分类的时候,在这种情况下你可以使用sigmoid激活函数作为输出层

image-20220420214841476

所以在这个例子中可以在隐层使用tanh函数,输出层用sigmoid函数,所以不同层的激活函数可以不一样,有时候为了表示不同层的不同激活函数,我们可能会用方括号上标来表示 g [ 1 ] g^{[1]} g[1]可能和 g [ 2 ] g^{[2]} g[2]不同。现在sigmoid函数和tanh函数都有一个缺点就是如果z非常大或者非常小那么这个导数的梯度可能就很小,所以z很大或很小的时候导数接近0,这样会拖慢梯度下降法,在机器学习里最受欢迎的一个工具是所谓的修正线性单元(ReLU)

image-20220420215845401

只要z为正导数就是1,当z为负时,导数为0,如果你实际使用这个函数,z刚好为0时导数是没有定义的,但如果你编程实现那么你得到z刚好等于0 0 0 0 …的概率很低,所以实践中不用担心这一点,你可以在z=0时给导数赋值,你可以赋值成1或0那样也是可以的,尽管事实上这个函数不可微。在选择激活函数时有一些经验法则。如果你的输出值是0和1,如果你在做二元分类那么sigmoid函数很适合作为输出层的激活函数,然后其他所有单元都用ReLU所谓的修正线性单元,现在已经变成激活函数的默认选择了,如果你不确定隐层应该用哪个,我就用ReLU作为激活函数,这是今天大多数人都在用的,虽然人们有时也会用tanh激活函数。而ReLU的一个缺点就是当z<0时导数等于0,在实践中这没什么问题,但ReLU还有另一个版本 叫做leaky ReLU,当z为负时,函数不再为0它有一个很平缓的斜率,

image-20220421211415203

这通常比ReLU激活函数更好,不过实际中的使用频率没那么高.这些你选一个就好了,如果你一定要选一个,我通常只用ReLU,使用ReLU和leaky ReLU的好处是对于很多z空间,激活函数的导数和0差很远,所以在实际中使用ReLU 你的神经网络的学习速度通常会快很多,比使用tanh或sigmoid函数快的多,主要原因在于ReLU函数没有这种函数斜率接近0的时减慢学习效率的效应。我知道对z的一半范围来说,ReLU的斜率为0但在实践中有足够多的隐藏单元令z大于0,所以对大多数训练样本来说还是很快的

总结,sigmoid函数除非在二元分类的输出层,不然绝对不要使用或者几乎从来不会用,我从来没用过,因为tanh几乎在所有的场合都更优越,还有最常用的默认激活函数是ReLU,或者你想用的话也可以试试leaky ReLU

image-20220421212231144

你可能会问为什么leaky ReLU表达式中第一项是0.01z,为什么常数是0.01,你也可以把它设置成学习函数的另一个参数,有人说这样效果更好,但我很小见到有人这么做所以如果你想要在你的应用中试试自己喜欢就好,你可以看看效果如何 有多好如果很好就一直使用它

我希望这样你就对如何在你的神经网络里选择激活函数有概念,深度学习其中一个特点是在建立神经网络时经常有很多不同的选择,比如隐层单元数,激活函数,还有如何初始化权重,这个我们接下来会讲,有时真的很难 去定下一个准则来确定什么参数最适合你的问题,所以接下来会介绍行业里见到的热门选择或冷门选择,但是对于你的应用的特质事实上很难预先准确的知道什么参数最有效,所以一个建议是如果你不确定哪种激化函数最有效,你可以先试试在你的保留交叉验证数据集上跑跑或者在测试集上试试,看看哪个参数效果更好就用那个,我想通过在你的应用中测试这些不同的选择你可以搭建出具有前瞻性的神经网络架构,可以对你的问题的特质更有针对性,让你的算法迭代更流畅,我这里不会告诉你一定要用ReLU激活函数而不用其他的那对你现在或者未来要处理的问题而言可能管用也可能不管用,好这就是激活函数的选择,你们看到了最热门的激活函数。还有一个问题经常有人会问为什么你需要激活函数呢为什么不直接去掉,下面我们会谈到为什么

7 为什么需要非线性激活函数

事实证明要让你的神经网络能够计算出有趣的函数你必须使用非线性激活函数

image-20220422090322608

这是神经网络的正向传播方程,为什么我们不能直接去掉g然后令 a [ 1 ] = z [ 1 ] a^{[1]}=z^{[1]} a[1]=z[1],或者你可以令g(z)=z,有时这叫线性激活函数,更学术一点的名字是恒等激活函数,因为它们就直接把输入值输出了。为了说明问题我们看看 a [ 2 ] = z [ 2 ] 会 怎 样 a^{[2]}=z^{[2]}会怎样 a[2]=z[2],事实证明,那么这个模型的输出 y ^ \hat{y} y^不过是你输入特征x的线性组合

image-20220422090612186

如果你要用线性激活函数或者叫恒等激活函数 那么神经网络只是把输入线性组合再输出,我们稍后会谈到深度神经网络,有很多层的神经网络很多隐层层,事实证明如果你使用线性激活函数或者如果没有激活函数那么无论你的神经网络有多少层一直在做的只是计算线性激活函数所以不如直接去掉全部隐藏层

image-20220422091707663

如果你在隐层使用线性激活函数,在输出层使用sigmoid函数,模型复杂度和没有任何隐层的的标准logistic回归是一样的,线性隐层一点用都没有,因为两个线性函数的组合本身就是线性函数,所以除非你引入非线性否则无法计算更有趣的函数网络层数再多也不行。只有一个地方可以使用线性激活函数g(z)=z,如果你要学习的是回归问题,所以y是一个实数,比如说你想要预测房价,y不是0,1而是一个实数,你知道房价是0美元一直到能多贵就多贵,我也不知道能到多少,也许几百万美元,但不管你的数据集里房价是多少,y都是一个实数,那么用线性激活函数也许可行,所以你的输出y也是一个实数,从负无穷到正无穷,但是隐层层不能用线性激活函数,它们可以用ReLU或leaky ReLU或其他,

image-20220422093246854

所以唯一可以用线性激活函数的地方通常就是输出层,除了这种情况会在隐层使用线性激活函数的可能除了一些和压缩有关的非常特殊的情况,在那之外使用线性激活函数非常少见。哦当然实际上预测住房价格就像在第一篇文章中看到的因为房价都是非负,也许可以使用ReLU函数

希望这样你就知道为什么使用非线性激活函数对神经网络来说很关键,接下来我们将开始讨论梯度下降

8激活函数的导数

在开始梯度下降之前,我们先看一下如何计算单个激活函数的导数,当你对你的神经网络使用反向传播的时候你真的需要计算激活函数的斜率或者导数

首先是sigmoid函数

image-20220422093809321

在神经网络中我们有a=g(z),所以公式就化简成a(1-a),导数用g’(z)表示,g’(z)在微积分中表示函数g()对输入变量z的导数

下面看一下tanh的导数

image-20220424192225016

g’(z)= 1 − a 2 1-a^2 1a2

最后我们看看如何计算ReLU和leaky ReLU激活函数的导数

image-20220424192439614

在z=0处导数没有定义,如果你要实现这个算法,可能在数学上不是百分之一百正确但实际是可行的,如果z刚好在0,你可以让导数等于1或者为0,这其实无关紧要,如果你对优化术语很熟悉g’(z)就变成所谓的激活函数g(z)的次梯度,这样梯度下降法依然有效,但是z精准为0的概率非常非常小,所以你将z=0处的导数设成哪个值实际无关紧要

如果你在训练自己的网络时使用leaky ReLU

image-20220424193110864

z精准为0时梯度也是没有定义的,但你可以写一段代码去定义这个梯度,在z=0时g’(z)=0.01或1,用哪个值其实无关紧要。

当我们知道了以上内容就已经准备好在你的神经网络上实现梯度下降法了

9 神经网络的梯度下降法

在本节你将看到梯度下降算法的具体实现,如何处理单隐层神经网络,我会提供给你所需要的方程来实现反向传播或者说梯度下降法,下一节会介绍更多推导,介绍为什么这特定的几个方程是精准的方程,可以针对你的神经网络实现梯度下降的正确方程

你的单隐层神经网络可能会有以下参数

image-20220425204458857

有 n x 也 常 用 n [ 0 ] 表 示 有 那 么 多 个 输 入 特 征 , n [ 1 ] 个 隐 藏 单 元 , n [ 2 ] 个 输 出 单 元 有n_x也常用n^{[0]}表示有那么多个输入特征, n^{[1]}个隐藏单元,n^{[2]}个输出单元 nxn[0]n[1]n[2]在我们的例子中只介绍过 n [ 2 ] = 1 n^{[2]}=1 n[2]=1的情况,这些参数的维度如下

image-20220425205916119

如果你在做二元分类,那么损失函数如下

image-20220425210054953

所以要训练参数,你的算法需要做梯度下降,在训练神经网络时,随机初始化参数很重要,而不是初始化为全0,我们稍后会看到为什么会这样,当你把参数初始化为某些值之后,每个梯度下降循环都会计算预测值,所以你基本上要计算i=1到m的预测值 h a t y hat{y} haty,然后你需要计算导数dw[1],db[1],dw[2],db[2],然后更新参数

image-20220425211026411

这就是梯度下降法的一次迭代循环,然后你重复这些步骤很多次,直到你的参数看起来在收敛。之前我们讨论过如何计算预测值,如何计算输出,也看到了如何用向量化的方式去做,所以关键在于如何计算这些偏导项dw[1],db[1],dw[2],db[2],我想做的是给出你需要的公式,求这些导数的公式,而在下一小节,会深入介绍我们是怎么想出这些公式的,所以总结一下,下面是正向传播的过程

image-20220425213420387

也就是下面

image-20220425220625678

如果你还记得前面讲的logistic回归,你会发现,第二层的计算和logistic回归几乎一样

image-20220425221124816

下面是反向传播

image-20220423090835707

这里有个细节,np.sum是python的numpy命令,用来对矩阵的一个维度求和,加上keepdims=True就是防止python直接输出这些古怪的秩为1的数组

image-20220426100243905

所以加上keepdims=True确保python输出的是矩阵,对于db[2]这个向量输出的维度是(n,1),准确的说应该是(n[2],1)这里n[2]等于1,db就是一个数字,但以后我们会看到真正需要考虑多维的情况。到目前为止我们所作的和logistic回归非常相似,但当你继续计算反向传播时

image-20220426181720093

这里的g’[1]是你用的隐藏层的激活函数的导数,*是逐个元素相乘。如果你不想使用keepdims可以使用reshape把np.sum的输出结果写成矩阵形式。

所以这是正向传播一共四个式子而反向传播有六个式子(下一小节会告诉你反向传播的式子为什么是这样),不过总之如果你要实现这些算法,你必须能正确的执行正向传播和反向传播计算,你必须要能够计算所有需要的导数,用梯度下降法来学习神经网络的参数,你也可以直接实现这个算法让它工作起来而不去了解很多微积分的知识,有很多成功的深度学习从业者是这样做的,但如果你想要了解,可以看下一小节

10(选修)直观理解反向传播

先复习一下之前讲的logistic回归

image-20220426185640541

在计算单隐层时和上面类似,不过是计算了两次,注意这里只是单个样本的推导

image-20220426194710535

把a[1]看成x

image-20220426195224602

就和之前的logistic回归一样了,接下来继续向左求导(假设 n [ 0 ] = 3 , n [ 1 ] = 4 , n [ 2 ] = 1 n^{[0]}=3,n^{[1]}=4,n^{[2]}=1 n[0]=3,n[1]=4,n[2]=1)

image-20220426201926905

其中
image-20220426201626239请添加图片描述

实现后向传播算法有个技巧,你必须保证矩阵的维度相互匹配,最后求处dw[1],db[1]

image-20220426202427959

所以反向传播就可以写成

image-20220426202533545

首先是正向,忽略b
在这里插入图片描述接下来是反向传播
image-20220516115133280

这就是单个样本的反向传播,但你应该不会意外我们实际不会一个个样本的计算,我们要把所有的训练样本向量化,把z堆叠起来

image-20220426203014089

所以向量化就是下图右侧的过程

image-20220426203302492

希望能让你们知道这些后向传播算法是怎么推导的,在所有的机器学习领域中,我认为反向传播算法的推导实际上是我看过最复杂的数学之一,它需要知道线性代数以及矩阵的导数要用链式法则推导出来,如果你不是矩阵微积分的专家,使用这个步骤你可以自己证明求导算法,但我认为实际上有很多的深度学习实践者看过类似的推导过程,他们已经掌握了这种直觉,并能够非常有效的实现该算法。最后还有一个细节想给大家分享,在你实现神经网络之前如何初始化你的神经网络的权重,事实证明初始化你的参数不要全零而是随机初始化,对训练你的神经网络来说这一点非常重要

11 随机初始化

对于logistics回归可以将权重全部初始化为0,但如果将神经网络的各参数数组全部初始化为0,再使用梯度下降法那会完全无效,let’s see why

假设你有两个输入特征

image-20220426204916519

把参数都初始成0,两个隐层单元在进行相同的计算,无论有多少个隐层单元它们都在计算完全一样的函数,所以没有一点用处

image-20220426205340308

这个问题的解决方案就是随机化所有参数

image-20220426210004008

乘以一个很小的数是为了将权重初始化成很小的随机数,b是可以初始化为0的。我们通常喜欢把权重矩阵初始化成非常非常小的随机数,因为如果你用的是sigmoid或tanh激活函数(或者在输出层使用了sigmoid),那么如果权重太大 当你计算激活函数值时w 很大,这些z会很大或很小,在这种情况下你最后可能落在这些tanh或sigmoid函数比较平缓的部分,梯度下降法会很慢,学习会很慢。如果你的神经网络中没有任何sigmoid或tanh激活函数那么问题可能没这么大,但如果你在做二元分类你的输出单元是sigmoid函数那么你就不希望初始参数太大,所以用0.01是比较合理的或者任意其他小数字。

image-20220426210216867

所以事实上有时有比0.01更好用的常数,但当你训练一个单隐层神经网络时这是一个相对较浅的神经网络没有太多的隐藏层,设为0.01应该还可以,但是当你训练一个很深的神经网络时你可能要试试0.01以外的常数

出来,如果你不是矩阵微积分的专家,使用这个步骤你可以自己证明求导算法,但我认为实际上有很多的深度学习实践者看过类似的推导过程,他们已经掌握了这种直觉,并能够非常有效的实现该算法。最后还有一个细节想给大家分享,在你实现神经网络之前如何初始化你的神经网络的权重,事实证明初始化你的参数不要全零而是随机初始化,对训练你的神经网络来说这一点非常重要

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

开始King

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

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

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

打赏作者

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

抵扣说明:

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

余额充值