MNIST手写识别系列——BP算法

本文通过BP算法实现MNIST手写数字识别,详细介绍BP算法原理及Python实现过程,包括权重与偏置初始化、前向传播、随机梯度下降等关键步骤。

MNIST手写识别系列——BP算法

这次想使用的是BP算法来进行求解。

关于BP算法的求解,我强烈推荐这篇博文曾梓华——一文详解神经网络 BP 算法原理及 Python 实现

详细推导可以仔细看博文,我这里只是列出几个重要的公式,下面代码讲解会用到。

在这里插入图片描述
这个公式是神经元输出值a的计算公式。其中w是权重,b是偏置,f函数采用的是sigmoid激活函数。

在这里插入图片描述
这个是均方误差,我们逆向反馈的目标就是让E函数的值尽可能小。接下来会对该值来求偏导。

在这里插入图片描述
权重和偏置的更新公式。

在这里插入图片描述
权重w的更新量(需要有一系列的推导)。

在这里插入图片描述
偏置b的更新量。

在这里插入图片描述
δ 值的更新式子。

在这里插入图片描述
sigmoid函数求导。

说完了数学的式子(具体的推导建议去看上面推荐的博客),接下来我们来看具体代码实现。

我使用的是anaconda+jupyter notebook的环境。用Keras框架,Keras可以很容易加载MNIST数据集。这里的代码参考:https://github.com/edvardHua/Articles 但是由于作者使用的是外部数据导入MNIST,并且使用的是python2.7,所以我进行了一些修改。使得更加符合自己以往的学习。

需要导入的包

import pdb
import random
import numpy as np
from keras.datasets import mnist

接下来的内容包含在一个类中。但为了方便说明,我把类的各个方法拆开,方便解释。

先来看看整体:

