手写数字识别:CNN-AlexNet

  1. 作业要求:
    手写体数字识别
    数据集:MNIST
    数据集下载网址:http://yann.lecun.com/exdb/mnist/
    参考网址: https://zhuanlan.zhihu.com/p/101262336
    https://blog.csdn.net/panrenlong/article/details/81736754
    深度神经网络方法(任选):CNN、RNN、GNN、LSTM、MM、GCN……
    典型结构(任选):AlexNet、VGG、GoogleNet……
    目标:只有一个,让网络跑起来

  2. 数据理解与获取:
    MNIST数据集是由60000个训练样本和10000个测试样本组成,每个样本都是一张28 * 28像素的灰度手写数字图片。
    获取方法一:
    官方网站下载:http://yann.lecun.com/exdb/mnist/
    一共4个文件,训练集、训练集标签、测试集、测试集标签
    在这里插入图片描述
    获取方法二:
    使用TensorFlow中input_data.py脚本来读取数据及标签,使用这种方式时,可以不用事先下载好数据集,会自动下载并存放到指定的位置。

  3. 实现思路
    使用tensorflow编写卷积神经网络(CNN)代码进行MNIST数据集的手写数字识别,整体采用Alexnet网络架构搭建神经网络,由于MNIST影像大小为28 x 28,所以网络结构做了相应调整,从而适应数据。MNIST训练集数据用于模型训练,计算训练集准确率,训练好模型将其保存在model文件夹,使用MNIST测试集进行模型测试,计算测试集准确率,总体测试准确率在96%以上。

  4. 网络架构介绍——AlexNet
    Alexnet架构本质上与Lenet架构相同,Alexnet将CNN卷积神经网络的基本原理应用到更深,更宽的网络中。
    对比Lenet架构:
    ① 更大的池化窗口,使用最大池化层
    ② 更大的卷积核窗口和步长
    ③ 新加了三层卷积层,更多的输出通道
    Alexnet架构主要使用到的新技术如下:
    (1) 使用ReLu作为CNN的激活函数,标准的CNN模型采用tanh或sigmoid激活函数,但是在进行梯度下降时,神经元梯度会趋于饱和,参数更新速度慢,在进行梯度下降计算时,Relu函数的训练速度更快,错误率更低。
    (2) 训练时使用Dropout随机忽略一部分神经元,以避免模型过拟合,以0.5的概率对每个隐层神经元的输出设为0,那些“失活的”的神经元不再进行前向传播并且不参与反向传播。dropout在前两个全连接层中使用,非常有效避免了过拟合。
    (3) 在CNN中采用重叠的最大池化,此前CNN中普遍使用平均池化,Alexnet网络架构全部采用最大池化,避免平均池化的模糊化效果。并且Alexnet中提出让步长比池化核的尺寸小,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。
    (4) 提出了LRU层,对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。
    AlexNet网络架构示意图:
    在这里插入图片描述
    AlexNet网络架构各层处理细节示意图:
    在这里插入图片描述
    Alexnet网络架构层级处理流程图:
    在这里插入图片描述

  5. 运行结果截图
    测试集测试结果,输出测试准确率,整体在96%以上:
    在这里插入图片描述
    在这里插入图片描述

  6. 核心代码和说明
    (1) 定义的超参数
    在这里插入图片描述
    (2) 模型结构
     input-输入数据:MNIST
    数据集被分为两部分:60000行的训练数据集(mnist.train)和10000行的测试数据集(mnist.test),每张图片是28281,输入数字是1784的数据,经过reshape后,维度格式为[28,28,1]
     第1层卷积,卷积大小变化为28
    28—>2828
     池化大小变化为28
    28—>1414
     归一化大小变化为 14
    14—>1414
     第2层卷积,卷积大小变化为14
    14—>1414
     池化大小变化为14
    14—>77
     归一化大小变化为 7
    7—>77
     第3层卷积,卷积大小变化为 7
    7—> 77
     第4层卷积,卷积大小变化为 7
    7—> 77
     第5层卷积,卷积大小变化为 7
    7—> 77
     池化大小变为 7
    7—>44
     归一化大小为7
    7—>77
     第1层全连接层 ‘wd1’: shape为4
    4*256
     dropout丢弃层
     第2层全连接层
     dropout丢弃层
     output输出层

