反向传播算法

反向传播算法

微信公众号:幼儿园的学霸
个人的学习笔记,关于OpenCV,关于机器学习, …。问题或建议,请公众号留言;

在上一篇文章中对神经网络模型的前向传播算法做了总结,这里更进一步,对神经网络的反向传播(Back Propagation,BP)算法的过程进行总结、数学推导。这一篇涉及的数学知识比较多,主要涉及的就是一个偏微分链式求导法则。由于理解上可能有偏差,如果阅读上吃力的话也可以粗略的浏览,大概了解反向传播算法即可。但是,学习这些数学知识可以帮助我们更深入的理解神经网络。

目录

反向传播算法要解决的问题

损失函数/代价函数

神经网络的作用就是我们预先给它大量的数据(包含输入和输出)来进行训练,训练完成后,我们希望它对于将来的真实环境的输入也能给出一个令我们满意的输出。怎么找到合适的参数呢?
这里我们引入损失函数(或称代价函数、Loss函数)的概念。
假设有n组包含了输入和真实结果(或称期望结果、期望输出)的样本数据,对于每组输入,神经网络的输出结果
记为 a i a_i ai,真实结果(期望结果)记为 y i y_i yi
使用数学工具中的MAE(Mean Absolute Error,平均绝对误差),可以非常直观地表达出输出结果和真实结果的偏差,因此我们可以用MAE来写出一个下面这样的Loss函数,记为C,Loss值越大、说明神经网络的输出结果越远离我们的期望。
C = 1 n ∑ i = 1 n ∣ a i − y i ∣ C=\frac{1}{n}\sum_{i=1}^{n}|a_i-y_i| C=n1i=1naiyi
也可以用MSE(Mean Squared Error,均方误差)作为损失函数,MSE能更好地评价数据的变化程度,简单地说因为平方了一下、偏差是会被放大的。
C = 1 n ∑ i = 1 n ∣ a i − y i ∣ 2 C=\frac{1}{n}\sum_{i=1}^{n}|a_i-y_i|^2 C=n1i=1naiyi2
将Sigmoid神经元的表达式 f ( x ) = σ ( w x + b ) f(x)=σ(wx+b) f(x)=σ(wx+b)代入上面的损失函数中,可以发现x(输入)是固定的,yi(期望结果)也是固定的,让我们感性地想象一下:实际上影响Loss的只有wb,而最重要的任务也就是寻找wb使得Loss最小。

再具象一点,其实对神经网络进行训练的目的就是为每个神经元找到最适合它的wb的值,从而使得整个神经网络的输出最接近我们的期望。


NOTE:最常用的损失函数
在实际中,为了方便求导,一般使用如下的二次损失函数:
C = 1 2 n ∑ i = 1 n ∣ a i − y i ∣ 2 C=\frac{1}{2n}\sum_{i=1}^{n}|a_i-y_i|^2 C=2n1i=1naiyi2


梯度下降

根据上面的结论,损失C只和权重w及偏置b 有关,那么可以把C看成是一个关于wb的函数,如下所示。
C = f ( w , b ) \bm {C=f(w,b)} C=f(w,b)
由于神经网络中有大量的权重w和偏置b,因此上式按照上一篇中的结论,按矩阵形式进行表示。
对于简单的情况,上面的函数可能是这样的:

我们的目标是找到Wb使C最小,当然上面这张图很容易看出来合适的Wb在哪,但当面对更复杂的情况时、比如下图这样的,应该如何快速地找到C最小的点呢?

这里我们引入梯度下降算法,原理很简单:把上图看作是一个丘陵地带,想象我们有一个球放在某个位置,让它“自然地向低处滚”,滚得越低,C就越小,我们就越高兴。
那么怎样使得它往低处滚呢?(注意这里要搬出全文中第一个比较烧脑的概念了)微分法则告诉我们,当w移动Δwb移动Δb时,有:
Δ C ≈ ∂ C ∂ w Δ w + ∂ C ∂ b Δ b \bm {\Delta C \approx \frac{\partial C}{\partial w}\Delta w + \frac{\partial C}{\partial b}\Delta b} ΔCwCΔw+bCΔb
由于C表示的是损失,我们想让球往低处滚,当然是希望C不断变小,那ΔC应该恒为负,那么Δw、Δb应该如何取值呢? 梯度下降法是这么设计的:
Δ W = − η ∂ C ∂ w Δ b = − η ∂ C ∂ b \bm{ \Delta W = -\eta \frac{\partial C}{\partial w} } \\ \bm{ \Delta b = -\eta \frac{\partial C}{\partial b} } ΔW=ηwCΔb=ηbC
可以看出如此取值可以使ΔC恒为负,其中的η称为学习率。