class Network(object):
    def __init__(self, sizes):
        # 作用:初始化w和b
        # param sizes: list 类型,存储每层神经网络的神经元数目
        # 例如sizes=[2,3,2]表示输出层有两个神经元,隐藏层有3个神经元,输出层有2个神经元
        
        # num_layers代表神经网络数目
        self.num_layers = len(sizes)
        self.sizes = sizes
        # 初始输入层,随机产生每层中的y个神经元的biase值(0,1)
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y,x) for x, y in zip(sizes[:-1],sizes[1:])]
        
        # biases和weights是共用的参数。只有一套
        
        # biases和weights其实是一个不规则的二维数组。如果输入是3层,那么数组有2行
        # 第一行30个维度,第二行10个维度
        
        # 这个weights的初始方法算是学到了。假设sizes是[a1,a2,a3,a4]
        # 那么输出是(a1,a2)(a2,a3) (a3,a4)
        
    def feedforward(self, x):
        # 作用:向前传输计算每个神经元的值
        # param a:输入值
        # return:计算后每个神经元的值
        
        a = x.reshape(x.shape[0],1) # 将数据由(x,)转换成(x,1)。否则矩阵计算会出错
        for b, w in zip(self.biases, self.weights):
            # 加权求和再加上biase
            a = sigmoid(np.dot(w,a)+b)
            # dot是矩阵乘法的意思
        return a
    
    def SGD(self, x_train, y_train, x_test, y_test, epochs, mini_batch_size, eta):
        # 作用:随机梯度下降
        # param training_data:输入的训练集
        # param epochs:迭代次数
        # param mini_batch_size:小样本数量
        # param eta:学习率
        # param test_data:测试数据集

        n_test = x_test.shape[0]
        n_train =  x_train.shape[0] # n_train是总的训练样本数目
        
        for j in range(epochs):
            # xrange和range类似,但是是返回一个迭代器,性能更优。但是在python3中,xrange和range是合并了的
            # xrange(start, stop[, step])
            
            # 搅乱训练集,让其排序顺序发生变化            
            temp = list(zip(x_train, y_train))
            random.shuffle(temp)
            x_train[:], y_train[:] = zip(*temp)
            
            # 按照小样本数量划分训练集          
            mini_batches_x = [
                x_train[k:k+mini_batch_size]
                for k in range(0, n_train, mini_batch_size)
            ]
            mini_batches_y = [
                y_train[k:k+mini_batch_size]
                for k in range(0, n_train, mini_batch_size)
            ]
            # print("mini_batches_x=", np.array(mini_batches_x).shape)
            # print("mini_batches_y= ", np.array(mini_batches_y).shape)
            for (mini_x,mini_y) in zip(mini_batches_x,mini_batches_y):
                # 根据每个小样本来更新w和b
                self.updata_mini_batch(mini_x,mini_y,eta)
            
            # 输出测试每轮结束后,神经网络的准确度
            if True:
                print("Epoch {0}: {1} / {2}".format(j, self.evaluate(x_test,y_test), n_test))
                #pdb.set_trace()
            else:
                # 若没有数据集的情况下,则表示该次训练结束?
                print("Epoch {0} complete".format(j))
                
    def updata_mini_batch(self, mini_x, mini_y, eta):
        # 作用:根据每个小样本来更新w和b
        # param mini_batch:一部分的样本
        # parma eta:学习率
        
        # 根据b和w的大小分别创建全为0的矩阵
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for (x, y) in zip(mini_x, mini_y):
            # 根据样本中的每一个输入x的其输出y,计算w和b的偏导数
            delta_nabla_b, delta_nabla_w = self.backprop(x,y)
            # 累加存储偏导值
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        
        # 根据累加的偏导值更新w和b,这里因为使用了小样本,所以eta要除以小样本的长度
        self.weights = [w-(eta/mini_x.shape[0])*nw
                       for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/mini_x.shape[0])*nb
                      for b, nb in zip(self.biases, nabla_b)]

    def backprop(self, x, y):
        # 作用:计算偏导数
        # param x:输入一个样本
        # param y:样本对应的值
        # return:b,w
        
        ##print("x= ", x.shape)
        ##print("y= ",y.shape)  
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        
        # 前向传输
        activation = x.reshape(x.shape[0], 1) # 将数据由(x,)转换成(x,1)。否则矩阵计算会出错
        # 存储每层的神经元的值的矩阵
        activations = [x]
        # 存储每个未经过sigmoid计算的神经元的值
        zs = []
        for b,w in zip(self.biases, self.weights):
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        
        # 求 δ 的值
        delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        # 乘于前一层的输出值
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        for l in range(2, self.num_layers):
            # 从倒数第l层开始更新
            # 下面这里利用 l+1 层的 δ 值来计算 l 的 δ 值
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].reshape(1, activations[-l-1].shape[0])) # 将数据由(x,)转换成(x,1)。否则矩阵计算会出错
        return (nabla_b, nabla_w)
    
    def evaluate(self, x_test, y_test):
        # 获得预测结果
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in zip(x_test, y_test)]
        # 返回正确识别的个数
        # print(test_results)
        res = sum(int(x == y) for (x, y) in test_results)
        print("This is the result: ", res)
        return res
    
    def cost_derivative(self, output_activations, y):    
        # 作用:二次损失函数
        # :param output_activations:
        # :param y: 一个数字,如6
        # :return: 
        
        e = np.zeros((10,1)) # 矩阵间的减法,需要对齐。在相应数字的位置置1,其余是0
        e[y] = 1.0

        return (output_activations-e)

其中:

def __init__(self, sizes):
        # 作用:初始化w和b
        # param sizes: list 类型,存储每层神经网络的神经元数目
        # 例如sizes=[2,3,2]表示输出层有两个神经元,隐藏层有3个神经元,输出层有2个神经元
        
        # num_layers代表神经网络数目
        self.num_layers = len(sizes)
        self.sizes = sizes
        # 初始输入层,随机产生每层中的y个神经元的biase值(0,1)
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        self.weights = [np.random.randn(y,x) for x, y in zip(sizes[:-1],sizes[1:])]
        
        # biases和weights是共用的参数。只有一套
        
        # biases和weights其实是一个不规则的二维数组。如果输入是3层,那么数组有2行
        # 第一行30个维度,第二行10个维度
        
        # 这个weights的初始方法算是学到了。假设sizes是[a1,a2,a3,a4]
        # 那么输出是(a1,a2)(a2,a3) (a3,a4)

这是初始化函数,对我们输入的神经网络格式,我们初始化相应的数据结构。主要是对权重w和偏置b进行初始化。

def feedforward(self, x):
        # 作用:向前传输计算每个神经元的值
        # param a:输入值
        # return:计算后每个神经元的值
        
        a = x.reshape(x.shape[0],1) # 将数据由(x,)转换成(x,1)。否则矩阵计算会出错
        for b, w in zip(self.biases, self.weights):
            # 加权求和再加上biase
            a = sigmoid(np.dot(w,a)+b)
            # dot是矩阵乘法的意思
        return a

这部分是向前输入的步骤,对应以下的公式:

在这里插入图片描述