# 数据获取与各参数定义与设置
# 获取mnist数据集,进行one-hot编码,其为一位有效编码
mnist = input_data.read_data_sets("mnist_sets", one_hot=True)
print("数据下载完成!")
# 设置网络超参数
# 学习率
learning_rate = 0.0001
# 迭代次数,每个样本迭代20次
epochs = 20
# 每进行一次迭代选择128个样本
batch_size = 128
# 模型训练的步数
display_step = 10
# 设置网络参数,图片大小为28*28像素,即784,输入维度
n_input = 784
# 类别有0-9十种,输出维度
n_classes = 10
# 神经元丢弃概率,以0.5的概率对每个隐层神经元的输出设为0
dropout = 0.5

# 占位符,用placeholder先占地方,样本个数不确定为None
x = tf.placeholder(tf.float32, [None, n_input])
y = tf.placeholder(tf.float32, [None, n_classes])

weights = {
    # wc1 卷积核11*11*1*96,输入为28*28*1,所以in_channel=1,96代表卷积核个数,表示有96个11*11*1的卷积核,96即产生特征图的个数
    'wc1': weight_var('wc1', [11, 11, 1, 96]),
    # wc2 卷积核5*5*96*256,输入为14*14*96,所以in_channel=96,256代表卷积核个数,表示有256个14*14*96的卷积核,256即产生特征图的个数
    'wc2': weight_var('wc2', [5, 5, 96, 256]),
    # wc3 卷积核3*3*256*384, 输入为7*7*256,所以in_channels=256,384代表卷积核个数,表示有384个7*7*256的卷积核,384即产生特征图的个数
    'wc3': weight_var('wc3', [3, 3, 256, 384]),
    # wc4 卷积核3*3*384*384, 输入为4*4*384,所以in_channels=384,384代表卷积核个数,表示有384个3*3*384的卷积核,384即产生特征图的个数
    'wc4': weight_var('wc4', [3, 3, 384, 384]),
    # wc5 卷积核3*3*384*256, 输入为4*4*384,所以in_channels=384,256代表卷积核个数,表示有256个3*3*384的卷积核,256即产生特征图的个数
    'wc5': weight_var('wc5', [3, 3, 384, 256]),
    # wd1 2*2*256*4096, 输入为2*2*256,所以将其展平则为1*(2*2*256),4096表示全连接层神经元的个数
    'wd1': weight_var('wd1', [4 * 4 * 256, 4096]),
    # wd2 4096*4096, 输入为[2*2*256,4096],4096表示有4096个神经元
    'wd2': weight_var('wd2', [4096, 4096]),
    # out 4096,10, 输入为[4096,4096],10表示有10类—> 0-9
    'out_w': weight_var('out_w', [4096, 10])
}
# 设置偏移量
biases = {
    # 卷积层
    'bc1': bias_var('bc1', [96]),
    'bc2': bias_var('bc2', [256]),
    'bc3': bias_var('bc3', [384]),
    'bc4': bias_var('bc4', [384]),
    'bc5': bias_var('bc5', [256]),
    # 全连接层
    'bd1': bias_var('bd1', [4096]),
    'bd2': bias_var('bd2', [4096]),
    # 输出层
    'out_b': bias_var('out_b', [n_classes])
}