那么现在问题变成了∂C/∂w、∂C/∂b,即 CwCb 的偏导,这就是反向传播算法所要解决的问题——
快速求解∂C/∂w∂C/∂b,从而算出ΔwΔb,使得ΔC恒为负、即使得Loss越来越小。

NOTE:用了反向传播算法的多层感知机(神经网络)–也就是这篇文章讲的神经网络–也就叫作BP神经网络

反向传播

损失函数的两个假设

反向传播算法是为了计算损失函数的偏导数∂C/∂w∂C/∂b,为了使算法可行,需要对损失函数的形式作两个假设。在介绍这些假设之前,先来看一个最常见的二次损失函数:
C = 1 2 n ∑ x ∣ ∣ y ( x ) − a L ( x ) ∣ ∣ 2 ( 1 ) \bm{ C=\frac{1}{2n} \sum_x ||y(x)-a^L(x)||^2} \quad \text(1) C=2n1xy(x)aL(x)2(1)
其中,n是训练样本总数,对所有训练样本损失求平均,y(x)是输入为x时对应的真实的输出,而L表示神经网络的层数,也就是说 a L ( x ) a^L(x) aL(x)表示的是输入为x时,神经网络最后一层输出层的输出,也就是神经网络的输出。

  • 第一个假设
    所有训练样本总的损失函数可以表示为单个样本损失函数和的平均值,即: C = 1 n ∑ x C x C=\frac{1}{n}\sum_x C_x C=n1xCx.我们很容易可以验证这个假设对于二次损失函数成立, C x = 1 2 ∣ ∣ y − a L ∣ ∣ 2 C_x=\frac{1}{2}||y-a^L||^2 Cx=21yaL2。这个假设其实大部分时候都是成立,除了对于少数比较另类的损失函数,不过本文并不涉及。
    我们需要这个假设的原因是因为反向传播算法实际是对于单个样本计算偏导数 ∂ C x ∂ w 和 ∂ C x ∂ b \frac{\partial C_x}{\partial w} \text{和}\frac{\partial C_x}{\partial b} wCxbCx,随后再通过对这些单样本的偏导数求平均作为 ∂ C ∂ w 和 ∂ C ∂ b \frac{\partial C}{\partial w} \text{和}\frac{\partial C}{\partial b} wCbC。事实上,在对wb求偏导的时候,我们将输入x当作是固定值,所以方便起见,暂时将Cx写作C,后面再写回来。

  • 第二个假设
    损失函数可以表示成神经网络输出的函数,即 C = C ( a L ) C=C(a^L) C=C(aL)

    例如,二次损失函数就满足这样的假设,因为对于一个训练样本x来说,有:
    C = 1 2 ∣ ∣ y − a L ∣ ∣ 2 = 1 2 ∑ j ( y j − a j L ) 2 C=\frac{1}{2}||y-a^L||^2 = \frac{1}{2}\sum_j(y_j - a^L_j)^2 C=21yaL2=21j(yjajL)2
    这样就表示成了输出的函数,因为对于一个输入x来说,它实际正确的输出y是个固定值,并不是我们可以修改的变量。我们可以改变的只能是通过改变weights和biases来改变神经网络的输出 a L a^L aL从而影响到损失函数的值。

Hadamard积-⊙