def SGD(self, x_train, y_train, x_test, y_test, epochs, mini_batch_size, eta):
        # 作用:随机梯度下降
        # param training_data:输入的训练集
        # param epochs:迭代次数
        # param mini_batch_size:小样本数量
        # param eta:学习率
        # param test_data:测试数据集

        n_test = x_test.shape[0]
        n_train =  x_train.shape[0] # n_train是总的训练样本数目
        
        for j in range(epochs):
            # xrange和range类似,但是是返回一个迭代器,性能更优。但是在python3中,xrange和range是合并了的
            # xrange(start, stop[, step])
            
            # 搅乱训练集,让其排序顺序发生变化            
            temp = list(zip(x_train, y_train))
            random.shuffle(temp)
            x_train[:], y_train[:] = zip(*temp)
            
            # 按照小样本数量划分训练集          
            mini_batches_x = [
                x_train[k:k+mini_batch_size]
                for k in range(0, n_train, mini_batch_size)
            ]
            mini_batches_y = [
                y_train[k:k+mini_batch_size]
                for k in range(0, n_train, mini_batch_size)
            ]
            # print("mini_batches_x=", np.array(mini_batches_x).shape)
            # print("mini_batches_y= ", np.array(mini_batches_y).shape)
            for (mini_x,mini_y) in zip(mini_batches_x,mini_batches_y):
                # 根据每个小样本来更新w和b
                self.updata_mini_batch(mini_x,mini_y,eta)
            
            # 输出测试每轮结束后,神经网络的准确度
            if True:
                print("Epoch {0}: {1} / {2}".format(j, self.evaluate(x_test,y_test), n_test))
                #pdb.set_trace()
            else:
                # 若没有数据集的情况下,则表示该次训练结束?
                print("Epoch {0} complete".format(j))

这部分其实是对数据集进行“拆分”,拆分成一小份一小份,我们每次根据这一份份数据来对权重w和偏置b进行更新。

 def updata_mini_batch(self, mini_x, mini_y, eta):
        # 作用:根据每个小样本来更新w和b
        # param mini_batch:一部分的样本
        # parma eta:学习率
        
        # 根据b和w的大小分别创建全为0的矩阵
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        for (x, y) in zip(mini_x, mini_y):
            # 根据样本中的每一个输入x的其输出y,计算w和b的偏导数
            delta_nabla_b, delta_nabla_w = self.backprop(x,y)
            # 累加存储偏导值
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        
        # 根据累加的偏导值更新w和b,这里因为使用了小样本,所以eta要除以小样本的长度
        self.weights = [w-(eta/mini_x.shape[0])*nw
                       for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b-(eta/mini_x.shape[0])*nb
                      for b, nb in zip(self.biases, nabla_b)]

这部分的内容就是用小样本来更新参数。对应下面的式子:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

def backprop(self, x, y):
        # 作用:计算偏导数
        # param x:输入一个样本
        # param y:样本对应的值
        # return:b,w
        
        ##print("x= ", x.shape)
        ##print("y= ",y.shape)  
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        
        # 前向传输
        activation = x.reshape(x.shape[0], 1) # 将数据由(x,)转换成(x,1)。否则矩阵计算会出错
        # 存储每层的神经元的值的矩阵
        activations = [x]
        # 存储每个未经过sigmoid计算的神经元的值
        zs = []
        for b,w in zip(self.biases, self.weights):
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        
        # 求 δ 的值
        delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        # 乘于前一层的输出值
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        for l in range(2, self.num_layers):
            # 从倒数第l层开始更新
            # 下面这里利用 l+1 层的 δ 值来计算 l 的 δ 值
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].reshape(1, activations[-l-1].shape[0])) # 将数据由(x,)转换成(x,1)。否则矩阵计算会出错
        return (nabla_b, nabla_w)

这部分是计算偏导,除了上面两个式子,还有下面的式子:
在这里插入图片描述

 def evaluate(self, x_test, y_test):
        # 获得预测结果
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in zip(x_test, y_test)]
        # 返回正确识别的个数
        # print(test_results)
        res = sum(int(x == y) for (x, y) in test_results)
        print("This is the result: ", res)
        return res

 def cost_derivative(self, output_activations, y):    
        # 作用:二次损失函数
        # :param output_activations:
        # :param y: 一个数字,如6
        # :return: 
        
        e = np.zeros((10,1)) # 矩阵间的减法,需要对齐。在相应数字的位置置1,其余是0
        e[y] = 1.0

        return (output_activations-e)

这两个函数就比较简单了,分别是计算预测结果和二次损失函数。