# 定义卷积,池化,归一化,参数设置操作
# 定义卷积过程,参数为:名字,输入,卷积核,偏移量,步长
# padding=‘SAME’表示在做卷积前需要对输入图像进行0填充使卷积后的图像与输入图像有相同的维度
# x为一个四维的张量,四个维度分别表示一批训练的图片数量,图片高度和宽度及图片的通道数
def conv2d(name, x, W, b, strides=1, padding='SAME'):
    # strides = [1, strides, strides, 1]表示每个维度做卷积的步幅
    x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding=padding)
    x = tf.nn.bias_add(x, b)
    # Alexnet架构使用relu作为激活函数
    return tf.nn.relu(x, name=name)

# 定义池化过程 3*3
def maxpool2d(name, x, k=3, s=2, padding='SAME'):
    return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, s, s, 1], padding=padding, name=name)

# 局部响应归一化LRU,使得其中响应比较大的值相对更大,并抑制其他反馈较小的神经单元,提高模型泛化能力
def norm(name, l_input, lsize=5):
    return tf.nn.lrn(l_input, lsize, bias=1.0, alpha=0.0001, beta=0.75, name=name)

# 设置网络参数,卷积核参数与偏移量
def weight_var(name, shape):
    return tf.get_variable(name=name, shape=shape, initializer=tf.contrib.layers.xavier_initializer())

def bias_var(name, shape):
    return tf.get_variable(name=name, shape=shape, initializer=tf.constant_initializer(0))

# 构建神经网络结构,定义Alexnet网络结构
def alexnet(x, weights, biases, dropout):
    # 输入数字是1*784的数据,将其reshape成28*28的影像
    x = tf.reshape(x, shape=[-1, 28, 28, 1])

    # 1 conv 第1层卷积,卷积大小变化为28*28->28*28
    conv1 = conv2d('conv1', x, weights['wc1'], biases['bc1'], padding='SAME')
    # 池化大小变化为28*28->14*14
    pool1 = maxpool2d('pool1', conv1, k=3, s=2, padding='SAME')
    # 归一化大小变化为 14*14->14*14
    norm1 = norm('norm1', pool1, lsize=5)

    # 2 conv 第2层卷积,卷积大小变化为14*14->14*14
    conv2 = conv2d('conv2', norm1, weights['wc2'], biases['bc2'], padding='SAME')
    # 池化大小变化为14*14->7*7
    pool2 = maxpool2d('pool2', conv2, k=3, s=2, padding='SAME')
    # 归一化大小变化为 7*7->7*7
    norm2 = norm('norm2', pool2, lsize=5)

    # 3 conv 第3层卷积,卷积大小变化为 7*7—> 7*7
    conv3 = conv2d('conv3', norm2, weights['wc3'], biases['bc3'], padding='SAME')

    # 4 conv 第4层卷积,卷积大小变化为 7*7—> 7*7
    conv4 = conv2d('conv4', conv3, weights['wc4'], biases['bc4'], padding='SAME')

    # 5 conv 第5层卷积,卷积大小变化为 7*7—> 7*7
    conv5 = conv2d('conv5', conv4, weights['wc5'], biases['bc5'], padding='SAME')
    # 池化大小变为 7*7->4*4
    pool5 = maxpool2d('pool5', conv5, k=3, s=2, padding='SAME')
    # 归一化大小为7*7->7*7
    norm5 = norm('norm5', pool5, lsize=5)

    # 1 fc 第1层全连接层 'wd1': 形状大小为4*4*256
    fc1 = tf.reshape(norm5, [-1, weights['wd1'].get_shape().as_list()[0]])
    fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
    fc1 = tf.nn.relu(fc1)

    # dropout丢弃层
    fc1 = tf.nn.dropout(fc1, dropout)

    # 2 fc 第2全连接层
    # fc2=tf.reshape(fc1,[-1,weights['wd2'].get_shape().as_list()[0]])
    fc2 = tf.add(tf.matmul(fc1, weights['wd2']), biases['bd2'])
    fc2 = tf.nn.relu(fc2)

    # dropout丢弃层
    fc2 = tf.nn.dropout(fc2, dropout)

    # output输出层
    out = tf.add(tf.matmul(fc2, weights['out_w']), biases['out_b'])
    return out