反向传播算法基于一些常见的线性代数操作:向量的相加,向量与矩阵的积等等。其中有一种操作不是很常见,这里简单介绍一下。假设st是两个相同维度的向量,我们使用s⊙t定义两个向量中对应分量相乘的操作,即 ( s ⊙ t ) j = s j t j (s \odot t)_j = s_jt_j (st)j=sjtj例如:
[ 1 2 ] ⊙ [ 3 4 ] = [ 1 ∗ 3 2 ∗ 4 ] = [ 3 8 ] \left[ \begin{matrix} 1 \\ 2 \end{matrix} \right] \odot \left[ \begin{matrix} 3 \\ 4 \end{matrix} \right] = \left[ \begin{matrix} 1*3 \\ 2*4 \end{matrix} \right] = \left[ \begin{matrix} 3 \\ 8 \end{matrix} \right] [12][34]=[1324]=[38]
这样的乘法操作被称为Hadamard积或Schur积。

反向传播算法的四个基本方程及证明

反向传播算法是关于理解改变weights和biases是如何改变损失函数C,也就是计算 ∂ C ∂ w j k l 和 ∂ C ∂ b j l \frac{\partial C}{\partial w^l_{jk}}\text{和}\frac{\partial C}{\partial b^l_{j}} wjklCbjlC。在介绍如何计算这些偏导数之前,先引入一个中间变量 δ j l \delta^l_j δjl,称其为第l层第j个神经元的误差。反向传播算法会先计算这个中间变量,随后再将其与需要的偏导数关联起来。其定义为
δ j l = ∂ C ∂ z j l ( 2 ) \delta^l_j = \frac{\partial C}{\partial z^l_j} \quad \text(2) δjl=zjlC(2)
反向传播算法将会对每一层l计算 δ l \delta^l δl,然后再得到对应的 ∂ C ∂ w j k l 和 ∂ C ∂ b j l \frac{\partial C}{\partial w^l_{jk}} \text{和} \frac{\partial C}{\partial b^l_j} wjklCbjlC

接下来就要介绍反向传播算法的四个方程了,同时将给出这些方程的简单证明。

方程一:输出层的误差

δ j L = ∂ C ∂ a j L σ ′ ( z j L ) ( B P 1 ) \delta^L_j = \frac{\partial C}{\partial a^L_j} \sigma{}'(z^L_j) \quad \text(BP1) δjL=ajLCσ(zjL)(BP1)
证明:
∵ δ j L = ∂ C ∂ z j L \because \delta^L_j = \frac{\partial C}{\partial z^L_j} δjL=zjLC
应用链式法则,我们可以就输出激活值的偏导数的形式重新表示上面的偏导数:
δ j L = ∑ k ∂ C ∂ a k L ∂ a k L ∂ z k L \delta^L_j = \sum_k \frac{\partial C}{\partial a^L_k} \frac{\partial a^L_k}{\partial z^L_k} δjL=kakLCzkLakL
这里的求和是在输出层的所有神经元k上运行的,当然,第 k t h k^{th} kth个神经元的输出激活值 a k L a^L_k akL只依赖于当 k = j k=j k=j时第 j t h j^{th} jth个神经元的带劝输入 z j L z^L_j zjL。所以,当 k ≠ j k \neq j k̸=j时:
∂ a k L ∂ z j L = 0 \frac{\partial a^L_k}{\partial z^L_j} = 0 zjLakL=0
则上一个方程可以简化为:
δ j L = ∂ C ∂ z j L = ∂ C ∂ a j L ∂ a j L ∂ z j L = ∂ C ∂ z j L ∂ σ ( z j L ) ∂ z j L = ∂ C ∂ a j L σ ′ ( z j L ) \begin{aligned} \delta^L_j = \frac{\partial C}{\partial z^L_j}\\ & =\frac{\partial C}{\partial a^L_j} \frac{\partial a^L_j}{\partial z^L_j} \\ & = \frac{\partial C}{\partial z^L_j} \frac{\partial \sigma(z^L_j)}{\partial z^L_j}\\ & =\frac{\partial C}{\partial a^L_j} \sigma{}'(z^L_j) \end{aligned} δjL=zjLC=ajLCzjLajL=zjLCzjLσ(zjL)=ajLCσ(zjL)
注意到这个公式的每个部分都不难计算得到, z j L 和 σ ′ ( z j L ) z^L_j\text{和} \sigma{}'(z^L_j) zjLσ(zjL)在计算神经网络输出的时候可以得到,左边的部分在确定了损失函数的形式之后也可以计算得到。
公式BP1是针对输出层上的某一个神经元而言的,为了方便反向传播计算,可以将其改写为矩阵形式:
δ L = ▽ a C ⊙ σ ′ ( z L ) ( B P 1 a ) \delta^L = \triangledown_a C \odot \sigma{}'(z^L) \quad \text(BP1a) δL=aCσ(zL)(BP1a)
对于这里的二次损失函数,有:
▽ a C = ▽ a ( 1 2 ∑ j ( y j − a j L ) 2 ) = a L − y \triangledown_a C = \triangledown_a(\frac{1}{2}\sum_j(y_j-a^L_j)^2) =a^L-y aC=a(21j(yjajL)2)=aLy
注意这里求导的过程只是对当前分量j求导,其余的分量就为0,于是有:
δ L = ( a L − y ) ⊙ σ ′ ( z L ) \delta^L = (a^L-y) \odot \sigma{}'(z^L) δL=(aLy)σ(zL)