到这里类的方法就结束了,下面是通用的方法。

def sigmoid(z): 
    #作用:求 sigmoid 函数的值
    # param z:
    # return:
    
    return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
    # 作用:求 sigmoid 函数的导数
    # :param z:
    # :return:
    
    return sigmoid(z)*(1-sigmoid(z))

这是关于sigmoid函数的实现和其导数。

# %pdb on
if __name__  ==   "__main__":
    
    (x_train, y_train), (x_test, y_test) = mnist.load_data() # 加载数据集,原本的数据格式是60000*28*28。三维的形式
    x_train = x_train.reshape(x_train.shape[0], x_train.shape[1]*x_train.shape[2])
    x_test = x_test.reshape(x_test.shape[0], x_test.shape[1]*x_test.shape[2])
    y_train = y_train.reshape(y_train.shape[0],1)
    y_test = y_test.reshape(y_test.shape[0],1)
    x_train = x_train.astype('float32') # 将数据类型转为float32,因为很多时候我们用numpy从文本文件读取数据作为numpy的数组,默认的dtype是float64
    x_test = x_test.astype('float32')
    x_train /= 255
    x_test /= 255
    print(x_train.shape) # (60000, 784)
    print(y_train.shape) # (60000, 1)
    print(x_test.shape) # (10000, 784)
    print(y_test.shape) # (10000, 1)
    print("load data finish")
    
    pdb.set_trace() # 设置中断,输入c继续运行
    
    net = Network([784, 30, 10])
    print("net.w1= ", np.array(net.biases[0]).shape)
    print("net.w2= ", np.array(net.biases[1]).shape)
    print("net.w1= ", np.array(net.weights[0]).shape)
    print("net.w2= ", np.array(net.weights[1]).shape)
    net.SGD(x_train, y_train, x_test, y_test, 30, 10, 3.0)

这部分是主函数。主要是加载数据集,并进行一定的转换。然后声明网络,并进行训练和分类。结果大致如下:

(60000, 784)
(60000, 1)
(10000, 784)
(10000, 1)
load data finish
> <ipython-input-10-84adde9c4133>(23)<module>()
-> net = Network([784, 30, 10])
(Pdb) c
net.w1=  (30, 1)
net.w2=  (10, 1)
net.w1=  (30, 784)
net.w2=  (10, 30)
This is the result:  7983
Epoch 0: 7983 / 10000
This is the result:  9144
Epoch 1: 9144 / 10000
This is the result:  9276
Epoch 2: 9276 / 10000
This is the result:  9296
Epoch 3: 9296 / 10000
This is the result:  9285
Epoch 4: 9285 / 10000
This is the result:  9300
Epoch 5: 9300 / 10000
This is the result:  9296
Epoch 6: 9296 / 10000
This is the result:  9303
Epoch 7: 9303 / 10000
This is the result:  9289
Epoch 8: 9289 / 10000
This is the result:  9291
Epoch 9: 9291 / 10000
This is the result:  9278
Epoch 10: 9278 / 10000
This is the result:  9289
Epoch 11: 9289 / 10000
This is the result:  9288
Epoch 12: 9288 / 10000
This is the result:  9278
Epoch 13: 9278 / 10000
This is the result:  9273
Epoch 14: 9273 / 10000
This is the result:  9275
Epoch 15: 9275 / 10000
This is the result:  9277
Epoch 16: 9277 / 10000
This is the result:  9264
Epoch 17: 9264 / 10000
This is the result:  9267
Epoch 18: 9267 / 10000
This is the result:  9264
Epoch 19: 9264 / 10000
This is the result:  9267
Epoch 20: 9267 / 10000
This is the result:  9260
Epoch 21: 9260 / 10000
This is the result:  9254
Epoch 22: 9254 / 10000
This is the result:  9253
Epoch 23: 9253 / 10000
This is the result:  9258
Epoch 24: 9258 / 10000
This is the result:  9262
Epoch 25: 9262 / 10000
This is the result:  9260
Epoch 26: 9260 / 10000
This is the result:  9260
Epoch 27: 9260 / 10000
This is the result:  9263
Epoch 28: 9263 / 10000
This is the result:  9257
Epoch 29: 9257 / 10000

小结:以上是使用BP算法实现MNIST数据集的代码啦,具体的可以参考我的github。其中需要比较注意的矩阵的相加和相乘。区别(x,1) 和 (x,)之间的区别,否则会导致矩阵运算错误。并且BP算法的思想比较简单,建议大家弄懂推导的过程。