# 定义模型,损失函数和优化器
# 前向传播的预测值
pred = alexnet(x, weights, biases, dropout)
# 交叉熵损失函数,参数分别为预测值pred和实际label值y,reduce_mean为求平均loss
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
# 梯度下降优化器
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
# tf.equal()对比预测值的索引和实际label的索引是否一样,一样返回True,不一样返回False
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
# 将pred即True或False转换为1或0,并对所有的判断结果求均值
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

# 训练和评估模型,获得训练集的训练结果与测试集的测试准确率
# 初始化变量
init = tf.global_variables_initializer()
# 定义一个Session
with tf.Session() as sess:
    # 在sess里run一下初始化操作
    sess.run(init)
    step = 1

    # epoch 逐代循环
    for epoch in range(epochs + 1):
        # 迭代
        for _ in range(mnist.train.num_examples // batch_size):
            step += 1
            # get x,y  逐个batch的去取数据
            batch_x, batch_y = mnist.train.next_batch(batch_size)
            # 获取批数据
            sess.run(optimizer, feed_dict={x: batch_x, y: batch_y})
            # 展示损失和精确度  每display_step显示一次
            if step % display_step == 0:
				 # 计算损失值和精确度,并输出
                loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x, y: batch_y})
                print("Epoch " + str(epoch) + ", Minibatch Loss=" + \
                      "{:.6f}".format(loss) + ", Training Accuracy= " + \
                      "{:.5f}".format(acc))
    print("Optimizer Finished!")
    # 存储训练好的模型
    saver.save(sess, model + "/model.ckpt", global_step=epoch)

    # 测试准确率并对应输出
    for _ in range(mnist.test.num_examples // batch_size):
        batch_x, batch_y = mnist.test.next_batch(batch_size)
        print("Testing Accuracy:", sess.run(accuracy, feed_dict={x: batch_x, y: batch_y}))

  1. 完整代码
# 导入tensorflow包
import tensorflow as tf
import os
# 导入数据
from tensorflow.examples.tutorials.mnist import input_data
# 获取mnist数据集,进行one-hot编码,其为一位有效编码
mnist = input_data.read_data_sets("mnist_sets", one_hot=True)
print("数据下载完成!")
# 设置网络超参数
# 学习率
learning_rate = 0.0001
# 迭代次数,每个样本迭代20次
epochs = 20
# 每进行一次迭代选择128个样本
batch_size = 128
# 模型训练的步数
display_step = 10
# 设置网络参数,图片大小为28*28像素,即784,输入维度
n_input = 784
# 类别有0-9十种,输出维度
n_classes = 10
# 神经元丢弃概率,以0.5的概率对每个隐层神经元的输出设为0
dropout = 0.5

# 占位符,用placeholder先占地方,样本个数不确定为None
x = tf.placeholder(tf.float32, [None, n_input])
y = tf.placeholder(tf.float32, [None, n_classes])

# 定义卷积,池化,归一化,参数设置操作
# 定义卷积过程,参数为:名字,输入,卷积核,偏移量,步长
# padding=‘SAME’表示在做卷积前需要对输入图像进行0填充使卷积后的图像与输入图像有相同的维度
# x为一个四维的张量,四个维度分别表示一批训练的图片数量,图片高度和宽度及图片的通道数
def conv2d(name, x, W, b, strides=1, padding='SAME'):
    # strides = [1, strides, strides, 1]表示每个维度做卷积的步幅
    x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding=padding)
    x = tf.nn.bias_add(x, b)
    # Alexnet架构使用relu作为激活函数
    return tf.nn.relu(x, name=name)

# 定义池化过程 3*3
def maxpool2d(name, x, k=3, s=2, padding='SAME'):
    return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, s, s, 1], padding=padding, name=name)