方程二:用当前层的误差表示下一层的误差

δ l = ( ( w l + 1 ) T ) δ l + 1 ⊙ σ ′ ( z l ) ( B P 2 ) \delta^l = ((w^{l+1})^T)\delta^{l+1} \odot \sigma{}'(z^l) \quad \text(BP2) δl=((wl+1)T)δl+1σ(zl)(BP2)
这个方程虽然看上去比较复杂,但是每个部分都有很明确的解释。假设我们知道l+1层的error: δ l + 1 \delta^{l+1} δl+1,当同这一层的权重矩阵相乘的时候就类似于将这个error传到上一层,最后再利用Hadamard积得到l层的error。
证明:
应用链式法则:
δ j l = ∂ C ∂ z j l = ∑ k ∂ C ∂ z k l + 1 ∂ z k l + 1 ∂ z j l = ∑ k δ k l + 1 ∂ z k l + 1 ∂ z j l = ∑ k ∂ z k l + 1 ∂ z j l δ k l + 1 ( 3 ) \begin{aligned} \delta^l_j = \frac{\partial C}{\partial z^l_j} \\ & = \sum_k \frac{\partial C}{\partial z^{l+1}_k} \frac{\partial z^{l+1}_k}{\partial z^l_j} \\ & = \sum_k \delta^{l+1}_k \frac{\partial z^{l+1}_k}{\partial z^l_j} \\ & = \sum_k \frac{\partial z^{l+1}_k}{\partial z^l_j}\delta^{l+1}_k \end{aligned} \quad \text(3) δjl=zjlC=kzkl+1Czjlzkl+1=kδkl+1zjlzkl+1=kzjlzkl+1δkl+1(3)
这里最后一行交换了右边的两项,并用 δ k l + 1 \delta^{l+1}_k δkl+1的定义代入。为了对最后一行的第一项求值,注意:
z k l + 1 = ∑ j w k j l + 1 a j l + b k l + 1 = ∑ j w k j l + 1 σ ( z j l ) + b k l + 1 z^{l+1}_k = \sum_j w^{l+1}_{kj}a^l_j+b^{l+1}_k = \sum_j w^{l+1}_{kj} \sigma(z^l_j) +b^{l+1}_k zkl+1=jwkjl+1ajl+bkl+1=jwkjl+1σ(zjl)+bkl+1
对上式做微分,得到:
∂ z k l + 1 ∂ z j l = w k j l + 1 σ ′ ( z j l ) \frac{\partial z^{l+1}_k}{\partial z^l_j} = w^{l+1}_{kj} \sigma{}'(z^l_j) zjlzkl+1=wkjl+1σ(zjl)
将上式代入式(3),可以得到:
δ j l = ∑ k w k j l + 1 δ k l + 1 σ ′ ( z j l ) \delta^l_j = \sum_k w^{l+1}_{kj} \delta^{l+1}_k \sigma{}'(z^l_j) δjl=kwkjl+1δkl+1σ(zjl)
这正是以分量形式写的(BP2)


NOTE:
仔细观察上面公式中的 w k j l + 1 w^{l+1}_{kj} wkjl+1会发现其中的jk的顺序与w的原始定义中的顺序发生了对调,在原始定义中,第j个神经元的权重应是 w j k l + 1 w^{l+1}_{jk} wjkl+1这样的形式,这可以理解成矩阵形式中转置的原因