如有不足之处,还望大家多多指教。

Github代码

### MNIST手写数字识别使用BP神经网络算法的实现 以下是基于BP神经网络算法实现MNIST手写数字识别的一个完整代码示例。该代码通过Python编程语言以及NumPy库完成,展示了如何构建一个多层感知器模型并对其进行训练。 #### 数据预处理 为了使数据适合于BP神经网络的输入,通常需要对MNIST数据进行标准化和归一化操作。每张图像被展平成一个长度为784的一维数组(28×28像素)。此外,标签也需要转换为独热编码形式以便用于分类任务[^1]。 ```python import numpy as np from keras.datasets import mnist from keras.utils import to_categorical # 加载MNIST数据 (x_train, y_train), (x_test, y_test) = mnist.load_data() # 归一化输入数据至[0, 1] x_train = x_train.reshape((x_train.shape[0], 784)).astype('float32') / 255 x_test = x_test.reshape((x_test.shape[0], 784)).astype('float32') / 255 # 将标签转换为独热编码 y_train = to_categorical(y_train, 10) y_test = to_categorical(y_test, 10) print("Data preprocessing completed.") ``` #### 构建BP神经网络模型 下面是一个简单的三层BP神经网络结构定义:一层输入层、一层隐藏层以及一层输出层。这里采用了sigmoid函数作为激活函数,并实现了前向传播与反向传播的过程[^3]。 ```python class NeuralNetwork: def __init__(self, input_size=784, hidden_size=100, output_size=10): self.weights_input_hidden = np.random.randn(input_size, hidden_size) * 0.01 self.bias_hidden = np.zeros(hidden_size) self.weights_hidden_output = np.random.randn(hidden_size, output_size) * 0.01 self.bias_output = np.zeros(output_size) @staticmethod def sigmoid(z): return 1 / (1 + np.exp(-z)) @staticmethod def derivative_sigmoid(z): return z * (1 - z) def forward(self, X): self.hidden_layer_input = np.dot(X, self.weights_input_hidden) + self.bias_hidden self.hidden_layer_output = self.sigmoid(self.hidden_layer_input) self.output_layer_input = np.dot(self.hidden_layer_output, self.weights_hidden_output) + self.bias_output self.predicted_output = self.sigmoid(self.output_layer_input) return self.predicted_output def backward(self, X, Y, learning_rate=0.1): error_output = Y - self.predicted_output delta_output = error_output * self.derivative_sigmoid(self.predicted_output) error_hidden = delta_output.dot(self.weights_hidden_output.T) delta_hidden = error_hidden * self.derivative_sigmoid(self.hidden_layer_output) # 更新权重和偏置 self.weights_hidden_output += self.hidden_layer_output.T.dot(delta_output) * learning_rate self.bias_output += np.sum(delta_output, axis=0) * learning_rate self.weights_input_hidden += X.T.dot(delta_hidden) * learning_rate self.bias_hidden += np.sum(delta_hidden, axis=0) * learning_rate ``` #### 训练过程 在这一部分中,我们将利用之前准备好的数据来训练我们的BP神经网络模型。每次迭代都会计算损失值以评估当前模型的表现情况。 ```python def train(model, epochs=10, batch_size=64, learning_rate=0.1): num_samples = x_train.shape[0] losses = [] for epoch in range(epochs): indices = np.arange(num_samples) np.random.shuffle(indices) x_train_shuffled = x_train[indices] y_train_shuffled = y_train[indices] for i in range(0, num_samples, batch_size): X_batch = x_train_shuffled[i:i+batch_size] Y_batch = y_train_shuffled[i:i+batch_size] predicted_output = model.forward(X_batch) loss = np.mean(np.square(Y_batch - predicted_output)) # MSE Loss losses.append(loss) model.backward(X_batch, Y_batch, learning_rate) accuracy = evaluate(model, x_test, y_test) print(f"Epoch {epoch+1}/{epochs}, Loss: {loss:.4f}, Test Accuracy: {accuracy*100:.2f}%") def evaluate(model, X, Y_true): predictions = model.forward(X) correct_predictions = np.argmax(predictions, axis=1) == np.argmax(Y_true, axis=1) accuracy = np.mean(correct_predictions) return accuracy model = NeuralNetwork() train(model=model, epochs=10, batch_size=64, learning_rate=0.1) test_accuracy = evaluate(model, x_test, y_test) print(f"Final test set accuracy: {test_accuracy*100:.2f}%") ``` 以上即为完整的BP神经网络应用于MNIST手写数字识别的例子。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值