# 局部响应归一化LRU,使得其中响应比较大的值相对更大,并抑制其他反馈较小的神经单元,提高模型泛化能力
def norm(name, l_input, lsize=5):
    return tf.nn.lrn(l_input, lsize, bias=1.0, alpha=0.0001, beta=0.75, name=name)

# 设置网络参数,卷积核参数与偏移量
def weight_var(name, shape):
    return tf.get_variable(name=name, shape=shape, initializer=tf.contrib.layers.xavier_initializer())

def bias_var(name, shape):
    return tf.get_variable(name=name, shape=shape, initializer=tf.constant_initializer(0))

weights = {
    # wc1 卷积核11*11*1*96,输入为28*28*1,所以in_channel=1,96代表卷积核个数,表示有96个11*11*1的卷积核,96即产生特征图的个数
    'wc1': weight_var('wc1', [11, 11, 1, 96]),
    # wc2 卷积核5*5*96*256,输入为14*14*96,所以in_channel=96,256代表卷积核个数,表示有256个14*14*96的卷积核,256即产生特征图的个数
    'wc2': weight_var('wc2', [5, 5, 96, 256]),
    # wc3 卷积核3*3*256*384, 输入为7*7*256,所以in_channels=256,384代表卷积核个数,表示有384个7*7*256的卷积核,384即产生特征图的个数
    'wc3': weight_var('wc3', [3, 3, 256, 384]),
    # wc4 卷积核3*3*384*384, 输入为4*4*384,所以in_channels=384,384代表卷积核个数,表示有384个3*3*384的卷积核,384即产生特征图的个数
    'wc4': weight_var('wc4', [3, 3, 384, 384]),
    # wc5 卷积核3*3*384*256, 输入为4*4*384,所以in_channels=384,256代表卷积核个数,表示有256个3*3*384的卷积核,256即产生特征图的个数
    'wc5': weight_var('wc5', [3, 3, 384, 256]),
    # wd1 2*2*256*4096, 输入为2*2*256,所以将其展平则为1*(2*2*256),4096表示全连接层神经元的个数
    'wd1': weight_var('wd1', [4 * 4 * 256, 4096]),
    # wd2 4096*4096, 输入为[2*2*256,4096],4096表示有4096个神经元
    'wd2': weight_var('wd2', [4096, 4096]),
    # out 4096,10, 输入为[4096,4096],10表示有10类————> 0-9
    'out_w': weight_var('out_w', [4096, 10])
}
# 设置偏移量
biases = {
    # 卷积层
    'bc1': bias_var('bc1', [96]),
    'bc2': bias_var('bc2', [256]),
    'bc3': bias_var('bc3', [384]),
    'bc4': bias_var('bc4', [384]),
    'bc5': bias_var('bc5', [256]),
    # 全连接层
    'bd1': bias_var('bd1', [4096]),
    'bd2': bias_var('bd2', [4096]),
    # 输出层
    'out_b': bias_var('out_b', [n_classes])
}