通过组合(BP1)(BP2),我们可以计算任何层的误差 δ l \delta^l δl。首先使用(BP1)计算 δ l \delta^l δl,然后应用方程(BP2)来计算 δ l − 1 \delta^{l-1} δl1,然后再次利用方程(BP2)来计算 δ l − 2 \delta^{l-2} δl2,如此一步一步地反向传播完整个网络。

方程三:代价函数与偏置的关系

∂ C ∂ b j l = δ j l \frac{\partial C}{\partial b^l_j} = \delta^l_j bjlC=δjl
这其实是,误差 δ j l \delta^l_j δjl和偏导数值 ∂ C / ∂ b j l \partial C / \partial b^l_j C/bjl完全一致。这是很好的性质,因为(BP1)(BP2)已经告诉我们如何计算 δ j l \delta^l_j δjl。所以就可以将(BP3)简记为:
∂ C ∂ b = δ \frac{\partial C}{\partial b} = \delta bC=δ
其中δ和偏置b都是针对同一个神经元。


NOTE:(BP3)的矩阵形式为:
∂ C ∂ b l = δ l \frac{\partial C}{\partial b^l} = \delta^l blC=δl


证明:
根据链式法则展开:
∂ C ∂ b j l = ∑ k ∂ C ∂ z k l ∂ z k l ∂ b j l = ∑ k δ k l ∂ ( ∑ j w k j l a k l − 1 + b k l ) ∂ b j l \begin{aligned} \frac{\partial C}{\partial b^l_j} = \sum_k \frac{\partial C}{\partial z^l_k} \frac{\partial z^l_k}{\partial b^l_j} \\ & = \sum_k \delta^l_k \frac{\partial (\sum_j w^l_{kj}a^{l-1}_k+b^l_k)}{\partial b^l_j} \end{aligned} bjlC=kzklCbjlzkl=kδklbjl(jwkjlakl1+bkl)
因为只有当 k = j k=j k=j的时候上式最后一项的右边部分才为1,而当 k ≠ j k\neq j k̸=j时,
∂ ( w k j l a k l − 1 + b k l ) ∂ b j l = 0 \frac{\partial (w^l_{kj}a^{l-1}_k+b^l_k)}{\partial b^l_j} = 0 bjl(wkjlakl1+bkl)=0
所以上式简化为
∂ C ∂ b j l = ∑ k ∂ C ∂ z k l ∂ z k l ∂ b j l = ∑ k δ k l ∂ ( ∑ j w k j l a k l − 1 + b k l ) ∂ b j l = δ j l ∗ 1 = δ j l \begin{aligned} \frac{\partial C}{\partial b^l_j} = \sum_k \frac{\partial C}{\partial z^l_k} \frac{\partial z^l_k}{\partial b^l_j} \\ & = \sum_k \delta^l_k \frac{\partial (\sum_j w^l_{kj}a^{l-1}_k+b^l_k)}{\partial b^l_j} \\ & = \delta^l_j * 1 \\ & = \delta^l_j \end{aligned} bjlC=kzklCbjlzkl=kδklbjl(jwkjlakl1+bkl)=δjl1=δjl
方程(BP3)得证!

方程四:代价函数与权重的关系

∂ C ∂ w j k l = a k l − 1 δ j l ( B P 4 ) \frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k\delta^l_j \quad \text(BP4) wjklC=akl1δjl(BP4)
这个方程说明,当我们要计算某两个神经元链接的权重对损失函数影响的时候,可以先计算上一层的 a k l − 1 a^{l-1}_k akl1和下一层的 δ j l \delta^l_j δjl。而这两个值我们根据先前的知识已经知道怎么计算了。这个方程也可以被写为:
∂ C ∂ w = a i n δ o u t ( 4 ) \frac{\partial C}{\partial w} = a_{in}\delta_{out} \quad \text(4) wC=ainδout(4)
从公式(4)可以看出来,当 a i n ≈ 0 a_{in} \approx 0 ain0的时候, ∂ C ∂ w \frac{\partial C}{\partial w} wC也会很小,那样这个权重的学习就会很慢,意味着在梯度下降的学习过程中,这个权重不会发生大的变化,换句话说就是,输出小的神经元的权重学习也慢。

证明:
额,这个证明和(BP3)的证明过程是一样的。
为了避免误解,这里的下表稍微改下,
∂ C ∂ b w j k l = ∑ a ∂ C ∂ z a l ∂ z k a ∂ b w j k l = ∑ a δ k l ∂ ( ∑ j w k j l a k l − 1 + b k l ) ∂ b j l \begin{aligned} \frac{\partial C}{\partial bw^l_{jk}} = \sum_a \frac{\partial C}{\partial z^l_a} \frac{\partial z^a_k}{\partial bw^l_{jk}} \\ & = \sum_a \delta^l_k \frac{\partial (\sum_j w^l_{kj}a^{l-1}_k+b^l_k)}{\partial b^l_j} \end{aligned} bwjklC=azalCbwjklzka=aδklbjl(jwkjlakl1+bkl)
很显然,
∂ b a l ∂ w j k l = 0 \frac{\partial b^l_a}{\partial w^l_{jk}} = 0 wjklbal=0
,而另一部分只有在 a = j , b = k a=j,b=k a=j,b=k时才不会0,此时就可以去掉求和符号得到:
∂ C ∂ w j k l = a k l − 1 δ j l \frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k\delta^l_j wjklC=akl1δjl

方程(BP4)得证!


NOTE:方程(BP4)的矩阵形式为:
∂ C ∂ w l = δ l ( a l − 1 ) T \frac{\partial C}{\partial w^l} = \delta^l (a^{l-1})^T wlC=δl(al1)T


总结

通过上面推导,可以发现在经过一次正向传播之后,可以通过输出层的误差、快速求解出C对每个wb的偏导,即∂C/∂w∂C/∂b,再对每个wb加上ΔwΔb,从而使得“球往下滚”,C、即Loss越来越小,神经网络在朝着我们期望的方向进行调整。

反向传播算法实现(神经网络训练流程)

基于上面的知识,我们现在可以总结出训练一个神经网络的全流程:
1.初始化神经网络.(1)设置输入层的输出为原始的输入x,即: a 1 = x a^1=x a1=x;
(2)对每个神经元的wb赋予随机值
2.对神经网络进行前向传播计算,得到输出层各个神经元的带权输入和激活输出: z l = w l a l − 1 + b l 和 a l = σ ( z l ) z^l=w^la^{l-1}+b^l \text{和}a^l=\sigma(z^l) zl=wlal1+blal=σ(zl)
3.求出输出层的误差 δ L = ▽ a C ⊙ σ ′ ( z L ) \delta^L = \triangledown_aC \odot \sigma{}'(z^L) δL=aCσ(zL)
4.通过反向传播算法,向后求出每一层(的每个神经元)的误差: δ l = ( ( w l + 1 ) T δ l + 1 ) ⊙ σ ′ ( z L ) \delta^l = ((w^{l+1})^T\delta^{l+1}) \odot \sigma{}'(z^L) δl=((wl+1)Tδl+1)σ(zL)
5.通过误差可以计算各个梯度: ∂ C ∂ w j k l = a k l − 1 和 ∂ C ∂ b j l = δ j l \frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k\text{和} \frac{\partial C}{\partial b^l_j} = \delta^l_j wjklC=akl1bjlC=δjl 得出每个神经元的∂C/∂w∂C/∂b,再乘上负的学习率(-η),就得到了ΔwΔb,将每个神经元的w和b更新为 w+Δwb+Δb

完成训练之后,一般情况下我们都能得到一个损失比较小的神经网络。

从算法的流程也可以明白为什么它被称为反向传播算法,因为我们是从最后一层的 δ L \delta^L δL开始反向计算前面的 δ l \delta^l δl的。因为损失函数是关于神经网络输出的函数,所以为了得到损失函数关于前面层的参数的梯度就需要不断的应用求导链式法则,一层层向前推导得到我们需要的关系的表达式。

参考链接

1. Neural Networks and Deep Learning,开源,有中文版,值得一看。



下面的是我的公众号二维码图片,欢迎关注。
图注:幼儿园的学霸

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值