# 构建神经网络结构,定义Alexnet网络结构
def alexnet(x, weights, biases, dropout):
    # 输入数字是1*784的数据,将其reshape成28*28的影像
    x = tf.reshape(x, shape=[-1, 28, 28, 1])

    # 1 conv 第1层卷积
    # 卷积大小变化为28*28->28*28
    conv1 = conv2d('conv1', x, weights['wc1'], biases['bc1'], padding='SAME')
    # 池化大小变化为28*28->14*14
    pool1 = maxpool2d('pool1', conv1, k=3, s=2, padding='SAME')
    # 归一化大小变化为 14*14->14*14
    norm1 = norm('norm1', pool1, lsize=5)

    # 2 conv 第2层卷积
    # 卷积大小变化为14*14->14*14
    conv2 = conv2d('conv2', norm1, weights['wc2'], biases['bc2'], padding='SAME')
    # 池化大小变化为14*14->7*7
    pool2 = maxpool2d('pool2', conv2, k=3, s=2, padding='SAME')
    # 归一化大小变化为 7*7->7*7
    norm2 = norm('norm2', pool2, lsize=5)

    # 3 conv 第3层卷积
    # 卷积大小变化为 7*7—> 7*7
    conv3 = conv2d('conv3', norm2, weights['wc3'], biases['bc3'], padding='SAME')

    # 4 conv 第4层卷积
    # 卷积大小变化为 7*7—> 7*7
    conv4 = conv2d('conv4', conv3, weights['wc4'], biases['bc4'], padding='SAME')

    # 5 conv 第5层卷积
    # 卷积大小变化为 7*7—> 7*7
    conv5 = conv2d('conv5', conv4, weights['wc5'], biases['bc5'], padding='SAME')
    # 池化大小变为 7*7->4*4
    pool5 = maxpool2d('pool5', conv5, k=3, s=2, padding='SAME')
    # 归一化大小为7*7->7*7
    norm5 = norm('norm5', pool5, lsize=5)

    # 1 fc 第1层全连接层 'wd1': shape为4*4*256
    fc1 = tf.reshape(norm5, [-1, weights['wd1'].get_shape().as_list()[0]])
    fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
    fc1 = tf.nn.relu(fc1)

    # dropout丢弃层
    fc1 = tf.nn.dropout(fc1, dropout)

    # 2 fc 第2全连接层
    # fc2=tf.reshape(fc1,[-1,weights['wd2'].get_shape().as_list()[0]])
    fc2 = tf.add(tf.matmul(fc1, weights['wd2']), biases['bd2'])
    fc2 = tf.nn.relu(fc2)

    # dropout丢弃层
    fc2 = tf.nn.dropout(fc2, dropout)

    # output输出层
    out = tf.add(tf.matmul(fc2, weights['out_w']), biases['out_b'])
    return out


# 定义模型,损失函数和优化器
# 前向传播的预测值
pred = alexnet(x, weights, biases, dropout)
# # 交叉熵损失函数,参数分别为预测值pred和实际label值y,reduce_mean为求平均loss
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
# 梯度下降优化器
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
# tf.equal()对比预测值的索引和实际label的索引是否一样,一样返回True,不一样返回False
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
# 将pred即True或False转换为1或0,并对所有的判断结果求均值
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

# 定义一个saver,用于存储训练好的模型
saver = tf.train.Saver()
# 定义存储路径
model = "./model"
if not os.path.exists(model):
    os.makedirs(model)

# 训练和评估模型,获得训练集的训练结果与测试集的测试准确率
# 初始化变量
init = tf.global_variables_initializer()
# 定义一个Session
with tf.Session() as sess:
    # 在sess里run一下初始化操作
    sess.run(init)
    step = 1

    # epoch 逐代循环
    for epoch in range(epochs + 1):
        # 迭代
        for _ in range(mnist.train.num_examples // batch_size):
            step += 1
            # get x,y  逐个batch的去取数据
            batch_x, batch_y = mnist.train.next_batch(batch_size)
            # 获取批数据
            sess.run(optimizer, feed_dict={x: batch_x, y: batch_y})
            # 展示损失和精确度  每display_step显示一次
            if step % display_step == 0:
                # 计算损失值和精确度,并输出
                loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x, y: batch_y})
                print("Epoch " + str(epoch) + ", Minibatch Loss=" + \
                      "{:.6f}".format(loss) + ", Training Accuracy= " + \
                      "{:.5f}".format(acc))

    print("Optimizer Finished!")
    # 存储训练好的模型
    saver.save(sess, model + "/model.ckpt", global_step=epoch)

    # 测试准确率
    for _ in range(mnist.test.num_examples // batch_size):
        batch_x, batch_y = mnist.test.next_batch(batch_size)
        print("Testing Accuracy:", sess.run(accuracy, feed_dict={x: batch_x, y: batch_y}))

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值